Skip to content

Commit

Permalink
Merge pull request #3214 from Expensify/cherry-pick-staging-3208
Browse files Browse the repository at this point in the history
  • Loading branch information
OSBotify authored May 28, 2021
2 parents 5d13c26 + d428542 commit 62b08ca
Show file tree
Hide file tree
Showing 14 changed files with 354 additions and 228 deletions.
41 changes: 33 additions & 8 deletions src/components/ScreenWrapper.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import {SafeAreaInsetsContext} from 'react-native-safe-area-context';
import styles, {getSafeAreaPadding} from '../styles/styles';
import HeaderGap from './HeaderGap';
import KeyboardShortcut from '../libs/KeyboardShortcut';
import onScreenTransitionEnd from '../libs/onScreenTransitionEnd';

const propTypes = {
/** Array of additional styles to add */
Expand All @@ -24,9 +25,15 @@ const propTypes = {
/** Whether to include padding top */
includePaddingTop: PropTypes.bool,

/** react-navigation object that will allow us to goBack() */
// Called when navigated Screen's transition is finished.
onTransitionEnd: PropTypes.func,

// react-navigation navigation object available to screen components
navigation: PropTypes.shape({
/** Returns to the previous navigation state e.g. if this is inside a Modal we will dismiss it */
// Method to attach listner to Navigaton state.
addListener: PropTypes.func.isRequired,

// Returns to the previous navigation state e.g. if this is inside a Modal we will dismiss it
goBack: PropTypes.func,
}),
};
Expand All @@ -35,24 +42,39 @@ const defaultProps = {
style: [],
includePaddingBottom: true,
includePaddingTop: true,
onTransitionEnd: () => {},
navigation: {
addListener: () => {},
goBack: () => {},
},
};

class ScreenWrapper extends React.Component {
constructor(props) {
super(props);
this.state = {
didScreenTransitionEnd: false,
};
}

componentDidMount() {
this.unsubscribe = KeyboardShortcut.subscribe('Escape', () => {
this.unsubscribeEscapeKey = KeyboardShortcut.subscribe('Escape', () => {
this.props.navigation.goBack();
}, [], true);

this.unsubscribeTransitionEnd = onScreenTransitionEnd(this.props.navigation, () => {
this.setState({didScreenTransitionEnd: true});
this.props.onTransitionEnd();
});
}

componentWillUnmount() {
if (!this.unsubscribe) {
return;
if (this.unsubscribeEscapeKey) {
this.unsubscribeEscapeKey();
}
if (this.unsubscribeTransitionEnd) {
this.unsubscribeTransitionEnd();
}

this.unsubscribe();
}

render() {
Expand Down Expand Up @@ -80,7 +102,10 @@ class ScreenWrapper extends React.Component {
<HeaderGap />
{// If props.children is a function, call it to provide the insets to the children.
_.isFunction(this.props.children)
? this.props.children(insets)
? this.props.children({
insets,
didScreenTransitionEnd: this.state.didScreenTransitionEnd,
})
: this.props.children
}
</View>
Expand Down
8 changes: 5 additions & 3 deletions src/libs/Navigation/AppNavigator/AuthScreens.js
Original file line number Diff line number Diff line change
Expand Up @@ -69,10 +69,12 @@ Onyx.connect({

const RootStack = createCustomModalStackNavigator();

// When modal screen gets focused, update modal visibility in Onyx
// We want to delay the re-rendering for components(e.g. ReportActionCompose)
// that depends on modal visibility until Modal is completely closed or its transition has ended
// When modal screen is focused and animation transition is ended, update modal visibility in Onyx
// https://reactnavigation.org/docs/navigation-events/
const modalScreenListeners = {
focus: () => {
transitionEnd: () => {
setModalVisibility(true);
},
beforeRemove: () => {
Expand Down Expand Up @@ -160,7 +162,7 @@ class AuthScreens extends React.Component {
const modalScreenOptions = {
headerShown: false,
cardStyle: getNavigationModalCardStyle(this.props.isSmallScreenWidth),
cardStyleInterpolator: modalCardStyleInterpolator,
cardStyleInterpolator: props => modalCardStyleInterpolator(this.props.isSmallScreenWidth, props),
animationEnabled: true,
gestureDirection: 'horizontal',
cardOverlayEnabled: true,
Expand Down
18 changes: 11 additions & 7 deletions src/libs/Navigation/AppNavigator/modalCardStyleInterpolator.js
Original file line number Diff line number Diff line change
@@ -1,15 +1,19 @@
import {Animated} from 'react-native';
import variables from '../../../styles/variables';

export default ({
current: {progress},
inverted,
layouts: {
screen,
export default (
isSmallScreen,
{
current: {progress},
inverted,
layouts: {
screen,
},
},
}) => {
) => {
const translateX = Animated.multiply(progress.interpolate({
inputRange: [0, 1],
outputRange: [screen.width, 0],
outputRange: [isSmallScreen ? screen.width : variables.sideBarWidth, 0],
extrapolate: 'clamp',
}), inverted);

Expand Down
52 changes: 43 additions & 9 deletions src/libs/OptionsListUtils.js
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,34 @@ function getPersonalDetailsForLogins(logins, personalDetails) {
});
}

/**
* Constructs a Set with all possible names (displayName, firstName, lastName, email) for all participants in a report,
* to be used in isSearchStringMatch.
*
* @param {Array<Object>} personalDetailList
* @return {Set<String>}
*/
function getParticipantNames(personalDetailList) {
// We use a Set because `Set.has(value)` on a Set of with n entries is up to n (or log(n)) times faster than
// `_.contains(Array, value)` for an Array with n members.
const participantNames = new Set();
_.each(personalDetailList, (participant) => {
if (participant.login) {
participantNames.add(participant.login.toLowerCase());
}
if (participant.firstName) {
participantNames.add(participant.firstName.toLowerCase());
}
if (participant.lastName) {
participantNames.add(participant.lastName.toLowerCase());
}
if (participant.displayName) {
participantNames.add(participant.displayName.toLowerCase());
}
});
return participantNames;
}

/**
* Returns a string with all relevant search terms
*
Expand All @@ -62,8 +90,8 @@ function getSearchText(report, personalDetailList) {
searchTerms.push(personalDetail.displayName);
searchTerms.push(personalDetail.login);
});

if (report) {
searchTerms.push(...report.reportName);
searchTerms.push(...report.reportName.split(',').map(name => name.trim()));
searchTerms.push(...report.participants);
}
Expand Down Expand Up @@ -140,14 +168,18 @@ Onyx.connect({
*
* @param {String} searchValue
* @param {String} searchText
* @param {Set<String>} participantNames
* @returns {Boolean}
*/
function isSearchStringMatch(searchValue, searchText) {
const searchWords = searchValue.split(' ');
function isSearchStringMatch(searchValue, searchText, participantNames) {
const searchWords = searchValue
.replace(/,/g, ' ')
.split(' ')
.map(word => word.trim());
return _.every(searchWords, (word) => {
const matchRegex = new RegExp(Str.escapeForRegExp(word), 'i');
const valueToSearch = searchText && searchText.replace(new RegExp(/&nbsp;/g), '');
return matchRegex.test(valueToSearch);
return matchRegex.test(valueToSearch) || participantNames.has(word);
});
}

Expand Down Expand Up @@ -250,8 +282,10 @@ function getOptions(reports, personalDetails, draftComments, activeReportID, {
continue;
}

// Finally check to see if this options is a match for the provided search string if we have one
if (searchValue && !isSearchStringMatch(searchValue, reportOption.searchText)) {
// Finally check to see if this option is a match for the provided search string if we have one
const {searchText, participantsList} = reportOption;
const participantNames = getParticipantNames(participantsList);
if (searchValue && !isSearchStringMatch(searchValue, searchText, participantNames)) {
continue;
}

Expand Down Expand Up @@ -284,11 +318,11 @@ function getOptions(reports, personalDetails, draftComments, activeReportID, {
))) {
return;
}

if (searchValue && !isSearchStringMatch(searchValue, personalDetailOption.searchText)) {
const {searchText, participantsList} = personalDetailOption;
const participantNames = getParticipantNames(participantsList);
if (searchValue && !isSearchStringMatch(searchValue, searchText, participantNames)) {
return;
}

personalDetailsOptions.push(personalDetailOption);
});
}
Expand Down
19 changes: 19 additions & 0 deletions src/libs/onScreenTransitionEnd/index.ios.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
/**
* Navigation's transitionEnd is not reliable on IOS thus we use InteractonManager
* Some information https://github.com/software-mansion/react-native-screens/issues/713
*/
import {InteractionManager} from 'react-native';

/**
* Call the callback after screen transiton has ended
*
* @param {Object} navigation Screen navigation prop
* @param {Function} callback Method to call
* @returns {Function}
*/
function onScreenTransitionEnd(navigation, callback) {
const handle = InteractionManager.runAfterInteractions(callback);
return () => handle.cancel();
}

export default onScreenTransitionEnd;
14 changes: 14 additions & 0 deletions src/libs/onScreenTransitionEnd/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import Str from 'expensify-common/lib/str';

/**
* Call the callback after screen transiton has ended
*
* @param {Object} navigation Screen navigation prop
* @param {Function} callback Method to call
* @returns {Function}
*/
function onScreenTransitionEnd(navigation, callback) {
return navigation.addListener('transitionEnd', evt => Str.result(callback, evt));
}

export default onScreenTransitionEnd;
75 changes: 41 additions & 34 deletions src/pages/NewChatPage.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import withWindowDimensions, {windowDimensionsPropTypes} from '../components/wit
import HeaderWithCloseButton from '../components/HeaderWithCloseButton';
import Navigation from '../libs/Navigation/Navigation';
import ScreenWrapper from '../components/ScreenWrapper';
import FullScreenLoadingIndicator from '../components/FullscreenLoadingIndicator';
import withLocalize, {withLocalizePropTypes} from '../components/withLocalize';
import compose from '../libs/compose';

Expand Down Expand Up @@ -53,7 +54,6 @@ class NewChatPage extends Component {
super(props);

this.createNewChat = this.createNewChat.bind(this);

const {
recentReports,
personalDetails,
Expand Down Expand Up @@ -127,39 +127,46 @@ class NewChatPage extends Component {

return (
<ScreenWrapper>
<HeaderWithCloseButton
title={this.props.translate('sidebarScreen.newChat')}
onCloseButtonPress={() => Navigation.dismissModal(true)}
/>
<View style={[styles.flex1, styles.w100]}>
<OptionsSelector
sections={sections}
value={this.state.searchValue}
onSelectRow={this.createNewChat}
onChangeText={(searchValue = '') => {
const {
recentReports,
personalDetails,
userToInvite,
} = getNewChatOptions(
this.props.reports,
this.props.personalDetails,
searchValue,
);
this.setState({
searchValue,
recentReports,
userToInvite,
personalDetails,
});
}}
headerMessage={headerMessage}
disableArrowKeysActions
hideAdditionalOptionStates
forceTextUnreadStyle
/>
</View>
<KeyboardSpacer />
{({didScreenTransitionEnd}) => (
<>
<HeaderWithCloseButton
title={this.props.translate('sidebarScreen.newChat')}
onCloseButtonPress={() => Navigation.dismissModal(true)}
/>
<View style={[styles.flex1, styles.w100, styles.pRelative]}>
<FullScreenLoadingIndicator visible={!didScreenTransitionEnd} />
{didScreenTransitionEnd && (
<OptionsSelector
sections={sections}
value={this.state.searchValue}
onSelectRow={this.createNewChat}
onChangeText={(searchValue = '') => {
const {
recentReports,
personalDetails,
userToInvite,
} = getNewChatOptions(
this.props.reports,
this.props.personalDetails,
searchValue,
);
this.setState({
searchValue,
recentReports,
userToInvite,
personalDetails,
});
}}
headerMessage={headerMessage}
disableArrowKeysActions
hideAdditionalOptionStates
forceTextUnreadStyle
/>
)}
</View>
<KeyboardSpacer />
</>
)}
</ScreenWrapper>
);
}
Expand Down
Loading

0 comments on commit 62b08ca

Please sign in to comment.