From 4aeb255d178f310a5ef1a6694d6f9087bf95ba3e Mon Sep 17 00:00:00 2001 From: github-actions Date: Mon, 5 Aug 2024 10:02:22 +0000 Subject: [PATCH 01/16] Set version number to 1.67 --- gradle/app.versions.toml | 2 +- iosHyperskillApp/NotificationServiceExtension/Info.plist | 2 +- iosHyperskillApp/iosHyperskillApp/Info.plist | 2 +- iosHyperskillApp/iosHyperskillAppTests/Info.plist | 2 +- iosHyperskillApp/iosHyperskillAppUITests/Info.plist | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/gradle/app.versions.toml b/gradle/app.versions.toml index b6103a3de..b10a8a4a1 100644 --- a/gradle/app.versions.toml +++ b/gradle/app.versions.toml @@ -2,5 +2,5 @@ minSdk = '24' targetSdk = '34' compileSdk = '34' -versionName = '1.66' +versionName = '1.67' versionCode = '514' \ No newline at end of file diff --git a/iosHyperskillApp/NotificationServiceExtension/Info.plist b/iosHyperskillApp/NotificationServiceExtension/Info.plist index 50905073f..f2a92487d 100644 --- a/iosHyperskillApp/NotificationServiceExtension/Info.plist +++ b/iosHyperskillApp/NotificationServiceExtension/Info.plist @@ -11,7 +11,7 @@ CFBundleVersion 542 CFBundleShortVersionString - 1.66 + 1.67 CFBundlePackageType $(PRODUCT_BUNDLE_PACKAGE_TYPE) CFBundleExecutable diff --git a/iosHyperskillApp/iosHyperskillApp/Info.plist b/iosHyperskillApp/iosHyperskillApp/Info.plist index 801d903b7..a6c9313c8 100644 --- a/iosHyperskillApp/iosHyperskillApp/Info.plist +++ b/iosHyperskillApp/iosHyperskillApp/Info.plist @@ -21,7 +21,7 @@ CFBundlePackageType $(PRODUCT_BUNDLE_PACKAGE_TYPE) CFBundleShortVersionString - 1.66 + 1.67 CFBundleURLTypes diff --git a/iosHyperskillApp/iosHyperskillAppTests/Info.plist b/iosHyperskillApp/iosHyperskillAppTests/Info.plist index c97732e5a..0177c4371 100644 --- a/iosHyperskillApp/iosHyperskillAppTests/Info.plist +++ b/iosHyperskillApp/iosHyperskillAppTests/Info.plist @@ -13,7 +13,7 @@ CFBundlePackageType BNDL CFBundleShortVersionString - 1.66 + 1.67 CFBundleVersion 542 diff --git a/iosHyperskillApp/iosHyperskillAppUITests/Info.plist b/iosHyperskillApp/iosHyperskillAppUITests/Info.plist index 03d090c58..4ddaac622 100644 --- a/iosHyperskillApp/iosHyperskillAppUITests/Info.plist +++ b/iosHyperskillApp/iosHyperskillAppUITests/Info.plist @@ -13,7 +13,7 @@ CFBundlePackageType $(PRODUCT_BUNDLE_PACKAGE_TYPE) CFBundleShortVersionString - 1.66 + 1.67 CFBundleVersion 542 From 2df8273f4cffefb30cd6e7bad5ac50deab937457 Mon Sep 17 00:00:00 2001 From: github-actions Date: Mon, 5 Aug 2024 10:02:26 +0000 Subject: [PATCH 02/16] Bump build number --- gradle/app.versions.toml | 2 +- .../NotificationServiceExtension/Info.plist | 2 +- .../iosHyperskillApp.xcodeproj/project.pbxproj | 16 ++++++++-------- iosHyperskillApp/iosHyperskillApp/Info.plist | 2 +- .../iosHyperskillAppTests/Info.plist | 2 +- .../iosHyperskillAppUITests/Info.plist | 2 +- 6 files changed, 13 insertions(+), 13 deletions(-) diff --git a/gradle/app.versions.toml b/gradle/app.versions.toml index b10a8a4a1..e029095e6 100644 --- a/gradle/app.versions.toml +++ b/gradle/app.versions.toml @@ -3,4 +3,4 @@ minSdk = '24' targetSdk = '34' compileSdk = '34' versionName = '1.67' -versionCode = '514' \ No newline at end of file +versionCode = '515' \ No newline at end of file diff --git a/iosHyperskillApp/NotificationServiceExtension/Info.plist b/iosHyperskillApp/NotificationServiceExtension/Info.plist index f2a92487d..b6872a69c 100644 --- a/iosHyperskillApp/NotificationServiceExtension/Info.plist +++ b/iosHyperskillApp/NotificationServiceExtension/Info.plist @@ -9,7 +9,7 @@ CFBundleIdentifier $(PRODUCT_BUNDLE_IDENTIFIER) CFBundleVersion - 542 + 543 CFBundleShortVersionString 1.67 CFBundlePackageType diff --git a/iosHyperskillApp/iosHyperskillApp.xcodeproj/project.pbxproj b/iosHyperskillApp/iosHyperskillApp.xcodeproj/project.pbxproj index 23371aea5..6ac1bf134 100644 --- a/iosHyperskillApp/iosHyperskillApp.xcodeproj/project.pbxproj +++ b/iosHyperskillApp/iosHyperskillApp.xcodeproj/project.pbxproj @@ -5653,7 +5653,7 @@ buildSettings = { CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 542; + CURRENT_PROJECT_VERSION = 543; DEBUG_INFORMATION_FORMAT = dwarf; DEVELOPMENT_TEAM = UJ4KC2QN7B; GENERATE_INFOPLIST_FILE = NO; @@ -5674,7 +5674,7 @@ buildSettings = { CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 542; + CURRENT_PROJECT_VERSION = 543; DEVELOPMENT_TEAM = UJ4KC2QN7B; GENERATE_INFOPLIST_FILE = NO; INFOPLIST_FILE = iosHyperskillAppUITests/Info.plist; @@ -5695,7 +5695,7 @@ BUNDLE_LOADER = "$(TEST_HOST)"; CLANG_CXX_LANGUAGE_STANDARD = "gnu++17"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 542; + CURRENT_PROJECT_VERSION = 543; DEBUG_INFORMATION_FORMAT = dwarf; DEVELOPMENT_TEAM = UJ4KC2QN7B; INFOPLIST_FILE = iosHyperskillAppTests/Info.plist; @@ -5716,7 +5716,7 @@ BUNDLE_LOADER = "$(TEST_HOST)"; CLANG_CXX_LANGUAGE_STANDARD = "gnu++17"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 542; + CURRENT_PROJECT_VERSION = 543; DEVELOPMENT_TEAM = UJ4KC2QN7B; INFOPLIST_FILE = iosHyperskillAppTests/Info.plist; IPHONEOS_DEPLOYMENT_TARGET = 14.0; @@ -5737,7 +5737,7 @@ CODE_SIGN_ENTITLEMENTS = NotificationServiceExtension/NotificationServiceExtension.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 542; + CURRENT_PROJECT_VERSION = 543; DEBUG_INFORMATION_FORMAT = dwarf; DEVELOPMENT_TEAM = UJ4KC2QN7B; INFOPLIST_FILE = NotificationServiceExtension/Info.plist; @@ -5766,7 +5766,7 @@ CODE_SIGN_ENTITLEMENTS = NotificationServiceExtension/NotificationServiceExtension.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 542; + CURRENT_PROJECT_VERSION = 543; DEVELOPMENT_TEAM = UJ4KC2QN7B; INFOPLIST_FILE = NotificationServiceExtension/Info.plist; IPHONEOS_DEPLOYMENT_TARGET = 14.0; @@ -5912,7 +5912,7 @@ CODE_SIGN_ENTITLEMENTS = iosHyperskillApp/iosHyperskillApp.entitlements; CODE_SIGN_IDENTITY = "iPhone Developer"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 542; + CURRENT_PROJECT_VERSION = 543; DEVELOPMENT_ASSET_PATHS = "\"iosHyperskillApp/Preview Content\""; DEVELOPMENT_TEAM = UJ4KC2QN7B; ENABLE_PREVIEWS = YES; @@ -5948,7 +5948,7 @@ CODE_SIGN_ENTITLEMENTS = iosHyperskillApp/iosHyperskillApp.entitlements; CODE_SIGN_IDENTITY = "iPhone Developer"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 542; + CURRENT_PROJECT_VERSION = 543; DEVELOPMENT_ASSET_PATHS = "\"iosHyperskillApp/Preview Content\""; DEVELOPMENT_TEAM = UJ4KC2QN7B; ENABLE_PREVIEWS = YES; diff --git a/iosHyperskillApp/iosHyperskillApp/Info.plist b/iosHyperskillApp/iosHyperskillApp/Info.plist index a6c9313c8..ec7cab724 100644 --- a/iosHyperskillApp/iosHyperskillApp/Info.plist +++ b/iosHyperskillApp/iosHyperskillApp/Info.plist @@ -34,7 +34,7 @@ CFBundleVersion - 542 + 543 FirebaseAppDelegateProxyEnabled FirebaseMessagingAutoInitEnabled diff --git a/iosHyperskillApp/iosHyperskillAppTests/Info.plist b/iosHyperskillApp/iosHyperskillAppTests/Info.plist index 0177c4371..7739f4891 100644 --- a/iosHyperskillApp/iosHyperskillAppTests/Info.plist +++ b/iosHyperskillApp/iosHyperskillAppTests/Info.plist @@ -15,6 +15,6 @@ CFBundleShortVersionString 1.67 CFBundleVersion - 542 + 543 diff --git a/iosHyperskillApp/iosHyperskillAppUITests/Info.plist b/iosHyperskillApp/iosHyperskillAppUITests/Info.plist index 4ddaac622..a5e5851d0 100644 --- a/iosHyperskillApp/iosHyperskillAppUITests/Info.plist +++ b/iosHyperskillApp/iosHyperskillAppUITests/Info.plist @@ -15,6 +15,6 @@ CFBundleShortVersionString 1.67 CFBundleVersion - 542 + 543 From 0bfd55d754efb7e7fbe33ecb12f2cf3ca2c5a9c3 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 6 Aug 2024 15:34:46 +0900 Subject: [PATCH 03/16] GitHub Actions: Bump gradle/actions/wrapper-validation from 3 to 4 (#1147) Bumps [gradle/actions](https://github.com/gradle/actions) from 3 to 4. - [Release notes](https://github.com/gradle/actions/releases) - [Commits](https://github.com/gradle/actions/compare/v3...v4) --- updated-dependencies: - dependency-name: gradle/actions dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/android_beta_deployment.yml | 2 +- .github/workflows/android_deploy_to_firebase_manually.yml | 2 +- .github/workflows/android_release_deployment.yml | 2 +- .github/workflows/ci.yml | 2 +- .github/workflows/ios_beta_deployment.yml | 2 +- .github/workflows/ios_release_deployment.yml | 2 +- 6 files changed, 6 insertions(+), 6 deletions(-) diff --git a/.github/workflows/android_beta_deployment.yml b/.github/workflows/android_beta_deployment.yml index 7cc81a964..7343127dc 100644 --- a/.github/workflows/android_beta_deployment.yml +++ b/.github/workflows/android_beta_deployment.yml @@ -28,7 +28,7 @@ jobs: uses: actions/checkout@v4.1.4 - name: Gradle Wrapper Validation - uses: gradle/actions/wrapper-validation@v3 + uses: gradle/actions/wrapper-validation@v4 # Build and submit to the Firebase App Distribution firebase-deployment: diff --git a/.github/workflows/android_deploy_to_firebase_manually.yml b/.github/workflows/android_deploy_to_firebase_manually.yml index 50dcd1358..8c8f8ce33 100644 --- a/.github/workflows/android_deploy_to_firebase_manually.yml +++ b/.github/workflows/android_deploy_to_firebase_manually.yml @@ -17,7 +17,7 @@ jobs: uses: actions/checkout@v4.1.4 - name: Gradle Wrapper Validation - uses: gradle/actions/wrapper-validation@v3 + uses: gradle/actions/wrapper-validation@v4 # Build and submit to the Firebase App Distribution firebase-deployment: diff --git a/.github/workflows/android_release_deployment.yml b/.github/workflows/android_release_deployment.yml index da4fbb000..895b0f401 100644 --- a/.github/workflows/android_release_deployment.yml +++ b/.github/workflows/android_release_deployment.yml @@ -27,7 +27,7 @@ jobs: uses: actions/checkout@v4.1.4 - name: Gradle Wrapper Validation - uses: gradle/actions/wrapper-validation@v3 + uses: gradle/actions/wrapper-validation@v4 # Build and submit to the Google Play deployment: diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 2e728d084..ea1839218 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -44,7 +44,7 @@ jobs: uses: actions/checkout@v4.1.4 - name: Gradle Wrapper Validation - uses: gradle/actions/wrapper-validation@v3 + uses: gradle/actions/wrapper-validation@v4 files-changed: name: Detect changes diff --git a/.github/workflows/ios_beta_deployment.yml b/.github/workflows/ios_beta_deployment.yml index a104cd732..2ad3d26e0 100644 --- a/.github/workflows/ios_beta_deployment.yml +++ b/.github/workflows/ios_beta_deployment.yml @@ -28,7 +28,7 @@ jobs: uses: actions/checkout@v4.1.4 - name: Gradle Wrapper Validation - uses: gradle/actions/wrapper-validation@v3 + uses: gradle/actions/wrapper-validation@v4 # Build, archive for ad-hoc and submit to Firebase App Distribution deployment: diff --git a/.github/workflows/ios_release_deployment.yml b/.github/workflows/ios_release_deployment.yml index 4b27f086e..375f23de2 100644 --- a/.github/workflows/ios_release_deployment.yml +++ b/.github/workflows/ios_release_deployment.yml @@ -24,7 +24,7 @@ jobs: uses: actions/checkout@v4.1.4 - name: Gradle Wrapper Validation - uses: gradle/actions/wrapper-validation@v3 + uses: gradle/actions/wrapper-validation@v4 # Build, archive for app-store and submit to App Store Connect deployment: From d13d78aac802681aeb7b25fd1998c200ff19ff1b Mon Sep 17 00:00:00 2001 From: github-actions Date: Tue, 6 Aug 2024 06:35:27 +0000 Subject: [PATCH 04/16] Bump build number --- gradle/app.versions.toml | 2 +- .../NotificationServiceExtension/Info.plist | 2 +- .../iosHyperskillApp.xcodeproj/project.pbxproj | 16 ++++++++-------- iosHyperskillApp/iosHyperskillApp/Info.plist | 2 +- .../iosHyperskillAppTests/Info.plist | 2 +- .../iosHyperskillAppUITests/Info.plist | 2 +- 6 files changed, 13 insertions(+), 13 deletions(-) diff --git a/gradle/app.versions.toml b/gradle/app.versions.toml index e029095e6..68b5be9f1 100644 --- a/gradle/app.versions.toml +++ b/gradle/app.versions.toml @@ -3,4 +3,4 @@ minSdk = '24' targetSdk = '34' compileSdk = '34' versionName = '1.67' -versionCode = '515' \ No newline at end of file +versionCode = '516' \ No newline at end of file diff --git a/iosHyperskillApp/NotificationServiceExtension/Info.plist b/iosHyperskillApp/NotificationServiceExtension/Info.plist index b6872a69c..f256bb10f 100644 --- a/iosHyperskillApp/NotificationServiceExtension/Info.plist +++ b/iosHyperskillApp/NotificationServiceExtension/Info.plist @@ -9,7 +9,7 @@ CFBundleIdentifier $(PRODUCT_BUNDLE_IDENTIFIER) CFBundleVersion - 543 + 544 CFBundleShortVersionString 1.67 CFBundlePackageType diff --git a/iosHyperskillApp/iosHyperskillApp.xcodeproj/project.pbxproj b/iosHyperskillApp/iosHyperskillApp.xcodeproj/project.pbxproj index 6ac1bf134..4305f5833 100644 --- a/iosHyperskillApp/iosHyperskillApp.xcodeproj/project.pbxproj +++ b/iosHyperskillApp/iosHyperskillApp.xcodeproj/project.pbxproj @@ -5653,7 +5653,7 @@ buildSettings = { CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 543; + CURRENT_PROJECT_VERSION = 544; DEBUG_INFORMATION_FORMAT = dwarf; DEVELOPMENT_TEAM = UJ4KC2QN7B; GENERATE_INFOPLIST_FILE = NO; @@ -5674,7 +5674,7 @@ buildSettings = { CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 543; + CURRENT_PROJECT_VERSION = 544; DEVELOPMENT_TEAM = UJ4KC2QN7B; GENERATE_INFOPLIST_FILE = NO; INFOPLIST_FILE = iosHyperskillAppUITests/Info.plist; @@ -5695,7 +5695,7 @@ BUNDLE_LOADER = "$(TEST_HOST)"; CLANG_CXX_LANGUAGE_STANDARD = "gnu++17"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 543; + CURRENT_PROJECT_VERSION = 544; DEBUG_INFORMATION_FORMAT = dwarf; DEVELOPMENT_TEAM = UJ4KC2QN7B; INFOPLIST_FILE = iosHyperskillAppTests/Info.plist; @@ -5716,7 +5716,7 @@ BUNDLE_LOADER = "$(TEST_HOST)"; CLANG_CXX_LANGUAGE_STANDARD = "gnu++17"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 543; + CURRENT_PROJECT_VERSION = 544; DEVELOPMENT_TEAM = UJ4KC2QN7B; INFOPLIST_FILE = iosHyperskillAppTests/Info.plist; IPHONEOS_DEPLOYMENT_TARGET = 14.0; @@ -5737,7 +5737,7 @@ CODE_SIGN_ENTITLEMENTS = NotificationServiceExtension/NotificationServiceExtension.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 543; + CURRENT_PROJECT_VERSION = 544; DEBUG_INFORMATION_FORMAT = dwarf; DEVELOPMENT_TEAM = UJ4KC2QN7B; INFOPLIST_FILE = NotificationServiceExtension/Info.plist; @@ -5766,7 +5766,7 @@ CODE_SIGN_ENTITLEMENTS = NotificationServiceExtension/NotificationServiceExtension.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 543; + CURRENT_PROJECT_VERSION = 544; DEVELOPMENT_TEAM = UJ4KC2QN7B; INFOPLIST_FILE = NotificationServiceExtension/Info.plist; IPHONEOS_DEPLOYMENT_TARGET = 14.0; @@ -5912,7 +5912,7 @@ CODE_SIGN_ENTITLEMENTS = iosHyperskillApp/iosHyperskillApp.entitlements; CODE_SIGN_IDENTITY = "iPhone Developer"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 543; + CURRENT_PROJECT_VERSION = 544; DEVELOPMENT_ASSET_PATHS = "\"iosHyperskillApp/Preview Content\""; DEVELOPMENT_TEAM = UJ4KC2QN7B; ENABLE_PREVIEWS = YES; @@ -5948,7 +5948,7 @@ CODE_SIGN_ENTITLEMENTS = iosHyperskillApp/iosHyperskillApp.entitlements; CODE_SIGN_IDENTITY = "iPhone Developer"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 543; + CURRENT_PROJECT_VERSION = 544; DEVELOPMENT_ASSET_PATHS = "\"iosHyperskillApp/Preview Content\""; DEVELOPMENT_TEAM = UJ4KC2QN7B; ENABLE_PREVIEWS = YES; diff --git a/iosHyperskillApp/iosHyperskillApp/Info.plist b/iosHyperskillApp/iosHyperskillApp/Info.plist index ec7cab724..0cdc25d22 100644 --- a/iosHyperskillApp/iosHyperskillApp/Info.plist +++ b/iosHyperskillApp/iosHyperskillApp/Info.plist @@ -34,7 +34,7 @@ CFBundleVersion - 543 + 544 FirebaseAppDelegateProxyEnabled FirebaseMessagingAutoInitEnabled diff --git a/iosHyperskillApp/iosHyperskillAppTests/Info.plist b/iosHyperskillApp/iosHyperskillAppTests/Info.plist index 7739f4891..a4699dca9 100644 --- a/iosHyperskillApp/iosHyperskillAppTests/Info.plist +++ b/iosHyperskillApp/iosHyperskillAppTests/Info.plist @@ -15,6 +15,6 @@ CFBundleShortVersionString 1.67 CFBundleVersion - 543 + 544 diff --git a/iosHyperskillApp/iosHyperskillAppUITests/Info.plist b/iosHyperskillApp/iosHyperskillAppUITests/Info.plist index a5e5851d0..cc6a683b8 100644 --- a/iosHyperskillApp/iosHyperskillAppUITests/Info.plist +++ b/iosHyperskillApp/iosHyperskillAppUITests/Info.plist @@ -15,6 +15,6 @@ CFBundleShortVersionString 1.67 CFBundleVersion - 543 + 544 From 43e483a66d8a80508b9768a3eb106589e1506a42 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 6 Aug 2024 16:01:54 +0900 Subject: [PATCH 05/16] Bump rexml from 3.3.2 to 3.3.3 in /androidHyperskillApp (#1142) * Bump rexml from 3.3.2 to 3.3.3 in /androidHyperskillApp Bumps [rexml](https://github.com/ruby/rexml) from 3.3.2 to 3.3.3. - [Release notes](https://github.com/ruby/rexml/releases) - [Changelog](https://github.com/ruby/rexml/blob/master/NEWS.md) - [Commits](https://github.com/ruby/rexml/compare/v3.3.2...v3.3.3) --- updated-dependencies: - dependency-name: rexml dependency-type: direct:production ... Signed-off-by: dependabot[bot] * Run bundle update --------- Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Ivan Magda --- androidHyperskillApp/Gemfile | 2 +- androidHyperskillApp/Gemfile.lock | 10 +++++----- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/androidHyperskillApp/Gemfile b/androidHyperskillApp/Gemfile index d2054b245..a64e4672e 100644 --- a/androidHyperskillApp/Gemfile +++ b/androidHyperskillApp/Gemfile @@ -2,6 +2,6 @@ source "https://rubygems.org" ruby "3.3.0" gem "fastlane", "2.222.0" -gem "rexml", ">= 3.3.2" +gem "rexml", ">= 3.3.3" eval_gemfile("fastlane/Pluginfile") diff --git a/androidHyperskillApp/Gemfile.lock b/androidHyperskillApp/Gemfile.lock index c9e376f95..c8fbca5dd 100644 --- a/androidHyperskillApp/Gemfile.lock +++ b/androidHyperskillApp/Gemfile.lock @@ -10,7 +10,7 @@ GEM artifactory (3.0.17) atomos (0.1.3) aws-eventstream (1.3.0) - aws-partitions (1.960.0) + aws-partitions (1.962.0) aws-sdk-core (3.201.3) aws-eventstream (~> 1, >= 1.3.0) aws-partitions (~> 1, >= 1.651.0) @@ -19,7 +19,7 @@ GEM aws-sdk-kms (1.88.0) aws-sdk-core (~> 3, >= 3.201.0) aws-sigv4 (~> 1.5) - aws-sdk-s3 (1.156.0) + aws-sdk-s3 (1.157.0) aws-sdk-core (~> 3, >= 3.201.0) aws-sdk-kms (~> 1) aws-sigv4 (~> 1.5) @@ -133,7 +133,7 @@ GEM google-apis-core (>= 0.11.0, < 2.a) google-apis-storage_v1 (0.31.0) google-apis-core (>= 0.11.0, < 2.a) - google-cloud-core (1.7.0) + google-cloud-core (1.7.1) google-cloud-env (>= 1.0, < 3.a) google-cloud-errors (~> 1.0) google-cloud-env (1.6.0) @@ -178,7 +178,7 @@ GEM trailblazer-option (>= 0.1.1, < 0.2.0) uber (< 0.2.0) retriable (3.1.2) - rexml (3.3.2) + rexml (3.3.4) strscan rouge (2.0.7) ruby2_keywords (0.0.5) @@ -225,7 +225,7 @@ PLATFORMS DEPENDENCIES fastlane (= 2.222.0) fastlane-plugin-firebase_app_distribution - rexml (>= 3.3.2) + rexml (>= 3.3.3) RUBY VERSION ruby 3.3.0p0 From e9d390301599c3ce95b12fecf3d3ef46a789054f Mon Sep 17 00:00:00 2001 From: Ivan Magda Date: Tue, 6 Aug 2024 14:03:29 +0700 Subject: [PATCH 06/16] GitHub Actions: Bump actions (#1148) * Bump ruby/setup-ruby * Bump actions/setup-java * Bump actions/checkout --- .github/actions/setup-android/action.yml | 4 ++-- .github/actions/setup-ios/action.yml | 4 ++-- .github/workflows/android_beta_deployment.yml | 4 ++-- .github/workflows/android_deploy_to_firebase.yml | 2 +- .../android_deploy_to_firebase_manually.yml | 2 +- .github/workflows/android_release_deployment.yml | 4 ++-- .github/workflows/automerge_into_release.yml | 4 ++-- .github/workflows/build_caches.yml | 4 ++-- .github/workflows/ci.yml | 16 ++++++++-------- .github/workflows/cleanup_pr_caches.yml | 2 +- .github/workflows/codeql.yml | 2 +- .../detect_changed_files_reusable_workflow.yml | 2 +- .github/workflows/gh_pages_analytics.yml | 2 +- .github/workflows/ios_beta_deployment.yml | 4 ++-- .github/workflows/ios_release_deployment.yml | 4 ++-- .github/workflows/ios_unit_testing.yml | 2 +- .github/workflows/merge_main_into_develop.yml | 2 +- 17 files changed, 32 insertions(+), 32 deletions(-) diff --git a/.github/actions/setup-android/action.yml b/.github/actions/setup-android/action.yml index 62e8f8662..e28669465 100644 --- a/.github/actions/setup-android/action.yml +++ b/.github/actions/setup-android/action.yml @@ -62,14 +62,14 @@ runs: - name: Setup Ruby if: ${{ inputs.setup-ruby == 'true' }} - uses: ruby/setup-ruby@v1.175.1 + uses: ruby/setup-ruby@v1.190.0 with: ruby-version: '3.3.0' bundler-cache: true working-directory: './androidHyperskillApp' - name: Setup Java JDK - uses: actions/setup-java@v4.2.1 + uses: actions/setup-java@v4.2.2 with: java-version: '19' distribution: 'temurin' diff --git a/.github/actions/setup-ios/action.yml b/.github/actions/setup-ios/action.yml index ec6fdc1ad..231916192 100644 --- a/.github/actions/setup-ios/action.yml +++ b/.github/actions/setup-ios/action.yml @@ -49,14 +49,14 @@ runs: shell: bash - name: Setup Ruby - uses: ruby/setup-ruby@v1.175.1 + uses: ruby/setup-ruby@v1.190.0 with: ruby-version: '3.3.0' bundler-cache: true working-directory: './iosHyperskillApp' - name: Setup Java JDK - uses: actions/setup-java@v4.2.1 + uses: actions/setup-java@v4.2.2 with: java-version: '19' distribution: 'temurin' diff --git a/.github/workflows/android_beta_deployment.yml b/.github/workflows/android_beta_deployment.yml index 7343127dc..665a52e0b 100644 --- a/.github/workflows/android_beta_deployment.yml +++ b/.github/workflows/android_beta_deployment.yml @@ -25,7 +25,7 @@ jobs: runs-on: ubuntu-22.04 steps: - name: Checkout - uses: actions/checkout@v4.1.4 + uses: actions/checkout@v4.1.7 - name: Gradle Wrapper Validation uses: gradle/actions/wrapper-validation@v4 @@ -47,7 +47,7 @@ jobs: steps: - name: Checkout - uses: actions/checkout@v4.1.4 + uses: actions/checkout@v4.1.7 - name: Setup CI id: setup diff --git a/.github/workflows/android_deploy_to_firebase.yml b/.github/workflows/android_deploy_to_firebase.yml index 754047025..0238f4677 100644 --- a/.github/workflows/android_deploy_to_firebase.yml +++ b/.github/workflows/android_deploy_to_firebase.yml @@ -11,7 +11,7 @@ jobs: steps: - name: Checkout - uses: actions/checkout@v4.1.4 + uses: actions/checkout@v4.1.7 - name: Setup CI id: setup diff --git a/.github/workflows/android_deploy_to_firebase_manually.yml b/.github/workflows/android_deploy_to_firebase_manually.yml index 8c8f8ce33..25567ceef 100644 --- a/.github/workflows/android_deploy_to_firebase_manually.yml +++ b/.github/workflows/android_deploy_to_firebase_manually.yml @@ -14,7 +14,7 @@ jobs: runs-on: ubuntu-22.04 steps: - name: Checkout - uses: actions/checkout@v4.1.4 + uses: actions/checkout@v4.1.7 - name: Gradle Wrapper Validation uses: gradle/actions/wrapper-validation@v4 diff --git a/.github/workflows/android_release_deployment.yml b/.github/workflows/android_release_deployment.yml index 895b0f401..7afcc0109 100644 --- a/.github/workflows/android_release_deployment.yml +++ b/.github/workflows/android_release_deployment.yml @@ -24,7 +24,7 @@ jobs: runs-on: ubuntu-22.04 steps: - name: Checkout - uses: actions/checkout@v4.1.4 + uses: actions/checkout@v4.1.7 - name: Gradle Wrapper Validation uses: gradle/actions/wrapper-validation@v4 @@ -39,7 +39,7 @@ jobs: steps: - name: Checkout - uses: actions/checkout@v4.1.4 + uses: actions/checkout@v4.1.7 - name: Setup CI id: setup diff --git a/.github/workflows/automerge_into_release.yml b/.github/workflows/automerge_into_release.yml index d96857230..24331cd0a 100644 --- a/.github/workflows/automerge_into_release.yml +++ b/.github/workflows/automerge_into_release.yml @@ -31,13 +31,13 @@ jobs: steps: - name: Checkout - uses: actions/checkout@v4.1.4 + uses: actions/checkout@v4.1.7 with: fetch-depth: 0 token: ${{ secrets.GH_PAT }} - name: Setup Ruby - uses: ruby/setup-ruby@v1.175.1 + uses: ruby/setup-ruby@v1.190.0 with: ruby-version: "3.3.0" bundler-cache: true diff --git a/.github/workflows/build_caches.yml b/.github/workflows/build_caches.yml index 836d0d6ee..841bb9ced 100644 --- a/.github/workflows/build_caches.yml +++ b/.github/workflows/build_caches.yml @@ -33,7 +33,7 @@ jobs: steps: - name: Checkout - uses: actions/checkout@v4.1.4 + uses: actions/checkout@v4.1.7 - name: Setup CI uses: ./.github/actions/setup-android @@ -55,7 +55,7 @@ jobs: steps: - name: Checkout - uses: actions/checkout@v4.1.4 + uses: actions/checkout@v4.1.7 - name: Setup CI id: setup diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index ea1839218..d3d145571 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -41,7 +41,7 @@ jobs: timeout-minutes: 5 steps: - name: Checkout - uses: actions/checkout@v4.1.4 + uses: actions/checkout@v4.1.7 - name: Gradle Wrapper Validation uses: gradle/actions/wrapper-validation@v4 @@ -64,7 +64,7 @@ jobs: steps: - name: Checkout - uses: actions/checkout@v4.1.4 + uses: actions/checkout@v4.1.7 - name: Setup CI uses: ./.github/actions/setup-android @@ -91,7 +91,7 @@ jobs: steps: - name: Checkout - uses: actions/checkout@v4.1.4 + uses: actions/checkout@v4.1.7 - name: Setup CI uses: ./.github/actions/setup-android @@ -115,10 +115,10 @@ jobs: steps: - name: Checkout - uses: actions/checkout@v4.1.4 + uses: actions/checkout@v4.1.7 - name: Setup Ruby - uses: ruby/setup-ruby@v1.175.1 + uses: ruby/setup-ruby@v1.190.0 with: ruby-version: '3.3.0' bundler-cache: true @@ -151,7 +151,7 @@ jobs: steps: - name: Checkout - uses: actions/checkout@v4.1.4 + uses: actions/checkout@v4.1.7 - name: Setup CI id: setup @@ -175,7 +175,7 @@ jobs: steps: - name: Checkout - uses: actions/checkout@v4.1.4 + uses: actions/checkout@v4.1.7 - name: Setup CI id: setup @@ -198,7 +198,7 @@ jobs: steps: - name: Checkout - uses: actions/checkout@v4.1.4 + uses: actions/checkout@v4.1.7 - name: Setup CI id: setup diff --git a/.github/workflows/cleanup_pr_caches.yml b/.github/workflows/cleanup_pr_caches.yml index aa92d98c9..f17a0b5c6 100644 --- a/.github/workflows/cleanup_pr_caches.yml +++ b/.github/workflows/cleanup_pr_caches.yml @@ -12,7 +12,7 @@ jobs: steps: - name: Checkout - uses: actions/checkout@v4.1.4 + uses: actions/checkout@v4.1.7 - name: Cleanup run: | diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index f69ca48ed..785663f38 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -27,7 +27,7 @@ jobs: steps: - name: Checkout - uses: actions/checkout@v4.1.4 + uses: actions/checkout@v4.1.7 - name: Initialize CodeQL uses: github/codeql-action/init@v3 diff --git a/.github/workflows/detect_changed_files_reusable_workflow.yml b/.github/workflows/detect_changed_files_reusable_workflow.yml index 06bc1e89a..22bd79772 100644 --- a/.github/workflows/detect_changed_files_reusable_workflow.yml +++ b/.github/workflows/detect_changed_files_reusable_workflow.yml @@ -52,7 +52,7 @@ jobs: steps: - name: Checkout - uses: actions/checkout@v4.1.4 + uses: actions/checkout@v4.1.7 - name: Detect changes uses: dorny/paths-filter@v3.0.2 diff --git a/.github/workflows/gh_pages_analytics.yml b/.github/workflows/gh_pages_analytics.yml index fefd4f9b3..54148562a 100644 --- a/.github/workflows/gh_pages_analytics.yml +++ b/.github/workflows/gh_pages_analytics.yml @@ -37,7 +37,7 @@ jobs: steps: - name: Checkout - uses: actions/checkout@v4.1.4 + uses: actions/checkout@v4.1.7 - name: Setup CI uses: ./.github/actions/setup-android diff --git a/.github/workflows/ios_beta_deployment.yml b/.github/workflows/ios_beta_deployment.yml index 2ad3d26e0..05b8fa95b 100644 --- a/.github/workflows/ios_beta_deployment.yml +++ b/.github/workflows/ios_beta_deployment.yml @@ -25,7 +25,7 @@ jobs: runs-on: ubuntu-22.04 steps: - name: Checkout - uses: actions/checkout@v4.1.4 + uses: actions/checkout@v4.1.7 - name: Gradle Wrapper Validation uses: gradle/actions/wrapper-validation@v4 @@ -40,7 +40,7 @@ jobs: steps: - name: Checkout - uses: actions/checkout@v4.1.4 + uses: actions/checkout@v4.1.7 - name: Setup CI id: setup diff --git a/.github/workflows/ios_release_deployment.yml b/.github/workflows/ios_release_deployment.yml index 375f23de2..cd76cb926 100644 --- a/.github/workflows/ios_release_deployment.yml +++ b/.github/workflows/ios_release_deployment.yml @@ -21,7 +21,7 @@ jobs: runs-on: ubuntu-22.04 steps: - name: Checkout - uses: actions/checkout@v4.1.4 + uses: actions/checkout@v4.1.7 - name: Gradle Wrapper Validation uses: gradle/actions/wrapper-validation@v4 @@ -36,7 +36,7 @@ jobs: steps: - name: Checkout - uses: actions/checkout@v4.1.4 + uses: actions/checkout@v4.1.7 - name: Setup CI id: setup diff --git a/.github/workflows/ios_unit_testing.yml b/.github/workflows/ios_unit_testing.yml index 0f109a154..12b35a697 100644 --- a/.github/workflows/ios_unit_testing.yml +++ b/.github/workflows/ios_unit_testing.yml @@ -23,7 +23,7 @@ jobs: steps: - name: Checkout - uses: actions/checkout@v4.1.4 + uses: actions/checkout@v4.1.7 - name: Setup CI id: setup diff --git a/.github/workflows/merge_main_into_develop.yml b/.github/workflows/merge_main_into_develop.yml index f9b8bf3cb..e95bfb114 100644 --- a/.github/workflows/merge_main_into_develop.yml +++ b/.github/workflows/merge_main_into_develop.yml @@ -22,7 +22,7 @@ jobs: runs-on: ubuntu-22.04 steps: - name: Checkout - uses: actions/checkout@v4.1.4 + uses: actions/checkout@v4.1.7 with: fetch-depth: 0 token: ${{ secrets.GH_PAT }} From 622350c4b731de8a48edd782218b3d37880b4861 Mon Sep 17 00:00:00 2001 From: Aleksandr Zhukov Date: Tue, 6 Aug 2024 15:05:58 +0200 Subject: [PATCH 07/16] Android fix StageImplementationFragment does not implement StepNavigationContainer (#1149) --- .../fragment/StageImplementationFragment.kt | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/androidHyperskillApp/src/main/java/org/hyperskill/app/android/stage_implementation/fragment/StageImplementationFragment.kt b/androidHyperskillApp/src/main/java/org/hyperskill/app/android/stage_implementation/fragment/StageImplementationFragment.kt index c44702a05..37424c62f 100644 --- a/androidHyperskillApp/src/main/java/org/hyperskill/app/android/stage_implementation/fragment/StageImplementationFragment.kt +++ b/androidHyperskillApp/src/main/java/org/hyperskill/app/android/stage_implementation/fragment/StageImplementationFragment.kt @@ -4,6 +4,7 @@ import android.os.Bundle import android.view.View import androidx.fragment.app.Fragment import by.kirich1409.viewbindingdelegate.viewBinding +import com.github.terrakok.cicerone.Router import dev.chrisbanes.insetter.applyInsetter import org.hyperskill.app.android.HyperskillApp import org.hyperskill.app.android.R @@ -16,6 +17,7 @@ import org.hyperskill.app.android.main.view.ui.navigation.Tabs import org.hyperskill.app.android.main.view.ui.navigation.switch import org.hyperskill.app.android.stage_implementation.dialog.ProjectCompletedBottomSheet import org.hyperskill.app.android.stage_implementation.dialog.StageCompletedBottomSheet +import org.hyperskill.app.android.step.view.navigation.StepNavigationContainer import org.hyperskill.app.core.injection.ReduxViewModelFactory import org.hyperskill.app.stage_implement.presentation.StageImplementFeature import org.hyperskill.app.stage_implementation.presentation.StageImplementationViewModel @@ -30,7 +32,8 @@ import ru.nobird.app.presentation.redux.container.ReduxView */ class StageImplementationFragment : Fragment(R.layout.fragment_stage_implementation), - ReduxView { + ReduxView, + StepNavigationContainer { companion object { private const val STEP_TAG = "StageImplementationStepTag" @@ -62,6 +65,9 @@ class StageImplementationFragment : private val mainScreenRouter: MainScreenRouter = HyperskillApp.graph().navigationComponent.mainScreenCicerone.router + override val router: Router + get() = requireRouter() + override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) injectComponent() From a0bab317e0f4e2adc694e413cc1309055bcf449a Mon Sep 17 00:00:00 2001 From: github-actions Date: Tue, 6 Aug 2024 13:06:32 +0000 Subject: [PATCH 08/16] Android: Bump build number --- gradle/app.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/app.versions.toml b/gradle/app.versions.toml index 68b5be9f1..74414437e 100644 --- a/gradle/app.versions.toml +++ b/gradle/app.versions.toml @@ -3,4 +3,4 @@ minSdk = '24' targetSdk = '34' compileSdk = '34' versionName = '1.67' -versionCode = '516' \ No newline at end of file +versionCode = '517' \ No newline at end of file From c4be8529625c063c25eecd254538c4afeb5705ef Mon Sep 17 00:00:00 2001 From: Aleksandr Zhukov Date: Wed, 7 Aug 2024 04:22:31 +0200 Subject: [PATCH 09/16] Shared: Research analytic logging issue (#1145) ^ALTAPPS-1323 --- .../AnalyticHyperskillCacheDataSourceImpl.kt | 4 + .../AnalyticHyperskillRepositoryImpl.kt | 12 +- .../AnalyticHyperskillCacheDataSource.kt | 1 + .../HyperskillAnalyticEngineImpl.kt | 3 + .../HyperskillAnalyticEngineComponentImpl.kt | 2 - .../presentation/AnalyticActionDispatcher.kt | 71 -------- .../presentation/WrapWithAnalyticLogger.kt | 47 +++--- .../CompletableCoroutineActionDispatcher.kt | 64 ++++++++ ...letableCoroutineActionDispatcherConfig.kt} | 10 +- .../app/step/injection/StepFeatureBuilder.kt | 8 +- .../presentation/ViewStepActionDispatcher.kt | 34 +--- .../AnalyticHyperskillRepositoryTest.kt | 99 ++++++++++++ ...ompletableCoroutineActionDispatcherTest.kt | 151 ++++++++++++++++++ 13 files changed, 379 insertions(+), 127 deletions(-) delete mode 100644 shared/src/commonMain/kotlin/org/hyperskill/app/analytic/presentation/AnalyticActionDispatcher.kt create mode 100644 shared/src/commonMain/kotlin/org/hyperskill/app/core/presentation/CompletableCoroutineActionDispatcher.kt rename shared/src/commonMain/kotlin/org/hyperskill/app/{analytic/presentation/AnalyticActionDispatcherConfig.kt => core/presentation/CompletableCoroutineActionDispatcherConfig.kt} (57%) create mode 100644 shared/src/commonTest/kotlin/org/hyperskill/analytic/data/repository/AnalyticHyperskillRepositoryTest.kt create mode 100644 shared/src/commonTest/kotlin/org/hyperskill/core/presentation/CompletableCoroutineActionDispatcherTest.kt diff --git a/shared/src/commonMain/kotlin/org/hyperskill/app/analytic/cache/AnalyticHyperskillCacheDataSourceImpl.kt b/shared/src/commonMain/kotlin/org/hyperskill/app/analytic/cache/AnalyticHyperskillCacheDataSourceImpl.kt index 364d069ef..9383586c9 100644 --- a/shared/src/commonMain/kotlin/org/hyperskill/app/analytic/cache/AnalyticHyperskillCacheDataSourceImpl.kt +++ b/shared/src/commonMain/kotlin/org/hyperskill/app/analytic/cache/AnalyticHyperskillCacheDataSourceImpl.kt @@ -10,6 +10,10 @@ class AnalyticHyperskillCacheDataSourceImpl : AnalyticHyperskillCacheDataSource events.add(event) } + override suspend fun logEvents(events: List) { + this.events.addAll(events) + } + override suspend fun getEvents(): List = events.toList() diff --git a/shared/src/commonMain/kotlin/org/hyperskill/app/analytic/data/repository/AnalyticHyperskillRepositoryImpl.kt b/shared/src/commonMain/kotlin/org/hyperskill/app/analytic/data/repository/AnalyticHyperskillRepositoryImpl.kt index 55e649ef8..5d361c799 100644 --- a/shared/src/commonMain/kotlin/org/hyperskill/app/analytic/data/repository/AnalyticHyperskillRepositoryImpl.kt +++ b/shared/src/commonMain/kotlin/org/hyperskill/app/analytic/data/repository/AnalyticHyperskillRepositoryImpl.kt @@ -8,10 +8,12 @@ import org.hyperskill.app.analytic.domain.model.AnalyticEvent import org.hyperskill.app.analytic.domain.repository.AnalyticHyperskillRepository internal class AnalyticHyperskillRepositoryImpl( - private val mutex: Mutex, private val hyperskillRemoteDataSource: AnalyticHyperskillRemoteDataSource, private val hyperskillCacheDataSource: AnalyticHyperskillCacheDataSource ) : AnalyticHyperskillRepository { + + private val mutex = Mutex() + override suspend fun logEvent(event: AnalyticEvent) { mutex.withLock { hyperskillCacheDataSource.logEvent(event) @@ -24,6 +26,12 @@ internal class AnalyticHyperskillRepositoryImpl( eventsToFlush = hyperskillCacheDataSource.getEvents() hyperskillCacheDataSource.clearEvents() } - return hyperskillRemoteDataSource.flushEvents(eventsToFlush, isAuthorized) + return hyperskillRemoteDataSource + .flushEvents(eventsToFlush, isAuthorized) + .onFailure { + mutex.withLock { + hyperskillCacheDataSource.logEvents(eventsToFlush) + } + } } } \ No newline at end of file diff --git a/shared/src/commonMain/kotlin/org/hyperskill/app/analytic/data/source/AnalyticHyperskillCacheDataSource.kt b/shared/src/commonMain/kotlin/org/hyperskill/app/analytic/data/source/AnalyticHyperskillCacheDataSource.kt index 186a42564..c4a740bb1 100644 --- a/shared/src/commonMain/kotlin/org/hyperskill/app/analytic/data/source/AnalyticHyperskillCacheDataSource.kt +++ b/shared/src/commonMain/kotlin/org/hyperskill/app/analytic/data/source/AnalyticHyperskillCacheDataSource.kt @@ -4,6 +4,7 @@ import org.hyperskill.app.analytic.domain.model.AnalyticEvent interface AnalyticHyperskillCacheDataSource { suspend fun logEvent(event: AnalyticEvent) + suspend fun logEvents(events: List) suspend fun getEvents(): List suspend fun clearEvents() } \ No newline at end of file diff --git a/shared/src/commonMain/kotlin/org/hyperskill/app/analytic/domain/model/hyperskill/HyperskillAnalyticEngineImpl.kt b/shared/src/commonMain/kotlin/org/hyperskill/app/analytic/domain/model/hyperskill/HyperskillAnalyticEngineImpl.kt index 6af20398c..fa31fb94d 100644 --- a/shared/src/commonMain/kotlin/org/hyperskill/app/analytic/domain/model/hyperskill/HyperskillAnalyticEngineImpl.kt +++ b/shared/src/commonMain/kotlin/org/hyperskill/app/analytic/domain/model/hyperskill/HyperskillAnalyticEngineImpl.kt @@ -75,6 +75,9 @@ internal class HyperskillAnalyticEngineImpl( analyticHyperskillRepository .flushEvents(isAuthorized) + .onSuccess { + logger.d { "Successfully flush events" } + } .onFailure { logger.e(it) { "Failed to flush events" } } } } diff --git a/shared/src/commonMain/kotlin/org/hyperskill/app/analytic/injection/HyperskillAnalyticEngineComponentImpl.kt b/shared/src/commonMain/kotlin/org/hyperskill/app/analytic/injection/HyperskillAnalyticEngineComponentImpl.kt index 4aa16b0bb..1fd1d041f 100644 --- a/shared/src/commonMain/kotlin/org/hyperskill/app/analytic/injection/HyperskillAnalyticEngineComponentImpl.kt +++ b/shared/src/commonMain/kotlin/org/hyperskill/app/analytic/injection/HyperskillAnalyticEngineComponentImpl.kt @@ -1,6 +1,5 @@ package org.hyperskill.app.analytic.injection -import kotlinx.coroutines.sync.Mutex import org.hyperskill.app.analytic.cache.AnalyticHyperskillCacheDataSourceImpl import org.hyperskill.app.analytic.data.repository.AnalyticHyperskillRepositoryImpl import org.hyperskill.app.analytic.data.source.AnalyticHyperskillCacheDataSource @@ -25,7 +24,6 @@ internal class HyperskillAnalyticEngineComponentImpl(appGraph: AppGraph) : Hyper AnalyticHyperskillCacheDataSourceImpl() private val hyperskillRepository: AnalyticHyperskillRepository = AnalyticHyperskillRepositoryImpl( - Mutex(), hyperskillRemoteDataSource, hyperskillCacheDataSource ) diff --git a/shared/src/commonMain/kotlin/org/hyperskill/app/analytic/presentation/AnalyticActionDispatcher.kt b/shared/src/commonMain/kotlin/org/hyperskill/app/analytic/presentation/AnalyticActionDispatcher.kt deleted file mode 100644 index 1394e2055..000000000 --- a/shared/src/commonMain/kotlin/org/hyperskill/app/analytic/presentation/AnalyticActionDispatcher.kt +++ /dev/null @@ -1,71 +0,0 @@ -package org.hyperskill.app.analytic.presentation - -import kotlinx.coroutines.CompletableJob -import kotlinx.coroutines.CoroutineExceptionHandler -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.Job -import kotlinx.coroutines.SupervisorJob -import kotlinx.coroutines.launch -import kotlinx.coroutines.plus -import org.hyperskill.app.analytic.domain.interactor.AnalyticInteractor -import org.hyperskill.app.analytic.domain.model.AnalyticEvent -import ru.nobird.app.presentation.redux.dispatcher.ActionDispatcher - -/** - * Is responsible for dispatching analytic events. - * - * @property analyticInteractor the [AnalyticInteractor] used for logging analytic events - * @property logAnalyticScope the [CoroutineScope] used for logging analytic events - * @property getAnalyticEvent a function that takes an [Action] as input and returns a collection of [AnalyticEvent]. - * If the function returns null, no events will be logged. - */ -internal class AnalyticActionDispatcher( - private val analyticInteractor: AnalyticInteractor, - private val logAnalyticScope: CoroutineScope, - private val getAnalyticEvent: (Action) -> Collection? -) : ActionDispatcher { - - private var isCancelled: Boolean = false - - override fun handleAction(action: Action) { - if (isCancelled) return - - val analyticEvents = getAnalyticEvent(action) - if (!analyticEvents.isNullOrEmpty()) { - logAnalyticScope.launch { - analyticEvents.forEach { analyticEvent -> - analyticInteractor.logEvent(analyticEvent) - } - } - } - } - - override fun setListener(listener: (message: Message) -> Unit) { - // no op - } - - override fun cancel() { - isCancelled = true - (logAnalyticScope.coroutineContext[Job] as? CompletableJob)?.complete() - } - - /** - * Represents [CoroutineScope] config for logging [AnalyticEvent]. - * If [logAnalyticParentScope] is not presented uses a [Dispatchers.Main].immediate + [SupervisorJob]. - */ - interface ScopeConfigOptions { - val logAnalyticParentScope: CoroutineScope? - - val logAnalyticCoroutineExceptionHandler: CoroutineExceptionHandler - - fun createLogAnalyticScope(): CoroutineScope { - val parentScope = logAnalyticParentScope - return if (parentScope != null) { - parentScope + SupervisorJob(parentScope.coroutineContext[Job]) + logAnalyticCoroutineExceptionHandler - } else { - CoroutineScope(SupervisorJob() + Dispatchers.Main.immediate + logAnalyticCoroutineExceptionHandler) - } - } - } -} \ No newline at end of file diff --git a/shared/src/commonMain/kotlin/org/hyperskill/app/analytic/presentation/WrapWithAnalyticLogger.kt b/shared/src/commonMain/kotlin/org/hyperskill/app/analytic/presentation/WrapWithAnalyticLogger.kt index 90f576220..3a073c3c9 100644 --- a/shared/src/commonMain/kotlin/org/hyperskill/app/analytic/presentation/WrapWithAnalyticLogger.kt +++ b/shared/src/commonMain/kotlin/org/hyperskill/app/analytic/presentation/WrapWithAnalyticLogger.kt @@ -3,21 +3,23 @@ package org.hyperskill.app.analytic.presentation import kotlinx.coroutines.CoroutineScope import org.hyperskill.app.analytic.domain.interactor.AnalyticInteractor import org.hyperskill.app.analytic.domain.model.AnalyticEvent +import org.hyperskill.app.core.presentation.CompletableCoroutineActionDispatcher +import org.hyperskill.app.core.presentation.CompletableCoroutineActionDispatcherConfig import ru.nobird.app.presentation.redux.dispatcher.wrapWithActionDispatcher import ru.nobird.app.presentation.redux.feature.Feature /** - * Wraps the given [Feature] with an [AnalyticActionDispatcher]. + * Wraps the given [Feature] with an [CompletableCoroutineActionDispatcher]. * * @param analyticInteractor The [AnalyticInteractor] used for logging analytic events. * * @param parentScope The parent [CoroutineScope] to use for creating the logAnalyticScope. The Default value is null. - * @see [AnalyticActionDispatcher.ScopeConfigOptions] for more details. + * @see [CompletableCoroutineActionDispatcher.ScopeConfigOptions] for more details. * * @param getAnalyticEvent A function that takes an [Action] as input and returns an [AnalyticEvent]. * If the function returns null, no events will be logged. * - * @see [AnalyticActionDispatcher], [AnalyticActionDispatcherConfig], [AnalyticActionDispatcher.ScopeConfigOptions] + * @see [CompletableCoroutineActionDispatcher], [CompletableCoroutineActionDispatcherConfig], [CompletableCoroutineActionDispatcher.ScopeConfigOptions] */ internal inline fun Feature.wrapWithAnalyticLogger( analyticInteractor: AnalyticInteractor, @@ -33,17 +35,17 @@ internal inline fun Feature.wra ) /** - * Wraps the given [Feature] with an [AnalyticActionDispatcher]. + * Wraps the given [Feature] with an [CompletableCoroutineActionDispatcher]. * * @param analyticInteractor The [AnalyticInteractor] used for logging analytic events. * * @param parentScope The parent CoroutineScope to use for creating the logAnalyticScope. The Default value is null. - * @see [AnalyticActionDispatcher.ScopeConfigOptions] for more details. + * @see [CompletableCoroutineActionDispatcher.ScopeConfigOptions] for more details. * * @param getAnalyticEvent A function that takes an [Action] as input and returns a collection of [AnalyticEvent]. * If the function returns null, no events will be logged. * - * @see [AnalyticActionDispatcher], [AnalyticActionDispatcherConfig], [AnalyticActionDispatcher.ScopeConfigOptions] + * @see [CompletableCoroutineActionDispatcher], [CompletableCoroutineActionDispatcherConfig], [CompletableCoroutineActionDispatcher.ScopeConfigOptions] */ internal fun Feature.wrapWithBatchAnalyticLogger( analyticInteractor: AnalyticInteractor, @@ -63,13 +65,16 @@ internal inline fun SingleAnalyticEventActionDispatcher( analyticInteractor: AnalyticInteractor, parentScope: CoroutineScope? = null, crossinline getAnalyticEvent: (Action) -> AnalyticEvent? -): AnalyticActionDispatcher = - AnalyticActionDispatcher( - analyticInteractor = analyticInteractor, - logAnalyticScope = AnalyticActionDispatcherConfig(parentScope).createLogAnalyticScope() - ) { action -> - val event = getAnalyticEvent(action) - if (event != null) listOf(event) else null +): CompletableCoroutineActionDispatcher = + object : CompletableCoroutineActionDispatcher( + coroutineScope = CompletableCoroutineActionDispatcherConfig(parentScope).createScope() + ) { + override suspend fun handleNonCancellableAction(action: Action) { + val event = getAnalyticEvent(action) + if (event != null) { + analyticInteractor.logEvent(event) + } + } } @Suppress("FunctionName", "unused") @@ -77,9 +82,13 @@ internal inline fun BatchAnalyticEventActionDispatcher( analyticInteractor: AnalyticInteractor, parentScope: CoroutineScope? = null, noinline getAnalyticEvent: (Action) -> Collection? -): AnalyticActionDispatcher = - AnalyticActionDispatcher( - analyticInteractor = analyticInteractor, - logAnalyticScope = AnalyticActionDispatcherConfig(parentScope).createLogAnalyticScope(), - getAnalyticEvent = getAnalyticEvent - ) \ No newline at end of file +): CompletableCoroutineActionDispatcher = + object : CompletableCoroutineActionDispatcher( + coroutineScope = CompletableCoroutineActionDispatcherConfig(parentScope).createScope(), + ) { + override suspend fun handleNonCancellableAction(action: Action) { + getAnalyticEvent(action)?.forEach { analyticEvent -> + analyticInteractor.logEvent(analyticEvent) + } + } + } \ No newline at end of file diff --git a/shared/src/commonMain/kotlin/org/hyperskill/app/core/presentation/CompletableCoroutineActionDispatcher.kt b/shared/src/commonMain/kotlin/org/hyperskill/app/core/presentation/CompletableCoroutineActionDispatcher.kt new file mode 100644 index 000000000..3f6672449 --- /dev/null +++ b/shared/src/commonMain/kotlin/org/hyperskill/app/core/presentation/CompletableCoroutineActionDispatcher.kt @@ -0,0 +1,64 @@ +package org.hyperskill.app.core.presentation + +import kotlinx.coroutines.CompletableJob +import kotlinx.coroutines.CoroutineExceptionHandler +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.Job +import kotlinx.coroutines.SupervisorJob +import kotlinx.coroutines.launch +import kotlinx.coroutines.plus +import org.hyperskill.app.analytic.domain.model.AnalyticEvent +import ru.nobird.app.presentation.redux.dispatcher.ActionDispatcher + +/** + * Base class for ActionDispatcher dispatching actions, that should not be cancelled with feature cancellation. + * E.g. analytic event logging. + * + * @param coroutineScope is not cancelled on [cancel]. + * Instead it is complete to wait for all launched coroutines to finish. + */ +internal abstract class CompletableCoroutineActionDispatcher( + private val coroutineScope: CoroutineScope +) : ActionDispatcher { + + private var isCancelled: Boolean = false + + abstract suspend fun handleNonCancellableAction(action: Action) + + override fun handleAction(action: Action) { + if (isCancelled) return + + coroutineScope.launch { + handleNonCancellableAction(action) + } + } + + override fun setListener(listener: (message: Message) -> Unit) { + // no op + } + + override fun cancel() { + isCancelled = true + (coroutineScope.coroutineContext[Job] as? CompletableJob)?.complete() + } + + /** + * Represents [CoroutineScope] config for logging [AnalyticEvent]. + * If [parentScope] is not presented uses a [Dispatchers.Main].immediate + [SupervisorJob]. + */ + interface ScopeConfigOptions { + val parentScope: CoroutineScope? + + val coroutineExceptionHandler: CoroutineExceptionHandler + + fun createScope(): CoroutineScope { + val parentScope = parentScope + return if (parentScope != null) { + parentScope + SupervisorJob(parentScope.coroutineContext[Job]) + coroutineExceptionHandler + } else { + CoroutineScope(SupervisorJob() + Dispatchers.Main.immediate + coroutineExceptionHandler) + } + } + } +} \ No newline at end of file diff --git a/shared/src/commonMain/kotlin/org/hyperskill/app/analytic/presentation/AnalyticActionDispatcherConfig.kt b/shared/src/commonMain/kotlin/org/hyperskill/app/core/presentation/CompletableCoroutineActionDispatcherConfig.kt similarity index 57% rename from shared/src/commonMain/kotlin/org/hyperskill/app/analytic/presentation/AnalyticActionDispatcherConfig.kt rename to shared/src/commonMain/kotlin/org/hyperskill/app/core/presentation/CompletableCoroutineActionDispatcherConfig.kt index 1c6c554f1..e2de0731f 100644 --- a/shared/src/commonMain/kotlin/org/hyperskill/app/analytic/presentation/AnalyticActionDispatcherConfig.kt +++ b/shared/src/commonMain/kotlin/org/hyperskill/app/core/presentation/CompletableCoroutineActionDispatcherConfig.kt @@ -1,14 +1,14 @@ -package org.hyperskill.app.analytic.presentation +package org.hyperskill.app.core.presentation import kotlinx.coroutines.CancellationException import kotlinx.coroutines.CoroutineExceptionHandler import kotlinx.coroutines.CoroutineScope import org.hyperskill.app.core.domain.throwError -internal class AnalyticActionDispatcherConfig( - override val logAnalyticParentScope: CoroutineScope? = null -) : AnalyticActionDispatcher.ScopeConfigOptions { - override val logAnalyticCoroutineExceptionHandler: CoroutineExceptionHandler = +internal class CompletableCoroutineActionDispatcherConfig( + override val parentScope: CoroutineScope? = null +) : CompletableCoroutineActionDispatcher.ScopeConfigOptions { + override val coroutineExceptionHandler: CoroutineExceptionHandler = CoroutineExceptionHandler { _, throwable -> if (throwable !is CancellationException) { throwError(throwable) // rethrow if not cancellation exception diff --git a/shared/src/commonMain/kotlin/org/hyperskill/app/step/injection/StepFeatureBuilder.kt b/shared/src/commonMain/kotlin/org/hyperskill/app/step/injection/StepFeatureBuilder.kt index 42fdefcd4..b550f4849 100644 --- a/shared/src/commonMain/kotlin/org/hyperskill/app/step/injection/StepFeatureBuilder.kt +++ b/shared/src/commonMain/kotlin/org/hyperskill/app/step/injection/StepFeatureBuilder.kt @@ -6,6 +6,7 @@ import org.hyperskill.app.analytic.presentation.wrapWithAnalyticLogger import org.hyperskill.app.core.domain.BuildVariant import org.hyperskill.app.core.domain.url.HyperskillUrlBuilder import org.hyperskill.app.core.presentation.ActionDispatcherOptions +import org.hyperskill.app.core.presentation.CompletableCoroutineActionDispatcherConfig import org.hyperskill.app.core.presentation.transformState import org.hyperskill.app.learning_activities.domain.repository.NextLearningActivityStateRepository import org.hyperskill.app.logging.presentation.wrapWithLogger @@ -69,6 +70,11 @@ internal object StepFeatureBuilder { logger = logger.withTag(LOG_TAG) ) + val viewStepActionDispatcher = ViewStepActionDispatcher( + config = CompletableCoroutineActionDispatcherConfig(), + stepInteractor = stepInteractor + ) + val stepViewStateMapper = StepViewStateMapper(stepRoute) return ReduxFeature(StepFeature.initialState(stepRoute), stepReducer) @@ -89,6 +95,6 @@ internal object StepFeatureBuilder { .wrapWithAnalyticLogger(analyticInteractor) { (it as? InternalAction.LogAnalyticEvent)?.analyticEvent } - .wrapWithActionDispatcher(ViewStepActionDispatcher(stepInteractor)) + .wrapWithActionDispatcher(viewStepActionDispatcher) } } \ No newline at end of file diff --git a/shared/src/commonMain/kotlin/org/hyperskill/app/step/presentation/ViewStepActionDispatcher.kt b/shared/src/commonMain/kotlin/org/hyperskill/app/step/presentation/ViewStepActionDispatcher.kt index 072a48ff2..6fc4a3e1b 100644 --- a/shared/src/commonMain/kotlin/org/hyperskill/app/step/presentation/ViewStepActionDispatcher.kt +++ b/shared/src/commonMain/kotlin/org/hyperskill/app/step/presentation/ViewStepActionDispatcher.kt @@ -1,37 +1,17 @@ package org.hyperskill.app.step.presentation -import kotlinx.coroutines.CompletableJob -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.Job -import kotlinx.coroutines.SupervisorJob -import kotlinx.coroutines.launch +import org.hyperskill.app.core.presentation.CompletableCoroutineActionDispatcher +import org.hyperskill.app.core.presentation.CompletableCoroutineActionDispatcherConfig import org.hyperskill.app.step.domain.interactor.StepInteractor -import ru.nobird.app.presentation.redux.dispatcher.ActionDispatcher internal class ViewStepActionDispatcher( + config: CompletableCoroutineActionDispatcherConfig, private val stepInteractor: StepInteractor -) : ActionDispatcher { +) : CompletableCoroutineActionDispatcher(config.createScope()) { - private val coroutineScope: CoroutineScope = - CoroutineScope(SupervisorJob() + Dispatchers.Main.immediate) - - private var isCancelled: Boolean = false - - override fun handleAction(action: StepFeature.Action) { - if (!isCancelled && action is StepFeature.InternalAction.ViewStep) { - coroutineScope.launch { - stepInteractor.viewStep(action.stepId, action.stepContext) - } + override suspend fun handleNonCancellableAction(action: StepFeature.Action) { + if (action is StepFeature.InternalAction.ViewStep) { + stepInteractor.viewStep(action.stepId, action.stepContext) } } - - override fun setListener(listener: (message: StepFeature.Message) -> Unit) { - // no op - } - - override fun cancel() { - isCancelled = true - (coroutineScope.coroutineContext[Job] as? CompletableJob)?.complete() - } } \ No newline at end of file diff --git a/shared/src/commonTest/kotlin/org/hyperskill/analytic/data/repository/AnalyticHyperskillRepositoryTest.kt b/shared/src/commonTest/kotlin/org/hyperskill/analytic/data/repository/AnalyticHyperskillRepositoryTest.kt new file mode 100644 index 000000000..74f4dbe39 --- /dev/null +++ b/shared/src/commonTest/kotlin/org/hyperskill/analytic/data/repository/AnalyticHyperskillRepositoryTest.kt @@ -0,0 +1,99 @@ +package org.hyperskill.analytic.data.repository + +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertTrue +import kotlin.time.Duration.Companion.seconds +import kotlinx.coroutines.delay +import kotlinx.coroutines.runBlocking +import org.hyperskill.app.analytic.cache.AnalyticHyperskillCacheDataSourceImpl +import org.hyperskill.app.analytic.data.repository.AnalyticHyperskillRepositoryImpl +import org.hyperskill.app.analytic.data.source.AnalyticHyperskillRemoteDataSource +import org.hyperskill.app.analytic.domain.model.AnalyticEvent +import org.hyperskill.app.analytic.domain.model.AnalyticSource + +class AnalyticHyperskillRepositoryTest { + + @Test + fun `Logged event should be written to the cache`() { + val testEvent = getAnalyticEventStub("Test event") + + val cacheDataSource = AnalyticHyperskillCacheDataSourceImpl() + + val remoteDataSource = object : AnalyticHyperskillRemoteDataSource { + override suspend fun flushEvents(events: List, isAuthorized: Boolean): Result { + TODO("Not yet implemented") + } + } + + val repository = AnalyticHyperskillRepositoryImpl(remoteDataSource, cacheDataSource) + + runBlocking { + repository.logEvent(testEvent) + val actualEvents = cacheDataSource.getEvents() + assertEquals(listOf(testEvent), actualEvents) + } + } + + @Test + fun `Cached events should be removed from the cache after flushing`() { + val initialEvent = getAnalyticEventStub("Initial") + + val cacheDataSource = AnalyticHyperskillCacheDataSourceImpl() + + val remoteDataSource = object : AnalyticHyperskillRemoteDataSource { + override suspend fun flushEvents(events: List, isAuthorized: Boolean): Result = + Result.success(Unit) + } + + val repository = AnalyticHyperskillRepositoryImpl(remoteDataSource, cacheDataSource) + + runBlocking { + repository.logEvent(initialEvent) + repository.flushEvents(isAuthorized = true) + assertTrue(cacheDataSource.getEvents().isEmpty()) + } + } + + @Test + fun `Cached events should be returned to cache after failed flushing`() { + val initialEvents = listOf( + getAnalyticEventStub("First"), + getAnalyticEventStub("Second"), + getAnalyticEventStub("Third") + ) + + val extraEvent = getAnalyticEventStub("Extra") + + val cacheDataSource = AnalyticHyperskillCacheDataSourceImpl() + val remoteDataSource = object : AnalyticHyperskillRemoteDataSource { + override suspend fun flushEvents(events: List, isAuthorized: Boolean): Result { + delay(1.seconds) + return Result.failure(Exception("Flush is failed")) + } + } + + val repository = AnalyticHyperskillRepositoryImpl(remoteDataSource, cacheDataSource) + + runBlocking { + cacheDataSource.logEvents(initialEvents) + repository.flushEvents(isAuthorized = true) + repository.logEvent(extraEvent) + assertEquals( + expected = cacheDataSource.getEvents(), + actual = initialEvents + extraEvent + ) + } + } + + private fun getAnalyticEventStub( + name: String + ): AnalyticEvent = + object : AnalyticEvent { + override val name: String = name + override val sources: Set + get() = setOf(AnalyticSource.HYPERSKILL_API) + + override fun toString(): String = name + } +} \ No newline at end of file diff --git a/shared/src/commonTest/kotlin/org/hyperskill/core/presentation/CompletableCoroutineActionDispatcherTest.kt b/shared/src/commonTest/kotlin/org/hyperskill/core/presentation/CompletableCoroutineActionDispatcherTest.kt new file mode 100644 index 000000000..64cbaeaa1 --- /dev/null +++ b/shared/src/commonTest/kotlin/org/hyperskill/core/presentation/CompletableCoroutineActionDispatcherTest.kt @@ -0,0 +1,151 @@ +package org.hyperskill.core.presentation + +import kotlin.test.Test +import kotlin.test.assertContentEquals +import kotlinx.coroutines.Job +import kotlinx.coroutines.delay +import kotlinx.coroutines.runBlocking +import org.hyperskill.app.core.presentation.CompletableCoroutineActionDispatcher +import org.hyperskill.app.core.presentation.CompletableCoroutineActionDispatcherConfig +import ru.nobird.app.presentation.redux.dispatcher.wrapWithActionDispatcher +import ru.nobird.app.presentation.redux.feature.ReduxFeature +import ru.nobird.app.presentation.redux.reducer.StateReducer + +class CompletableCoroutineActionDispatcherTest { + + @Test + fun `Action handling should not be cancelled if the feature is cancelled`() { + runBlocking { + val actionDispatcherScope = + CompletableCoroutineActionDispatcherConfig(this).createScope() + + val actualHandledActions = mutableListOf() + + val actionDispatcher = + object : CompletableCoroutineActionDispatcher( + coroutineScope = actionDispatcherScope + ) { + override suspend fun handleNonCancellableAction(action: TestFeature.Action) { + delay(5) + actualHandledActions.add(action) + } + } + + val feature = ReduxFeature( + initialState = TestFeature.State, + reducer = TestReducer() + ).wrapWithActionDispatcher(actionDispatcher) + + feature.addActionListener { action -> + if (action is TestFeature.Action.CancelFeatureAction) { + feature.cancel() + } + } + + feature.onNewMessage(TestFeature.Message.ProduceThreeActionsAndCancel) + + // Wait for actionDispatcher tasks to be finished + actionDispatcherScope.coroutineContext[Job]?.join() + + val expectedHandledActions = listOf( + TestFeature.Action.Action1, + TestFeature.Action.Action2, + TestFeature.Action.Action3, + TestFeature.Action.CancelFeatureAction + ) + + assertContentEquals( + expected = expectedHandledActions, + actual = actualHandledActions + ) + } + } + + @Test + fun `Actions produced after feature cancellation should not be handled`() { + runBlocking { + val actionDispatcherScope = + CompletableCoroutineActionDispatcherConfig(this).createScope() + + val actualHandledActions = mutableListOf() + + val actionDispatcher = + object : CompletableCoroutineActionDispatcher( + coroutineScope = actionDispatcherScope + ) { + override suspend fun handleNonCancellableAction(action: TestFeature.Action) { + delay(5) + actualHandledActions.add(action) + } + } + + val feature = ReduxFeature( + initialState = TestFeature.State, + reducer = TestReducer() + ).wrapWithActionDispatcher(actionDispatcher) + + feature.addActionListener { action -> + if (action is TestFeature.Action.CancelFeatureAction) { + feature.cancel() + } + } + + feature.onNewMessage(TestFeature.Message.ProduceThreeActionsWithCancellationInTheMiddle) + + // Wait for actionDispatcher tasks to be finished + actionDispatcherScope.coroutineContext[Job]?.join() + + val expectedHandledActions = listOf( + TestFeature.Action.Action1, + TestFeature.Action.CancelFeatureAction + ) + + assertContentEquals( + expected = expectedHandledActions, + actual = actualHandledActions + ) + } + } +} + +private object TestFeature { + object State + + sealed interface Message { + data object ProduceThreeActionsAndCancel : Message + data object ProduceThreeActionsWithCancellationInTheMiddle : Message + } + + sealed interface Action { + data object Action1 : Action + data object Action2 : Action + data object Action3 : Action + + data object CancelFeatureAction : Action + } +} + +private class TestReducer : StateReducer { + override fun reduce( + state: TestFeature.State, + message: TestFeature.Message + ): Pair> = + when (message) { + TestFeature.Message.ProduceThreeActionsAndCancel -> { + state to setOf( + TestFeature.Action.Action1, + TestFeature.Action.Action2, + TestFeature.Action.Action3, + TestFeature.Action.CancelFeatureAction + ) + } + TestFeature.Message.ProduceThreeActionsWithCancellationInTheMiddle -> { + state to setOf( + TestFeature.Action.Action1, + TestFeature.Action.CancelFeatureAction, + TestFeature.Action.Action2, + TestFeature.Action.Action3 + ) + } + } +} \ No newline at end of file From f80d4e027cc97684457752ec75314c8a07560093 Mon Sep 17 00:00:00 2001 From: github-actions Date: Wed, 7 Aug 2024 02:23:11 +0000 Subject: [PATCH 10/16] Bump build number --- gradle/app.versions.toml | 2 +- .../NotificationServiceExtension/Info.plist | 2 +- .../iosHyperskillApp.xcodeproj/project.pbxproj | 16 ++++++++-------- iosHyperskillApp/iosHyperskillApp/Info.plist | 2 +- .../iosHyperskillAppTests/Info.plist | 2 +- .../iosHyperskillAppUITests/Info.plist | 2 +- 6 files changed, 13 insertions(+), 13 deletions(-) diff --git a/gradle/app.versions.toml b/gradle/app.versions.toml index 74414437e..bad0e4456 100644 --- a/gradle/app.versions.toml +++ b/gradle/app.versions.toml @@ -3,4 +3,4 @@ minSdk = '24' targetSdk = '34' compileSdk = '34' versionName = '1.67' -versionCode = '517' \ No newline at end of file +versionCode = '518' \ No newline at end of file diff --git a/iosHyperskillApp/NotificationServiceExtension/Info.plist b/iosHyperskillApp/NotificationServiceExtension/Info.plist index f256bb10f..ebce726d4 100644 --- a/iosHyperskillApp/NotificationServiceExtension/Info.plist +++ b/iosHyperskillApp/NotificationServiceExtension/Info.plist @@ -9,7 +9,7 @@ CFBundleIdentifier $(PRODUCT_BUNDLE_IDENTIFIER) CFBundleVersion - 544 + 545 CFBundleShortVersionString 1.67 CFBundlePackageType diff --git a/iosHyperskillApp/iosHyperskillApp.xcodeproj/project.pbxproj b/iosHyperskillApp/iosHyperskillApp.xcodeproj/project.pbxproj index 4305f5833..1225edb77 100644 --- a/iosHyperskillApp/iosHyperskillApp.xcodeproj/project.pbxproj +++ b/iosHyperskillApp/iosHyperskillApp.xcodeproj/project.pbxproj @@ -5653,7 +5653,7 @@ buildSettings = { CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 544; + CURRENT_PROJECT_VERSION = 545; DEBUG_INFORMATION_FORMAT = dwarf; DEVELOPMENT_TEAM = UJ4KC2QN7B; GENERATE_INFOPLIST_FILE = NO; @@ -5674,7 +5674,7 @@ buildSettings = { CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 544; + CURRENT_PROJECT_VERSION = 545; DEVELOPMENT_TEAM = UJ4KC2QN7B; GENERATE_INFOPLIST_FILE = NO; INFOPLIST_FILE = iosHyperskillAppUITests/Info.plist; @@ -5695,7 +5695,7 @@ BUNDLE_LOADER = "$(TEST_HOST)"; CLANG_CXX_LANGUAGE_STANDARD = "gnu++17"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 544; + CURRENT_PROJECT_VERSION = 545; DEBUG_INFORMATION_FORMAT = dwarf; DEVELOPMENT_TEAM = UJ4KC2QN7B; INFOPLIST_FILE = iosHyperskillAppTests/Info.plist; @@ -5716,7 +5716,7 @@ BUNDLE_LOADER = "$(TEST_HOST)"; CLANG_CXX_LANGUAGE_STANDARD = "gnu++17"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 544; + CURRENT_PROJECT_VERSION = 545; DEVELOPMENT_TEAM = UJ4KC2QN7B; INFOPLIST_FILE = iosHyperskillAppTests/Info.plist; IPHONEOS_DEPLOYMENT_TARGET = 14.0; @@ -5737,7 +5737,7 @@ CODE_SIGN_ENTITLEMENTS = NotificationServiceExtension/NotificationServiceExtension.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 544; + CURRENT_PROJECT_VERSION = 545; DEBUG_INFORMATION_FORMAT = dwarf; DEVELOPMENT_TEAM = UJ4KC2QN7B; INFOPLIST_FILE = NotificationServiceExtension/Info.plist; @@ -5766,7 +5766,7 @@ CODE_SIGN_ENTITLEMENTS = NotificationServiceExtension/NotificationServiceExtension.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 544; + CURRENT_PROJECT_VERSION = 545; DEVELOPMENT_TEAM = UJ4KC2QN7B; INFOPLIST_FILE = NotificationServiceExtension/Info.plist; IPHONEOS_DEPLOYMENT_TARGET = 14.0; @@ -5912,7 +5912,7 @@ CODE_SIGN_ENTITLEMENTS = iosHyperskillApp/iosHyperskillApp.entitlements; CODE_SIGN_IDENTITY = "iPhone Developer"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 544; + CURRENT_PROJECT_VERSION = 545; DEVELOPMENT_ASSET_PATHS = "\"iosHyperskillApp/Preview Content\""; DEVELOPMENT_TEAM = UJ4KC2QN7B; ENABLE_PREVIEWS = YES; @@ -5948,7 +5948,7 @@ CODE_SIGN_ENTITLEMENTS = iosHyperskillApp/iosHyperskillApp.entitlements; CODE_SIGN_IDENTITY = "iPhone Developer"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 544; + CURRENT_PROJECT_VERSION = 545; DEVELOPMENT_ASSET_PATHS = "\"iosHyperskillApp/Preview Content\""; DEVELOPMENT_TEAM = UJ4KC2QN7B; ENABLE_PREVIEWS = YES; diff --git a/iosHyperskillApp/iosHyperskillApp/Info.plist b/iosHyperskillApp/iosHyperskillApp/Info.plist index 0cdc25d22..c0da1e1be 100644 --- a/iosHyperskillApp/iosHyperskillApp/Info.plist +++ b/iosHyperskillApp/iosHyperskillApp/Info.plist @@ -34,7 +34,7 @@ CFBundleVersion - 544 + 545 FirebaseAppDelegateProxyEnabled FirebaseMessagingAutoInitEnabled diff --git a/iosHyperskillApp/iosHyperskillAppTests/Info.plist b/iosHyperskillApp/iosHyperskillAppTests/Info.plist index a4699dca9..2bca603e6 100644 --- a/iosHyperskillApp/iosHyperskillAppTests/Info.plist +++ b/iosHyperskillApp/iosHyperskillAppTests/Info.plist @@ -15,6 +15,6 @@ CFBundleShortVersionString 1.67 CFBundleVersion - 544 + 545 diff --git a/iosHyperskillApp/iosHyperskillAppUITests/Info.plist b/iosHyperskillApp/iosHyperskillAppUITests/Info.plist index cc6a683b8..eee0dd380 100644 --- a/iosHyperskillApp/iosHyperskillAppUITests/Info.plist +++ b/iosHyperskillApp/iosHyperskillAppUITests/Info.plist @@ -15,6 +15,6 @@ CFBundleShortVersionString 1.67 CFBundleVersion - 544 + 545 From 3975bc4494ab78922df329b78943ef6e166ef69d Mon Sep 17 00:00:00 2001 From: Ivan Magda Date: Thu, 8 Aug 2024 15:48:30 +0700 Subject: [PATCH 11/16] Shared, iOS: Code blanks variable block type (#1150) ^ALTAPPS-1318 --- config/detekt/baseline.xml | 3 + .../project.pbxproj | 4 + .../Shared/Model/BlockOptionsExtensions.swift | 26 +- .../Modules/StepQuiz/StepQuizViewModel.swift | 14 + .../StepQuizCodeBlanksOutputProtocol.swift | 4 + .../StepQuizCodeBlanksViewModel.swift | 12 + .../Views/StepQuizCodeBlanksBlankView.swift | 2 +- ...uizCodeBlanksVariableInstructionView.swift | 95 +++ .../Views/StepQuizCodeBlanksView.swift | 36 +- .../hyperskill/HyperskillAnalyticPart.kt | 1 + .../hyperskill/HyperskillAnalyticTarget.kt | 1 + .../hyperskill/app/step/domain/model/Block.kt | 6 +- .../StepQuizCodeBlanksAnalyticParams.kt | 1 + ...edCodeBlockChildHyperskillAnalyticEvent.kt | 45 ++ .../domain/model/CodeBlock.kt | 79 +- .../domain/model/CodeBlockChild.kt | 18 + .../domain/model/Suggestion.kt | 7 + .../presentation/StepQuizCodeBlanksFeature.kt | 12 +- .../presentation/StepQuizCodeBlanksReducer.kt | 227 +++++- .../StepQuizCodeBlanksStateExtensions.kt | 13 +- .../StepQuizCodeBlanksViewStateMapper.kt | 49 +- .../view/model/StepQuizCodeBlanksViewState.kt | 35 +- .../org/hyperskill/step_quiz/StepQuizTest.kt | 11 +- .../StepQuizCodeBlanksReducerTest.kt | 699 ++++++++++++++++-- .../StepQuizCodeBlanksStateExtensionsTest.kt | 114 ++- .../StepQuizCodeBlanksViewStateMapperTest.kt | 439 ++++++++++- 26 files changed, 1758 insertions(+), 195 deletions(-) create mode 100644 iosHyperskillApp/iosHyperskillApp/Sources/Modules/StepQuizSubmodules/StepQuizCodeBlanks/Views/StepQuizCodeBlanksVariableInstructionView.swift create mode 100644 shared/src/commonMain/kotlin/org/hyperskill/app/step_quiz_code_blanks/domain/analytic/StepQuizCodeBlanksClickedCodeBlockChildHyperskillAnalyticEvent.kt create mode 100644 shared/src/commonMain/kotlin/org/hyperskill/app/step_quiz_code_blanks/domain/model/CodeBlockChild.kt diff --git a/config/detekt/baseline.xml b/config/detekt/baseline.xml index e6d290dfe..93b955676 100644 --- a/config/detekt/baseline.xml +++ b/config/detekt/baseline.xml @@ -26,6 +26,7 @@ CyclomaticComplexMethod:LoadingView.kt$LoadingView$override fun onDraw(canvas: Canvas) CyclomaticComplexMethod:ProfileReducer.kt$ProfileReducer$override fun reduce(state: State, message: Message): ReducerResult CyclomaticComplexMethod:StepQuizActionDispatcher.kt$StepQuizActionDispatcher$override suspend fun doSuspendableAction(action: Action) + CyclomaticComplexMethod:StepQuizCodeBlanksReducer.kt$StepQuizCodeBlanksReducer$private fun handleDeleteButtonClicked( state: State ): StepQuizCodeBlanksReducerResult? CyclomaticComplexMethod:StepQuizHintsReducer.kt$StepQuizHintsReducer$override fun reduce(state: State, message: Message): StepQuizHintsReducerResult CyclomaticComplexMethod:StepQuizReducer.kt$StepQuizReducer$override fun reduce(state: State, message: Message): StepQuizReducerResult CyclomaticComplexMethod:StepQuizReplyValidator.kt$StepQuizReplyValidator$fun validate(dataset: Dataset?, reply: Reply, stepBlockName: String): ReplyValidationResult @@ -64,6 +65,8 @@ LongMethod:ProblemOfDayCardFormDelegate.kt$ProblemOfDayCardFormDelegate$fun render( dateFormatter: SharedDateFormatter, binding: LayoutProblemOfTheDayCardBinding, state: HomeFeature.ProblemOfDayState, areProblemsLimited: Boolean ) LongMethod:ProfileBadges.kt$@Composable fun ProfileBadges( viewState: BadgesViewState, windowWidthSizeClass: WindowWidthSizeClass, onBadgeClick: (BadgeKind) -> Unit, onExpandButtonClick: (ProfileFeature.Message.BadgesVisibilityButton) -> Unit, modifier: Modifier = Modifier ) LongMethod:ProfileSettingsDialogFragment.kt$ProfileSettingsDialogFragment$override fun onViewCreated(view: View, savedInstanceState: Bundle?) + LongMethod:StepQuizCodeBlanksReducer.kt$StepQuizCodeBlanksReducer$private fun handleDeleteButtonClicked( state: State ): StepQuizCodeBlanksReducerResult? + LongMethod:StepQuizCodeBlanksReducer.kt$StepQuizCodeBlanksReducer$private fun handleSuggestionClicked( state: State, message: Message.SuggestionClicked ): StepQuizCodeBlanksReducerResult? LongMethod:StreakFreezeDialogFragment.kt$StreakFreezeDialogFragment$override fun onViewCreated(view: View, savedInstanceState: Bundle?) LongMethod:TrackProgressContent.kt$@Composable fun TrackProgressContent( viewState: ProgressScreenViewState.TrackProgressViewState.Content, onNewMessage: (ProgressScreenFeature.Message) -> Unit, modifier: Modifier = Modifier ) LongParameterList:AppInteractor.kt$AppInteractor$( private val appRepository: AppRepository, private val authInteractor: AuthInteractor, private val currentProfileStateRepository: CurrentProfileStateRepository, private val userStorageInteractor: UserStorageInteractor, private val analyticInteractor: AnalyticInteractor, private val progressesRepository: ProgressesRepository, private val trackRepository: TrackRepository, private val providersRepository: ProvidersRepository, private val projectsRepository: ProjectsRepository, private val shareStreakRepository: ShareStreakRepository, private val pushNotificationsInteractor: PushNotificationsInteractor ) diff --git a/iosHyperskillApp/iosHyperskillApp.xcodeproj/project.pbxproj b/iosHyperskillApp/iosHyperskillApp.xcodeproj/project.pbxproj index 23371aea5..e67ce0e83 100644 --- a/iosHyperskillApp/iosHyperskillApp.xcodeproj/project.pbxproj +++ b/iosHyperskillApp/iosHyperskillApp.xcodeproj/project.pbxproj @@ -166,6 +166,7 @@ 2C370439288BE57A008043BF /* StepQuizCodeFullScreenCodeView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2C370438288BE57A008043BF /* StepQuizCodeFullScreenCodeView.swift */; }; 2C37960F2876F36F00C197E2 /* ProfileViewData.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2C37960E2876F36F00C197E2 /* ProfileViewData.swift */; }; 2C3796122877001700C197E2 /* ProfileHeaderView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2C3796112877001700C197E2 /* ProfileHeaderView.swift */; }; + 2C3B84E82C637AE100FE9D5C /* StepQuizCodeBlanksVariableInstructionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2C3B84E72C637AE100FE9D5C /* StepQuizCodeBlanksVariableInstructionView.swift */; }; 2C3CE3962C1073990011BECA /* StepToolbarContent.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2C3CE3952C1073990011BECA /* StepToolbarContent.swift */; }; 2C3E656D2A12722800BC8DC0 /* TrackSelectionListHeaderView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2C3E656C2A12722800BC8DC0 /* TrackSelectionListHeaderView.swift */; }; 2C3E65702A127F2300BC8DC0 /* BrandLinearGradient.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2C3E656F2A127F2300BC8DC0 /* BrandLinearGradient.swift */; }; @@ -957,6 +958,7 @@ 2C370438288BE57A008043BF /* StepQuizCodeFullScreenCodeView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StepQuizCodeFullScreenCodeView.swift; sourceTree = ""; }; 2C37960E2876F36F00C197E2 /* ProfileViewData.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfileViewData.swift; sourceTree = ""; }; 2C3796112877001700C197E2 /* ProfileHeaderView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfileHeaderView.swift; sourceTree = ""; }; + 2C3B84E72C637AE100FE9D5C /* StepQuizCodeBlanksVariableInstructionView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StepQuizCodeBlanksVariableInstructionView.swift; sourceTree = ""; }; 2C3CE3952C1073990011BECA /* StepToolbarContent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StepToolbarContent.swift; sourceTree = ""; }; 2C3E656C2A12722800BC8DC0 /* TrackSelectionListHeaderView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TrackSelectionListHeaderView.swift; sourceTree = ""; }; 2C3E656F2A127F2300BC8DC0 /* BrandLinearGradient.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BrandLinearGradient.swift; sourceTree = ""; }; @@ -3714,6 +3716,7 @@ 2CD67CA22C452DED00240C17 /* StepQuizCodeBlanksOptionView.swift */, 2CB3BC552C46171000F5354F /* StepQuizCodeBlanksPrintInstructionView.swift */, 2C677D012C4A3F860019AF03 /* StepQuizCodeBlanksSuggestionsView.swift */, + 2C3B84E72C637AE100FE9D5C /* StepQuizCodeBlanksVariableInstructionView.swift */, 2C84E70B2C47BAB6002EE787 /* StepQuizCodeBlanksView.swift */, ); path = Views; @@ -5127,6 +5130,7 @@ 2CD48D872858639500CFCC4A /* StepQuizViewModel.swift in Sources */, 2C7CB6862ADFF389006F78DA /* FillBlanksQuizView.swift in Sources */, E94D238D28057F440003273F /* AuthCredentialsView.swift in Sources */, + 2C3B84E82C637AE100FE9D5C /* StepQuizCodeBlanksVariableInstructionView.swift in Sources */, 2CEEE03728917F1100282849 /* TimeIntervalExtensions.swift in Sources */, 2CE31F4827F1BB79008EEE66 /* AuthSocialAssembly.swift in Sources */, 2C0DB9012864332B001EA35E /* CodeTextViewLayoutManager.swift in Sources */, diff --git a/iosHyperskillApp/iosHyperskillApp/Sources/Extensions/Shared/Model/BlockOptionsExtensions.swift b/iosHyperskillApp/iosHyperskillApp/Sources/Extensions/Shared/Model/BlockOptionsExtensions.swift index d2b2cde49..4304f3bc5 100644 --- a/iosHyperskillApp/iosHyperskillApp/Sources/Extensions/Shared/Model/BlockOptionsExtensions.swift +++ b/iosHyperskillApp/iosHyperskillApp/Sources/Extensions/Shared/Model/BlockOptionsExtensions.swift @@ -11,31 +11,21 @@ extension Block.Options { codeTemplates: [String: String]? = nil, samples: [[String]]? = nil, files: [Block.OptionsFile]? = nil, - codeBlanksStrings: [String]? = nil + codeBlanksStrings: [String]? = nil, + codeBlanksVariables: [String]? = nil, + codeBlanksEnabled: Bool? = nil ) { - let isMultipleChoice: KotlinBoolean? = { - if let isMultipleChoice { - return KotlinBoolean(value: isMultipleChoice) - } - return nil - }() - - let isCheckbox: KotlinBoolean? = { - if let isCheckbox { - return KotlinBoolean(value: isCheckbox) - } - return nil - }() - self.init( - isMultipleChoice: isMultipleChoice, + isMultipleChoice: isMultipleChoice.flatMap(KotlinBoolean.init(value:)), language: language, - isCheckbox: isCheckbox, + isCheckbox: isCheckbox.flatMap(KotlinBoolean.init(value:)), limits: limits, codeTemplates: codeTemplates, samples: samples, files: files, - codeBlanksStrings: codeBlanksStrings + codeBlanksStrings: codeBlanksStrings, + codeBlanksVariables: codeBlanksVariables, + codeBlanksEnabled: codeBlanksEnabled.flatMap(KotlinBoolean.init(value:)) ) } // swiftlint:enable discouraged_optional_boolean diff --git a/iosHyperskillApp/iosHyperskillApp/Sources/Modules/StepQuiz/StepQuizViewModel.swift b/iosHyperskillApp/iosHyperskillApp/Sources/Modules/StepQuiz/StepQuizViewModel.swift index 2a14cbd41..438b1c506 100644 --- a/iosHyperskillApp/iosHyperskillApp/Sources/Modules/StepQuiz/StepQuizViewModel.swift +++ b/iosHyperskillApp/iosHyperskillApp/Sources/Modules/StepQuiz/StepQuizViewModel.swift @@ -248,6 +248,20 @@ extension StepQuizViewModel: StepQuizCodeBlanksOutputProtocol { ) } + func handleStepQuizCodeBlanksDidTapOnCodeBlockChild( + codeBlock: StepQuizCodeBlanksViewStateCodeBlockItem, + codeBlockChild: StepQuizCodeBlanksViewStateCodeBlockChildItem + ) { + onNewMessage( + StepQuizFeatureMessageStepQuizCodeBlanksMessage( + message: StepQuizCodeBlanksFeatureMessageCodeBlockChildClicked( + codeBlockItem: codeBlock, + codeBlockChildItem: codeBlockChild + ) + ) + ) + } + func handleStepQuizCodeBlanksDidTapDelete() { onNewMessage( StepQuizFeatureMessageStepQuizCodeBlanksMessage( diff --git a/iosHyperskillApp/iosHyperskillApp/Sources/Modules/StepQuizSubmodules/StepQuizCodeBlanks/StepQuizCodeBlanksOutputProtocol.swift b/iosHyperskillApp/iosHyperskillApp/Sources/Modules/StepQuizSubmodules/StepQuizCodeBlanks/StepQuizCodeBlanksOutputProtocol.swift index 4fdb8635a..bc0cfcd6b 100644 --- a/iosHyperskillApp/iosHyperskillApp/Sources/Modules/StepQuizSubmodules/StepQuizCodeBlanks/StepQuizCodeBlanksOutputProtocol.swift +++ b/iosHyperskillApp/iosHyperskillApp/Sources/Modules/StepQuizSubmodules/StepQuizCodeBlanks/StepQuizCodeBlanksOutputProtocol.swift @@ -4,6 +4,10 @@ import shared protocol StepQuizCodeBlanksOutputProtocol: AnyObject { func handleStepQuizCodeBlanksDidTapOnSuggestion(_ suggestion: Suggestion) func handleStepQuizCodeBlanksDidTapOnCodeBlock(_ codeBlock: StepQuizCodeBlanksViewStateCodeBlockItem) + func handleStepQuizCodeBlanksDidTapOnCodeBlockChild( + codeBlock: StepQuizCodeBlanksViewStateCodeBlockItem, + codeBlockChild: StepQuizCodeBlanksViewStateCodeBlockChildItem + ) func handleStepQuizCodeBlanksDidTapDelete() func handleStepQuizCodeBlanksDidTapEnter() } diff --git a/iosHyperskillApp/iosHyperskillApp/Sources/Modules/StepQuizSubmodules/StepQuizCodeBlanks/StepQuizCodeBlanksViewModel.swift b/iosHyperskillApp/iosHyperskillApp/Sources/Modules/StepQuizSubmodules/StepQuizCodeBlanks/StepQuizCodeBlanksViewModel.swift index 192568caf..faeb221cb 100644 --- a/iosHyperskillApp/iosHyperskillApp/Sources/Modules/StepQuizSubmodules/StepQuizCodeBlanks/StepQuizCodeBlanksViewModel.swift +++ b/iosHyperskillApp/iosHyperskillApp/Sources/Modules/StepQuizSubmodules/StepQuizCodeBlanks/StepQuizCodeBlanksViewModel.swift @@ -19,6 +19,18 @@ final class StepQuizCodeBlanksViewModel { moduleOutput?.handleStepQuizCodeBlanksDidTapOnCodeBlock(codeBlock) } + @MainActor + func doCodeBlockChildMainAction( + codeBlock: StepQuizCodeBlanksViewStateCodeBlockItem, + codeBlockChild: StepQuizCodeBlanksViewStateCodeBlockChildItem + ) { + selectionFeedbackGenerator.triggerFeedback() + moduleOutput?.handleStepQuizCodeBlanksDidTapOnCodeBlockChild( + codeBlock: codeBlock, + codeBlockChild: codeBlockChild + ) + } + @MainActor func doDeleteAction() { impactFeedbackGenerator.triggerFeedback() diff --git a/iosHyperskillApp/iosHyperskillApp/Sources/Modules/StepQuizSubmodules/StepQuizCodeBlanks/Views/StepQuizCodeBlanksBlankView.swift b/iosHyperskillApp/iosHyperskillApp/Sources/Modules/StepQuizSubmodules/StepQuizCodeBlanks/Views/StepQuizCodeBlanksBlankView.swift index ce5b84c10..e0418f71e 100644 --- a/iosHyperskillApp/iosHyperskillApp/Sources/Modules/StepQuizSubmodules/StepQuizCodeBlanks/Views/StepQuizCodeBlanksBlankView.swift +++ b/iosHyperskillApp/iosHyperskillApp/Sources/Modules/StepQuizSubmodules/StepQuizCodeBlanks/Views/StepQuizCodeBlanksBlankView.swift @@ -30,7 +30,7 @@ extension StepQuizCodeBlanksBlankView { fileprivate var size: CGSize { switch self { case .small: - CGSize(width: 100, height: 32) + CGSize(width: 100, height: 40) case .large: CGSize(width: 208, height: 48) } diff --git a/iosHyperskillApp/iosHyperskillApp/Sources/Modules/StepQuizSubmodules/StepQuizCodeBlanks/Views/StepQuizCodeBlanksVariableInstructionView.swift b/iosHyperskillApp/iosHyperskillApp/Sources/Modules/StepQuizSubmodules/StepQuizCodeBlanks/Views/StepQuizCodeBlanksVariableInstructionView.swift new file mode 100644 index 000000000..262f16adc --- /dev/null +++ b/iosHyperskillApp/iosHyperskillApp/Sources/Modules/StepQuizSubmodules/StepQuizCodeBlanks/Views/StepQuizCodeBlanksVariableInstructionView.swift @@ -0,0 +1,95 @@ +import shared +import SwiftUI + +struct StepQuizCodeBlanksVariableInstructionView: View { + let variableItem: StepQuizCodeBlanksViewStateCodeBlockItemVariable + + let onChildTap: (StepQuizCodeBlanksViewStateCodeBlockChildItem) -> Void + + var body: some View { + ScrollView(.horizontal, showsIndicators: false) { + HStack(alignment: .center, spacing: LayoutInsets.smallInset) { + if let nameChild = variableItem.name { + childView(child: nameChild) + .onTapGesture { + onChildTap(nameChild) + } + } + + Text("=") + .font(StepQuizCodeBlanksAppearance.blankFont) + .foregroundColor(StepQuizCodeBlanksAppearance.blankTextColor) + + if let valueChild = variableItem.value { + childView(child: valueChild) + .onTapGesture { + onChildTap(valueChild) + } + } + } + .padding(.horizontal, LayoutInsets.defaultInset) + .padding(.vertical, LayoutInsets.smallInset) + .background(Color(ColorPalette.violet400Alpha7)) + .cornerRadius(8) + .animation(.default, value: variableItem) + .padding(.horizontal) + } + .scrollBounceBehaviorBasedOnSize(axes: .horizontal) + } + + @ViewBuilder + private func childView( + child: StepQuizCodeBlanksViewStateCodeBlockChildItem + ) -> some View { + if let value = child.value { + StepQuizCodeBlanksOptionView(text: value, isActive: child.isActive) + } else { + StepQuizCodeBlanksBlankView(style: .small, isActive: child.isActive) + } + } +} + +#if DEBUG +#Preview { + VStack { + StepQuizCodeBlanksVariableInstructionView( + variableItem: StepQuizCodeBlanksViewStateCodeBlockItemVariable( + id: 0, + children: [ + StepQuizCodeBlanksViewStateCodeBlockChildItem(id: 0, isActive: true, value: nil), + StepQuizCodeBlanksViewStateCodeBlockChildItem(id: 1, isActive: false, value: nil) + ] + ), + onChildTap: { _ in } + ) + + StepQuizCodeBlanksVariableInstructionView( + variableItem: StepQuizCodeBlanksViewStateCodeBlockItemVariable( + id: 0, + children: [ + StepQuizCodeBlanksViewStateCodeBlockChildItem(id: 0, isActive: false, value: "fruit_a"), + StepQuizCodeBlanksViewStateCodeBlockChildItem(id: 1, isActive: true, value: nil) + ] + ), + onChildTap: { _ in } + ) + + StepQuizCodeBlanksVariableInstructionView( + variableItem: StepQuizCodeBlanksViewStateCodeBlockItemVariable( + id: 0, + children: [ + StepQuizCodeBlanksViewStateCodeBlockChildItem(id: 0, isActive: false, value: "fruit_a"), + StepQuizCodeBlanksViewStateCodeBlockChildItem( + id: 1, + isActive: true, + value: "Typing messages out of the blue" + ) + ] + ), + onChildTap: { _ in } + ) + } + .frame(maxWidth: .infinity) + .padding() +} +#endif diff --git a/iosHyperskillApp/iosHyperskillApp/Sources/Modules/StepQuizSubmodules/StepQuizCodeBlanks/Views/StepQuizCodeBlanksView.swift b/iosHyperskillApp/iosHyperskillApp/Sources/Modules/StepQuizSubmodules/StepQuizCodeBlanks/Views/StepQuizCodeBlanksView.swift index 4d29b861c..2ba805069 100644 --- a/iosHyperskillApp/iosHyperskillApp/Sources/Modules/StepQuizSubmodules/StepQuizCodeBlanks/Views/StepQuizCodeBlanksView.swift +++ b/iosHyperskillApp/iosHyperskillApp/Sources/Modules/StepQuizSubmodules/StepQuizCodeBlanks/Views/StepQuizCodeBlanksView.swift @@ -74,6 +74,19 @@ struct StepQuizCodeBlanksView: View { .onTapGesture { viewModel.doCodeBlockMainAction(codeBlock) } + case .variable(let variableItem): + StepQuizCodeBlanksVariableInstructionView( + variableItem: variableItem, + onChildTap: { codeBlockChild in + viewModel.doCodeBlockChildMainAction( + codeBlock: codeBlock, + codeBlockChild: codeBlockChild + ) + } + ) + .onTapGesture { + viewModel.doCodeBlockMainAction(codeBlock) + } } } @@ -125,7 +138,14 @@ extension StepQuizCodeBlanksView: Equatable { StepQuizCodeBlanksView( viewStateKs: .content( StepQuizCodeBlanksViewStateContent( - codeBlocks: [StepQuizCodeBlanksViewStateCodeBlockItemPrint(id: 0, isActive: true, output: nil)], + codeBlocks: [ + StepQuizCodeBlanksViewStateCodeBlockItemPrint( + id: 0, + children: [ + StepQuizCodeBlanksViewStateCodeBlockChildItem(id: 0, isActive: true, value: nil) + ] + ) + ], suggestions: [ Suggestion.ConstantString(text: "There is a cat on the keyboard, it is true"), Suggestion.ConstantString(text: "Typing messages out of the blue") @@ -149,13 +169,19 @@ extension StepQuizCodeBlanksView: Equatable { codeBlocks: [ StepQuizCodeBlanksViewStateCodeBlockItemPrint( id: 0, - isActive: false, - output: "There is a cat on the keyboard, it is true" + children: [ + StepQuizCodeBlanksViewStateCodeBlockChildItem( + id: 0, + isActive: false, + value: "There is a cat on the keyboard, it is true" + ) + ] ), StepQuizCodeBlanksViewStateCodeBlockItemPrint( id: 1, - isActive: true, - output: nil + children: [ + StepQuizCodeBlanksViewStateCodeBlockChildItem(id: 0, isActive: true, value: nil) + ] ) ], suggestions: [ diff --git a/shared/src/commonMain/kotlin/org/hyperskill/app/analytic/domain/model/hyperskill/HyperskillAnalyticPart.kt b/shared/src/commonMain/kotlin/org/hyperskill/app/analytic/domain/model/hyperskill/HyperskillAnalyticPart.kt index a7b0df89b..048fbbcae 100644 --- a/shared/src/commonMain/kotlin/org/hyperskill/app/analytic/domain/model/hyperskill/HyperskillAnalyticPart.kt +++ b/shared/src/commonMain/kotlin/org/hyperskill/app/analytic/domain/model/hyperskill/HyperskillAnalyticPart.kt @@ -45,5 +45,6 @@ enum class HyperskillAnalyticPart(val partName: String) { USERS_INTERVIEW_WIDGET("users_interview_widget"), UNSUPPORTED_QUIZ_PLACEHOLDER("unsupported_quiz_placeholder"), CODE_BLANKS("code_blanks"), + CODE_BLOCK("code_block"), STEP_QUIZ_FEEDBACK("step_quiz_feedback") } \ No newline at end of file diff --git a/shared/src/commonMain/kotlin/org/hyperskill/app/analytic/domain/model/hyperskill/HyperskillAnalyticTarget.kt b/shared/src/commonMain/kotlin/org/hyperskill/app/analytic/domain/model/hyperskill/HyperskillAnalyticTarget.kt index b4251ea2d..c1ad1a150 100644 --- a/shared/src/commonMain/kotlin/org/hyperskill/app/analytic/domain/model/hyperskill/HyperskillAnalyticTarget.kt +++ b/shared/src/commonMain/kotlin/org/hyperskill/app/analytic/domain/model/hyperskill/HyperskillAnalyticTarget.kt @@ -136,5 +136,6 @@ enum class HyperskillAnalyticTarget(val targetName: String) { SHOW_REPLIES("show_replies"), CODE_BLOCK("code_block"), CODE_BLOCK_SUGGESTION("code_block_suggestion"), + CODE_BLOCK_CHILD("code_block_child"), LOAD_MORE("load_more") } \ No newline at end of file diff --git a/shared/src/commonMain/kotlin/org/hyperskill/app/step/domain/model/Block.kt b/shared/src/commonMain/kotlin/org/hyperskill/app/step/domain/model/Block.kt index 91e5d8757..dffdbfb51 100644 --- a/shared/src/commonMain/kotlin/org/hyperskill/app/step/domain/model/Block.kt +++ b/shared/src/commonMain/kotlin/org/hyperskill/app/step/domain/model/Block.kt @@ -30,7 +30,11 @@ data class Block( @SerialName("files") val files: List? = null, @SerialName("code_blanks_strings") - val codeBlanksStrings: List? = null + val codeBlanksStrings: List? = null, + @SerialName("code_blanks_variables") + val codeBlanksVariables: List? = null, + @SerialName("code_blanks_enabled") + val codeBlanksEnabled: Boolean? = null ) { @Serializable data class File( diff --git a/shared/src/commonMain/kotlin/org/hyperskill/app/step_quiz_code_blanks/domain/analytic/StepQuizCodeBlanksAnalyticParams.kt b/shared/src/commonMain/kotlin/org/hyperskill/app/step_quiz_code_blanks/domain/analytic/StepQuizCodeBlanksAnalyticParams.kt index 663584ab9..499ac6958 100644 --- a/shared/src/commonMain/kotlin/org/hyperskill/app/step_quiz_code_blanks/domain/analytic/StepQuizCodeBlanksAnalyticParams.kt +++ b/shared/src/commonMain/kotlin/org/hyperskill/app/step_quiz_code_blanks/domain/analytic/StepQuizCodeBlanksAnalyticParams.kt @@ -3,4 +3,5 @@ package org.hyperskill.app.step_quiz_code_blanks.domain.analytic internal object StepQuizCodeBlanksAnalyticParams { const val PARAM_CODE_BLOCK = "code_block" const val PARAM_SUGGESTION = "suggestion" + const val PARAM_CODE_BLOCK_CHILD = "code_block_child" } \ No newline at end of file diff --git a/shared/src/commonMain/kotlin/org/hyperskill/app/step_quiz_code_blanks/domain/analytic/StepQuizCodeBlanksClickedCodeBlockChildHyperskillAnalyticEvent.kt b/shared/src/commonMain/kotlin/org/hyperskill/app/step_quiz_code_blanks/domain/analytic/StepQuizCodeBlanksClickedCodeBlockChildHyperskillAnalyticEvent.kt new file mode 100644 index 000000000..d84afcf0c --- /dev/null +++ b/shared/src/commonMain/kotlin/org/hyperskill/app/step_quiz_code_blanks/domain/analytic/StepQuizCodeBlanksClickedCodeBlockChildHyperskillAnalyticEvent.kt @@ -0,0 +1,45 @@ +package org.hyperskill.app.step_quiz_code_blanks.domain.analytic + +import org.hyperskill.app.analytic.domain.model.hyperskill.HyperskillAnalyticAction +import org.hyperskill.app.analytic.domain.model.hyperskill.HyperskillAnalyticEvent +import org.hyperskill.app.analytic.domain.model.hyperskill.HyperskillAnalyticPart +import org.hyperskill.app.analytic.domain.model.hyperskill.HyperskillAnalyticRoute +import org.hyperskill.app.analytic.domain.model.hyperskill.HyperskillAnalyticTarget +import org.hyperskill.app.step_quiz_code_blanks.domain.model.CodeBlock +import org.hyperskill.app.step_quiz_code_blanks.domain.model.CodeBlockChild +import ru.nobird.app.core.model.mapOfNotNull + +/** + * Represents click on the code block child in the code blanks analytic event. + * + * JSON payload: + * ``` + * { + * "route": "/learn/step/1", + * "action": "click", + * "part": "code_block", + * "target": "code_block_child", + * "context": + * { + * "code_block": "Blank(isActive=true, suggestions=[Print])", + * "code_block_child": "SelectSuggestion(isActive=true, suggestions=[Print], selectedSuggestion=null)" + * } + * } + * ``` + * + * @see HyperskillAnalyticEvent + */ +class StepQuizCodeBlanksClickedCodeBlockChildHyperskillAnalyticEvent( + route: HyperskillAnalyticRoute, + codeBlock: CodeBlock?, + codeBlockChild: CodeBlockChild? +) : HyperskillAnalyticEvent( + route = route, + action = HyperskillAnalyticAction.CLICK, + part = HyperskillAnalyticPart.CODE_BLOCK, + target = HyperskillAnalyticTarget.CODE_BLOCK_CHILD, + context = mapOfNotNull( + StepQuizCodeBlanksAnalyticParams.PARAM_CODE_BLOCK to codeBlock?.analyticRepresentation, + StepQuizCodeBlanksAnalyticParams.PARAM_CODE_BLOCK_CHILD to codeBlockChild?.toString() + ) +) \ No newline at end of file diff --git a/shared/src/commonMain/kotlin/org/hyperskill/app/step_quiz_code_blanks/domain/model/CodeBlock.kt b/shared/src/commonMain/kotlin/org/hyperskill/app/step_quiz_code_blanks/domain/model/CodeBlock.kt index aa6cfefc5..5dcfec10c 100644 --- a/shared/src/commonMain/kotlin/org/hyperskill/app/step_quiz_code_blanks/domain/model/CodeBlock.kt +++ b/shared/src/commonMain/kotlin/org/hyperskill/app/step_quiz_code_blanks/domain/model/CodeBlock.kt @@ -1,39 +1,84 @@ package org.hyperskill.app.step_quiz_code_blanks.domain.model sealed class CodeBlock { - abstract val isActive: Boolean + internal abstract val isActive: Boolean - abstract val suggestions: List + internal abstract val suggestions: List + + internal abstract val children: List internal abstract val analyticRepresentation: String - data class Blank( - override val isActive: Boolean - ) : CodeBlock() { - override val suggestions: List - get() = listOf(Suggestion.Print) + internal abstract fun toReplyString(): String + + internal fun activeChild(): CodeBlockChild? = + children.firstOrNull { it.isActive } - override fun toString(): String = "" + internal fun activeChildIndex(): Int? = + children.indexOfFirst { it.isActive }.takeIf { it != -1 } + + internal data class Blank( + override val isActive: Boolean, + override val suggestions: List + ) : CodeBlock() { + override val children: List = emptyList() override val analyticRepresentation: String get() = "Blank(isActive=$isActive, suggestions=$suggestions)" + + override fun toReplyString(): String = "" } - data class Print( - override val isActive: Boolean, - override val suggestions: List, - val selectedSuggestion: Suggestion.ConstantString? + internal data class Print( + override val children: List ) : CodeBlock() { - override fun toString(): String = + val select: CodeBlockChild.SelectSuggestion? + get() = children.firstOrNull() + + override val isActive: Boolean = false + + override val suggestions: List = emptyList() + + override val analyticRepresentation: String = + "Print(children=$children)" + + override fun toReplyString(): String = buildString { append("print(") - if (selectedSuggestion != null) { - append(selectedSuggestion.text) - } + append( + children.joinToString(separator = ", ") { it.toReplyString() } + ) append(")") } + override fun toString(): String = + "Print(children=$children)" + } + + internal data class Variable( + override val children: List + ) : CodeBlock() { + val name: CodeBlockChild.SelectSuggestion? + get() = children.firstOrNull() + + val value: CodeBlockChild.SelectSuggestion? + get() = children.lastOrNull() + + override val isActive: Boolean = false + + override val suggestions: List = emptyList() + override val analyticRepresentation: String - get() = "Print(isActive=$isActive, suggestions=$suggestions, selectedSuggestion=$selectedSuggestion)" + get() = "Variable(children=$children)" + + override fun toReplyString(): String = + buildString { + append(name?.toReplyString() ?: "") + append(" = ") + append(value?.toReplyString() ?: "") + } + + override fun toString(): String = + "Variable(children=$children)" } } \ No newline at end of file diff --git a/shared/src/commonMain/kotlin/org/hyperskill/app/step_quiz_code_blanks/domain/model/CodeBlockChild.kt b/shared/src/commonMain/kotlin/org/hyperskill/app/step_quiz_code_blanks/domain/model/CodeBlockChild.kt new file mode 100644 index 000000000..861bf11bf --- /dev/null +++ b/shared/src/commonMain/kotlin/org/hyperskill/app/step_quiz_code_blanks/domain/model/CodeBlockChild.kt @@ -0,0 +1,18 @@ +package org.hyperskill.app.step_quiz_code_blanks.domain.model + +sealed class CodeBlockChild { + internal abstract val isActive: Boolean + + internal abstract val suggestions: List + + internal abstract fun toReplyString(): String + + internal data class SelectSuggestion( + override val isActive: Boolean, + override val suggestions: List, + val selectedSuggestion: Suggestion.ConstantString? + ) : CodeBlockChild() { + override fun toReplyString(): String = + selectedSuggestion?.text ?: "" + } +} \ No newline at end of file diff --git a/shared/src/commonMain/kotlin/org/hyperskill/app/step_quiz_code_blanks/domain/model/Suggestion.kt b/shared/src/commonMain/kotlin/org/hyperskill/app/step_quiz_code_blanks/domain/model/Suggestion.kt index 83cfbd449..f8eb34b21 100644 --- a/shared/src/commonMain/kotlin/org/hyperskill/app/step_quiz_code_blanks/domain/model/Suggestion.kt +++ b/shared/src/commonMain/kotlin/org/hyperskill/app/step_quiz_code_blanks/domain/model/Suggestion.kt @@ -12,6 +12,13 @@ sealed class Suggestion { "Print(text='$text')" } + data object Variable : Suggestion() { + override val text: String = "variable" + + override val analyticRepresentation: String = + "Variable(text='$text')" + } + data class ConstantString( override val text: String ) : Suggestion() { diff --git a/shared/src/commonMain/kotlin/org/hyperskill/app/step_quiz_code_blanks/presentation/StepQuizCodeBlanksFeature.kt b/shared/src/commonMain/kotlin/org/hyperskill/app/step_quiz_code_blanks/presentation/StepQuizCodeBlanksFeature.kt index 8037054fe..a9badc7ea 100644 --- a/shared/src/commonMain/kotlin/org/hyperskill/app/step_quiz_code_blanks/presentation/StepQuizCodeBlanksFeature.kt +++ b/shared/src/commonMain/kotlin/org/hyperskill/app/step_quiz_code_blanks/presentation/StepQuizCodeBlanksFeature.kt @@ -8,7 +8,10 @@ import org.hyperskill.app.step_quiz_code_blanks.view.model.StepQuizCodeBlanksVie object StepQuizCodeBlanksFeature { internal fun isCodeBlanksFeatureAvailable(step: Step): Boolean = - step.block.options.codeBlanksStrings.isNullOrEmpty().not() + step.block.options.codeBlanksEnabled == true + + internal fun isVariableSuggestionsAvailable(step: Step): Boolean = + step.block.options.codeBlanksVariables?.isNotEmpty() == true internal fun initialState(): State = State.Idle @@ -21,6 +24,9 @@ object StepQuizCodeBlanksFeature { ) : State { internal val codeBlanksStringsSuggestions: List = step.block.options.codeBlanksStrings.orEmpty().map(Suggestion::ConstantString) + + internal val codeBlanksVariablesSuggestions: List = + step.block.options.codeBlanksVariables.orEmpty().map(Suggestion::ConstantString) } } @@ -28,6 +34,10 @@ object StepQuizCodeBlanksFeature { data class SuggestionClicked(val suggestion: Suggestion) : Message data class CodeBlockClicked(val codeBlockItem: StepQuizCodeBlanksViewState.CodeBlockItem) : Message + data class CodeBlockChildClicked( + val codeBlockItem: StepQuizCodeBlanksViewState.CodeBlockItem, + val codeBlockChildItem: StepQuizCodeBlanksViewState.CodeBlockChildItem + ) : Message data object DeleteButtonClicked : Message data object EnterButtonClicked : Message diff --git a/shared/src/commonMain/kotlin/org/hyperskill/app/step_quiz_code_blanks/presentation/StepQuizCodeBlanksReducer.kt b/shared/src/commonMain/kotlin/org/hyperskill/app/step_quiz_code_blanks/presentation/StepQuizCodeBlanksReducer.kt index 9326e100c..e3032c8f7 100644 --- a/shared/src/commonMain/kotlin/org/hyperskill/app/step_quiz_code_blanks/presentation/StepQuizCodeBlanksReducer.kt +++ b/shared/src/commonMain/kotlin/org/hyperskill/app/step_quiz_code_blanks/presentation/StepQuizCodeBlanksReducer.kt @@ -1,11 +1,13 @@ package org.hyperskill.app.step_quiz_code_blanks.presentation import org.hyperskill.app.step.domain.model.StepRoute +import org.hyperskill.app.step_quiz_code_blanks.domain.analytic.StepQuizCodeBlanksClickedCodeBlockChildHyperskillAnalyticEvent import org.hyperskill.app.step_quiz_code_blanks.domain.analytic.StepQuizCodeBlanksClickedCodeBlockHyperskillAnalyticEvent import org.hyperskill.app.step_quiz_code_blanks.domain.analytic.StepQuizCodeBlanksClickedDeleteHyperskillAnalyticEvent import org.hyperskill.app.step_quiz_code_blanks.domain.analytic.StepQuizCodeBlanksClickedEnterHyperskillAnalyticEvent import org.hyperskill.app.step_quiz_code_blanks.domain.analytic.StepQuizCodeBlanksClickedSuggestionHyperskillAnalyticEvent import org.hyperskill.app.step_quiz_code_blanks.domain.model.CodeBlock +import org.hyperskill.app.step_quiz_code_blanks.domain.model.CodeBlockChild import org.hyperskill.app.step_quiz_code_blanks.domain.model.Suggestion import org.hyperskill.app.step_quiz_code_blanks.presentation.StepQuizCodeBlanksFeature.Action import org.hyperskill.app.step_quiz_code_blanks.presentation.StepQuizCodeBlanksFeature.InternalAction @@ -25,6 +27,7 @@ class StepQuizCodeBlanksReducer( is InternalMessage.Initialize -> initialize(message) is Message.SuggestionClicked -> handleSuggestionClicked(state, message) is Message.CodeBlockClicked -> handleCodeBlockClicked(state, message) + is Message.CodeBlockChildClicked -> handleCodeBlockChildClicked(state, message) Message.DeleteButtonClicked -> handleDeleteButtonClicked(state) Message.EnterButtonClicked -> handleEnterButtonClicked(state) } ?: (state to emptySet()) @@ -34,7 +37,14 @@ class StepQuizCodeBlanksReducer( ): StepQuizCodeBlanksReducerResult = State.Content( step = message.step, - codeBlocks = listOf(CodeBlock.Blank(isActive = true)) + codeBlocks = listOf( + createBlankCodeBlock( + isActive = true, + isVariableSuggestionAvailable = StepQuizCodeBlanksFeature.isVariableSuggestionsAvailable( + step = message.step + ) + ) + ) ) to emptySet() private fun handleSuggestionClicked( @@ -60,24 +70,61 @@ class StepQuizCodeBlanksReducer( if (activeCodeBlockIndex == null) { return state to actions } - val activeCodeBlock = state.codeBlocks[activeCodeBlockIndex] - - if (!activeCodeBlock.suggestions.contains(message.suggestion)) { - return state to actions - } val newCodeBlock = - when (activeCodeBlock) { - is CodeBlock.Blank -> - CodeBlock.Print( - isActive = true, - suggestions = state.codeBlanksStringsSuggestions, - selectedSuggestion = null - ) - is CodeBlock.Print -> + when (val activeCodeBlock = state.codeBlocks[activeCodeBlockIndex]) { + is CodeBlock.Blank -> when (message.suggestion) { + Suggestion.Print -> + CodeBlock.Print( + children = listOf( + CodeBlockChild.SelectSuggestion( + isActive = true, + suggestions = state.codeBlanksVariablesSuggestions + + state.codeBlanksStringsSuggestions, + selectedSuggestion = null + ) + ) + ) + Suggestion.Variable -> + CodeBlock.Variable( + children = listOf( + CodeBlockChild.SelectSuggestion( + isActive = true, + suggestions = state.codeBlanksVariablesSuggestions, + selectedSuggestion = null + ), + CodeBlockChild.SelectSuggestion( + isActive = false, + suggestions = state.codeBlanksStringsSuggestions, + selectedSuggestion = null + ) + ) + ) + else -> activeCodeBlock + } + is CodeBlock.Print -> { activeCodeBlock.copy( - selectedSuggestion = message.suggestion as? Suggestion.ConstantString + children = listOfNotNull( + activeCodeBlock.select?.copy( + selectedSuggestion = message.suggestion as? Suggestion.ConstantString + ) + ) ) + } + is CodeBlock.Variable -> { + activeCodeBlock.activeChildIndex()?.let { activeChildIndex -> + activeCodeBlock.copy( + children = activeCodeBlock.children.mutate { + set( + activeChildIndex, + activeCodeBlock.children[activeChildIndex].copy( + selectedSuggestion = message.suggestion as? Suggestion.ConstantString + ) + ) + } + ) + } ?: activeCodeBlock + } } val newCodeBlocks = state.codeBlocks.mutate { set(activeCodeBlockIndex, newCodeBlock) } @@ -109,14 +156,60 @@ class StepQuizCodeBlanksReducer( } else { val newCodeBlocks = state.codeBlocks.mutate { state.activeCodeBlockIndex()?.let { - set(it, copyCodeBlock(state.codeBlocks[it], isActive = false)) + set(it, setCodeBlockIsActive(codeBlock = state.codeBlocks[it], isActive = false)) } - set(targetCodeBlockIndex, copyCodeBlock(targetCodeBlock, isActive = true)) + set(targetCodeBlockIndex, setCodeBlockIsActive(codeBlock = targetCodeBlock, isActive = true)) } state.copy(codeBlocks = newCodeBlocks) to actions } } + private fun handleCodeBlockChildClicked( + state: State, + message: Message.CodeBlockChildClicked + ): StepQuizCodeBlanksReducerResult? { + if (state !is State.Content) { + return null + } + + val targetCodeBlockIndex = message.codeBlockItem.id + val targetCodeBlock = state.codeBlocks.getOrNull(index = targetCodeBlockIndex) + + val actions = setOf( + InternalAction.LogAnalyticEvent( + StepQuizCodeBlanksClickedCodeBlockChildHyperskillAnalyticEvent( + route = stepRoute.analyticRoute, + codeBlock = targetCodeBlock, + codeBlockChild = targetCodeBlock?.children?.getOrNull(index = message.codeBlockChildItem.id) + ) + ) + ) + + return when (targetCodeBlock) { + is CodeBlock.Variable -> { + val newCodeBlocks = state.codeBlocks.mutate { + state.activeCodeBlockIndex()?.let { + set(it, setCodeBlockIsActive(codeBlock = state.codeBlocks[it], isActive = false)) + } + set( + targetCodeBlockIndex, + targetCodeBlock.copy( + children = targetCodeBlock.children.mapIndexed { index, child -> + if (index == message.codeBlockChildItem.id) { + child.copy(isActive = true) + } else { + child.copy(isActive = false) + } + } + ) + ) + } + state.copy(codeBlocks = newCodeBlocks) to actions + } + else -> state to actions + } + } + private fun handleDeleteButtonClicked( state: State ): StepQuizCodeBlanksReducerResult? { @@ -148,10 +241,19 @@ class StepQuizCodeBlanksReducer( activeCodeBlockIndex + 1 } state.codeBlocks.getOrNull(nextActiveIndex)?.let { - set(nextActiveIndex, copyCodeBlock(it, isActive = true)) + set(nextActiveIndex, setCodeBlockIsActive(codeBlock = it, isActive = true)) } removeAt(activeCodeBlockIndex) } + val replaceActiveCodeWithBlank = { + set( + activeCodeBlockIndex, + createBlankCodeBlock( + isActive = true, + isVariableSuggestionAvailable = state.isVariableSuggestionsAvailable + ) + ) + } when (val activeCodeBlock = state.codeBlocks[activeCodeBlockIndex]) { is CodeBlock.Blank -> { @@ -160,12 +262,46 @@ class StepQuizCodeBlanksReducer( } } is CodeBlock.Print -> { - if (activeCodeBlock.selectedSuggestion != null) { - set(activeCodeBlockIndex, activeCodeBlock.copy(selectedSuggestion = null)) + if (activeCodeBlock.select?.selectedSuggestion != null) { + set( + activeCodeBlockIndex, + activeCodeBlock.copy( + children = listOfNotNull( + activeCodeBlock.select?.copy(selectedSuggestion = null) + ) + ) + ) } else if (state.codeBlocks.size > 1) { removeActiveCodeBlockAndSetNextActive() } else { - set(activeCodeBlockIndex, CodeBlock.Blank(isActive = true)) + replaceActiveCodeWithBlank() + } + } + is CodeBlock.Variable -> { + val activeChildIndex = activeCodeBlock.activeChildIndex() ?: return@mutate + val activeChild = activeCodeBlock.children[activeChildIndex] + + if (activeChild.selectedSuggestion != null) { + set( + activeCodeBlockIndex, + activeCodeBlock.copy( + children = activeCodeBlock.children.mutate { + set( + activeChildIndex, + activeChild.copy(selectedSuggestion = null) + ) + } + ) + ) + } else if ( + activeCodeBlock.name?.selectedSuggestion == null && + activeCodeBlock.value?.selectedSuggestion == null + ) { + if (state.codeBlocks.size > 1) { + removeActiveCodeBlockAndSetNextActive() + } else { + replaceActiveCodeWithBlank() + } } } } @@ -194,8 +330,17 @@ class StepQuizCodeBlanksReducer( return if (activeCodeBlockIndex != null) { val newCodeBlocks = state.codeBlocks.mutate { - set(activeCodeBlockIndex, copyCodeBlock(state.codeBlocks[activeCodeBlockIndex], isActive = false)) - add(activeCodeBlockIndex + 1, CodeBlock.Blank(isActive = true)) + set( + activeCodeBlockIndex, + setCodeBlockIsActive(codeBlock = state.codeBlocks[activeCodeBlockIndex], isActive = false) + ) + add( + activeCodeBlockIndex + 1, + createBlankCodeBlock( + isActive = true, + isVariableSuggestionAvailable = state.isVariableSuggestionsAvailable + ) + ) } state.copy(codeBlocks = newCodeBlocks) to actions } else { @@ -203,9 +348,41 @@ class StepQuizCodeBlanksReducer( } } - private fun copyCodeBlock(codeBlock: CodeBlock, isActive: Boolean): CodeBlock = + private fun setCodeBlockIsActive(codeBlock: CodeBlock, isActive: Boolean): CodeBlock = when (codeBlock) { is CodeBlock.Blank -> codeBlock.copy(isActive = isActive) - is CodeBlock.Print -> codeBlock.copy(isActive = isActive) + is CodeBlock.Print -> codeBlock.copy(children = listOfNotNull(codeBlock.select?.copy(isActive = isActive))) + is CodeBlock.Variable -> { + if (isActive) { + if (codeBlock.activeChild() != null) { + codeBlock + } else { + codeBlock.copy( + children = codeBlock.children.mapIndexed { index, child -> + if (index == 0) { + child.copy(isActive = true) + } else { + child.copy(isActive = false) + } + } + ) + } + } else { + codeBlock.copy(children = codeBlock.children.map { it.copy(isActive = false) }) + } + } } + + private fun createBlankCodeBlock( + isActive: Boolean, + isVariableSuggestionAvailable: Boolean + ): CodeBlock.Blank = + CodeBlock.Blank( + isActive = isActive, + suggestions = if (isVariableSuggestionAvailable) { + listOf(Suggestion.Print, Suggestion.Variable) + } else { + listOf(Suggestion.Print) + } + ) } \ No newline at end of file diff --git a/shared/src/commonMain/kotlin/org/hyperskill/app/step_quiz_code_blanks/presentation/StepQuizCodeBlanksStateExtensions.kt b/shared/src/commonMain/kotlin/org/hyperskill/app/step_quiz_code_blanks/presentation/StepQuizCodeBlanksStateExtensions.kt index 98af111b1..84d450a18 100644 --- a/shared/src/commonMain/kotlin/org/hyperskill/app/step_quiz_code_blanks/presentation/StepQuizCodeBlanksStateExtensions.kt +++ b/shared/src/commonMain/kotlin/org/hyperskill/app/step_quiz_code_blanks/presentation/StepQuizCodeBlanksStateExtensions.kt @@ -3,10 +3,19 @@ package org.hyperskill.app.step_quiz_code_blanks.presentation import org.hyperskill.app.submissions.domain.model.Reply internal fun StepQuizCodeBlanksFeature.State.Content.activeCodeBlockIndex(): Int? = - codeBlocks.indexOfFirst { it.isActive }.takeIf { it != -1 } + codeBlocks + .indexOfFirst { codeBlock -> + codeBlock.isActive || codeBlock.children.any { it.isActive } + } + .takeIf { it != -1 } + +internal val StepQuizCodeBlanksFeature.State.isVariableSuggestionsAvailable: Boolean + get() = (this as? StepQuizCodeBlanksFeature.State.Content)?.step?.let { + StepQuizCodeBlanksFeature.isVariableSuggestionsAvailable(it) + } ?: false fun StepQuizCodeBlanksFeature.State.Content.createReply(): Reply = Reply.code( - code = codeBlocks.joinToString(separator = "\n") { it.toString() }, + code = codeBlocks.joinToString(separator = "\n") { it.toReplyString() }, language = step.block.options.codeTemplates?.keys?.firstOrNull() ) \ No newline at end of file diff --git a/shared/src/commonMain/kotlin/org/hyperskill/app/step_quiz_code_blanks/view/mapper/StepQuizCodeBlanksViewStateMapper.kt b/shared/src/commonMain/kotlin/org/hyperskill/app/step_quiz_code_blanks/view/mapper/StepQuizCodeBlanksViewStateMapper.kt index e580f8e77..98009a7a1 100644 --- a/shared/src/commonMain/kotlin/org/hyperskill/app/step_quiz_code_blanks/view/mapper/StepQuizCodeBlanksViewStateMapper.kt +++ b/shared/src/commonMain/kotlin/org/hyperskill/app/step_quiz_code_blanks/view/mapper/StepQuizCodeBlanksViewStateMapper.kt @@ -1,7 +1,9 @@ package org.hyperskill.app.step_quiz_code_blanks.view.mapper import org.hyperskill.app.step_quiz_code_blanks.domain.model.CodeBlock +import org.hyperskill.app.step_quiz_code_blanks.domain.model.CodeBlockChild import org.hyperskill.app.step_quiz_code_blanks.presentation.StepQuizCodeBlanksFeature +import org.hyperskill.app.step_quiz_code_blanks.presentation.activeCodeBlockIndex import org.hyperskill.app.step_quiz_code_blanks.view.model.StepQuizCodeBlanksViewState object StepQuizCodeBlanksViewStateMapper { @@ -15,24 +17,42 @@ object StepQuizCodeBlanksViewStateMapper { state: StepQuizCodeBlanksFeature.State.Content ): StepQuizCodeBlanksViewState.Content { val codeBlocks = state.codeBlocks.mapIndexed(::mapCodeBlock) - val activeCodeBlock = state.codeBlocks.firstOrNull { it.isActive } + val activeCodeBlock = state.activeCodeBlockIndex()?.let { state.codeBlocks[it] } val suggestions = when (activeCodeBlock) { is CodeBlock.Blank -> activeCodeBlock.suggestions is CodeBlock.Print -> - if (activeCodeBlock.selectedSuggestion == null) { - activeCodeBlock.suggestions + if (activeCodeBlock.select?.selectedSuggestion == null) { + activeCodeBlock.select?.suggestions } else { emptyList() } + is CodeBlock.Variable -> + (activeCodeBlock.activeChild() as? CodeBlockChild.SelectSuggestion)?.let { + if (it.selectedSuggestion == null) { + it.suggestions + } else { + emptyList() + } + } null -> emptyList() - } + } ?: emptyList() val isDeleteButtonEnabled = when (activeCodeBlock) { is CodeBlock.Blank -> codeBlocks.size > 1 is CodeBlock.Print -> true + is CodeBlock.Variable -> { + val activeChild = activeCodeBlock.activeChild() as? CodeBlockChild.SelectSuggestion + if (activeChild?.selectedSuggestion == null && + activeCodeBlock.children.any { it.selectedSuggestion != null } + ) { + false + } else { + true + } + } null -> false } @@ -53,8 +73,25 @@ object StepQuizCodeBlanksViewStateMapper { is CodeBlock.Print -> StepQuizCodeBlanksViewState.CodeBlockItem.Print( id = index, - isActive = codeBlock.isActive, - output = codeBlock.selectedSuggestion?.text + children = codeBlock.children.mapIndexed(::mapCodeBlockChild) + ) + is CodeBlock.Variable -> + StepQuizCodeBlanksViewState.CodeBlockItem.Variable( + id = index, + children = codeBlock.children.mapIndexed(::mapCodeBlockChild) + ) + } + + private fun mapCodeBlockChild( + index: Int, + codeBlockChild: CodeBlockChild + ): StepQuizCodeBlanksViewState.CodeBlockChildItem = + when (codeBlockChild) { + is CodeBlockChild.SelectSuggestion -> + StepQuizCodeBlanksViewState.CodeBlockChildItem( + id = index, + isActive = codeBlockChild.isActive, + value = codeBlockChild.selectedSuggestion?.text ) } } \ No newline at end of file diff --git a/shared/src/commonMain/kotlin/org/hyperskill/app/step_quiz_code_blanks/view/model/StepQuizCodeBlanksViewState.kt b/shared/src/commonMain/kotlin/org/hyperskill/app/step_quiz_code_blanks/view/model/StepQuizCodeBlanksViewState.kt index 6208c6cab..7bf0381fe 100644 --- a/shared/src/commonMain/kotlin/org/hyperskill/app/step_quiz_code_blanks/view/model/StepQuizCodeBlanksViewState.kt +++ b/shared/src/commonMain/kotlin/org/hyperskill/app/step_quiz_code_blanks/view/model/StepQuizCodeBlanksViewState.kt @@ -13,17 +13,42 @@ sealed interface StepQuizCodeBlanksViewState { sealed interface CodeBlockItem { val id: Int - val isActive: Boolean + + val children: List data class Blank( override val id: Int, - override val isActive: Boolean - ) : CodeBlockItem + val isActive: Boolean + ) : CodeBlockItem { + override val children: List = emptyList() + } data class Print( override val id: Int, - override val isActive: Boolean, + override val children: List + ) : CodeBlockItem { + val isActive: Boolean + get() = children.firstOrNull()?.isActive == true + val output: String? - ) : CodeBlockItem + get() = children.firstOrNull()?.value + } + + data class Variable( + override val id: Int, + override val children: List + ) : CodeBlockItem { + val name: CodeBlockChildItem? + get() = children.firstOrNull() + + val value: CodeBlockChildItem? + get() = children.lastOrNull() + } } + + data class CodeBlockChildItem( + val id: Int, + val isActive: Boolean, + val value: String? + ) } \ No newline at end of file diff --git a/shared/src/commonTest/kotlin/org/hyperskill/step_quiz/StepQuizTest.kt b/shared/src/commonTest/kotlin/org/hyperskill/step_quiz/StepQuizTest.kt index a94b3ce64..8d2e03e8c 100644 --- a/shared/src/commonTest/kotlin/org/hyperskill/step_quiz/StepQuizTest.kt +++ b/shared/src/commonTest/kotlin/org/hyperskill/step_quiz/StepQuizTest.kt @@ -661,9 +661,7 @@ class StepQuizTest { val step = Step.stub( id = 1, block = Block.stub( - options = Block.Options( - codeBlanksStrings = listOf("a", "b") - ) + options = Block.Options(codeBlanksEnabled = true) ) ) val attempt = Attempt.stub() @@ -716,7 +714,12 @@ class StepQuizTest { @Test fun `StepQuizCodeBlanksFeature should not be initialized when isCodeBlanksFeatureAvailable returns false`() { - val step = Step.stub(id = 1) + val step = Step.stub( + id = 1, + block = Block.stub( + options = Block.Options(codeBlanksEnabled = false) + ) + ) val attempt = Attempt.stub() val submissionState = StepQuizFeature.SubmissionState.Empty() val stepRoute = StepRoute.Learn.Step(step.id, null) diff --git a/shared/src/commonTest/kotlin/org/hyperskill/step_quiz_code_blanks/StepQuizCodeBlanksReducerTest.kt b/shared/src/commonTest/kotlin/org/hyperskill/step_quiz_code_blanks/StepQuizCodeBlanksReducerTest.kt index a1164116d..17390f36a 100644 --- a/shared/src/commonTest/kotlin/org/hyperskill/step_quiz_code_blanks/StepQuizCodeBlanksReducerTest.kt +++ b/shared/src/commonTest/kotlin/org/hyperskill/step_quiz_code_blanks/StepQuizCodeBlanksReducerTest.kt @@ -3,13 +3,16 @@ package org.hyperskill.step_quiz_code_blanks import kotlin.test.Test import kotlin.test.assertEquals import kotlin.test.assertTrue +import org.hyperskill.app.step.domain.model.Block import org.hyperskill.app.step.domain.model.Step import org.hyperskill.app.step.domain.model.StepRoute +import org.hyperskill.app.step_quiz_code_blanks.domain.analytic.StepQuizCodeBlanksClickedCodeBlockChildHyperskillAnalyticEvent import org.hyperskill.app.step_quiz_code_blanks.domain.analytic.StepQuizCodeBlanksClickedCodeBlockHyperskillAnalyticEvent import org.hyperskill.app.step_quiz_code_blanks.domain.analytic.StepQuizCodeBlanksClickedDeleteHyperskillAnalyticEvent import org.hyperskill.app.step_quiz_code_blanks.domain.analytic.StepQuizCodeBlanksClickedEnterHyperskillAnalyticEvent import org.hyperskill.app.step_quiz_code_blanks.domain.analytic.StepQuizCodeBlanksClickedSuggestionHyperskillAnalyticEvent import org.hyperskill.app.step_quiz_code_blanks.domain.model.CodeBlock +import org.hyperskill.app.step_quiz_code_blanks.domain.model.CodeBlockChild import org.hyperskill.app.step_quiz_code_blanks.domain.model.Suggestion import org.hyperskill.app.step_quiz_code_blanks.presentation.StepQuizCodeBlanksFeature import org.hyperskill.app.step_quiz_code_blanks.presentation.StepQuizCodeBlanksReducer @@ -20,7 +23,32 @@ class StepQuizCodeBlanksReducerTest { private val reducer = StepQuizCodeBlanksReducer(StepRoute.Learn.Step(1, null)) @Test - fun `Initialize should return Content state with active Blank code block`() { + fun `Initialize should return Content state with active Blank and Print and Variable suggestions`() { + val step = Step.stub( + id = 1, + block = Block.stub(options = Block.Options(codeBlanksVariables = listOf("a", "b"))) + ) + + val message = StepQuizCodeBlanksFeature.InternalMessage.Initialize(step) + val (state, actions) = reducer.reduce(StepQuizCodeBlanksFeature.State.Idle, message) + + val expectedState = StepQuizCodeBlanksFeature.State.Content( + step = step, + codeBlocks = listOf( + CodeBlock.Blank( + isActive = true, + suggestions = listOf(Suggestion.Print, Suggestion.Variable) + ) + ) + ) + + assertTrue(state is StepQuizCodeBlanksFeature.State.Content) + assertEquals(expectedState.codeBlocks, state.codeBlocks) + assertTrue(actions.isEmpty()) + } + + @Test + fun `Initialize should return Content state with active Blank and Print suggestion`() { val step = Step.stub(id = 1) val message = StepQuizCodeBlanksFeature.InternalMessage.Initialize(step) @@ -28,7 +56,7 @@ class StepQuizCodeBlanksReducerTest { val expectedState = StepQuizCodeBlanksFeature.State.Content( step = step, - codeBlocks = listOf(CodeBlock.Blank(isActive = true)) + codeBlocks = listOf(CodeBlock.Blank(isActive = true, suggestions = listOf(Suggestion.Print))) ) assertTrue(state is StepQuizCodeBlanksFeature.State.Content) @@ -38,7 +66,8 @@ class StepQuizCodeBlanksReducerTest { @Test fun `SuggestionClicked should not update state if no active code block`() { - val initialState = stubContentState(codeBlocks = listOf(CodeBlock.Blank(isActive = false))) + val initialState = + stubContentState(codeBlocks = listOf(CodeBlock.Blank(isActive = false, suggestions = emptyList()))) val message = StepQuizCodeBlanksFeature.Message.SuggestionClicked(Suggestion.Print) val (state, actions) = reducer.reduce(initialState, message) @@ -49,7 +78,8 @@ class StepQuizCodeBlanksReducerTest { @Test fun `SuggestionClicked should not update state if suggestion does not exist`() { - val initialState = stubContentState(codeBlocks = listOf(CodeBlock.Blank(isActive = true))) + val initialState = + stubContentState(codeBlocks = listOf(CodeBlock.Blank(isActive = true, suggestions = emptyList()))) val message = StepQuizCodeBlanksFeature.Message.SuggestionClicked(Suggestion.ConstantString("test")) val (state, actions) = reducer.reduce(initialState, message) @@ -70,7 +100,14 @@ class StepQuizCodeBlanksReducerTest { @Test fun `SuggestionClicked should update active Blank code block to Print if suggestion exists`() { - val initialState = stubContentState(codeBlocks = listOf(CodeBlock.Blank(isActive = true))) + val initialState = stubContentState( + codeBlocks = listOf( + CodeBlock.Blank( + isActive = true, + suggestions = listOf(Suggestion.Print) + ) + ) + ) val message = StepQuizCodeBlanksFeature.Message.SuggestionClicked(Suggestion.Print) val (state, actions) = reducer.reduce(initialState, message) @@ -78,9 +115,50 @@ class StepQuizCodeBlanksReducerTest { val expectedState = initialState.copy( codeBlocks = listOf( CodeBlock.Print( + children = listOf( + CodeBlockChild.SelectSuggestion( + isActive = true, + suggestions = initialState.codeBlanksStringsSuggestions, + selectedSuggestion = null + ) + ) + ) + ) + ) + + assertEquals(expectedState, state) + assertContainsSuggestionClickedAnalyticEvent(actions) + } + + @Test + fun `SuggestionClicked should update active Blank code block to Variable if suggestion exists`() { + val initialState = stubContentState( + codeBlocks = listOf( + CodeBlock.Blank( isActive = true, - suggestions = initialState.codeBlanksStringsSuggestions, - selectedSuggestion = null + suggestions = listOf(Suggestion.Print, Suggestion.Variable) + ) + ) + ) + + val message = StepQuizCodeBlanksFeature.Message.SuggestionClicked(Suggestion.Variable) + val (state, actions) = reducer.reduce(initialState, message) + + val expectedState = initialState.copy( + codeBlocks = listOf( + CodeBlock.Variable( + children = listOf( + CodeBlockChild.SelectSuggestion( + isActive = true, + suggestions = initialState.codeBlanksVariablesSuggestions, + selectedSuggestion = null + ), + CodeBlockChild.SelectSuggestion( + isActive = false, + suggestions = initialState.codeBlanksStringsSuggestions, + selectedSuggestion = null + ) + ) ) ) ) @@ -95,9 +173,13 @@ class StepQuizCodeBlanksReducerTest { val initialState = stubContentState( codeBlocks = listOf( CodeBlock.Print( - isActive = true, - suggestions = listOf(suggestion), - selectedSuggestion = null + children = listOf( + CodeBlockChild.SelectSuggestion( + isActive = true, + suggestions = listOf(suggestion), + selectedSuggestion = null + ) + ) ) ) ) @@ -106,7 +188,56 @@ class StepQuizCodeBlanksReducerTest { val (state, actions) = reducer.reduce(initialState, message) assertTrue(state is StepQuizCodeBlanksFeature.State.Content) - assertEquals(suggestion, (state.codeBlocks[0] as CodeBlock.Print).selectedSuggestion) + assertEquals(suggestion, (state.codeBlocks[0] as CodeBlock.Print).select?.selectedSuggestion) + assertContainsSuggestionClickedAnalyticEvent(actions) + } + + @Test + fun `SuggestionClicked should update Variable code block with selected suggestion`() { + val suggestion = Suggestion.ConstantString("suggestion") + val initialState = stubContentState( + codeBlocks = listOf( + CodeBlock.Variable( + children = listOf( + CodeBlockChild.SelectSuggestion( + isActive = true, + suggestions = listOf(suggestion), + selectedSuggestion = null + ), + CodeBlockChild.SelectSuggestion( + isActive = false, + suggestions = listOf(suggestion), + selectedSuggestion = null + ) + ) + ) + ) + ) + + val message = StepQuizCodeBlanksFeature.Message.SuggestionClicked(suggestion) + val (state, actions) = reducer.reduce(initialState, message) + + val expectedState = initialState.copy( + codeBlocks = listOf( + CodeBlock.Variable( + children = listOf( + CodeBlockChild.SelectSuggestion( + isActive = true, + suggestions = listOf(suggestion), + selectedSuggestion = suggestion + ), + CodeBlockChild.SelectSuggestion( + isActive = false, + suggestions = listOf(suggestion), + selectedSuggestion = null + ) + ) + ) + ) + ) + + assertTrue(state is StepQuizCodeBlanksFeature.State.Content) + assertEquals(expectedState.codeBlocks, state.codeBlocks) assertContainsSuggestionClickedAnalyticEvent(actions) } @@ -123,11 +254,68 @@ class StepQuizCodeBlanksReducerTest { } @Test - fun `CodeBlockClicked should update active code block`() { + fun `CodeBlockClicked should update active Print code block`() { + val initialState = stubContentState( + codeBlocks = listOf( + CodeBlock.Blank(isActive = false, suggestions = emptyList()), + CodeBlock.Print( + children = listOf( + CodeBlockChild.SelectSuggestion( + isActive = true, suggestions = emptyList(), selectedSuggestion = null + ) + ) + ) + ) + ) + + val message = StepQuizCodeBlanksFeature.Message.CodeBlockClicked( + codeBlockItem = StepQuizCodeBlanksViewState.CodeBlockItem.Blank(id = 0, isActive = false) + ) + val (state, actions) = reducer.reduce(initialState, message) + + val expectedState = initialState.copy( + codeBlocks = listOf( + CodeBlock.Blank(isActive = true, suggestions = emptyList()), + CodeBlock.Print( + children = listOf( + CodeBlockChild.SelectSuggestion( + isActive = false, suggestions = emptyList(), selectedSuggestion = null + ) + ) + ) + ) + ) + + assertEquals(expectedState, state) + assertTrue { + actions.any { + it is StepQuizCodeBlanksFeature.InternalAction.LogAnalyticEvent && + it.analyticEvent is StepQuizCodeBlanksClickedCodeBlockHyperskillAnalyticEvent + } + } + } + + @Test + fun `CodeBlockClicked should update active Variable code block`() { val initialState = stubContentState( codeBlocks = listOf( - CodeBlock.Blank(isActive = false), - CodeBlock.Print(isActive = true, suggestions = emptyList(), selectedSuggestion = null) + CodeBlock.Print( + children = listOf( + CodeBlockChild.SelectSuggestion( + isActive = false, suggestions = emptyList(), selectedSuggestion = null + ) + ) + ), + CodeBlock.Variable( + children = listOf( + CodeBlockChild.SelectSuggestion( + isActive = true, suggestions = emptyList(), selectedSuggestion = null + ), + CodeBlockChild.SelectSuggestion( + isActive = false, suggestions = emptyList(), selectedSuggestion = null + ) + ) + ) ) ) @@ -138,8 +326,23 @@ class StepQuizCodeBlanksReducerTest { val expectedState = initialState.copy( codeBlocks = listOf( - CodeBlock.Blank(isActive = true), - CodeBlock.Print(isActive = false, suggestions = emptyList(), selectedSuggestion = null) + CodeBlock.Print( + children = listOf( + CodeBlockChild.SelectSuggestion( + isActive = true, suggestions = emptyList(), selectedSuggestion = null + ) + ) + ), + CodeBlock.Variable( + children = listOf( + CodeBlockChild.SelectSuggestion( + isActive = false, suggestions = emptyList(), selectedSuggestion = null + ), + CodeBlockChild.SelectSuggestion( + isActive = false, suggestions = emptyList(), selectedSuggestion = null + ) + ) + ) ) ) @@ -152,6 +355,95 @@ class StepQuizCodeBlanksReducerTest { } } + @Test + fun `CodeBlockChildClicked should not update state if state is not Content`() { + val initialState = StepQuizCodeBlanksFeature.State.Idle + val message = StepQuizCodeBlanksFeature.Message.CodeBlockChildClicked( + codeBlockItem = StepQuizCodeBlanksViewState.CodeBlockItem.Variable(id = 0, children = emptyList()), + codeBlockChildItem = StepQuizCodeBlanksViewState.CodeBlockChildItem(id = 0, isActive = false, value = null) + ) + val (state, actions) = reducer.reduce(initialState, message) + + assertEquals(initialState, state) + assertTrue(actions.isEmpty()) + } + + @Test + fun `CodeBlockChildClicked should not update state if target code block is not found`() { + val initialState = stubContentState( + codeBlocks = listOf( + CodeBlock.Variable( + children = listOf( + CodeBlockChild.SelectSuggestion( + isActive = false, + suggestions = emptyList(), + selectedSuggestion = null + ) + ) + ) + ) + ) + + val message = StepQuizCodeBlanksFeature.Message.CodeBlockChildClicked( + codeBlockItem = StepQuizCodeBlanksViewState.CodeBlockItem.Variable(id = 1, children = emptyList()), + codeBlockChildItem = StepQuizCodeBlanksViewState.CodeBlockChildItem(id = 0, isActive = false, value = null) + ) + val (state, actions) = reducer.reduce(initialState, message) + + assertEquals(initialState, state) + assertContainsCodeBlockChildClickedAnalyticEvent(actions) + } + + @Test + fun `CodeBlockChildClicked should update state to activate the clicked child`() { + val initialState = stubContentState( + codeBlocks = listOf( + CodeBlock.Variable( + children = listOf( + CodeBlockChild.SelectSuggestion( + isActive = false, + suggestions = emptyList(), + selectedSuggestion = null + ), + CodeBlockChild.SelectSuggestion( + isActive = true, + suggestions = emptyList(), + selectedSuggestion = null + ) + ) + ) + ) + ) + + val message = StepQuizCodeBlanksFeature.Message.CodeBlockChildClicked( + codeBlockItem = StepQuizCodeBlanksViewState.CodeBlockItem.Variable(id = 0, children = emptyList()), + codeBlockChildItem = StepQuizCodeBlanksViewState.CodeBlockChildItem(id = 0, isActive = false, value = null) + ) + val (state, actions) = reducer.reduce(initialState, message) + + val expectedState = initialState.copy( + codeBlocks = listOf( + CodeBlock.Variable( + children = listOf( + CodeBlockChild.SelectSuggestion( + isActive = true, + suggestions = emptyList(), + selectedSuggestion = null + ), + CodeBlockChild.SelectSuggestion( + isActive = false, + suggestions = emptyList(), + selectedSuggestion = null + ) + ) + ) + ) + ) + + assertEquals(expectedState, state) + assertContainsCodeBlockChildClickedAnalyticEvent(actions) + } + @Test fun `DeleteButtonClicked should not update state if state is not Content`() { val initialState = StepQuizCodeBlanksFeature.State.Idle @@ -163,7 +455,8 @@ class StepQuizCodeBlanksReducerTest { @Test fun `DeleteButtonClicked should log analytic event and not update state if no active code block`() { - val initialState = stubContentState(codeBlocks = listOf(CodeBlock.Blank(isActive = false))) + val initialState = + stubContentState(codeBlocks = listOf(CodeBlock.Blank(isActive = false, suggestions = emptyList()))) val (state, actions) = reducer.reduce(initialState, StepQuizCodeBlanksFeature.Message.DeleteButtonClicked) @@ -173,7 +466,8 @@ class StepQuizCodeBlanksReducerTest { @Test fun `DeleteButtonClicked should not update state if active code block is Blank and single`() { - val initialState = stubContentState(codeBlocks = listOf(CodeBlock.Blank(isActive = true))) + val initialState = + stubContentState(codeBlocks = listOf(CodeBlock.Blank(isActive = true, suggestions = emptyList()))) val (state, actions) = reducer.reduce(initialState, StepQuizCodeBlanksFeature.Message.DeleteButtonClicked) @@ -187,9 +481,13 @@ class StepQuizCodeBlanksReducerTest { val initialState = stubContentState( codeBlocks = listOf( CodeBlock.Print( - isActive = true, - suggestions = listOf(suggestion), - selectedSuggestion = suggestion + children = listOf( + CodeBlockChild.SelectSuggestion( + isActive = true, + suggestions = listOf(suggestion), + selectedSuggestion = suggestion + ) + ) ) ) ) @@ -199,9 +497,13 @@ class StepQuizCodeBlanksReducerTest { val expectedState = initialState.copy( codeBlocks = listOf( CodeBlock.Print( - isActive = true, - suggestions = listOf(suggestion), - selectedSuggestion = null + children = listOf( + CodeBlockChild.SelectSuggestion( + isActive = true, + suggestions = listOf(suggestion), + selectedSuggestion = null + ) + ) ) ) ) @@ -215,51 +517,194 @@ class StepQuizCodeBlanksReducerTest { val initialStates = listOf( stubContentState( codeBlocks = listOf( - CodeBlock.Print(isActive = true, suggestions = emptyList(), selectedSuggestion = null), - CodeBlock.Blank(isActive = false) + CodeBlock.Print( + children = listOf( + CodeBlockChild.SelectSuggestion( + isActive = true, + suggestions = emptyList(), + selectedSuggestion = null + ) + ) + ), + CodeBlock.Blank(isActive = false, suggestions = emptyList()) ) ), stubContentState( codeBlocks = listOf( - CodeBlock.Blank(isActive = true), - CodeBlock.Blank(isActive = false) + CodeBlock.Blank(isActive = true, suggestions = emptyList()), + CodeBlock.Blank(isActive = false, suggestions = emptyList()) ) ), stubContentState( codeBlocks = listOf( - CodeBlock.Blank(isActive = true), - CodeBlock.Print(isActive = false, suggestions = emptyList(), selectedSuggestion = null) + CodeBlock.Blank(isActive = true, suggestions = emptyList()), + CodeBlock.Print( + children = listOf( + CodeBlockChild.SelectSuggestion( + isActive = false, + suggestions = emptyList(), + selectedSuggestion = null + ) + ) + ) ) ), stubContentState( codeBlocks = listOf( - CodeBlock.Print(isActive = true, suggestions = emptyList(), selectedSuggestion = null), CodeBlock.Print( - isActive = false, - suggestions = listOf(Suggestion.ConstantString("suggestion")), - selectedSuggestion = Suggestion.ConstantString("suggestion") + children = listOf( + CodeBlockChild.SelectSuggestion( + isActive = true, + suggestions = emptyList(), + selectedSuggestion = null + ) + ) + ), + CodeBlock.Print( + children = listOf( + CodeBlockChild.SelectSuggestion( + isActive = false, + suggestions = emptyList(), + selectedSuggestion = null + ) + ) + ) + ) + ), + stubContentState( + codeBlocks = listOf( + CodeBlock.Variable( + children = listOf( + CodeBlockChild.SelectSuggestion( + isActive = true, + suggestions = emptyList(), + selectedSuggestion = null + ), + CodeBlockChild.SelectSuggestion( + isActive = false, + suggestions = emptyList(), + selectedSuggestion = null + ) + ) + ), + CodeBlock.Blank(isActive = false, suggestions = emptyList()) + ) + ), + stubContentState( + codeBlocks = listOf( + CodeBlock.Blank(isActive = true, suggestions = emptyList()), + CodeBlock.Variable( + children = listOf( + CodeBlockChild.SelectSuggestion( + isActive = false, + suggestions = emptyList(), + selectedSuggestion = null + ), + CodeBlockChild.SelectSuggestion( + isActive = false, + suggestions = emptyList(), + selectedSuggestion = null + ) + ) + ) + ) + ), + stubContentState( + codeBlocks = listOf( + CodeBlock.Variable( + children = listOf( + CodeBlockChild.SelectSuggestion( + isActive = false, + suggestions = emptyList(), + selectedSuggestion = null + ), + CodeBlockChild.SelectSuggestion( + isActive = true, + suggestions = emptyList(), + selectedSuggestion = null + ) + ) + ), + CodeBlock.Variable( + children = listOf( + CodeBlockChild.SelectSuggestion( + isActive = false, + suggestions = emptyList(), + selectedSuggestion = null + ), + CodeBlockChild.SelectSuggestion( + isActive = false, + suggestions = emptyList(), + selectedSuggestion = Suggestion.ConstantString("suggestion") + ) + ) ) ) ) ) val expectedStates = listOf( - initialStates[0].copy(codeBlocks = listOf(CodeBlock.Blank(isActive = true))), - initialStates[1].copy(codeBlocks = listOf(CodeBlock.Blank(isActive = true))), + initialStates[0].copy(codeBlocks = listOf(CodeBlock.Blank(isActive = true, suggestions = emptyList()))), + initialStates[1].copy(codeBlocks = listOf(CodeBlock.Blank(isActive = true, suggestions = emptyList()))), initialStates[2].copy( codeBlocks = listOf( CodeBlock.Print( - isActive = true, - suggestions = emptyList(), - selectedSuggestion = null + children = listOf( + CodeBlockChild.SelectSuggestion( + isActive = true, + suggestions = emptyList(), + selectedSuggestion = null + ) + ) ) ) ), initialStates[3].copy( codeBlocks = listOf( CodeBlock.Print( - isActive = true, - suggestions = listOf(Suggestion.ConstantString("suggestion")), - selectedSuggestion = Suggestion.ConstantString("suggestion") + children = listOf( + CodeBlockChild.SelectSuggestion( + isActive = true, + suggestions = emptyList(), + selectedSuggestion = null + ) + ) + ) + ) + ), + initialStates[4].copy(codeBlocks = listOf(CodeBlock.Blank(isActive = true, suggestions = emptyList()))), + initialStates[5].copy( + codeBlocks = listOf( + CodeBlock.Variable( + children = listOf( + CodeBlockChild.SelectSuggestion( + isActive = true, + suggestions = emptyList(), + selectedSuggestion = null + ), + CodeBlockChild.SelectSuggestion( + isActive = false, + suggestions = emptyList(), + selectedSuggestion = null + ) + ) + ) + ) + ), + initialStates[6].copy( + codeBlocks = listOf( + CodeBlock.Variable( + children = listOf( + CodeBlockChild.SelectSuggestion( + isActive = true, + suggestions = emptyList(), + selectedSuggestion = null + ), + CodeBlockChild.SelectSuggestion( + isActive = false, + suggestions = emptyList(), + selectedSuggestion = Suggestion.ConstantString("suggestion") + ) + ) ) ) ) @@ -277,50 +722,90 @@ class StepQuizCodeBlanksReducerTest { val initialStates = listOf( stubContentState( codeBlocks = listOf( - CodeBlock.Blank(isActive = false), - CodeBlock.Print(isActive = true, suggestions = emptyList(), selectedSuggestion = null) + CodeBlock.Blank(isActive = false, suggestions = emptyList()), + CodeBlock.Print( + children = listOf( + CodeBlockChild.SelectSuggestion( + isActive = true, + suggestions = emptyList(), + selectedSuggestion = null + ) + ) + ) ) ), stubContentState( codeBlocks = listOf( - CodeBlock.Print(isActive = false, suggestions = emptyList(), selectedSuggestion = null), - CodeBlock.Blank(isActive = true) + CodeBlock.Print( + children = listOf( + CodeBlockChild.SelectSuggestion( + isActive = false, + suggestions = emptyList(), + selectedSuggestion = null + ) + ) + ), + CodeBlock.Blank(isActive = true, suggestions = emptyList()) ) ), stubContentState( codeBlocks = listOf( CodeBlock.Print( - isActive = false, - suggestions = listOf(Suggestion.ConstantString("suggestion")), - selectedSuggestion = Suggestion.ConstantString("suggestion") + children = listOf( + CodeBlockChild.SelectSuggestion( + isActive = false, + suggestions = listOf(Suggestion.ConstantString("suggestion")), + selectedSuggestion = Suggestion.ConstantString("suggestion") + ) + ) ), - CodeBlock.Print(isActive = true, suggestions = emptyList(), selectedSuggestion = null) + CodeBlock.Print( + children = listOf( + CodeBlockChild.SelectSuggestion( + isActive = true, + suggestions = emptyList(), + selectedSuggestion = null + ) + ) + ) ) ), stubContentState( codeBlocks = listOf( - CodeBlock.Blank(isActive = false), - CodeBlock.Blank(isActive = true) + CodeBlock.Blank(isActive = false, suggestions = emptyList()), + CodeBlock.Blank(isActive = true, suggestions = emptyList()) ) ) ) val expectedStates = listOf( - initialStates[0].copy(codeBlocks = listOf(CodeBlock.Blank(isActive = true))), + initialStates[0].copy(codeBlocks = listOf(CodeBlock.Blank(isActive = true, suggestions = emptyList()))), initialStates[1].copy( codeBlocks = listOf( - CodeBlock.Print(isActive = true, suggestions = emptyList(), selectedSuggestion = null), + CodeBlock.Print( + children = listOf( + CodeBlockChild.SelectSuggestion( + isActive = true, + suggestions = emptyList(), + selectedSuggestion = null + ) + ) + ) ) ), initialStates[2].copy( codeBlocks = listOf( CodeBlock.Print( - isActive = true, - suggestions = listOf(Suggestion.ConstantString("suggestion")), - selectedSuggestion = Suggestion.ConstantString("suggestion") + children = listOf( + CodeBlockChild.SelectSuggestion( + isActive = true, + suggestions = listOf(Suggestion.ConstantString("suggestion")), + selectedSuggestion = Suggestion.ConstantString("suggestion") + ) + ) ) ) ), - initialStates[0].copy(codeBlocks = listOf(CodeBlock.Blank(isActive = true))), + initialStates[0].copy(codeBlocks = listOf(CodeBlock.Blank(isActive = true, suggestions = emptyList()))), ) initialStates.zip(expectedStates).forEach { (initialState, expectedState) -> @@ -335,9 +820,13 @@ class StepQuizCodeBlanksReducerTest { val initialState = stubContentState( codeBlocks = listOf( CodeBlock.Print( - isActive = false, - suggestions = listOf(Suggestion.ConstantString("suggestion")), - selectedSuggestion = null + children = listOf( + CodeBlockChild.SelectSuggestion( + isActive = false, + suggestions = emptyList(), + selectedSuggestion = null + ) + ) ) ) ) @@ -353,9 +842,44 @@ class StepQuizCodeBlanksReducerTest { val initialState = stubContentState( codeBlocks = listOf( CodeBlock.Print( - isActive = true, - suggestions = emptyList(), - selectedSuggestion = null + children = listOf( + CodeBlockChild.SelectSuggestion( + isActive = true, + suggestions = emptyList(), + selectedSuggestion = null + ) + ) + ) + ) + ) + + val (state, actions) = reducer.reduce(initialState, StepQuizCodeBlanksFeature.Message.DeleteButtonClicked) + + val expectedState = initialState.copy( + codeBlocks = listOf(CodeBlock.Blank(isActive = true, suggestions = listOf(Suggestion.Print))) + ) + + assertEquals(expectedState, state) + assertContainsDeleteButtonClickedAnalyticEvent(actions) + } + + @Test + fun `DeleteButtonClicked should replace single Variable code block with Blank`() { + val initialState = stubContentState( + codeBlocks = listOf( + CodeBlock.Variable( + children = listOf( + CodeBlockChild.SelectSuggestion( + isActive = true, + suggestions = emptyList(), + selectedSuggestion = null + ), + CodeBlockChild.SelectSuggestion( + isActive = false, + suggestions = emptyList(), + selectedSuggestion = null + ) + ) ) ) ) @@ -363,7 +887,7 @@ class StepQuizCodeBlanksReducerTest { val (state, actions) = reducer.reduce(initialState, StepQuizCodeBlanksFeature.Message.DeleteButtonClicked) val expectedState = initialState.copy( - codeBlocks = listOf(CodeBlock.Blank(isActive = true)) + codeBlocks = listOf(CodeBlock.Blank(isActive = true, suggestions = listOf(Suggestion.Print))) ) assertEquals(expectedState, state) @@ -381,7 +905,8 @@ class StepQuizCodeBlanksReducerTest { @Test fun `EnterButtonClicked should log analytic event and not update state if no active code block`() { - val initialState = stubContentState(codeBlocks = listOf(CodeBlock.Blank(isActive = false))) + val initialState = + stubContentState(codeBlocks = listOf(CodeBlock.Blank(isActive = false, suggestions = emptyList()))) val (state, actions) = reducer.reduce(initialState, StepQuizCodeBlanksFeature.Message.EnterButtonClicked) @@ -391,14 +916,15 @@ class StepQuizCodeBlanksReducerTest { @Test fun `EnterButtonClicked should log analytic event and add new active Blank block if active code block exists`() { - val initialState = stubContentState(codeBlocks = listOf(CodeBlock.Blank(isActive = true))) + val initialState = + stubContentState(codeBlocks = listOf(CodeBlock.Blank(isActive = true, suggestions = emptyList()))) val (state, actions) = reducer.reduce(initialState, StepQuizCodeBlanksFeature.Message.EnterButtonClicked) val expectedState = initialState.copy( codeBlocks = listOf( - CodeBlock.Blank(isActive = false), - CodeBlock.Blank(isActive = true) + CodeBlock.Blank(isActive = false, suggestions = emptyList()), + CodeBlock.Blank(isActive = true, suggestions = listOf(Suggestion.Print)) ) ) @@ -410,11 +936,15 @@ class StepQuizCodeBlanksReducerTest { fun `EnterButtonClicked should add new active Blank block after active code block`() { val initialState = stubContentState( codeBlocks = listOf( - CodeBlock.Blank(isActive = true), + CodeBlock.Blank(isActive = true, suggestions = emptyList()), CodeBlock.Print( - isActive = false, - suggestions = listOf(Suggestion.ConstantString("suggestion")), - selectedSuggestion = null + children = listOf( + CodeBlockChild.SelectSuggestion( + isActive = false, + suggestions = emptyList(), + selectedSuggestion = null + ) + ) ) ) ) @@ -423,12 +953,16 @@ class StepQuizCodeBlanksReducerTest { val expectedState = initialState.copy( codeBlocks = listOf( - CodeBlock.Blank(isActive = false), - CodeBlock.Blank(isActive = true), + CodeBlock.Blank(isActive = false, suggestions = emptyList()), + CodeBlock.Blank(isActive = true, suggestions = listOf(Suggestion.Print)), CodeBlock.Print( - isActive = false, - suggestions = listOf(Suggestion.ConstantString("suggestion")), - selectedSuggestion = null + children = listOf( + CodeBlockChild.SelectSuggestion( + isActive = false, + suggestions = emptyList(), + selectedSuggestion = null + ) + ) ) ) ) @@ -446,6 +980,15 @@ class StepQuizCodeBlanksReducerTest { } } + private fun assertContainsCodeBlockChildClickedAnalyticEvent(actions: Set) { + assertTrue { + actions.any { + it is StepQuizCodeBlanksFeature.InternalAction.LogAnalyticEvent && + it.analyticEvent is StepQuizCodeBlanksClickedCodeBlockChildHyperskillAnalyticEvent + } + } + } + private fun assertContainsDeleteButtonClickedAnalyticEvent(actions: Set) { assertTrue { actions.any { diff --git a/shared/src/commonTest/kotlin/org/hyperskill/step_quiz_code_blanks/StepQuizCodeBlanksStateExtensionsTest.kt b/shared/src/commonTest/kotlin/org/hyperskill/step_quiz_code_blanks/StepQuizCodeBlanksStateExtensionsTest.kt index 1dbf4b570..26e636298 100644 --- a/shared/src/commonTest/kotlin/org/hyperskill/step_quiz_code_blanks/StepQuizCodeBlanksStateExtensionsTest.kt +++ b/shared/src/commonTest/kotlin/org/hyperskill/step_quiz_code_blanks/StepQuizCodeBlanksStateExtensionsTest.kt @@ -2,14 +2,18 @@ package org.hyperskill.step_quiz_code_blanks import kotlin.test.Test import kotlin.test.assertEquals +import kotlin.test.assertFalse import kotlin.test.assertNull +import kotlin.test.assertTrue import org.hyperskill.app.step.domain.model.Block import org.hyperskill.app.step.domain.model.Step import org.hyperskill.app.step_quiz_code_blanks.domain.model.CodeBlock +import org.hyperskill.app.step_quiz_code_blanks.domain.model.CodeBlockChild import org.hyperskill.app.step_quiz_code_blanks.domain.model.Suggestion import org.hyperskill.app.step_quiz_code_blanks.presentation.StepQuizCodeBlanksFeature import org.hyperskill.app.step_quiz_code_blanks.presentation.activeCodeBlockIndex import org.hyperskill.app.step_quiz_code_blanks.presentation.createReply +import org.hyperskill.app.step_quiz_code_blanks.presentation.isVariableSuggestionsAvailable import org.hyperskill.app.submissions.domain.model.Reply import org.hyperskill.step.domain.model.stub @@ -18,11 +22,15 @@ class StepQuizCodeBlanksStateExtensionsTest { fun `activeCodeBlockIndex should return null if no active code block`() { val state = stubContentState( codeBlocks = listOf( - CodeBlock.Blank(isActive = false), + CodeBlock.Blank(isActive = false, suggestions = emptyList()), CodeBlock.Print( - isActive = false, - suggestions = emptyList(), - selectedSuggestion = null + children = listOf( + CodeBlockChild.SelectSuggestion( + isActive = false, + suggestions = emptyList(), + selectedSuggestion = null + ) + ) ) ) ) @@ -33,11 +41,15 @@ class StepQuizCodeBlanksStateExtensionsTest { fun `activeCodeBlockIndex should return index of the active code block`() { val state = stubContentState( codeBlocks = listOf( - CodeBlock.Blank(isActive = false), + CodeBlock.Blank(isActive = false, suggestions = emptyList()), CodeBlock.Print( - isActive = true, - suggestions = emptyList(), - selectedSuggestion = null + children = listOf( + CodeBlockChild.SelectSuggestion( + isActive = true, + suggestions = emptyList(), + selectedSuggestion = null + ) + ) ) ) ) @@ -48,11 +60,15 @@ class StepQuizCodeBlanksStateExtensionsTest { fun `createReply should return Reply with code from code blocks and language from step options`() { val codeBlocks = listOf( CodeBlock.Print( - isActive = false, - suggestions = emptyList(), - selectedSuggestion = Suggestion.ConstantString("\"test\"") + children = listOf( + CodeBlockChild.SelectSuggestion( + isActive = false, + suggestions = emptyList(), + selectedSuggestion = Suggestion.ConstantString("\"test\"") + ) + ) ), - CodeBlock.Blank(isActive = true) + CodeBlock.Blank(isActive = true, suggestions = emptyList()) ) val step = Step.stub(id = 1).copy( block = Block.stub( @@ -71,6 +87,80 @@ class StepQuizCodeBlanksStateExtensionsTest { assertEquals(expectedReply, state.createReply()) } + @Test + fun `createReply should return correct Reply with Variable code block`() { + val codeBlocks = listOf( + CodeBlock.Variable( + children = listOf( + CodeBlockChild.SelectSuggestion( + isActive = false, + suggestions = emptyList(), + selectedSuggestion = Suggestion.ConstantString("a") + ), + CodeBlockChild.SelectSuggestion( + isActive = false, + suggestions = emptyList(), + selectedSuggestion = Suggestion.ConstantString("1") + ) + ) + ), + CodeBlock.Print( + children = listOf( + CodeBlockChild.SelectSuggestion( + isActive = true, + suggestions = emptyList(), + selectedSuggestion = Suggestion.ConstantString("a") + ) + ) + ), + ) + val step = Step.stub(id = 1).copy( + block = Block.stub( + options = Block.Options( + codeTemplates = mapOf("python3" to "# put your python code here") + ) + ) + ) + val state = stubContentState( + step = step, + codeBlocks = codeBlocks + ) + + val expectedReply = Reply.code(code = "a = 1\nprint(a)", language = "python3") + + assertEquals(expectedReply, state.createReply()) + } + + @Test + fun `isVariableSuggestionsAvailable should return true if variable suggestions are available`() { + val step = Step.stub( + id = 1, + block = Block.stub(options = Block.Options(codeBlanksVariables = listOf("a", "b"))) + ) + val state = stubContentState( + step = step, + codeBlocks = emptyList() + ) + + assertTrue(state.isVariableSuggestionsAvailable) + } + + @Test + fun `isVariableSuggestionsAvailable should return false if variable suggestions are not available`() { + listOf(null, emptyList()).forEach { codeBlanksVariables -> + val step = Step.stub( + id = 1, + block = Block.stub(options = Block.Options(codeBlanksVariables = codeBlanksVariables)) + ) + val state = stubContentState( + step = step, + codeBlocks = emptyList() + ) + + assertFalse(state.isVariableSuggestionsAvailable) + } + } + private fun stubContentState( step: Step = Step.stub(id = 1), codeBlocks: List diff --git a/shared/src/commonTest/kotlin/org/hyperskill/step_quiz_code_blanks/StepQuizCodeBlanksViewStateMapperTest.kt b/shared/src/commonTest/kotlin/org/hyperskill/step_quiz_code_blanks/StepQuizCodeBlanksViewStateMapperTest.kt index f90896def..5d966b712 100644 --- a/shared/src/commonTest/kotlin/org/hyperskill/step_quiz_code_blanks/StepQuizCodeBlanksViewStateMapperTest.kt +++ b/shared/src/commonTest/kotlin/org/hyperskill/step_quiz_code_blanks/StepQuizCodeBlanksViewStateMapperTest.kt @@ -4,6 +4,7 @@ import kotlin.test.Test import kotlin.test.assertEquals import org.hyperskill.app.step.domain.model.Step import org.hyperskill.app.step_quiz_code_blanks.domain.model.CodeBlock +import org.hyperskill.app.step_quiz_code_blanks.domain.model.CodeBlockChild import org.hyperskill.app.step_quiz_code_blanks.domain.model.Suggestion import org.hyperskill.app.step_quiz_code_blanks.presentation.StepQuizCodeBlanksFeature import org.hyperskill.app.step_quiz_code_blanks.view.mapper.StepQuizCodeBlanksViewStateMapper @@ -21,7 +22,7 @@ class StepQuizCodeBlanksViewStateMapperTest { @Test fun `Content with print suggestion and disabled delete button when active code block is Blank`() { val state = stubState( - codeBlocks = listOf(CodeBlock.Blank(isActive = true)) + codeBlocks = listOf(CodeBlock.Blank(isActive = true, suggestions = listOf(Suggestion.Print))) ) val expectedViewState = StepQuizCodeBlanksViewState.Content( codeBlocks = listOf(StepQuizCodeBlanksViewState.CodeBlockItem.Blank(id = 0, isActive = true)), @@ -43,15 +44,28 @@ class StepQuizCodeBlanksViewStateMapperTest { val state = stubState( codeBlocks = listOf( CodeBlock.Print( - isActive = true, - selectedSuggestion = null, - suggestions = suggestions + children = listOf( + CodeBlockChild.SelectSuggestion( + isActive = true, + suggestions = suggestions, + selectedSuggestion = null + ) + ) ) ) ) val expectedViewState = StepQuizCodeBlanksViewState.Content( codeBlocks = listOf( - StepQuizCodeBlanksViewState.CodeBlockItem.Print(id = 0, isActive = true, output = null) + StepQuizCodeBlanksViewState.CodeBlockItem.Print( + id = 0, + children = listOf( + StepQuizCodeBlanksViewState.CodeBlockChildItem( + id = 0, + isActive = true, + value = null + ) + ) + ) ), suggestions = suggestions, isDeleteButtonEnabled = true @@ -71,19 +85,28 @@ class StepQuizCodeBlanksViewStateMapperTest { val state = stubState( codeBlocks = listOf( CodeBlock.Print( - isActive = false, - selectedSuggestion = printSuggestions[0], - suggestions = printSuggestions + children = listOf( + CodeBlockChild.SelectSuggestion( + isActive = false, + suggestions = printSuggestions, + selectedSuggestion = printSuggestions[0] + ) + ) ), - CodeBlock.Blank(isActive = true) + CodeBlock.Blank(isActive = true, suggestions = listOf(Suggestion.Print)) ) ) val expectedViewState = StepQuizCodeBlanksViewState.Content( codeBlocks = listOf( StepQuizCodeBlanksViewState.CodeBlockItem.Print( id = 0, - isActive = false, - output = printSuggestions[0].text + children = listOf( + StepQuizCodeBlanksViewState.CodeBlockChildItem( + id = 0, + isActive = false, + value = printSuggestions[0].text + ) + ) ), StepQuizCodeBlanksViewState.CodeBlockItem.Blank(id = 1, isActive = true) ), @@ -105,14 +128,22 @@ class StepQuizCodeBlanksViewStateMapperTest { val state = stubState( codeBlocks = listOf( CodeBlock.Print( - isActive = false, - selectedSuggestion = printSuggestions[0], - suggestions = printSuggestions + children = listOf( + CodeBlockChild.SelectSuggestion( + isActive = false, + suggestions = printSuggestions, + selectedSuggestion = printSuggestions[0] + ) + ) ), CodeBlock.Print( - isActive = true, - selectedSuggestion = null, - suggestions = printSuggestions + children = listOf( + CodeBlockChild.SelectSuggestion( + isActive = true, + suggestions = printSuggestions, + selectedSuggestion = null + ) + ) ) ) ) @@ -120,10 +151,24 @@ class StepQuizCodeBlanksViewStateMapperTest { codeBlocks = listOf( StepQuizCodeBlanksViewState.CodeBlockItem.Print( id = 0, - isActive = false, - output = printSuggestions[0].text + children = listOf( + StepQuizCodeBlanksViewState.CodeBlockChildItem( + id = 0, + isActive = false, + value = printSuggestions[0].text + ) + ) ), - StepQuizCodeBlanksViewState.CodeBlockItem.Print(id = 1, isActive = true, output = null) + StepQuizCodeBlanksViewState.CodeBlockItem.Print( + id = 1, + children = listOf( + StepQuizCodeBlanksViewState.CodeBlockChildItem( + id = 0, + isActive = true, + value = null + ) + ) + ) ), suggestions = printSuggestions, isDeleteButtonEnabled = true @@ -134,6 +179,360 @@ class StepQuizCodeBlanksViewStateMapperTest { assertEquals(expectedViewState, actualViewState) } + @Test + fun `Content with active Variable and disabled delete button`() { + val suggestions = listOf( + Suggestion.ConstantString("1"), + Suggestion.ConstantString("2") + ) + val state = stubState( + codeBlocks = listOf( + CodeBlock.Variable( + children = listOf( + CodeBlockChild.SelectSuggestion( + isActive = true, + suggestions = suggestions, + selectedSuggestion = null + ), + CodeBlockChild.SelectSuggestion( + isActive = false, + suggestions = suggestions, + selectedSuggestion = suggestions[0] + ) + ) + ) + ) + ) + val expectedViewState = StepQuizCodeBlanksViewState.Content( + codeBlocks = listOf( + StepQuizCodeBlanksViewState.CodeBlockItem.Variable( + id = 0, + children = listOf( + StepQuizCodeBlanksViewState.CodeBlockChildItem( + id = 0, + isActive = true, + value = null + ), + StepQuizCodeBlanksViewState.CodeBlockChildItem( + id = 1, + isActive = false, + value = suggestions[0].text + ) + ) + ) + ), + suggestions = suggestions, + isDeleteButtonEnabled = false + ) + + val actualViewState = StepQuizCodeBlanksViewStateMapper.map(state) + + assertEquals(expectedViewState, actualViewState) + } + + @Test + fun `Content with active not filled Variable and enabled delete button`() { + val suggestions = listOf( + Suggestion.ConstantString("1"), + Suggestion.ConstantString("2") + ) + val state = stubState( + codeBlocks = listOf( + CodeBlock.Variable( + children = listOf( + CodeBlockChild.SelectSuggestion( + isActive = true, + suggestions = suggestions, + selectedSuggestion = null + ), + CodeBlockChild.SelectSuggestion( + isActive = false, + suggestions = suggestions, + selectedSuggestion = null + ) + ) + ) + ) + ) + val expectedViewState = StepQuizCodeBlanksViewState.Content( + codeBlocks = listOf( + StepQuizCodeBlanksViewState.CodeBlockItem.Variable( + id = 0, + children = listOf( + StepQuizCodeBlanksViewState.CodeBlockChildItem( + id = 0, + isActive = true, + value = null + ), + StepQuizCodeBlanksViewState.CodeBlockChildItem( + id = 1, + isActive = false, + value = null + ) + ) + ) + ), + suggestions = suggestions, + isDeleteButtonEnabled = true + ) + + val actualViewState = StepQuizCodeBlanksViewStateMapper.map(state) + + assertEquals(expectedViewState, actualViewState) + } + + @Test + fun `Content with active filled Variable and enabled delete button`() { + val suggestions = listOf( + Suggestion.ConstantString("1"), + Suggestion.ConstantString("2") + ) + val state = stubState( + codeBlocks = listOf( + CodeBlock.Variable( + children = listOf( + CodeBlockChild.SelectSuggestion( + isActive = true, + suggestions = suggestions, + selectedSuggestion = suggestions[0] + ), + CodeBlockChild.SelectSuggestion( + isActive = false, + suggestions = suggestions, + selectedSuggestion = suggestions[1] + ) + ) + ) + ) + ) + val expectedViewState = StepQuizCodeBlanksViewState.Content( + codeBlocks = listOf( + StepQuizCodeBlanksViewState.CodeBlockItem.Variable( + id = 0, + children = listOf( + StepQuizCodeBlanksViewState.CodeBlockChildItem( + id = 0, + isActive = true, + value = suggestions[0].text + ), + StepQuizCodeBlanksViewState.CodeBlockChildItem( + id = 1, + isActive = false, + value = suggestions[1].text + ) + ) + ) + ), + suggestions = emptyList(), + isDeleteButtonEnabled = true + ) + + val actualViewState = StepQuizCodeBlanksViewStateMapper.map(state) + + assertEquals(expectedViewState, actualViewState) + } + + @Test + fun `Content with suggestions when active code block is Blank`() { + val suggestions = listOf(Suggestion.Print, Suggestion.Variable) + val state = stubState( + codeBlocks = listOf(CodeBlock.Blank(isActive = true, suggestions = suggestions)) + ) + val expectedViewState = StepQuizCodeBlanksViewState.Content( + codeBlocks = listOf(StepQuizCodeBlanksViewState.CodeBlockItem.Blank(id = 0, isActive = true)), + suggestions = suggestions, + isDeleteButtonEnabled = false + ) + + val actualViewState = StepQuizCodeBlanksViewStateMapper.map(state) + + assertEquals(expectedViewState, actualViewState) + } + + @Test + fun `Content with suggestions when active code block is Print and no selected suggestion`() { + val suggestions = listOf( + Suggestion.ConstantString("1"), + Suggestion.ConstantString("2") + ) + val state = stubState( + codeBlocks = listOf( + CodeBlock.Print( + children = listOf( + CodeBlockChild.SelectSuggestion( + isActive = true, + suggestions = suggestions, + selectedSuggestion = null + ) + ) + ) + ) + ) + val expectedViewState = StepQuizCodeBlanksViewState.Content( + codeBlocks = listOf( + StepQuizCodeBlanksViewState.CodeBlockItem.Print( + id = 0, + children = listOf( + StepQuizCodeBlanksViewState.CodeBlockChildItem( + id = 0, + isActive = true, + value = null + ) + ) + ) + ), + suggestions = suggestions, + isDeleteButtonEnabled = true + ) + + val actualViewState = StepQuizCodeBlanksViewStateMapper.map(state) + + assertEquals(expectedViewState, actualViewState) + } + + @Test + fun `Content with no suggestions when active code block is Print and has selected suggestion`() { + val suggestions = listOf( + Suggestion.ConstantString("1"), + Suggestion.ConstantString("2") + ) + val state = stubState( + codeBlocks = listOf( + CodeBlock.Print( + children = listOf( + CodeBlockChild.SelectSuggestion( + isActive = true, + suggestions = suggestions, + selectedSuggestion = suggestions[0] + ) + ) + ) + ) + ) + val expectedViewState = StepQuizCodeBlanksViewState.Content( + codeBlocks = listOf( + StepQuizCodeBlanksViewState.CodeBlockItem.Print( + id = 0, + children = listOf( + StepQuizCodeBlanksViewState.CodeBlockChildItem( + id = 0, + isActive = true, + value = suggestions[0].text + ) + ) + ) + ), + suggestions = emptyList(), + isDeleteButtonEnabled = true + ) + + val actualViewState = StepQuizCodeBlanksViewStateMapper.map(state) + + assertEquals(expectedViewState, actualViewState) + } + + @Test + fun `Content with suggestions when active code block is Variable and active child has no selected suggestion`() { + val suggestions = listOf( + Suggestion.ConstantString("1"), + Suggestion.ConstantString("2") + ) + val state = stubState( + codeBlocks = listOf( + CodeBlock.Variable( + children = listOf( + CodeBlockChild.SelectSuggestion( + isActive = true, + suggestions = suggestions, + selectedSuggestion = null + ), + CodeBlockChild.SelectSuggestion( + isActive = false, + suggestions = suggestions, + selectedSuggestion = null + ) + ) + ) + ) + ) + val expectedViewState = StepQuizCodeBlanksViewState.Content( + codeBlocks = listOf( + StepQuizCodeBlanksViewState.CodeBlockItem.Variable( + id = 0, + children = listOf( + StepQuizCodeBlanksViewState.CodeBlockChildItem( + id = 0, + isActive = true, + value = null + ), + StepQuizCodeBlanksViewState.CodeBlockChildItem( + id = 1, + isActive = false, + value = null + ) + ) + ) + ), + suggestions = suggestions, + isDeleteButtonEnabled = true + ) + + val actualViewState = StepQuizCodeBlanksViewStateMapper.map(state) + + assertEquals(expectedViewState, actualViewState) + } + + @Test + fun `Content with no suggestions when active code block is Variable and active child has selected suggestion`() { + val suggestions = listOf( + Suggestion.ConstantString("1"), + Suggestion.ConstantString("2") + ) + val state = stubState( + codeBlocks = listOf( + CodeBlock.Variable( + children = listOf( + CodeBlockChild.SelectSuggestion( + isActive = true, + suggestions = suggestions, + selectedSuggestion = suggestions[0] + ), + CodeBlockChild.SelectSuggestion( + isActive = false, + suggestions = suggestions, + selectedSuggestion = suggestions[1] + ) + ) + ) + ) + ) + val expectedViewState = StepQuizCodeBlanksViewState.Content( + codeBlocks = listOf( + StepQuizCodeBlanksViewState.CodeBlockItem.Variable( + id = 0, + children = listOf( + StepQuizCodeBlanksViewState.CodeBlockChildItem( + id = 0, + isActive = true, + value = suggestions[0].text + ), + StepQuizCodeBlanksViewState.CodeBlockChildItem( + id = 1, + isActive = false, + value = suggestions[1].text + ) + ) + ) + ), + suggestions = emptyList(), + isDeleteButtonEnabled = true + ) + + val actualViewState = StepQuizCodeBlanksViewStateMapper.map(state) + + assertEquals(expectedViewState, actualViewState) + } + private fun stubState(codeBlocks: List): StepQuizCodeBlanksFeature.State.Content = StepQuizCodeBlanksFeature.State.Content( step = Step.stub(id = 0), From 518c5f9e7c7507a0251619d7e04b90bc90172229 Mon Sep 17 00:00:00 2001 From: github-actions Date: Thu, 8 Aug 2024 08:49:06 +0000 Subject: [PATCH 12/16] Bump build number --- gradle/app.versions.toml | 2 +- .../NotificationServiceExtension/Info.plist | 2 +- .../iosHyperskillApp.xcodeproj/project.pbxproj | 16 ++++++++-------- iosHyperskillApp/iosHyperskillApp/Info.plist | 2 +- .../iosHyperskillAppTests/Info.plist | 2 +- .../iosHyperskillAppUITests/Info.plist | 2 +- 6 files changed, 13 insertions(+), 13 deletions(-) diff --git a/gradle/app.versions.toml b/gradle/app.versions.toml index bad0e4456..a6731c56c 100644 --- a/gradle/app.versions.toml +++ b/gradle/app.versions.toml @@ -3,4 +3,4 @@ minSdk = '24' targetSdk = '34' compileSdk = '34' versionName = '1.67' -versionCode = '518' \ No newline at end of file +versionCode = '519' \ No newline at end of file diff --git a/iosHyperskillApp/NotificationServiceExtension/Info.plist b/iosHyperskillApp/NotificationServiceExtension/Info.plist index ebce726d4..025c01cb6 100644 --- a/iosHyperskillApp/NotificationServiceExtension/Info.plist +++ b/iosHyperskillApp/NotificationServiceExtension/Info.plist @@ -9,7 +9,7 @@ CFBundleIdentifier $(PRODUCT_BUNDLE_IDENTIFIER) CFBundleVersion - 545 + 546 CFBundleShortVersionString 1.67 CFBundlePackageType diff --git a/iosHyperskillApp/iosHyperskillApp.xcodeproj/project.pbxproj b/iosHyperskillApp/iosHyperskillApp.xcodeproj/project.pbxproj index fa86cd727..219a0da21 100644 --- a/iosHyperskillApp/iosHyperskillApp.xcodeproj/project.pbxproj +++ b/iosHyperskillApp/iosHyperskillApp.xcodeproj/project.pbxproj @@ -5657,7 +5657,7 @@ buildSettings = { CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 545; + CURRENT_PROJECT_VERSION = 546; DEBUG_INFORMATION_FORMAT = dwarf; DEVELOPMENT_TEAM = UJ4KC2QN7B; GENERATE_INFOPLIST_FILE = NO; @@ -5678,7 +5678,7 @@ buildSettings = { CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 545; + CURRENT_PROJECT_VERSION = 546; DEVELOPMENT_TEAM = UJ4KC2QN7B; GENERATE_INFOPLIST_FILE = NO; INFOPLIST_FILE = iosHyperskillAppUITests/Info.plist; @@ -5699,7 +5699,7 @@ BUNDLE_LOADER = "$(TEST_HOST)"; CLANG_CXX_LANGUAGE_STANDARD = "gnu++17"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 545; + CURRENT_PROJECT_VERSION = 546; DEBUG_INFORMATION_FORMAT = dwarf; DEVELOPMENT_TEAM = UJ4KC2QN7B; INFOPLIST_FILE = iosHyperskillAppTests/Info.plist; @@ -5720,7 +5720,7 @@ BUNDLE_LOADER = "$(TEST_HOST)"; CLANG_CXX_LANGUAGE_STANDARD = "gnu++17"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 545; + CURRENT_PROJECT_VERSION = 546; DEVELOPMENT_TEAM = UJ4KC2QN7B; INFOPLIST_FILE = iosHyperskillAppTests/Info.plist; IPHONEOS_DEPLOYMENT_TARGET = 14.0; @@ -5741,7 +5741,7 @@ CODE_SIGN_ENTITLEMENTS = NotificationServiceExtension/NotificationServiceExtension.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 545; + CURRENT_PROJECT_VERSION = 546; DEBUG_INFORMATION_FORMAT = dwarf; DEVELOPMENT_TEAM = UJ4KC2QN7B; INFOPLIST_FILE = NotificationServiceExtension/Info.plist; @@ -5770,7 +5770,7 @@ CODE_SIGN_ENTITLEMENTS = NotificationServiceExtension/NotificationServiceExtension.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 545; + CURRENT_PROJECT_VERSION = 546; DEVELOPMENT_TEAM = UJ4KC2QN7B; INFOPLIST_FILE = NotificationServiceExtension/Info.plist; IPHONEOS_DEPLOYMENT_TARGET = 14.0; @@ -5916,7 +5916,7 @@ CODE_SIGN_ENTITLEMENTS = iosHyperskillApp/iosHyperskillApp.entitlements; CODE_SIGN_IDENTITY = "iPhone Developer"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 545; + CURRENT_PROJECT_VERSION = 546; DEVELOPMENT_ASSET_PATHS = "\"iosHyperskillApp/Preview Content\""; DEVELOPMENT_TEAM = UJ4KC2QN7B; ENABLE_PREVIEWS = YES; @@ -5952,7 +5952,7 @@ CODE_SIGN_ENTITLEMENTS = iosHyperskillApp/iosHyperskillApp.entitlements; CODE_SIGN_IDENTITY = "iPhone Developer"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 545; + CURRENT_PROJECT_VERSION = 546; DEVELOPMENT_ASSET_PATHS = "\"iosHyperskillApp/Preview Content\""; DEVELOPMENT_TEAM = UJ4KC2QN7B; ENABLE_PREVIEWS = YES; diff --git a/iosHyperskillApp/iosHyperskillApp/Info.plist b/iosHyperskillApp/iosHyperskillApp/Info.plist index c0da1e1be..8914e0260 100644 --- a/iosHyperskillApp/iosHyperskillApp/Info.plist +++ b/iosHyperskillApp/iosHyperskillApp/Info.plist @@ -34,7 +34,7 @@ CFBundleVersion - 545 + 546 FirebaseAppDelegateProxyEnabled FirebaseMessagingAutoInitEnabled diff --git a/iosHyperskillApp/iosHyperskillAppTests/Info.plist b/iosHyperskillApp/iosHyperskillAppTests/Info.plist index 2bca603e6..dff454a34 100644 --- a/iosHyperskillApp/iosHyperskillAppTests/Info.plist +++ b/iosHyperskillApp/iosHyperskillAppTests/Info.plist @@ -15,6 +15,6 @@ CFBundleShortVersionString 1.67 CFBundleVersion - 545 + 546 diff --git a/iosHyperskillApp/iosHyperskillAppUITests/Info.plist b/iosHyperskillApp/iosHyperskillAppUITests/Info.plist index eee0dd380..83c9babc2 100644 --- a/iosHyperskillApp/iosHyperskillAppUITests/Info.plist +++ b/iosHyperskillApp/iosHyperskillAppUITests/Info.plist @@ -15,6 +15,6 @@ CFBundleShortVersionString 1.67 CFBundleVersion - 545 + 546 From 443f3a17b53392f60f988e0b82a3fde18e4b2414 Mon Sep 17 00:00:00 2001 From: Ivan Magda Date: Fri, 9 Aug 2024 09:45:50 +0700 Subject: [PATCH 13/16] Shared: Code blanks variable block type improvements (#1151) ^ALTAPPS-1318 --- .../delegate/MatchingStepQuizFormDelegate.kt | 8 ++- .../presentation/CommentsScreenReducer.kt | 4 +- .../app/core/utils/CollectionExtensions.kt | 5 +- .../domain/model/CodeBlock.kt | 4 +- .../presentation/StepQuizCodeBlanksReducer.kt | 13 +++++ .../StepQuizCodeBlanksStateExtensions.kt | 14 ++--- .../presentation/FillBlanksItemMapper.kt | 5 +- .../presentation/StudyPlanWidgetReducer.kt | 4 +- .../StepQuizCodeBlanksReducerTest.kt | 53 ++++++++++++++++++- .../StepQuizCodeBlanksStateExtensionsTest.kt | 16 +++++- 10 files changed, 102 insertions(+), 24 deletions(-) diff --git a/androidHyperskillApp/src/main/java/org/hyperskill/app/android/step_quiz_matching/view/delegate/MatchingStepQuizFormDelegate.kt b/androidHyperskillApp/src/main/java/org/hyperskill/app/android/step_quiz_matching/view/delegate/MatchingStepQuizFormDelegate.kt index e4ea515a1..11dda95f4 100644 --- a/androidHyperskillApp/src/main/java/org/hyperskill/app/android/step_quiz_matching/view/delegate/MatchingStepQuizFormDelegate.kt +++ b/androidHyperskillApp/src/main/java/org/hyperskill/app/android/step_quiz_matching/view/delegate/MatchingStepQuizFormDelegate.kt @@ -9,6 +9,7 @@ import org.hyperskill.app.android.step_quiz_table.view.adapter.TableSelectionIte import org.hyperskill.app.android.step_quiz_table.view.fragment.TableColumnSelectionBottomSheetDialogFragment import org.hyperskill.app.android.step_quiz_table.view.model.TableChoiceItem import org.hyperskill.app.android.step_quiz_table.view.model.TableSelectionItem +import org.hyperskill.app.core.utils.indexOfFirstOrNull import org.hyperskill.app.step_quiz.presentation.StepQuizFeature import org.hyperskill.app.step_quiz.presentation.StepQuizResolver import org.hyperskill.app.step_quiz.presentation.submission @@ -79,13 +80,10 @@ class MatchingStepQuizFormDelegate( ): List { val selectedAnswerIndex = answers - .indexOfFirst { it.answer } - .takeIf { it != -1 } + .indexOfFirstOrNull { it.answer } ?: return rows val rowIndexToSwap = - rows - .indexOfFirst { it.tableChoices[selectedAnswerIndex].answer } - .takeIf { it != -1 } + rows.indexOfFirstOrNull { it.tableChoices[selectedAnswerIndex].answer } return rows.mutate { val currentRow = get(currentRowIndex) if (rowIndexToSwap != null) { diff --git a/shared/src/commonMain/kotlin/org/hyperskill/app/comments/screen/presentation/CommentsScreenReducer.kt b/shared/src/commonMain/kotlin/org/hyperskill/app/comments/screen/presentation/CommentsScreenReducer.kt index c76c35137..7e2e1c0c1 100644 --- a/shared/src/commonMain/kotlin/org/hyperskill/app/comments/screen/presentation/CommentsScreenReducer.kt +++ b/shared/src/commonMain/kotlin/org/hyperskill/app/comments/screen/presentation/CommentsScreenReducer.kt @@ -11,6 +11,7 @@ import org.hyperskill.app.comments.screen.presentation.CommentsScreenFeature.Int import org.hyperskill.app.comments.screen.presentation.CommentsScreenFeature.InternalMessage import org.hyperskill.app.comments.screen.presentation.CommentsScreenFeature.Message import org.hyperskill.app.comments.screen.presentation.CommentsScreenFeature.State +import org.hyperskill.app.core.utils.indexOfFirstOrNull import org.hyperskill.app.core.utils.mutate import org.hyperskill.app.discussions.domain.model.getRepliesIds import org.hyperskill.app.discussions.remote.model.toPagedList @@ -186,8 +187,7 @@ internal class CommentsScreenReducer : StateReducer { val comment = discussionsState.commentsMap[message.commentId] ?: return null val reactionIndex = comment.reactions - .indexOfFirst { it.reactionType == message.reactionType } - .takeIf { it != -1 } + .indexOfFirstOrNull { it.reactionType == message.reactionType } ?: return null val reaction = comment.reactions[reactionIndex] val isSettingReaction = !reaction.isSet diff --git a/shared/src/commonMain/kotlin/org/hyperskill/app/core/utils/CollectionExtensions.kt b/shared/src/commonMain/kotlin/org/hyperskill/app/core/utils/CollectionExtensions.kt index 310a0ef2e..ea9263d64 100644 --- a/shared/src/commonMain/kotlin/org/hyperskill/app/core/utils/CollectionExtensions.kt +++ b/shared/src/commonMain/kotlin/org/hyperskill/app/core/utils/CollectionExtensions.kt @@ -1,4 +1,7 @@ package org.hyperskill.app.core.utils fun Map.mutate(block: MutableMap.() -> Unit): Map = - this.toMutableMap().apply(block) \ No newline at end of file + this.toMutableMap().apply(block) + +inline fun List.indexOfFirstOrNull(predicate: (T) -> Boolean): Int? = + indexOfFirst(predicate).takeIf { it >= 0 } \ No newline at end of file diff --git a/shared/src/commonMain/kotlin/org/hyperskill/app/step_quiz_code_blanks/domain/model/CodeBlock.kt b/shared/src/commonMain/kotlin/org/hyperskill/app/step_quiz_code_blanks/domain/model/CodeBlock.kt index 5dcfec10c..4199c716a 100644 --- a/shared/src/commonMain/kotlin/org/hyperskill/app/step_quiz_code_blanks/domain/model/CodeBlock.kt +++ b/shared/src/commonMain/kotlin/org/hyperskill/app/step_quiz_code_blanks/domain/model/CodeBlock.kt @@ -1,5 +1,7 @@ package org.hyperskill.app.step_quiz_code_blanks.domain.model +import org.hyperskill.app.core.utils.indexOfFirstOrNull + sealed class CodeBlock { internal abstract val isActive: Boolean @@ -15,7 +17,7 @@ sealed class CodeBlock { children.firstOrNull { it.isActive } internal fun activeChildIndex(): Int? = - children.indexOfFirst { it.isActive }.takeIf { it != -1 } + children.indexOfFirstOrNull { it.isActive } internal data class Blank( override val isActive: Boolean, diff --git a/shared/src/commonMain/kotlin/org/hyperskill/app/step_quiz_code_blanks/presentation/StepQuizCodeBlanksReducer.kt b/shared/src/commonMain/kotlin/org/hyperskill/app/step_quiz_code_blanks/presentation/StepQuizCodeBlanksReducer.kt index e3032c8f7..164cb7bd5 100644 --- a/shared/src/commonMain/kotlin/org/hyperskill/app/step_quiz_code_blanks/presentation/StepQuizCodeBlanksReducer.kt +++ b/shared/src/commonMain/kotlin/org/hyperskill/app/step_quiz_code_blanks/presentation/StepQuizCodeBlanksReducer.kt @@ -1,5 +1,6 @@ package org.hyperskill.app.step_quiz_code_blanks.presentation +import org.hyperskill.app.core.utils.indexOfFirstOrNull import org.hyperskill.app.step.domain.model.StepRoute import org.hyperskill.app.step_quiz_code_blanks.domain.analytic.StepQuizCodeBlanksClickedCodeBlockChildHyperskillAnalyticEvent import org.hyperskill.app.step_quiz_code_blanks.domain.analytic.StepQuizCodeBlanksClickedCodeBlockHyperskillAnalyticEvent @@ -121,6 +122,18 @@ class StepQuizCodeBlanksReducer( selectedSuggestion = message.suggestion as? Suggestion.ConstantString ) ) + + val nextUnselectedChildIndex = this.indexOfFirstOrNull { it.selectedSuggestion == null } + if (nextUnselectedChildIndex != null) { + set( + nextUnselectedChildIndex, + this[nextUnselectedChildIndex].copy(isActive = true) + ) + set( + activeChildIndex, + this[activeChildIndex].copy(isActive = false) + ) + } } ) } ?: activeCodeBlock diff --git a/shared/src/commonMain/kotlin/org/hyperskill/app/step_quiz_code_blanks/presentation/StepQuizCodeBlanksStateExtensions.kt b/shared/src/commonMain/kotlin/org/hyperskill/app/step_quiz_code_blanks/presentation/StepQuizCodeBlanksStateExtensions.kt index 84d450a18..46710ef6d 100644 --- a/shared/src/commonMain/kotlin/org/hyperskill/app/step_quiz_code_blanks/presentation/StepQuizCodeBlanksStateExtensions.kt +++ b/shared/src/commonMain/kotlin/org/hyperskill/app/step_quiz_code_blanks/presentation/StepQuizCodeBlanksStateExtensions.kt @@ -1,13 +1,12 @@ package org.hyperskill.app.step_quiz_code_blanks.presentation +import org.hyperskill.app.core.utils.indexOfFirstOrNull import org.hyperskill.app.submissions.domain.model.Reply internal fun StepQuizCodeBlanksFeature.State.Content.activeCodeBlockIndex(): Int? = - codeBlocks - .indexOfFirst { codeBlock -> - codeBlock.isActive || codeBlock.children.any { it.isActive } - } - .takeIf { it != -1 } + codeBlocks.indexOfFirstOrNull { codeBlock -> + codeBlock.isActive || codeBlock.children.any { it.isActive } + } internal val StepQuizCodeBlanksFeature.State.isVariableSuggestionsAvailable: Boolean get() = (this as? StepQuizCodeBlanksFeature.State.Content)?.step?.let { @@ -16,6 +15,9 @@ internal val StepQuizCodeBlanksFeature.State.isVariableSuggestionsAvailable: Boo fun StepQuizCodeBlanksFeature.State.Content.createReply(): Reply = Reply.code( - code = codeBlocks.joinToString(separator = "\n") { it.toReplyString() }, + code = buildString { + append("# solved with code blanks\n") + append(codeBlocks.joinToString(separator = "\n") { it.toReplyString() }) + }, language = step.block.options.codeTemplates?.keys?.firstOrNull() ) \ No newline at end of file diff --git a/shared/src/commonMain/kotlin/org/hyperskill/app/step_quiz_fill_blanks/presentation/FillBlanksItemMapper.kt b/shared/src/commonMain/kotlin/org/hyperskill/app/step_quiz_fill_blanks/presentation/FillBlanksItemMapper.kt index 5d42331ea..087a46b5a 100644 --- a/shared/src/commonMain/kotlin/org/hyperskill/app/step_quiz_fill_blanks/presentation/FillBlanksItemMapper.kt +++ b/shared/src/commonMain/kotlin/org/hyperskill/app/step_quiz_fill_blanks/presentation/FillBlanksItemMapper.kt @@ -1,6 +1,7 @@ package org.hyperskill.app.step_quiz_fill_blanks.presentation import org.hyperskill.app.core.utils.DotMatchesAllRegexOption +import org.hyperskill.app.core.utils.indexOfFirstOrNull import org.hyperskill.app.step_quiz.domain.model.attempts.Attempt import org.hyperskill.app.step_quiz.domain.model.attempts.Component import org.hyperskill.app.step_quiz.domain.model.attempts.Dataset @@ -191,9 +192,7 @@ class FillBlanksItemMapper(private val mode: FillBlanksMode) { optionIndex: Int ): Int? { val replyOption = replyBlanks?.getOrNull(optionIndex) ?: return null - return blankOptions - .indexOfFirst { it.originalText == replyOption } - .takeIf { it != -1 } + return blankOptions.indexOfFirstOrNull { it.originalText == replyOption } } private fun parseLanguage(langClass: String): String? = diff --git a/shared/src/commonMain/kotlin/org/hyperskill/app/study_plan/widget/presentation/StudyPlanWidgetReducer.kt b/shared/src/commonMain/kotlin/org/hyperskill/app/study_plan/widget/presentation/StudyPlanWidgetReducer.kt index 2ea1eac61..6ce719407 100644 --- a/shared/src/commonMain/kotlin/org/hyperskill/app/study_plan/widget/presentation/StudyPlanWidgetReducer.kt +++ b/shared/src/commonMain/kotlin/org/hyperskill/app/study_plan/widget/presentation/StudyPlanWidgetReducer.kt @@ -2,6 +2,7 @@ package org.hyperskill.app.study_plan.widget.presentation import kotlin.math.min import org.hyperskill.app.analytic.domain.model.hyperskill.HyperskillAnalyticRoute +import org.hyperskill.app.core.utils.indexOfFirstOrNull import org.hyperskill.app.core.utils.mutate import org.hyperskill.app.learning_activities.domain.model.LearningActivity import org.hyperskill.app.learning_activities.presentation.mapper.LearningActivityTargetViewActionMapper @@ -203,10 +204,9 @@ class StudyPlanWidgetReducer : StateReducer { */ val visibleSections = sections.filter { it.isVisible } val currentSectionIndex = visibleSections - .indexOfFirst { studyPlanSection -> + .indexOfFirstOrNull { studyPlanSection -> studyPlanSection.activities.intersect(learningActivitiesIds).isNotEmpty() } - .takeIf { it != -1 } ?: return emptyList() return visibleSections.slice(from = currentSectionIndex) } diff --git a/shared/src/commonTest/kotlin/org/hyperskill/step_quiz_code_blanks/StepQuizCodeBlanksReducerTest.kt b/shared/src/commonTest/kotlin/org/hyperskill/step_quiz_code_blanks/StepQuizCodeBlanksReducerTest.kt index 17390f36a..29e6ae3d1 100644 --- a/shared/src/commonTest/kotlin/org/hyperskill/step_quiz_code_blanks/StepQuizCodeBlanksReducerTest.kt +++ b/shared/src/commonTest/kotlin/org/hyperskill/step_quiz_code_blanks/StepQuizCodeBlanksReducerTest.kt @@ -193,7 +193,7 @@ class StepQuizCodeBlanksReducerTest { } @Test - fun `SuggestionClicked should update Variable code block with selected suggestion`() { + fun `SuggestionClicked should update Variable code block with selected suggestion for name`() { val suggestion = Suggestion.ConstantString("suggestion") val initialState = stubContentState( codeBlocks = listOf( @@ -222,14 +222,63 @@ class StepQuizCodeBlanksReducerTest { CodeBlock.Variable( children = listOf( CodeBlockChild.SelectSuggestion( - isActive = true, + isActive = false, suggestions = listOf(suggestion), selectedSuggestion = suggestion ), + CodeBlockChild.SelectSuggestion( + isActive = true, + suggestions = listOf(suggestion), + selectedSuggestion = null + ) + ) + ) + ) + ) + + assertTrue(state is StepQuizCodeBlanksFeature.State.Content) + assertEquals(expectedState.codeBlocks, state.codeBlocks) + assertContainsSuggestionClickedAnalyticEvent(actions) + } + + @Test + fun `SuggestionClicked should update Variable code block with selected suggestion for value`() { + val suggestion = Suggestion.ConstantString("suggestion") + val initialState = stubContentState( + codeBlocks = listOf( + CodeBlock.Variable( + children = listOf( CodeBlockChild.SelectSuggestion( isActive = false, suggestions = listOf(suggestion), selectedSuggestion = null + ), + CodeBlockChild.SelectSuggestion( + isActive = true, + suggestions = listOf(suggestion), + selectedSuggestion = null + ) + ) + ) + ) + ) + + val message = StepQuizCodeBlanksFeature.Message.SuggestionClicked(suggestion) + val (state, actions) = reducer.reduce(initialState, message) + + val expectedState = initialState.copy( + codeBlocks = listOf( + CodeBlock.Variable( + children = listOf( + CodeBlockChild.SelectSuggestion( + isActive = true, + suggestions = listOf(suggestion), + selectedSuggestion = null + ), + CodeBlockChild.SelectSuggestion( + isActive = false, + suggestions = listOf(suggestion), + selectedSuggestion = suggestion ) ) ) diff --git a/shared/src/commonTest/kotlin/org/hyperskill/step_quiz_code_blanks/StepQuizCodeBlanksStateExtensionsTest.kt b/shared/src/commonTest/kotlin/org/hyperskill/step_quiz_code_blanks/StepQuizCodeBlanksStateExtensionsTest.kt index 26e636298..cf5654c54 100644 --- a/shared/src/commonTest/kotlin/org/hyperskill/step_quiz_code_blanks/StepQuizCodeBlanksStateExtensionsTest.kt +++ b/shared/src/commonTest/kotlin/org/hyperskill/step_quiz_code_blanks/StepQuizCodeBlanksStateExtensionsTest.kt @@ -82,7 +82,13 @@ class StepQuizCodeBlanksStateExtensionsTest { codeBlocks = codeBlocks ) - val expectedReply = Reply.code(code = "print(\"test\")\n", language = "python3") + val expectedReply = Reply.code( + code = buildString { + append("# solved with code blanks\n") + append("print(\"test\")\n") + }, + language = "python3" + ) assertEquals(expectedReply, state.createReply()) } @@ -126,7 +132,13 @@ class StepQuizCodeBlanksStateExtensionsTest { codeBlocks = codeBlocks ) - val expectedReply = Reply.code(code = "a = 1\nprint(a)", language = "python3") + val expectedReply = Reply.code( + code = buildString { + append("# solved with code blanks\n") + append("a = 1\nprint(a)") + }, + language = "python3" + ) assertEquals(expectedReply, state.createReply()) } From 603b6b5ff395f182bb7591468418b7e5f3c23799 Mon Sep 17 00:00:00 2001 From: github-actions Date: Fri, 9 Aug 2024 02:46:27 +0000 Subject: [PATCH 14/16] Bump build number --- gradle/app.versions.toml | 2 +- .../NotificationServiceExtension/Info.plist | 2 +- .../iosHyperskillApp.xcodeproj/project.pbxproj | 16 ++++++++-------- iosHyperskillApp/iosHyperskillApp/Info.plist | 2 +- .../iosHyperskillAppTests/Info.plist | 2 +- .../iosHyperskillAppUITests/Info.plist | 2 +- 6 files changed, 13 insertions(+), 13 deletions(-) diff --git a/gradle/app.versions.toml b/gradle/app.versions.toml index a6731c56c..7a64f6ba8 100644 --- a/gradle/app.versions.toml +++ b/gradle/app.versions.toml @@ -3,4 +3,4 @@ minSdk = '24' targetSdk = '34' compileSdk = '34' versionName = '1.67' -versionCode = '519' \ No newline at end of file +versionCode = '520' \ No newline at end of file diff --git a/iosHyperskillApp/NotificationServiceExtension/Info.plist b/iosHyperskillApp/NotificationServiceExtension/Info.plist index 025c01cb6..57dc1f405 100644 --- a/iosHyperskillApp/NotificationServiceExtension/Info.plist +++ b/iosHyperskillApp/NotificationServiceExtension/Info.plist @@ -9,7 +9,7 @@ CFBundleIdentifier $(PRODUCT_BUNDLE_IDENTIFIER) CFBundleVersion - 546 + 547 CFBundleShortVersionString 1.67 CFBundlePackageType diff --git a/iosHyperskillApp/iosHyperskillApp.xcodeproj/project.pbxproj b/iosHyperskillApp/iosHyperskillApp.xcodeproj/project.pbxproj index 219a0da21..f913df379 100644 --- a/iosHyperskillApp/iosHyperskillApp.xcodeproj/project.pbxproj +++ b/iosHyperskillApp/iosHyperskillApp.xcodeproj/project.pbxproj @@ -5657,7 +5657,7 @@ buildSettings = { CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 546; + CURRENT_PROJECT_VERSION = 547; DEBUG_INFORMATION_FORMAT = dwarf; DEVELOPMENT_TEAM = UJ4KC2QN7B; GENERATE_INFOPLIST_FILE = NO; @@ -5678,7 +5678,7 @@ buildSettings = { CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 546; + CURRENT_PROJECT_VERSION = 547; DEVELOPMENT_TEAM = UJ4KC2QN7B; GENERATE_INFOPLIST_FILE = NO; INFOPLIST_FILE = iosHyperskillAppUITests/Info.plist; @@ -5699,7 +5699,7 @@ BUNDLE_LOADER = "$(TEST_HOST)"; CLANG_CXX_LANGUAGE_STANDARD = "gnu++17"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 546; + CURRENT_PROJECT_VERSION = 547; DEBUG_INFORMATION_FORMAT = dwarf; DEVELOPMENT_TEAM = UJ4KC2QN7B; INFOPLIST_FILE = iosHyperskillAppTests/Info.plist; @@ -5720,7 +5720,7 @@ BUNDLE_LOADER = "$(TEST_HOST)"; CLANG_CXX_LANGUAGE_STANDARD = "gnu++17"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 546; + CURRENT_PROJECT_VERSION = 547; DEVELOPMENT_TEAM = UJ4KC2QN7B; INFOPLIST_FILE = iosHyperskillAppTests/Info.plist; IPHONEOS_DEPLOYMENT_TARGET = 14.0; @@ -5741,7 +5741,7 @@ CODE_SIGN_ENTITLEMENTS = NotificationServiceExtension/NotificationServiceExtension.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 546; + CURRENT_PROJECT_VERSION = 547; DEBUG_INFORMATION_FORMAT = dwarf; DEVELOPMENT_TEAM = UJ4KC2QN7B; INFOPLIST_FILE = NotificationServiceExtension/Info.plist; @@ -5770,7 +5770,7 @@ CODE_SIGN_ENTITLEMENTS = NotificationServiceExtension/NotificationServiceExtension.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 546; + CURRENT_PROJECT_VERSION = 547; DEVELOPMENT_TEAM = UJ4KC2QN7B; INFOPLIST_FILE = NotificationServiceExtension/Info.plist; IPHONEOS_DEPLOYMENT_TARGET = 14.0; @@ -5916,7 +5916,7 @@ CODE_SIGN_ENTITLEMENTS = iosHyperskillApp/iosHyperskillApp.entitlements; CODE_SIGN_IDENTITY = "iPhone Developer"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 546; + CURRENT_PROJECT_VERSION = 547; DEVELOPMENT_ASSET_PATHS = "\"iosHyperskillApp/Preview Content\""; DEVELOPMENT_TEAM = UJ4KC2QN7B; ENABLE_PREVIEWS = YES; @@ -5952,7 +5952,7 @@ CODE_SIGN_ENTITLEMENTS = iosHyperskillApp/iosHyperskillApp.entitlements; CODE_SIGN_IDENTITY = "iPhone Developer"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 546; + CURRENT_PROJECT_VERSION = 547; DEVELOPMENT_ASSET_PATHS = "\"iosHyperskillApp/Preview Content\""; DEVELOPMENT_TEAM = UJ4KC2QN7B; ENABLE_PREVIEWS = YES; diff --git a/iosHyperskillApp/iosHyperskillApp/Info.plist b/iosHyperskillApp/iosHyperskillApp/Info.plist index 8914e0260..e65fc78e8 100644 --- a/iosHyperskillApp/iosHyperskillApp/Info.plist +++ b/iosHyperskillApp/iosHyperskillApp/Info.plist @@ -34,7 +34,7 @@ CFBundleVersion - 546 + 547 FirebaseAppDelegateProxyEnabled FirebaseMessagingAutoInitEnabled diff --git a/iosHyperskillApp/iosHyperskillAppTests/Info.plist b/iosHyperskillApp/iosHyperskillAppTests/Info.plist index dff454a34..9d9721c31 100644 --- a/iosHyperskillApp/iosHyperskillAppTests/Info.plist +++ b/iosHyperskillApp/iosHyperskillAppTests/Info.plist @@ -15,6 +15,6 @@ CFBundleShortVersionString 1.67 CFBundleVersion - 546 + 547 diff --git a/iosHyperskillApp/iosHyperskillAppUITests/Info.plist b/iosHyperskillApp/iosHyperskillAppUITests/Info.plist index 83c9babc2..6cae99c7f 100644 --- a/iosHyperskillApp/iosHyperskillAppUITests/Info.plist +++ b/iosHyperskillApp/iosHyperskillAppUITests/Info.plist @@ -15,6 +15,6 @@ CFBundleShortVersionString 1.67 CFBundleVersion - 546 + 547 From 059974acbe35f25708409dabb3c7d81449ee8c75 Mon Sep 17 00:00:00 2001 From: Ivan Magda Date: Mon, 12 Aug 2024 10:13:11 +0700 Subject: [PATCH 15/16] iOS: Remove "My" prefix from the app name in App Store (#1152) ^ALTAPPS-1325 --- .../fastlane/metadata/android/en-US/title.txt | 2 +- androidHyperskillApp/src/main/AndroidManifest.xml | 2 +- .../view/ui/fragment/AuthCredentialsFragment.kt | 5 +---- .../auth/view/ui/fragment/AuthSocialFragment.kt | 10 ++-------- .../src/main/res/layout/fragment_welcome.xml | 2 +- iosHyperskillApp/fastlane/metadata/en-US/name.txt | 2 +- .../fastlane/metadata/en-US/promotional_text.txt | 2 +- iosHyperskillApp/iosHyperskillApp/Info.plist | 2 +- .../Sources/Models/Constants/Strings.swift | 7 +++---- .../app/core/domain/platform/Platform.kt | 5 ----- .../app/core/domain/platform/Platform.kt | 4 ---- .../domain/model/FeedbackEmailDataBuilder.kt | 2 +- .../ProfileSettingsActionDispatcher.kt | 2 +- .../injection/RequestReviewModalComponentImpl.kt | 1 - .../injection/RequestReviewModalFeatureBuilder.kt | 3 --- .../mapper/RequestReviewModalViewStateMapper.kt | 5 +---- .../view/WelcomeQuestionnaireViewStateMapper.kt | 7 +------ .../src/commonMain/moko-resources/base/strings.xml | 14 ++++++-------- .../interactor/ApplicationShortcutsInteractor.kt | 2 +- .../app/core/domain/platform/Platform.kt | 4 ---- 20 files changed, 23 insertions(+), 60 deletions(-) diff --git a/androidHyperskillApp/fastlane/metadata/android/en-US/title.txt b/androidHyperskillApp/fastlane/metadata/android/en-US/title.txt index 41dd3eb97..87c746993 100644 --- a/androidHyperskillApp/fastlane/metadata/android/en-US/title.txt +++ b/androidHyperskillApp/fastlane/metadata/android/en-US/title.txt @@ -1 +1 @@ -My Hyperskill \ No newline at end of file +Hyperskill: Learn to Code \ No newline at end of file diff --git a/androidHyperskillApp/src/main/AndroidManifest.xml b/androidHyperskillApp/src/main/AndroidManifest.xml index a9f294cb7..4a7e598e7 100644 --- a/androidHyperskillApp/src/main/AndroidManifest.xml +++ b/androidHyperskillApp/src/main/AndroidManifest.xml @@ -5,7 +5,7 @@ if (!isAdded) return@addKeyboardVisibilityListener viewBinding.signInHyperskillLogoShapeableImageView.isVisible = !isVisible diff --git a/androidHyperskillApp/src/main/java/org/hyperskill/app/android/auth/view/ui/fragment/AuthSocialFragment.kt b/androidHyperskillApp/src/main/java/org/hyperskill/app/android/auth/view/ui/fragment/AuthSocialFragment.kt index 28b0654c5..6829eb7cd 100644 --- a/androidHyperskillApp/src/main/java/org/hyperskill/app/android/auth/view/ui/fragment/AuthSocialFragment.kt +++ b/androidHyperskillApp/src/main/java/org/hyperskill/app/android/auth/view/ui/fragment/AuthSocialFragment.kt @@ -145,15 +145,9 @@ class AuthSocialFragment : viewBinding.signInToTextView.text = if (isInSignUpMode) { - getString( - SharedRes.string.auth_sign_up_title, - getString(SharedRes.string.android_app_name) - ) + getString(SharedRes.string.auth_sign_up_title) } else { - getString( - SharedRes.string.auth_log_in_title, - getString(SharedRes.string.android_app_name) - ) + getString(SharedRes.string.auth_log_in_title) } authMaterialCardViewsAdapter.items = listOf( diff --git a/androidHyperskillApp/src/main/res/layout/fragment_welcome.xml b/androidHyperskillApp/src/main/res/layout/fragment_welcome.xml index 1533f8a89..287283e0d 100644 --- a/androidHyperskillApp/src/main/res/layout/fragment_welcome.xml +++ b/androidHyperskillApp/src/main/res/layout/fragment_welcome.xml @@ -28,7 +28,7 @@ app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toTopOf="parent" app:layout_constraintWidth_max="@dimen/auth_button_max_width" - android:text="@string/android_onboarding_title" /> + android:text="@string/onboarding_title" /> CFBundleDevelopmentRegion $(DEVELOPMENT_LANGUAGE) CFBundleDisplayName - My Hyperskill + Hyperskill CFBundleExecutable $(EXECUTABLE_NAME) CFBundleIdentifier diff --git a/iosHyperskillApp/iosHyperskillApp/Sources/Models/Constants/Strings.swift b/iosHyperskillApp/iosHyperskillApp/Sources/Models/Constants/Strings.swift index 40f2a2b11..8a456e9dc 100644 --- a/iosHyperskillApp/iosHyperskillApp/Sources/Models/Constants/Strings.swift +++ b/iosHyperskillApp/iosHyperskillApp/Sources/Models/Constants/Strings.swift @@ -7,7 +7,6 @@ enum Strings { // MARK: - Common - enum Common { - static let appName = sharedStrings.ios_app_name.localized() static let connectionError = sharedStrings.connection_error.localized() static let done = sharedStrings.done.localized() static let yes = sharedStrings.yes.localized() @@ -53,8 +52,8 @@ enum Strings { // MARK: Social enum Social { - static let logInTitle = sharedStrings.auth_log_in_title.format(args_: [Common.appName]).localized() - static let signUpTitle = sharedStrings.auth_sign_up_title.format(args_: [Common.appName]).localized() + static let logInTitle = sharedStrings.auth_log_in_title.localized() + static let signUpTitle = sharedStrings.auth_sign_up_title.localized() static let jetBrainsAccount = sharedStrings.auth_jetbrains_account_text.localized() static let googleAccount = sharedStrings.auth_google_account_text.localized() static let gitHubAccount = sharedStrings.auth_github_account_text.localized() @@ -484,7 +483,7 @@ enum Strings { // MARK: - Welcome - enum Welcome { - static let title = sharedStrings.ios_onboarding_title.localized() + static let title = sharedStrings.onboarding_title.localized() static let text = sharedStrings.onboarding_text.localized() static let primaryButton = sharedStrings.onboarding_primary_button_text.localized() static let secondaryButton = sharedStrings.onboarding_secondary_button_text.localized() diff --git a/shared/src/androidMain/kotlin/org/hyperskill/app/core/domain/platform/Platform.kt b/shared/src/androidMain/kotlin/org/hyperskill/app/core/domain/platform/Platform.kt index 51c02f56a..16a2ccdec 100644 --- a/shared/src/androidMain/kotlin/org/hyperskill/app/core/domain/platform/Platform.kt +++ b/shared/src/androidMain/kotlin/org/hyperskill/app/core/domain/platform/Platform.kt @@ -1,8 +1,5 @@ package org.hyperskill.app.core.domain.platform -import dev.icerock.moko.resources.StringResource -import org.hyperskill.app.SharedResources - actual class Platform actual constructor() { actual val platformType: PlatformType = PlatformType.ANDROID actual val platformDescription: String = "Android ${android.os.Build.VERSION.SDK_INT}" @@ -10,6 +7,4 @@ actual class Platform actual constructor() { actual val analyticName: String = "android" actual val feedbackName: String = "Android" - - actual val appNameResource: StringResource = SharedResources.strings.android_app_name } \ No newline at end of file diff --git a/shared/src/commonMain/kotlin/org/hyperskill/app/core/domain/platform/Platform.kt b/shared/src/commonMain/kotlin/org/hyperskill/app/core/domain/platform/Platform.kt index 74cd19ac1..7809c38a3 100644 --- a/shared/src/commonMain/kotlin/org/hyperskill/app/core/domain/platform/Platform.kt +++ b/shared/src/commonMain/kotlin/org/hyperskill/app/core/domain/platform/Platform.kt @@ -1,7 +1,5 @@ package org.hyperskill.app.core.domain.platform -import dev.icerock.moko.resources.StringResource - expect class Platform() { val platformType: PlatformType val platformDescription: String @@ -9,6 +7,4 @@ expect class Platform() { val analyticName: String val feedbackName: String - - val appNameResource: StringResource } \ No newline at end of file diff --git a/shared/src/commonMain/kotlin/org/hyperskill/app/profile_settings/domain/model/FeedbackEmailDataBuilder.kt b/shared/src/commonMain/kotlin/org/hyperskill/app/profile_settings/domain/model/FeedbackEmailDataBuilder.kt index 43ce9c772..a7b80bb1f 100644 --- a/shared/src/commonMain/kotlin/org/hyperskill/app/profile_settings/domain/model/FeedbackEmailDataBuilder.kt +++ b/shared/src/commonMain/kotlin/org/hyperskill/app/profile_settings/domain/model/FeedbackEmailDataBuilder.kt @@ -2,7 +2,7 @@ package org.hyperskill.app.profile_settings.domain.model import org.hyperskill.app.core.domain.platform.Platform -object FeedbackEmailDataBuilder { +internal object FeedbackEmailDataBuilder { fun build( supportEmail: String, applicationName: String, diff --git a/shared/src/commonMain/kotlin/org/hyperskill/app/profile_settings/presentation/ProfileSettingsActionDispatcher.kt b/shared/src/commonMain/kotlin/org/hyperskill/app/profile_settings/presentation/ProfileSettingsActionDispatcher.kt index 21549c644..6fc77b4d5 100644 --- a/shared/src/commonMain/kotlin/org/hyperskill/app/profile_settings/presentation/ProfileSettingsActionDispatcher.kt +++ b/shared/src/commonMain/kotlin/org/hyperskill/app/profile_settings/presentation/ProfileSettingsActionDispatcher.kt @@ -66,7 +66,7 @@ internal class ProfileSettingsActionDispatcher( val feedbackEmailData = FeedbackEmailDataBuilder.build( supportEmail = resourceProvider.getString(strings.settings_send_feedback_support_email), - applicationName = resourceProvider.getString(platform.appNameResource), + applicationName = resourceProvider.getString(strings.app_name), platform = platform, userId = currentProfile?.id, applicationVersion = userAgentInfo.versionCode diff --git a/shared/src/commonMain/kotlin/org/hyperskill/app/request_review/modal/injection/RequestReviewModalComponentImpl.kt b/shared/src/commonMain/kotlin/org/hyperskill/app/request_review/modal/injection/RequestReviewModalComponentImpl.kt index 41e5a0b73..3e269ddbe 100644 --- a/shared/src/commonMain/kotlin/org/hyperskill/app/request_review/modal/injection/RequestReviewModalComponentImpl.kt +++ b/shared/src/commonMain/kotlin/org/hyperskill/app/request_review/modal/injection/RequestReviewModalComponentImpl.kt @@ -17,7 +17,6 @@ internal class RequestReviewModalComponentImpl( analyticInteractor = appGraph.analyticComponent.analyticInteractor, logger = appGraph.loggerComponent.logger, buildVariant = appGraph.commonComponent.buildKonfig.buildVariant, - platform = appGraph.commonComponent.platform, resourceProvider = appGraph.commonComponent.resourceProvider ) } \ No newline at end of file diff --git a/shared/src/commonMain/kotlin/org/hyperskill/app/request_review/modal/injection/RequestReviewModalFeatureBuilder.kt b/shared/src/commonMain/kotlin/org/hyperskill/app/request_review/modal/injection/RequestReviewModalFeatureBuilder.kt index 45666823d..001f84e07 100644 --- a/shared/src/commonMain/kotlin/org/hyperskill/app/request_review/modal/injection/RequestReviewModalFeatureBuilder.kt +++ b/shared/src/commonMain/kotlin/org/hyperskill/app/request_review/modal/injection/RequestReviewModalFeatureBuilder.kt @@ -4,7 +4,6 @@ import co.touchlab.kermit.Logger import org.hyperskill.app.analytic.domain.interactor.AnalyticInteractor import org.hyperskill.app.analytic.presentation.wrapWithAnalyticLogger import org.hyperskill.app.core.domain.BuildVariant -import org.hyperskill.app.core.domain.platform.Platform import org.hyperskill.app.core.presentation.transformState import org.hyperskill.app.core.view.mapper.ResourceProvider import org.hyperskill.app.logging.presentation.wrapWithLogger @@ -27,7 +26,6 @@ internal object RequestReviewModalFeatureBuilder { analyticInteractor: AnalyticInteractor, logger: Logger, buildVariant: BuildVariant, - platform: Platform, resourceProvider: ResourceProvider ): Feature { val requestReviewModalReducer = RequestReviewModalReducer( @@ -36,7 +34,6 @@ internal object RequestReviewModalFeatureBuilder { ).wrapWithLogger(buildVariant, logger, LOG_TAG) val requestReviewModalViewStateMapper = RequestReviewModalViewStateMapper( - platform = platform, resourceProvider = resourceProvider ) diff --git a/shared/src/commonMain/kotlin/org/hyperskill/app/request_review/modal/view/mapper/RequestReviewModalViewStateMapper.kt b/shared/src/commonMain/kotlin/org/hyperskill/app/request_review/modal/view/mapper/RequestReviewModalViewStateMapper.kt index ffd2678fb..370554b27 100644 --- a/shared/src/commonMain/kotlin/org/hyperskill/app/request_review/modal/view/mapper/RequestReviewModalViewStateMapper.kt +++ b/shared/src/commonMain/kotlin/org/hyperskill/app/request_review/modal/view/mapper/RequestReviewModalViewStateMapper.kt @@ -1,13 +1,11 @@ package org.hyperskill.app.request_review.modal.view.mapper import org.hyperskill.app.SharedResources -import org.hyperskill.app.core.domain.platform.Platform import org.hyperskill.app.core.view.mapper.ResourceProvider import org.hyperskill.app.request_review.modal.presentation.RequestReviewModalFeature.State import org.hyperskill.app.request_review.modal.presentation.RequestReviewModalFeature.ViewState internal class RequestReviewModalViewStateMapper( - private val platform: Platform, private val resourceProvider: ResourceProvider ) { fun map(state: State): ViewState = @@ -15,8 +13,7 @@ internal class RequestReviewModalViewStateMapper( State.Awaiting, State.Positive -> ViewState( title = resourceProvider.getString( - SharedResources.strings.request_review_modal_state_awaiting_title, - resourceProvider.getString(platform.appNameResource) + SharedResources.strings.request_review_modal_state_awaiting_title ), description = null, positiveButtonText = resourceProvider.getString( diff --git a/shared/src/commonMain/kotlin/org/hyperskill/app/welcome_onboarding/questionnaire/view/WelcomeQuestionnaireViewStateMapper.kt b/shared/src/commonMain/kotlin/org/hyperskill/app/welcome_onboarding/questionnaire/view/WelcomeQuestionnaireViewStateMapper.kt index bf14cbcf2..26975b8eb 100644 --- a/shared/src/commonMain/kotlin/org/hyperskill/app/welcome_onboarding/questionnaire/view/WelcomeQuestionnaireViewStateMapper.kt +++ b/shared/src/commonMain/kotlin/org/hyperskill/app/welcome_onboarding/questionnaire/view/WelcomeQuestionnaireViewStateMapper.kt @@ -13,11 +13,6 @@ class WelcomeQuestionnaireViewStateMapper( private val platform: Platform, private val resourceProvider: ResourceProvider ) { - private val title = resourceProvider.getString( - SharedResources.strings.welcome_questionnaire_title_template, - resourceProvider.getString(platform.appNameResource) - ) - fun mapQuestionnaireTypeToViewState(type: WelcomeQuestionnaireType): WelcomeQuestionnaireViewState = when (type) { WelcomeQuestionnaireType.HOW_DID_YOU_HEAR_ABOUT_HYPERSKILL -> @@ -28,7 +23,7 @@ class WelcomeQuestionnaireViewStateMapper( private fun getHowDidYouHearAboutHyperskillViewState(): WelcomeQuestionnaireViewState = WelcomeQuestionnaireViewState( - title = title, + title = resourceProvider.getString(SharedResources.strings.welcome_questionnaire_title), items = listOf( WelcomeQuestionnaireItem( ClientSource.TIK_TOK, diff --git a/shared/src/commonMain/moko-resources/base/strings.xml b/shared/src/commonMain/moko-resources/base/strings.xml index ebc27bcb7..20e864405 100644 --- a/shared/src/commonMain/moko-resources/base/strings.xml +++ b/shared/src/commonMain/moko-resources/base/strings.xml @@ -1,8 +1,7 @@ - My Hyperskill - Hyperskill + Hyperskill Connection was lost Sorry, something went wrong: please try again later. @@ -39,8 +38,8 @@ User name - Sign in to %s - Sign up to %s + Sign in to Hyperskill + Sign up to Hyperskill JetBrains Account Google GitHub @@ -475,8 +474,7 @@ Me: If the IDE knows I\'m missing a semicolon, why won\'t it just add it itself? 🤔 - My Hyperskill - Hyperskill + Hyperskill Code a little every day, wherever you are. Start for free I already have an account @@ -649,7 +647,7 @@ Search all of Hyperskill for topic theory - Do you enjoy\n%s app? + Do you enjoy\nHyperskill app? 👍 Yes 👎 No @@ -738,7 +736,7 @@ Start my journey - How did you hear about\n%s? + How did you hear about\nHyperskill? TikTok Google Search News/article/blog diff --git a/shared/src/iosMain/kotlin/org/hyperskill/app/application_shortcuts/domain/interactor/ApplicationShortcutsInteractor.kt b/shared/src/iosMain/kotlin/org/hyperskill/app/application_shortcuts/domain/interactor/ApplicationShortcutsInteractor.kt index 2400cdd0c..7d6058537 100644 --- a/shared/src/iosMain/kotlin/org/hyperskill/app/application_shortcuts/domain/interactor/ApplicationShortcutsInteractor.kt +++ b/shared/src/iosMain/kotlin/org/hyperskill/app/application_shortcuts/domain/interactor/ApplicationShortcutsInteractor.kt @@ -21,7 +21,7 @@ class ApplicationShortcutsInteractor( return FeedbackEmailDataBuilder.build( supportEmail = resourceProvider.getString(SharedResources.strings.settings_send_feedback_support_email), - applicationName = resourceProvider.getString(platform.appNameResource), + applicationName = resourceProvider.getString(SharedResources.strings.app_name), platform = platform, userId = currentProfile?.id, applicationVersion = userAgentInfo.versionCode diff --git a/shared/src/iosMain/kotlin/org/hyperskill/app/core/domain/platform/Platform.kt b/shared/src/iosMain/kotlin/org/hyperskill/app/core/domain/platform/Platform.kt index bc020ed73..5d9e08807 100644 --- a/shared/src/iosMain/kotlin/org/hyperskill/app/core/domain/platform/Platform.kt +++ b/shared/src/iosMain/kotlin/org/hyperskill/app/core/domain/platform/Platform.kt @@ -1,7 +1,5 @@ package org.hyperskill.app.core.domain.platform -import dev.icerock.moko.resources.StringResource -import org.hyperskill.app.SharedResources import platform.UIKit.UIDevice actual class Platform actual constructor() { @@ -12,6 +10,4 @@ actual class Platform actual constructor() { actual val analyticName: String = "ios" actual val feedbackName: String = "iOS" - - actual val appNameResource: StringResource = SharedResources.strings.ios_app_name } \ No newline at end of file From 11956296485d780d6010f768c23fcedae4c18236 Mon Sep 17 00:00:00 2001 From: github-actions Date: Mon, 12 Aug 2024 03:13:54 +0000 Subject: [PATCH 16/16] Bump build number --- gradle/app.versions.toml | 2 +- .../NotificationServiceExtension/Info.plist | 2 +- .../iosHyperskillApp.xcodeproj/project.pbxproj | 16 ++++++++-------- iosHyperskillApp/iosHyperskillApp/Info.plist | 2 +- .../iosHyperskillAppTests/Info.plist | 2 +- .../iosHyperskillAppUITests/Info.plist | 2 +- 6 files changed, 13 insertions(+), 13 deletions(-) diff --git a/gradle/app.versions.toml b/gradle/app.versions.toml index 7a64f6ba8..78c40c8e4 100644 --- a/gradle/app.versions.toml +++ b/gradle/app.versions.toml @@ -3,4 +3,4 @@ minSdk = '24' targetSdk = '34' compileSdk = '34' versionName = '1.67' -versionCode = '520' \ No newline at end of file +versionCode = '521' \ No newline at end of file diff --git a/iosHyperskillApp/NotificationServiceExtension/Info.plist b/iosHyperskillApp/NotificationServiceExtension/Info.plist index 57dc1f405..bff70b1f9 100644 --- a/iosHyperskillApp/NotificationServiceExtension/Info.plist +++ b/iosHyperskillApp/NotificationServiceExtension/Info.plist @@ -9,7 +9,7 @@ CFBundleIdentifier $(PRODUCT_BUNDLE_IDENTIFIER) CFBundleVersion - 547 + 548 CFBundleShortVersionString 1.67 CFBundlePackageType diff --git a/iosHyperskillApp/iosHyperskillApp.xcodeproj/project.pbxproj b/iosHyperskillApp/iosHyperskillApp.xcodeproj/project.pbxproj index f913df379..9d022d34f 100644 --- a/iosHyperskillApp/iosHyperskillApp.xcodeproj/project.pbxproj +++ b/iosHyperskillApp/iosHyperskillApp.xcodeproj/project.pbxproj @@ -5657,7 +5657,7 @@ buildSettings = { CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 547; + CURRENT_PROJECT_VERSION = 548; DEBUG_INFORMATION_FORMAT = dwarf; DEVELOPMENT_TEAM = UJ4KC2QN7B; GENERATE_INFOPLIST_FILE = NO; @@ -5678,7 +5678,7 @@ buildSettings = { CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 547; + CURRENT_PROJECT_VERSION = 548; DEVELOPMENT_TEAM = UJ4KC2QN7B; GENERATE_INFOPLIST_FILE = NO; INFOPLIST_FILE = iosHyperskillAppUITests/Info.plist; @@ -5699,7 +5699,7 @@ BUNDLE_LOADER = "$(TEST_HOST)"; CLANG_CXX_LANGUAGE_STANDARD = "gnu++17"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 547; + CURRENT_PROJECT_VERSION = 548; DEBUG_INFORMATION_FORMAT = dwarf; DEVELOPMENT_TEAM = UJ4KC2QN7B; INFOPLIST_FILE = iosHyperskillAppTests/Info.plist; @@ -5720,7 +5720,7 @@ BUNDLE_LOADER = "$(TEST_HOST)"; CLANG_CXX_LANGUAGE_STANDARD = "gnu++17"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 547; + CURRENT_PROJECT_VERSION = 548; DEVELOPMENT_TEAM = UJ4KC2QN7B; INFOPLIST_FILE = iosHyperskillAppTests/Info.plist; IPHONEOS_DEPLOYMENT_TARGET = 14.0; @@ -5741,7 +5741,7 @@ CODE_SIGN_ENTITLEMENTS = NotificationServiceExtension/NotificationServiceExtension.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 547; + CURRENT_PROJECT_VERSION = 548; DEBUG_INFORMATION_FORMAT = dwarf; DEVELOPMENT_TEAM = UJ4KC2QN7B; INFOPLIST_FILE = NotificationServiceExtension/Info.plist; @@ -5770,7 +5770,7 @@ CODE_SIGN_ENTITLEMENTS = NotificationServiceExtension/NotificationServiceExtension.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 547; + CURRENT_PROJECT_VERSION = 548; DEVELOPMENT_TEAM = UJ4KC2QN7B; INFOPLIST_FILE = NotificationServiceExtension/Info.plist; IPHONEOS_DEPLOYMENT_TARGET = 14.0; @@ -5916,7 +5916,7 @@ CODE_SIGN_ENTITLEMENTS = iosHyperskillApp/iosHyperskillApp.entitlements; CODE_SIGN_IDENTITY = "iPhone Developer"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 547; + CURRENT_PROJECT_VERSION = 548; DEVELOPMENT_ASSET_PATHS = "\"iosHyperskillApp/Preview Content\""; DEVELOPMENT_TEAM = UJ4KC2QN7B; ENABLE_PREVIEWS = YES; @@ -5952,7 +5952,7 @@ CODE_SIGN_ENTITLEMENTS = iosHyperskillApp/iosHyperskillApp.entitlements; CODE_SIGN_IDENTITY = "iPhone Developer"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 547; + CURRENT_PROJECT_VERSION = 548; DEVELOPMENT_ASSET_PATHS = "\"iosHyperskillApp/Preview Content\""; DEVELOPMENT_TEAM = UJ4KC2QN7B; ENABLE_PREVIEWS = YES; diff --git a/iosHyperskillApp/iosHyperskillApp/Info.plist b/iosHyperskillApp/iosHyperskillApp/Info.plist index 49211fbb8..7ea383816 100644 --- a/iosHyperskillApp/iosHyperskillApp/Info.plist +++ b/iosHyperskillApp/iosHyperskillApp/Info.plist @@ -34,7 +34,7 @@ CFBundleVersion - 547 + 548 FirebaseAppDelegateProxyEnabled FirebaseMessagingAutoInitEnabled diff --git a/iosHyperskillApp/iosHyperskillAppTests/Info.plist b/iosHyperskillApp/iosHyperskillAppTests/Info.plist index 9d9721c31..c06e32d07 100644 --- a/iosHyperskillApp/iosHyperskillAppTests/Info.plist +++ b/iosHyperskillApp/iosHyperskillAppTests/Info.plist @@ -15,6 +15,6 @@ CFBundleShortVersionString 1.67 CFBundleVersion - 547 + 548 diff --git a/iosHyperskillApp/iosHyperskillAppUITests/Info.plist b/iosHyperskillApp/iosHyperskillAppUITests/Info.plist index 6cae99c7f..1c1e4c39b 100644 --- a/iosHyperskillApp/iosHyperskillAppUITests/Info.plist +++ b/iosHyperskillApp/iosHyperskillAppUITests/Info.plist @@ -15,6 +15,6 @@ CFBundleShortVersionString 1.67 CFBundleVersion - 547 + 548