Merge 4.29.0 into master
This commit is contained in:
commit
772e8afaf5
|
@ -1,7 +1,3 @@
|
|||
export default {
|
||||
getModel: () => '',
|
||||
getReadableVersion: () => '',
|
||||
getBundleId: () => '',
|
||||
isTablet: () => false,
|
||||
hasNotch: () => false
|
||||
};
|
||||
import mockDeviceInfo from 'react-native-device-info/jest/react-native-device-info-mock';
|
||||
|
||||
export default mockDeviceInfo;
|
||||
|
|
|
@ -1,78 +1,102 @@
|
|||
GEM
|
||||
remote: https://rubygems.org/
|
||||
specs:
|
||||
CFPropertyList (3.0.2)
|
||||
addressable (2.7.0)
|
||||
CFPropertyList (3.0.5)
|
||||
rexml
|
||||
addressable (2.8.0)
|
||||
public_suffix (>= 2.0.2, < 5.0)
|
||||
artifactory (3.0.15)
|
||||
atomos (0.1.3)
|
||||
aws-eventstream (1.0.3)
|
||||
aws-partitions (1.294.0)
|
||||
aws-sdk-core (3.92.0)
|
||||
aws-eventstream (~> 1.0, >= 1.0.2)
|
||||
aws-partitions (~> 1, >= 1.239.0)
|
||||
aws-eventstream (1.2.0)
|
||||
aws-partitions (1.600.0)
|
||||
aws-sdk-core (3.131.2)
|
||||
aws-eventstream (~> 1, >= 1.0.2)
|
||||
aws-partitions (~> 1, >= 1.525.0)
|
||||
aws-sigv4 (~> 1.1)
|
||||
jmespath (~> 1.0)
|
||||
aws-sdk-kms (1.30.0)
|
||||
aws-sdk-core (~> 3, >= 3.71.0)
|
||||
jmespath (~> 1, >= 1.6.1)
|
||||
aws-sdk-kms (1.57.0)
|
||||
aws-sdk-core (~> 3, >= 3.127.0)
|
||||
aws-sigv4 (~> 1.1)
|
||||
aws-sdk-s3 (1.61.2)
|
||||
aws-sdk-core (~> 3, >= 3.83.0)
|
||||
aws-sdk-s3 (1.114.0)
|
||||
aws-sdk-core (~> 3, >= 3.127.0)
|
||||
aws-sdk-kms (~> 1)
|
||||
aws-sigv4 (~> 1.1)
|
||||
aws-sigv4 (1.1.1)
|
||||
aws-eventstream (~> 1.0, >= 1.0.2)
|
||||
babosa (1.0.3)
|
||||
claide (1.0.3)
|
||||
aws-sigv4 (~> 1.4)
|
||||
aws-sigv4 (1.5.0)
|
||||
aws-eventstream (~> 1, >= 1.0.2)
|
||||
babosa (1.0.4)
|
||||
claide (1.1.0)
|
||||
colored (1.2)
|
||||
colored2 (3.1.2)
|
||||
commander-fastlane (4.4.6)
|
||||
highline (~> 1.7.2)
|
||||
declarative (0.0.10)
|
||||
declarative-option (0.1.0)
|
||||
digest-crc (0.5.1)
|
||||
commander (4.6.0)
|
||||
highline (~> 2.0.0)
|
||||
declarative (0.0.20)
|
||||
digest-crc (0.6.4)
|
||||
rake (>= 12.0.0, < 14.0.0)
|
||||
domain_name (0.5.20190701)
|
||||
unf (>= 0.0.5, < 1.0.0)
|
||||
dotenv (2.7.5)
|
||||
emoji_regex (1.0.1)
|
||||
excon (0.73.0)
|
||||
faraday (0.17.3)
|
||||
multipart-post (>= 1.2, < 3)
|
||||
faraday-cookie_jar (0.0.6)
|
||||
faraday (>= 0.7.4)
|
||||
dotenv (2.7.6)
|
||||
emoji_regex (3.2.3)
|
||||
excon (0.92.3)
|
||||
faraday (1.10.0)
|
||||
faraday-em_http (~> 1.0)
|
||||
faraday-em_synchrony (~> 1.0)
|
||||
faraday-excon (~> 1.1)
|
||||
faraday-httpclient (~> 1.0)
|
||||
faraday-multipart (~> 1.0)
|
||||
faraday-net_http (~> 1.0)
|
||||
faraday-net_http_persistent (~> 1.0)
|
||||
faraday-patron (~> 1.0)
|
||||
faraday-rack (~> 1.0)
|
||||
faraday-retry (~> 1.0)
|
||||
ruby2_keywords (>= 0.0.4)
|
||||
faraday-cookie_jar (0.0.7)
|
||||
faraday (>= 0.8.0)
|
||||
http-cookie (~> 1.0.0)
|
||||
faraday_middleware (0.13.1)
|
||||
faraday (>= 0.7.4, < 1.0)
|
||||
fastimage (2.1.7)
|
||||
fastlane (2.145.0)
|
||||
faraday-em_http (1.0.0)
|
||||
faraday-em_synchrony (1.0.0)
|
||||
faraday-excon (1.1.0)
|
||||
faraday-httpclient (1.0.1)
|
||||
faraday-multipart (1.0.4)
|
||||
multipart-post (~> 2)
|
||||
faraday-net_http (1.0.1)
|
||||
faraday-net_http_persistent (1.2.0)
|
||||
faraday-patron (1.0.0)
|
||||
faraday-rack (1.0.0)
|
||||
faraday-retry (1.0.3)
|
||||
faraday_middleware (1.2.0)
|
||||
faraday (~> 1.0)
|
||||
fastimage (2.2.6)
|
||||
fastlane (2.206.2)
|
||||
CFPropertyList (>= 2.3, < 4.0.0)
|
||||
addressable (>= 2.3, < 3.0.0)
|
||||
addressable (>= 2.8, < 3.0.0)
|
||||
artifactory (~> 3.0)
|
||||
aws-sdk-s3 (~> 1.0)
|
||||
babosa (>= 1.0.2, < 2.0.0)
|
||||
babosa (>= 1.0.3, < 2.0.0)
|
||||
bundler (>= 1.12.0, < 3.0.0)
|
||||
colored
|
||||
commander-fastlane (>= 4.4.6, < 5.0.0)
|
||||
commander (~> 4.6)
|
||||
dotenv (>= 2.1.1, < 3.0.0)
|
||||
emoji_regex (>= 0.1, < 2.0)
|
||||
emoji_regex (>= 0.1, < 4.0)
|
||||
excon (>= 0.71.0, < 1.0.0)
|
||||
faraday (~> 0.17)
|
||||
faraday (~> 1.0)
|
||||
faraday-cookie_jar (~> 0.0.6)
|
||||
faraday_middleware (~> 0.13.1)
|
||||
faraday_middleware (~> 1.0)
|
||||
fastimage (>= 2.1.0, < 3.0.0)
|
||||
gh_inspector (>= 1.1.2, < 2.0.0)
|
||||
google-api-client (>= 0.29.2, < 0.37.0)
|
||||
google-cloud-storage (>= 1.15.0, < 2.0.0)
|
||||
highline (>= 1.7.2, < 2.0.0)
|
||||
google-apis-androidpublisher_v3 (~> 0.3)
|
||||
google-apis-playcustomapp_v1 (~> 0.1)
|
||||
google-cloud-storage (~> 1.31)
|
||||
highline (~> 2.0)
|
||||
json (< 3.0.0)
|
||||
jwt (~> 2.1.0)
|
||||
jwt (>= 2.1.0, < 3)
|
||||
mini_magick (>= 4.9.4, < 5.0.0)
|
||||
multi_xml (~> 0.5)
|
||||
multipart-post (~> 2.0.0)
|
||||
naturally (~> 2.2)
|
||||
optparse (~> 0.1.1)
|
||||
plist (>= 3.1.0, < 4.0.0)
|
||||
public_suffix (~> 2.0.0)
|
||||
rubyzip (>= 1.3.0, < 2.0.0)
|
||||
rubyzip (>= 2.0.0, < 3.0.0)
|
||||
security (= 0.1.3)
|
||||
simctl (~> 1.6.3)
|
||||
slack-notifier (>= 2.0.0, < 3.0.0)
|
||||
terminal-notifier (>= 2.0.0, < 3.0.0)
|
||||
terminal-table (>= 1.4.5, < 2.0.0)
|
||||
tty-screen (>= 0.6.3, < 1.0.0)
|
||||
|
@ -82,92 +106,106 @@ GEM
|
|||
xcpretty (~> 0.3.0)
|
||||
xcpretty-travis-formatter (>= 0.0.3)
|
||||
gh_inspector (1.1.3)
|
||||
google-api-client (0.36.4)
|
||||
google-apis-androidpublisher_v3 (0.22.0)
|
||||
google-apis-core (>= 0.5, < 2.a)
|
||||
google-apis-core (0.6.0)
|
||||
addressable (~> 2.5, >= 2.5.1)
|
||||
googleauth (~> 0.9)
|
||||
httpclient (>= 2.8.1, < 3.0)
|
||||
googleauth (>= 0.16.2, < 2.a)
|
||||
httpclient (>= 2.8.1, < 3.a)
|
||||
mini_mime (~> 1.0)
|
||||
representable (~> 3.0)
|
||||
retriable (>= 2.0, < 4.0)
|
||||
signet (~> 0.12)
|
||||
google-cloud-core (1.5.0)
|
||||
retriable (>= 2.0, < 4.a)
|
||||
rexml
|
||||
webrick
|
||||
google-apis-iamcredentials_v1 (0.12.0)
|
||||
google-apis-core (>= 0.6, < 2.a)
|
||||
google-apis-playcustomapp_v1 (0.9.0)
|
||||
google-apis-core (>= 0.6, < 2.a)
|
||||
google-apis-storage_v1 (0.15.0)
|
||||
google-apis-core (>= 0.5, < 2.a)
|
||||
google-cloud-core (1.6.0)
|
||||
google-cloud-env (~> 1.0)
|
||||
google-cloud-errors (~> 1.0)
|
||||
google-cloud-env (1.3.1)
|
||||
faraday (>= 0.17.3, < 2.0)
|
||||
google-cloud-errors (1.0.0)
|
||||
google-cloud-storage (1.25.1)
|
||||
addressable (~> 2.5)
|
||||
google-cloud-env (1.6.0)
|
||||
faraday (>= 0.17.3, < 3.0)
|
||||
google-cloud-errors (1.2.0)
|
||||
google-cloud-storage (1.36.2)
|
||||
addressable (~> 2.8)
|
||||
digest-crc (~> 0.4)
|
||||
google-api-client (~> 0.33)
|
||||
google-cloud-core (~> 1.2)
|
||||
googleauth (~> 0.9)
|
||||
google-apis-iamcredentials_v1 (~> 0.1)
|
||||
google-apis-storage_v1 (~> 0.1)
|
||||
google-cloud-core (~> 1.6)
|
||||
googleauth (>= 0.16.2, < 2.a)
|
||||
mini_mime (~> 1.0)
|
||||
googleauth (0.11.0)
|
||||
faraday (>= 0.17.3, < 2.0)
|
||||
googleauth (1.2.0)
|
||||
faraday (>= 0.17.3, < 3.a)
|
||||
jwt (>= 1.4, < 3.0)
|
||||
memoist (~> 0.16)
|
||||
multi_json (~> 1.11)
|
||||
os (>= 0.9, < 2.0)
|
||||
signet (~> 0.12)
|
||||
highline (1.7.10)
|
||||
http-cookie (1.0.3)
|
||||
signet (>= 0.16, < 2.a)
|
||||
highline (2.0.3)
|
||||
http-cookie (1.0.5)
|
||||
domain_name (~> 0.5)
|
||||
httpclient (2.8.3)
|
||||
jmespath (1.4.0)
|
||||
json (2.3.0)
|
||||
jwt (2.1.0)
|
||||
jmespath (1.6.1)
|
||||
json (2.6.2)
|
||||
jwt (2.4.1)
|
||||
memoist (0.16.2)
|
||||
mini_magick (4.10.1)
|
||||
mini_mime (1.0.2)
|
||||
multi_json (1.14.1)
|
||||
multi_xml (0.6.0)
|
||||
mini_magick (4.11.0)
|
||||
mini_mime (1.1.2)
|
||||
multi_json (1.15.0)
|
||||
multipart-post (2.0.0)
|
||||
nanaimo (0.2.6)
|
||||
naturally (2.2.0)
|
||||
os (1.1.0)
|
||||
plist (3.5.0)
|
||||
public_suffix (2.0.5)
|
||||
representable (3.0.4)
|
||||
nanaimo (0.3.0)
|
||||
naturally (2.2.1)
|
||||
optparse (0.1.1)
|
||||
os (1.1.4)
|
||||
plist (3.6.0)
|
||||
public_suffix (4.0.7)
|
||||
rake (13.0.6)
|
||||
representable (3.2.0)
|
||||
declarative (< 0.1.0)
|
||||
declarative-option (< 0.2.0)
|
||||
trailblazer-option (>= 0.1.1, < 0.2.0)
|
||||
uber (< 0.2.0)
|
||||
retriable (3.1.2)
|
||||
rexml (3.2.5)
|
||||
rouge (2.0.7)
|
||||
rubyzip (1.3.0)
|
||||
ruby2_keywords (0.0.5)
|
||||
rubyzip (2.3.2)
|
||||
security (0.1.3)
|
||||
signet (0.14.0)
|
||||
addressable (~> 2.3)
|
||||
faraday (>= 0.17.3, < 2.0)
|
||||
signet (0.17.0)
|
||||
addressable (~> 2.8)
|
||||
faraday (>= 0.17.5, < 3.a)
|
||||
jwt (>= 1.5, < 3.0)
|
||||
multi_json (~> 1.10)
|
||||
simctl (1.6.8)
|
||||
CFPropertyList
|
||||
naturally
|
||||
slack-notifier (2.3.2)
|
||||
terminal-notifier (2.0.0)
|
||||
terminal-table (1.8.0)
|
||||
unicode-display_width (~> 1.1, >= 1.1.1)
|
||||
trailblazer-option (0.1.2)
|
||||
tty-cursor (0.7.1)
|
||||
tty-screen (0.7.1)
|
||||
tty-screen (0.8.1)
|
||||
tty-spinner (0.9.3)
|
||||
tty-cursor (~> 0.7)
|
||||
uber (0.1.0)
|
||||
unf (0.1.4)
|
||||
unf_ext
|
||||
unf_ext (0.0.7.7)
|
||||
unf_ext (0.0.7.7-x64-mingw32)
|
||||
unicode-display_width (1.7.0)
|
||||
unf_ext (0.0.8.2)
|
||||
unicode-display_width (1.8.0)
|
||||
webrick (1.7.0)
|
||||
word_wrap (1.0.0)
|
||||
xcodeproj (1.15.0)
|
||||
xcodeproj (1.22.0)
|
||||
CFPropertyList (>= 2.3.3, < 4.0)
|
||||
atomos (~> 0.1.3)
|
||||
claide (>= 1.0.2, < 2.0)
|
||||
colored2 (~> 3.1)
|
||||
nanaimo (~> 0.2.6)
|
||||
nanaimo (~> 0.3.0)
|
||||
rexml (~> 3.2.4)
|
||||
xcpretty (0.3.0)
|
||||
rouge (~> 2.0.7)
|
||||
xcpretty-travis-formatter (1.0.0)
|
||||
xcpretty-travis-formatter (1.0.1)
|
||||
xcpretty (~> 0.2, >= 0.0.7)
|
||||
|
||||
PLATFORMS
|
||||
|
@ -178,4 +216,4 @@ DEPENDENCIES
|
|||
fastlane
|
||||
|
||||
BUNDLED WITH
|
||||
2.0.2
|
||||
2.3.11
|
||||
|
|
|
@ -144,7 +144,7 @@ android {
|
|||
minSdkVersion rootProject.ext.minSdkVersion
|
||||
targetSdkVersion rootProject.ext.targetSdkVersion
|
||||
versionCode VERSIONCODE as Integer
|
||||
versionName "4.28.0"
|
||||
versionName "4.29.0"
|
||||
vectorDrawables.useSupportLibrary = true
|
||||
if (!isFoss) {
|
||||
manifestPlaceholders = [BugsnagAPIKey: BugsnagAPIKey as String]
|
||||
|
|
Binary file not shown.
|
@ -12,7 +12,6 @@ import com.facebook.react.ReactRootView;
|
|||
import com.facebook.react.ReactActivityDelegate;
|
||||
import com.facebook.react.ReactFragmentActivity;
|
||||
|
||||
import com.swmansion.gesturehandler.react.RNGestureHandlerEnabledRootView;
|
||||
import com.zoontek.rnbootsplash.RNBootSplash;
|
||||
import com.google.gson.Gson;
|
||||
|
||||
|
@ -51,16 +50,6 @@ public class MainActivity extends ReactFragmentActivity {
|
|||
return "RocketChatRN";
|
||||
}
|
||||
|
||||
@Override
|
||||
protected ReactActivityDelegate createReactActivityDelegate() {
|
||||
return new ReactActivityDelegate(this, getMainComponentName()) {
|
||||
@Override
|
||||
protected ReactRootView createRootView() {
|
||||
return new RNGestureHandlerEnabledRootView(MainActivity.this);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
// from react-native-orientation
|
||||
@Override
|
||||
public void onConfigurationChanged(Configuration newConfig) {
|
||||
|
|
|
@ -3,21 +3,10 @@ package chat.rocket.reactnative.share;
|
|||
import com.facebook.react.ReactActivity;
|
||||
import com.facebook.react.ReactActivityDelegate;
|
||||
import com.facebook.react.ReactRootView;
|
||||
import com.swmansion.gesturehandler.react.RNGestureHandlerEnabledRootView;
|
||||
|
||||
public class ShareActivity extends ReactActivity {
|
||||
@Override
|
||||
protected String getMainComponentName() {
|
||||
return "ShareRocketChatRN";
|
||||
}
|
||||
|
||||
@Override
|
||||
protected ReactActivityDelegate createReactActivityDelegate() {
|
||||
return new ReactActivityDelegate(this, getMainComponentName()) {
|
||||
@Override
|
||||
protected ReactRootView createRootView() {
|
||||
return new RNGestureHandlerEnabledRootView(ShareActivity.this);
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
|
@ -14,7 +14,7 @@ buildscript {
|
|||
targetSdkVersion = 30
|
||||
ndkVersion = "20.1.5948944"
|
||||
glideVersion = "4.11.0"
|
||||
kotlin_version = "1.3.50"
|
||||
kotlin_version = "1.6.10"
|
||||
supportLibVersion = "28.0.0"
|
||||
libre_build = !(isPlay.toBoolean())
|
||||
jitsi_url = isPlay ? "https://github.com/RocketChat/jitsi-maven-repository/raw/master/releases" : "https://github.com/RocketChat/jitsi-maven-repository/raw/libre/releases"
|
||||
|
|
|
@ -5,7 +5,7 @@ import { connect } from 'react-redux';
|
|||
|
||||
import { SetUsernameStackParamList, StackParamList } from './definitions/navigationTypes';
|
||||
import Navigation from './lib/navigation/appNavigation';
|
||||
import { defaultHeader, getActiveRouteName, navigationTheme } from './utils/navigation';
|
||||
import { defaultHeader, getActiveRouteName, navigationTheme } from './lib/methods/helpers/navigation';
|
||||
import { RootEnum } from './definitions';
|
||||
// Stacks
|
||||
import AuthLoadingView from './views/AuthLoadingView';
|
||||
|
@ -15,7 +15,7 @@ import OutsideStack from './stacks/OutsideStack';
|
|||
import InsideStack from './stacks/InsideStack';
|
||||
import MasterDetailStack from './stacks/MasterDetailStack';
|
||||
import { ThemeContext } from './theme';
|
||||
import { setCurrentScreen } from './utils/log';
|
||||
import { setCurrentScreen } from './lib/methods/helpers/log';
|
||||
|
||||
// SetUsernameStack
|
||||
const SetUsername = createStackNavigator<SetUsernameStackParamList>();
|
||||
|
|
|
@ -27,7 +27,6 @@ export const ROOM = createRequestTypes('ROOM', [
|
|||
'LEAVE',
|
||||
'DELETE',
|
||||
'REMOVED',
|
||||
'CLOSE',
|
||||
'FORWARD',
|
||||
'USER_TYPING'
|
||||
]);
|
||||
|
@ -54,6 +53,7 @@ export const SERVER = createRequestTypes('SERVER', [
|
|||
]);
|
||||
export const METEOR = createRequestTypes('METEOR_CONNECT', [...defaultTypes, 'DISCONNECT']);
|
||||
export const LOGOUT = 'LOGOUT'; // logout is always success
|
||||
export const DELETE_ACCOUNT = 'DELETE_ACCOUNT';
|
||||
export const SNIPPETED_MESSAGES = createRequestTypes('SNIPPETED_MESSAGES', ['OPEN', 'READY', 'CLOSE', 'MESSAGES_RECEIVED']);
|
||||
export const DEEP_LINKING = createRequestTypes('DEEP_LINKING', ['OPEN']);
|
||||
export const SORT_PREFERENCES = createRequestTypes('SORT_PREFERENCES', ['SET_ALL', 'SET']);
|
||||
|
|
|
@ -121,3 +121,9 @@ export function setLocalAuthenticated(isLocalAuthenticated: boolean): ISetLocalA
|
|||
isLocalAuthenticated
|
||||
};
|
||||
}
|
||||
|
||||
export function deleteAccount(): Action {
|
||||
return {
|
||||
type: types.DELETE_ACCOUNT
|
||||
};
|
||||
}
|
||||
|
|
|
@ -19,7 +19,6 @@ interface IBaseReturn extends Action {
|
|||
|
||||
type TSubscribeRoom = IBaseReturn;
|
||||
type TUnsubscribeRoom = IBaseReturn;
|
||||
type TCloseRoom = IBaseReturn;
|
||||
|
||||
type TRoom = Record<string, any>;
|
||||
|
||||
|
@ -45,7 +44,7 @@ interface IUserTyping extends Action {
|
|||
status: boolean;
|
||||
}
|
||||
|
||||
export type TActionsRoom = TSubscribeRoom & TUnsubscribeRoom & TCloseRoom & ILeaveRoom & IDeleteRoom & IForwardRoom & IUserTyping;
|
||||
export type TActionsRoom = TSubscribeRoom & TUnsubscribeRoom & ILeaveRoom & IDeleteRoom & IForwardRoom & IUserTyping;
|
||||
|
||||
export function subscribeRoom(rid: string): TSubscribeRoom {
|
||||
return {
|
||||
|
@ -79,13 +78,6 @@ export function deleteRoom(roomType: ERoomType, room: TRoom, selected?: ISelecte
|
|||
};
|
||||
}
|
||||
|
||||
export function closeRoom(rid: string): TCloseRoom {
|
||||
return {
|
||||
type: ROOM.CLOSE,
|
||||
rid
|
||||
};
|
||||
}
|
||||
|
||||
export function forwardRoom(rid: string, transferData: ITransferData): IForwardRoom {
|
||||
return {
|
||||
type: ROOM.FORWARD,
|
||||
|
|
|
@ -8,7 +8,7 @@ import BottomSheet, { BottomSheetBackdrop } from '@gorhom/bottom-sheet';
|
|||
|
||||
import { useDimensions, useOrientation } from '../../dimensions';
|
||||
import { useTheme } from '../../theme';
|
||||
import { isIOS, isTablet } from '../../utils/deviceInfo';
|
||||
import { isIOS, isTablet } from '../../lib/methods/helpers';
|
||||
import { Handle } from './Handle';
|
||||
import { TActionSheetOptions } from './Provider';
|
||||
import BottomSheetContent from './BottomSheetContent';
|
||||
|
@ -101,6 +101,11 @@ const ActionSheet = React.memo(
|
|||
</>
|
||||
);
|
||||
|
||||
const onClose = () => {
|
||||
toggleVisible();
|
||||
data?.onClose && data?.onClose();
|
||||
};
|
||||
|
||||
const renderBackdrop = useCallback(
|
||||
props => (
|
||||
<BottomSheetBackdrop
|
||||
|
@ -116,6 +121,10 @@ const ActionSheet = React.memo(
|
|||
|
||||
const bottomSheet = isLandscape || isTablet ? styles.bottomSheet : {};
|
||||
|
||||
// Must need this prop to avoid keyboard dismiss
|
||||
// when is android tablet and the input text is focused
|
||||
const androidTablet: any = isTablet && isLandscape && !isIOS ? { android_keyboardInputMode: 'adjustResize' } : {};
|
||||
|
||||
return (
|
||||
<>
|
||||
{children}
|
||||
|
@ -130,7 +139,8 @@ const ActionSheet = React.memo(
|
|||
enablePanDownToClose
|
||||
style={{ ...styles.container, ...bottomSheet }}
|
||||
backgroundStyle={{ backgroundColor: colors.focusedBackground }}
|
||||
onChange={index => index === -1 && toggleVisible()}>
|
||||
onChange={index => index === -1 && onClose()}
|
||||
{...androidTablet}>
|
||||
<BottomSheetContent options={data?.options} hide={hide} children={data?.children} hasCancel={data?.hasCancel} />
|
||||
</BottomSheet>
|
||||
)}
|
||||
|
|
|
@ -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;
|
|
@ -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;
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
import React from 'react';
|
||||
import { TouchableOpacity } from 'react-native';
|
||||
|
||||
import { isAndroid } from '../../utils/deviceInfo';
|
||||
import Touch from '../../utils/touch';
|
||||
import { isAndroid } from '../../lib/methods/helpers';
|
||||
import Touch from '../../lib/methods/helpers/touch';
|
||||
|
||||
// Taken from https://github.com/rgommezz/react-native-scroll-bottom-sheet#touchables
|
||||
export const Button: typeof React.Component = isAndroid ? Touch : TouchableOpacity;
|
||||
|
|
|
@ -8,7 +8,7 @@ import { useTheme } from '../../theme';
|
|||
export const Handle = React.memo(() => {
|
||||
const { theme } = useTheme();
|
||||
return (
|
||||
<View style={[styles.handle, { backgroundColor: themes[theme].focusedBackground }]} testID='action-sheet-handle'>
|
||||
<View style={[styles.handle]} testID='action-sheet-handle'>
|
||||
<View style={[styles.handleIndicator, { backgroundColor: themes[theme].auxiliaryText }]} />
|
||||
</View>
|
||||
);
|
||||
|
|
|
@ -1,7 +1,8 @@
|
|||
import hoistNonReactStatics from 'hoist-non-react-statics';
|
||||
import React, { ForwardedRef, forwardRef, useContext, useRef } from 'react';
|
||||
|
||||
import ActionSheet from './ActionSheet';
|
||||
import { TIconsName } from '../CustomIcon';
|
||||
import ActionSheet from './ActionSheet';
|
||||
|
||||
export type TActionSheetOptionsItem = {
|
||||
title: string;
|
||||
|
@ -19,9 +20,10 @@ export type TActionSheetOptions = {
|
|||
hasCancel?: boolean;
|
||||
type?: string;
|
||||
children?: React.ReactElement | null;
|
||||
snaps?: string[] | number[];
|
||||
snaps?: (string | number)[];
|
||||
onClose?: () => void;
|
||||
};
|
||||
interface IActionSheetProvider {
|
||||
export interface IActionSheetProvider {
|
||||
showActionSheet: (item: TActionSheetOptions) => void;
|
||||
hideActionSheet: () => void;
|
||||
}
|
||||
|
@ -35,11 +37,15 @@ export const useActionSheet = () => useContext(context);
|
|||
|
||||
const { Provider, Consumer } = context;
|
||||
|
||||
export const withActionSheet = (Component: React.ComponentType<any>): typeof Component =>
|
||||
forwardRef((props: typeof React.Component, ref: ForwardedRef<IActionSheetProvider>) => (
|
||||
export const withActionSheet = (Component: React.ComponentType<any>): typeof Component => {
|
||||
const WithActionSheetComponent = forwardRef((props: typeof React.Component, ref: ForwardedRef<IActionSheetProvider>) => (
|
||||
<Consumer>{(contexts: IActionSheetProvider) => <Component {...props} {...contexts} ref={ref} />}</Consumer>
|
||||
));
|
||||
|
||||
hoistNonReactStatics(WithActionSheetComponent, Component);
|
||||
return WithActionSheetComponent;
|
||||
};
|
||||
|
||||
export const ActionSheetProvider = React.memo(({ children }: { children: React.ReactElement | React.ReactElement[] }) => {
|
||||
const ref: ForwardedRef<IActionSheetProvider> = useRef(null);
|
||||
|
||||
|
|
|
@ -63,5 +63,15 @@ export default StyleSheet.create({
|
|||
},
|
||||
rightContainer: {
|
||||
paddingLeft: 12
|
||||
},
|
||||
footerButtonsContainer: {
|
||||
flexDirection: 'row',
|
||||
paddingTop: 16
|
||||
},
|
||||
buttonSeparator: {
|
||||
marginRight: 8
|
||||
},
|
||||
contentContainer: {
|
||||
flex: 1
|
||||
}
|
||||
});
|
||||
|
|
|
@ -3,7 +3,7 @@ import { StyleSheet, Text, View } from 'react-native';
|
|||
|
||||
import { themes } from '../lib/constants';
|
||||
import sharedStyles from '../views/Styles';
|
||||
import { getReadableVersion } from '../utils/deviceInfo';
|
||||
import { getReadableVersion } from '../lib/methods/helpers';
|
||||
import I18n from '../i18n';
|
||||
import { TSupportedThemes } from '../theme';
|
||||
|
||||
|
|
|
@ -1,14 +1,13 @@
|
|||
import React from 'react';
|
||||
import { View } from 'react-native';
|
||||
import FastImage from '@rocket.chat/react-native-fast-image';
|
||||
import FastImage from 'react-native-fast-image';
|
||||
import Touchable from 'react-native-platform-touchable';
|
||||
import { settings as RocketChatSettings } from '@rocket.chat/sdk';
|
||||
|
||||
import { avatarURL } from '../../utils/avatar';
|
||||
import { SubscriptionType } from '../../definitions/ISubscription';
|
||||
import { getAvatarURL } from '../../lib/methods/helpers/getAvatarUrl';
|
||||
import { SubscriptionType } from '../../definitions';
|
||||
import Emoji from '../markdown/Emoji';
|
||||
import { IAvatar } from './interfaces';
|
||||
import { useTheme } from '../../theme';
|
||||
|
||||
const Avatar = React.memo(
|
||||
({
|
||||
|
@ -16,7 +15,8 @@ const Avatar = React.memo(
|
|||
style,
|
||||
avatar,
|
||||
children,
|
||||
user,
|
||||
userId,
|
||||
token,
|
||||
onPress,
|
||||
emoji,
|
||||
getCustomEmoji,
|
||||
|
@ -31,8 +31,6 @@ const Avatar = React.memo(
|
|||
type = SubscriptionType.DIRECT,
|
||||
externalProviderUrl
|
||||
}: IAvatar) => {
|
||||
const { theme } = useTheme();
|
||||
|
||||
if ((!text && !avatar && !emoji && !rid) || !server) {
|
||||
return null;
|
||||
}
|
||||
|
@ -46,23 +44,17 @@ const Avatar = React.memo(
|
|||
let image;
|
||||
if (emoji) {
|
||||
image = (
|
||||
<Emoji
|
||||
theme={theme}
|
||||
baseUrl={server}
|
||||
getCustomEmoji={getCustomEmoji}
|
||||
isMessageContainsOnlyEmoji
|
||||
literal={emoji}
|
||||
style={avatarStyle}
|
||||
/>
|
||||
<Emoji baseUrl={server} getCustomEmoji={getCustomEmoji} isMessageContainsOnlyEmoji literal={emoji} style={avatarStyle} />
|
||||
);
|
||||
} else {
|
||||
let uri = avatar;
|
||||
if (!isStatic) {
|
||||
uri = avatarURL({
|
||||
uri = getAvatarURL({
|
||||
type,
|
||||
text,
|
||||
size,
|
||||
user,
|
||||
userId,
|
||||
token,
|
||||
avatar,
|
||||
server,
|
||||
avatarETag,
|
||||
|
|
|
@ -1,84 +1,65 @@
|
|||
import React from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import { Q } from '@nozbe/watermelondb';
|
||||
import React, { useEffect, useRef, useState } from 'react';
|
||||
import { shallowEqual, useSelector } from 'react-redux';
|
||||
import { Observable, Subscription } from 'rxjs';
|
||||
|
||||
import { IApplicationState, TSubscriptionModel, TUserModel } from '../../definitions';
|
||||
import database from '../../lib/database';
|
||||
import { getUserSelector } from '../../selectors/login';
|
||||
import { IApplicationState, TSubscriptionModel, TUserModel } from '../../definitions';
|
||||
import Avatar from './Avatar';
|
||||
import { IAvatar } from './interfaces';
|
||||
|
||||
class AvatarContainer extends React.Component<IAvatar, any> {
|
||||
private subscription?: Subscription;
|
||||
const AvatarContainer = ({
|
||||
style,
|
||||
text = '',
|
||||
avatar,
|
||||
emoji,
|
||||
size,
|
||||
borderRadius,
|
||||
type,
|
||||
children,
|
||||
onPress,
|
||||
getCustomEmoji,
|
||||
isStatic,
|
||||
rid
|
||||
}: IAvatar): React.ReactElement => {
|
||||
const subscription = useRef<Subscription>();
|
||||
const [avatarETag, setAvatarETag] = useState<string | undefined>('');
|
||||
|
||||
static defaultProps = {
|
||||
text: '',
|
||||
type: 'd'
|
||||
};
|
||||
const isDirect = () => type === 'd';
|
||||
|
||||
constructor(props: IAvatar) {
|
||||
super(props);
|
||||
this.state = { avatarETag: '' };
|
||||
this.init();
|
||||
}
|
||||
const server = useSelector((state: IApplicationState) => state.share.server.server || state.server.server);
|
||||
const serverVersion = useSelector((state: IApplicationState) => state.share.server.version || state.server.version);
|
||||
const { id, token } = useSelector(
|
||||
(state: IApplicationState) => ({
|
||||
id: getUserSelector(state).id,
|
||||
token: getUserSelector(state).token
|
||||
}),
|
||||
shallowEqual
|
||||
);
|
||||
|
||||
componentDidUpdate(prevProps: IAvatar) {
|
||||
const { text, type } = this.props;
|
||||
if (prevProps.text !== text || prevProps.type !== type) {
|
||||
this.init();
|
||||
}
|
||||
}
|
||||
const externalProviderUrl = useSelector(
|
||||
(state: IApplicationState) => state.settings.Accounts_AvatarExternalProviderUrl as string
|
||||
);
|
||||
const blockUnauthenticatedAccess = useSelector(
|
||||
(state: IApplicationState) =>
|
||||
(state.share.settings?.Accounts_AvatarBlockUnauthenticatedAccess as boolean) ??
|
||||
state.settings.Accounts_AvatarBlockUnauthenticatedAccess ??
|
||||
true
|
||||
);
|
||||
|
||||
shouldComponentUpdate(nextProps: IAvatar, nextState: { avatarETag: string }) {
|
||||
const { avatarETag } = this.state;
|
||||
const { text, type, size, externalProviderUrl } = this.props;
|
||||
if (nextProps.externalProviderUrl !== externalProviderUrl) {
|
||||
return true;
|
||||
}
|
||||
if (nextState.avatarETag !== avatarETag) {
|
||||
return true;
|
||||
}
|
||||
if (nextProps.text !== text) {
|
||||
return true;
|
||||
}
|
||||
if (nextProps.type !== type) {
|
||||
return true;
|
||||
}
|
||||
if (nextProps.size !== size) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
if (this.subscription?.unsubscribe) {
|
||||
this.subscription.unsubscribe();
|
||||
}
|
||||
}
|
||||
|
||||
get isDirect() {
|
||||
const { type } = this.props;
|
||||
return type === 'd';
|
||||
}
|
||||
|
||||
init = async () => {
|
||||
const init = async () => {
|
||||
const db = database.active;
|
||||
const usersCollection = db.get('users');
|
||||
const subsCollection = db.get('subscriptions');
|
||||
|
||||
let record;
|
||||
try {
|
||||
if (this.isDirect) {
|
||||
const { text } = this.props;
|
||||
if (isDirect()) {
|
||||
const [user] = await usersCollection.query(Q.where('username', text)).fetch();
|
||||
record = user;
|
||||
} else {
|
||||
const { rid } = this.props;
|
||||
if (rid) {
|
||||
record = await subsCollection.find(rid);
|
||||
}
|
||||
} else if (rid) {
|
||||
record = await subsCollection.find(rid);
|
||||
}
|
||||
} catch {
|
||||
// Record not found
|
||||
|
@ -86,28 +67,46 @@ class AvatarContainer extends React.Component<IAvatar, any> {
|
|||
|
||||
if (record) {
|
||||
const observable = record.observe() as Observable<TSubscriptionModel | TUserModel>;
|
||||
this.subscription = observable.subscribe(r => {
|
||||
const { avatarETag } = r;
|
||||
this.setState({ avatarETag });
|
||||
subscription.current = observable.subscribe(r => {
|
||||
setAvatarETag(r.avatarETag);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
render() {
|
||||
const { avatarETag } = this.state;
|
||||
const { serverVersion } = this.props;
|
||||
return <Avatar {...this.props} avatarETag={avatarETag} serverVersion={serverVersion} />;
|
||||
}
|
||||
}
|
||||
useEffect(() => {
|
||||
if (!avatarETag) {
|
||||
init();
|
||||
}
|
||||
return () => {
|
||||
if (subscription?.current?.unsubscribe) {
|
||||
subscription.current.unsubscribe();
|
||||
}
|
||||
};
|
||||
}, [text, type, size, avatarETag, externalProviderUrl]);
|
||||
|
||||
const mapStateToProps = (state: IApplicationState) => ({
|
||||
user: getUserSelector(state),
|
||||
server: state.share.server.server || state.server.server,
|
||||
serverVersion: state.share.server.version || state.server.version,
|
||||
blockUnauthenticatedAccess:
|
||||
(state.share.settings?.Accounts_AvatarBlockUnauthenticatedAccess as boolean) ??
|
||||
state.settings.Accounts_AvatarBlockUnauthenticatedAccess ??
|
||||
true,
|
||||
externalProviderUrl: state.settings.Accounts_AvatarExternalProviderUrl as string
|
||||
});
|
||||
export default connect(mapStateToProps)(AvatarContainer);
|
||||
return (
|
||||
<Avatar
|
||||
server={server}
|
||||
style={style}
|
||||
text={text}
|
||||
avatar={avatar}
|
||||
emoji={emoji}
|
||||
size={size}
|
||||
borderRadius={borderRadius}
|
||||
type={type}
|
||||
children={children}
|
||||
userId={id}
|
||||
token={token}
|
||||
onPress={onPress}
|
||||
getCustomEmoji={getCustomEmoji}
|
||||
isStatic={isStatic}
|
||||
rid={rid}
|
||||
blockUnauthenticatedAccess={blockUnauthenticatedAccess}
|
||||
externalProviderUrl={externalProviderUrl}
|
||||
avatarETag={avatarETag}
|
||||
serverVersion={serverVersion}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default AvatarContainer;
|
||||
|
|
|
@ -5,23 +5,21 @@ import { TGetCustomEmoji } from '../../definitions/IEmoji';
|
|||
export interface IAvatar {
|
||||
server?: string;
|
||||
style?: any;
|
||||
text: string;
|
||||
text?: string;
|
||||
avatar?: string;
|
||||
emoji?: string;
|
||||
size?: number;
|
||||
borderRadius?: number;
|
||||
type?: string;
|
||||
children?: React.ReactElement | null;
|
||||
user?: {
|
||||
id?: string;
|
||||
token?: string;
|
||||
};
|
||||
userId?: string;
|
||||
token?: string;
|
||||
onPress?: () => void;
|
||||
getCustomEmoji?: TGetCustomEmoji;
|
||||
avatarETag?: string;
|
||||
isStatic?: boolean | string;
|
||||
rid?: string;
|
||||
blockUnauthenticatedAccess?: boolean;
|
||||
serverVersion: string | null;
|
||||
serverVersion?: string | null;
|
||||
externalProviderUrl?: string;
|
||||
}
|
||||
|
|
|
@ -10,7 +10,7 @@ export const IconSet = createIconSetFromIcoMoon(icoMoonConfig, 'custom', 'custom
|
|||
|
||||
export type TIconsName = keyof typeof mappedIcons;
|
||||
|
||||
interface ICustomIcon extends TextProps {
|
||||
export interface ICustomIcon extends TextProps {
|
||||
name: TIconsName;
|
||||
size: number;
|
||||
color: string;
|
||||
|
|
|
@ -1,6 +1,18 @@
|
|||
export const mappedIcons = {
|
||||
attach: 59676,
|
||||
link: 59752,
|
||||
'lamp-bulb': 59812,
|
||||
'basketball': 59776,
|
||||
'percentage': 59777,
|
||||
'burger': 59813,
|
||||
'leaf': 59814,
|
||||
'airplane': 59815,
|
||||
'rocket': 59816,
|
||||
'directory': 59648,
|
||||
'directory-disabled': 59649,
|
||||
'directory-error': 59650,
|
||||
'federation-disabled': 59651,
|
||||
'federation': 59652,
|
||||
'attach': 59676,
|
||||
'link': 59752,
|
||||
'status-away': 59741,
|
||||
'status-busy': 59742,
|
||||
'status-loading': 59743,
|
||||
|
@ -10,193 +22,191 @@ export const mappedIcons = {
|
|||
'channel-auto-join': 59746,
|
||||
'channel-move-to-team': 59747,
|
||||
'lock-filled': 59748,
|
||||
locker: 59749,
|
||||
teams: 59751,
|
||||
shield: 59661,
|
||||
ignore: 59740,
|
||||
'checkbox-unchecked': 59648,
|
||||
'checkbox-checked': 59649,
|
||||
'github-monochromatic': 59650,
|
||||
'gitlab-monochromatic': 59651,
|
||||
'google-monochromatic': 59652,
|
||||
'linkedin-monochromatic': 59653,
|
||||
'meteor-monochromatic': 59654,
|
||||
'twitter-monochromatic': 59655,
|
||||
administration: 59657,
|
||||
'adobe-reader-monochromatic': 59658,
|
||||
'all-contacts-in-channels': 59659,
|
||||
'all-contacts-in-queue': 59660,
|
||||
'apple-monochromatic': 59662,
|
||||
apps: 59663,
|
||||
'arrow-back': 59664,
|
||||
'arrow-collapse': 59665,
|
||||
'arrow-decrease': 59666,
|
||||
'arrow-down-box': 59667,
|
||||
'arrow-down-circle': 59668,
|
||||
'arrow-down': 59669,
|
||||
'arrow-expand': 59670,
|
||||
'arrow-increase': 59671,
|
||||
'arrow-looping': 59672,
|
||||
'arrow-return': 59673,
|
||||
'arrow-up-box': 59674,
|
||||
'arrow-up': 59675,
|
||||
'audio-disabled': 59677,
|
||||
'audio-unavailable': 59678,
|
||||
audio: 59679,
|
||||
auditing: 59680,
|
||||
auth: 59681,
|
||||
avatar: 59682,
|
||||
backspace: 59683,
|
||||
bold: 59684,
|
||||
book: 59685,
|
||||
business: 59686,
|
||||
calendar: 59687,
|
||||
'camera-disabled': 59688,
|
||||
'camera-filled': 59689,
|
||||
'camera-photo': 59690,
|
||||
'camera-unavailable': 59691,
|
||||
camera: 59692,
|
||||
'canned-response': 59693,
|
||||
card: 59694,
|
||||
'channel-private': 59695,
|
||||
'channel-public': 59696,
|
||||
'chat-close': 59697,
|
||||
'chat-forward': 59698,
|
||||
check: 59699,
|
||||
'chevron-down': 59700,
|
||||
'chevron-left-big': 59701,
|
||||
'chevron-left': 59702,
|
||||
'chevron-right': 59703,
|
||||
'chevron-up': 59704,
|
||||
'circle-check': 59705,
|
||||
clipboard: 59706,
|
||||
clock: 59707,
|
||||
close: 59708,
|
||||
'cloud-connectivity': 59709,
|
||||
code: 59710,
|
||||
contacts: 59711,
|
||||
copy: 59712,
|
||||
create: 59713,
|
||||
dashboard: 59714,
|
||||
delete: 59715,
|
||||
desktop: 59716,
|
||||
dialpad: 59717,
|
||||
'directory-disabled': 59718,
|
||||
directory: 59719,
|
||||
discussions: 59720,
|
||||
document: 59721,
|
||||
donner: 59722,
|
||||
download: 59723,
|
||||
edit: 59724,
|
||||
'emoji-bad-mood': 59725,
|
||||
'emoji-neutral-mood': 59726,
|
||||
emoji: 59727,
|
||||
encrypted: 59728,
|
||||
'engagement-dashboard': 59729,
|
||||
'enterprise-feature': 59730,
|
||||
'facebook-monochromatic': 59731,
|
||||
'file-document': 59732,
|
||||
'file-sheet': 59733,
|
||||
filter: 59734,
|
||||
fingerprint: 59735,
|
||||
flag: 59736,
|
||||
folder: 59737,
|
||||
game: 59738,
|
||||
'giphy-monochromatic': 59739,
|
||||
'locker': 59749,
|
||||
'teams': 59751,
|
||||
'shield': 59661,
|
||||
'ignore': 59740,
|
||||
'checkbox-unchecked': 59653,
|
||||
'checkbox-checked': 59654,
|
||||
'github-monochromatic': 59655,
|
||||
'gitlab-monochromatic': 59656,
|
||||
'google-monochromatic': 59657,
|
||||
'linkedin-monochromatic': 59658,
|
||||
'meteor-monochromatic': 59659,
|
||||
'twitter-monochromatic': 59660,
|
||||
'administration': 59662,
|
||||
'adobe-reader-monochromatic': 59663,
|
||||
'all-contacts-in-channels': 59664,
|
||||
'all-contacts-in-queue': 59665,
|
||||
'apple-monochromatic': 59666,
|
||||
'apps': 59667,
|
||||
'arrow-back': 59668,
|
||||
'arrow-collapse': 59669,
|
||||
'arrow-decrease': 59670,
|
||||
'arrow-down-box': 59671,
|
||||
'arrow-down-circle': 59672,
|
||||
'arrow-down': 59673,
|
||||
'arrow-expand': 59674,
|
||||
'arrow-increase': 59675,
|
||||
'arrow-looping': 59677,
|
||||
'arrow-return': 59678,
|
||||
'arrow-up-box': 59679,
|
||||
'arrow-up': 59680,
|
||||
'audio-disabled': 59681,
|
||||
'audio-unavailable': 59682,
|
||||
'audio': 59683,
|
||||
'auditing': 59684,
|
||||
'auth': 59685,
|
||||
'avatar': 59686,
|
||||
'backspace': 59687,
|
||||
'bold': 59688,
|
||||
'book': 59689,
|
||||
'business': 59690,
|
||||
'calendar': 59691,
|
||||
'camera-disabled': 59692,
|
||||
'camera-filled': 59693,
|
||||
'camera-photo': 59694,
|
||||
'camera-unavailable': 59695,
|
||||
'camera': 59696,
|
||||
'canned-response': 59697,
|
||||
'card': 59698,
|
||||
'channel-private': 59699,
|
||||
'channel-public': 59700,
|
||||
'chat-close': 59701,
|
||||
'chat-forward': 59702,
|
||||
'check': 59703,
|
||||
'chevron-down': 59704,
|
||||
'chevron-left-big': 59705,
|
||||
'chevron-left': 59706,
|
||||
'chevron-right': 59707,
|
||||
'chevron-up': 59708,
|
||||
'circle-check': 59709,
|
||||
'clipboard': 59710,
|
||||
'clock': 59711,
|
||||
'close': 59712,
|
||||
'cloud-connectivity': 59713,
|
||||
'code': 59714,
|
||||
'contacts': 59715,
|
||||
'copy': 59716,
|
||||
'create': 59717,
|
||||
'dashboard': 59718,
|
||||
'delete': 59719,
|
||||
'desktop': 59720,
|
||||
'dialpad': 59721,
|
||||
'discussions': 59722,
|
||||
'document': 59723,
|
||||
'donner': 59724,
|
||||
'download': 59725,
|
||||
'edit': 59726,
|
||||
'emoji-bad-mood': 59727,
|
||||
'emoji-neutral-mood': 59728,
|
||||
'emoji': 59729,
|
||||
'encrypted': 59730,
|
||||
'engagement-dashboard': 59731,
|
||||
'enterprise-feature': 59732,
|
||||
'facebook-monochromatic': 59733,
|
||||
'file-document': 59734,
|
||||
'file-sheet': 59735,
|
||||
'filter': 59736,
|
||||
'fingerprint': 59737,
|
||||
'flag': 59738,
|
||||
'folder': 59739,
|
||||
'game': 59753,
|
||||
'giphy-monochromatic': 59754,
|
||||
'google-drive-monochromatic': 59756,
|
||||
'group-by-type': 59757,
|
||||
hamburguer: 59758,
|
||||
history: 59759,
|
||||
home: 59760,
|
||||
image: 59761,
|
||||
info: 59762,
|
||||
'hamburguer': 59758,
|
||||
'history': 59759,
|
||||
'home': 59760,
|
||||
'image': 59761,
|
||||
'info': 59762,
|
||||
'input-clear': 59763,
|
||||
instance: 59764,
|
||||
italic: 59765,
|
||||
'instance': 59764,
|
||||
'italic': 59765,
|
||||
'jump-backward': 59766,
|
||||
'jump-forward': 59767,
|
||||
'jump-to-message': 59768,
|
||||
kebab: 59769,
|
||||
keyboard: 59770,
|
||||
language: 59771,
|
||||
'kebab': 59769,
|
||||
'keyboard': 59770,
|
||||
'language': 59771,
|
||||
'live-streaming': 59773,
|
||||
live: 59774,
|
||||
'live': 59774,
|
||||
'livechat-monochromatic': 59775,
|
||||
'log-view': 59778,
|
||||
login: 59779,
|
||||
logout: 59780,
|
||||
mail: 59781,
|
||||
marketplace: 59782,
|
||||
meatballs: 59783,
|
||||
mention: 59784,
|
||||
'login': 59779,
|
||||
'logout': 59780,
|
||||
'mail': 59781,
|
||||
'marketplace': 59782,
|
||||
'meatballs': 59783,
|
||||
'mention': 59784,
|
||||
'message-disabled': 59785,
|
||||
message: 59786,
|
||||
'message': 59786,
|
||||
'microphone-disabled': 59787,
|
||||
microphone: 59788,
|
||||
mobile: 59789,
|
||||
moon: 59790,
|
||||
'microphone': 59788,
|
||||
'mobile': 59789,
|
||||
'moon': 59790,
|
||||
'move-to-the-queue': 59791,
|
||||
'musical-note': 59792,
|
||||
'new-window': 59793,
|
||||
'notification-disabled': 59794,
|
||||
notification: 59795,
|
||||
omnichannel: 59796,
|
||||
order: 59797,
|
||||
'notification': 59795,
|
||||
'omnichannel': 59796,
|
||||
'order': 59797,
|
||||
'ordering-ascending': 59798,
|
||||
'ordering-descending': 59800,
|
||||
'pause-filled': 59802,
|
||||
pause: 59803,
|
||||
'pause': 59803,
|
||||
'phone-disabled': 59804,
|
||||
'phone-end': 59805,
|
||||
phone: 59806,
|
||||
'phone': 59806,
|
||||
'pin-map': 59807,
|
||||
pin: 59808,
|
||||
Pipe: 59809,
|
||||
'pin': 59808,
|
||||
'Pipe': 59809,
|
||||
'play-filled': 59810,
|
||||
play: 59811,
|
||||
prune: 59817,
|
||||
queue: 59818,
|
||||
quote: 59819,
|
||||
'play': 59811,
|
||||
'prune': 59817,
|
||||
'queue': 59818,
|
||||
'quote': 59819,
|
||||
'reaction-add': 59820,
|
||||
record: 59821,
|
||||
refresh: 59822,
|
||||
search: 59823,
|
||||
'record': 59821,
|
||||
'refresh': 59822,
|
||||
'search': 59823,
|
||||
'send-filled': 59824,
|
||||
send: 59825,
|
||||
settings: 59826,
|
||||
share: 59827,
|
||||
'send': 59825,
|
||||
'settings': 59826,
|
||||
'share': 59827,
|
||||
'shield-check': 59828,
|
||||
'shield-alt': 59829,
|
||||
signal: 59830,
|
||||
'signal': 59830,
|
||||
'sort-az': 59831,
|
||||
sort: 59832,
|
||||
'sort': 59832,
|
||||
'star-filled': 59833,
|
||||
star: 59834,
|
||||
strike: 59846,
|
||||
sun: 59847,
|
||||
support: 59848,
|
||||
team: 59849,
|
||||
threads: 59850,
|
||||
total: 59851,
|
||||
transcript: 59852,
|
||||
underline: 59853,
|
||||
undo: 59854,
|
||||
Unlimited: 59855,
|
||||
'star': 59834,
|
||||
'strike': 59846,
|
||||
'sun': 59847,
|
||||
'support': 59848,
|
||||
'team': 59849,
|
||||
'threads': 59850,
|
||||
'total': 59851,
|
||||
'transcript': 59852,
|
||||
'underline': 59853,
|
||||
'undo': 59854,
|
||||
'Unlimited': 59855,
|
||||
'unread-on-top-disabled': 59856,
|
||||
'unread-on-top': 59857,
|
||||
upload: 59858,
|
||||
'upload': 59858,
|
||||
'user-add': 59859,
|
||||
'user-forward': 59860,
|
||||
user: 59861,
|
||||
'user': 59861,
|
||||
'view-condensed': 59862,
|
||||
'view-extended': 59863,
|
||||
'view-medium': 59864,
|
||||
'waiting-on-me': 59865,
|
||||
warning: 59866,
|
||||
'warning': 59866,
|
||||
'whatsapp-monochromatic': 59868,
|
||||
'wordpress-monochromatic': 59656,
|
||||
workspaces: 59870,
|
||||
zip: 59871,
|
||||
add: 59872,
|
||||
sms: 59753
|
||||
'wordpress-monochromatic': 59755,
|
||||
'workspaces': 59870,
|
||||
'zip': 59871,
|
||||
'add': 59872,
|
||||
'sms': 59772
|
||||
};
|
||||
|
|
File diff suppressed because one or more lines are too long
|
@ -1,7 +1,7 @@
|
|||
import React from 'react';
|
||||
import { Text, View, ViewStyle } from 'react-native';
|
||||
|
||||
import Touch from '../../utils/touch';
|
||||
import Touch from '../../lib/methods/helpers/touch';
|
||||
import Avatar from '../Avatar';
|
||||
import RoomTypeIcon from '../RoomTypeIcon';
|
||||
import styles, { ROW_HEIGHT } from './styles';
|
||||
|
@ -54,7 +54,7 @@ const DirectoryItem = ({
|
|||
<Avatar text={avatar} size={30} type={type} rid={rid} style={styles.directoryItemAvatar} />
|
||||
<View style={styles.directoryItemTextContainer}>
|
||||
<View style={styles.directoryItemTextTitle}>
|
||||
<RoomTypeIcon type={type} teamMain={teamMain} />
|
||||
{type !== 'd' ? <RoomTypeIcon type={type} teamMain={teamMain} /> : null}
|
||||
<Text style={[styles.directoryItemName, { color: themes[theme].titleText }]} numberOfLines={1}>
|
||||
{title}
|
||||
</Text>
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import React from 'react';
|
||||
import FastImage from '@rocket.chat/react-native-fast-image';
|
||||
import FastImage from 'react-native-fast-image';
|
||||
|
||||
import { ICustomEmoji } from '../../definitions/IEmoji';
|
||||
|
||||
|
|
|
@ -1,10 +1,10 @@
|
|||
import React from 'react';
|
||||
import { FlatList, Text, TouchableOpacity } from 'react-native';
|
||||
|
||||
import shortnameToUnicode from '../../utils/shortnameToUnicode';
|
||||
import shortnameToUnicode from '../../lib/methods/helpers/shortnameToUnicode';
|
||||
import styles from './styles';
|
||||
import CustomEmoji from './CustomEmoji';
|
||||
import scrollPersistTaps from '../../utils/scrollPersistTaps';
|
||||
import scrollPersistTaps from '../../lib/methods/helpers/scrollPersistTaps';
|
||||
import { IEmoji, IEmojiCategory } from '../../definitions/IEmoji';
|
||||
|
||||
const EMOJI_SIZE = 50;
|
||||
|
|
|
@ -5,7 +5,7 @@ import { dequal } from 'dequal';
|
|||
import { connect } from 'react-redux';
|
||||
import orderBy from 'lodash/orderBy';
|
||||
import { sanitizedRaw } from '@nozbe/watermelondb/RawRecord';
|
||||
import { ImageStyle } from '@rocket.chat/react-native-fast-image';
|
||||
import { ImageStyle } from 'react-native-fast-image';
|
||||
|
||||
import TabBar from './TabBar';
|
||||
import EmojiCategory from './EmojiCategory';
|
||||
|
@ -14,8 +14,8 @@ import categories from './categories';
|
|||
import database from '../../lib/database';
|
||||
import { emojisByCategory } from './emojis';
|
||||
import protectedFunction from '../../lib/methods/helpers/protectedFunction';
|
||||
import shortnameToUnicode from '../../utils/shortnameToUnicode';
|
||||
import log from '../../utils/log';
|
||||
import shortnameToUnicode from '../../lib/methods/helpers/shortnameToUnicode';
|
||||
import log from '../../lib/methods/helpers/log';
|
||||
import { themes } from '../../lib/constants';
|
||||
import { TSupportedThemes, withTheme } from '../../theme';
|
||||
import { IEmoji, TGetCustomEmoji, IApplicationState, ICustomEmojis, TFrequentlyUsedEmojiModel } from '../../definitions';
|
||||
|
|
|
@ -3,12 +3,12 @@ import { ScrollView, ScrollViewProps, StyleSheet, View } from 'react-native';
|
|||
|
||||
import { themes } from '../lib/constants';
|
||||
import sharedStyles from '../views/Styles';
|
||||
import scrollPersistTaps from '../utils/scrollPersistTaps';
|
||||
import scrollPersistTaps from '../lib/methods/helpers/scrollPersistTaps';
|
||||
import KeyboardView from './KeyboardView';
|
||||
import { useTheme } from '../theme';
|
||||
import StatusBar from './StatusBar';
|
||||
import AppVersion from './AppVersion';
|
||||
import { isTablet } from '../utils/deviceInfo';
|
||||
import { isTablet } from '../lib/methods/helpers';
|
||||
import SafeAreaView from './SafeAreaView';
|
||||
|
||||
interface IFormContainer extends ScrollViewProps {
|
||||
|
|
|
@ -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;
|
|
@ -1,14 +1,12 @@
|
|||
import React from 'react';
|
||||
|
||||
import { isIOS } from '../../utils/deviceInfo';
|
||||
import { isIOS } from '../../lib/methods/helpers';
|
||||
import I18n from '../../i18n';
|
||||
import Container from './HeaderButtonContainer';
|
||||
import Item from './HeaderButtonItem';
|
||||
import Item, { IHeaderButtonItem } from './HeaderButtonItem';
|
||||
|
||||
interface IHeaderButtonCommon {
|
||||
interface IHeaderButtonCommon extends IHeaderButtonItem {
|
||||
navigation?: any; // TODO: Evaluate proper type
|
||||
onPress?: () => void;
|
||||
testID?: string;
|
||||
}
|
||||
|
||||
// Left
|
||||
|
@ -28,20 +26,20 @@ export const CloseModal = React.memo(
|
|||
)
|
||||
);
|
||||
|
||||
export const CancelModal = React.memo(({ onPress, testID }: Partial<IHeaderButtonCommon>) => (
|
||||
export const CancelModal = React.memo(({ onPress, testID, ...props }: IHeaderButtonCommon) => (
|
||||
<Container left>
|
||||
{isIOS ? (
|
||||
<Item title={I18n.t('Cancel')} onPress={onPress} testID={testID} />
|
||||
<Item title={I18n.t('Cancel')} onPress={onPress} testID={testID} {...props} />
|
||||
) : (
|
||||
<Item iconName='close' onPress={onPress} testID={testID} />
|
||||
<Item iconName='close' onPress={onPress} testID={testID} {...props} />
|
||||
)}
|
||||
</Container>
|
||||
));
|
||||
|
||||
// Right
|
||||
export const More = React.memo(({ onPress, testID }: Partial<IHeaderButtonCommon>) => (
|
||||
export const More = React.memo(({ onPress, testID, ...props }: IHeaderButtonCommon) => (
|
||||
<Container>
|
||||
<Item iconName='kebab' onPress={onPress} testID={testID} />
|
||||
<Item iconName='kebab' onPress={onPress} testID={testID} {...props} />
|
||||
</Container>
|
||||
));
|
||||
|
||||
|
@ -58,7 +56,7 @@ export const Preferences = React.memo(({ onPress, testID, ...props }: IHeaderBut
|
|||
));
|
||||
|
||||
export const Legal = React.memo(
|
||||
({ navigation, testID, onPress = () => navigation?.navigate('LegalView') }: IHeaderButtonCommon) => (
|
||||
<More onPress={onPress} testID={testID} />
|
||||
({ navigation, testID, onPress = () => navigation?.navigate('LegalView'), ...props }: IHeaderButtonCommon) => (
|
||||
<More onPress={onPress} testID={testID} {...props} />
|
||||
)
|
||||
);
|
||||
|
|
|
@ -1,18 +1,18 @@
|
|||
import React from 'react';
|
||||
import { Platform, StyleSheet, Text } from 'react-native';
|
||||
import Touchable from 'react-native-platform-touchable';
|
||||
import { PlatformPressable } from '@react-navigation/elements';
|
||||
|
||||
import { CustomIcon, TIconsName } from '../CustomIcon';
|
||||
import { CustomIcon, ICustomIcon, TIconsName } from '../CustomIcon';
|
||||
import { useTheme } from '../../theme';
|
||||
import { themes } from '../../lib/constants';
|
||||
import sharedStyles from '../../views/Styles';
|
||||
|
||||
interface IHeaderButtonItem {
|
||||
export interface IHeaderButtonItem extends Omit<ICustomIcon, 'name' | 'size' | 'color'> {
|
||||
title?: string;
|
||||
iconName?: TIconsName;
|
||||
onPress?: <T>(arg: T) => void;
|
||||
testID?: string;
|
||||
badge?(): void;
|
||||
color?: string;
|
||||
}
|
||||
|
||||
export const BUTTON_HIT_SLOP = {
|
||||
|
@ -24,7 +24,7 @@ export const BUTTON_HIT_SLOP = {
|
|||
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
marginHorizontal: 6
|
||||
padding: 6
|
||||
},
|
||||
title: {
|
||||
...Platform.select({
|
||||
|
@ -39,19 +39,21 @@ const styles = StyleSheet.create({
|
|||
}
|
||||
});
|
||||
|
||||
const Item = ({ title, iconName, onPress, testID, badge }: IHeaderButtonItem): React.ReactElement => {
|
||||
const { theme } = useTheme();
|
||||
const Item = ({ title, iconName, onPress, testID, badge, color, ...props }: IHeaderButtonItem): React.ReactElement => {
|
||||
const { colors } = useTheme();
|
||||
return (
|
||||
<Touchable onPress={onPress} testID={testID} hitSlop={BUTTON_HIT_SLOP} style={styles.container}>
|
||||
<PlatformPressable onPress={onPress} testID={testID} hitSlop={BUTTON_HIT_SLOP} style={styles.container}>
|
||||
<>
|
||||
{iconName ? (
|
||||
<CustomIcon name={iconName} size={24} color={themes[theme].headerTintColor} />
|
||||
<CustomIcon name={iconName} size={24} color={color || colors.headerTintColor} {...props} />
|
||||
) : (
|
||||
<Text style={[styles.title, { color: themes[theme].headerTintColor }]}>{title}</Text>
|
||||
<Text style={[styles.title, { color: color || colors.headerTintColor }]} {...props}>
|
||||
{title}
|
||||
</Text>
|
||||
)}
|
||||
{badge ? badge() : null}
|
||||
</>
|
||||
</Touchable>
|
||||
</PlatformPressable>
|
||||
);
|
||||
};
|
||||
|
||||
|
|
|
@ -7,8 +7,8 @@ const styles = StyleSheet.create({
|
|||
badgeContainer: {
|
||||
padding: 2,
|
||||
position: 'absolute',
|
||||
right: -3,
|
||||
top: -3,
|
||||
right: 2,
|
||||
top: 2,
|
||||
borderRadius: 10,
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center'
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import React from 'react';
|
||||
import { Image } from 'react-native';
|
||||
import { FastImageProps } from '@rocket.chat/react-native-fast-image';
|
||||
import { FastImageProps } from 'react-native-fast-image';
|
||||
|
||||
import { types } from './types';
|
||||
|
||||
|
@ -10,7 +10,7 @@ export const ImageComponent = (type?: string): React.ComponentType<Partial<Image
|
|||
const { Image } = require('react-native');
|
||||
Component = Image;
|
||||
} else {
|
||||
const FastImage = require('@rocket.chat/react-native-fast-image').default;
|
||||
const FastImage = require('react-native-fast-image');
|
||||
Component = FastImage;
|
||||
}
|
||||
return Component;
|
|
@ -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>
|
||||
);
|
||||
};
|
|
@ -11,7 +11,7 @@ import sharedStyles from '../../views/Styles';
|
|||
import { themes } from '../../lib/constants';
|
||||
import { useTheme } from '../../theme';
|
||||
import { ROW_HEIGHT } from '../RoomItem';
|
||||
import { goRoom } from '../../utils/goRoom';
|
||||
import { goRoom } from '../../lib/methods/helpers/goRoom';
|
||||
import Navigation from '../../lib/navigation/appNavigation';
|
||||
import { useOrientation } from '../../dimensions';
|
||||
import { IApplicationState, ISubscription, SubscriptionType } from '../../definitions';
|
||||
|
|
|
@ -4,9 +4,9 @@ import { connect } from 'react-redux';
|
|||
import { dequal } from 'dequal';
|
||||
|
||||
import NotifierComponent, { INotifierComponent } from './NotifierComponent';
|
||||
import EventEmitter from '../../utils/events';
|
||||
import EventEmitter from '../../lib/methods/helpers/events';
|
||||
import Navigation from '../../lib/navigation/appNavigation';
|
||||
import { getActiveRoute } from '../../utils/navigation';
|
||||
import { getActiveRoute } from '../../lib/methods/helpers/navigation';
|
||||
import { IApplicationState } from '../../definitions';
|
||||
import { IRoom } from '../../reducers/room';
|
||||
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import React from 'react';
|
||||
import { KeyboardAwareScrollView, KeyboardAwareScrollViewProps } from '@codler/react-native-keyboard-aware-scroll-view';
|
||||
|
||||
import scrollPersistTaps from '../utils/scrollPersistTaps';
|
||||
import scrollPersistTaps from '../lib/methods/helpers/scrollPersistTaps';
|
||||
|
||||
interface IKeyboardViewProps extends KeyboardAwareScrollViewProps {
|
||||
keyboardVerticalOffset?: number;
|
||||
|
|
|
@ -2,7 +2,7 @@ import React from 'react';
|
|||
import { ScrollView, StyleSheet } from 'react-native';
|
||||
|
||||
import { withTheme } from '../../theme';
|
||||
import scrollPersistTaps from '../../utils/scrollPersistTaps';
|
||||
import scrollPersistTaps from '../../lib/methods/helpers/scrollPersistTaps';
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import React from 'react';
|
||||
import { I18nManager, StyleProp, StyleSheet, Text, TextStyle, View } from 'react-native';
|
||||
|
||||
import Touch from '../../utils/touch';
|
||||
import Touch from '../../lib/methods/helpers/touch';
|
||||
import { themes } from '../../lib/constants';
|
||||
import sharedStyles from '../../views/Styles';
|
||||
import { TSupportedThemes, useTheme } from '../../theme';
|
||||
|
|
|
@ -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));
|
|
@ -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;
|
|
@ -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}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</>
|
||||
));
|
|
@ -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;
|
|
@ -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;
|
|
@ -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}]}]}]}"`;
|
|
@ -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;
|
|
@ -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;
|
||||
}
|
|
@ -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 });
|
||||
};
|
|
@ -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
|
||||
}
|
||||
});
|
|
@ -4,7 +4,7 @@ import { FlatList, StyleSheet, Text, View } from 'react-native';
|
|||
import { TSupportedThemes, useTheme } from '../../theme';
|
||||
import { themes } from '../../lib/constants';
|
||||
import { CustomIcon } from '../CustomIcon';
|
||||
import shortnameToUnicode from '../../utils/shortnameToUnicode';
|
||||
import shortnameToUnicode from '../../lib/methods/helpers/shortnameToUnicode';
|
||||
import CustomEmoji from '../EmojiPicker/CustomEmoji';
|
||||
import database from '../../lib/database';
|
||||
import { Button } from '../ActionSheet';
|
||||
|
|
|
@ -6,17 +6,18 @@ import moment from 'moment';
|
|||
|
||||
import database from '../../lib/database';
|
||||
import I18n from '../../i18n';
|
||||
import log, { logEvent } from '../../utils/log';
|
||||
import log, { logEvent } from '../../lib/methods/helpers/log';
|
||||
import Navigation from '../../lib/navigation/appNavigation';
|
||||
import { getMessageTranslation } from '../message/utils';
|
||||
import { LISTENER } from '../Toast';
|
||||
import EventEmitter from '../../utils/events';
|
||||
import { showConfirmationAlert } from '../../utils/info';
|
||||
import EventEmitter from '../../lib/methods/helpers/events';
|
||||
import { showConfirmationAlert } from '../../lib/methods/helpers/info';
|
||||
import { TActionSheetOptionsItem, useActionSheet } from '../ActionSheet';
|
||||
import Header, { HEADER_HEIGHT, IHeader } from './Header';
|
||||
import events from '../../utils/log/events';
|
||||
import events from '../../lib/methods/helpers/log/events';
|
||||
import { IApplicationState, ILoggedUser, TAnyMessageModel, TSubscriptionModel } from '../../definitions';
|
||||
import { getPermalinkMessage, hasPermission } from '../../lib/methods';
|
||||
import { getPermalinkMessage } from '../../lib/methods';
|
||||
import { hasPermission } from '../../lib/methods/helpers';
|
||||
import { Services } from '../../lib/services';
|
||||
|
||||
export interface IMessageActionsProps {
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import FastImage from '@rocket.chat/react-native-fast-image';
|
||||
import FastImage from 'react-native-fast-image';
|
||||
import React, { useContext, useState } from 'react';
|
||||
import { TouchableOpacity } from 'react-native';
|
||||
|
||||
|
|
|
@ -2,7 +2,7 @@ import React, { useContext } from 'react';
|
|||
import { Text } from 'react-native';
|
||||
|
||||
import { IEmoji } from '../../../definitions/IEmoji';
|
||||
import shortnameToUnicode from '../../../utils/shortnameToUnicode';
|
||||
import shortnameToUnicode from '../../../lib/methods/helpers/shortnameToUnicode';
|
||||
import CustomEmoji from '../../EmojiPicker/CustomEmoji';
|
||||
import MessageboxContext from '../Context';
|
||||
import styles from '../styles';
|
||||
|
|
|
@ -9,7 +9,7 @@ import styles from './styles';
|
|||
import I18n from '../../i18n';
|
||||
import { themes } from '../../lib/constants';
|
||||
import { CustomIcon } from '../CustomIcon';
|
||||
import { events, logEvent } from '../../utils/log';
|
||||
import { events, logEvent } from '../../lib/methods/helpers/log';
|
||||
import { TSupportedThemes } from '../../theme';
|
||||
|
||||
interface IMessageBoxRecordAudioProps {
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import { ImageOrVideo } from 'react-native-image-crop-picker';
|
||||
|
||||
import { isIOS } from '../../utils/deviceInfo';
|
||||
import { isIOS } from '../../lib/methods/helpers';
|
||||
|
||||
const regex = new RegExp(/\.[^/.]+$/); // Check from last '.' of the string
|
||||
|
||||
|
|
|
@ -9,16 +9,15 @@ import { Q } from '@nozbe/watermelondb';
|
|||
import { TouchableWithoutFeedback } from 'react-native-gesture-handler';
|
||||
|
||||
import { generateTriggerId } from '../../lib/methods/actions';
|
||||
import TextInput, { IThemedTextInput } from '../TextInput';
|
||||
import { TextInput, IThemedTextInput } from '../TextInput';
|
||||
import { userTyping as userTypingAction } from '../../actions/room';
|
||||
import styles from './styles';
|
||||
import database from '../../lib/database';
|
||||
import { emojis } from '../EmojiPicker/emojis';
|
||||
import log, { events, logEvent } from '../../utils/log';
|
||||
import log, { events, logEvent } from '../../lib/methods/helpers/log';
|
||||
import RecordAudio from './RecordAudio';
|
||||
import I18n from '../../i18n';
|
||||
import ReplyPreview from './ReplyPreview';
|
||||
import debounce from '../../utils/debounce';
|
||||
import { themes } from '../../lib/constants';
|
||||
// @ts-ignore
|
||||
// eslint-disable-next-line import/extensions,import/no-unresolved
|
||||
|
@ -26,9 +25,8 @@ import LeftButtons from './LeftButtons';
|
|||
// @ts-ignore
|
||||
// eslint-disable-next-line import/extensions,import/no-unresolved
|
||||
import RightButtons from './RightButtons';
|
||||
import { isAndroid, isTablet } from '../../utils/deviceInfo';
|
||||
import { canUploadFile } from '../../utils/media';
|
||||
import EventEmiter from '../../utils/events';
|
||||
import { canUploadFile } from '../../lib/methods/helpers/media';
|
||||
import EventEmiter from '../../lib/methods/helpers/events';
|
||||
import { KEY_COMMAND, handleCommandShowUpload, handleCommandSubmit, handleCommandTyping } from '../../commands';
|
||||
import getMentionRegexp from './getMentionRegexp';
|
||||
import Mentions from './Mentions';
|
||||
|
@ -47,13 +45,23 @@ import Navigation from '../../lib/navigation/appNavigation';
|
|||
import { withActionSheet } from '../ActionSheet';
|
||||
import { sanitizeLikeString } from '../../lib/database/utils';
|
||||
import { CustomIcon } from '../CustomIcon';
|
||||
import { IMessage } from '../../definitions/IMessage';
|
||||
import { forceJpgExtension } from './forceJpgExtension';
|
||||
import { IBaseScreen, IPreviewItem, IUser, TGetCustomEmoji, TSubscriptionModel, TThreadModel } from '../../definitions';
|
||||
import {
|
||||
IApplicationState,
|
||||
IBaseScreen,
|
||||
IPreviewItem,
|
||||
IUser,
|
||||
TGetCustomEmoji,
|
||||
TSubscriptionModel,
|
||||
TThreadModel,
|
||||
IMessage
|
||||
} from '../../definitions';
|
||||
import { MasterDetailInsideStackParamList } from '../../stacks/MasterDetailStack/types';
|
||||
import { getPermalinkMessage, hasPermission, search, sendFileMessage } from '../../lib/methods';
|
||||
import { getPermalinkMessage, search, sendFileMessage } from '../../lib/methods';
|
||||
import { hasPermission, debounce, isAndroid, isTablet } from '../../lib/methods/helpers';
|
||||
import { Services } from '../../lib/services';
|
||||
import { TSupportedThemes } from '../../theme';
|
||||
import { ChatsStackParamList } from '../../stacks/types';
|
||||
|
||||
if (isAndroid) {
|
||||
require('./EmojiKeyboard');
|
||||
|
@ -77,7 +85,7 @@ const videoPickerConfig: Options = {
|
|||
mediaType: 'video'
|
||||
};
|
||||
|
||||
export interface IMessageBoxProps extends IBaseScreen<MasterDetailInsideStackParamList, any> {
|
||||
export interface IMessageBoxProps extends IBaseScreen<ChatsStackParamList & MasterDetailInsideStackParamList, any> {
|
||||
rid: string;
|
||||
baseUrl: string;
|
||||
message: IMessage;
|
||||
|
@ -109,6 +117,7 @@ export interface IMessageBoxProps extends IBaseScreen<MasterDetailInsideStackPar
|
|||
usedCannedResponse: string;
|
||||
uploadFilePermission: string[];
|
||||
serverVersion: string;
|
||||
goToCannedResponses: () => void | null;
|
||||
}
|
||||
|
||||
interface IMessageBoxState {
|
||||
|
@ -307,7 +316,17 @@ class MessageBox extends Component<IMessageBoxProps, IMessageBoxState> {
|
|||
permissionToUpload
|
||||
} = this.state;
|
||||
|
||||
const { roomType, replying, editing, isFocused, message, theme, usedCannedResponse, uploadFilePermission } = this.props;
|
||||
const {
|
||||
roomType,
|
||||
replying,
|
||||
editing,
|
||||
isFocused,
|
||||
message,
|
||||
theme,
|
||||
usedCannedResponse,
|
||||
uploadFilePermission,
|
||||
goToCannedResponses
|
||||
} = this.props;
|
||||
if (nextProps.theme !== theme) {
|
||||
return true;
|
||||
}
|
||||
|
@ -359,12 +378,15 @@ class MessageBox extends Component<IMessageBoxProps, IMessageBoxState> {
|
|||
if (nextProps.usedCannedResponse !== usedCannedResponse) {
|
||||
return true;
|
||||
}
|
||||
if (nextProps.goToCannedResponses !== goToCannedResponses) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps: IMessageBoxProps) {
|
||||
const { uploadFilePermission } = this.props;
|
||||
if (!dequal(prevProps.uploadFilePermission, uploadFilePermission)) {
|
||||
const { uploadFilePermission, goToCannedResponses } = this.props;
|
||||
if (!dequal(prevProps.uploadFilePermission, uploadFilePermission) || prevProps.goToCannedResponses !== goToCannedResponses) {
|
||||
this.setOptions();
|
||||
}
|
||||
}
|
||||
|
@ -785,9 +807,16 @@ class MessageBox extends Component<IMessageBoxProps, IMessageBoxState> {
|
|||
showMessageBoxActions = () => {
|
||||
logEvent(events.ROOM_SHOW_BOX_ACTIONS);
|
||||
const { permissionToUpload } = this.state;
|
||||
const { showActionSheet } = this.props;
|
||||
const { showActionSheet, goToCannedResponses } = this.props;
|
||||
|
||||
const options = [];
|
||||
if (goToCannedResponses) {
|
||||
options.push({
|
||||
title: I18n.t('Canned_Responses'),
|
||||
icon: 'canned-response',
|
||||
onPress: () => goToCannedResponses()
|
||||
});
|
||||
}
|
||||
if (permissionToUpload) {
|
||||
options.push(
|
||||
{
|
||||
|
@ -1106,7 +1135,6 @@ class MessageBox extends Component<IMessageBoxProps, IMessageBoxState> {
|
|||
defaultValue=''
|
||||
multiline
|
||||
testID={`messagebox-input${tmid ? '-thread' : ''}`}
|
||||
theme={theme}
|
||||
{...isAndroidTablet}
|
||||
/>
|
||||
<RightButtons
|
||||
|
@ -1172,7 +1200,7 @@ class MessageBox extends Component<IMessageBoxProps, IMessageBoxState> {
|
|||
}
|
||||
}
|
||||
|
||||
const mapStateToProps = (state: any) => ({
|
||||
const mapStateToProps = (state: IApplicationState) => ({
|
||||
isMasterDetail: state.app.isMasterDetail,
|
||||
baseUrl: state.server.server,
|
||||
threadsEnabled: state.settings.Threads_enabled,
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import { StyleSheet } from 'react-native';
|
||||
|
||||
import { isIOS } from '../../utils/deviceInfo';
|
||||
import { isIOS } from '../../lib/methods/helpers';
|
||||
import sharedStyles from '../../views/Styles';
|
||||
|
||||
const MENTION_HEIGHT = 50;
|
||||
|
|
|
@ -5,7 +5,7 @@ import database from '../lib/database';
|
|||
import protectedFunction from '../lib/methods/helpers/protectedFunction';
|
||||
import { useActionSheet } from './ActionSheet';
|
||||
import I18n from '../i18n';
|
||||
import log from '../utils/log';
|
||||
import log from '../lib/methods/helpers/log';
|
||||
import { TMessageModel } from '../definitions';
|
||||
import { resendMessage } from '../lib/methods';
|
||||
|
||||
|
|
|
@ -3,7 +3,7 @@ import { Text } from 'react-native';
|
|||
|
||||
import styles from './styles';
|
||||
import { themes } from '../../../lib/constants';
|
||||
import Touch from '../../../utils/touch';
|
||||
import Touch from '../../../lib/methods/helpers/touch';
|
||||
import { CustomIcon, TIconsName } from '../../CustomIcon';
|
||||
import { useTheme } from '../../../theme';
|
||||
|
||||
|
|
|
@ -2,7 +2,7 @@ import React, { useEffect, useState } from 'react';
|
|||
import { Grid } from 'react-native-easy-grid';
|
||||
|
||||
import { themes } from '../../../lib/constants';
|
||||
import { resetAttempts } from '../../../utils/localAuthentication';
|
||||
import { resetAttempts } from '../../../lib/methods/helpers/localAuthentication';
|
||||
import { TYPE } from '../constants';
|
||||
import { getDiff, getLockedUntil } from '../utils';
|
||||
import I18n from '../../../i18n';
|
||||
|
|
|
@ -8,7 +8,7 @@ import Base, { IBase } from './Base';
|
|||
import Locked from './Base/Locked';
|
||||
import { TYPE } from './constants';
|
||||
import { ATTEMPTS_KEY, LOCKED_OUT_TIMER_KEY, MAX_ATTEMPTS, PASSCODE_KEY } from '../../lib/constants';
|
||||
import { biometryAuth, resetAttempts } from '../../utils/localAuthentication';
|
||||
import { biometryAuth, resetAttempts } from '../../lib/methods/helpers/localAuthentication';
|
||||
import { getDiff, getLockedUntil } from './utils';
|
||||
import { useUserPreferences } from '../../lib/methods/userPreferences';
|
||||
import I18n from '../../i18n';
|
||||
|
|
|
@ -1,24 +1,33 @@
|
|||
/* eslint-disable import/no-extraneous-dependencies, import/no-unresolved, import/extensions, react/prop-types, react/destructuring-assignment */
|
||||
import React from 'react';
|
||||
import { Dimensions, View } from 'react-native';
|
||||
import { Dimensions, SafeAreaView } from 'react-native';
|
||||
import { storiesOf } from '@storybook/react-native';
|
||||
import { Provider } from 'react-redux';
|
||||
import { SafeAreaProvider } from 'react-native-safe-area-context';
|
||||
import { Header, HeaderBackground } from '@react-navigation/elements';
|
||||
|
||||
import Header from '../Header';
|
||||
import { longText } from '../../../storybook/utils';
|
||||
import { ThemeContext } from '../../theme';
|
||||
import { store } from '../../../storybook/stories';
|
||||
import { colors, themes } from '../../lib/constants';
|
||||
import RoomHeaderComponent from './RoomHeader';
|
||||
|
||||
const stories = storiesOf('RoomHeader', module).addDecorator(story => <Provider store={store}>{story()}</Provider>);
|
||||
|
||||
// TODO: refactor after react-navigation v6
|
||||
const HeaderExample = ({ title }) => (
|
||||
<Header headerTitle={() => <View style={{ flex: 1, paddingHorizontal: 12 }}>{title()}</View>} />
|
||||
);
|
||||
const stories = storiesOf('RoomHeader', module)
|
||||
.addDecorator(story => <Provider store={store}>{story()}</Provider>)
|
||||
.addDecorator(story => <SafeAreaProvider>{story()}</SafeAreaProvider>);
|
||||
|
||||
const { width, height } = Dimensions.get('window');
|
||||
|
||||
const HeaderExample = ({ title, theme = 'light' }) => (
|
||||
<SafeAreaView>
|
||||
<Header
|
||||
title=''
|
||||
headerTitle={title}
|
||||
headerTitleAlign='left'
|
||||
headerBackground={() => <HeaderBackground style={{ backgroundColor: themes[theme].headerBackground }} />}
|
||||
/>
|
||||
</SafeAreaView>
|
||||
);
|
||||
|
||||
const RoomHeader = ({ ...props }) => (
|
||||
<RoomHeaderComponent
|
||||
width={width}
|
||||
|
@ -27,6 +36,8 @@ const RoomHeader = ({ ...props }) => (
|
|||
type='p'
|
||||
testID={props.title}
|
||||
onPress={() => alert('header pressed!')}
|
||||
status={props.status}
|
||||
usersTyping={props.usersTyping}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
|
@ -82,8 +93,8 @@ stories.add('thread', () => (
|
|||
));
|
||||
|
||||
const ThemeStory = ({ theme }) => (
|
||||
<ThemeContext.Provider value={{ theme }}>
|
||||
<HeaderExample title={() => <RoomHeader subtitle='subtitle' />} />
|
||||
<ThemeContext.Provider value={{ theme, colors: colors[theme] }}>
|
||||
<HeaderExample title={() => <RoomHeader subtitle='subtitle' />} theme={theme} />
|
||||
</ThemeContext.Provider>
|
||||
);
|
||||
|
||||
|
|
|
@ -3,7 +3,6 @@ import { StyleSheet, Text, TouchableOpacity, View } from 'react-native';
|
|||
|
||||
import I18n from '../../i18n';
|
||||
import sharedStyles from '../../views/Styles';
|
||||
import { themes } from '../../lib/constants';
|
||||
import { MarkdownPreview } from '../markdown';
|
||||
import RoomTypeIcon from '../RoomTypeIcon';
|
||||
import { TUserStatus, IOmnichannelSource } from '../../definitions';
|
||||
|
@ -44,39 +43,39 @@ const styles = StyleSheet.create({
|
|||
|
||||
type TRoomHeaderSubTitle = {
|
||||
usersTyping: [];
|
||||
subtitle: string;
|
||||
subtitle?: string;
|
||||
renderFunc?: () => React.ReactElement;
|
||||
scale: number;
|
||||
};
|
||||
|
||||
type TRoomHeaderHeaderTitle = {
|
||||
title: string;
|
||||
tmid: string;
|
||||
prid: string;
|
||||
title?: string;
|
||||
tmid?: string;
|
||||
prid?: string;
|
||||
scale: number;
|
||||
testID: string;
|
||||
testID?: string;
|
||||
};
|
||||
|
||||
interface IRoomHeader {
|
||||
title: string;
|
||||
subtitle: string;
|
||||
title?: string;
|
||||
subtitle?: string;
|
||||
type: string;
|
||||
width: number;
|
||||
height: number;
|
||||
prid: string;
|
||||
tmid: string;
|
||||
teamMain: boolean;
|
||||
prid?: string;
|
||||
tmid?: string;
|
||||
teamMain?: boolean;
|
||||
status: TUserStatus;
|
||||
usersTyping: [];
|
||||
isGroupChat: boolean;
|
||||
parentTitle: string;
|
||||
onPress: () => void;
|
||||
testID: string;
|
||||
isGroupChat?: boolean;
|
||||
parentTitle?: string;
|
||||
onPress: Function;
|
||||
testID?: string;
|
||||
sourceType?: IOmnichannelSource;
|
||||
}
|
||||
|
||||
const SubTitle = React.memo(({ usersTyping, subtitle, renderFunc, scale }: TRoomHeaderSubTitle) => {
|
||||
const { theme } = useTheme();
|
||||
const { colors } = useTheme();
|
||||
const fontSize = getSubTitleSize(scale);
|
||||
// typing
|
||||
if (usersTyping.length) {
|
||||
|
@ -87,7 +86,7 @@ const SubTitle = React.memo(({ usersTyping, subtitle, renderFunc, scale }: TRoom
|
|||
usersText = usersTyping.join(', ');
|
||||
}
|
||||
return (
|
||||
<Text style={[styles.subtitle, { fontSize, color: themes[theme].auxiliaryText }]} numberOfLines={1}>
|
||||
<Text style={[styles.subtitle, { fontSize, color: colors.auxiliaryText }]} numberOfLines={1}>
|
||||
<Text style={styles.typingUsers}>{usersText} </Text>
|
||||
{usersTyping.length > 1 ? I18n.t('are_typing') : I18n.t('is_typing')}...
|
||||
</Text>
|
||||
|
@ -101,15 +100,15 @@ const SubTitle = React.memo(({ usersTyping, subtitle, renderFunc, scale }: TRoom
|
|||
|
||||
// subtitle
|
||||
if (subtitle) {
|
||||
return <MarkdownPreview msg={subtitle} style={[styles.subtitle, { fontSize, color: themes[theme].auxiliaryText }]} />;
|
||||
return <MarkdownPreview msg={subtitle} style={[styles.subtitle, { fontSize, color: colors.auxiliaryText }]} />;
|
||||
}
|
||||
|
||||
return null;
|
||||
});
|
||||
|
||||
const HeaderTitle = React.memo(({ title, tmid, prid, scale, testID }: TRoomHeaderHeaderTitle) => {
|
||||
const { theme } = useTheme();
|
||||
const titleStyle = { fontSize: TITLE_SIZE * scale, color: themes[theme].headerTitleColor };
|
||||
const { colors } = useTheme();
|
||||
const titleStyle = { fontSize: TITLE_SIZE * scale, color: colors.headerTitleColor };
|
||||
if (!tmid && !prid) {
|
||||
return (
|
||||
<Text style={[styles.title, titleStyle]} numberOfLines={1} testID={testID}>
|
||||
|
@ -139,7 +138,7 @@ const Header = React.memo(
|
|||
usersTyping = [],
|
||||
sourceType
|
||||
}: IRoomHeader) => {
|
||||
const { theme } = useTheme();
|
||||
const { colors } = useTheme();
|
||||
const portrait = height > width;
|
||||
let scale = 1;
|
||||
|
||||
|
@ -154,7 +153,7 @@ const Header = React.memo(
|
|||
renderFunc = () => (
|
||||
<View style={styles.titleContainer}>
|
||||
<RoomTypeIcon type={prid ? 'discussion' : type} isGroupChat={isGroupChat} status={status} teamMain={teamMain} />
|
||||
<Text style={[styles.subtitle, { color: themes[theme].auxiliaryText }]} numberOfLines={1}>
|
||||
<Text style={[styles.subtitle, { color: colors.auxiliaryText }]} numberOfLines={1}>
|
||||
{parentTitle}
|
||||
</Text>
|
||||
</View>
|
||||
|
|
File diff suppressed because one or more lines are too long
|
@ -1,117 +1,56 @@
|
|||
import { dequal } from 'dequal';
|
||||
import React, { Component } from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import React from 'react';
|
||||
import { shallowEqual, useSelector } from 'react-redux';
|
||||
|
||||
import { IApplicationState, TUserStatus, IOmnichannelSource } from '../../definitions';
|
||||
import { withDimensions } from '../../dimensions';
|
||||
import { IApplicationState, TUserStatus, IOmnichannelSource, IVisitor } from '../../definitions';
|
||||
import { useDimensions } from '../../dimensions';
|
||||
import I18n from '../../i18n';
|
||||
import RoomHeader from './RoomHeader';
|
||||
|
||||
interface IRoomHeaderContainerProps {
|
||||
title: string;
|
||||
subtitle: string;
|
||||
title?: string;
|
||||
subtitle?: string;
|
||||
type: string;
|
||||
prid: string;
|
||||
tmid: string;
|
||||
teamMain: boolean;
|
||||
usersTyping: [];
|
||||
status: TUserStatus;
|
||||
statusText: string;
|
||||
connecting: boolean;
|
||||
connected: boolean;
|
||||
roomUserId: string;
|
||||
widthOffset: number;
|
||||
onPress(): void;
|
||||
width: number;
|
||||
height: number;
|
||||
parentTitle: string;
|
||||
isGroupChat: boolean;
|
||||
testID: string;
|
||||
prid?: string;
|
||||
tmid?: string;
|
||||
teamMain?: boolean;
|
||||
roomUserId?: string | null;
|
||||
onPress: Function;
|
||||
parentTitle?: string;
|
||||
isGroupChat?: boolean;
|
||||
testID?: string;
|
||||
sourceType?: IOmnichannelSource;
|
||||
visitor?: IVisitor;
|
||||
}
|
||||
|
||||
class RoomHeaderContainer extends Component<IRoomHeaderContainerProps, any> {
|
||||
shouldComponentUpdate(nextProps: IRoomHeaderContainerProps) {
|
||||
const {
|
||||
type,
|
||||
title,
|
||||
subtitle,
|
||||
status,
|
||||
statusText,
|
||||
connecting,
|
||||
connected,
|
||||
onPress,
|
||||
usersTyping,
|
||||
width,
|
||||
height,
|
||||
teamMain,
|
||||
sourceType
|
||||
} = this.props;
|
||||
if (nextProps.type !== type) {
|
||||
return true;
|
||||
}
|
||||
if (nextProps.title !== title) {
|
||||
return true;
|
||||
}
|
||||
if (nextProps.subtitle !== subtitle) {
|
||||
return true;
|
||||
}
|
||||
if (nextProps.status !== status) {
|
||||
return true;
|
||||
}
|
||||
if (nextProps.statusText !== statusText) {
|
||||
return true;
|
||||
}
|
||||
if (nextProps.connecting !== connecting) {
|
||||
return true;
|
||||
}
|
||||
if (nextProps.connected !== connected) {
|
||||
return true;
|
||||
}
|
||||
if (nextProps.width !== width) {
|
||||
return true;
|
||||
}
|
||||
if (nextProps.height !== height) {
|
||||
return true;
|
||||
}
|
||||
if (!dequal(nextProps.usersTyping, usersTyping)) {
|
||||
return true;
|
||||
}
|
||||
if (!dequal(nextProps.sourceType, sourceType)) {
|
||||
return true;
|
||||
}
|
||||
if (nextProps.onPress !== onPress) {
|
||||
return true;
|
||||
}
|
||||
if (nextProps.teamMain !== teamMain) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
const RoomHeaderContainer = React.memo(
|
||||
({
|
||||
isGroupChat,
|
||||
onPress,
|
||||
parentTitle,
|
||||
prid,
|
||||
roomUserId,
|
||||
subtitle: subtitleProp,
|
||||
teamMain,
|
||||
testID,
|
||||
title,
|
||||
tmid,
|
||||
type,
|
||||
sourceType,
|
||||
visitor
|
||||
}: IRoomHeaderContainerProps) => {
|
||||
let subtitle: string | undefined;
|
||||
let status: TUserStatus = 'offline';
|
||||
let statusText: string | undefined;
|
||||
const { width, height } = useDimensions();
|
||||
|
||||
render() {
|
||||
const {
|
||||
title,
|
||||
subtitle: subtitleProp,
|
||||
type,
|
||||
teamMain,
|
||||
prid,
|
||||
tmid,
|
||||
status = 'offline',
|
||||
statusText,
|
||||
connecting,
|
||||
connected,
|
||||
usersTyping,
|
||||
onPress,
|
||||
width,
|
||||
height,
|
||||
parentTitle,
|
||||
isGroupChat,
|
||||
testID,
|
||||
sourceType
|
||||
} = this.props;
|
||||
const connecting = useSelector((state: IApplicationState) => state.meteor.connecting || state.server.loading);
|
||||
const usersTyping = useSelector((state: IApplicationState) => state.usersTyping, shallowEqual);
|
||||
const connected = useSelector((state: IApplicationState) => state.meteor.connected);
|
||||
const activeUser = useSelector(
|
||||
(state: IApplicationState) => (roomUserId ? state.activeUsers?.[roomUserId] : undefined),
|
||||
shallowEqual
|
||||
);
|
||||
|
||||
let subtitle;
|
||||
if (connecting) {
|
||||
subtitle = I18n.t('Connecting');
|
||||
} else if (!connected) {
|
||||
|
@ -120,6 +59,17 @@ class RoomHeaderContainer extends Component<IRoomHeaderContainerProps, any> {
|
|||
subtitle = subtitleProp;
|
||||
}
|
||||
|
||||
if (connected) {
|
||||
if ((type === 'd' || (tmid && roomUserId)) && activeUser) {
|
||||
const { status: statusActiveUser, statusText: statusTextActiveUser } = activeUser;
|
||||
status = statusActiveUser;
|
||||
statusText = statusTextActiveUser;
|
||||
} else if (type === 'l' && visitor?.status) {
|
||||
const { status: statusVisitor } = visitor;
|
||||
status = statusVisitor;
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<RoomHeader
|
||||
prid={prid}
|
||||
|
@ -140,28 +90,6 @@ class RoomHeaderContainer extends Component<IRoomHeaderContainerProps, any> {
|
|||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
const mapStateToProps = (state: IApplicationState, ownProps: any) => {
|
||||
let statusText = '';
|
||||
let status = 'offline';
|
||||
const { roomUserId, type, visitor = {}, tmid } = ownProps;
|
||||
|
||||
if (state.meteor.connected) {
|
||||
if ((type === 'd' || (tmid && roomUserId)) && state.activeUsers[roomUserId]) {
|
||||
({ status, statusText } = state.activeUsers[roomUserId]);
|
||||
} else if (type === 'l' && visitor?.status) {
|
||||
({ status } = visitor);
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
connecting: state.meteor.connecting || state.server.loading,
|
||||
connected: state.meteor.connected,
|
||||
usersTyping: state.usersTyping,
|
||||
status: status as TUserStatus,
|
||||
statusText
|
||||
};
|
||||
};
|
||||
|
||||
export default connect(mapStateToProps)(withDimensions(RoomHeaderContainer));
|
||||
export default RoomHeaderContainer;
|
||||
|
|
|
@ -1,25 +1,32 @@
|
|||
import React from 'react';
|
||||
import { Animated, View } from 'react-native';
|
||||
import { View } from 'react-native';
|
||||
import Animated, {
|
||||
useAnimatedStyle,
|
||||
interpolate,
|
||||
withSpring,
|
||||
runOnJS,
|
||||
useAnimatedReaction,
|
||||
useSharedValue
|
||||
} from 'react-native-reanimated';
|
||||
import { RectButton } from 'react-native-gesture-handler';
|
||||
import * as Haptics from 'expo-haptics';
|
||||
|
||||
import { isRTL } from '../../i18n';
|
||||
import { CustomIcon } from '../CustomIcon';
|
||||
import { DisplayMode, themes } from '../../lib/constants';
|
||||
import { DisplayMode } from '../../lib/constants';
|
||||
import styles, { ACTION_WIDTH, LONG_SWIPE, ROW_HEIGHT_CONDENSED } from './styles';
|
||||
import { ILeftActionsProps, IRightActionsProps } from './interfaces';
|
||||
import { useTheme } from '../../theme';
|
||||
import I18n from '../../i18n';
|
||||
|
||||
const reverse = new Animated.Value(isRTL() ? -1 : 1);
|
||||
const CONDENSED_ICON_SIZE = 24;
|
||||
const EXPANDED_ICON_SIZE = 28;
|
||||
|
||||
export const LeftActions = React.memo(({ theme, transX, isRead, width, onToggleReadPress, displayMode }: ILeftActionsProps) => {
|
||||
const translateX = Animated.multiply(
|
||||
transX.interpolate({
|
||||
inputRange: [0, ACTION_WIDTH],
|
||||
outputRange: [-ACTION_WIDTH, 0]
|
||||
}),
|
||||
reverse
|
||||
);
|
||||
export const LeftActions = React.memo(({ transX, isRead, width, onToggleReadPress, displayMode }: ILeftActionsProps) => {
|
||||
const { colors } = useTheme();
|
||||
|
||||
const animatedStyles = useAnimatedStyle(() => ({
|
||||
transform: [{ translateX: transX.value }]
|
||||
}));
|
||||
|
||||
const isCondensed = displayMode === DisplayMode.Condensed;
|
||||
const viewHeight = isCondensed ? { height: ROW_HEIGHT_CONDENSED } : null;
|
||||
|
@ -29,20 +36,16 @@ export const LeftActions = React.memo(({ theme, transX, isRead, width, onToggleR
|
|||
<Animated.View
|
||||
style={[
|
||||
styles.actionLeftButtonContainer,
|
||||
{
|
||||
right: width - ACTION_WIDTH,
|
||||
width,
|
||||
transform: [{ translateX }],
|
||||
backgroundColor: themes[theme].tintColor
|
||||
},
|
||||
viewHeight
|
||||
{ width: width * 2, backgroundColor: colors.tintColor, right: '100%' },
|
||||
viewHeight,
|
||||
animatedStyles
|
||||
]}>
|
||||
<View style={[styles.actionLeftButtonContainer, viewHeight]}>
|
||||
<RectButton style={styles.actionButton} onPress={onToggleReadPress}>
|
||||
<CustomIcon
|
||||
size={isCondensed ? CONDENSED_ICON_SIZE : EXPANDED_ICON_SIZE}
|
||||
name={isRead ? 'flag' : 'check'}
|
||||
color={themes[theme].buttonText}
|
||||
color={colors.buttonText}
|
||||
/>
|
||||
</RectButton>
|
||||
</View>
|
||||
|
@ -51,64 +54,102 @@ export const LeftActions = React.memo(({ theme, transX, isRead, width, onToggleR
|
|||
);
|
||||
});
|
||||
|
||||
export const RightActions = React.memo(
|
||||
({ transX, favorite, width, toggleFav, onHidePress, theme, displayMode }: IRightActionsProps) => {
|
||||
const translateXFav = Animated.multiply(
|
||||
transX.interpolate({
|
||||
inputRange: [-width / 2, -ACTION_WIDTH * 2, 0],
|
||||
outputRange: [width / 2, width - ACTION_WIDTH * 2, width]
|
||||
}),
|
||||
reverse
|
||||
);
|
||||
const translateXHide = Animated.multiply(
|
||||
transX.interpolate({
|
||||
inputRange: [-width, -LONG_SWIPE, -ACTION_WIDTH * 2, 0],
|
||||
outputRange: [0, width - LONG_SWIPE, width - ACTION_WIDTH, width]
|
||||
}),
|
||||
reverse
|
||||
);
|
||||
export const RightActions = React.memo(({ transX, favorite, width, toggleFav, onHidePress, displayMode }: IRightActionsProps) => {
|
||||
const { colors } = useTheme();
|
||||
|
||||
const isCondensed = displayMode === DisplayMode.Condensed;
|
||||
const viewHeight = isCondensed ? { height: ROW_HEIGHT_CONDENSED } : null;
|
||||
const animatedFavStyles = useAnimatedStyle(() => ({ transform: [{ translateX: transX.value }] }));
|
||||
|
||||
return (
|
||||
<View style={[styles.actionsLeftContainer, viewHeight]} pointerEvents='box-none'>
|
||||
<Animated.View
|
||||
style={[
|
||||
styles.actionRightButtonContainer,
|
||||
{
|
||||
width,
|
||||
transform: [{ translateX: translateXFav }],
|
||||
backgroundColor: themes[theme].hideBackground
|
||||
},
|
||||
viewHeight
|
||||
]}>
|
||||
<RectButton style={[styles.actionButton, { backgroundColor: themes[theme].favoriteBackground }]} onPress={toggleFav}>
|
||||
<CustomIcon
|
||||
size={isCondensed ? CONDENSED_ICON_SIZE : EXPANDED_ICON_SIZE}
|
||||
name={favorite ? 'star-filled' : 'star'}
|
||||
color={themes[theme].buttonText}
|
||||
/>
|
||||
</RectButton>
|
||||
</Animated.View>
|
||||
<Animated.View
|
||||
style={[
|
||||
styles.actionRightButtonContainer,
|
||||
{
|
||||
width,
|
||||
transform: [{ translateX: translateXHide }]
|
||||
},
|
||||
isCondensed && { height: ROW_HEIGHT_CONDENSED }
|
||||
]}>
|
||||
<RectButton style={[styles.actionButton, { backgroundColor: themes[theme].hideBackground }]} onPress={onHidePress}>
|
||||
<CustomIcon
|
||||
size={isCondensed ? CONDENSED_ICON_SIZE : EXPANDED_ICON_SIZE}
|
||||
name='unread-on-top-disabled'
|
||||
color={themes[theme].buttonText}
|
||||
/>
|
||||
</RectButton>
|
||||
</Animated.View>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
);
|
||||
const translateXHide = useSharedValue(0);
|
||||
|
||||
const triggerHideAnimation = (toValue: number) => {
|
||||
Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light);
|
||||
translateXHide.value = withSpring(toValue, { overshootClamping: true, mass: 0.7 });
|
||||
};
|
||||
|
||||
useAnimatedReaction(
|
||||
() => transX.value,
|
||||
(currentTransX, previousTransX) => {
|
||||
// Triggers the animation and hapticFeedback if swipe reaches/unreaches the threshold.
|
||||
if (I18n.isRTL) {
|
||||
if (previousTransX && currentTransX > LONG_SWIPE && previousTransX <= LONG_SWIPE) {
|
||||
runOnJS(triggerHideAnimation)(ACTION_WIDTH);
|
||||
} else if (previousTransX && currentTransX <= LONG_SWIPE && previousTransX > LONG_SWIPE) {
|
||||
runOnJS(triggerHideAnimation)(0);
|
||||
}
|
||||
} else if (previousTransX && currentTransX < -LONG_SWIPE && previousTransX >= -LONG_SWIPE) {
|
||||
runOnJS(triggerHideAnimation)(-ACTION_WIDTH);
|
||||
} else if (previousTransX && currentTransX >= -LONG_SWIPE && previousTransX < -LONG_SWIPE) {
|
||||
runOnJS(triggerHideAnimation)(0);
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
const animatedHideStyles = useAnimatedStyle(() => {
|
||||
if (I18n.isRTL) {
|
||||
if (transX.value < LONG_SWIPE && transX.value >= 2 * ACTION_WIDTH) {
|
||||
const parallaxSwipe = interpolate(
|
||||
transX.value,
|
||||
[2 * ACTION_WIDTH, LONG_SWIPE],
|
||||
[ACTION_WIDTH, ACTION_WIDTH + 0.1 * transX.value]
|
||||
);
|
||||
return { transform: [{ translateX: parallaxSwipe + translateXHide.value }] };
|
||||
}
|
||||
return { transform: [{ translateX: transX.value - ACTION_WIDTH + translateXHide.value }] };
|
||||
}
|
||||
if (transX.value > -LONG_SWIPE && transX.value <= -2 * ACTION_WIDTH) {
|
||||
const parallaxSwipe = interpolate(
|
||||
transX.value,
|
||||
[-2 * ACTION_WIDTH, -LONG_SWIPE],
|
||||
[-ACTION_WIDTH, -ACTION_WIDTH + 0.1 * transX.value]
|
||||
);
|
||||
return { transform: [{ translateX: parallaxSwipe + translateXHide.value }] };
|
||||
}
|
||||
return { transform: [{ translateX: transX.value + ACTION_WIDTH + translateXHide.value }] };
|
||||
});
|
||||
|
||||
const isCondensed = displayMode === DisplayMode.Condensed;
|
||||
const viewHeight = isCondensed ? { height: ROW_HEIGHT_CONDENSED } : null;
|
||||
|
||||
return (
|
||||
<View style={[styles.actionsLeftContainer, viewHeight]} pointerEvents='box-none'>
|
||||
<Animated.View
|
||||
style={[
|
||||
styles.actionRightButtonContainer,
|
||||
{
|
||||
width,
|
||||
backgroundColor: colors.favoriteBackground,
|
||||
left: '100%'
|
||||
},
|
||||
viewHeight,
|
||||
animatedFavStyles
|
||||
]}>
|
||||
<RectButton style={[styles.actionButton, { backgroundColor: colors.favoriteBackground }]} onPress={toggleFav}>
|
||||
<CustomIcon
|
||||
size={isCondensed ? CONDENSED_ICON_SIZE : EXPANDED_ICON_SIZE}
|
||||
name={favorite ? 'star-filled' : 'star'}
|
||||
color={colors.buttonText}
|
||||
/>
|
||||
</RectButton>
|
||||
</Animated.View>
|
||||
<Animated.View
|
||||
style={[
|
||||
styles.actionRightButtonContainer,
|
||||
{
|
||||
width: width * 2,
|
||||
backgroundColor: colors.hideBackground,
|
||||
left: '100%'
|
||||
},
|
||||
isCondensed && { height: ROW_HEIGHT_CONDENSED },
|
||||
animatedHideStyles
|
||||
]}>
|
||||
<RectButton style={[styles.actionButton, { backgroundColor: colors.hideBackground }]} onPress={onHidePress}>
|
||||
<CustomIcon
|
||||
size={isCondensed ? CONDENSED_ICON_SIZE : EXPANDED_ICON_SIZE}
|
||||
name='unread-on-top-disabled'
|
||||
color={colors.buttonText}
|
||||
/>
|
||||
</RectButton>
|
||||
</Animated.View>
|
||||
</View>
|
||||
);
|
||||
});
|
||||
|
|
|
@ -1,11 +1,11 @@
|
|||
import React from 'react';
|
||||
import { View } from 'react-native';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import Avatar from '../Avatar';
|
||||
import { DisplayMode } from '../../lib/constants';
|
||||
import TypeIcon from './TypeIcon';
|
||||
import styles from './styles';
|
||||
import { IIconOrAvatar } from './interfaces';
|
||||
|
||||
const IconOrAvatar = ({
|
||||
avatar,
|
||||
|
@ -17,10 +17,9 @@ const IconOrAvatar = ({
|
|||
isGroupChat,
|
||||
teamMain,
|
||||
showLastMessage,
|
||||
theme,
|
||||
displayMode,
|
||||
sourceType
|
||||
}) => {
|
||||
}: IIconOrAvatar): React.ReactElement | null => {
|
||||
if (showAvatar) {
|
||||
return (
|
||||
<Avatar text={avatar} size={displayMode === DisplayMode.Condensed ? 36 : 48} type={type} style={styles.avatar} rid={rid} />
|
||||
|
@ -35,7 +34,6 @@ const IconOrAvatar = ({
|
|||
prid={prid}
|
||||
status={status}
|
||||
isGroupChat={isGroupChat}
|
||||
theme={theme}
|
||||
teamMain={teamMain}
|
||||
size={24}
|
||||
style={{ marginRight: 12 }}
|
||||
|
@ -48,18 +46,4 @@ const IconOrAvatar = ({
|
|||
return null;
|
||||
};
|
||||
|
||||
IconOrAvatar.propTypes = {
|
||||
avatar: PropTypes.string,
|
||||
type: PropTypes.string,
|
||||
theme: PropTypes.string,
|
||||
rid: PropTypes.string,
|
||||
showAvatar: PropTypes.bool,
|
||||
displayMode: PropTypes.string,
|
||||
prid: PropTypes.string,
|
||||
status: PropTypes.string,
|
||||
isGroupChat: PropTypes.bool,
|
||||
teamMain: PropTypes.bool,
|
||||
showLastMessage: PropTypes.bool
|
||||
};
|
||||
|
||||
export default IconOrAvatar;
|
|
@ -4,8 +4,9 @@ import { dequal } from 'dequal';
|
|||
import I18n from '../../i18n';
|
||||
import styles from './styles';
|
||||
import { MarkdownPreview } from '../markdown';
|
||||
import { E2E_MESSAGE_TYPE, E2E_STATUS, themes } from '../../lib/constants';
|
||||
import { E2E_MESSAGE_TYPE, E2E_STATUS } from '../../lib/constants';
|
||||
import { ILastMessageProps } from './interfaces';
|
||||
import { useTheme } from '../../theme';
|
||||
|
||||
const formatMsg = ({ lastMessage, type, showLastMessage, username, useRealName }: Partial<ILastMessageProps>) => {
|
||||
if (!showLastMessage) {
|
||||
|
@ -46,8 +47,9 @@ const formatMsg = ({ lastMessage, type, showLastMessage, username, useRealName }
|
|||
|
||||
const arePropsEqual = (oldProps: any, newProps: any) => dequal(oldProps, newProps);
|
||||
|
||||
const LastMessage = React.memo(
|
||||
({ lastMessage, type, showLastMessage, username, alert, useRealName, theme }: ILastMessageProps) => (
|
||||
const LastMessage = React.memo(({ lastMessage, type, showLastMessage, username, alert, useRealName }: ILastMessageProps) => {
|
||||
const { colors } = useTheme();
|
||||
return (
|
||||
<MarkdownPreview
|
||||
msg={formatMsg({
|
||||
lastMessage,
|
||||
|
@ -56,12 +58,11 @@ const LastMessage = React.memo(
|
|||
username,
|
||||
useRealName
|
||||
})}
|
||||
style={[styles.markdownText, { color: alert ? themes[theme].bodyText : themes[theme].auxiliaryText }]}
|
||||
style={[styles.markdownText, { color: alert ? colors.bodyText : colors.auxiliaryText }]}
|
||||
numberOfLines={2}
|
||||
testID='room-item-last-message'
|
||||
/>
|
||||
),
|
||||
arePropsEqual
|
||||
);
|
||||
);
|
||||
}, arePropsEqual);
|
||||
|
||||
export default LastMessage;
|
||||
|
|
|
@ -25,7 +25,6 @@ const RoomItem = ({
|
|||
showLastMessage,
|
||||
status = 'offline',
|
||||
useRealName,
|
||||
theme,
|
||||
isFocused,
|
||||
isGroupChat,
|
||||
isRead,
|
||||
|
@ -52,7 +51,8 @@ const RoomItem = ({
|
|||
autoJoin,
|
||||
showAvatar,
|
||||
displayMode,
|
||||
sourceType
|
||||
sourceType,
|
||||
hideMentionStatus
|
||||
}: IRoomItemProps) => (
|
||||
<Touchable
|
||||
onPress={onPress}
|
||||
|
@ -66,15 +66,13 @@ const RoomItem = ({
|
|||
hideChannel={hideChannel}
|
||||
testID={testID}
|
||||
type={type}
|
||||
theme={theme}
|
||||
isFocused={isFocused}
|
||||
isFocused={!!isFocused}
|
||||
swipeEnabled={swipeEnabled}
|
||||
displayMode={displayMode}>
|
||||
<Wrapper
|
||||
accessibilityLabel={accessibilityLabel}
|
||||
avatar={avatar}
|
||||
type={type}
|
||||
theme={theme}
|
||||
rid={rid}
|
||||
prid={prid}
|
||||
status={status}
|
||||
|
@ -82,7 +80,7 @@ const RoomItem = ({
|
|||
teamMain={teamMain}
|
||||
displayMode={displayMode}
|
||||
showAvatar={showAvatar}
|
||||
showLastMessage={showLastMessage}
|
||||
showLastMessage={!!showLastMessage}
|
||||
sourceType={sourceType}>
|
||||
{showLastMessage && displayMode === DisplayMode.Expanded ? (
|
||||
<>
|
||||
|
@ -97,19 +95,18 @@ const RoomItem = ({
|
|||
sourceType={sourceType}
|
||||
/>
|
||||
) : null}
|
||||
<Title name={name} theme={theme} hideUnreadStatus={hideUnreadStatus} alert={alert} />
|
||||
<Title name={name} hideUnreadStatus={hideUnreadStatus} alert={alert} />
|
||||
{autoJoin ? <Tag testID='auto-join-tag' name={I18n.t('Auto-join')} /> : null}
|
||||
<UpdatedAt date={date} theme={theme} hideUnreadStatus={hideUnreadStatus} alert={alert} />
|
||||
<UpdatedAt date={date} hideUnreadStatus={hideUnreadStatus} alert={alert} />
|
||||
</View>
|
||||
<View style={styles.row}>
|
||||
<LastMessage
|
||||
lastMessage={lastMessage}
|
||||
type={type}
|
||||
showLastMessage={showLastMessage}
|
||||
username={username}
|
||||
username={username || ''}
|
||||
alert={alert && !hideUnreadStatus}
|
||||
useRealName={useRealName}
|
||||
theme={theme}
|
||||
/>
|
||||
<UnreadBadge
|
||||
unread={unread}
|
||||
|
@ -118,6 +115,8 @@ const RoomItem = ({
|
|||
tunread={tunread}
|
||||
tunreadUser={tunreadUser}
|
||||
tunreadGroup={tunreadGroup}
|
||||
hideMentionStatus={hideMentionStatus}
|
||||
hideUnreadStatus={hideUnreadStatus}
|
||||
/>
|
||||
</View>
|
||||
</>
|
||||
|
@ -133,10 +132,10 @@ const RoomItem = ({
|
|||
style={{ marginRight: 8 }}
|
||||
sourceType={sourceType}
|
||||
/>
|
||||
<Title name={name} theme={theme} hideUnreadStatus={hideUnreadStatus} alert={alert} />
|
||||
<Title name={name} hideUnreadStatus={hideUnreadStatus} alert={alert} />
|
||||
{autoJoin ? <Tag name={I18n.t('Auto-join')} /> : null}
|
||||
<View style={styles.wrapUpdatedAndBadge}>
|
||||
<UpdatedAt date={date} theme={theme} hideUnreadStatus={hideUnreadStatus} alert={alert} />
|
||||
<UpdatedAt date={date} hideUnreadStatus={hideUnreadStatus} alert={alert} />
|
||||
<UnreadBadge
|
||||
unread={unread}
|
||||
userMentions={userMentions}
|
||||
|
@ -144,6 +143,8 @@ const RoomItem = ({
|
|||
tunread={tunread}
|
||||
tunreadUser={tunreadUser}
|
||||
tunreadGroup={tunreadGroup}
|
||||
hideMentionStatus={hideMentionStatus}
|
||||
hideUnreadStatus={hideUnreadStatus}
|
||||
/>
|
||||
</View>
|
||||
</View>
|
||||
|
|
|
@ -2,16 +2,19 @@ import React from 'react';
|
|||
import { Text } from 'react-native';
|
||||
|
||||
import styles from './styles';
|
||||
import { themes } from '../../lib/constants';
|
||||
import { ITitleProps } from './interfaces';
|
||||
import { useTheme } from '../../theme';
|
||||
|
||||
const Title = React.memo(({ name, theme, hideUnreadStatus, alert }: ITitleProps) => (
|
||||
<Text
|
||||
style={[styles.title, alert && !hideUnreadStatus && styles.alert, { color: themes[theme].titleText }]}
|
||||
ellipsizeMode='tail'
|
||||
numberOfLines={1}>
|
||||
{name}
|
||||
</Text>
|
||||
));
|
||||
const Title = React.memo(({ name, hideUnreadStatus, alert }: ITitleProps) => {
|
||||
const { colors } = useTheme();
|
||||
return (
|
||||
<Text
|
||||
style={[styles.title, alert && !hideUnreadStatus && styles.alert, { color: colors.titleText }]}
|
||||
ellipsizeMode='tail'
|
||||
numberOfLines={1}>
|
||||
{name}
|
||||
</Text>
|
||||
);
|
||||
});
|
||||
|
||||
export default Title;
|
||||
|
|
|
@ -1,207 +1,98 @@
|
|||
import React from 'react';
|
||||
import { Animated } from 'react-native';
|
||||
import Animated, {
|
||||
useAnimatedGestureHandler,
|
||||
useSharedValue,
|
||||
useAnimatedStyle,
|
||||
withSpring,
|
||||
runOnJS
|
||||
} from 'react-native-reanimated';
|
||||
import {
|
||||
GestureEvent,
|
||||
HandlerStateChangeEventPayload,
|
||||
LongPressGestureHandler,
|
||||
PanGestureHandler,
|
||||
PanGestureHandlerEventPayload,
|
||||
State
|
||||
State,
|
||||
HandlerStateChangeEventPayload,
|
||||
PanGestureHandlerEventPayload
|
||||
} from 'react-native-gesture-handler';
|
||||
|
||||
import Touch from '../../utils/touch';
|
||||
import Touch from '../../lib/methods/helpers/touch';
|
||||
import { ACTION_WIDTH, LONG_SWIPE, SMALL_SWIPE } from './styles';
|
||||
import { isRTL } from '../../i18n';
|
||||
import { themes } from '../../lib/constants';
|
||||
import { LeftActions, RightActions } from './Actions';
|
||||
import { ITouchableProps } from './interfaces';
|
||||
import { useTheme } from '../../theme';
|
||||
import I18n from '../../i18n';
|
||||
|
||||
class Touchable extends React.Component<ITouchableProps, any> {
|
||||
private dragX: Animated.Value;
|
||||
private rowOffSet: Animated.Value;
|
||||
private reverse: Animated.Value;
|
||||
private transX: Animated.AnimatedAddition;
|
||||
private transXReverse: Animated.AnimatedMultiplication;
|
||||
private _onGestureEvent: (event: GestureEvent<PanGestureHandlerEventPayload>) => void;
|
||||
private _value: number;
|
||||
const Touchable = ({
|
||||
children,
|
||||
type,
|
||||
onPress,
|
||||
onLongPress,
|
||||
testID,
|
||||
width,
|
||||
favorite,
|
||||
isRead,
|
||||
rid,
|
||||
toggleFav,
|
||||
toggleRead,
|
||||
hideChannel,
|
||||
isFocused,
|
||||
swipeEnabled,
|
||||
displayMode
|
||||
}: ITouchableProps): React.ReactElement => {
|
||||
const { theme, colors } = useTheme();
|
||||
|
||||
constructor(props: ITouchableProps) {
|
||||
super(props);
|
||||
this.dragX = new Animated.Value(0);
|
||||
this.rowOffSet = new Animated.Value(0);
|
||||
this.reverse = new Animated.Value(isRTL() ? -1 : 1);
|
||||
this.transX = Animated.add(this.rowOffSet, this.dragX);
|
||||
this.transXReverse = Animated.multiply(this.transX, this.reverse);
|
||||
this.state = {
|
||||
rowState: 0 // 0: closed, 1: right opened, -1: left opened
|
||||
};
|
||||
this._onGestureEvent = Animated.event([{ nativeEvent: { translationX: this.dragX } }], { useNativeDriver: true });
|
||||
this._value = 0;
|
||||
}
|
||||
const rowOffSet = useSharedValue(0);
|
||||
const transX = useSharedValue(0);
|
||||
const rowState = useSharedValue(0); // 0: closed, 1: right opened, -1: left opened
|
||||
let _value = 0;
|
||||
|
||||
_onHandlerStateChange = ({ nativeEvent }: { nativeEvent: HandlerStateChangeEventPayload & PanGestureHandlerEventPayload }) => {
|
||||
if (nativeEvent.oldState === State.ACTIVE) {
|
||||
this._handleRelease(nativeEvent);
|
||||
}
|
||||
const close = () => {
|
||||
rowState.value = 0;
|
||||
transX.value = withSpring(0, { overshootClamping: true });
|
||||
rowOffSet.value = 0;
|
||||
};
|
||||
|
||||
onLongPressHandlerStateChange = ({ nativeEvent }: { nativeEvent: HandlerStateChangeEventPayload }) => {
|
||||
if (nativeEvent.state === State.ACTIVE) {
|
||||
this.onLongPress();
|
||||
}
|
||||
};
|
||||
|
||||
_handleRelease = (nativeEvent: PanGestureHandlerEventPayload) => {
|
||||
const { translationX } = nativeEvent;
|
||||
const { rowState } = this.state;
|
||||
this._value += translationX;
|
||||
|
||||
let toValue = 0;
|
||||
if (rowState === 0) {
|
||||
// if no option is opened
|
||||
if (translationX > 0 && translationX < LONG_SWIPE) {
|
||||
// open leading option if he swipe right but not enough to trigger action
|
||||
if (isRTL()) {
|
||||
toValue = 2 * ACTION_WIDTH;
|
||||
} else {
|
||||
toValue = ACTION_WIDTH;
|
||||
}
|
||||
this.setState({ rowState: -1 });
|
||||
} else if (translationX >= LONG_SWIPE) {
|
||||
toValue = 0;
|
||||
if (isRTL()) {
|
||||
this.hideChannel();
|
||||
} else {
|
||||
this.toggleRead();
|
||||
}
|
||||
} else if (translationX < 0 && translationX > -LONG_SWIPE) {
|
||||
// open trailing option if he swipe left
|
||||
if (isRTL()) {
|
||||
toValue = -ACTION_WIDTH;
|
||||
} else {
|
||||
toValue = -2 * ACTION_WIDTH;
|
||||
}
|
||||
this.setState({ rowState: 1 });
|
||||
} else if (translationX <= -LONG_SWIPE) {
|
||||
toValue = 0;
|
||||
this.setState({ rowState: 0 });
|
||||
if (isRTL()) {
|
||||
this.toggleRead();
|
||||
} else {
|
||||
this.hideChannel();
|
||||
}
|
||||
} else {
|
||||
toValue = 0;
|
||||
}
|
||||
}
|
||||
|
||||
if (rowState === -1) {
|
||||
// if left option is opened
|
||||
if (this._value < SMALL_SWIPE) {
|
||||
toValue = 0;
|
||||
this.setState({ rowState: 0 });
|
||||
} else if (this._value > LONG_SWIPE) {
|
||||
toValue = 0;
|
||||
this.setState({ rowState: 0 });
|
||||
if (isRTL()) {
|
||||
this.hideChannel();
|
||||
} else {
|
||||
this.toggleRead();
|
||||
}
|
||||
} else if (isRTL()) {
|
||||
toValue = 2 * ACTION_WIDTH;
|
||||
} else {
|
||||
toValue = ACTION_WIDTH;
|
||||
}
|
||||
}
|
||||
|
||||
if (rowState === 1) {
|
||||
// if right option is opened
|
||||
if (this._value > -2 * SMALL_SWIPE) {
|
||||
toValue = 0;
|
||||
this.setState({ rowState: 0 });
|
||||
} else if (this._value < -LONG_SWIPE) {
|
||||
toValue = 0;
|
||||
this.setState({ rowState: 0 });
|
||||
if (isRTL()) {
|
||||
this.toggleRead();
|
||||
} else {
|
||||
this.hideChannel();
|
||||
}
|
||||
} else if (isRTL()) {
|
||||
toValue = -ACTION_WIDTH;
|
||||
} else {
|
||||
toValue = -2 * ACTION_WIDTH;
|
||||
}
|
||||
}
|
||||
this._animateRow(toValue);
|
||||
};
|
||||
|
||||
_animateRow = (toValue: number) => {
|
||||
this.rowOffSet.setValue(this._value);
|
||||
this._value = toValue;
|
||||
this.dragX.setValue(0);
|
||||
Animated.spring(this.rowOffSet, {
|
||||
toValue,
|
||||
bounciness: 0,
|
||||
useNativeDriver: true
|
||||
}).start();
|
||||
};
|
||||
|
||||
close = () => {
|
||||
this.setState({ rowState: 0 });
|
||||
this._animateRow(0);
|
||||
};
|
||||
|
||||
toggleFav = () => {
|
||||
const { toggleFav, rid, favorite } = this.props;
|
||||
const handleToggleFav = () => {
|
||||
if (toggleFav) {
|
||||
toggleFav(rid, favorite);
|
||||
}
|
||||
this.close();
|
||||
close();
|
||||
};
|
||||
|
||||
toggleRead = () => {
|
||||
const { toggleRead, rid, isRead } = this.props;
|
||||
const handleToggleRead = () => {
|
||||
if (toggleRead) {
|
||||
toggleRead(rid, isRead);
|
||||
}
|
||||
};
|
||||
|
||||
hideChannel = () => {
|
||||
const { hideChannel, rid, type } = this.props;
|
||||
const handleHideChannel = () => {
|
||||
if (hideChannel) {
|
||||
hideChannel(rid, type);
|
||||
}
|
||||
};
|
||||
|
||||
onToggleReadPress = () => {
|
||||
this.toggleRead();
|
||||
this.close();
|
||||
const onToggleReadPress = () => {
|
||||
handleToggleRead();
|
||||
close();
|
||||
};
|
||||
|
||||
onHidePress = () => {
|
||||
this.hideChannel();
|
||||
this.close();
|
||||
const onHidePress = () => {
|
||||
handleHideChannel();
|
||||
close();
|
||||
};
|
||||
|
||||
onPress = () => {
|
||||
const { rowState } = this.state;
|
||||
if (rowState !== 0) {
|
||||
this.close();
|
||||
const handlePress = () => {
|
||||
if (rowState.value !== 0) {
|
||||
close();
|
||||
return;
|
||||
}
|
||||
const { onPress } = this.props;
|
||||
if (onPress) {
|
||||
onPress();
|
||||
}
|
||||
};
|
||||
|
||||
onLongPress = () => {
|
||||
const { rowState } = this.state;
|
||||
const { onLongPress } = this.props;
|
||||
if (rowState !== 0) {
|
||||
this.close();
|
||||
const handleLongPress = () => {
|
||||
if (rowState.value !== 0) {
|
||||
close();
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -210,55 +101,139 @@ class Touchable extends React.Component<ITouchableProps, any> {
|
|||
}
|
||||
};
|
||||
|
||||
render() {
|
||||
const { testID, isRead, width, favorite, children, theme, isFocused, swipeEnabled, displayMode } = this.props;
|
||||
const onLongPressHandlerStateChange = ({ nativeEvent }: { nativeEvent: HandlerStateChangeEventPayload }) => {
|
||||
if (nativeEvent.state === State.ACTIVE) {
|
||||
handleLongPress();
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<LongPressGestureHandler onHandlerStateChange={this.onLongPressHandlerStateChange}>
|
||||
<Animated.View>
|
||||
<PanGestureHandler
|
||||
minDeltaX={20}
|
||||
onGestureEvent={this._onGestureEvent}
|
||||
onHandlerStateChange={this._onHandlerStateChange}
|
||||
enabled={swipeEnabled}>
|
||||
<Animated.View>
|
||||
<LeftActions
|
||||
transX={this.transXReverse}
|
||||
isRead={isRead}
|
||||
width={width}
|
||||
onToggleReadPress={this.onToggleReadPress}
|
||||
const handleRelease = (event: PanGestureHandlerEventPayload) => {
|
||||
const { translationX } = event;
|
||||
_value += translationX;
|
||||
let toValue = 0;
|
||||
if (rowState.value === 0) {
|
||||
// if no option is opened
|
||||
if (translationX > 0 && translationX < LONG_SWIPE) {
|
||||
if (I18n.isRTL) {
|
||||
toValue = 2 * ACTION_WIDTH;
|
||||
} else {
|
||||
toValue = ACTION_WIDTH;
|
||||
}
|
||||
rowState.value = -1;
|
||||
} else if (translationX >= LONG_SWIPE) {
|
||||
toValue = 0;
|
||||
if (I18n.isRTL) {
|
||||
handleHideChannel();
|
||||
} else {
|
||||
handleToggleRead();
|
||||
}
|
||||
} else if (translationX < 0 && translationX > -LONG_SWIPE) {
|
||||
// open trailing option if he swipe left
|
||||
if (I18n.isRTL) {
|
||||
toValue = -ACTION_WIDTH;
|
||||
} else {
|
||||
toValue = -2 * ACTION_WIDTH;
|
||||
}
|
||||
rowState.value = 1;
|
||||
} else if (translationX <= -LONG_SWIPE) {
|
||||
toValue = 0;
|
||||
rowState.value = 1;
|
||||
if (I18n.isRTL) {
|
||||
handleToggleRead();
|
||||
} else {
|
||||
handleHideChannel();
|
||||
}
|
||||
} else {
|
||||
toValue = 0;
|
||||
}
|
||||
} else if (rowState.value === -1) {
|
||||
// if left option is opened
|
||||
if (_value < SMALL_SWIPE) {
|
||||
toValue = 0;
|
||||
rowState.value = 0;
|
||||
} else if (_value > LONG_SWIPE) {
|
||||
toValue = 0;
|
||||
rowState.value = 0;
|
||||
if (I18n.isRTL) {
|
||||
handleHideChannel();
|
||||
} else {
|
||||
handleToggleRead();
|
||||
}
|
||||
} else if (I18n.isRTL) {
|
||||
toValue = 2 * ACTION_WIDTH;
|
||||
} else {
|
||||
toValue = ACTION_WIDTH;
|
||||
}
|
||||
} else if (rowState.value === 1) {
|
||||
// if right option is opened
|
||||
if (_value > -2 * SMALL_SWIPE) {
|
||||
toValue = 0;
|
||||
rowState.value = 0;
|
||||
} else if (_value < -LONG_SWIPE) {
|
||||
if (I18n.isRTL) {
|
||||
handleToggleRead();
|
||||
} else {
|
||||
handleHideChannel();
|
||||
}
|
||||
} else if (I18n.isRTL) {
|
||||
toValue = -ACTION_WIDTH;
|
||||
} else {
|
||||
toValue = -2 * ACTION_WIDTH;
|
||||
}
|
||||
}
|
||||
transX.value = withSpring(toValue, { overshootClamping: true });
|
||||
rowOffSet.value = toValue;
|
||||
_value = toValue;
|
||||
};
|
||||
|
||||
const onGestureEvent = useAnimatedGestureHandler({
|
||||
onActive: event => {
|
||||
transX.value = event.translationX + rowOffSet.value;
|
||||
if (transX.value > 2 * width) transX.value = 2 * width;
|
||||
},
|
||||
onEnd: event => {
|
||||
runOnJS(handleRelease)(event);
|
||||
}
|
||||
});
|
||||
|
||||
const animatedStyles = useAnimatedStyle(() => ({ transform: [{ translateX: transX.value }] }));
|
||||
|
||||
return (
|
||||
<LongPressGestureHandler onHandlerStateChange={onLongPressHandlerStateChange}>
|
||||
<Animated.View>
|
||||
<PanGestureHandler activeOffsetX={[-20, 20]} onGestureEvent={onGestureEvent} enabled={swipeEnabled}>
|
||||
<Animated.View>
|
||||
<LeftActions
|
||||
transX={transX}
|
||||
isRead={isRead}
|
||||
width={width}
|
||||
onToggleReadPress={onToggleReadPress}
|
||||
displayMode={displayMode}
|
||||
/>
|
||||
<RightActions
|
||||
transX={transX}
|
||||
favorite={favorite}
|
||||
width={width}
|
||||
toggleFav={handleToggleFav}
|
||||
onHidePress={onHidePress}
|
||||
displayMode={displayMode}
|
||||
/>
|
||||
<Animated.View style={animatedStyles}>
|
||||
<Touch
|
||||
onPress={handlePress}
|
||||
theme={theme}
|
||||
displayMode={displayMode}
|
||||
/>
|
||||
<RightActions
|
||||
transX={this.transXReverse}
|
||||
favorite={favorite}
|
||||
width={width}
|
||||
toggleFav={this.toggleFav}
|
||||
onHidePress={this.onHidePress}
|
||||
theme={theme}
|
||||
displayMode={displayMode}
|
||||
/>
|
||||
<Animated.View
|
||||
testID={testID}
|
||||
style={{
|
||||
transform: [{ translateX: this.transX }]
|
||||
backgroundColor: isFocused ? colors.chatComponentBackground : colors.backgroundColor
|
||||
}}>
|
||||
<Touch
|
||||
onPress={this.onPress}
|
||||
theme={theme}
|
||||
testID={testID}
|
||||
style={{
|
||||
backgroundColor: isFocused ? themes[theme].chatComponentBackground : themes[theme].backgroundColor
|
||||
}}>
|
||||
{children}
|
||||
</Touch>
|
||||
</Animated.View>
|
||||
{children}
|
||||
</Touch>
|
||||
</Animated.View>
|
||||
</PanGestureHandler>
|
||||
</Animated.View>
|
||||
</LongPressGestureHandler>
|
||||
);
|
||||
}
|
||||
}
|
||||
</Animated.View>
|
||||
</PanGestureHandler>
|
||||
</Animated.View>
|
||||
</LongPressGestureHandler>
|
||||
);
|
||||
};
|
||||
|
||||
export default Touchable;
|
||||
|
|
|
@ -2,11 +2,13 @@ import React from 'react';
|
|||
import { Text } from 'react-native';
|
||||
|
||||
import styles from './styles';
|
||||
import { themes } from '../../lib/constants';
|
||||
import { capitalize } from '../../utils/room';
|
||||
import { capitalize } from '../../lib/methods/helpers/room';
|
||||
import { IUpdatedAtProps } from './interfaces';
|
||||
import { useTheme } from '../../theme';
|
||||
|
||||
const UpdatedAt = React.memo(({ date, hideUnreadStatus, alert }: IUpdatedAtProps) => {
|
||||
const { colors } = useTheme();
|
||||
|
||||
const UpdatedAt = React.memo(({ date, theme, hideUnreadStatus, alert }: IUpdatedAtProps) => {
|
||||
if (!date) {
|
||||
return null;
|
||||
}
|
||||
|
@ -15,13 +17,13 @@ const UpdatedAt = React.memo(({ date, theme, hideUnreadStatus, alert }: IUpdated
|
|||
style={[
|
||||
styles.date,
|
||||
{
|
||||
color: themes[theme].auxiliaryText
|
||||
color: colors.auxiliaryText
|
||||
},
|
||||
alert &&
|
||||
!hideUnreadStatus && [
|
||||
styles.updateAlert,
|
||||
{
|
||||
color: themes[theme].tintColor
|
||||
color: colors.tintColor
|
||||
}
|
||||
]
|
||||
]}
|
||||
|
|
|
@ -1,26 +1,30 @@
|
|||
import React from 'react';
|
||||
import { View } from 'react-native';
|
||||
|
||||
import { DisplayMode, themes } from '../../lib/constants';
|
||||
import { DisplayMode } from '../../lib/constants';
|
||||
import { useTheme } from '../../theme';
|
||||
import IconOrAvatar from './IconOrAvatar';
|
||||
import { IWrapperProps } from './interfaces';
|
||||
import styles from './styles';
|
||||
|
||||
const Wrapper = ({ accessibilityLabel, theme, children, displayMode, ...props }: IWrapperProps): React.ReactElement => (
|
||||
<View
|
||||
style={[styles.container, displayMode === DisplayMode.Condensed && styles.containerCondensed]}
|
||||
accessibilityLabel={accessibilityLabel}>
|
||||
<IconOrAvatar theme={theme} displayMode={displayMode} {...props} />
|
||||
const Wrapper = ({ accessibilityLabel, children, displayMode, ...props }: IWrapperProps): React.ReactElement => {
|
||||
const { colors } = useTheme();
|
||||
return (
|
||||
<View
|
||||
style={[
|
||||
styles.centerContainer,
|
||||
{
|
||||
borderColor: themes[theme].separatorColor
|
||||
}
|
||||
]}>
|
||||
{children}
|
||||
style={[styles.container, displayMode === DisplayMode.Condensed && styles.containerCondensed]}
|
||||
accessibilityLabel={accessibilityLabel}>
|
||||
<IconOrAvatar displayMode={displayMode} {...props} />
|
||||
<View
|
||||
style={[
|
||||
styles.centerContainer,
|
||||
{
|
||||
borderColor: colors.separatorColor
|
||||
}
|
||||
]}>
|
||||
{children}
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
);
|
||||
};
|
||||
|
||||
export default Wrapper;
|
||||
|
|
|
@ -1,172 +1,115 @@
|
|||
import React from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import React, { useEffect, useReducer, useRef } from 'react';
|
||||
import { Subscription } from 'rxjs';
|
||||
|
||||
import I18n from '../../i18n';
|
||||
import { ROW_HEIGHT, ROW_HEIGHT_CONDENSED } from './styles';
|
||||
import { formatDate } from '../../utils/room';
|
||||
import RoomItem from './RoomItem';
|
||||
import { ISubscription, TUserStatus } from '../../definitions';
|
||||
import { useAppSelector } from '../../lib/hooks';
|
||||
import { getUserPresence } from '../../lib/methods';
|
||||
import { isGroupChat } from '../../lib/methods/helpers';
|
||||
import { formatDate } from '../../lib/methods/helpers/room';
|
||||
import { IRoomItemContainerProps } from './interfaces';
|
||||
import RoomItem from './RoomItem';
|
||||
import { ROW_HEIGHT, ROW_HEIGHT_CONDENSED } from './styles';
|
||||
|
||||
export { ROW_HEIGHT, ROW_HEIGHT_CONDENSED };
|
||||
|
||||
const attrs = [
|
||||
'width',
|
||||
'status',
|
||||
'connected',
|
||||
'theme',
|
||||
'isFocused',
|
||||
'forceUpdate',
|
||||
'showLastMessage',
|
||||
'autoJoin',
|
||||
'showAvatar',
|
||||
'displayMode'
|
||||
];
|
||||
const attrs = ['width', 'isFocused', 'showLastMessage', 'autoJoin', 'showAvatar', 'displayMode'];
|
||||
|
||||
class RoomItemContainer extends React.Component<IRoomItemContainerProps, any> {
|
||||
private roomSubscription: ISubscription | undefined;
|
||||
|
||||
static defaultProps: Partial<IRoomItemContainerProps> = {
|
||||
status: 'offline',
|
||||
getUserPresence: () => {},
|
||||
getRoomTitle: () => 'title',
|
||||
getRoomAvatar: () => '',
|
||||
getIsGroupChat: () => false,
|
||||
getIsRead: () => false,
|
||||
swipeEnabled: true
|
||||
};
|
||||
|
||||
constructor(props: IRoomItemContainerProps) {
|
||||
super(props);
|
||||
this.init();
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
const { connected, getUserPresence, id } = this.props;
|
||||
if (connected && this.isDirect) {
|
||||
getUserPresence(id);
|
||||
}
|
||||
}
|
||||
|
||||
shouldComponentUpdate(nextProps: IRoomItemContainerProps) {
|
||||
const { props } = this;
|
||||
return !attrs.every(key => props[key] === nextProps[key]);
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps: IRoomItemContainerProps) {
|
||||
const { connected, getUserPresence, id } = this.props;
|
||||
if (prevProps.connected !== connected && connected && this.isDirect) {
|
||||
getUserPresence(id);
|
||||
}
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
if (this.roomSubscription?.unsubscribe) {
|
||||
this.roomSubscription.unsubscribe();
|
||||
}
|
||||
}
|
||||
|
||||
get isGroupChat() {
|
||||
const { item, getIsGroupChat } = this.props;
|
||||
return getIsGroupChat(item);
|
||||
}
|
||||
|
||||
get isDirect() {
|
||||
const {
|
||||
item: { t },
|
||||
id
|
||||
} = this.props;
|
||||
return t === 'd' && id && !this.isGroupChat;
|
||||
}
|
||||
|
||||
init = () => {
|
||||
const { item } = this.props;
|
||||
if (item?.observe) {
|
||||
const observable = item.observe();
|
||||
this.roomSubscription = observable?.subscribe?.(() => {
|
||||
this.forceUpdate();
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
onPress = () => {
|
||||
const { item, onPress } = this.props;
|
||||
return onPress(item);
|
||||
};
|
||||
|
||||
onLongPress = () => {
|
||||
const { item, onLongPress } = this.props;
|
||||
if (onLongPress) {
|
||||
return onLongPress(item);
|
||||
}
|
||||
};
|
||||
|
||||
render() {
|
||||
const {
|
||||
item,
|
||||
getRoomTitle,
|
||||
getRoomAvatar,
|
||||
getIsRead,
|
||||
width,
|
||||
toggleFav,
|
||||
toggleRead,
|
||||
hideChannel,
|
||||
theme,
|
||||
isFocused,
|
||||
status,
|
||||
showLastMessage,
|
||||
username,
|
||||
useRealName,
|
||||
swipeEnabled,
|
||||
autoJoin,
|
||||
showAvatar,
|
||||
displayMode
|
||||
} = this.props;
|
||||
const RoomItemContainer = React.memo(
|
||||
({
|
||||
item,
|
||||
id,
|
||||
onPress,
|
||||
onLongPress,
|
||||
width,
|
||||
toggleFav,
|
||||
toggleRead,
|
||||
hideChannel,
|
||||
isFocused,
|
||||
showLastMessage,
|
||||
username,
|
||||
useRealName,
|
||||
autoJoin,
|
||||
showAvatar,
|
||||
displayMode,
|
||||
getRoomTitle = () => 'title',
|
||||
getRoomAvatar = () => '',
|
||||
getIsRead = () => false,
|
||||
swipeEnabled = true
|
||||
}: IRoomItemContainerProps) => {
|
||||
const name = getRoomTitle(item);
|
||||
const testID = `rooms-list-view-item-${name}`;
|
||||
const avatar = getRoomAvatar(item);
|
||||
const isRead = getIsRead(item);
|
||||
const date = item.roomUpdatedAt && formatDate(item.roomUpdatedAt);
|
||||
const alert = item.alert || item.tunread?.length;
|
||||
const connected = useAppSelector(state => state.meteor.connected);
|
||||
const userStatus = useAppSelector(state => state.activeUsers[id || '']?.status);
|
||||
const [_, forceUpdate] = useReducer(x => x + 1, 1);
|
||||
const roomSubscription = useRef<Subscription | null>(null);
|
||||
|
||||
let accessibilityLabel = name;
|
||||
useEffect(() => {
|
||||
const init = () => {
|
||||
if (item?.observe) {
|
||||
const observable = item.observe();
|
||||
roomSubscription.current = observable?.subscribe?.(() => {
|
||||
if (_) forceUpdate();
|
||||
});
|
||||
}
|
||||
};
|
||||
init();
|
||||
|
||||
return () => roomSubscription.current?.unsubscribe();
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
const isDirect = !!(item.t === 'd' && id && !isGroupChat(item));
|
||||
if (connected && isDirect) {
|
||||
getUserPresence(id);
|
||||
}
|
||||
}, [connected]);
|
||||
|
||||
const handleOnPress = () => onPress(item);
|
||||
|
||||
const handleOnLongPress = () => onLongPress && onLongPress(item);
|
||||
|
||||
let accessibilityLabel = '';
|
||||
if (item.unread === 1) {
|
||||
accessibilityLabel += `, ${item.unread} ${I18n.t('alert')}`;
|
||||
accessibilityLabel = `, ${item.unread} ${I18n.t('alert')}`;
|
||||
} else if (item.unread > 1) {
|
||||
accessibilityLabel += `, ${item.unread} ${I18n.t('alerts')}`;
|
||||
accessibilityLabel = `, ${item.unread} ${I18n.t('alerts')}`;
|
||||
}
|
||||
|
||||
if (item.userMentions > 0) {
|
||||
accessibilityLabel += `, ${I18n.t('you_were_mentioned')}`;
|
||||
accessibilityLabel = `, ${I18n.t('you_were_mentioned')}`;
|
||||
}
|
||||
if (date) {
|
||||
accessibilityLabel = `, ${I18n.t('last_message')} ${date}`;
|
||||
}
|
||||
|
||||
if (date) {
|
||||
accessibilityLabel += `, ${I18n.t('last_message')} ${date}`;
|
||||
}
|
||||
const status = item.t === 'l' ? item.visitor?.status || item.v?.status : userStatus;
|
||||
|
||||
return (
|
||||
<RoomItem
|
||||
name={name}
|
||||
avatar={avatar}
|
||||
isGroupChat={this.isGroupChat}
|
||||
isGroupChat={isGroupChat(item)}
|
||||
isRead={isRead}
|
||||
onPress={this.onPress}
|
||||
onLongPress={this.onLongPress}
|
||||
onPress={handleOnPress}
|
||||
onLongPress={handleOnLongPress}
|
||||
date={date}
|
||||
accessibilityLabel={accessibilityLabel}
|
||||
width={width}
|
||||
favorite={item.f}
|
||||
toggleFav={toggleFav}
|
||||
rid={item.rid}
|
||||
toggleFav={toggleFav}
|
||||
toggleRead={toggleRead}
|
||||
hideChannel={hideChannel}
|
||||
testID={testID}
|
||||
type={item.t}
|
||||
theme={theme}
|
||||
isFocused={isFocused}
|
||||
prid={item.prid}
|
||||
status={status}
|
||||
hideUnreadStatus={item.hideUnreadStatus}
|
||||
hideMentionStatus={item.hideMentionStatus}
|
||||
alert={alert}
|
||||
lastMessage={item.lastMessage}
|
||||
showLastMessage={showLastMessage}
|
||||
|
@ -186,23 +129,8 @@ class RoomItemContainer extends React.Component<IRoomItemContainerProps, any> {
|
|||
sourceType={item.source}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
},
|
||||
(props, nextProps) => attrs.every(key => props[key] === nextProps[key])
|
||||
);
|
||||
|
||||
const mapStateToProps = (state: any, ownProps: any) => {
|
||||
let status = 'loading';
|
||||
const { id, type, visitor = {} } = ownProps;
|
||||
if (state.meteor.connected) {
|
||||
if (type === 'd') {
|
||||
status = state.activeUsers[id]?.status || 'loading';
|
||||
} else if (type === 'l' && visitor?.status) {
|
||||
({ status } = visitor);
|
||||
}
|
||||
}
|
||||
return {
|
||||
connected: state.meteor.connected,
|
||||
status: status as TUserStatus
|
||||
};
|
||||
};
|
||||
|
||||
export default connect(mapStateToProps)(RoomItemContainer);
|
||||
export default RoomItemContainer;
|
||||
|
|
|
@ -1,12 +1,11 @@
|
|||
import React from 'react';
|
||||
import { Animated } from 'react-native';
|
||||
import Animated from 'react-native-reanimated';
|
||||
|
||||
import { TSupportedThemes } from '../../theme';
|
||||
import { TUserStatus, ILastMessage, SubscriptionType, IOmnichannelSource } from '../../definitions';
|
||||
|
||||
export interface ILeftActionsProps {
|
||||
theme: TSupportedThemes;
|
||||
transX: Animated.AnimatedAddition | Animated.AnimatedMultiplication;
|
||||
transX: Animated.SharedValue<number>;
|
||||
isRead: boolean;
|
||||
width: number;
|
||||
onToggleReadPress(): void;
|
||||
|
@ -14,8 +13,7 @@ export interface ILeftActionsProps {
|
|||
}
|
||||
|
||||
export interface IRightActionsProps {
|
||||
theme: TSupportedThemes;
|
||||
transX: Animated.AnimatedAddition | Animated.AnimatedMultiplication;
|
||||
transX: Animated.SharedValue<number>;
|
||||
favorite: boolean;
|
||||
width: number;
|
||||
toggleFav(): void;
|
||||
|
@ -25,14 +23,12 @@ export interface IRightActionsProps {
|
|||
|
||||
export interface ITitleProps {
|
||||
name: string;
|
||||
theme: TSupportedThemes;
|
||||
hideUnreadStatus: boolean;
|
||||
alert: boolean;
|
||||
}
|
||||
|
||||
export interface IUpdatedAtProps {
|
||||
date: string;
|
||||
theme: TSupportedThemes;
|
||||
hideUnreadStatus: boolean;
|
||||
alert: boolean;
|
||||
}
|
||||
|
@ -41,13 +37,12 @@ export interface IWrapperProps {
|
|||
accessibilityLabel: string;
|
||||
avatar: string;
|
||||
type: string;
|
||||
theme: TSupportedThemes;
|
||||
rid: string;
|
||||
children: React.ReactElement;
|
||||
displayMode: string;
|
||||
prid: string;
|
||||
showLastMessage: boolean;
|
||||
status: string;
|
||||
status: TUserStatus;
|
||||
isGroupChat: boolean;
|
||||
teamMain: boolean;
|
||||
showAvatar: boolean;
|
||||
|
@ -66,48 +61,43 @@ export interface ITypeIconProps {
|
|||
sourceType: IOmnichannelSource;
|
||||
}
|
||||
|
||||
export interface IRoomItemContainerProps {
|
||||
[key: string]: string | boolean | Function | number;
|
||||
item: any;
|
||||
showLastMessage: boolean;
|
||||
id: string;
|
||||
onPress: (item: any) => void;
|
||||
onLongPress: (item: any) => Promise<void>;
|
||||
username: string;
|
||||
width: number;
|
||||
status: TUserStatus;
|
||||
toggleFav(): void;
|
||||
toggleRead(): void;
|
||||
hideChannel(): void;
|
||||
useRealName: boolean;
|
||||
getUserPresence: (uid: string) => void;
|
||||
connected: boolean;
|
||||
theme: TSupportedThemes;
|
||||
isFocused: boolean;
|
||||
getRoomTitle: (item: any) => string;
|
||||
getRoomAvatar: (item: any) => string;
|
||||
getIsGroupChat: (item: any) => boolean;
|
||||
getIsRead: (item: any) => boolean;
|
||||
swipeEnabled: boolean;
|
||||
autoJoin: boolean;
|
||||
showAvatar: boolean;
|
||||
displayMode: string;
|
||||
interface IRoomItemTouchables {
|
||||
toggleFav?: (rid: string, favorite: boolean) => Promise<void>;
|
||||
toggleRead?: (rid: string, tIsRead: boolean) => Promise<void>;
|
||||
hideChannel?: (rid: string, type: SubscriptionType) => Promise<void>;
|
||||
onPress: (item?: any) => void;
|
||||
onLongPress?: (item?: any) => void;
|
||||
}
|
||||
|
||||
export interface IRoomItemProps {
|
||||
interface IBaseRoomItem extends IRoomItemTouchables {
|
||||
[key: string]: any;
|
||||
showLastMessage?: boolean;
|
||||
useRealName: boolean;
|
||||
isFocused?: boolean;
|
||||
displayMode: string;
|
||||
showAvatar: boolean;
|
||||
swipeEnabled: boolean;
|
||||
autoJoin?: boolean;
|
||||
width: number;
|
||||
username?: string;
|
||||
}
|
||||
|
||||
export interface IRoomItemContainerProps extends IBaseRoomItem {
|
||||
item: any;
|
||||
id?: string;
|
||||
getRoomTitle: (item: any) => string;
|
||||
getRoomAvatar: (item: any) => string;
|
||||
getIsRead?: (item: any) => boolean;
|
||||
}
|
||||
|
||||
export interface IRoomItemProps extends IBaseRoomItem {
|
||||
rid: string;
|
||||
type: SubscriptionType;
|
||||
prid: string;
|
||||
name: string;
|
||||
avatar: string;
|
||||
showLastMessage: boolean;
|
||||
username: string;
|
||||
testID: string;
|
||||
width: number;
|
||||
status: TUserStatus;
|
||||
useRealName: boolean;
|
||||
theme: TSupportedThemes;
|
||||
isFocused: boolean;
|
||||
isGroupChat: boolean;
|
||||
isRead: boolean;
|
||||
teamMain: boolean;
|
||||
|
@ -123,21 +113,12 @@ export interface IRoomItemProps {
|
|||
tunread: [];
|
||||
tunreadUser: [];
|
||||
tunreadGroup: [];
|
||||
swipeEnabled: boolean;
|
||||
toggleFav(): void;
|
||||
toggleRead(): void;
|
||||
onPress(): void;
|
||||
onLongPress(): void;
|
||||
hideChannel(): void;
|
||||
autoJoin: boolean;
|
||||
size?: number;
|
||||
showAvatar: boolean;
|
||||
displayMode: string;
|
||||
sourceType: IOmnichannelSource;
|
||||
hideMentionStatus?: boolean;
|
||||
}
|
||||
|
||||
export interface ILastMessageProps {
|
||||
theme: TSupportedThemes;
|
||||
lastMessage: ILastMessage;
|
||||
type: SubscriptionType;
|
||||
showLastMessage: boolean;
|
||||
|
@ -146,21 +127,29 @@ export interface ILastMessageProps {
|
|||
alert: boolean;
|
||||
}
|
||||
|
||||
export interface ITouchableProps {
|
||||
export interface ITouchableProps extends IRoomItemTouchables {
|
||||
children: JSX.Element;
|
||||
type: string;
|
||||
onPress(): void;
|
||||
onLongPress(): void;
|
||||
type: SubscriptionType;
|
||||
testID: string;
|
||||
width: number;
|
||||
favorite: boolean;
|
||||
isRead: boolean;
|
||||
rid: string;
|
||||
toggleFav: Function;
|
||||
toggleRead: Function;
|
||||
hideChannel: Function;
|
||||
theme: TSupportedThemes;
|
||||
isFocused: boolean;
|
||||
swipeEnabled: boolean;
|
||||
displayMode: string;
|
||||
}
|
||||
|
||||
export interface IIconOrAvatar {
|
||||
avatar: string;
|
||||
type: string;
|
||||
rid: string;
|
||||
showAvatar: boolean;
|
||||
displayMode: string;
|
||||
prid: string;
|
||||
status: TUserStatus;
|
||||
isGroupChat: boolean;
|
||||
teamMain: boolean;
|
||||
showLastMessage: boolean;
|
||||
sourceType: IOmnichannelSource;
|
||||
}
|
||||
|
|
|
@ -6,7 +6,7 @@ export const ROW_HEIGHT = 75 * PixelRatio.getFontScale();
|
|||
export const ROW_HEIGHT_CONDENSED = 60 * PixelRatio.getFontScale();
|
||||
export const ACTION_WIDTH = 80;
|
||||
export const SMALL_SWIPE = ACTION_WIDTH / 2;
|
||||
export const LONG_SWIPE = ACTION_WIDTH * 3;
|
||||
export const LONG_SWIPE = ACTION_WIDTH * 2.5;
|
||||
|
||||
export default StyleSheet.create({
|
||||
flex: {
|
||||
|
|
|
@ -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 />);
|
|
@ -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');
|
||||
});
|
||||
});
|
|
@ -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\\":[\\"\\"]}]}]}]}"`;
|
|
@ -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;
|
|
@ -5,8 +5,8 @@ import I18n from '../i18n';
|
|||
import { useTheme } from '../theme';
|
||||
import sharedStyles from '../views/Styles';
|
||||
import { themes } from '../lib/constants';
|
||||
import TextInput from './TextInput';
|
||||
import { isIOS, isTablet } from '../utils/deviceInfo';
|
||||
import { TextInput } from './TextInput';
|
||||
import { isIOS, isTablet } from '../lib/methods/helpers';
|
||||
import { useOrientation } from '../dimensions';
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
|
@ -39,7 +39,6 @@ const SearchHeader = ({ onSearchChangeText, testID }: ISearchHeaderProps): JSX.E
|
|||
style={[styles.title, isLight && { color: themes[theme].headerTitleColor }, { fontSize: titleFontSize }]}
|
||||
placeholder={I18n.t('Search')}
|
||||
onChangeText={onSearchChangeText}
|
||||
theme={theme}
|
||||
testID={testID}
|
||||
/>
|
||||
</View>
|
||||
|
|
|
@ -1,13 +1,13 @@
|
|||
import React from 'react';
|
||||
// @ts-ignore // TODO: Remove on react-native update
|
||||
import { Pressable, Text, View } from 'react-native';
|
||||
import FastImage from '@rocket.chat/react-native-fast-image';
|
||||
import FastImage from 'react-native-fast-image';
|
||||
|
||||
import { IServerInfo } from '../../definitions';
|
||||
import Check from '../Check';
|
||||
import styles, { ROW_HEIGHT } from './styles';
|
||||
import { themes } from '../../lib/constants';
|
||||
import { isIOS } from '../../utils/deviceInfo';
|
||||
import { isIOS } from '../../lib/methods/helpers';
|
||||
import { useTheme } from '../../theme';
|
||||
|
||||
export { ROW_HEIGHT };
|
||||
|
|
|
@ -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();
|
||||
});
|
||||
});
|
|
@ -1,13 +1,13 @@
|
|||
import React from 'react';
|
||||
import { StyleProp, StyleSheet, Text, TextInputProps, TextInput as RNTextInput, TextStyle, View, ViewStyle } from 'react-native';
|
||||
import { BottomSheetTextInput } from '@gorhom/bottom-sheet';
|
||||
import React, { useState } from 'react';
|
||||
import { StyleProp, StyleSheet, Text, TextInput as RNTextInput, TextInputProps, TextStyle, View, ViewStyle } from 'react-native';
|
||||
import Touchable from 'react-native-platform-touchable';
|
||||
|
||||
import { useTheme } from '../../theme';
|
||||
import sharedStyles from '../../views/Styles';
|
||||
import TextInput from './index';
|
||||
import { themes } from '../../lib/constants';
|
||||
import { CustomIcon, TIconsName } from '../CustomIcon';
|
||||
import ActivityIndicator from '../ActivityIndicator';
|
||||
import { TSupportedThemes } from '../../theme';
|
||||
import { CustomIcon, TIconsName } from '../CustomIcon';
|
||||
import { TextInput } from './TextInput';
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
error: {
|
||||
|
@ -58,134 +58,118 @@ export interface IRCTextInputProps extends TextInputProps {
|
|||
containerStyle?: StyleProp<ViewStyle>;
|
||||
inputStyle?: StyleProp<TextStyle>;
|
||||
inputRef?: React.Ref<RNTextInput>;
|
||||
testID?: string;
|
||||
iconLeft?: TIconsName;
|
||||
iconRight?: TIconsName;
|
||||
left?: JSX.Element;
|
||||
onIconRightPress?(): void;
|
||||
theme: TSupportedThemes;
|
||||
bottomSheet?: boolean;
|
||||
onClearInput?: () => void;
|
||||
}
|
||||
|
||||
interface IRCTextInputState {
|
||||
showPassword: boolean;
|
||||
}
|
||||
export const FormTextInput = ({
|
||||
label,
|
||||
error,
|
||||
loading,
|
||||
containerStyle,
|
||||
inputStyle,
|
||||
inputRef,
|
||||
iconLeft,
|
||||
iconRight,
|
||||
onClearInput,
|
||||
value,
|
||||
left,
|
||||
testID,
|
||||
secureTextEntry,
|
||||
bottomSheet,
|
||||
placeholder,
|
||||
...inputProps
|
||||
}: IRCTextInputProps): React.ReactElement => {
|
||||
const { colors } = useTheme();
|
||||
const [showPassword, setShowPassword] = useState(false);
|
||||
const showClearInput = onClearInput && value && value.length > 0;
|
||||
const Input = bottomSheet ? BottomSheetTextInput : TextInput;
|
||||
return (
|
||||
<View style={[styles.inputContainer, containerStyle]}>
|
||||
{label ? (
|
||||
<Text style={[styles.label, { color: colors.titleText }, error?.error && { color: colors.dangerColor }]}>{label}</Text>
|
||||
) : null}
|
||||
|
||||
export default class FormTextInput extends React.PureComponent<IRCTextInputProps, IRCTextInputState> {
|
||||
static defaultProps = {
|
||||
error: {},
|
||||
theme: 'light'
|
||||
};
|
||||
|
||||
state = {
|
||||
showPassword: false
|
||||
};
|
||||
|
||||
get iconLeft() {
|
||||
const { testID, iconLeft, theme } = this.props;
|
||||
return iconLeft ? (
|
||||
<CustomIcon
|
||||
name={iconLeft}
|
||||
testID={testID ? `${testID}-icon-left` : undefined}
|
||||
size={20}
|
||||
color={themes[theme].bodyText}
|
||||
style={[styles.iconContainer, styles.iconLeft]}
|
||||
/>
|
||||
) : null;
|
||||
}
|
||||
|
||||
get iconRight() {
|
||||
const { iconRight, onIconRightPress, theme } = this.props;
|
||||
return iconRight ? (
|
||||
<Touchable onPress={onIconRightPress} style={[styles.iconContainer, styles.iconRight]}>
|
||||
<CustomIcon name={iconRight} size={20} color={themes[theme].bodyText} />
|
||||
</Touchable>
|
||||
) : null;
|
||||
}
|
||||
|
||||
get iconPassword() {
|
||||
const { showPassword } = this.state;
|
||||
const { testID, theme } = this.props;
|
||||
return (
|
||||
<Touchable onPress={this.tooglePassword} style={[styles.iconContainer, styles.iconRight]}>
|
||||
<CustomIcon
|
||||
name={showPassword ? 'unread-on-top' : 'unread-on-top-disabled'}
|
||||
testID={testID ? `${testID}-icon-right` : undefined}
|
||||
size={20}
|
||||
color={themes[theme].auxiliaryText}
|
||||
<View style={styles.wrap}>
|
||||
<Input
|
||||
style={[
|
||||
styles.input,
|
||||
iconLeft && styles.inputIconLeft,
|
||||
(secureTextEntry || iconRight) && styles.inputIconRight,
|
||||
{
|
||||
backgroundColor: colors.backgroundColor,
|
||||
borderColor: colors.separatorColor,
|
||||
color: colors.titleText
|
||||
},
|
||||
error?.error && {
|
||||
color: colors.dangerColor,
|
||||
borderColor: colors.dangerColor
|
||||
},
|
||||
inputStyle
|
||||
]}
|
||||
// @ts-ignore ref error
|
||||
ref={inputRef}
|
||||
autoCorrect={false}
|
||||
autoCapitalize='none'
|
||||
underlineColorAndroid='transparent'
|
||||
secureTextEntry={secureTextEntry && !showPassword}
|
||||
testID={testID}
|
||||
accessibilityLabel={placeholder}
|
||||
placeholder={placeholder}
|
||||
value={value}
|
||||
{...inputProps}
|
||||
/>
|
||||
</Touchable>
|
||||
);
|
||||
}
|
||||
|
||||
get loading() {
|
||||
const { theme } = this.props;
|
||||
return <ActivityIndicator style={[styles.iconContainer, styles.iconRight]} color={themes[theme].bodyText} />;
|
||||
}
|
||||
|
||||
tooglePassword = () => {
|
||||
this.setState(prevState => ({ showPassword: !prevState.showPassword }));
|
||||
};
|
||||
|
||||
render() {
|
||||
const { showPassword } = this.state;
|
||||
const {
|
||||
label,
|
||||
left,
|
||||
error,
|
||||
loading,
|
||||
secureTextEntry,
|
||||
containerStyle,
|
||||
inputRef,
|
||||
iconLeft,
|
||||
iconRight,
|
||||
inputStyle,
|
||||
testID,
|
||||
placeholder,
|
||||
theme,
|
||||
...inputProps
|
||||
} = this.props;
|
||||
const { dangerColor } = themes[theme];
|
||||
return (
|
||||
<View style={[styles.inputContainer, containerStyle]}>
|
||||
{label ? (
|
||||
<Text style={[styles.label, { color: themes[theme].titleText }, error?.error && { color: dangerColor }]}>{label}</Text>
|
||||
) : null}
|
||||
<View style={styles.wrap}>
|
||||
<TextInput
|
||||
style={[
|
||||
styles.input,
|
||||
iconLeft && styles.inputIconLeft,
|
||||
(secureTextEntry || iconRight) && styles.inputIconRight,
|
||||
{
|
||||
backgroundColor: themes[theme].backgroundColor,
|
||||
borderColor: themes[theme].separatorColor,
|
||||
color: themes[theme].titleText
|
||||
},
|
||||
error?.error && {
|
||||
color: dangerColor,
|
||||
borderColor: dangerColor
|
||||
},
|
||||
inputStyle
|
||||
]}
|
||||
ref={inputRef}
|
||||
autoCorrect={false}
|
||||
autoCapitalize='none'
|
||||
underlineColorAndroid='transparent'
|
||||
secureTextEntry={secureTextEntry && !showPassword}
|
||||
testID={testID}
|
||||
accessibilityLabel={placeholder}
|
||||
placeholder={placeholder}
|
||||
theme={theme}
|
||||
{...inputProps}
|
||||
{iconLeft ? (
|
||||
<CustomIcon
|
||||
name={iconLeft}
|
||||
testID={testID ? `${testID}-icon-left` : undefined}
|
||||
size={20}
|
||||
color={colors.auxiliaryText}
|
||||
style={[styles.iconContainer, styles.iconLeft]}
|
||||
/>
|
||||
{iconLeft ? this.iconLeft : null}
|
||||
{iconRight ? this.iconRight : null}
|
||||
{secureTextEntry ? this.iconPassword : null}
|
||||
{loading ? this.loading : null}
|
||||
{left}
|
||||
</View>
|
||||
{error && error.reason ? <Text style={[styles.error, { color: dangerColor }]}>{error.reason}</Text> : null}
|
||||
) : null}
|
||||
|
||||
{showClearInput ? (
|
||||
<Touchable onPress={onClearInput} style={[styles.iconContainer, styles.iconRight]} testID='clear-text-input'>
|
||||
<CustomIcon name='input-clear' size={20} color={colors.auxiliaryTintColor} />
|
||||
</Touchable>
|
||||
) : null}
|
||||
|
||||
{iconRight && !showClearInput ? (
|
||||
<CustomIcon
|
||||
name={iconRight}
|
||||
testID={testID ? `${testID}-icon-right` : undefined}
|
||||
size={20}
|
||||
color={colors.bodyText}
|
||||
style={[styles.iconContainer, styles.iconRight]}
|
||||
/>
|
||||
) : null}
|
||||
|
||||
{secureTextEntry ? (
|
||||
<Touchable onPress={() => setShowPassword(!showPassword)} style={[styles.iconContainer, styles.iconRight]}>
|
||||
<CustomIcon
|
||||
name={showPassword ? 'unread-on-top' : 'unread-on-top-disabled'}
|
||||
testID={testID ? `${testID}-icon-password` : undefined}
|
||||
size={20}
|
||||
color={colors.auxiliaryText}
|
||||
/>
|
||||
</Touchable>
|
||||
) : null}
|
||||
|
||||
{loading ? (
|
||||
<ActivityIndicator
|
||||
style={[styles.iconContainer, styles.iconRight]}
|
||||
color={colors.bodyText}
|
||||
testID={testID ? `${testID}-loading` : undefined}
|
||||
/>
|
||||
) : null}
|
||||
{left}
|
||||
</View>
|
||||
);
|
||||
}
|
||||
}
|
||||
{error && error.reason ? <Text style={[styles.error, { color: colors.dangerColor }]}>{error.reason}</Text> : null}
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -3,7 +3,7 @@ import React from 'react';
|
|||
import { storiesOf } from '@storybook/react-native';
|
||||
|
||||
import { View, StyleSheet } from 'react-native';
|
||||
import FormTextInput from './FormTextInput';
|
||||
import { FormTextInput } from '.';
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
paddingHorizontal: {
|
||||
|
@ -18,14 +18,12 @@ const item = {
|
|||
longText: 'https://open.rocket.chat/images/logo/android-chrome-512x512.png'
|
||||
};
|
||||
|
||||
const theme = 'light';
|
||||
|
||||
stories.add('Short and Long Text', () => (
|
||||
<>
|
||||
<View style={styles.paddingHorizontal}>
|
||||
<FormTextInput label='Short Text' placeholder='placeholder' value={item.name} theme={theme} />
|
||||
<FormTextInput label='Short Text' placeholder='placeholder' value={item.name} />
|
||||
|
||||
<FormTextInput label='Long Text' placeholder='placeholder' value={item.longText} theme={theme} />
|
||||
<FormTextInput label='Long Text' placeholder='placeholder' value={item.longText} />
|
||||
</View>
|
||||
</>
|
||||
));
|
||||
|
|
|
@ -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}
|
||||
/>
|
||||
);
|
||||
});
|
|
@ -0,0 +1,2 @@
|
|||
export * from './TextInput';
|
||||
export * from './FormTextInput';
|
|
@ -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;
|
|
@ -1,11 +1,10 @@
|
|||
import React from 'react';
|
||||
import React, { useEffect } from 'react';
|
||||
import { StyleSheet } from 'react-native';
|
||||
import EasyToast from 'react-native-easy-toast';
|
||||
|
||||
import { themes } from '../lib/constants';
|
||||
import EventEmitter from '../lib/methods/helpers/events';
|
||||
import { useTheme } from '../theme';
|
||||
import sharedStyles from '../views/Styles';
|
||||
import EventEmitter from '../utils/events';
|
||||
import { TSupportedThemes, withTheme } from '../theme';
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
toast: {
|
||||
|
@ -21,54 +20,37 @@ const styles = StyleSheet.create({
|
|||
|
||||
export const LISTENER = 'Toast';
|
||||
|
||||
interface IToastProps {
|
||||
theme?: TSupportedThemes;
|
||||
}
|
||||
let listener: Function;
|
||||
let toast: EasyToast | null | undefined;
|
||||
|
||||
class Toast extends React.Component<IToastProps, any> {
|
||||
private listener?: Function;
|
||||
const Toast = (): React.ReactElement => {
|
||||
const { colors } = useTheme();
|
||||
|
||||
private toast: EasyToast | null | undefined;
|
||||
useEffect(() => {
|
||||
listener = EventEmitter.addEventListener(LISTENER, showToast);
|
||||
return () => {
|
||||
EventEmitter.removeListener(LISTENER, listener);
|
||||
};
|
||||
}, []);
|
||||
|
||||
componentDidMount() {
|
||||
this.listener = EventEmitter.addEventListener(LISTENER, this.showToast);
|
||||
}
|
||||
const getToastRef = (newToast: EasyToast | null) => (toast = newToast);
|
||||
|
||||
shouldComponentUpdate(nextProps: any) {
|
||||
const { theme } = this.props;
|
||||
if (nextProps.theme !== theme) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
if (this.listener) {
|
||||
EventEmitter.removeListener(LISTENER, this.listener);
|
||||
}
|
||||
}
|
||||
|
||||
getToastRef = (toast: EasyToast | null) => (this.toast = toast);
|
||||
|
||||
showToast = ({ message }: { message: string }) => {
|
||||
if (this.toast && this.toast.show) {
|
||||
this.toast.show(message, 1000);
|
||||
const showToast = ({ message }: { message: string }) => {
|
||||
if (toast && toast.show) {
|
||||
toast.show(message, 1000);
|
||||
}
|
||||
};
|
||||
|
||||
render() {
|
||||
const { theme } = this.props;
|
||||
return (
|
||||
<EasyToast
|
||||
ref={this.getToastRef}
|
||||
// @ts-ignore
|
||||
position='center'
|
||||
style={[styles.toast, { backgroundColor: themes[theme!].toastBackground }]}
|
||||
textStyle={[styles.text, { color: themes[theme!].buttonText }]}
|
||||
opacity={0.9}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
return (
|
||||
<EasyToast
|
||||
ref={getToastRef}
|
||||
// @ts-ignore
|
||||
position='center'
|
||||
style={[styles.toast, { backgroundColor: colors.toastBackground }]}
|
||||
textStyle={[styles.text, { color: colors.buttonText }]}
|
||||
opacity={0.9}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default withTheme(Toast);
|
||||
export default Toast;
|
||||
|
|
|
@ -6,9 +6,9 @@ import Modal from 'react-native-modal';
|
|||
import useDeepCompareEffect from 'use-deep-compare-effect';
|
||||
import { connect } from 'react-redux';
|
||||
|
||||
import FormTextInput from '../TextInput/FormTextInput';
|
||||
import { FormTextInput } from '../TextInput';
|
||||
import I18n from '../../i18n';
|
||||
import EventEmitter from '../../utils/events';
|
||||
import EventEmitter from '../../lib/methods/helpers/events';
|
||||
import { useTheme } from '../../theme';
|
||||
import { themes } from '../../lib/constants';
|
||||
import Button from '../Button';
|
||||
|
@ -116,7 +116,6 @@ const TwoFactor = React.memo(({ isMasterDetail }: { isMasterDetail: boolean }) =
|
|||
{method?.text ? <Text style={[styles.subtitle, { color }]}>{I18n.t(method.text)}</Text> : null}
|
||||
<FormTextInput
|
||||
value={code}
|
||||
theme={theme}
|
||||
inputRef={(e: any) => InteractionManager.runAfterInteractions(() => e?.getNativeRef()?.focus())}
|
||||
returnKeyType='send'
|
||||
autoCapitalize='none'
|
||||
|
|
|
@ -10,7 +10,7 @@ import { textParser } from './utils';
|
|||
import { themes } from '../../lib/constants';
|
||||
import sharedStyles from '../../views/Styles';
|
||||
import { CustomIcon } from '../CustomIcon';
|
||||
import { isAndroid } from '../../utils/deviceInfo';
|
||||
import { isAndroid } from '../../lib/methods/helpers';
|
||||
import { useTheme } from '../../theme';
|
||||
import ActivityIndicator from '../ActivityIndicator';
|
||||
import { IDatePicker } from './interfaces';
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import React from 'react';
|
||||
import { StyleSheet, View } from 'react-native';
|
||||
import FastImage from '@rocket.chat/react-native-fast-image';
|
||||
import FastImage from 'react-native-fast-image';
|
||||
import { BlockContext } from '@rocket.chat/ui-kit';
|
||||
|
||||
import ImageContainer from '../message/Image';
|
||||
|
|
|
@ -1,52 +1,52 @@
|
|||
import React from 'react';
|
||||
import { Text, View } from 'react-native';
|
||||
import Touchable from 'react-native-platform-touchable';
|
||||
import FastImage from '@rocket.chat/react-native-fast-image';
|
||||
import FastImage from 'react-native-fast-image';
|
||||
|
||||
import { themes } from '../../../lib/constants';
|
||||
import { textParser } from '../utils';
|
||||
import { CustomIcon } from '../../CustomIcon';
|
||||
import styles from './styles';
|
||||
import { IItemData } from '.';
|
||||
import { TSupportedThemes } from '../../../theme';
|
||||
import { useTheme } from '../../../theme';
|
||||
|
||||
interface IChip {
|
||||
item: IItemData;
|
||||
onSelect: (item: IItemData) => void;
|
||||
style?: object;
|
||||
theme: TSupportedThemes;
|
||||
}
|
||||
|
||||
interface IChips {
|
||||
items: IItemData[];
|
||||
onSelect: (item: IItemData) => void;
|
||||
style?: object;
|
||||
theme: TSupportedThemes;
|
||||
}
|
||||
|
||||
const keyExtractor = (item: IItemData) => item.value.toString();
|
||||
|
||||
const Chip = ({ item, onSelect, style, theme }: IChip) => (
|
||||
<Touchable
|
||||
key={item.value}
|
||||
onPress={() => onSelect(item)}
|
||||
style={[styles.chip, { backgroundColor: themes[theme].auxiliaryBackground }, style]}
|
||||
background={Touchable.Ripple(themes[theme].bannerBackground)}>
|
||||
<>
|
||||
{item.imageUrl ? <FastImage style={styles.chipImage} source={{ uri: item.imageUrl }} /> : null}
|
||||
<Text numberOfLines={1} style={[styles.chipText, { color: themes[theme].titleText }]}>
|
||||
{textParser([item.text])}
|
||||
</Text>
|
||||
<CustomIcon name='close' size={16} color={themes[theme].auxiliaryText} />
|
||||
</>
|
||||
</Touchable>
|
||||
);
|
||||
const Chip = ({ item, onSelect, style }: IChip) => {
|
||||
const { colors } = useTheme();
|
||||
return (
|
||||
<Touchable
|
||||
key={item.value}
|
||||
onPress={() => onSelect(item)}
|
||||
style={[styles.chip, { backgroundColor: colors.auxiliaryBackground }, style]}
|
||||
background={Touchable.Ripple(colors.bannerBackground)}>
|
||||
<>
|
||||
{item.imageUrl ? <FastImage style={styles.chipImage} source={{ uri: item.imageUrl }} /> : null}
|
||||
<Text numberOfLines={1} style={[styles.chipText, { color: colors.titleText }]}>
|
||||
{textParser([item.text])}
|
||||
</Text>
|
||||
<CustomIcon name='close' size={16} color={colors.auxiliaryText} />
|
||||
</>
|
||||
</Touchable>
|
||||
);
|
||||
};
|
||||
Chip.propTypes = {};
|
||||
|
||||
const Chips = ({ items, onSelect, style, theme }: IChips) => (
|
||||
const Chips = ({ items, onSelect, style }: IChips) => (
|
||||
<View style={styles.chips}>
|
||||
{items.map(item => (
|
||||
<Chip key={keyExtractor(item)} item={item} onSelect={onSelect} style={style} theme={theme} />
|
||||
<Chip key={keyExtractor(item)} item={item} onSelect={onSelect} style={style} />
|
||||
))}
|
||||
</View>
|
||||
);
|
||||
|
|
|
@ -3,15 +3,13 @@ import { Text, View } from 'react-native';
|
|||
import Touchable from 'react-native-platform-touchable';
|
||||
|
||||
import { CustomIcon } from '../../CustomIcon';
|
||||
import { themes } from '../../../lib/constants';
|
||||
import ActivityIndicator from '../../ActivityIndicator';
|
||||
import styles from './styles';
|
||||
import { TSupportedThemes } from '../../../theme';
|
||||
import { useTheme } from '../../../theme';
|
||||
|
||||
interface IInput {
|
||||
children?: JSX.Element;
|
||||
onPress: () => void;
|
||||
theme: TSupportedThemes;
|
||||
inputStyle?: object;
|
||||
disabled?: boolean | null;
|
||||
placeholder?: string;
|
||||
|
@ -19,21 +17,23 @@ interface IInput {
|
|||
innerInputStyle?: object;
|
||||
}
|
||||
|
||||
const Input = ({ children, onPress, theme, loading, inputStyle, placeholder, disabled, innerInputStyle }: IInput) => (
|
||||
<Touchable
|
||||
onPress={onPress}
|
||||
style={[{ backgroundColor: themes[theme].backgroundColor }, inputStyle]}
|
||||
background={Touchable.Ripple(themes[theme].bannerBackground)}
|
||||
disabled={disabled}>
|
||||
<View style={[styles.input, { borderColor: themes[theme].separatorColor }, innerInputStyle]}>
|
||||
{placeholder ? <Text style={[styles.pickerText, { color: themes[theme].auxiliaryText }]}>{placeholder}</Text> : children}
|
||||
{loading ? (
|
||||
<ActivityIndicator style={[styles.loading, styles.icon]} />
|
||||
) : (
|
||||
<CustomIcon name='chevron-down' size={22} color={themes[theme].auxiliaryText} style={styles.icon} />
|
||||
)}
|
||||
</View>
|
||||
</Touchable>
|
||||
);
|
||||
|
||||
const Input = ({ children, onPress, loading, inputStyle, placeholder, disabled, innerInputStyle }: IInput) => {
|
||||
const { colors } = useTheme();
|
||||
return (
|
||||
<Touchable
|
||||
onPress={onPress}
|
||||
style={[{ backgroundColor: colors.backgroundColor }, inputStyle]}
|
||||
background={Touchable.Ripple(colors.bannerBackground)}
|
||||
disabled={disabled}>
|
||||
<View style={[styles.input, { borderColor: colors.separatorColor }, innerInputStyle]}>
|
||||
{placeholder ? <Text style={[styles.pickerText, { color: colors.auxiliaryText }]}>{placeholder}</Text> : children}
|
||||
{loading ? (
|
||||
<ActivityIndicator style={styles.icon} />
|
||||
) : (
|
||||
<CustomIcon name='chevron-down' size={22} color={colors.auxiliaryText} style={styles.icon} />
|
||||
)}
|
||||
</View>
|
||||
</Touchable>
|
||||
);
|
||||
};
|
||||
export default Input;
|
||||
|
|
|
@ -1,61 +1,54 @@
|
|||
import React from 'react';
|
||||
import { FlatList, Text } from 'react-native';
|
||||
import { Text } from 'react-native';
|
||||
import Touchable from 'react-native-platform-touchable';
|
||||
import FastImage from '@rocket.chat/react-native-fast-image';
|
||||
import FastImage from 'react-native-fast-image';
|
||||
import { FlatList } from 'react-native-gesture-handler';
|
||||
|
||||
import Check from '../../Check';
|
||||
import * as List from '../../List';
|
||||
import { textParser } from '../utils';
|
||||
import { themes } from '../../../lib/constants';
|
||||
import styles from './styles';
|
||||
import { IItemData } from '.';
|
||||
import { TSupportedThemes } from '../../../theme';
|
||||
import { useTheme } from '../../../theme';
|
||||
|
||||
interface IItem {
|
||||
item: IItemData;
|
||||
selected?: string;
|
||||
onSelect: Function;
|
||||
theme: TSupportedThemes;
|
||||
}
|
||||
|
||||
interface IItems {
|
||||
items: IItemData[];
|
||||
selected: string[];
|
||||
onSelect: Function;
|
||||
theme: TSupportedThemes;
|
||||
}
|
||||
|
||||
const keyExtractor = (item: IItemData) => item.value.toString();
|
||||
const keyExtractor = (item: IItemData) => item.value?.name || item.text?.text;
|
||||
|
||||
// RectButton doesn't work on modal (Android)
|
||||
const Item = ({ item, selected, onSelect, theme }: IItem) => {
|
||||
const Item = ({ item, selected, onSelect }: IItem) => {
|
||||
const itemName = item.value?.name || item.text.text.toLowerCase();
|
||||
const { colors } = useTheme();
|
||||
return (
|
||||
<Touchable
|
||||
testID={`multi-select-item-${itemName}`}
|
||||
key={itemName}
|
||||
onPress={() => onSelect(item)}
|
||||
style={[styles.item, { backgroundColor: themes[theme].backgroundColor }]}>
|
||||
<Touchable testID={`multi-select-item-${itemName}`} key={itemName} onPress={() => onSelect(item)} style={[styles.item]}>
|
||||
<>
|
||||
{item.imageUrl ? <FastImage style={styles.itemImage} source={{ uri: item.imageUrl }} /> : null}
|
||||
<Text style={{ color: themes[theme].titleText }}>{textParser([item.text])}</Text>
|
||||
<Text style={{ color: colors.titleText }}>{textParser([item.text])}</Text>
|
||||
{selected ? <Check /> : null}
|
||||
</>
|
||||
</Touchable>
|
||||
);
|
||||
};
|
||||
|
||||
const Items = ({ items, selected, onSelect, theme }: IItems) => (
|
||||
const Items = ({ items, selected, onSelect }: IItems) => (
|
||||
<FlatList
|
||||
data={items}
|
||||
style={[styles.items, { backgroundColor: themes[theme].backgroundColor }]}
|
||||
contentContainerStyle={[styles.itemContent, { backgroundColor: themes[theme].backgroundColor }]}
|
||||
style={[styles.items]}
|
||||
contentContainerStyle={[styles.itemContent]}
|
||||
keyboardShouldPersistTaps='always'
|
||||
ItemSeparatorComponent={List.Separator}
|
||||
keyExtractor={keyExtractor}
|
||||
renderItem={({ item }) => (
|
||||
<Item item={item} onSelect={onSelect} theme={theme} selected={selected.find(s => s === item.value)} />
|
||||
)}
|
||||
renderItem={({ item }) => <Item item={item} onSelect={onSelect} selected={selected.find(s => s === item.value)} />}
|
||||
/>
|
||||
);
|
||||
|
||||
|
|
|
@ -0,0 +1,85 @@
|
|||
import React, { useState } from 'react';
|
||||
import { View } from 'react-native';
|
||||
|
||||
import { FormTextInput } from '../../TextInput/FormTextInput';
|
||||
import { textParser } from '../utils';
|
||||
import I18n from '../../../i18n';
|
||||
import Items from './Items';
|
||||
import styles from './styles';
|
||||
import { useTheme } from '../../../theme';
|
||||
import { IItemData } from '.';
|
||||
import { debounce } from '../../../lib/methods/helpers/debounce';
|
||||
import { isIOS } from '../../../lib/methods/helpers';
|
||||
import { useActionSheet } from '../../ActionSheet';
|
||||
|
||||
interface IMultiSelectContentProps {
|
||||
onSearch?: (keyword: string) => IItemData[] | Promise<IItemData[] | undefined>;
|
||||
options?: IItemData[];
|
||||
multiselect: boolean;
|
||||
select: React.Dispatch<any>;
|
||||
onChange: Function;
|
||||
setCurrentValue: React.Dispatch<React.SetStateAction<string>>;
|
||||
onHide: Function;
|
||||
selectedItems: string[];
|
||||
}
|
||||
|
||||
export const MultiSelectContent = React.memo(
|
||||
({ onSearch, options, multiselect, select, onChange, setCurrentValue, onHide, selectedItems }: IMultiSelectContentProps) => {
|
||||
const { colors } = useTheme();
|
||||
const [selected, setSelected] = useState<string[]>(Array.isArray(selectedItems) ? selectedItems : []);
|
||||
const [items, setItems] = useState<IItemData[] | undefined>(options);
|
||||
const { hideActionSheet } = useActionSheet();
|
||||
|
||||
const onSelect = (item: IItemData) => {
|
||||
const {
|
||||
value,
|
||||
text: { text }
|
||||
} = item;
|
||||
if (multiselect) {
|
||||
let newSelect = [];
|
||||
if (!selected.includes(value)) {
|
||||
newSelect = [...selected, value];
|
||||
} else {
|
||||
newSelect = selected.filter((s: any) => s !== value);
|
||||
}
|
||||
setSelected(newSelect);
|
||||
select(newSelect);
|
||||
onChange({ value: newSelect });
|
||||
} else {
|
||||
onChange({ value });
|
||||
setCurrentValue(text);
|
||||
onHide();
|
||||
}
|
||||
};
|
||||
|
||||
const handleSearch = debounce(
|
||||
async (text: string) => {
|
||||
if (onSearch) {
|
||||
const res = await onSearch(text);
|
||||
setItems(res);
|
||||
} else {
|
||||
setItems(options?.filter((option: any) => textParser([option.text]).toLowerCase().includes(text.toLowerCase())));
|
||||
}
|
||||
},
|
||||
onSearch ? 300 : 0
|
||||
);
|
||||
|
||||
return (
|
||||
<View style={[styles.actionSheetContainer]}>
|
||||
<FormTextInput
|
||||
testID='multi-select-search'
|
||||
onChangeText={handleSearch}
|
||||
placeholder={I18n.t('Search')}
|
||||
inputStyle={{ backgroundColor: colors.focusedBackground }}
|
||||
bottomSheet={isIOS}
|
||||
onSubmitEditing={() => {
|
||||
setTimeout(() => {
|
||||
hideActionSheet();
|
||||
}, 150);
|
||||
}}
|
||||
/>
|
||||
<Items items={items || []} selected={selected} onSelect={onSelect} />
|
||||
</View>
|
||||
);
|
||||
}
|
||||
);
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue