diff --git a/android/app/build.gradle b/android/app/build.gradle index eb562c91ba9e..9717b8bdb5ae 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -106,8 +106,8 @@ android { minSdkVersion rootProject.ext.minSdkVersion targetSdkVersion rootProject.ext.targetSdkVersion multiDexEnabled rootProject.ext.multiDexEnabled - versionCode 1001031102 - versionName "1.3.11-2" + versionCode 1001031200 + versionName "1.3.12-0" } splits { diff --git a/ios/NewExpensify/Info.plist b/ios/NewExpensify/Info.plist index 5700fa2f1e14..0b9fed869cfb 100644 --- a/ios/NewExpensify/Info.plist +++ b/ios/NewExpensify/Info.plist @@ -17,7 +17,7 @@ CFBundlePackageType APPL CFBundleShortVersionString - 1.3.11 + 1.3.12 CFBundleSignature ???? CFBundleURLTypes @@ -30,7 +30,7 @@ CFBundleVersion - 1.3.11.2 + 1.3.12.0 ITSAppUsesNonExemptEncryption LSApplicationQueriesSchemes diff --git a/ios/NewExpensifyTests/Info.plist b/ios/NewExpensifyTests/Info.plist index 97baae013fcd..16721dc12277 100644 --- a/ios/NewExpensifyTests/Info.plist +++ b/ios/NewExpensifyTests/Info.plist @@ -15,10 +15,10 @@ CFBundlePackageType BNDL CFBundleShortVersionString - 1.3.11 + 1.3.12 CFBundleSignature ???? CFBundleVersion - 1.3.11.2 + 1.3.12.0 diff --git a/package-lock.json b/package-lock.json index bb48c6d6fa3a..d64762c9172d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "new.expensify", - "version": "1.3.11-2", + "version": "1.3.12-0", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "new.expensify", - "version": "1.3.11-2", + "version": "1.3.12-0", "hasInstallScript": true, "license": "MIT", "dependencies": { diff --git a/package.json b/package.json index 477fdb2dfe5b..9598abd9edf7 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "new.expensify", - "version": "1.3.11-2", + "version": "1.3.12-0", "author": "Expensify, Inc.", "homepage": "https://new.expensify.com", "description": "New Expensify is the next generation of Expensify: a reimagination of payments based atop a foundation of chat.", diff --git a/src/components/ArrowKeyFocusManager.js b/src/components/ArrowKeyFocusManager.js index e548067f17aa..72a590547fa0 100644 --- a/src/components/ArrowKeyFocusManager.js +++ b/src/components/ArrowKeyFocusManager.js @@ -52,7 +52,7 @@ class ArrowKeyFocusManager extends Component { } this.props.onFocusedIndexChanged(newFocusedIndex); - }, arrowUpConfig.descriptionKey, arrowUpConfig.modifiers, true, false, 1, true, [this.props.shouldExcludeTextAreaNodes && 'TEXTAREA']); + }, arrowUpConfig.descriptionKey, arrowUpConfig.modifiers, true, false, 0, true, [this.props.shouldExcludeTextAreaNodes && 'TEXTAREA']); this.unsubscribeArrowDownKey = KeyboardShortcut.subscribe(arrowDownConfig.shortcutKey, () => { if (this.props.maxIndex < 0) { @@ -70,7 +70,7 @@ class ArrowKeyFocusManager extends Component { } this.props.onFocusedIndexChanged(newFocusedIndex); - }, arrowDownConfig.descriptionKey, arrowDownConfig.modifiers, true, false, 1, true, [this.props.shouldExcludeTextAreaNodes && 'TEXTAREA']); + }, arrowDownConfig.descriptionKey, arrowDownConfig.modifiers, true, false, 0, true, [this.props.shouldExcludeTextAreaNodes && 'TEXTAREA']); } componentWillUnmount() { diff --git a/src/components/ButtonWithMenu.js b/src/components/ButtonWithMenu.js index 1f8d9a7c0ec5..eea6b5ffb835 100644 --- a/src/components/ButtonWithMenu.js +++ b/src/components/ButtonWithMenu.js @@ -75,7 +75,6 @@ class ButtonWithMenu extends PureComponent { text={selectedItem.text} onPress={event => this.props.onPress(event, this.props.options[0].value)} pressOnEnter - enterKeyEventListenerPriority={1} /> )} {this.props.options.length > 1 && ( diff --git a/src/components/HTMLEngineProvider/BaseHTMLEngineProvider.js b/src/components/HTMLEngineProvider/BaseHTMLEngineProvider.js index f195d4bc624d..b49896ac3fed 100755 --- a/src/components/HTMLEngineProvider/BaseHTMLEngineProvider.js +++ b/src/components/HTMLEngineProvider/BaseHTMLEngineProvider.js @@ -49,6 +49,7 @@ const customHTMLElementModels = { tagName: 'strong', mixedUAStyles: {whiteSpace: 'pre'}, }), + 'mention-user': defaultHTMLElementModels.span.extend({tagName: 'mention-user'}), }; // We are using the explicit composite architecture for performance gains. diff --git a/src/components/HTMLEngineProvider/HTMLRenderers/MentionUserRenderer.js b/src/components/HTMLEngineProvider/HTMLRenderers/MentionUserRenderer.js new file mode 100644 index 000000000000..91ac89fdc33c --- /dev/null +++ b/src/components/HTMLEngineProvider/HTMLRenderers/MentionUserRenderer.js @@ -0,0 +1,59 @@ +import React from 'react'; +import _ from 'underscore'; +import { + TNodeChildrenRenderer, +} from 'react-native-render-html'; +import Navigation from '../../../libs/Navigation/Navigation'; +import ROUTES from '../../../ROUTES'; +import Text from '../../Text'; +import Tooltip from '../../Tooltip'; +import htmlRendererPropTypes from './htmlRendererPropTypes'; +import withCurrentUserPersonalDetails from '../../withCurrentUserPersonalDetails'; +import personalDetailsPropType from '../../../pages/personalDetailsPropType'; +import * as StyleUtils from '../../../styles/StyleUtils'; + +const propTypes = { + ...htmlRendererPropTypes, + + /** + * Current user personal details + */ + currentUserPersonalDetails: personalDetailsPropType.isRequired, +}; + +/** + * Navigates to user details screen based on email + * @param {String} email + * @returns {void} + * */ +const showUserDetails = email => Navigation.navigate(ROUTES.getDetailsRoute(email)); + +const MentionUserRenderer = (props) => { + const defaultRendererProps = _.omit(props, ['TDefaultRenderer', 'style']); + + // We need to remove the leading @ from data as it is not part of the login + const loginWhithoutLeadingAt = props.tnode.data.slice(1); + + const isOurMention = loginWhithoutLeadingAt === props.currentUserPersonalDetails.login; + + return ( + + + showUserDetails(loginWhithoutLeadingAt)} + > + + + + + ); +}; + +MentionUserRenderer.propTypes = propTypes; +MentionUserRenderer.displayName = 'MentionUserRenderer'; + +export default withCurrentUserPersonalDetails(MentionUserRenderer); diff --git a/src/components/HTMLEngineProvider/HTMLRenderers/index.js b/src/components/HTMLEngineProvider/HTMLRenderers/index.js index 4b9d0fc85962..01a0721cd5c4 100644 --- a/src/components/HTMLEngineProvider/HTMLRenderers/index.js +++ b/src/components/HTMLEngineProvider/HTMLRenderers/index.js @@ -2,6 +2,7 @@ import AnchorRenderer from './AnchorRenderer'; import CodeRenderer from './CodeRenderer'; import EditedRenderer from './EditedRenderer'; import ImageRenderer from './ImageRenderer'; +import MentionUserRenderer from './MentionUserRenderer'; import PreRenderer from './PreRenderer'; /** @@ -16,4 +17,5 @@ export default { // Custom tag renderers edited: EditedRenderer, pre: PreRenderer, + 'mention-user': MentionUserRenderer, }; diff --git a/src/components/KeyboardShortcutsModal.js b/src/components/KeyboardShortcutsModal.js index 845a8779a731..627cccf77e6b 100644 --- a/src/components/KeyboardShortcutsModal.js +++ b/src/components/KeyboardShortcutsModal.js @@ -38,6 +38,10 @@ class KeyboardShortcutsModal extends React.Component { const openShortcutModalConfig = CONST.KEYBOARD_SHORTCUTS.SHORTCUT_MODAL; this.unsubscribeShortcutModal = KeyboardShortcut.subscribe(openShortcutModalConfig.shortcutKey, () => { + if (this.props.isShortcutsModalOpen) { + return; + } + ModalActions.close(); KeyboardShortcutsActions.showKeyboardShortcutModal(); }, openShortcutModalConfig.descriptionKey, openShortcutModalConfig.modifiers, true); @@ -161,6 +165,9 @@ export default compose( withWindowDimensions, withLocalize, withOnyx({ - isShortcutsModalOpen: {key: ONYXKEYS.IS_SHORTCUTS_MODAL_OPEN}, + isShortcutsModalOpen: { + key: ONYXKEYS.IS_SHORTCUTS_MODAL_OPEN, + initWithStoredValues: false, + }, }), )(KeyboardShortcutsModal); diff --git a/src/components/Onfido/index.native.js b/src/components/Onfido/index.native.js index 41d91a9e259a..e93d2731b479 100644 --- a/src/components/Onfido/index.native.js +++ b/src/components/Onfido/index.native.js @@ -12,6 +12,7 @@ import onfidoPropTypes from './onfidoPropTypes'; import CONST from '../../CONST'; import withLocalize, {withLocalizePropTypes} from '../withLocalize'; import Log from '../../libs/Log'; +import FullscreenLoadingIndicator from '../FullscreenLoadingIndicator'; const propTypes = { ...withLocalizePropTypes, @@ -88,7 +89,7 @@ class Onfido extends React.Component { } render() { - return null; + return ; } } diff --git a/src/components/OptionsSelector/BaseOptionsSelector.js b/src/components/OptionsSelector/BaseOptionsSelector.js index 3d0362062f2d..01d1612b65d6 100755 --- a/src/components/OptionsSelector/BaseOptionsSelector.js +++ b/src/components/OptionsSelector/BaseOptionsSelector.js @@ -70,7 +70,6 @@ class BaseOptionsSelector extends Component { enterConfig.modifiers, true, () => !this.state.allOptions[this.state.focusedIndex], - 1, ); const CTRLEnterConfig = CONST.KEYBOARD_SHORTCUTS.CTRL_ENTER; @@ -360,7 +359,7 @@ class BaseOptionsSelector extends Component { text={defaultConfirmButtonText} onPress={this.props.onConfirmSelection} pressOnEnter - enterKeyEventListenerPriority={2} + enterKeyEventListenerPriority={1} /> )} {this.props.footerContent} diff --git a/src/components/ReportActionItem/IOUPreview.js b/src/components/ReportActionItem/IOUPreview.js index d3a43c07384e..95866a057e66 100644 --- a/src/components/ReportActionItem/IOUPreview.js +++ b/src/components/ReportActionItem/IOUPreview.js @@ -7,6 +7,7 @@ import PropTypes from 'prop-types'; import {withOnyx} from 'react-native-onyx'; import lodashGet from 'lodash/get'; import _ from 'underscore'; +import Str from 'expensify-common/lib/str'; import compose from '../../libs/compose'; import styles from '../../styles/styles'; import ONYXKEYS from '../../ONYXKEYS'; @@ -85,6 +86,13 @@ const propTypes = { /** True if the IOU Preview card is hovered */ isHovered: PropTypes.bool, + /** All of the personal details for everyone */ + personalDetails: PropTypes.objectOf(PropTypes.shape({ + + /** This is either the user's full name, or their login if full name is an empty string */ + displayName: PropTypes.string.isRequired, + })), + /** Session info for the currently logged in user. */ session: PropTypes.shape({ /** Currently logged in user email */ @@ -117,6 +125,7 @@ const defaultProps = { walletTerms: {}, pendingAction: null, isHovered: false, + personalDetails: {}, session: { email: null, }, @@ -138,7 +147,7 @@ const IOUPreview = (props) => { // When displaying within a IOUDetailsModal we cannot guarentee that participants are included in the originalMessage data // Because an IOUPreview of type split can never be rendered within the IOUDetailsModal, manually building the email array is only needed for non-billSplit ious const participantEmails = props.isBillSplit ? props.action.originalMessage.participants : [managerEmail, ownerEmail]; - const participantAvatars = OptionsListUtils.getAvatarsForLogins(participantEmails); + const participantAvatars = OptionsListUtils.getAvatarsForLogins(participantEmails, props.personalDetails); // Pay button should only be visible to the manager of the report. const isCurrentUserManager = managerEmail === sessionEmail; @@ -210,7 +219,7 @@ const IOUPreview = (props) => { )} - {lodashGet(props.action, 'originalMessage.comment', '')} + {Str.htmlDecode(lodashGet(props.action, 'originalMessage.comment', ''))} {(isCurrentUserManager && !props.shouldHidePayButton @@ -254,6 +263,9 @@ IOUPreview.displayName = 'IOUPreview'; export default compose( withLocalize, withOnyx({ + personalDetails: { + key: ONYXKEYS.PERSONAL_DETAILS, + }, iouReport: { key: ({iouReportID}) => `${ONYXKEYS.COLLECTION.REPORT}${iouReportID}`, }, diff --git a/src/components/TextInput/BaseTextInput.js b/src/components/TextInput/BaseTextInput.js index 12211de67ba3..176f39ac673d 100644 --- a/src/components/TextInput/BaseTextInput.js +++ b/src/components/TextInput/BaseTextInput.js @@ -25,7 +25,7 @@ class BaseTextInput extends Component { super(props); const value = props.value || props.defaultValue || ''; - const activeLabel = props.forceActiveLabel || value.length > 0 || props.prefixCharacter; + const activeLabel = props.forceActiveLabel || value.length > 0 || Boolean(props.prefixCharacter); this.state = { isFocused: false, @@ -212,14 +212,15 @@ class BaseTextInput extends Component { const isEditable = _.isUndefined(this.props.editable) ? !this.props.disabled : this.props.editable; const inputHelpText = this.props.errorText || this.props.hint; const placeholder = (this.props.prefixCharacter || this.state.isFocused || !hasLabel || (hasLabel && this.props.forceActiveLabel)) ? this.props.placeholder : null; + const maxHeight = StyleSheet.flatten(this.props.containerStyles).maxHeight; const textInputContainerStyles = _.reduce([ styles.textInputContainer, ...this.props.textInputContainerStyles, this.props.autoGrow && StyleUtils.getWidthStyle(this.state.textInputWidth), !this.props.hideFocusedState && this.state.isFocused && styles.borderColorFocus, (this.props.hasError || this.props.errorText) && styles.borderColorDanger, + this.props.autoGrowHeight && ({scrollPaddingTop: 2 * maxHeight}), ], (finalStyles, s) => ({...finalStyles, ...s}), {}); - const maxHeight = StyleSheet.flatten(this.props.containerStyles).maxHeight; const isMultiline = this.props.multiline || this.props.autoGrowHeight; return ( @@ -262,6 +263,7 @@ class BaseTextInput extends Component { to prevent text overlapping with label when scrolling */} {isMultiline && } {this.props.label} diff --git a/src/libs/actions/KeyboardShortcuts.js b/src/libs/actions/KeyboardShortcuts.js index d619e9c8f922..f3550839359f 100644 --- a/src/libs/actions/KeyboardShortcuts.js +++ b/src/libs/actions/KeyboardShortcuts.js @@ -5,6 +5,7 @@ let isShortcutsModalOpen; Onyx.connect({ key: ONYXKEYS.IS_SHORTCUTS_MODAL_OPEN, callback: flag => isShortcutsModalOpen = flag, + initWithStoredValues: false, }); /** diff --git a/src/libs/actions/Session/index.js b/src/libs/actions/Session/index.js index f351c4961305..82be9822b4d2 100644 --- a/src/libs/actions/Session/index.js +++ b/src/libs/actions/Session/index.js @@ -652,7 +652,7 @@ function authenticatePusher(socketID, channelName, callback) { */ function requestUnlinkValidationLink() { const optimisticData = [{ - onyxMethod: CONST.ONYX.METHOD.MERGE, + onyxMethod: Onyx.METHOD.MERGE, key: ONYXKEYS.ACCOUNT, value: { isLoading: true, @@ -661,7 +661,7 @@ function requestUnlinkValidationLink() { }, }]; const successData = [{ - onyxMethod: CONST.ONYX.METHOD.MERGE, + onyxMethod: Onyx.METHOD.MERGE, key: ONYXKEYS.ACCOUNT, value: { isLoading: false, @@ -669,7 +669,7 @@ function requestUnlinkValidationLink() { }, }]; const failureData = [{ - onyxMethod: CONST.ONYX.METHOD.MERGE, + onyxMethod: Onyx.METHOD.MERGE, key: ONYXKEYS.ACCOUNT, value: { isLoading: false, @@ -681,7 +681,7 @@ function requestUnlinkValidationLink() { function unlinkLogin(accountID, validateCode) { const optimisticData = [{ - onyxMethod: CONST.ONYX.METHOD.MERGE, + onyxMethod: Onyx.METHOD.MERGE, key: ONYXKEYS.ACCOUNT, value: { ...CONST.DEFAULT_ACCOUNT_DATA, @@ -689,7 +689,7 @@ function unlinkLogin(accountID, validateCode) { }, }]; const successData = [{ - onyxMethod: CONST.ONYX.METHOD.MERGE, + onyxMethod: Onyx.METHOD.MERGE, key: ONYXKEYS.ACCOUNT, value: { isLoading: false, @@ -697,14 +697,14 @@ function unlinkLogin(accountID, validateCode) { }, }, { - onyxMethod: CONST.ONYX.METHOD.MERGE, + onyxMethod: Onyx.METHOD.MERGE, key: ONYXKEYS.CREDENTIALS, value: { login: '', }, }]; const failureData = [{ - onyxMethod: CONST.ONYX.METHOD.MERGE, + onyxMethod: Onyx.METHOD.MERGE, key: ONYXKEYS.ACCOUNT, value: { isLoading: false, diff --git a/src/pages/iou/steps/MoneyRequestAmountPage.js b/src/pages/iou/steps/MoneyRequestAmountPage.js index 4cc0b1b2eef2..783f5d59a803 100755 --- a/src/pages/iou/steps/MoneyRequestAmountPage.js +++ b/src/pages/iou/steps/MoneyRequestAmountPage.js @@ -354,7 +354,6 @@ class MoneyRequestAmountPage extends React.Component { pressOnEnter isDisabled={!this.state.amount.length || parseFloat(this.state.amount) < 0.01} text={this.props.buttonText} - enterKeyEventListenerPriority={1} /> diff --git a/src/pages/workspace/WorkspaceInviteMessagePage.js b/src/pages/workspace/WorkspaceInviteMessagePage.js index 611d5c778863..b0293075ee58 100644 --- a/src/pages/workspace/WorkspaceInviteMessagePage.js +++ b/src/pages/workspace/WorkspaceInviteMessagePage.js @@ -169,7 +169,7 @@ class WorkspaceInviteMessagePage extends React.Component { maxHeight) { - return styles.overflowAuto; + return { + ...styles.pr0, + ...styles.overflowAuto, + }; } return { + ...styles.pr0, ...styles.overflowHidden, height: maxHeight, }; @@ -1109,6 +1113,29 @@ function getGoogleListViewStyle(shouldDisplayBorder) { }; } +/** + * Returns style object for the user mention component based on whether the mention is ours or not. + * @param {Boolean} isOurMention + * @returns {Object} + */ +function getUserMentionStyle(isOurMention) { + const backgroundColor = isOurMention ? themeColors.ourMentionBG : themeColors.mentionBG; + return { + backgroundColor, + borderRadius: variables.componentBorderRadiusSmall, + paddingHorizontal: 2, + }; +} + +/** + * Returns text color for the user mention text based on whether the mention is ours or not. + * @param {Boolean} isOurMention + * @returns {Object} + */ +function getUserMentionTextColor(isOurMention) { + return isOurMention ? themeColors.ourMentionText : themeColors.mentionText; +} + export { getAvatarSize, getAvatarStyle, @@ -1169,4 +1196,6 @@ export { getFontSizeStyle, getSignInWordmarkWidthStyle, getGoogleListViewStyle, + getUserMentionStyle, + getUserMentionTextColor, }; diff --git a/src/styles/colors.js b/src/styles/colors.js index 592715e66ab1..0fc14575ca84 100644 --- a/src/styles/colors.js +++ b/src/styles/colors.js @@ -37,13 +37,17 @@ export default { midnight: '#002140', // Brand Colors from Figma + blue100: '#B0D9FF', blue200: '#8DC8FF', blue400: '#0185FF', + blue600: '#0164BF', blue700: '#003C73', blue800: '#002140', + green100: '#B1F2D6', green200: '#8EECC4', green400: '#03D47C', + green600: '#008C59', green700: '#085239', green800: '#002E22', diff --git a/src/styles/styles.js b/src/styles/styles.js index e6a24c610595..52f6c5447cab 100644 --- a/src/styles/styles.js +++ b/src/styles/styles.js @@ -821,7 +821,6 @@ const styles = { borderBottomWidth: 2, borderColor: themeColors.border, overflow: 'hidden', - scrollPaddingTop: '100%', }, textInputLabel: { @@ -862,7 +861,6 @@ const styles = { paddingTop: 23, paddingBottom: 8, paddingLeft: 0, - paddingRight: 0, borderWidth: 0, }, @@ -2447,7 +2445,7 @@ const styles = { hiddenElementOutsideOfWindow: { position: 'absolute', - top: 0, + top: -10000, left: 0, opacity: 0, }, diff --git a/src/styles/themes/default.js b/src/styles/themes/default.js index c35b0d53eb26..0917d1d5b794 100644 --- a/src/styles/themes/default.js +++ b/src/styles/themes/default.js @@ -69,6 +69,10 @@ const darkTheme = { reactionActive: '#003C73', badgeAdHoc: colors.pink600, badgeAdHocHover: colors.pink700, + mentionText: colors.blue100, + mentionBG: colors.blue600, + ourMentionText: colors.green100, + ourMentionBG: colors.green600, }; const oldTheme = { diff --git a/tests/unit/OptionsListUtilsTest.js b/tests/unit/OptionsListUtilsTest.js index 1f39d30154fb..fa3ee0840012 100644 --- a/tests/unit/OptionsListUtilsTest.js +++ b/tests/unit/OptionsListUtilsTest.js @@ -631,8 +631,9 @@ describe('OptionsListUtils', () => { // When we pass an empty search value let results = OptionsListUtils.getShareDestinationOptions(REPORTS, PERSONAL_DETAILS, [], ''); - // Then we should expect 10 recent reports to show because we're grabbing DM chats and group chats - expect(results.recentReports.length).toBe(10); + // Then we should expect 5 recent reports to show because we're grabbing DM chats and group chats + // because we've limited the number of recent reports to 5 + expect(results.recentReports.length).toBe(5); // When we pass a search value that doesn't match the group chat name results = OptionsListUtils.getShareDestinationOptions(REPORTS, PERSONAL_DETAILS, [], 'mutants'); @@ -650,7 +651,8 @@ describe('OptionsListUtils', () => { results = OptionsListUtils.getShareDestinationOptions(REPORTS_WITH_WORKSPACE_ROOMS, PERSONAL_DETAILS, [], ''); // Then we should expect the DMS, the group chats and the workspace room to show - expect(results.recentReports.length).toBe(11); + // We should expect 5 recent reports to show because we've limited the number of recent reports to 5 + expect(results.recentReports.length).toBe(5); // When we search for a workspace room results = OptionsListUtils.getShareDestinationOptions(REPORTS_WITH_WORKSPACE_ROOMS, PERSONAL_DETAILS, [], 'Avengers Room');