diff --git a/.github/actions/composite/setupNode/action.yml b/.github/actions/composite/setupNode/action.yml index d475acf5380f..643c707da230 100644 --- a/.github/actions/composite/setupNode/action.yml +++ b/.github/actions/composite/setupNode/action.yml @@ -12,6 +12,6 @@ runs: - name: Install node packages uses: nick-invision/retry@0711ba3d7808574133d713a0d92d2941be03a350 with: - timeout_minutes: 10 - max_attempts: 5 + timeout_minutes: 30 + max_attempts: 3 command: npm ci diff --git a/.github/workflows/platformDeploy.yml b/.github/workflows/platformDeploy.yml index 4a8a3fd732c0..e1b38b713d7b 100644 --- a/.github/workflows/platformDeploy.yml +++ b/.github/workflows/platformDeploy.yml @@ -10,7 +10,7 @@ on: env: SHOULD_DEPLOY_PRODUCTION: ${{ github.event_name == 'release' }} - DEVELOPER_DIR: /Applications/Xcode_14.0.1.app/Contents/Developer + DEVELOPER_DIR: /Applications/Xcode_14.1.app/Contents/Developer jobs: validateActor: diff --git a/.github/workflows/testBuild.yml b/.github/workflows/testBuild.yml index 6b068c9f6f8e..f52453dcdf40 100644 --- a/.github/workflows/testBuild.yml +++ b/.github/workflows/testBuild.yml @@ -11,7 +11,7 @@ on: branches: ['*ci-test/**'] env: - DEVELOPER_DIR: /Applications/Xcode_14.0.1.app/Contents/Developer + DEVELOPER_DIR: /Applications/Xcode_14.1.app/Contents/Developer jobs: validateActor: diff --git a/android/app/build.gradle b/android/app/build.gradle index eebe2c40c3a4..6ddd0630e3cb 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -156,8 +156,8 @@ android { minSdkVersion rootProject.ext.minSdkVersion targetSdkVersion rootProject.ext.targetSdkVersion multiDexEnabled rootProject.ext.multiDexEnabled - versionCode 1001029004 - versionName "1.2.90-4" + versionCode 1001029006 + versionName "1.2.90-6" buildConfigField "boolean", "IS_NEW_ARCHITECTURE_ENABLED", isNewArchitectureEnabled().toString() if (isNewArchitectureEnabled()) { diff --git a/docs/_layouts/default.html b/docs/_layouts/default.html index ac648bfe4fed..9c256602144a 100644 --- a/docs/_layouts/default.html +++ b/docs/_layouts/default.html @@ -2,6 +2,7 @@ + Expensify Help diff --git a/ios/NewExpensify.xcodeproj/project.pbxproj b/ios/NewExpensify.xcodeproj/project.pbxproj index ee77a2595272..192571488305 100644 --- a/ios/NewExpensify.xcodeproj/project.pbxproj +++ b/ios/NewExpensify.xcodeproj/project.pbxproj @@ -61,28 +61,28 @@ 13B07FAF1A68108700A75B9A /* AppDelegate.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = AppDelegate.h; path = NewExpensify/AppDelegate.h; sourceTree = ""; }; 13B07FB61A68108700A75B9A /* Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = Info.plist; path = NewExpensify/Info.plist; sourceTree = ""; }; 13B07FB71A68108700A75B9A /* main.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = main.m; path = NewExpensify/main.m; sourceTree = ""; }; - 177D06D4BF2346EB90E37D3D /* ExpensifyMono-Bold.otf */ = {isa = PBXFileReference; explicitFileType = undefined; fileEncoding = undefined; includeInIndex = 0; lastKnownFileType = unknown; name = "ExpensifyMono-Bold.otf"; path = "../assets/fonts/native/ExpensifyMono-Bold.otf"; sourceTree = ""; }; + 177D06D4BF2346EB90E37D3D /* ExpensifyMono-Bold.otf */ = {isa = PBXFileReference; explicitFileType = undefined; fileEncoding = 9; includeInIndex = 0; lastKnownFileType = unknown; name = "ExpensifyMono-Bold.otf"; path = "../assets/fonts/native/ExpensifyMono-Bold.otf"; sourceTree = ""; }; 18D050DF262400AF000D658B /* BridgingFile.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BridgingFile.swift; sourceTree = ""; }; - 1977066010294D51AEB35F3B /* ExpensifyNeue-Italic.otf */ = {isa = PBXFileReference; explicitFileType = undefined; fileEncoding = undefined; includeInIndex = 0; lastKnownFileType = unknown; name = "ExpensifyNeue-Italic.otf"; path = "../assets/fonts/native/ExpensifyNeue-Italic.otf"; sourceTree = ""; }; - 1B3F09A4E4EA4CFFA5E4E7CD /* ExpensifyMono-Regular.otf */ = {isa = PBXFileReference; explicitFileType = undefined; fileEncoding = undefined; includeInIndex = 0; lastKnownFileType = unknown; name = "ExpensifyMono-Regular.otf"; path = "../assets/fonts/native/ExpensifyMono-Regular.otf"; sourceTree = ""; }; + 1977066010294D51AEB35F3B /* ExpensifyNeue-Italic.otf */ = {isa = PBXFileReference; explicitFileType = undefined; fileEncoding = 9; includeInIndex = 0; lastKnownFileType = unknown; name = "ExpensifyNeue-Italic.otf"; path = "../assets/fonts/native/ExpensifyNeue-Italic.otf"; sourceTree = ""; }; + 1B3F09A4E4EA4CFFA5E4E7CD /* ExpensifyMono-Regular.otf */ = {isa = PBXFileReference; explicitFileType = undefined; fileEncoding = 9; includeInIndex = 0; lastKnownFileType = unknown; name = "ExpensifyMono-Regular.otf"; path = "../assets/fonts/native/ExpensifyMono-Regular.otf"; sourceTree = ""; }; 374FB8D528A133A7000D84EF /* OriginImageRequestHandler.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = OriginImageRequestHandler.h; path = NewExpensify/OriginImageRequestHandler.h; sourceTree = ""; }; 374FB8D628A133FE000D84EF /* OriginImageRequestHandler.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; name = OriginImageRequestHandler.mm; path = NewExpensify/OriginImageRequestHandler.mm; sourceTree = ""; }; - 38E61473EAA34C598CB6B345 /* ExpensifyNeue-BoldItalic.otf */ = {isa = PBXFileReference; explicitFileType = undefined; fileEncoding = undefined; includeInIndex = 0; lastKnownFileType = unknown; name = "ExpensifyNeue-BoldItalic.otf"; path = "../assets/fonts/native/ExpensifyNeue-BoldItalic.otf"; sourceTree = ""; }; + 38E61473EAA34C598CB6B345 /* ExpensifyNeue-BoldItalic.otf */ = {isa = PBXFileReference; explicitFileType = undefined; fileEncoding = 9; includeInIndex = 0; lastKnownFileType = unknown; name = "ExpensifyNeue-BoldItalic.otf"; path = "../assets/fonts/native/ExpensifyNeue-BoldItalic.otf"; sourceTree = ""; }; 3EDF186626B8D2CBA203E08D /* libPods-NewExpensify.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-NewExpensify.a"; sourceTree = BUILT_PRODUCTS_DIR; }; - 44BF435285B94E5B95F90994 /* ExpensifyNewKansas-Medium.otf */ = {isa = PBXFileReference; explicitFileType = undefined; fileEncoding = undefined; includeInIndex = 0; lastKnownFileType = unknown; name = "ExpensifyNewKansas-Medium.otf"; path = "../assets/fonts/native/ExpensifyNewKansas-Medium.otf"; sourceTree = ""; }; - 6BEDED270C49437581EBE50D /* ExpensifyNeue-Regular.otf */ = {isa = PBXFileReference; explicitFileType = undefined; fileEncoding = undefined; includeInIndex = 0; lastKnownFileType = unknown; name = "ExpensifyNeue-Regular.otf"; path = "../assets/fonts/native/ExpensifyNeue-Regular.otf"; sourceTree = ""; }; + 44BF435285B94E5B95F90994 /* ExpensifyNewKansas-Medium.otf */ = {isa = PBXFileReference; explicitFileType = undefined; fileEncoding = 9; includeInIndex = 0; lastKnownFileType = unknown; name = "ExpensifyNewKansas-Medium.otf"; path = "../assets/fonts/native/ExpensifyNewKansas-Medium.otf"; sourceTree = ""; }; + 6BEDED270C49437581EBE50D /* ExpensifyNeue-Regular.otf */ = {isa = PBXFileReference; explicitFileType = undefined; fileEncoding = 9; includeInIndex = 0; lastKnownFileType = unknown; name = "ExpensifyNeue-Regular.otf"; path = "../assets/fonts/native/ExpensifyNeue-Regular.otf"; sourceTree = ""; }; 7041848326A8E40900E09F4D /* RCTStartupTimer.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = RCTStartupTimer.h; path = NewExpensify/RCTStartupTimer.h; sourceTree = ""; }; 7041848426A8E47D00E09F4D /* RCTStartupTimer.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; name = RCTStartupTimer.m; path = NewExpensify/RCTStartupTimer.m; sourceTree = ""; }; 70CF6E81262E297300711ADC /* BootSplash.storyboard */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.storyboard; name = BootSplash.storyboard; path = NewExpensify/BootSplash.storyboard; sourceTree = ""; }; A2695CF895D81C31AFB4A074 /* libPods-NewExpensify-NewExpensifyTests.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-NewExpensify-NewExpensifyTests.a"; sourceTree = BUILT_PRODUCTS_DIR; }; B66D52EC75F78B8A06F1E035 /* Pods-NewExpensify.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-NewExpensify.debug.xcconfig"; path = "Target Support Files/Pods-NewExpensify/Pods-NewExpensify.debug.xcconfig"; sourceTree = ""; }; - D2AFB39EC1D44BF9B91D3227 /* ExpensifyNewKansas-MediumItalic.otf */ = {isa = PBXFileReference; explicitFileType = undefined; fileEncoding = undefined; includeInIndex = 0; lastKnownFileType = unknown; name = "ExpensifyNewKansas-MediumItalic.otf"; path = "../assets/fonts/native/ExpensifyNewKansas-MediumItalic.otf"; sourceTree = ""; }; + D2AFB39EC1D44BF9B91D3227 /* ExpensifyNewKansas-MediumItalic.otf */ = {isa = PBXFileReference; explicitFileType = undefined; fileEncoding = 9; includeInIndex = 0; lastKnownFileType = unknown; name = "ExpensifyNewKansas-MediumItalic.otf"; path = "../assets/fonts/native/ExpensifyNewKansas-MediumItalic.otf"; sourceTree = ""; }; DD7904292792E76D004484B4 /* RCTBootSplash.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = RCTBootSplash.h; path = NewExpensify/RCTBootSplash.h; sourceTree = ""; }; DD79042A2792E76D004484B4 /* RCTBootSplash.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = RCTBootSplash.m; path = NewExpensify/RCTBootSplash.m; sourceTree = ""; }; E9DF872C2525201700607FDC /* AirshipConfig.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = AirshipConfig.plist; sourceTree = ""; }; ED297162215061F000B7C4FE /* JavaScriptCore.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = JavaScriptCore.framework; path = System/Library/Frameworks/JavaScriptCore.framework; sourceTree = SDKROOT; }; ED2971642150620600B7C4FE /* JavaScriptCore.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = JavaScriptCore.framework; path = Platforms/AppleTVOS.platform/Developer/SDKs/AppleTVOS12.0.sdk/System/Library/Frameworks/JavaScriptCore.framework; sourceTree = DEVELOPER_DIR; }; - EDFC169F9D7A43BDB924151F /* ExpensifyNeue-Bold.otf */ = {isa = PBXFileReference; explicitFileType = undefined; fileEncoding = undefined; includeInIndex = 0; lastKnownFileType = unknown; name = "ExpensifyNeue-Bold.otf"; path = "../assets/fonts/native/ExpensifyNeue-Bold.otf"; sourceTree = ""; }; + EDFC169F9D7A43BDB924151F /* ExpensifyNeue-Bold.otf */ = {isa = PBXFileReference; explicitFileType = undefined; fileEncoding = 9; includeInIndex = 0; lastKnownFileType = unknown; name = "ExpensifyNeue-Bold.otf"; path = "../assets/fonts/native/ExpensifyNeue-Bold.otf"; sourceTree = ""; }; F0C450E92705020500FD2970 /* colors.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; name = colors.json; path = ../colors.json; sourceTree = ""; }; /* End PBXFileReference section */ @@ -744,7 +744,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 11.0; + IPHONEOS_DEPLOYMENT_TARGET = 16.1; LD_RUNPATH_SEARCH_PATHS = "/usr/lib/swift $(inherited)"; LIBRARY_SEARCH_PATHS = ( "$(SDKROOT)/usr/lib/swift", @@ -799,7 +799,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 11.0; + IPHONEOS_DEPLOYMENT_TARGET = 16.1; LD_RUNPATH_SEARCH_PATHS = "/usr/lib/swift $(inherited)"; LIBRARY_SEARCH_PATHS = ( "$(SDKROOT)/usr/lib/swift", diff --git a/ios/NewExpensify/Info.plist b/ios/NewExpensify/Info.plist index cff7e96c5ec0..75e0a39c60c9 100644 --- a/ios/NewExpensify/Info.plist +++ b/ios/NewExpensify/Info.plist @@ -30,7 +30,7 @@ CFBundleVersion - 1.2.90.4 + 1.2.90.6 ITSAppUsesNonExemptEncryption LSApplicationQueriesSchemes diff --git a/ios/NewExpensifyTests/Info.plist b/ios/NewExpensifyTests/Info.plist index bee389de6da6..bb56aa13111a 100644 --- a/ios/NewExpensifyTests/Info.plist +++ b/ios/NewExpensifyTests/Info.plist @@ -19,6 +19,6 @@ CFBundleSignature ???? CFBundleVersion - 1.2.90.4 + 1.2.90.6 diff --git a/package-lock.json b/package-lock.json index 890859c71d19..5a79292f32ed 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "new.expensify", - "version": "1.2.90-4", + "version": "1.2.90-6", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "new.expensify", - "version": "1.2.90-4", + "version": "1.2.90-6", "hasInstallScript": true, "license": "MIT", "dependencies": { @@ -135,7 +135,7 @@ "css-loader": "^6.7.2", "diff-so-fancy": "^1.3.0", "dotenv": "^16.0.3", - "electron": "^22.3.4", + "electron": "22.3.4", "electron-builder": "23.5.0", "electron-notarize": "^1.2.1", "eslint": "^7.6.0", diff --git a/package.json b/package.json index 66c0613cdd3a..7c384a103aeb 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "new.expensify", - "version": "1.2.90-4", + "version": "1.2.90-6", "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/CONST.js b/src/CONST.js index 0e19417f5962..53d176719019 100755 --- a/src/CONST.js +++ b/src/CONST.js @@ -606,6 +606,10 @@ const CONST = { WIDTH: 320, HEIGHT: 416, }, + CATEGORY_SHORTCUT_BAR_HEIGHT: 40, + SMALL_EMOJI_PICKER_SIZE: { + WIDTH: '100%', + }, NON_NATIVE_EMOJI_PICKER_LIST_HEIGHT: 256, EMOJI_PICKER_ITEM_HEIGHT: 32, EMOJI_PICKER_HEADER_HEIGHT: 32, diff --git a/src/components/AttachmentCarousel/index.js b/src/components/AttachmentCarousel/index.js index 144d0aaa0874..bd232526bd9b 100644 --- a/src/components/AttachmentCarousel/index.js +++ b/src/components/AttachmentCarousel/index.js @@ -196,6 +196,7 @@ class AttachmentCarousel extends React.Component { this.toggleArrowsVisibility(!this.state.shouldShowArrow)} source={authSource} + key={authSource} file={this.state.file} /> diff --git a/src/components/EmojiPicker/EmojiPicker.js b/src/components/EmojiPicker/EmojiPicker/index.js similarity index 90% rename from src/components/EmojiPicker/EmojiPicker.js rename to src/components/EmojiPicker/EmojiPicker/index.js index 369365a8db84..5238c2dbdb1b 100644 --- a/src/components/EmojiPicker/EmojiPicker.js +++ b/src/components/EmojiPicker/EmojiPicker/index.js @@ -1,9 +1,17 @@ import React from 'react'; import {Dimensions, Keyboard} from 'react-native'; import _ from 'underscore'; -import EmojiPickerMenu from './EmojiPickerMenu'; -import CONST from '../../CONST'; -import PopoverWithMeasuredContent from '../PopoverWithMeasuredContent'; +import EmojiPickerMenu from '../EmojiPickerMenu'; +import CONST from '../../../CONST'; +import PopoverWithMeasuredContent from '../../PopoverWithMeasuredContent'; +import compose from '../../../libs/compose'; +import withViewportOffsetTop, {viewportOffsetTopPropTypes} from '../../withViewportOffsetTop'; +import withWindowDimensions, {windowDimensionsPropTypes} from '../../withWindowDimensions'; + +const propTypes = { + ...viewportOffsetTopPropTypes, + ...windowDimensionsPropTypes, +}; const DEFAULT_ANCHOR_ORIGIN = { horizontal: CONST.MODAL.ANCHOR_ORIGIN_HORIZONTAL.RIGHT, @@ -176,6 +184,7 @@ class EmojiPicker extends React.Component { }} anchorOrigin={this.state.emojiPopoverAnchorOrigin} measureContent={this.measureContent} + outerStyle={{maxHeight: this.props.windowHeight, marginTop: this.props.viewportOffsetTop}} > - {!this.props.isSmallScreenWidth && ( - - this.searchInput = el} - autoFocus - selectTextOnFocus={this.state.selectTextOnFocus} - onSelectionChange={this.onSelectionChange} - onFocus={() => this.setState({isFocused: true, highlightedIndex: -1, isUsingKeyboardMovement: false})} - onBlur={() => this.setState({isFocused: false})} - /> - - )} + + this.searchInput = el} + autoFocus={!this.isMobileLandscape() || this.props.isSmallScreenWidth} + selectTextOnFocus={this.state.selectTextOnFocus} + onSelectionChange={this.onSelectionChange} + onFocus={() => this.setState({isFocused: true, highlightedIndex: -1, isUsingKeyboardMovement: false})} + onBlur={() => this.setState({isFocused: false})} + autoCorrect={false} + /> + {!isFiltered && ( this.emojiList = el} data={this.state.filteredEmojis} renderItem={this.renderItem} - keyExtractor={item => `emoji_picker_${item.code}`} + keyExtractor={this.keyExtractor} numColumns={CONST.EMOJI_NUM_PER_ROW} style={[ styles.emojiPickerList, + StyleUtils.getEmojiPickerListHeight(isFiltered), this.isMobileLandscape() && styles.emojiPickerListLandscape, ]} extraData={ diff --git a/src/components/EmojiPicker/EmojiPickerMenu/index.native.js b/src/components/EmojiPicker/EmojiPickerMenu/index.native.js index 702be109e5e1..149e180c7cd7 100644 --- a/src/components/EmojiPicker/EmojiPickerMenu/index.native.js +++ b/src/components/EmojiPicker/EmojiPickerMenu/index.native.js @@ -16,7 +16,9 @@ import withLocalize, {withLocalizePropTypes} from '../../withLocalize'; import EmojiSkinToneList from '../EmojiSkinToneList'; import * as EmojiUtils from '../../../libs/EmojiUtils'; import * as User from '../../../libs/actions/User'; +import TextInput from '../../TextInput'; import CategoryShortcutBar from '../CategoryShortcutBar'; +import * as StyleUtils from '../../../styles/StyleUtils'; const propTypes = { /** Function to add the selected emoji to the main compose text input */ @@ -64,14 +66,48 @@ class EmojiPickerMenu extends Component { this.renderItem = this.renderItem.bind(this); this.isMobileLandscape = this.isMobileLandscape.bind(this); this.updatePreferredSkinTone = this.updatePreferredSkinTone.bind(this); + this.filterEmojis = _.debounce(this.filterEmojis.bind(this), 300); this.scrollToHeader = this.scrollToHeader.bind(this); this.getItemLayout = this.getItemLayout.bind(this); + + this.state = { + filteredEmojis: this.emojis, + headerIndices: this.headerRowIndices, + }; } getItemLayout(data, index) { return {length: CONST.EMOJI_PICKER_ITEM_HEIGHT, offset: CONST.EMOJI_PICKER_ITEM_HEIGHT * index, index}; } + /** + * Filter the entire list of emojis to only emojis that have the search term in their keywords + * + * @param {String} searchTerm + */ + filterEmojis(searchTerm) { + const normalizedSearchTerm = searchTerm.toLowerCase().trim(); + + if (this.emojiList) { + this.emojiList.scrollToOffset({offset: 0, animated: false}); + } + + if (normalizedSearchTerm === '') { + this.setState({ + filteredEmojis: this.emojis, + headerIndices: this.headerRowIndices, + }); + + return; + } + const newFilteredEmojiList = EmojiUtils.suggestEmojis(`:${normalizedSearchTerm}`, this.emojis.length); + + this.setState({ + filteredEmojis: newFilteredEmojiList, + headerIndices: undefined, + }); + } + /** * @param {String} emoji * @param {Object} emojiObject @@ -112,6 +148,16 @@ class EmojiPickerMenu extends Component { })(); } + /** + * Return a unique key for each emoji item + * + * @param {Object} item + * @returns {String} + */ + keyExtractor(item) { + return (`emoji_picker_${item.code}`); + } + /** * Given an emoji item object, render a component based on its type. * Items with the code "SPACER" return nothing and are used to fill rows up to 8 @@ -149,28 +195,56 @@ class EmojiPickerMenu extends Component { } render() { + const isFiltered = this.emojis.length !== this.state.filteredEmojis.length; return ( - + + + + {!isFiltered && ( - - this.emojiList = el} - data={this.emojis} - renderItem={this.renderItem} - keyExtractor={item => (`emoji_picker_${item.code}`)} - numColumns={CONST.EMOJI_NUM_PER_ROW} - style={[ - styles.emojiPickerList, - this.isMobileLandscape() && styles.emojiPickerListLandscape, - ]} - stickyHeaderIndices={this.headerRowIndices} - getItemLayout={this.getItemLayout} - showsVerticalScrollIndicator - /> + )} + {this.state.filteredEmojis.length === 0 + ? ( + + + {this.props.translate('common.noResultsFound')} + + + ) + : ( + this.emojiList = el} + keyboardShouldPersistTaps="handled" + data={this.state.filteredEmojis} + renderItem={this.renderItem} + keyExtractor={this.keyExtractor} + numColumns={CONST.EMOJI_NUM_PER_ROW} + style={[ + styles.emojiPickerList, + StyleUtils.getEmojiPickerListHeight(isFiltered), + this.isMobileLandscape() && styles.emojiPickerListLandscape, + ]} + stickyHeaderIndices={this.state.headerIndices} + getItemLayout={this.getItemLayout} + showsVerticalScrollIndicator + + // used because of a bug in RN where stickyHeaderIndices can't be updated after the list is rendered https://github.com/facebook/react-native/issues/25157 + removeClippedSubviews={false} + /> + )} { this.inputRefs[inputID] = node; - // Call the original ref, if any const {ref} = child; if (_.isFunction(ref)) { ref(node); @@ -285,7 +284,7 @@ class Form extends React.Component { }, value: this.state.inputValues[inputID], errorText: this.state.errors[inputID] || fieldErrorMessage, - onBlur: () => { + onBlur: (event) => { // We delay the validation in order to prevent Checkbox loss of focus when // the user are focusing a TextInput and proceeds to toggle a CheckBox in // web and mobile web platforms. @@ -293,6 +292,10 @@ class Form extends React.Component { this.setTouchedInput(inputID); this.validate(this.state.inputValues); }, 200); + + if (_.isFunction(child.props.onBlur)) { + child.props.onBlur(event); + } }, onInputChange: (value, key) => { const inputKey = key || inputID; diff --git a/src/components/Hoverable/index.js b/src/components/Hoverable/index.js index 2f10cca0c1e3..12145b93b260 100644 --- a/src/components/Hoverable/index.js +++ b/src/components/Hoverable/index.js @@ -69,19 +69,15 @@ class Hoverable extends Component { onMouseEnter: (el) => { this.setIsHovered(true); - // Call the original onMouseEnter, if any - const {onMouseEnter} = this.props.children; - if (_.isFunction(onMouseEnter)) { - onMouseEnter(el); + if (_.isFunction(this.props.children.props.onMouseEnter)) { + this.props.children.props.onMouseEnter(el); } }, onMouseLeave: (el) => { this.setIsHovered(false); - // Call the original onMouseLeave, if any - const {onMouseLeave} = this.props.children; - if (_.isFunction(onMouseLeave)) { - onMouseLeave(el); + if (_.isFunction(this.props.children.props.onMouseLeave)) { + this.props.children.props.onMouseLeave(el); } }, onBlur: (el) => { @@ -89,10 +85,8 @@ class Hoverable extends Component { this.setIsHovered(false); } - // Call the original onBlur, if any - const {onBlur} = this.props.children; - if (_.isFunction(onBlur)) { - onBlur(el); + if (_.isFunction(this.props.children.props.onBlur)) { + this.props.children.props.onBlur(el); } }, }); diff --git a/src/components/KeyboardAvoidingView/index.android.js b/src/components/KeyboardAvoidingView/index.android.js new file mode 100644 index 000000000000..6f3adcc95b1f --- /dev/null +++ b/src/components/KeyboardAvoidingView/index.android.js @@ -0,0 +1,32 @@ +import React from 'react'; +import {View, KeyboardAvoidingView as KeyboardAvoidingViewComponent} from 'react-native'; +import _ from 'underscore'; +import PropTypes from 'prop-types'; + +const propTypes = { + /** In most cases, we do not need to use KeyboardAvoidingView on Android, so it is set to false by default. */ + shouldApplyToAndroid: PropTypes.bool, +}; +const defaultProps = { + shouldApplyToAndroid: false, +}; + +const KeyboardAvoidingView = (props) => { + if (props.shouldApplyToAndroid) { + return ( + // eslint-disable-next-line react/jsx-props-no-spreading + + ); + } + const viewProps = _.omit(props, ['behavior', 'contentContainerStyle', 'enabled', 'keyboardVerticalOffset']); + return ( + // eslint-disable-next-line react/jsx-props-no-spreading + + ); +}; + +KeyboardAvoidingView.displayName = 'KeyboardAvoidingView'; +KeyboardAvoidingView.propTypes = propTypes; +KeyboardAvoidingView.defaultProps = defaultProps; + +export default KeyboardAvoidingView; diff --git a/src/components/KeyboardAvoidingView/index.ios.js b/src/components/KeyboardAvoidingView/index.ios.js index aeeb32e417bc..58f40b3276a3 100644 --- a/src/components/KeyboardAvoidingView/index.ios.js +++ b/src/components/KeyboardAvoidingView/index.ios.js @@ -1,6 +1,3 @@ -/* - * The KeyboardAvoidingView is only used on ios - */ import React from 'react'; import {KeyboardAvoidingView as KeyboardAvoidingViewComponent} from 'react-native'; diff --git a/src/components/Modal/BaseModal.js b/src/components/Modal/BaseModal.js index 1e314654bba3..26018641d6c1 100644 --- a/src/components/Modal/BaseModal.js +++ b/src/components/Modal/BaseModal.js @@ -10,6 +10,7 @@ import {propTypes as modalPropTypes, defaultProps as modalDefaultProps} from './ import * as Modal from '../../libs/actions/Modal'; import getModalStyles from '../../styles/getModalStyles'; import variables from '../../styles/variables'; +import KeyboardAvoidingView from '../KeyboardAvoidingView'; const propTypes = { ...modalPropTypes, @@ -132,6 +133,7 @@ class BaseModal extends PureComponent { animationInTiming={this.props.animationInTiming} animationOutTiming={this.props.animationOutTiming} statusBarTranslucent={this.props.statusBarTranslucent} + avoidKeyboard={this.props.avoidKeyboard} > {(insets) => { @@ -156,8 +158,7 @@ class BaseModal extends PureComponent { modalContainerStylePaddingTop: modalContainerStyle.paddingTop, modalContainerStylePaddingBottom: modalContainerStyle.paddingBottom, }); - - return ( + const content = ( ); + + return ( + + {content} + + ); }} diff --git a/src/components/Reactions/AddReactionBubble.js b/src/components/Reactions/AddReactionBubble.js index 3b57771311ab..d91af3c0ff14 100644 --- a/src/components/Reactions/AddReactionBubble.js +++ b/src/components/Reactions/AddReactionBubble.js @@ -79,7 +79,7 @@ const AddReactionBubble = (props) => { }; return ( - + { ref={ref} onPress={openEmojiPicker} isDelayButtonStateComplete={false} - tooltipText={props.translate('reportActionContextMenu.addReactionTooltip')} + tooltipText={props.translate('emojiReactions.addReactionTooltip')} > {({hovered, pressed}) => ( { styles.fontColorReactionLabel, ]} > - {`reacted with :${props.emojiName}:`} + {`${props.translate('emojiReactions.reactedWith')} :${props.emojiName}:`} ); diff --git a/src/components/Tooltip/index.js b/src/components/Tooltip/index.js index 104098b375f0..b84fbd6cb0ed 100644 --- a/src/components/Tooltip/index.js +++ b/src/components/Tooltip/index.js @@ -172,10 +172,8 @@ class Tooltip extends PureComponent { onBlur: (el) => { this.hideTooltip(); - // Call the original onBlur, if any - const {onBlur} = this.props.children; - if (_.isFunction(onBlur)) { - onBlur(el); + if (_.isFunction(this.props.children.props.onBlur)) { + this.props.children.props.onBlur(el); } }, focusable: true, diff --git a/src/languages/en.js b/src/languages/en.js index 765be98ed4dc..de94fc63e838 100755 --- a/src/languages/en.js +++ b/src/languages/en.js @@ -231,7 +231,10 @@ export default { editComment: 'Edit comment', deleteComment: 'Delete comment', deleteConfirmation: 'Are you sure you want to delete this comment?', + }, + emojiReactions: { addReactionTooltip: 'Add reaction', + reactedWith: 'reacted with', }, reportActionsView: { beginningOfArchivedRoomPartOne: 'You missed the party in ', diff --git a/src/languages/es.js b/src/languages/es.js index f7012c3d6654..be0090cbcf79 100644 --- a/src/languages/es.js +++ b/src/languages/es.js @@ -230,7 +230,10 @@ export default { editComment: 'Editar comentario', deleteComment: 'Eliminar comentario', deleteConfirmation: '¿Estás seguro de que quieres eliminar este comentario?', + }, + emojiReactions: { addReactionTooltip: 'Añadir una reacción', + reactedWith: 'reaccionó con', }, reportActionsView: { beginningOfArchivedRoomPartOne: 'Te perdiste la fiesta en ', diff --git a/src/libs/ReportActionsUtils.js b/src/libs/ReportActionsUtils.js index 7c60e65eda3c..df55ae49faee 100644 --- a/src/libs/ReportActionsUtils.js +++ b/src/libs/ReportActionsUtils.js @@ -41,19 +41,8 @@ function isDeletedAction(reportAction) { } /** - * @param {Object} reportAction - * @returns {Boolean} - */ -function isOptimisticAction(reportAction) { - return lodashGet(reportAction, 'pendingAction') === CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD; -} - -/** - * Sort an array of reportActions by: - * - * - Finalized actions always are "later" than optimistic actions - * - then sort by created timestamp - * - then sort by reportActionID. This gives us a stable order even in the case of multiple reportActions created on the same millisecond + * Sort an array of reportActions by their created timestamp first, and reportActionID second + * This gives us a stable order even in the case of multiple reportActions created on the same millisecond * * @param {Array} reportActions * @param {Boolean} shouldSortInDescendingOrder @@ -68,11 +57,6 @@ function getSortedReportActions(reportActions, shouldSortInDescendingOrder = fal return _.chain(reportActions) .compact() .sort((first, second) => { - // First, make sure that optimistic reportActions appear at the end - if (isOptimisticAction(second) && !isOptimisticAction(first)) { - return -1 * invertedMultiplier; - } - // First sort by timestamp if (first.created !== second.created) { return (first.created < second.created ? -1 : 1) * invertedMultiplier; diff --git a/src/libs/ReportUtils.js b/src/libs/ReportUtils.js index d57a559c3396..df0a3209e5a0 100644 --- a/src/libs/ReportUtils.js +++ b/src/libs/ReportUtils.js @@ -21,7 +21,6 @@ import linkingConfig from './Navigation/linkingConfig'; import * as defaultAvatars from '../components/Icon/DefaultAvatars'; import isReportMessageAttachment from './isReportMessageAttachment'; import * as defaultWorkspaceAvatars from '../components/Icon/WorkspaceDefaultAvatars'; -import * as CollectionUtils from './CollectionUtils'; let sessionEmail; Onyx.connect({ @@ -72,21 +71,6 @@ Onyx.connect({ callback: val => allReports = val, }); -const lastReportActions = {}; -Onyx.connect({ - key: ONYXKEYS.COLLECTION.REPORT_ACTIONS, - callback: (actions, key) => { - if (!key || !actions) { - return; - } - const reportID = CollectionUtils.extractCollectionItemID(key); - lastReportActions[reportID] = _.find( - ReportActionsUtils.getSortedReportActionsForDisplay(_.toArray(actions)), - reportAction => reportAction.pendingAction !== CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE, - ); - }, -}); - let doesDomainHaveApprovedAccountant; Onyx.connect({ key: ONYXKEYS.ACCOUNT, @@ -450,31 +434,6 @@ function canShowReportRecipientLocalTime(personalDetails, report) { && isReportParticipantValidated); } -/** - * Gets the last message text from the report. - * Looks at reportActions data as the "best source" for information, because the front-end may have optimistic reportActions that the server is not yet aware of. - * If reportActions are not loaded for the report, then there can't be any optimistic reportActions, and the lastMessageText rNVP will be accurate as a fallback. - * - * @param {Object} report - * @returns {String} - */ -function getLastMessageText(report) { - if (!report) { - return ''; - } - - const lastReportAction = lastReportActions[report.reportID]; - let lastReportActionText = report.lastMessageText; - let lastReportActionHtml = report.lastMessageHtml; - if (lastReportAction && lastReportAction.actionName === CONST.REPORT.ACTIONS.TYPE.ADDCOMMENT) { - lastReportActionText = lodashGet(lastReportAction, 'message[0].text', report.lastMessageText); - lastReportActionHtml = lodashGet(lastReportAction, 'message[0].html', report.lastMessageHtml); - } - return isReportMessageAttachment({text: lastReportActionText, html: lastReportActionHtml}) - ? `[${Localize.translateLocal('common.attachment')}]` - : lastReportActionText; -} - /** * Trim the last message text to a fixed limit. * @param {String} lastMessageText @@ -1714,7 +1673,6 @@ export { isIOUOwnedByCurrentUser, getIOUTotal, canShowReportRecipientLocalTime, - getLastMessageText, formatReportLastMessageText, chatIncludesConcierge, isPolicyExpenseChat, diff --git a/src/libs/SidebarUtils.js b/src/libs/SidebarUtils.js index eaa6d3929c5d..3e6fd500c98e 100644 --- a/src/libs/SidebarUtils.js +++ b/src/libs/SidebarUtils.js @@ -5,7 +5,6 @@ import lodashOrderBy from 'lodash/orderBy'; import Str from 'expensify-common/lib/str'; import ONYXKEYS from '../ONYXKEYS'; import * as ReportUtils from './ReportUtils'; -import * as ReportActionsUtils from './ReportActionsUtils'; import * as Localize from './Localize'; import CONST from '../CONST'; import * as OptionsListUtils from './OptionsListUtils'; @@ -62,7 +61,7 @@ Onyx.connect({ return; } const reportID = CollectionUtils.extractCollectionItemID(key); - lastReportActions[reportID] = _.first(ReportActionsUtils.getSortedReportActionsForDisplay(_.toArray(actions))); + lastReportActions[reportID] = _.last(_.toArray(actions)); reportActions[key] = actions; }, }); @@ -244,7 +243,12 @@ function getOptionData(reportID) { // We only create tooltips for the first 10 users or so since some reports have hundreds of users, causing performance to degrade. const displayNamesWithTooltips = ReportUtils.getDisplayNamesWithTooltips((participantPersonalDetailList || []).slice(0, 10), hasMultipleParticipants); - const lastMessageTextFromReport = ReportUtils.getLastMessageText(report); + let lastMessageTextFromReport = ''; + if (ReportUtils.isReportMessageAttachment({text: report.lastMessageText, html: report.lastMessageHtml})) { + lastMessageTextFromReport = `[${Localize.translateLocal('common.attachment')}]`; + } else { + lastMessageTextFromReport = Str.htmlDecode(report ? report.lastMessageText : ''); + } // If the last actor's details are not currently saved in Onyx Collection, // then try to get that from the last report action. @@ -259,7 +263,7 @@ function getOptionData(reportID) { let lastMessageText = hasMultipleParticipants && lastActorDetails && (lastActorDetails.login !== currentUserLogin.email) ? `${lastActorDetails.displayName}: ` : ''; - lastMessageText += lastMessageTextFromReport; + lastMessageText += report ? lastMessageTextFromReport : ''; if (result.isPolicyExpenseChat && result.isArchivedRoom) { const archiveReason = (lastReportActions[report.reportID] && lastReportActions[report.reportID].originalMessage && lastReportActions[report.reportID].originalMessage.reason) diff --git a/src/pages/home/report/ReportActionItem.js b/src/pages/home/report/ReportActionItem.js index d06e75de566f..7415a51aff48 100644 --- a/src/pages/home/report/ReportActionItem.js +++ b/src/pages/home/report/ReportActionItem.js @@ -126,7 +126,7 @@ class ReportActionItem extends Component { // Newline characters need to be removed here because getCurrentSelection() returns html mixed with newlines, and when //
tags are converted later to markdown, it creates duplicate newline characters. This means that when the content // is pasted, there are extra newlines in the content that we want to avoid. - const selection = SelectionScraper.getCurrentSelection().replace(/\n/g, ''); + const selection = SelectionScraper.getCurrentSelection().replace(/
\n/g, '
'); ReportActionContextMenu.showContextMenu( ContextMenuActions.CONTEXT_MENU_TYPES.REPORT_ACTION, event, diff --git a/src/styles/StyleUtils.js b/src/styles/StyleUtils.js index 17893cb94c85..2ffadfc7c26d 100644 --- a/src/styles/StyleUtils.js +++ b/src/styles/StyleUtils.js @@ -458,7 +458,7 @@ function getFontFamilyMonospace({fontStyle, fontWeight}) { function getEmojiPickerStyle(isSmallScreenWidth) { if (isSmallScreenWidth) { return { - width: '100%', + width: CONST.SMALL_EMOJI_PICKER_SIZE.WIDTH, }; } return { @@ -811,6 +811,18 @@ function getReportWelcomeContainerStyle(isSmallScreenWidth) { }; } +/** + * Gets the correct height for emoji picker list based on screen dimensions + * + * @param {Boolean} hasAdditionalSpace + * @returns {Object} + */ +function getEmojiPickerListHeight(hasAdditionalSpace) { + return { + height: hasAdditionalSpace ? CONST.NON_NATIVE_EMOJI_PICKER_LIST_HEIGHT + CONST.CATEGORY_SHORTCUT_BAR_HEIGHT : CONST.NON_NATIVE_EMOJI_PICKER_LIST_HEIGHT, + }; +} + /** * Gets styles for Emoji Suggestion row * @@ -967,6 +979,7 @@ export { getReportWelcomeBackgroundImageStyle, getReportWelcomeTopMarginStyle, getReportWelcomeContainerStyle, + getEmojiPickerListHeight, getEmojiSuggestionItemStyle, getEmojiSuggestionContainerStyle, getColoredBackgroundStyle, diff --git a/src/styles/styles.js b/src/styles/styles.js index d59ca9ae9a25..fafa5d1267d0 100644 --- a/src/styles/styles.js +++ b/src/styles/styles.js @@ -1523,12 +1523,25 @@ const styles = { emojiPickerContainer: { backgroundColor: themeColors.componentBG, }, - emojiPickerList: { - height: 288, + height: CONST.NON_NATIVE_EMOJI_PICKER_LIST_HEIGHT, + width: '100%', + ...spacing.ph4, + }, + emojiPickerListWithPadding: { + height: CONST.NON_NATIVE_EMOJI_PICKER_LIST_HEIGHT + CONST.CATEGORY_SHORTCUT_BAR_HEIGHT, width: '100%', ...spacing.ph4, }, + emojiPickerSearchListContainer: { + position: 'absolute', + top: 60, + right: 0, + bottom: 4, + left: 0, + backgroundColor: themeColors.appBG, + }, + emojiPickerListLandscape: { height: 240, }, diff --git a/tests/unit/ReportActionsUtilsTest.js b/tests/unit/ReportActionsUtilsTest.js index ad473647dc60..20bf389969b7 100644 --- a/tests/unit/ReportActionsUtilsTest.js +++ b/tests/unit/ReportActionsUtilsTest.js @@ -6,15 +6,7 @@ describe('ReportActionsUtils', () => { const cases = [ [ [ - // This is the lowest created timestamp, but because it's an optimistic action it should appear last - { - created: '2022-11-09 20:00:00.000', - reportActionID: '395268342', - actionName: CONST.REPORT.ACTIONS.TYPE.ADDCOMMENT, - pendingAction: CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD, - }, - - // This is the highest created timestamp, so should appear 2nd-to-last + // This is the highest created timestamp, so should appear last { created: '2022-11-09 22:27:01.825', reportActionID: '8401445780099176', @@ -69,12 +61,6 @@ describe('ReportActionsUtils', () => { reportActionID: '8401445780099176', actionName: CONST.REPORT.ACTIONS.TYPE.ADDCOMMENT, }, - { - created: '2022-11-09 20:00:00.000', - reportActionID: '395268342', - actionName: CONST.REPORT.ACTIONS.TYPE.ADDCOMMENT, - pendingAction: CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD, - }, ], ], [