diff --git a/.github/workflows/testBuild.yml b/.github/workflows/testBuild.yml index 98b75f7da3de..edadad1fdca4 100644 --- a/.github/workflows/testBuild.yml +++ b/.github/workflows/testBuild.yml @@ -267,6 +267,6 @@ jobs: | ![Android](https://api.qrserver.com/v1/create-qr-code/?size=200x200&data=${{fromJson(steps.set_var.outputs.android_paths).html_path}}) | ![iOS](https://api.qrserver.com/v1/create-qr-code/?size=200x200&data=${{fromJson(steps.set_var.outputs.ios_paths).html_path}}) | | desktop :computer: | web :spider_web: | | https://ad-hoc-expensify-cash.s3.amazonaws.com/desktop/$PULL_REQUEST_NUMBER/NewExpensify.dmg | https://$PULL_REQUEST_NUMBER.pr-testing.expensify.com | - | ![desktop](https://api.qrserver.com/v1/create-qr-code/?size=200x200&data=https://ad-hoc-expensify-cash.s3.amazonaws.com/desktop/$PULL_REQUEST_NUMBER/NewExpensify.dmg) | ![web](https://api.qrserver.com/v1/create-qr-code/?size=200x200&data=https://$(PULL_REQUEST_NUMBER).pr-testing.expensify.com) |" + | ![desktop](https://api.qrserver.com/v1/create-qr-code/?size=200x200&data=https://ad-hoc-expensify-cash.s3.amazonaws.com/desktop/$PULL_REQUEST_NUMBER/NewExpensify.dmg) | ![web](https://api.qrserver.com/v1/create-qr-code/?size=200x200&data=https://$PULL_REQUEST_NUMBER.pr-testing.expensify.com) |" env: GITHUB_TOKEN: ${{ secrets.OS_BOTIFY_TOKEN }} diff --git a/android/app/build.gradle b/android/app/build.gradle index fb08766d230f..36db063b9177 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 1001024103 - versionName "1.2.41-3" + versionCode 1001024201 + versionName "1.2.42-1" buildConfigField "boolean", "IS_NEW_ARCHITECTURE_ENABLED", isNewArchitectureEnabled().toString() if (isNewArchitectureEnabled()) { diff --git a/android/app/src/main/java/com/expensify/chat/customairshipextender/CustomNotificationProvider.java b/android/app/src/main/java/com/expensify/chat/customairshipextender/CustomNotificationProvider.java index deeff81bf76d..7c1f4a245cd2 100644 --- a/android/app/src/main/java/com/expensify/chat/customairshipextender/CustomNotificationProvider.java +++ b/android/app/src/main/java/com/expensify/chat/customairshipextender/CustomNotificationProvider.java @@ -1,5 +1,10 @@ package com.expensify.chat.customairshipextender; +import static androidx.core.app.NotificationCompat.PRIORITY_MAX; + +import android.app.NotificationChannel; +import android.app.NotificationChannelGroup; +import android.app.NotificationManager; import android.content.Context; import android.graphics.Bitmap; import android.graphics.Canvas; @@ -8,11 +13,13 @@ import android.graphics.Bitmap.Config; import android.graphics.PorterDuffXfermode; import android.graphics.PorterDuff.Mode; +import android.os.Build; import android.util.DisplayMetrics; import android.util.Log; import android.util.TypedValue; import android.view.WindowManager; import androidx.annotation.NonNull; +import androidx.annotation.RequiresApi; import androidx.core.app.NotificationCompat; import androidx.core.app.NotificationManagerCompat; import androidx.core.app.Person; @@ -48,6 +55,12 @@ public class CustomNotificationProvider extends ReactNotificationProvider { // Logging private static final String TAG = "NotificationProvider"; + // Define notification channel + public static final String CHANNEL_MESSAGES_ID = "CHANNEL_MESSAGES"; + public static final String CHANNEL_MESSAGES_NAME = "Message Notifications"; + public static final String CHANNEL_GROUP_ID = "CHANNEL_GROUP_CHATS"; + public static final String CHANNEL_GROUP_NAME = "Chats"; + // Conversation JSON keys private static final String PAYLOAD_KEY = "payload"; private static final String TYPE_KEY = "type"; @@ -58,6 +71,9 @@ public class CustomNotificationProvider extends ReactNotificationProvider { public CustomNotificationProvider(@NonNull Context context, @NonNull AirshipConfigOptions configOptions) { super(context, configOptions); + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + createAndRegisterNotificationChannel(context); + } } @NonNull @@ -66,6 +82,13 @@ protected NotificationCompat.Builder onExtendBuilder(@NonNull Context context, @ super.onExtendBuilder(context, builder, arguments); PushMessage message = arguments.getMessage(); + // Configure the notification channel or priority to ensure it shows in foreground + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + builder.setChannelId(CHANNEL_MESSAGES_ID); + } else { + builder.setPriority(PRIORITY_MAX); + } + if (message.containsKey(PAYLOAD_KEY)) { try { JsonMap payload = JsonValue.parseString(message.getExtra(PAYLOAD_KEY)).optMap(); @@ -82,6 +105,17 @@ protected NotificationCompat.Builder onExtendBuilder(@NonNull Context context, @ return builder; } + @RequiresApi(api = Build.VERSION_CODES.O) + private void createAndRegisterNotificationChannel(@NonNull Context context) { + NotificationChannelGroup channelGroup = new NotificationChannelGroup(CHANNEL_GROUP_ID, CHANNEL_GROUP_NAME); + NotificationChannel channel = new NotificationChannel(CHANNEL_MESSAGES_ID, CHANNEL_MESSAGES_NAME, NotificationManager.IMPORTANCE_HIGH); + channel.setGroup(CHANNEL_GROUP_ID); + + NotificationManager notificationManager = context.getSystemService(NotificationManager.class); + notificationManager.createNotificationChannelGroup(channelGroup); + notificationManager.createNotificationChannel(channel); + } + /** * Creates a canvas to draw a circle and then draws the bitmap avatar within that circle * to clip off the area of the bitmap outside the circular path and returns a circular diff --git a/docs/README.md b/docs/README.md index 0ff0d480ee41..9193439c4b59 100644 --- a/docs/README.md +++ b/docs/README.md @@ -41,6 +41,14 @@ ln -sf universal-darwin22 universal-darwin21 - _Note: If you see an error like `Unable to load the EventMachine C Extension...`, try running `gem uninstall eventmachine && bundle install`. If that doesn't work just removing the `--livereload` flag should work._ 1. Modify a `.html` or `.md` file and save your changes, and the browser should quickly auto-refresh. +## Troubleshooting + +### Android Chrome emulator +To visit the site on the Android emulator, go to `10.0.2.2:4000`. + +If you're getting an error page that says "Refused to connect", try running `adb reverse tcp:4000 tcp:4000` with your emulator open. + + # How the project is structured The [docs](https://github.com/Expensify/App/tree/main/docs) folder will contain the following main folders: diff --git a/ios/NewExpensify/Info.plist b/ios/NewExpensify/Info.plist index 5655a13a353c..2088673e7654 100644 --- a/ios/NewExpensify/Info.plist +++ b/ios/NewExpensify/Info.plist @@ -17,7 +17,7 @@ CFBundlePackageType APPL CFBundleShortVersionString - 1.2.41 + 1.2.42 CFBundleSignature ???? CFBundleURLTypes @@ -30,7 +30,7 @@ CFBundleVersion - 1.2.41.3 + 1.2.42.1 ITSAppUsesNonExemptEncryption LSApplicationQueriesSchemes @@ -65,8 +65,8 @@ NSCameraUsageDescription Your camera is used to create chat attachments, documents, and facial capture. - NSLocationWhenInUseUsageDescription - Your location is used to determine your default currency. + NSLocationAlwaysAndWhenInUseUsageDescription + Your location is used to determine your default currency and timezone. NSMicrophoneUsageDescription Required for video capture NSPhotoLibraryAddUsageDescription diff --git a/ios/NewExpensifyTests/Info.plist b/ios/NewExpensifyTests/Info.plist index e82772c4259b..0ab913d00326 100644 --- a/ios/NewExpensifyTests/Info.plist +++ b/ios/NewExpensifyTests/Info.plist @@ -15,10 +15,10 @@ CFBundlePackageType BNDL CFBundleShortVersionString - 1.2.41 + 1.2.42 CFBundleSignature ???? CFBundleVersion - 1.2.41.3 + 1.2.42.1 diff --git a/package-lock.json b/package-lock.json index ac7cc5424abd..8a64675399b1 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "new.expensify", - "version": "1.2.41-3", + "version": "1.2.42-1", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "new.expensify", - "version": "1.2.41-3", + "version": "1.2.42-1", "hasInstallScript": true, "license": "MIT", "dependencies": { diff --git a/package.json b/package.json index 918cf4ef3dc4..e40ecdc2286e 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "new.expensify", - "version": "1.2.41-3", + "version": "1.2.42-1", "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/CheckboxWithTooltip/CheckboxWithTooltipForMobileWebAndNative.js b/src/components/CheckboxWithTooltip/CheckboxWithTooltipForMobileWebAndNative.js index ac58d030e962..43d3c062608b 100644 --- a/src/components/CheckboxWithTooltip/CheckboxWithTooltipForMobileWebAndNative.js +++ b/src/components/CheckboxWithTooltip/CheckboxWithTooltipForMobileWebAndNative.js @@ -38,6 +38,7 @@ class CheckboxWithTooltipForMobileWebAndNative extends React.Component { ); diff --git a/src/components/CheckboxWithTooltip/checkboxWithTooltipPropTypes.js b/src/components/CheckboxWithTooltip/checkboxWithTooltipPropTypes.js index e0aa67e85b26..f764e216156b 100644 --- a/src/components/CheckboxWithTooltip/checkboxWithTooltipPropTypes.js +++ b/src/components/CheckboxWithTooltip/checkboxWithTooltipPropTypes.js @@ -22,6 +22,9 @@ const propTypes = { /** Container styles */ style: stylePropTypes, + /** Wheter the checkbox is disabled */ + disabled: PropTypes.bool, + /** Props inherited from withWindowDimensions */ ...windowDimensionsPropTypes, }; diff --git a/src/components/CheckboxWithTooltip/index.js b/src/components/CheckboxWithTooltip/index.js index 83756feb0bc3..ced15a589acb 100644 --- a/src/components/CheckboxWithTooltip/index.js +++ b/src/components/CheckboxWithTooltip/index.js @@ -15,6 +15,7 @@ const CheckboxWithTooltip = (props) => { onPress={props.onPress} text={props.text} toggleTooltip={props.toggleTooltip} + disabled={props.disabled} /> ); } @@ -22,7 +23,7 @@ const CheckboxWithTooltip = (props) => { ); return ( diff --git a/src/components/EmojiPicker/EmojiPickerMenu/index.js b/src/components/EmojiPicker/EmojiPickerMenu/index.js index 2d97a24439fa..ab68b51802c1 100755 --- a/src/components/EmojiPicker/EmojiPickerMenu/index.js +++ b/src/components/EmojiPicker/EmojiPickerMenu/index.js @@ -101,6 +101,7 @@ class EmojiPickerMenu extends Component { end: 0, }, isFocused: false, + isUsingKeyboardMovement: false, }; } @@ -245,7 +246,10 @@ class EmojiPickerMenu extends Component { ) { return; } + + // Blur the input and change the highlight type to keyboard this.searchInput.blur(); + this.setState({isUsingKeyboardMovement: true}); // We only want to hightlight the Emoji if none was highlighted already // If we already have a highlighted Emoji, lets just skip the first navigation @@ -313,9 +317,9 @@ class EmojiPickerMenu extends Component { break; } - // Actually highlight the new emoji and scroll to it if the index was changed + // Actually highlight the new emoji, apply keyboard movement styles, and scroll to it if the index was changed if (newIndex !== this.state.highlightedIndex) { - this.setState({highlightedIndex: newIndex}); + this.setState({highlightedIndex: newIndex, isUsingKeyboardMovement: true}); this.scrollToHighlightedIndex(); } } @@ -440,7 +444,7 @@ class EmojiPickerMenu extends Component { return ( this.addToFrequentAndSelectEmoji(emoji, item)} - onHoverIn={() => this.setState({highlightedIndex: index})} + onHoverIn={() => this.setState({highlightedIndex: index, isUsingKeyboardMovement: false})} onHoverOut={() => { if (this.state.arePointerEventsDisabled) { return; @@ -449,6 +453,7 @@ class EmojiPickerMenu extends Component { }} emoji={emojiCode} isHighlighted={index === this.state.highlightedIndex} + isUsingKeyboardMovement={this.state.isUsingKeyboardMovement} /> ); } @@ -472,7 +477,7 @@ class EmojiPickerMenu extends Component { autoFocus selectTextOnFocus={this.state.selectTextOnFocus} onSelectionChange={this.onSelectionChange} - onFocus={() => this.setState({isFocused: true})} + onFocus={() => this.setState({isFocused: true, highlightedIndex: -1, isUsingKeyboardMovement: false})} onBlur={() => this.setState({isFocused: false})} /> diff --git a/src/components/EmojiPicker/EmojiPickerMenuItem.js b/src/components/EmojiPicker/EmojiPickerMenuItem.js index a0c0ab2f71d6..60be79adb9d5 100644 --- a/src/components/EmojiPicker/EmojiPickerMenuItem.js +++ b/src/components/EmojiPicker/EmojiPickerMenuItem.js @@ -21,6 +21,9 @@ const propTypes = { /** Whether this menu item is currently highlighted or not */ isHighlighted: PropTypes.bool, + + /** Whether the emoji is highlighted by the keyboard/mouse */ + isUsingKeyboardMovement: PropTypes.bool, }; const EmojiPickerMenuItem = props => ( @@ -32,7 +35,8 @@ const EmojiPickerMenuItem = props => ( pressed, }) => ([ StyleUtils.getButtonBackgroundColorStyle(getButtonState(false, pressed)), - props.isHighlighted ? styles.emojiItemHighlighted : {}, + props.isHighlighted && props.isUsingKeyboardMovement ? styles.emojiItemKeyboardHighlighted : {}, + props.isHighlighted && !props.isUsingKeyboardMovement ? styles.emojiItemHighlighted : {}, styles.emojiItem, ])} > @@ -46,6 +50,7 @@ EmojiPickerMenuItem.propTypes = propTypes; EmojiPickerMenuItem.displayName = 'EmojiPickerMenuItem'; EmojiPickerMenuItem.defaultProps = { isHighlighted: false, + isUsingKeyboardMovement: false, onHoverIn: () => {}, onHoverOut: () => {}, }; @@ -55,5 +60,6 @@ EmojiPickerMenuItem.defaultProps = { export default React.memo( EmojiPickerMenuItem, (prevProps, nextProps) => prevProps.isHighlighted === nextProps.isHighlighted - && prevProps.emoji === nextProps.emoji, + && prevProps.emoji === nextProps.emoji + && prevProps.isUsingKeyboardMovement === nextProps.isUsingKeyboardMovement, ); diff --git a/src/components/ReportActionItem/IOUPreview.js b/src/components/ReportActionItem/IOUPreview.js index c8e40e636b58..ceec76c279c6 100644 --- a/src/components/ReportActionItem/IOUPreview.js +++ b/src/components/ReportActionItem/IOUPreview.js @@ -1,7 +1,6 @@ import React from 'react'; import { View, - ActivityIndicator, TouchableWithoutFeedback, } from 'react-native'; import PropTypes from 'prop-types'; @@ -34,6 +33,7 @@ const propTypes = { onPayButtonPressed: PropTypes.func, /** The active IOUReport, used for Onyx subscription */ + // eslint-disable-next-line react/no-unused-prop-types iouReportID: PropTypes.string.isRequired, /** The associated chatReport */ @@ -117,11 +117,6 @@ const IOUPreview = (props) => { // Pay button should only be visible to the manager of the report. const isCurrentUserManager = managerEmail === sessionEmail; - const reportIsLoading = _.isEmpty(props.iouReport); - - if (reportIsLoading) { - Report.fetchIOUReportByID(props.iouReportID, props.chatReportID); - } const managerName = lodashGet(props.personalDetails, [managerEmail, 'firstName'], '') || Str.removeSMSDomain(managerEmail); @@ -137,72 +132,68 @@ const IOUPreview = (props) => { return ( - {reportIsLoading - ? - : ( - { - PaymentMethods.clearWalletTermsError(); - Report.clearIOUError(props.chatReportID); - }} - errorRowStyles={[styles.mbn1]} - > - - - - - {cachedTotal} - - {!props.iouReport.hasOutstandingIOU && ( - - - - )} + { + PaymentMethods.clearWalletTermsError(); + Report.clearIOUError(props.chatReportID); + }} + errorRowStyles={[styles.mbn1]} + > + + + + + {cachedTotal} + + {!props.iouReport.hasOutstandingIOU && ( + + - - - - - {isCurrentUserManager - ? ( - - {props.iouReport.hasOutstandingIOU - ? props.translate('iou.youowe', {owner: ownerName}) - : props.translate('iou.youpaid', {owner: ownerName})} - - ) - : ( - - {props.iouReport.hasOutstandingIOU - ? props.translate('iou.owesyou', {manager: managerName}) - : props.translate('iou.paidyou', {manager: managerName})} - - )} - {(isCurrentUserManager - && !props.shouldHidePayButton - && props.iouReport.stateNum === CONST.REPORT.STATE_NUM.PROCESSING && ( -