Page Menu
Home
Phabricator
Search
Configure Global Search
Log In
Files
F3509315
No One
Temporary
Actions
View File
Edit File
Delete File
View Transforms
Subscribe
Mute Notifications
Award Token
Flag For Later
Size
19 KB
Referenced Files
None
Subscribers
None
View Options
diff --git a/native/account/logged-out-modal.react.js b/native/account/logged-out-modal.react.js
index 82ee55ef2..b81c6eb19 100644
--- a/native/account/logged-out-modal.react.js
+++ b/native/account/logged-out-modal.react.js
@@ -1,659 +1,659 @@
// @flow
import _isEqual from 'lodash/fp/isEqual';
import * as React from 'react';
import {
View,
StyleSheet,
Text,
TouchableOpacity,
Image,
Keyboard,
Platform,
BackHandler,
ActivityIndicator,
} from 'react-native';
import Animated, { EasingNode } from 'react-native-reanimated';
import { SafeAreaView } from 'react-native-safe-area-context';
import Icon from 'react-native-vector-icons/FontAwesome';
import { useDispatch } from 'react-redux';
import {
appStartNativeCredentialsAutoLogIn,
appStartReduxLoggedInButInvalidCookie,
} from 'lib/actions/user-actions';
import { isLoggedIn } from 'lib/selectors/user-selectors';
import type { Shape } from 'lib/types/core';
import type { Dispatch } from 'lib/types/redux-types';
import { fetchNewCookieFromNativeCredentials } from 'lib/utils/action-utils';
import KeyboardAvoidingView from '../components/keyboard-avoiding-view.react';
import ConnectedStatusBar from '../connected-status-bar.react';
import {
type KeyboardEvent,
addKeyboardShowListener,
addKeyboardDismissListener,
removeKeyboardListener,
} from '../keyboard/keyboard';
import { createIsForegroundSelector } from '../navigation/nav-selectors';
import { NavContext } from '../navigation/navigation-context';
import { LoggedOutModalRouteName } from '../navigation/route-names';
import { resetUserStateActionType } from '../redux/action-types';
import { useSelector } from '../redux/redux-utils';
import { usePersistedStateLoaded } from '../selectors/app-state-selectors';
import {
type DerivedDimensionsInfo,
derivedDimensionsInfoSelector,
} from '../selectors/dimensions-selectors';
import { splashStyleSelector } from '../splash';
import type { EmitterSubscription } from '../types/react-native';
import type { ImageStyle } from '../types/styles';
import {
runTiming,
ratchetAlongWithKeyboardHeight,
} from '../utils/animation-utils';
import {
type StateContainer,
type StateChange,
setStateForContainer,
} from '../utils/state-container';
import { splashBackgroundURI } from './background-info';
import LogInPanel from './log-in-panel.react';
import type { LogInState } from './log-in-panel.react';
import RegisterPanel from './register-panel.react';
import type { RegisterState } from './register-panel.react';
let initialAppLoad = true;
const safeAreaEdges = ['top', 'bottom'];
/* eslint-disable import/no-named-as-default-member */
const {
Value,
Node,
Clock,
block,
set,
call,
cond,
not,
and,
eq,
neq,
lessThan,
greaterOrEq,
add,
sub,
divide,
max,
stopClock,
clockRunning,
} = Animated;
/* eslint-enable import/no-named-as-default-member */
type LoggedOutMode = 'loading' | 'prompt' | 'log-in' | 'register';
const modeNumbers: { [LoggedOutMode]: number } = {
'loading': 0,
'prompt': 1,
'log-in': 2,
'register': 3,
};
function isPastPrompt(modeValue: Node) {
return and(
neq(modeValue, modeNumbers['loading']),
neq(modeValue, modeNumbers['prompt']),
);
}
type Props = {
// Navigation state
+isForeground: boolean,
// Redux state
+persistedStateLoaded: boolean,
+rehydrateConcluded: boolean,
+cookie: ?string,
+urlPrefix: string,
+loggedIn: boolean,
+dimensions: DerivedDimensionsInfo,
+splashStyle: ImageStyle,
// Redux dispatch functions
+dispatch: Dispatch,
...
};
type State = {
+mode: LoggedOutMode,
+logInState: StateContainer<LogInState>,
+registerState: StateContainer<RegisterState>,
};
class LoggedOutModal extends React.PureComponent<Props, State> {
keyboardShowListener: ?EmitterSubscription;
keyboardHideListener: ?EmitterSubscription;
mounted = false;
nextMode: LoggedOutMode = 'loading';
activeAlert = false;
contentHeight: Value;
keyboardHeightValue = new Value(0);
modeValue: Value;
buttonOpacity: Value;
panelPaddingTopValue: Node;
panelOpacityValue: Node;
constructor(props: Props) {
super(props);
// Man, this is a lot of boilerplate just to containerize some state.
// Mostly due to Flow typing requirements...
const setLogInState = setStateForContainer(
this.guardedSetState,
(change: Shape<LogInState>) => (fullState: State) => ({
logInState: {
...fullState.logInState,
state: { ...fullState.logInState.state, ...change },
},
}),
);
const setRegisterState = setStateForContainer(
this.guardedSetState,
(change: Shape<RegisterState>) => (fullState: State) => ({
registerState: {
...fullState.registerState,
state: { ...fullState.registerState.state, ...change },
},
}),
);
this.state = {
mode: props.persistedStateLoaded ? 'prompt' : 'loading',
logInState: {
state: {
usernameInputText: null,
passwordInputText: null,
},
setState: setLogInState,
},
registerState: {
state: {
usernameInputText: '',
passwordInputText: '',
confirmPasswordInputText: '',
},
setState: setRegisterState,
},
};
if (props.persistedStateLoaded) {
this.nextMode = 'prompt';
}
this.contentHeight = new Value(props.dimensions.safeAreaHeight);
this.modeValue = new Value(modeNumbers[this.nextMode]);
- this.buttonOpacity = new Value(props.rehydrateConcluded ? 1 : 0);
+ this.buttonOpacity = new Value(props.persistedStateLoaded ? 1 : 0);
this.panelPaddingTopValue = this.panelPaddingTop();
this.panelOpacityValue = this.panelOpacity();
}
guardedSetState = (change: StateChange<State>, callback?: () => mixed) => {
if (this.mounted) {
this.setState(change, callback);
}
};
setMode(newMode: LoggedOutMode) {
this.nextMode = newMode;
this.guardedSetState({ mode: newMode });
this.modeValue.setValue(modeNumbers[newMode]);
}
proceedToNextMode = () => {
this.guardedSetState({ mode: this.nextMode });
};
componentDidMount() {
this.mounted = true;
if (this.props.rehydrateConcluded) {
this.onInitialAppLoad();
}
if (this.props.isForeground) {
this.onForeground();
}
}
componentWillUnmount() {
this.mounted = false;
if (this.props.isForeground) {
this.onBackground();
}
}
componentDidUpdate(prevProps: Props, prevState: State) {
if (!prevProps.persistedStateLoaded && this.props.persistedStateLoaded) {
this.setMode('prompt');
}
if (!prevProps.rehydrateConcluded && this.props.rehydrateConcluded) {
this.onInitialAppLoad();
}
if (!prevProps.isForeground && this.props.isForeground) {
this.onForeground();
} else if (prevProps.isForeground && !this.props.isForeground) {
this.onBackground();
}
if (this.state.mode === 'prompt' && prevState.mode !== 'prompt') {
this.buttonOpacity.setValue(0);
Animated.timing(this.buttonOpacity, {
easing: EasingNode.out(EasingNode.ease),
duration: 250,
toValue: 1.0,
}).start();
}
const newContentHeight = this.props.dimensions.safeAreaHeight;
const oldContentHeight = prevProps.dimensions.safeAreaHeight;
if (newContentHeight !== oldContentHeight) {
this.contentHeight.setValue(newContentHeight);
}
}
onForeground() {
this.keyboardShowListener = addKeyboardShowListener(this.keyboardShow);
this.keyboardHideListener = addKeyboardDismissListener(this.keyboardHide);
BackHandler.addEventListener('hardwareBackPress', this.hardwareBack);
}
onBackground() {
if (this.keyboardShowListener) {
removeKeyboardListener(this.keyboardShowListener);
this.keyboardShowListener = null;
}
if (this.keyboardHideListener) {
removeKeyboardListener(this.keyboardHideListener);
this.keyboardHideListener = null;
}
BackHandler.removeEventListener('hardwareBackPress', this.hardwareBack);
}
// This gets triggered when an app is killed and restarted
// Not when it is returned from being backgrounded
async onInitialAppLoad() {
if (!initialAppLoad) {
return;
}
initialAppLoad = false;
const { loggedIn, cookie, urlPrefix, dispatch } = this.props;
const hasUserCookie = cookie && cookie.startsWith('user=');
if (loggedIn && hasUserCookie) {
return;
}
if (!__DEV__) {
const actionSource = loggedIn
? appStartReduxLoggedInButInvalidCookie
: appStartNativeCredentialsAutoLogIn;
const sessionChange = await fetchNewCookieFromNativeCredentials(
dispatch,
cookie,
urlPrefix,
actionSource,
);
if (
sessionChange &&
sessionChange.cookie &&
sessionChange.cookie.startsWith('user=')
) {
// success! we can expect subsequent actions to fix up the state
return;
}
}
if (loggedIn || hasUserCookie) {
this.props.dispatch({ type: resetUserStateActionType });
}
}
hardwareBack = () => {
if (this.nextMode !== 'prompt') {
this.goBackToPrompt();
return true;
}
return false;
};
panelPaddingTop() {
const headerHeight = Platform.OS === 'ios' ? 62.33 : 58.54;
const promptButtonsSize = Platform.OS === 'ios' ? 40 : 61;
const logInContainerSize = 140;
const registerPanelSize = Platform.OS === 'ios' ? 181 : 180;
const containerSize = add(
headerHeight,
cond(not(isPastPrompt(this.modeValue)), promptButtonsSize, 0),
cond(eq(this.modeValue, modeNumbers['log-in']), logInContainerSize, 0),
cond(eq(this.modeValue, modeNumbers['register']), registerPanelSize, 0),
);
const potentialPanelPaddingTop = divide(
max(sub(this.contentHeight, this.keyboardHeightValue, containerSize), 0),
2,
);
const panelPaddingTop = new Value(-1);
const targetPanelPaddingTop = new Value(-1);
const prevModeValue = new Value(modeNumbers[this.nextMode]);
const clock = new Clock();
const keyboardTimeoutClock = new Clock();
return block([
cond(lessThan(panelPaddingTop, 0), [
set(panelPaddingTop, potentialPanelPaddingTop),
set(targetPanelPaddingTop, potentialPanelPaddingTop),
]),
cond(
lessThan(this.keyboardHeightValue, 0),
[
runTiming(keyboardTimeoutClock, 0, 1, true, { duration: 500 }),
cond(
not(clockRunning(keyboardTimeoutClock)),
set(this.keyboardHeightValue, 0),
),
],
stopClock(keyboardTimeoutClock),
),
cond(
and(
greaterOrEq(this.keyboardHeightValue, 0),
neq(prevModeValue, this.modeValue),
),
[
stopClock(clock),
cond(
neq(isPastPrompt(prevModeValue), isPastPrompt(this.modeValue)),
set(targetPanelPaddingTop, potentialPanelPaddingTop),
),
set(prevModeValue, this.modeValue),
],
),
ratchetAlongWithKeyboardHeight(this.keyboardHeightValue, [
stopClock(clock),
set(targetPanelPaddingTop, potentialPanelPaddingTop),
]),
cond(
neq(panelPaddingTop, targetPanelPaddingTop),
set(
panelPaddingTop,
runTiming(clock, panelPaddingTop, targetPanelPaddingTop),
),
),
panelPaddingTop,
]);
}
panelOpacity() {
const targetPanelOpacity = isPastPrompt(this.modeValue);
const panelOpacity = new Value(-1);
const prevPanelOpacity = new Value(-1);
const prevTargetPanelOpacity = new Value(-1);
const clock = new Clock();
return block([
cond(lessThan(panelOpacity, 0), [
set(panelOpacity, targetPanelOpacity),
set(prevPanelOpacity, targetPanelOpacity),
set(prevTargetPanelOpacity, targetPanelOpacity),
]),
cond(greaterOrEq(this.keyboardHeightValue, 0), [
cond(neq(targetPanelOpacity, prevTargetPanelOpacity), [
stopClock(clock),
set(prevTargetPanelOpacity, targetPanelOpacity),
]),
cond(
neq(panelOpacity, targetPanelOpacity),
set(panelOpacity, runTiming(clock, panelOpacity, targetPanelOpacity)),
),
]),
cond(
and(eq(panelOpacity, 0), neq(prevPanelOpacity, 0)),
call([], this.proceedToNextMode),
),
set(prevPanelOpacity, panelOpacity),
panelOpacity,
]);
}
keyboardShow = (event: KeyboardEvent) => {
if (_isEqual(event.startCoordinates)(event.endCoordinates)) {
return;
}
const keyboardHeight = Platform.select({
// Android doesn't include the bottomInset in this height measurement
android: event.endCoordinates.height,
default: Math.max(
event.endCoordinates.height - this.props.dimensions.bottomInset,
0,
),
});
this.keyboardHeightValue.setValue(keyboardHeight);
};
keyboardHide = () => {
if (!this.activeAlert) {
this.keyboardHeightValue.setValue(0);
}
};
setActiveAlert = (activeAlert: boolean) => {
this.activeAlert = activeAlert;
};
goBackToPrompt = () => {
this.nextMode = 'prompt';
this.keyboardHeightValue.setValue(0);
this.modeValue.setValue(modeNumbers['prompt']);
Keyboard.dismiss();
};
render() {
let panel = null;
let buttons = null;
if (this.state.mode === 'log-in') {
panel = (
<LogInPanel
setActiveAlert={this.setActiveAlert}
opacityValue={this.panelOpacityValue}
logInState={this.state.logInState}
/>
);
} else if (this.state.mode === 'register') {
panel = (
<RegisterPanel
setActiveAlert={this.setActiveAlert}
opacityValue={this.panelOpacityValue}
registerState={this.state.registerState}
/>
);
} else if (this.state.mode === 'prompt') {
const opacityStyle = { opacity: this.buttonOpacity };
buttons = (
<Animated.View style={[styles.buttonContainer, opacityStyle]}>
<TouchableOpacity
onPress={this.onPressLogIn}
style={styles.button}
activeOpacity={0.6}
>
<Text style={styles.buttonText}>LOG IN</Text>
</TouchableOpacity>
<TouchableOpacity
onPress={this.onPressRegister}
style={styles.button}
activeOpacity={0.6}
>
<Text style={styles.buttonText}>SIGN UP</Text>
</TouchableOpacity>
</Animated.View>
);
} else if (this.state.mode === 'loading') {
panel = (
<ActivityIndicator
color="white"
size="large"
style={styles.loadingIndicator}
/>
);
}
const windowWidth = this.props.dimensions.width;
const buttonStyle = {
opacity: this.panelOpacityValue,
left: windowWidth < 360 ? 28 : 40,
};
const padding = { paddingTop: this.panelPaddingTopValue };
const animatedContent = (
<Animated.View style={[styles.animationContainer, padding]}>
<View>
<Text style={styles.header}>Comm</Text>
<Animated.View style={[styles.backButton, buttonStyle]}>
<TouchableOpacity activeOpacity={0.6} onPress={this.hardwareBack}>
<Icon name="arrow-circle-o-left" size={36} color="#FFFFFFAA" />
</TouchableOpacity>
</Animated.View>
</View>
{panel}
</Animated.View>
);
const backgroundSource = { uri: splashBackgroundURI };
return (
<React.Fragment>
<ConnectedStatusBar barStyle="light-content" />
<Image
source={backgroundSource}
style={[styles.modalBackground, this.props.splashStyle]}
/>
<SafeAreaView style={styles.container} edges={safeAreaEdges}>
<KeyboardAvoidingView behavior="padding" style={styles.container}>
{animatedContent}
{buttons}
</KeyboardAvoidingView>
</SafeAreaView>
</React.Fragment>
);
}
onPressLogIn = () => {
if (Platform.OS !== 'ios') {
// For some strange reason, iOS's password management logic doesn't
// realize that the username and password fields in LogInPanel are related
// if the username field gets focused on mount. To avoid this issue we
// need the username and password fields to both appear on-screen before
// we focus the username field. However, when we set keyboardHeightValue
// to -1 here, we are telling our Reanimated logic to wait until the
// keyboard appears before showing LogInPanel. Since we need LogInPanel
// to appear before the username field is focused, we need to avoid this
// behavior on iOS.
this.keyboardHeightValue.setValue(-1);
}
this.setMode('log-in');
};
onPressRegister = () => {
this.keyboardHeightValue.setValue(-1);
this.setMode('register');
};
}
const styles = StyleSheet.create({
animationContainer: {
flex: 1,
},
backButton: {
position: 'absolute',
top: 13,
},
button: {
backgroundColor: '#FFFFFFAA',
borderRadius: 6,
marginBottom: 10,
marginLeft: 40,
marginRight: 40,
marginTop: 10,
paddingBottom: 6,
paddingLeft: 18,
paddingRight: 18,
paddingTop: 6,
},
buttonContainer: {
bottom: 0,
left: 0,
paddingBottom: 20,
position: 'absolute',
right: 0,
},
buttonText: {
color: '#000000FF',
fontFamily: 'OpenSans-Semibold',
fontSize: 22,
textAlign: 'center',
},
container: {
backgroundColor: 'transparent',
flex: 1,
},
header: {
color: 'white',
fontFamily: Platform.OS === 'ios' ? 'IBMPlexSans' : 'IBMPlexSans-Medium',
fontSize: 56,
fontWeight: '500',
lineHeight: 66,
textAlign: 'center',
},
loadingIndicator: {
paddingTop: 15,
},
modalBackground: {
bottom: 0,
left: 0,
position: 'absolute',
right: 0,
top: 0,
},
});
const isForegroundSelector = createIsForegroundSelector(
LoggedOutModalRouteName,
);
const ConnectedLoggedOutModal: React.ComponentType<{ ... }> = React.memo<{
...
}>(function ConnectedLoggedOutModal(props: { ... }) {
const navContext = React.useContext(NavContext);
const isForeground = isForegroundSelector(navContext);
const rehydrateConcluded = useSelector(
state => !!(state._persist && state._persist.rehydrated && navContext),
);
const persistedStateLoaded = usePersistedStateLoaded();
const cookie = useSelector(state => state.cookie);
const urlPrefix = useSelector(state => state.urlPrefix);
const loggedIn = useSelector(isLoggedIn);
const dimensions = useSelector(derivedDimensionsInfoSelector);
const splashStyle = useSelector(splashStyleSelector);
const dispatch = useDispatch();
return (
<LoggedOutModal
{...props}
isForeground={isForeground}
persistedStateLoaded={persistedStateLoaded}
rehydrateConcluded={rehydrateConcluded}
cookie={cookie}
urlPrefix={urlPrefix}
loggedIn={loggedIn}
dimensions={dimensions}
splashStyle={splashStyle}
dispatch={dispatch}
/>
);
});
export default ConnectedLoggedOutModal;
File Metadata
Details
Attached
Mime Type
text/x-diff
Expires
Mon, Dec 23, 3:24 AM (6 h, 5 m)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
2690283
Default Alt Text
(19 KB)
Attached To
Mode
rCOMM Comm
Attached
Detach File
Event Timeline
Log In to Comment