diff --git a/.github/actions/setup-ios/action.yml b/.github/actions/setup-ios/action.yml index e55eded82e..9cd7b9d42e 100644 --- a/.github/actions/setup-ios/action.yml +++ b/.github/actions/setup-ios/action.yml @@ -20,7 +20,7 @@ runs: - name: Setup Xcode version uses: maxim-lobanov/setup-xcode@v1.6.0 with: - xcode-version: '15.0.1' + xcode-version: '15.1' - name: Homebrew install git-crypt run: brew install git-crypt diff --git a/.github/filters.yaml b/.github/filters.yaml index 73912842ce..0e0517e27d 100644 --- a/.github/filters.yaml +++ b/.github/filters.yaml @@ -5,6 +5,7 @@ shared: - 'shared/**' ios: - 'iosHyperskillApp/**' + - '.github/actions/setup-ios/action.yml' analytic: - 'shared/**/analytic/**' - '.github/workflows/gh_pages_analytics.yml' \ No newline at end of file diff --git a/.github/workflows/automerge_into_release.yml b/.github/workflows/automerge_into_release.yml index 81c6f1e032..a175cab740 100644 --- a/.github/workflows/automerge_into_release.yml +++ b/.github/workflows/automerge_into_release.yml @@ -64,23 +64,27 @@ jobs: git checkout release/$next_version_number git merge --no-edit origin/develop + increment_option="--both" if [ ${{ needs.files-changed.outputs.shared }} == 'true' ] then echo "Shared files changed" - ./increment_build_number.sh --both + elif [ ${{ needs.files-changed.outputs.android }} == 'true' ] && [ ${{ needs.files-changed.outputs.ios }} == 'true' ] + then + echo "Android and iOS files changed" elif [ ${{ needs.files-changed.outputs.android }} == 'true' ] then echo "Android files changed" - ./increment_build_number.sh --android + increment_option="--android" elif [ ${{ needs.files-changed.outputs.ios }} == 'true' ] then echo "iOS files changed" - ./increment_build_number.sh --ios + increment_option="--ios" else echo "No files changed" - ./increment_build_number.sh --both fi + ./increment_build_number.sh $increment_option + git push origin release/$next_version_number else echo "No existing release/$next_version_number branch found" diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index eb537fb160..a01d405382 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -137,7 +137,7 @@ jobs: android-unit-testing: needs: [files-changed, ktlint] - if: ${{ needs.files-changed.outputs.android == 'true' }} + if: ${{ needs.files-changed.outputs.android == 'true' || needs.files-changed.outputs.shared == 'true' }} name: Run Android unit tests runs-on: ubuntu-22.04 timeout-minutes: 30 diff --git a/.github/workflows/gh_pages_analytics.yml b/.github/workflows/gh_pages_analytics.yml index 62ab099e71..a40692bd3c 100644 --- a/.github/workflows/gh_pages_analytics.yml +++ b/.github/workflows/gh_pages_analytics.yml @@ -52,7 +52,7 @@ jobs: GITHUB_PERSONAL_ACCESS_TOKEN: ${{ secrets.GITHUB_TOKEN }} - name: Upload Artifact - uses: actions/upload-pages-artifact@v2.0.0 + uses: actions/upload-pages-artifact@v3.0.0 with: name: 'github-pages-analytics' path: 'shared/build/dokka/analytics' @@ -69,6 +69,6 @@ jobs: steps: - name: Deploy to GitHub Pages id: deployment - uses: actions/deploy-pages@v3.0.1 + uses: actions/deploy-pages@v4.0.2 with: artifact_name: 'github-pages-analytics' \ No newline at end of file diff --git a/androidHyperskillApp/build.gradle.kts b/androidHyperskillApp/build.gradle.kts index e6afffb9db..cbe0a886cc 100644 --- a/androidHyperskillApp/build.gradle.kts +++ b/androidHyperskillApp/build.gradle.kts @@ -84,10 +84,13 @@ dependencies { } android { + val applicationPackage = "org.hyperskill.app.android" + namespace = applicationPackage + compileSdk = appVersions.versions.compileSdk.get().toInt() defaultConfig { - applicationId = "org.hyperskill.app.android" + applicationId = applicationPackage minSdk = appVersions.versions.minSdk.get().toInt() targetSdk = appVersions.versions.targetSdk.get().toInt() versionCode = appVersions.versions.versionCode.get().toInt() diff --git a/androidHyperskillApp/src/main/AndroidManifest.xml b/androidHyperskillApp/src/main/AndroidManifest.xml index 6a8ed9fbf0..2edc661025 100644 --- a/androidHyperskillApp/src/main/AndroidManifest.xml +++ b/androidHyperskillApp/src/main/AndroidManifest.xml @@ -1,6 +1,6 @@ + > diff --git a/androidHyperskillApp/src/main/java/org/hyperskill/app/android/HyperskillApp.kt b/androidHyperskillApp/src/main/java/org/hyperskill/app/android/HyperskillApp.kt index 44e48e4049..0d9114e02f 100644 --- a/androidHyperskillApp/src/main/java/org/hyperskill/app/android/HyperskillApp.kt +++ b/androidHyperskillApp/src/main/java/org/hyperskill/app/android/HyperskillApp.kt @@ -3,13 +3,16 @@ package org.hyperskill.app.android import android.app.Application import android.content.Context import android.os.Build +import androidx.appcompat.app.AppCompatDelegate import coil.ImageLoader import coil.ImageLoaderFactory import org.hyperskill.app.android.core.extensions.NotificationChannelInitializer import org.hyperskill.app.android.core.injection.AndroidAppComponent import org.hyperskill.app.android.core.injection.AndroidAppComponentImpl +import org.hyperskill.app.android.profile_settings.view.mapper.asNightMode import org.hyperskill.app.android.util.DebugToolsHelper import org.hyperskill.app.core.domain.BuildVariant +import org.hyperskill.app.core.injection.AppGraph import org.hyperskill.app.core.remote.UserAgentInfo import ru.nobird.android.view.base.ui.extension.isMainProcess @@ -51,6 +54,7 @@ class HyperskillApp : Application(), ImageLoaderFactory { buildVariant = BuildVariant.getByValue(BuildConfig.BUILD_TYPE)!! ) + setNightMode(appGraph) initSentry() initChannels() } @@ -73,4 +77,11 @@ class HyperskillApp : Application(), ImageLoaderFactory { private fun initChannels() { NotificationChannelInitializer.initNotificationChannels(this) } + + private fun setNightMode(appGraph: AppGraph) { + val profileSettings = appGraph.buildProfileSettingsComponent().profileSettingsInteractor.getProfileSettings() + AppCompatDelegate.setDefaultNightMode( + profileSettings.theme.asNightMode() + ) + } } \ No newline at end of file diff --git a/androidHyperskillApp/src/main/java/org/hyperskill/app/android/code/presentation/model/ProgrammingLanguage.kt b/androidHyperskillApp/src/main/java/org/hyperskill/app/android/code/presentation/model/ProgrammingLanguage.kt deleted file mode 100644 index 89c70c440d..0000000000 --- a/androidHyperskillApp/src/main/java/org/hyperskill/app/android/code/presentation/model/ProgrammingLanguage.kt +++ /dev/null @@ -1,166 +0,0 @@ -package org.hyperskill.app.android.code.presentation.model - -import android.content.Context -import android.os.Parcel -import android.os.Parcelable -import org.hyperskill.app.android.R - -enum class ProgrammingLanguage(val serverPrintableName: String) : Parcelable { - PYTHON("python3"), - CPP11("c++11"), - CPP("c++"), - C("c"), - HASKELL("haskell"), - HASKELL7("haskell 7.10"), - HASKELL8("haskell 8.0"), - JAVA("java"), - JAVA8("java8"), - OCTAVE("octave"), - ASM32("asm32"), - ASM64("asm64"), - SHELL("shell"), - RUST("rust"), - R("r"), - RUBY("ruby"), - CLOJURE("clojure"), - CS("mono c#"), - JAVASCRIPT("javascript"), - SCALA("scala"), - KOTLIN("kotlin"), - GO("go"), - PASCAL("pascalabc"), - PERL("perl"), - SQL("sql"); - - override fun describeContents(): Int = 0 - - override fun writeToParcel(parcel: Parcel, flags: Int) { - parcel.writeInt(ordinal) - } - - companion object CREATOR : Parcelable.Creator { - - override fun createFromParcel(parcel: Parcel): ProgrammingLanguage = - values()[parcel.readInt()] - - override fun newArray(size: Int): Array = - arrayOfNulls(size) - - // make it public and resolve highlighting - @Suppress("unused") - private fun highlighting(serverName: String) { - when (serverNameToLanguage(serverName)) { - PYTHON -> TODO() - CPP11 -> TODO() - CPP -> TODO() - C -> TODO() - HASKELL -> TODO() - HASKELL7 -> TODO() - HASKELL8 -> TODO() - JAVA -> TODO() - JAVA8 -> TODO() - OCTAVE -> TODO() - ASM32 -> TODO() - ASM64 -> TODO() - SHELL -> TODO() - RUST -> TODO() - R -> TODO() - RUBY -> TODO() - CLOJURE -> TODO() - CS -> TODO() - JAVASCRIPT -> TODO() - SCALA -> TODO() - KOTLIN -> TODO() - GO -> TODO() - PASCAL -> TODO() - PERL -> TODO() - SQL -> TODO() - null -> TODO() - } - } - } -} - -private fun serverNameToLanguage(serverName: String): ProgrammingLanguage? = - ProgrammingLanguage.values() - .find { - it.serverPrintableName.equals(serverName, ignoreCase = true) - } - -fun symbolsForLanguage(lang: String, context: Context): Array { - val programmingLanguage = serverNameToLanguage(lang) - with(context.resources) { - return when (programmingLanguage) { - ProgrammingLanguage.PYTHON -> - getStringArray(R.array.frequent_symbols_py) - ProgrammingLanguage.CPP11, ProgrammingLanguage.CPP, ProgrammingLanguage.C -> - getStringArray(R.array.frequent_symbols_cpp) - ProgrammingLanguage.HASKELL, ProgrammingLanguage.HASKELL7, ProgrammingLanguage.HASKELL8 -> - getStringArray(R.array.frequent_symbols_default) - ProgrammingLanguage.JAVA, ProgrammingLanguage.JAVA8 -> - getStringArray(R.array.frequent_symbols_java) - ProgrammingLanguage.OCTAVE -> - getStringArray(R.array.frequent_symbols_default) - ProgrammingLanguage.ASM32, ProgrammingLanguage.ASM64 -> - getStringArray(R.array.frequent_symbols_default) - ProgrammingLanguage.SHELL -> - getStringArray(R.array.frequent_symbols_default) - ProgrammingLanguage.RUST -> - getStringArray(R.array.frequent_symbols_default) - ProgrammingLanguage.R -> - getStringArray(R.array.frequent_symbols_default) - ProgrammingLanguage.RUBY -> - getStringArray(R.array.frequent_symbols_default) - ProgrammingLanguage.CLOJURE -> - getStringArray(R.array.frequent_symbols_default) - ProgrammingLanguage.CS -> - getStringArray(R.array.frequent_symbols_cs) - ProgrammingLanguage.JAVASCRIPT -> - getStringArray(R.array.frequent_symbols_js) - ProgrammingLanguage.SCALA -> - getStringArray(R.array.frequent_symbols_default) - ProgrammingLanguage.KOTLIN -> - getStringArray(R.array.frequent_symbols_default) - ProgrammingLanguage.GO -> - getStringArray(R.array.frequent_symbols_default) - ProgrammingLanguage.PASCAL -> - getStringArray(R.array.frequent_symbols_default) - ProgrammingLanguage.PERL -> - getStringArray(R.array.frequent_symbols_default) - ProgrammingLanguage.SQL -> - getStringArray(R.array.frequent_symbols_sql) - null -> - getStringArray(R.array.frequent_symbols_default) - } - } -} - -fun extensionForLanguage(lang: String): String = - when (serverNameToLanguage(lang)) { - ProgrammingLanguage.PYTHON -> "py" - ProgrammingLanguage.CPP11, - ProgrammingLanguage.CPP, - ProgrammingLanguage.C -> "cpp" - ProgrammingLanguage.HASKELL, - ProgrammingLanguage.HASKELL7, - ProgrammingLanguage.HASKELL8 -> "hs" - ProgrammingLanguage.JAVA, - ProgrammingLanguage.JAVA8 -> "java" - ProgrammingLanguage.OCTAVE -> "matlab" - ProgrammingLanguage.ASM32, - ProgrammingLanguage.ASM64 -> "asm" - ProgrammingLanguage.SHELL -> "sh" - ProgrammingLanguage.RUST -> "rust" - ProgrammingLanguage.R -> "r" - ProgrammingLanguage.RUBY -> "rb" - ProgrammingLanguage.CLOJURE -> "clj" - ProgrammingLanguage.CS -> "cs" - ProgrammingLanguage.JAVASCRIPT -> "js" - ProgrammingLanguage.SCALA -> "scala" - ProgrammingLanguage.KOTLIN -> "kt" - ProgrammingLanguage.GO -> "go" - ProgrammingLanguage.PASCAL -> "pascal" - ProgrammingLanguage.PERL -> "perl" - ProgrammingLanguage.SQL -> "sql" - null -> "" - } \ No newline at end of file diff --git a/androidHyperskillApp/src/main/java/org/hyperskill/app/android/code/presentation/model/ProgrammingLanguageExtension.kt b/androidHyperskillApp/src/main/java/org/hyperskill/app/android/code/presentation/model/ProgrammingLanguageExtension.kt new file mode 100644 index 0000000000..576c10d1f1 --- /dev/null +++ b/androidHyperskillApp/src/main/java/org/hyperskill/app/android/code/presentation/model/ProgrammingLanguageExtension.kt @@ -0,0 +1,46 @@ +package org.hyperskill.app.android.code.presentation.model + +import android.content.Context +import org.hyperskill.app.android.R +import org.hyperskill.app.code.domain.model.ProgrammingLanguage +/* ktlint-disable */ +import org.hyperskill.app.code.domain.model.ProgrammingLanguage.* + +fun ProgrammingLanguage?.getSymbols(context: Context): Array = + when (this) { + PYTHON, PYTHON3, PYTHON3_1, PYTHON3_11 -> + R.array.frequent_symbols_py + CPP, CPP11, CPP20, C, C_VALGRIND -> + R.array.frequent_symbols_cpp + JAVA, JAVA8, JAVA9, JAVA11, JAVA17 -> + R.array.frequent_symbols_java + CS, CS_MONO -> + R.array.frequent_symbols_cs + JAVASCRIPT, TYPESCRIPT -> + R.array.frequent_symbols_js + SQL -> + R.array.frequent_symbols_sql + PHP -> + R.array.frequent_symbols_php + + HASKELL, HASKELL7, HASKELL8, HASKELL8_8, + OCTAVE, + ASM32, ASM64, + SHELL, + RUST, + R, + RUBY, + CLOJURE, + SCALA, SCALA3, + KOTLIN, + GO, + PASCAL, + PERL, + SWIFT, + JULIA, + DART, + null -> R.array.frequent_symbols_default + }.let(context.resources::getStringArray) + +fun symbolsForLanguage(lang: String, context: Context): Array = + ProgrammingLanguage.of(lang).getSymbols(context) \ No newline at end of file diff --git a/androidHyperskillApp/src/main/java/org/hyperskill/app/android/code/util/CodeEditorKeyboardExtensionUtil.kt b/androidHyperskillApp/src/main/java/org/hyperskill/app/android/code/util/CodeEditorKeyboardExtensionUtil.kt index e7f419b425..d831f9392f 100644 --- a/androidHyperskillApp/src/main/java/org/hyperskill/app/android/code/util/CodeEditorKeyboardExtensionUtil.kt +++ b/androidHyperskillApp/src/main/java/org/hyperskill/app/android/code/util/CodeEditorKeyboardExtensionUtil.kt @@ -19,6 +19,10 @@ object CodeEditorKeyboardExtensionUtil { ) } + fun interface OnToolbarSymbolClickListener { + fun onToolbarSymbolClick(symbol: String, resultCode: String) + } + fun setupKeyboardExtension( context: Context, rootView: View, @@ -26,12 +30,16 @@ object CodeEditorKeyboardExtensionUtil { codeLayout: CodeEditorLayout, codeToolbarAdapter: CodeToolbarAdapter, isToolbarEnabled: () -> Boolean = { true }, - onToolbarSymbolClicked: ((String) -> Unit)? = null, + onToolbarSymbolClicked: OnToolbarSymbolClickListener? = null, codeEditorKeyboardListener: CodeEditorKeyboardListener? = null ) { codeToolbarAdapter.onSymbolClickListener = CodeToolbarAdapter.OnSymbolClickListener { symbol, offset -> - onToolbarSymbolClicked?.invoke(symbol) + // insert should be done first applySymbolTo(codeLayout, symbol, offset) + onToolbarSymbolClicked?.onToolbarSymbolClick( + symbol = symbol, + resultCode = codeLayout.text + ) } codeLayout.codeToolbarAdapter = codeToolbarAdapter diff --git a/androidHyperskillApp/src/main/java/org/hyperskill/app/android/code/view/widget/CodeEditorLayout.kt b/androidHyperskillApp/src/main/java/org/hyperskill/app/android/code/view/widget/CodeEditorLayout.kt index b539bea16c..d278123d4e 100644 --- a/androidHyperskillApp/src/main/java/org/hyperskill/app/android/code/view/widget/CodeEditorLayout.kt +++ b/androidHyperskillApp/src/main/java/org/hyperskill/app/android/code/view/widget/CodeEditorLayout.kt @@ -28,7 +28,7 @@ constructor( codeEditor.theme = value } - val text: CharSequence + val text: String get() = codeEditor.text.toString() var langExtension: String @@ -72,7 +72,7 @@ constructor( } fun setTextIfChanged(text: String) { - if (this.text.toString() != text) { + if (this.text != text) { codeEditor.setText(text) } } diff --git a/androidHyperskillApp/src/main/java/org/hyperskill/app/android/core/view/ui/widget/compose/AlignmentExtension.kt b/androidHyperskillApp/src/main/java/org/hyperskill/app/android/core/view/ui/widget/compose/AlignmentExtension.kt new file mode 100644 index 0000000000..12146bd63f --- /dev/null +++ b/androidHyperskillApp/src/main/java/org/hyperskill/app/android/core/view/ui/widget/compose/AlignmentExtension.kt @@ -0,0 +1,16 @@ +package org.hyperskill.app.android.core.view.ui.widget.compose + +import androidx.compose.runtime.Stable +import androidx.compose.ui.Alignment +import androidx.compose.ui.BiasAlignment + +/** + * An [Alignment] specified by vertical bias: for example, a bias of -1 represents alignment to the + * top, a bias of 0 will represent centering, and a bias of 1 will represent bottom. + * Any value can be specified to obtain an alignment. + * + * @see BiasAlignment + */ +@Stable +fun Alignment.Companion.centerWithVerticalBias(verticalBias: Float): Alignment = + BiasAlignment(0f, verticalBias) \ No newline at end of file diff --git a/androidHyperskillApp/src/main/java/org/hyperskill/app/android/main/view/ui/activity/MainActivity.kt b/androidHyperskillApp/src/main/java/org/hyperskill/app/android/main/view/ui/activity/MainActivity.kt index 27741e3c6d..1888cce220 100644 --- a/androidHyperskillApp/src/main/java/org/hyperskill/app/android/main/view/ui/activity/MainActivity.kt +++ b/androidHyperskillApp/src/main/java/org/hyperskill/app/android/main/view/ui/activity/MainActivity.kt @@ -6,7 +6,6 @@ import android.content.pm.PackageManager import android.os.Bundle import androidx.activity.viewModels import androidx.appcompat.app.AppCompatActivity -import androidx.appcompat.app.AppCompatDelegate import androidx.core.app.NotificationManagerCompat import androidx.core.content.ContextCompat import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen @@ -41,7 +40,6 @@ import org.hyperskill.app.android.notification.model.DefaultNotificationClickedD import org.hyperskill.app.android.notification.model.PushNotificationClickedData import org.hyperskill.app.android.notification_onboarding.fragment.NotificationsOnboardingFragment import org.hyperskill.app.android.notification_onboarding.navigation.NotificationsOnboardingScreen -import org.hyperskill.app.android.profile_settings.view.mapper.ThemeMapper import org.hyperskill.app.android.step.view.screen.StepScreen import org.hyperskill.app.android.streak_recovery.view.delegate.StreakRecoveryViewActionDelegate import org.hyperskill.app.android.track_selection.list.navigation.TrackSelectionListScreen @@ -51,7 +49,6 @@ import org.hyperskill.app.main.presentation.MainViewModel import org.hyperskill.app.notification.click_handling.presentation.NotificationClickHandlingFeature import org.hyperskill.app.notification.local.domain.analytic.NotificationDailyStudyReminderClickedHyperskillAnalyticEvent import org.hyperskill.app.profile.domain.model.Profile -import org.hyperskill.app.profile_settings.domain.model.ProfileSettings import org.hyperskill.app.step.domain.model.StepRoute import org.hyperskill.app.track_selection.list.injection.TrackSelectionListParams import org.hyperskill.app.welcome_onboarding.presentation.WelcomeOnboardingFeature @@ -83,8 +80,6 @@ class MainActivity : private val localCicerone: Cicerone = Cicerone.create() override val router: Router = localCicerone.router - private lateinit var profileSettings: ProfileSettings - private lateinit var analyticInteractor: AnalyticInteractor private val notificationInteractor = @@ -132,16 +127,12 @@ class MainActivity : observeNotificationsOnboardingFlowFinished() observeFirstProblemOnboardingFlowFinished() - AppCompatDelegate.setDefaultNightMode(ThemeMapper.getAppCompatDelegate(profileSettings.theme)) - mainViewModel.logScreenOrientation(screenOrientation = resources.configuration.screenOrientation) logNotificationAvailability() } private fun injectManual() { viewModelFactory = HyperskillApp.graph().platformMainComponent.reduxViewModelFactory - profileSettings = - HyperskillApp.graph().buildProfileSettingsComponent().profileSettingsInteractor.getProfileSettings() analyticInteractor = HyperskillApp.graph().analyticComponent.analyticInteractor } diff --git a/androidHyperskillApp/src/main/java/org/hyperskill/app/android/notification/remote/model/PushNotificationDataExtensions.kt b/androidHyperskillApp/src/main/java/org/hyperskill/app/android/notification/remote/model/PushNotificationDataExtensions.kt index 8453dab3fc..864aec69dd 100644 --- a/androidHyperskillApp/src/main/java/org/hyperskill/app/android/notification/remote/model/PushNotificationDataExtensions.kt +++ b/androidHyperskillApp/src/main/java/org/hyperskill/app/android/notification/remote/model/PushNotificationDataExtensions.kt @@ -21,6 +21,7 @@ val PushNotificationData.id: NotificationId PushNotificationType.REMIND_MEDIUM -> NotificationId.RemindMedium PushNotificationType.USER_BADGE_UPDATED -> NotificationId.UserBadgeUpdated PushNotificationType.USER_BADGE_UNLOCKED -> NotificationId.UserBadgeUnlocked + PushNotificationType.DAILY_REMINDER -> NotificationId.DailyStudyReminder PushNotificationType.UNKNOWN -> NotificationId.Unknown } @@ -43,5 +44,7 @@ val PushNotificationData.channel: HyperskillNotificationChannel PushNotificationType.USER_BADGE_UNLOCKED, PushNotificationType.USER_BADGE_UPDATED -> HyperskillNotificationChannel.RegularLearningReminders + PushNotificationType.DAILY_REMINDER -> HyperskillNotificationChannel.DailyReminder + PushNotificationType.UNKNOWN -> HyperskillNotificationChannel.Other } \ No newline at end of file diff --git a/androidHyperskillApp/src/main/java/org/hyperskill/app/android/notification_onboarding/fragment/NotificationsOnboardingFragment.kt b/androidHyperskillApp/src/main/java/org/hyperskill/app/android/notification_onboarding/fragment/NotificationsOnboardingFragment.kt index d88284a1c9..6f9ae4de09 100644 --- a/androidHyperskillApp/src/main/java/org/hyperskill/app/android/notification_onboarding/fragment/NotificationsOnboardingFragment.kt +++ b/androidHyperskillApp/src/main/java/org/hyperskill/app/android/notification_onboarding/fragment/NotificationsOnboardingFragment.kt @@ -19,13 +19,15 @@ import org.hyperskill.app.android.core.extensions.startAppNotificationSettingsIn import org.hyperskill.app.android.core.view.ui.navigation.requireAppRouter import org.hyperskill.app.android.notification.permission.NotificationPermissionDelegate import org.hyperskill.app.android.notification_onboarding.ui.NotificationsOnboardingScreen +import org.hyperskill.app.android.profile.view.fragment.TimeIntervalPickerDialogFragment import org.hyperskill.app.core.view.handleActions import org.hyperskill.app.notifications_onboarding.presentation.NotificationsOnboardingFeature.Action.ViewAction import org.hyperskill.app.notifications_onboarding.presentation.NotificationsOnboardingFeature.Message import org.hyperskill.app.notifications_onboarding.presentation.NotificationsOnboardingViewModel import ru.nobird.android.view.base.ui.extension.argument +import ru.nobird.android.view.base.ui.extension.showIfNotExists -class NotificationsOnboardingFragment : Fragment() { +class NotificationsOnboardingFragment : Fragment(), TimeIntervalPickerDialogFragment.Callback { companion object { const val NOTIFICATIONS_ONBOARDING_FINISHED = "NOTIFICATIONS_ONBOARDING_FINISHED" @@ -109,7 +111,9 @@ class NotificationsOnboardingFragment : Fragment() { notificationPermissionDelegate?.requestNotificationPermission() } is ViewAction.ShowDailyStudyRemindersIntervalStartHourPickerModal -> { - // TODO: ALTAPPS-1071 show modal + TimeIntervalPickerDialogFragment + .newInstance(selectedHour = action.dailyStudyRemindersStartHour) + .showIfNotExists(childFragmentManager, TimeIntervalPickerDialogFragment.TAG) } } } @@ -139,4 +143,22 @@ class NotificationsOnboardingFragment : Fragment() { } } } + + override fun onTimeIntervalPicked(chosenInterval: Int) { + notificationsOnboardingViewModel.onNewMessage( + Message.DailyStudyRemindsIntervalStartHourSelected(chosenInterval) + ) + } + + override fun onTimeIntervalDialogShown() { + notificationsOnboardingViewModel.onNewMessage( + Message.DailyStudyRemindersIntervalStartHourPickerModalShownEventMessage + ) + } + + override fun onTimeIntervalDialogHidden() { + notificationsOnboardingViewModel.onNewMessage( + Message.DailyStudyRemindersIntervalStartHourPickerModalHiddenEventMessage + ) + } } \ No newline at end of file diff --git a/androidHyperskillApp/src/main/java/org/hyperskill/app/android/notification_onboarding/ui/NotificationsOnboardingScreen.kt b/androidHyperskillApp/src/main/java/org/hyperskill/app/android/notification_onboarding/ui/NotificationsOnboardingScreen.kt index bcd31da02e..e0f6013cae 100644 --- a/androidHyperskillApp/src/main/java/org/hyperskill/app/android/notification_onboarding/ui/NotificationsOnboardingScreen.kt +++ b/androidHyperskillApp/src/main/java/org/hyperskill/app/android/notification_onboarding/ui/NotificationsOnboardingScreen.kt @@ -3,31 +3,44 @@ package org.hyperskill.app.android.notification_onboarding.ui import android.content.res.Configuration import androidx.compose.foundation.Image import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material.MaterialTheme import androidx.compose.material.Text +import androidx.compose.material.ripple.rememberRipple import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue +import androidx.compose.runtime.remember import androidx.compose.runtime.rememberUpdatedState import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip import androidx.compose.ui.res.colorResource +import androidx.compose.ui.res.dimensionResource import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp +import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.google.accompanist.themeadapter.material.MdcTheme import org.hyperskill.app.R import org.hyperskill.app.android.core.view.ui.widget.compose.HyperskillButton import org.hyperskill.app.android.core.view.ui.widget.compose.HyperskillTextButton +import org.hyperskill.app.android.core.view.ui.widget.compose.centerWithVerticalBias import org.hyperskill.app.notifications_onboarding.presentation.NotificationsOnboardingFeature import org.hyperskill.app.notifications_onboarding.presentation.NotificationsOnboardingViewModel @@ -35,7 +48,7 @@ object NotificationsOnboardingDefaults { val ContentPadding = PaddingValues( start = 20.dp, end = 20.dp, - top = 24.dp, + top = 40.dp, bottom = 32.dp ) } @@ -44,11 +57,16 @@ object NotificationsOnboardingDefaults { fun NotificationsOnboardingScreen( viewModel: NotificationsOnboardingViewModel ) { - NotificationsOnboardingScreen(viewModel::onNewMessage) + val viewState: NotificationsOnboardingFeature.ViewState by viewModel.state.collectAsStateWithLifecycle() + NotificationsOnboardingScreen( + formattedInterval = viewState.formattedDailyStudyRemindersInterval, + onNewMessage = viewModel::onNewMessage + ) } @Composable fun NotificationsOnboardingScreen( + formattedInterval: String, onNewMessage: (NotificationsOnboardingFeature.Message) -> Unit ) { val onAllowNotificationsClick by rememberUpdatedState { @@ -57,23 +75,28 @@ fun NotificationsOnboardingScreen( val onRemindMeLaterClick by rememberUpdatedState { onNewMessage(NotificationsOnboardingFeature.Message.NotNowClicked) } + val onTimeClick by rememberUpdatedState { + onNewMessage(NotificationsOnboardingFeature.Message.DailyStudyRemindsIntervalHourClicked) + } Column( modifier = Modifier .fillMaxSize() .background(colorResource(id = R.color.layer_1)) - .padding(NotificationsOnboardingDefaults.ContentPadding), - verticalArrangement = Arrangement.SpaceBetween + .padding(NotificationsOnboardingDefaults.ContentPadding) ) { - NotificationsOnboardingHeader() - Image( - painter = painterResource(id = org.hyperskill.app.android.R.drawable.img_notifications_onboarding), - contentDescription = null, - modifier = Modifier - .fillMaxWidth() - .align(Alignment.CenterHorizontally) - ) + NotificationsOnboardingHeader(formattedInterval, onTimeClick = onTimeClick) + Box( + modifier = Modifier.weight(1f) + ) { + Image( + painter = painterResource(id = org.hyperskill.app.android.R.drawable.img_notifications_onboarding), + contentDescription = null, + modifier = Modifier + .fillMaxWidth() + .align(Alignment.centerWithVerticalBias(-0.5f)) + ) + } NotificationsOnboardingButtons( - modifier = Modifier.weight(1f, fill = false), onAllowNotificationsClick = onAllowNotificationsClick, onRemindMeLaterClick = onRemindMeLaterClick ) @@ -82,31 +105,69 @@ fun NotificationsOnboardingScreen( @Composable private fun NotificationsOnboardingHeader( - modifier: Modifier = Modifier + formattedInterval: String, + modifier: Modifier = Modifier, + onTimeClick: () -> Unit ) { Column( modifier = modifier, - horizontalAlignment = Alignment.CenterHorizontally + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.spacedBy(16.dp) ) { Text( - text = stringResource(id = R.string.notifications_onboarding_title), + text = stringResource(id = R.string.notifications_onboarding_title_new), textAlign = TextAlign.Center, style = MaterialTheme.typography.h6 ) - Spacer(modifier = Modifier.height(8.dp)) - Text( - text = stringResource(id = R.string.notifications_onboarding_description), - textAlign = TextAlign.Center, - style = MaterialTheme.typography.body1, - // Can't use color_text_secondary in TextAppearance.AppTheme.Body1 - // because it is used in many other components - color = colorResource(id = R.color.text_secondary) + DailyNotificationTime( + formattedInterval = formattedInterval, + onTimeClick = onTimeClick ) } } @Composable -fun NotificationsOnboardingButtons( +private fun DailyNotificationTime( + formattedInterval: String, + onTimeClick: () -> Unit, + modifier: Modifier = Modifier +) { + Box( + modifier = modifier + .clip(RoundedCornerShape(dimensionResource(id = org.hyperskill.app.android.R.dimen.corner_radius))) + ) { + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.Center, + modifier = Modifier + .clickable( + interactionSource = remember { + MutableInteractionSource() + }, + indication = rememberRipple(), + onClick = onTimeClick + ) + .padding(vertical = 4.dp, horizontal = 8.dp) + ) { + Text(text = stringResource(id = R.string.notifications_onboarding_daily_study_reminders_interval_prefix)) + Spacer(modifier = Modifier.width(8.dp)) + Text(text = formattedInterval) + Spacer(modifier = Modifier.width(4.dp)) + Box(modifier = Modifier.size(24.dp)) { + Image( + painter = painterResource( + id = org.hyperskill.app.android.R.drawable.ic_notification_onboarding_arrow + ), + contentDescription = null, + modifier.align(Alignment.Center) + ) + } + } + } +} + +@Composable +private fun NotificationsOnboardingButtons( modifier: Modifier = Modifier, onAllowNotificationsClick: () -> Unit, onRemindMeLaterClick: () -> Unit @@ -132,12 +193,12 @@ fun NotificationsOnboardingButtons( @Preview(showBackground = true, showSystemUi = true) @Composable -fun NotificationsOnboardingScreenLightModePreview() { +private fun NotificationsOnboardingScreenLightModePreview() { MdcTheme( setTextColors = true, setDefaultFontFamily = true ) { - NotificationsOnboardingScreen {} + NotificationsOnboardingScreen("12:00 - 13:00") {} } } @@ -148,11 +209,11 @@ fun NotificationsOnboardingScreenLightModePreview() { device = "id:Nexus One" ) @Composable -fun NotificationsOnboardingScreenDarkModePreview() { +private fun NotificationsOnboardingScreenDarkModePreview() { MdcTheme( setTextColors = true, setDefaultFontFamily = true ) { - NotificationsOnboardingScreen {} + NotificationsOnboardingScreen("12:00 - 13:00") {} } } \ No newline at end of file diff --git a/androidHyperskillApp/src/main/java/org/hyperskill/app/android/profile/view/fragment/ProfileFragment.kt b/androidHyperskillApp/src/main/java/org/hyperskill/app/android/profile/view/fragment/ProfileFragment.kt index 2c0e97706e..2f7bea4863 100644 --- a/androidHyperskillApp/src/main/java/org/hyperskill/app/android/profile/view/fragment/ProfileFragment.kt +++ b/androidHyperskillApp/src/main/java/org/hyperskill/app/android/profile/view/fragment/ProfileFragment.kt @@ -47,7 +47,7 @@ import ru.nobird.app.presentation.redux.container.ReduxView class ProfileFragment : Fragment(R.layout.fragment_profile), ReduxView, - TimeIntervalPickerDialogFragment.Companion.Callback, + TimeIntervalPickerDialogFragment.Callback, BadgeDetailsDialogFragment.Callback { companion object { fun newInstance(profileId: Long? = null, isInitCurrent: Boolean = true): Fragment = diff --git a/androidHyperskillApp/src/main/java/org/hyperskill/app/android/profile/view/fragment/TimeIntervalPickerDialogFragment.kt b/androidHyperskillApp/src/main/java/org/hyperskill/app/android/profile/view/fragment/TimeIntervalPickerDialogFragment.kt index a364c17ab6..28abe961b2 100644 --- a/androidHyperskillApp/src/main/java/org/hyperskill/app/android/profile/view/fragment/TimeIntervalPickerDialogFragment.kt +++ b/androidHyperskillApp/src/main/java/org/hyperskill/app/android/profile/view/fragment/TimeIntervalPickerDialogFragment.kt @@ -1,6 +1,7 @@ package org.hyperskill.app.android.profile.view.fragment import android.app.Dialog +import android.content.DialogInterface import android.os.Bundle import androidx.fragment.app.DialogFragment import com.google.android.material.dialog.MaterialAlertDialogBuilder @@ -17,46 +18,72 @@ class TimeIntervalPickerDialogFragment : DialogFragment() { TimeIntervalPickerDialogFragment().apply { this.selectedHour = selectedHour } - - interface Callback { - fun onTimeIntervalPicked(chosenInterval: Int) - } } private var selectedHour: Int by argument() - private lateinit var picker: NumberPicker + private var picker: NumberPicker? = null - private lateinit var callback: Callback + private val callback: Callback + get() = parentFragment as Callback override fun onCreateDialog(savedInstanceState: Bundle?): Dialog { - picker = NumberPicker(context) - picker.minValue = 0 - picker.maxValue = TimeIntervalUtil.values.size - 1 - picker.displayedValues = TimeIntervalUtil.values - picker.value = selectedHour - picker.descendantFocusability = NumberPicker.FOCUS_BLOCK_DESCENDANTS - picker.wrapSelectorWheel = false - picker.setBackgroundColor(0x0) - picker.textColor = requireContext().resolveColorAttribute(com.google.android.material.R.attr.colorOnSurface) - picker.selectedTextColor = - requireContext().resolveColorAttribute(com.google.android.material.R.attr.colorOnSurface) - picker.dividerColor = 0x0 - - try { - picker.textSize = 50f // Warning: reflection! - } catch (exception: Exception) { + val picker = NumberPicker(context) + this.picker = picker + setupPicked(picker) + return getDialog(picker).apply { + setOnShowListener { + if (savedInstanceState == null) { + callback.onTimeIntervalDialogShown() + } + } } + } - callback = parentFragment as Callback + override fun onDestroyView() { + super.onDestroyView() + picker = null + } + + override fun onCancel(dialog: DialogInterface) { + callback.onTimeIntervalDialogHidden() + } - return MaterialAlertDialogBuilder(requireContext()) + private fun getDialog(picker: NumberPicker): Dialog = + MaterialAlertDialogBuilder(requireContext()) .setTitle(org.hyperskill.app.R.string.choose_notification_time_interval) .setView(picker) .setPositiveButton(org.hyperskill.app.R.string.ok) { _, _ -> - callback.onTimeIntervalPicked(picker.value) + this.picker?.value?.let(callback::onTimeIntervalPicked) } .setNegativeButton(org.hyperskill.app.R.string.cancel, null) .create() + + private fun setupPicked(picker: NumberPicker) { + with(picker) { + minValue = 0 + maxValue = TimeIntervalUtil.values.size - 1 + displayedValues = TimeIntervalUtil.values + value = selectedHour + descendantFocusability = NumberPicker.FOCUS_BLOCK_DESCENDANTS + wrapSelectorWheel = false + setBackgroundColor(0x0) + textColor = requireContext().resolveColorAttribute(com.google.android.material.R.attr.colorOnSurface) + selectedTextColor = + requireContext().resolveColorAttribute(com.google.android.material.R.attr.colorOnSurface) + dividerColor = 0x0 + + try { + textSize = 50f // Warning: reflection! + } catch (exception: Exception) { + } + } + } + + interface Callback { + fun onTimeIntervalDialogShown() {} + + fun onTimeIntervalDialogHidden() {} + fun onTimeIntervalPicked(chosenInterval: Int) } } \ No newline at end of file diff --git a/androidHyperskillApp/src/main/java/org/hyperskill/app/android/profile_settings/view/dialog/ProfileSettingsDialogFragment.kt b/androidHyperskillApp/src/main/java/org/hyperskill/app/android/profile_settings/view/dialog/ProfileSettingsDialogFragment.kt index dfb4e1eba7..d3a18551ab 100644 --- a/androidHyperskillApp/src/main/java/org/hyperskill/app/android/profile_settings/view/dialog/ProfileSettingsDialogFragment.kt +++ b/androidHyperskillApp/src/main/java/org/hyperskill/app/android/profile_settings/view/dialog/ProfileSettingsDialogFragment.kt @@ -19,7 +19,7 @@ import org.hyperskill.app.android.core.extensions.representation import org.hyperskill.app.android.core.view.ui.dialog.LoadingProgressDialogFragment import org.hyperskill.app.android.core.view.ui.dialog.dismissDialogFragmentIfExists import org.hyperskill.app.android.databinding.FragmentProfileSettingsBinding -import org.hyperskill.app.android.profile_settings.view.mapper.ThemeMapper +import org.hyperskill.app.android.profile_settings.view.mapper.asNightMode import org.hyperskill.app.android.view.base.ui.extension.snackbar import org.hyperskill.app.profile.presentation.ProfileSettingsViewModel import org.hyperskill.app.profile_settings.domain.model.FeedbackEmailData @@ -88,9 +88,7 @@ class ProfileSettingsDialogFragment : profileSettingsViewModel.onNewMessage(ProfileSettingsFeature.Message.ThemeChanged(theme = newTheme)) viewBinding.settingsThemeChosenTextView.text = newTheme.representation - - val mode = ThemeMapper.getAppCompatDelegate(newTheme) - AppCompatDelegate.setDefaultNightMode(mode) + AppCompatDelegate.setDefaultNightMode(newTheme.asNightMode()) } .setNegativeButton(org.hyperskill.app.R.string.cancel) { dialog, _ -> dialog.dismiss() diff --git a/androidHyperskillApp/src/main/java/org/hyperskill/app/android/profile_settings/view/mapper/ThemeMapper.kt b/androidHyperskillApp/src/main/java/org/hyperskill/app/android/profile_settings/view/mapper/ThemeMapper.kt index fb0be24122..95923c2480 100644 --- a/androidHyperskillApp/src/main/java/org/hyperskill/app/android/profile_settings/view/mapper/ThemeMapper.kt +++ b/androidHyperskillApp/src/main/java/org/hyperskill/app/android/profile_settings/view/mapper/ThemeMapper.kt @@ -3,11 +3,9 @@ package org.hyperskill.app.android.profile_settings.view.mapper import androidx.appcompat.app.AppCompatDelegate import org.hyperskill.app.profile_settings.domain.model.Theme -object ThemeMapper { - fun getAppCompatDelegate(theme: Theme): Int = - when (theme) { - Theme.LIGHT -> AppCompatDelegate.MODE_NIGHT_NO - Theme.DARK -> AppCompatDelegate.MODE_NIGHT_YES - Theme.SYSTEM -> AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM - } -} \ No newline at end of file +fun Theme.asNightMode(): Int = + when (this) { + Theme.LIGHT -> AppCompatDelegate.MODE_NIGHT_NO + Theme.DARK -> AppCompatDelegate.MODE_NIGHT_YES + Theme.SYSTEM -> AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM + } \ No newline at end of file diff --git a/androidHyperskillApp/src/main/java/org/hyperskill/app/android/sentry/domain/model/manager/SentryManagerImpl.kt b/androidHyperskillApp/src/main/java/org/hyperskill/app/android/sentry/domain/model/manager/SentryManagerImpl.kt index a5cc386fec..67b65047e9 100644 --- a/androidHyperskillApp/src/main/java/org/hyperskill/app/android/sentry/domain/model/manager/SentryManagerImpl.kt +++ b/androidHyperskillApp/src/main/java/org/hyperskill/app/android/sentry/domain/model/manager/SentryManagerImpl.kt @@ -1,6 +1,7 @@ package org.hyperskill.app.android.sentry.domain.model.manager import io.sentry.Sentry +import io.sentry.SentryLevel import io.sentry.SpanStatus import io.sentry.android.core.SentryAndroid import io.sentry.android.fragment.FragmentLifecycleIntegration @@ -18,8 +19,7 @@ import org.hyperskill.app.sentry.domain.model.transaction.HyperskillSentryTransa import org.hyperskill.app.sentry.domain.model.transaction.HyperskillSentryTransactionKeyValues class SentryManagerImpl( - private val buildKonfig: BuildKonfig, - private val minLogLevel: HyperskillSentryLevel = HyperskillSentryLevel.min(buildKonfig.buildVariant) + private val buildKonfig: BuildKonfig ) : SentryManager { private val currentTransactionsMap = mutableMapOf() @@ -27,7 +27,7 @@ class SentryManagerImpl( SentryAndroid.init(HyperskillApp.application) { options -> options.dsn = BuildConfig.SENTRY_DSN options.environment = "${buildKonfig.flavor}-${BuildConfig.BUILD_TYPE}" - options.release = "${BuildConfig.VERSION_NAME} (${BuildConfig.VERSION_CODE})" + options.release = "${BuildConfig.APPLICATION_ID}@${BuildConfig.VERSION_NAME}+${BuildConfig.VERSION_CODE}" options.isEnableAutoSessionTracking = true options.isAnrEnabled = true options.addIntegration( @@ -37,13 +37,13 @@ class SentryManagerImpl( enableAutoFragmentLifecycleTracing = true ) ) - options.setDiagnosticLevel(minLogLevel.toSentryLevel()) if (BuildConfig.DEBUG) { - options.setDebug(true) + options.isDebug = true options.tracesSampleRate = 1.0 + options.setDiagnosticLevel(SentryLevel.INFO) } else { - options.setDebug(false) + options.isDebug = false options.tracesSampleRate = 0.3 } } diff --git a/androidHyperskillApp/src/main/java/org/hyperskill/app/android/step_quiz_code/view/delegate/CodeLayoutDelegate.kt b/androidHyperskillApp/src/main/java/org/hyperskill/app/android/step_quiz_code/view/delegate/CodeLayoutDelegate.kt index 3efa36b0ab..5d70ff4dcc 100644 --- a/androidHyperskillApp/src/main/java/org/hyperskill/app/android/step_quiz_code/view/delegate/CodeLayoutDelegate.kt +++ b/androidHyperskillApp/src/main/java/org/hyperskill/app/android/step_quiz_code/view/delegate/CodeLayoutDelegate.kt @@ -1,9 +1,9 @@ package org.hyperskill.app.android.step_quiz_code.view.delegate -import org.hyperskill.app.android.code.presentation.model.extensionForLanguage import org.hyperskill.app.android.code.view.adapter.CodeToolbarAdapter import org.hyperskill.app.android.code.view.widget.CodeEditorLayout import org.hyperskill.app.android.step_quiz_code.view.model.config.CodeStepQuizConfig +import org.hyperskill.app.code.domain.model.fileExtensionForLanguage class CodeLayoutDelegate( private val codeLayout: CodeEditorLayout, @@ -16,7 +16,7 @@ class CodeLayoutDelegate( * if [code] is null then [CodeStepQuizConfig.initialCode] will be used */ fun setLanguage(lang: String, code: String? = null) { - codeLayout.langExtension = extensionForLanguage(lang) + codeLayout.langExtension = fileExtensionForLanguage(lang) codeLayout.setTextIfChanged(code ?: config.initialCode) codeToolbarAdapter?.setLanguage(lang) } diff --git a/androidHyperskillApp/src/main/java/org/hyperskill/app/android/step_quiz_code/view/delegate/CodeQuizInstructionDelegate.kt b/androidHyperskillApp/src/main/java/org/hyperskill/app/android/step_quiz_code/view/delegate/CodeQuizInstructionDelegate.kt index c34768ff83..36d7de4ff1 100644 --- a/androidHyperskillApp/src/main/java/org/hyperskill/app/android/step_quiz_code/view/delegate/CodeQuizInstructionDelegate.kt +++ b/androidHyperskillApp/src/main/java/org/hyperskill/app/android/step_quiz_code/view/delegate/CodeQuizInstructionDelegate.kt @@ -9,11 +9,11 @@ import androidx.recyclerview.widget.RecyclerView import com.google.android.material.card.MaterialCardView import com.google.android.material.divider.MaterialDividerItemDecoration import org.hyperskill.app.android.R -import org.hyperskill.app.android.code.presentation.model.ProgrammingLanguage import org.hyperskill.app.android.step.view.delegate.CollapsibleStepBlockDelegate import org.hyperskill.app.android.step_quiz_code.view.adapter.CodeDetailSampleAdapterDelegate import org.hyperskill.app.android.step_quiz_code.view.model.CodeDetail import org.hyperskill.app.android.ui.custom.ArrowImageView +import org.hyperskill.app.code.domain.model.ProgrammingLanguage import ru.nobird.android.ui.adapters.DefaultDelegateAdapter class CodeQuizInstructionDelegate( @@ -62,7 +62,7 @@ class CodeQuizInstructionDelegate( } fun setCodeDetailsData(details: List, lang: String?) { - if (lang == ProgrammingLanguage.SQL.serverPrintableName) { + if (lang == ProgrammingLanguage.SQL.languageName) { detailsContainerView.isVisible = false } else { stepQuizCodeDetailsAdapter.items = details diff --git a/androidHyperskillApp/src/main/java/org/hyperskill/app/android/step_quiz_code/view/fragment/CodeStepQuizFragment.kt b/androidHyperskillApp/src/main/java/org/hyperskill/app/android/step_quiz_code/view/fragment/CodeStepQuizFragment.kt index 8c69ad9c84..8242ab4d77 100644 --- a/androidHyperskillApp/src/main/java/org/hyperskill/app/android/step_quiz_code/view/fragment/CodeStepQuizFragment.kt +++ b/androidHyperskillApp/src/main/java/org/hyperskill/app/android/step_quiz_code/view/fragment/CodeStepQuizFragment.kt @@ -99,7 +99,11 @@ class CodeStepQuizFragment : recyclerView = keyboardExtensionViewBinding.stepQuizCodeKeyboardExtensionRecycler, codeLayout = viewBinding.stepQuizCodeEmbeddedEditor.codeStepLayout, codeToolbarAdapter = requireNotNull(codeToolbarAdapter), - onToolbarSymbolClicked = ::onKeyboardExtensionSymbolClicked, + onToolbarSymbolClicked = { symbol, _ -> + logAnalyticEventMessage( + StepQuizFeature.Message.CodeEditorClickedInputAccessoryButtonEventMessage(symbol) + ) + }, codeEditorKeyboardListener = { isKeyboardShown, toolbarHeight -> if (isResumed) { onKeyboardStateChanged(isKeyboardShown) @@ -177,7 +181,8 @@ class CodeStepQuizFragment : onRetryButtonClicked() } - override fun onKeyboardExtensionSymbolClicked(symbol: String) { + override fun onKeyboardExtensionSymbolClicked(symbol: String, code: String) { + syncReplyState(config.createReply(code)) logAnalyticEventMessage( StepQuizFeature.Message.CodeEditorClickedInputAccessoryButtonEventMessage(symbol) ) diff --git a/androidHyperskillApp/src/main/java/org/hyperskill/app/android/step_quiz_code/view/model/config/SqlCodeStepConfig.kt b/androidHyperskillApp/src/main/java/org/hyperskill/app/android/step_quiz_code/view/model/config/SqlCodeStepConfig.kt index 7fc44f5baf..ce7217cf5e 100644 --- a/androidHyperskillApp/src/main/java/org/hyperskill/app/android/step_quiz_code/view/model/config/SqlCodeStepConfig.kt +++ b/androidHyperskillApp/src/main/java/org/hyperskill/app/android/step_quiz_code/view/model/config/SqlCodeStepConfig.kt @@ -1,7 +1,7 @@ package org.hyperskill.app.android.step_quiz_code.view.model.config -import org.hyperskill.app.android.code.presentation.model.ProgrammingLanguage import org.hyperskill.app.android.step_quiz_code.view.model.CodeDetail +import org.hyperskill.app.code.domain.model.ProgrammingLanguage import org.hyperskill.app.step.domain.model.Step import org.hyperskill.app.step_quiz.domain.model.submissions.Reply import org.hyperskill.app.step_quiz.domain.model.submissions.Submission @@ -9,7 +9,7 @@ import org.hyperskill.app.step_quiz.domain.model.submissions.Submission class SqlCodeStepConfig(private val step: Step) : CodeStepQuizConfig { override val langName: String - get() = ProgrammingLanguage.SQL.serverPrintableName + get() = ProgrammingLanguage.SQL.languageName override val displayedLangName: String? get() = null diff --git a/androidHyperskillApp/src/main/java/org/hyperskill/app/android/step_quiz_fullscreen_code/dialog/CodeStepQuizFullScreenDialogFragment.kt b/androidHyperskillApp/src/main/java/org/hyperskill/app/android/step_quiz_fullscreen_code/dialog/CodeStepQuizFullScreenDialogFragment.kt index c2eb92659c..b9dc5a58f3 100644 --- a/androidHyperskillApp/src/main/java/org/hyperskill/app/android/step_quiz_fullscreen_code/dialog/CodeStepQuizFullScreenDialogFragment.kt +++ b/androidHyperskillApp/src/main/java/org/hyperskill/app/android/step_quiz_fullscreen_code/dialog/CodeStepQuizFullScreenDialogFragment.kt @@ -17,7 +17,6 @@ import by.kirich1409.viewbindingdelegate.viewBinding import com.google.android.material.button.MaterialButton import org.hyperskill.app.android.HyperskillApp import org.hyperskill.app.android.R -import org.hyperskill.app.android.code.presentation.model.ProgrammingLanguage import org.hyperskill.app.android.code.util.CodeEditorKeyboardExtensionUtil import org.hyperskill.app.android.code.view.adapter.CodeToolbarAdapter import org.hyperskill.app.android.code.view.widget.CodeEditorLayout @@ -32,6 +31,7 @@ import org.hyperskill.app.android.step_quiz_code.view.delegate.CodeQuizInstructi import org.hyperskill.app.android.step_quiz_code.view.model.CodeStepQuizConfigFactory import org.hyperskill.app.android.step_quiz_code.view.model.config.CodeStepQuizConfig import org.hyperskill.app.android.step_quiz_fullscreen_code.adapter.CodeStepQuizFullScreenPagerAdapter +import org.hyperskill.app.code.domain.model.ProgrammingLanguage import org.hyperskill.app.step.domain.model.Step import org.hyperskill.app.step_quiz.view.mapper.StepQuizStatsTextMapper import ru.nobird.android.view.base.ui.extension.argument @@ -189,7 +189,8 @@ class CodeStepQuizFullScreenDialogFragment : DialogFragment() { } private fun initViewPager() { - val pagerAdapter = CodeStepQuizFullScreenPagerAdapter(requireContext()) + val pagerAdapter = + CodeStepQuizFullScreenPagerAdapter(requireContext()) viewBinding.fullScreenCodeViewPager.adapter = pagerAdapter viewBinding.fullScreenCodeTabs.setupWithViewPager(viewBinding.fullScreenCodeViewPager) @@ -285,8 +286,8 @@ class CodeStepQuizFullScreenDialogFragment : DialogFragment() { // We show the keyboard extension only when "Code" tab is opened viewBinding.fullScreenCodeViewPager.currentItem == CODE_TAB }, - onToolbarSymbolClicked = { symbol -> - callback?.onKeyboardExtensionSymbolClicked(symbol) + onToolbarSymbolClicked = { symbol, resultCode -> + callback?.onKeyboardExtensionSymbolClicked(symbol, resultCode) }, codeEditorKeyboardListener = { isKeyboardShown, toolbarHeight -> with(codeLayout) { @@ -325,14 +326,14 @@ class CodeStepQuizFullScreenDialogFragment : DialogFragment() { } private fun syncCodeStateWithParent(onSubmitClicked: Boolean = false) { - callback?.onSyncCodeStateWithParent(codeLayout.text.toString(), onSubmitClicked) + callback?.onSyncCodeStateWithParent(codeLayout.text, onSubmitClicked) } interface Callback { fun onSyncCodeStateWithParent(code: String, onSubmitClicked: Boolean = false) fun onResetCodeClick() - fun onKeyboardExtensionSymbolClicked(symbol: String) + fun onKeyboardExtensionSymbolClicked(symbol: String, code: String) } data class Params( @@ -342,7 +343,7 @@ class CodeStepQuizFullScreenDialogFragment : DialogFragment() { val isShowRetryButton: Boolean ) { val titleRes: Int - get() = if (lang == ProgrammingLanguage.SQL.serverPrintableName) { + get() = if (lang == ProgrammingLanguage.SQL.languageName) { org.hyperskill.app.R.string.step_quiz_sql_title } else { org.hyperskill.app.R.string.step_quiz_code_title diff --git a/androidHyperskillApp/src/main/java/org/hyperskill/app/android/step_quiz_parsons/view/mapper/ParsonsLinesMapper.kt b/androidHyperskillApp/src/main/java/org/hyperskill/app/android/step_quiz_parsons/view/mapper/ParsonsLinesMapper.kt index fc00b6aec8..b6b73b6841 100644 --- a/androidHyperskillApp/src/main/java/org/hyperskill/app/android/step_quiz_parsons/view/mapper/ParsonsLinesMapper.kt +++ b/androidHyperskillApp/src/main/java/org/hyperskill/app/android/step_quiz_parsons/view/mapper/ParsonsLinesMapper.kt @@ -3,10 +3,10 @@ package org.hyperskill.app.android.step_quiz_parsons.view.mapper import android.text.Spannable import androidx.core.text.HtmlCompat import org.hyperskill.app.android.code.presentation.highlight.prettify.PrettifyParser -import org.hyperskill.app.android.code.presentation.model.extensionForLanguage import org.hyperskill.app.android.code.view.applyPrettifyParseResults import org.hyperskill.app.android.code.view.model.themes.CodeTheme import org.hyperskill.app.android.step_quiz_parsons.view.model.UiParsonsLine +import org.hyperskill.app.code.domain.model.fileExtensionForLanguage import org.hyperskill.app.step.domain.model.Step import org.hyperskill.app.step_quiz.domain.model.attempts.Attempt import org.hyperskill.app.step_quiz.domain.model.submissions.Submission @@ -93,7 +93,7 @@ class ParsonsLinesMapper( val htmlSpannable = HtmlCompat.fromHtml(text, HtmlCompat.FROM_HTML_MODE_COMPACT) as Spannable val parsedHtmlString = htmlSpannable.toString() val prettifyParseResult = prettifyParser.parse( - /* fileExtension = */ extensionForLanguage(langName), + /* fileExtension = */ fileExtensionForLanguage(langName), /* content = */ parsedHtmlString ) return htmlSpannable.applyPrettifyParseResults( diff --git a/androidHyperskillApp/src/main/res/drawable-hdpi/img_notifications_onboarding.webp b/androidHyperskillApp/src/main/res/drawable-hdpi/img_notifications_onboarding.webp index ca70a55f40..d3921ed16c 100644 Binary files a/androidHyperskillApp/src/main/res/drawable-hdpi/img_notifications_onboarding.webp and b/androidHyperskillApp/src/main/res/drawable-hdpi/img_notifications_onboarding.webp differ diff --git a/androidHyperskillApp/src/main/res/drawable-mdpi/img_notifications_onboarding.webp b/androidHyperskillApp/src/main/res/drawable-mdpi/img_notifications_onboarding.webp index c1f8a82bcf..cbeb30be93 100644 Binary files a/androidHyperskillApp/src/main/res/drawable-mdpi/img_notifications_onboarding.webp and b/androidHyperskillApp/src/main/res/drawable-mdpi/img_notifications_onboarding.webp differ diff --git a/androidHyperskillApp/src/main/res/drawable-night-hdpi/img_notifications_onboarding.webp b/androidHyperskillApp/src/main/res/drawable-night-hdpi/img_notifications_onboarding.webp deleted file mode 100644 index abebf1b6e5..0000000000 Binary files a/androidHyperskillApp/src/main/res/drawable-night-hdpi/img_notifications_onboarding.webp and /dev/null differ diff --git a/androidHyperskillApp/src/main/res/drawable-night-mdpi/img_notifications_onboarding.webp b/androidHyperskillApp/src/main/res/drawable-night-mdpi/img_notifications_onboarding.webp deleted file mode 100644 index 43d7f32c12..0000000000 Binary files a/androidHyperskillApp/src/main/res/drawable-night-mdpi/img_notifications_onboarding.webp and /dev/null differ diff --git a/androidHyperskillApp/src/main/res/drawable-night-xhdpi/img_notifications_onboarding.webp b/androidHyperskillApp/src/main/res/drawable-night-xhdpi/img_notifications_onboarding.webp deleted file mode 100644 index 847c2cd859..0000000000 Binary files a/androidHyperskillApp/src/main/res/drawable-night-xhdpi/img_notifications_onboarding.webp and /dev/null differ diff --git a/androidHyperskillApp/src/main/res/drawable-night-xxhdpi/img_notifications_onboarding.webp b/androidHyperskillApp/src/main/res/drawable-night-xxhdpi/img_notifications_onboarding.webp deleted file mode 100644 index 84487ef354..0000000000 Binary files a/androidHyperskillApp/src/main/res/drawable-night-xxhdpi/img_notifications_onboarding.webp and /dev/null differ diff --git a/androidHyperskillApp/src/main/res/drawable-night-xxxhdpi/img_notifications_onboarding.webp b/androidHyperskillApp/src/main/res/drawable-night-xxxhdpi/img_notifications_onboarding.webp deleted file mode 100644 index f5df6d5396..0000000000 Binary files a/androidHyperskillApp/src/main/res/drawable-night-xxxhdpi/img_notifications_onboarding.webp and /dev/null differ diff --git a/androidHyperskillApp/src/main/res/drawable-xhdpi/img_notifications_onboarding.webp b/androidHyperskillApp/src/main/res/drawable-xhdpi/img_notifications_onboarding.webp index 468779bc5b..e5f60ff0aa 100644 Binary files a/androidHyperskillApp/src/main/res/drawable-xhdpi/img_notifications_onboarding.webp and b/androidHyperskillApp/src/main/res/drawable-xhdpi/img_notifications_onboarding.webp differ diff --git a/androidHyperskillApp/src/main/res/drawable-xxhdpi/img_notifications_onboarding.webp b/androidHyperskillApp/src/main/res/drawable-xxhdpi/img_notifications_onboarding.webp index 45d762e981..bc2917cf21 100644 Binary files a/androidHyperskillApp/src/main/res/drawable-xxhdpi/img_notifications_onboarding.webp and b/androidHyperskillApp/src/main/res/drawable-xxhdpi/img_notifications_onboarding.webp differ diff --git a/androidHyperskillApp/src/main/res/drawable-xxxhdpi/img_notifications_onboarding.webp b/androidHyperskillApp/src/main/res/drawable-xxxhdpi/img_notifications_onboarding.webp index 9e4ada1f78..140738d9ac 100644 Binary files a/androidHyperskillApp/src/main/res/drawable-xxxhdpi/img_notifications_onboarding.webp and b/androidHyperskillApp/src/main/res/drawable-xxxhdpi/img_notifications_onboarding.webp differ diff --git a/androidHyperskillApp/src/main/res/drawable/ic_notification_onboarding_arrow.xml b/androidHyperskillApp/src/main/res/drawable/ic_notification_onboarding_arrow.xml new file mode 100644 index 0000000000..b6d5b3e844 --- /dev/null +++ b/androidHyperskillApp/src/main/res/drawable/ic_notification_onboarding_arrow.xml @@ -0,0 +1,9 @@ + + + diff --git a/androidHyperskillApp/src/main/res/layout/fragment_stage_step_wrapper.xml b/androidHyperskillApp/src/main/res/layout/fragment_stage_step_wrapper.xml index d8a6b9cb08..5ae1f7b733 100644 --- a/androidHyperskillApp/src/main/res/layout/fragment_stage_step_wrapper.xml +++ b/androidHyperskillApp/src/main/res/layout/fragment_stage_step_wrapper.xml @@ -12,12 +12,13 @@ layout="@layout/view_stage_implementation_appbar" /> + app:layout_constraintBottom_toTopOf="@+id/stepQuizFillBlanksOptionsContainer"> + layout="@layout/layout_step_quiz_fill_blanks_options" + android:visibility="gone" + tools:visibility="visible"/> \ No newline at end of file diff --git a/androidHyperskillApp/src/main/res/layout/layout_step_quiz_code_keyboard_extension.xml b/androidHyperskillApp/src/main/res/layout/layout_step_quiz_code_keyboard_extension.xml index b0d6cefd25..0e2c341453 100644 --- a/androidHyperskillApp/src/main/res/layout/layout_step_quiz_code_keyboard_extension.xml +++ b/androidHyperskillApp/src/main/res/layout/layout_step_quiz_code_keyboard_extension.xml @@ -1,7 +1,8 @@ \ No newline at end of file + app:layout_constraintBottom_toBottomOf="parent"/> \ No newline at end of file diff --git a/gradle/app.versions.toml b/gradle/app.versions.toml index 09956f6ccd..1f6788f0d0 100644 --- a/gradle/app.versions.toml +++ b/gradle/app.versions.toml @@ -2,5 +2,5 @@ minSdk = '24' targetSdk = '33' compileSdk = '33' -versionName = '1.45' -versionCode = '271' \ No newline at end of file +versionName = '1.46' +versionCode = '285' \ No newline at end of file diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index a6b63f331a..f0191c8c00 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,13 +1,14 @@ [versions] -androidGradlePlugin = '7.2.2' +androidGradlePlugin = "7.4.2" kotlin = '1.8.22' kotlinCoroutines = '1.7.2' ktor = '2.3.3' -mokoResources = "0.20.1" +mokoResources = "0.23.0" mokoKswift = "0.6.1" ktlintRules = '1.0.0' adapters = '1.1.1' -sentry = '6.12.1' +sentry = '7.1.0' +sentryAndroidGradlePlugin = '4.1.0' fragment = '1.5.7' flipper = '0.148.0' dokka = '1.7.20' @@ -61,7 +62,7 @@ multiplatform-settings = { module = "com.russhwolf:multiplatform-settings", vers kermit-common = { module = "co.touchlab:kermit", version.ref = "kermit" } android-ui-material = { module = "com.google.android.material:material", version = "1.4.0" } -android-ui-appcompat = { module = "androidx.appcompat:appcompat", version = "1.4.0" } +android-ui-appcompat = { module = "androidx.appcompat:appcompat", version = "1.6.1" } android-ui-constraintlayout = { module = "androidx.constraintlayout:constraintlayout", version = "2.1.4" } android-ui-swiperefreshlayout = { module = "androidx.swiperefreshlayout:swiperefreshlayout", version = "1.1.0" } android-ui-flexbox = { module = "com.google.android.flexbox:flexbox", version = "3.0.0" } @@ -135,4 +136,4 @@ play-servises = ['gms-play-services', 'gms-play-login'] firebase = ['firebase-bom', 'firebase-messaging'] [plugins] -sentry-androidGradle = { id = "io.sentry.android.gradle", version = "3.11.0" } \ No newline at end of file +sentry-androidGradle = { id = "io.sentry.android.gradle", version.ref = "sentryAndroidGradlePlugin" } \ No newline at end of file diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar index f6b961fd5a..249e5832f0 100644 Binary files a/gradle/wrapper/gradle-wrapper.jar and b/gradle/wrapper/gradle-wrapper.jar differ diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index ada848a9bb..7e62b17aca 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,5 @@ -#Wed Nov 10 21:02:04 MSK 2021 distributionBase=GRADLE_USER_HOME -distributionUrl=https\://services.gradle.org/distributions/gradle-7.3.3-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-7.5-bin.zip distributionPath=wrapper/dists zipStorePath=wrapper/dists zipStoreBase=GRADLE_USER_HOME diff --git a/gradlew b/gradlew index 4f906e0c81..a69d9cb6c2 100755 --- a/gradlew +++ b/gradlew @@ -1,7 +1,7 @@ -#!/usr/bin/env sh +#!/bin/sh # -# Copyright 2015 the original author or authors. +# Copyright © 2015-2021 the original authors. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -17,67 +17,101 @@ # ############################################################################## -## -## Gradle start up script for UN*X -## +# +# Gradle start up script for POSIX generated by Gradle. +# +# Important for running: +# +# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is +# noncompliant, but you have some other compliant shell such as ksh or +# bash, then to run this script, type that shell name before the whole +# command line, like: +# +# ksh Gradle +# +# Busybox and similar reduced shells will NOT work, because this script +# requires all of these POSIX shell features: +# * functions; +# * expansions «$var», «${var}», «${var:-default}», «${var+SET}», +# «${var#prefix}», «${var%suffix}», and «$( cmd )»; +# * compound commands having a testable exit status, especially «case»; +# * various built-in commands including «command», «set», and «ulimit». +# +# Important for patching: +# +# (2) This script targets any POSIX shell, so it avoids extensions provided +# by Bash, Ksh, etc; in particular arrays are avoided. +# +# The "traditional" practice of packing multiple parameters into a +# space-separated string is a well documented source of bugs and security +# problems, so this is (mostly) avoided, by progressively accumulating +# options in "$@", and eventually passing that to Java. +# +# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, +# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; +# see the in-line comments for details. +# +# There are tweaks for specific operating systems such as AIX, CygWin, +# Darwin, MinGW, and NonStop. +# +# (3) This script is generated from the Groovy template +# https://github.com/gradle/gradle/blob/master/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt +# within the Gradle project. +# +# You can find Gradle at https://github.com/gradle/gradle/. +# ############################################################################## # Attempt to set APP_HOME + # Resolve links: $0 may be a link -PRG="$0" -# Need this for relative symlinks. -while [ -h "$PRG" ] ; do - ls=`ls -ld "$PRG"` - link=`expr "$ls" : '.*-> \(.*\)$'` - if expr "$link" : '/.*' > /dev/null; then - PRG="$link" - else - PRG=`dirname "$PRG"`"/$link" - fi +app_path=$0 + +# Need this for daisy-chained symlinks. +while + APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path + [ -h "$app_path" ] +do + ls=$( ls -ld "$app_path" ) + link=${ls#*' -> '} + case $link in #( + /*) app_path=$link ;; #( + *) app_path=$APP_HOME$link ;; + esac done -SAVED="`pwd`" -cd "`dirname \"$PRG\"`/" >/dev/null -APP_HOME="`pwd -P`" -cd "$SAVED" >/dev/null + +APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit APP_NAME="Gradle" -APP_BASE_NAME=`basename "$0"` +APP_BASE_NAME=${0##*/} # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' # Use the maximum available, or set MAX_FD != -1 to use that value. -MAX_FD="maximum" +MAX_FD=maximum warn () { echo "$*" -} +} >&2 die () { echo echo "$*" echo exit 1 -} +} >&2 # OS specific support (must be 'true' or 'false'). cygwin=false msys=false darwin=false nonstop=false -case "`uname`" in - CYGWIN* ) - cygwin=true - ;; - Darwin* ) - darwin=true - ;; - MINGW* ) - msys=true - ;; - NONSTOP* ) - nonstop=true - ;; +case "$( uname )" in #( + CYGWIN* ) cygwin=true ;; #( + Darwin* ) darwin=true ;; #( + MSYS* | MINGW* ) msys=true ;; #( + NONSTOP* ) nonstop=true ;; esac CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar @@ -87,9 +121,9 @@ CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar if [ -n "$JAVA_HOME" ] ; then if [ -x "$JAVA_HOME/jre/sh/java" ] ; then # IBM's JDK on AIX uses strange locations for the executables - JAVACMD="$JAVA_HOME/jre/sh/java" + JAVACMD=$JAVA_HOME/jre/sh/java else - JAVACMD="$JAVA_HOME/bin/java" + JAVACMD=$JAVA_HOME/bin/java fi if [ ! -x "$JAVACMD" ] ; then die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME @@ -98,7 +132,7 @@ Please set the JAVA_HOME variable in your environment to match the location of your Java installation." fi else - JAVACMD="java" + JAVACMD=java which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. Please set the JAVA_HOME variable in your environment to match the @@ -106,80 +140,101 @@ location of your Java installation." fi # Increase the maximum file descriptors if we can. -if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then - MAX_FD_LIMIT=`ulimit -H -n` - if [ $? -eq 0 ] ; then - if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then - MAX_FD="$MAX_FD_LIMIT" - fi - ulimit -n $MAX_FD - if [ $? -ne 0 ] ; then - warn "Could not set maximum file descriptor limit: $MAX_FD" - fi - else - warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" - fi +if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then + case $MAX_FD in #( + max*) + MAX_FD=$( ulimit -H -n ) || + warn "Could not query maximum file descriptor limit" + esac + case $MAX_FD in #( + '' | soft) :;; #( + *) + ulimit -n "$MAX_FD" || + warn "Could not set maximum file descriptor limit to $MAX_FD" + esac fi -# For Darwin, add options to specify how the application appears in the dock -if $darwin; then - GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" -fi +# Collect all arguments for the java command, stacking in reverse order: +# * args from the command line +# * the main class name +# * -classpath +# * -D...appname settings +# * --module-path (only if needed) +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. # For Cygwin or MSYS, switch paths to Windows format before running java -if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then - APP_HOME=`cygpath --path --mixed "$APP_HOME"` - CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` - - JAVACMD=`cygpath --unix "$JAVACMD"` - - # We build the pattern for arguments to be converted via cygpath - ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` - SEP="" - for dir in $ROOTDIRSRAW ; do - ROOTDIRS="$ROOTDIRS$SEP$dir" - SEP="|" - done - OURCYGPATTERN="(^($ROOTDIRS))" - # Add a user-defined pattern to the cygpath arguments - if [ "$GRADLE_CYGPATTERN" != "" ] ; then - OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" - fi +if "$cygwin" || "$msys" ; then + APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) + CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) + + JAVACMD=$( cygpath --unix "$JAVACMD" ) + # Now convert the arguments - kludge to limit ourselves to /bin/sh - i=0 - for arg in "$@" ; do - CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` - CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option - - if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition - eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` - else - eval `echo args$i`="\"$arg\"" + for arg do + if + case $arg in #( + -*) false ;; # don't mess with options #( + /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath + [ -e "$t" ] ;; #( + *) false ;; + esac + then + arg=$( cygpath --path --ignore --mixed "$arg" ) fi - i=`expr $i + 1` + # Roll the args list around exactly as many times as the number of + # args, so each arg winds up back in the position where it started, but + # possibly modified. + # + # NB: a `for` loop captures its iteration list before it begins, so + # changing the positional parameters here affects neither the number of + # iterations, nor the values presented in `arg`. + shift # remove old arg + set -- "$@" "$arg" # push replacement arg done - case $i in - 0) set -- ;; - 1) set -- "$args0" ;; - 2) set -- "$args0" "$args1" ;; - 3) set -- "$args0" "$args1" "$args2" ;; - 4) set -- "$args0" "$args1" "$args2" "$args3" ;; - 5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; - 6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; - 7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; - 8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; - 9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; - esac fi -# Escape application args -save () { - for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done - echo " " -} -APP_ARGS=`save "$@"` +# Collect all arguments for the java command; +# * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of +# shell script including quotes and variable substitutions, so put them in +# double quotes to make sure that they get re-expanded; and +# * put everything else in single quotes, so that it's not re-expanded. + +set -- \ + "-Dorg.gradle.appname=$APP_BASE_NAME" \ + -classpath "$CLASSPATH" \ + org.gradle.wrapper.GradleWrapperMain \ + "$@" + +# Stop when "xargs" is not available. +if ! command -v xargs >/dev/null 2>&1 +then + die "xargs is not available" +fi + +# Use "xargs" to parse quoted args. +# +# With -n1 it outputs one arg per line, with the quotes and backslashes removed. +# +# In Bash we could simply go: +# +# readarray ARGS < <( xargs -n1 <<<"$var" ) && +# set -- "${ARGS[@]}" "$@" +# +# but POSIX shell has neither arrays nor command substitution, so instead we +# post-process each arg (as a line of input to sed) to backslash-escape any +# character that might be a shell metacharacter, then use eval to reverse +# that process (while maintaining the separation between arguments), and wrap +# the whole thing up as a single "set" statement. +# +# This will of course break if any of these variables contains a newline or +# an unmatched quote. +# -# Collect all arguments for the java command, following the shell quoting and substitution rules -eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" +eval "set -- $( + printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | + xargs -n1 | + sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | + tr '\n' ' ' + )" '"$@"' exec "$JAVACMD" "$@" diff --git a/gradlew.bat b/gradlew.bat index ac1b06f938..53a6b238d4 100644 --- a/gradlew.bat +++ b/gradlew.bat @@ -14,7 +14,7 @@ @rem limitations under the License. @rem -@if "%DEBUG%" == "" @echo off +@if "%DEBUG%"=="" @echo off @rem ########################################################################## @rem @rem Gradle startup script for Windows @@ -25,7 +25,7 @@ if "%OS%"=="Windows_NT" setlocal set DIRNAME=%~dp0 -if "%DIRNAME%" == "" set DIRNAME=. +if "%DIRNAME%"=="" set DIRNAME=. set APP_BASE_NAME=%~n0 set APP_HOME=%DIRNAME% @@ -40,7 +40,7 @@ if defined JAVA_HOME goto findJavaFromJavaHome set JAVA_EXE=java.exe %JAVA_EXE% -version >NUL 2>&1 -if "%ERRORLEVEL%" == "0" goto execute +if %ERRORLEVEL% equ 0 goto execute echo. echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. @@ -75,13 +75,15 @@ set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar :end @rem End local scope for the variables with windows NT shell -if "%ERRORLEVEL%"=="0" goto mainEnd +if %ERRORLEVEL% equ 0 goto mainEnd :fail rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of rem the _cmd.exe /c_ return code! -if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 -exit /b 1 +set EXIT_CODE=%ERRORLEVEL% +if %EXIT_CODE% equ 0 set EXIT_CODE=1 +if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% +exit /b %EXIT_CODE% :mainEnd if "%OS%"=="Windows_NT" endlocal diff --git a/iosHyperskillApp/NotificationServiceExtension/Info.plist b/iosHyperskillApp/NotificationServiceExtension/Info.plist index 6b3a7161ec..b7c9b19a00 100644 --- a/iosHyperskillApp/NotificationServiceExtension/Info.plist +++ b/iosHyperskillApp/NotificationServiceExtension/Info.plist @@ -9,9 +9,9 @@ CFBundleIdentifier $(PRODUCT_BUNDLE_IDENTIFIER) CFBundleVersion - 272 + 285 CFBundleShortVersionString - 1.45 + 1.46 CFBundlePackageType $(PRODUCT_BUNDLE_PACKAGE_TYPE) CFBundleExecutable diff --git a/iosHyperskillApp/Podfile b/iosHyperskillApp/Podfile index 41cb278328..cc06ccbe88 100644 --- a/iosHyperskillApp/Podfile +++ b/iosHyperskillApp/Podfile @@ -10,7 +10,7 @@ target "iosHyperskillApp" do pod "shared", :path => "../shared" pod "SwiftLint", "0.53.0" - pod "Sentry", "8.14.2" + pod "Sentry", "8.17.2" # Firebase pod "Firebase/CoreOnly", "10.17.0" diff --git a/iosHyperskillApp/Podfile.lock b/iosHyperskillApp/Podfile.lock index efb3bd0a71..34ec506632 100644 --- a/iosHyperskillApp/Podfile.lock +++ b/iosHyperskillApp/Podfile.lock @@ -83,12 +83,12 @@ PODS: - Nuke (~> 10.5) - PanModal (1.2.7) - PromisesObjC (2.3.1) - - Sentry (8.14.2): - - Sentry/Core (= 8.14.2) - - SentryPrivate (= 8.14.2) - - Sentry/Core (8.14.2): - - SentryPrivate (= 8.14.2) - - SentryPrivate (8.14.2) + - Sentry (8.17.2): + - Sentry/Core (= 8.17.2) + - SentryPrivate (= 8.17.2) + - Sentry/Core (8.17.2): + - SentryPrivate (= 8.17.2) + - SentryPrivate (8.17.2) - shared (1.0) - SkeletonUI (1.0.11) - SnapKit (5.6.0) @@ -111,7 +111,7 @@ DEPENDENCIES: - lottie-ios (= 4.3.3) - NukeUI (from `https://github.com/kean/NukeUI.git`, tag `0.8.3`) - PanModal (from `https://github.com/ivan-magda/PanModal.git`, branch `remove-presenting-appearance-transitions`) - - Sentry (= 8.14.2) + - Sentry (= 8.17.2) - shared (from `../shared`) - SkeletonUI (= 1.0.11) - SnapKit (= 5.6.0) @@ -213,8 +213,8 @@ SPEC CHECKSUMS: NukeUI: 3c7ec7b299dd99707afdc783b436f39768b4493b PanModal: 3e16ead1a907fb06f4df3f13492fd00149fa4974 PromisesObjC: c50d2056b5253dadbd6c2bea79b0674bd5a52fa4 - Sentry: e0ea366f95ebb68f26d6030d8c22d6b2e6d23dd0 - SentryPrivate: 949a21fa59872427edc73b524c3ec8456761d97f + Sentry: 64a9f9c3637af913adcf53deced05bbe452d1410 + SentryPrivate: 024c6fed507ac39ae98e6d087034160f942920d5 shared: 210f065b47c10083f8ccc49e665dec6d32c5b2d3 SkeletonUI: a5514a3877d39f28229c852a567660d0f7542330 SnapKit: e01d52ebb8ddbc333eefe2132acf85c8227d9c25 @@ -223,6 +223,6 @@ SPEC CHECKSUMS: SVProgressHUD: 1428aafac632c1f86f62aa4243ec12008d7a51d6 SwiftLint: 5ce4d6a8ff83f1b5fd5ad5dbf30965d35af65e44 -PODFILE CHECKSUM: 5b9e11f6c950f7555e8c01422a68067fdaf449d9 +PODFILE CHECKSUM: 1613f08afc0d23eb5c1805973d23b2f5c885203e COCOAPODS: 1.14.3 diff --git a/iosHyperskillApp/fastlane/Devicefile b/iosHyperskillApp/fastlane/Devicefile index d15e96e4fb..0c05630236 100644 Binary files a/iosHyperskillApp/fastlane/Devicefile and b/iosHyperskillApp/fastlane/Devicefile differ diff --git a/iosHyperskillApp/iosHyperskillApp.xcodeproj/project.pbxproj b/iosHyperskillApp/iosHyperskillApp.xcodeproj/project.pbxproj index 9ed31c76ef..501294a5f9 100644 --- a/iosHyperskillApp/iosHyperskillApp.xcodeproj/project.pbxproj +++ b/iosHyperskillApp/iosHyperskillApp.xcodeproj/project.pbxproj @@ -153,9 +153,8 @@ 2C41127828376F6D004948A3 /* StepQuizDiscussionsButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2C41127728376F6D004948A3 /* StepQuizDiscussionsButton.swift */; }; 2C42EFA827F2C03D00B4648B /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = 2C42EFAA27F2C03D00B4648B /* Localizable.strings */; }; 2C42EFAD27F2C29500B4648B /* Strings.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2C42EFAC27F2C29500B4648B /* Strings.swift */; }; - 2C42EFB327F2EA7B00B4648B /* ResourcesStringResource+Localized.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2C42EFB227F2EA7B00B4648B /* ResourcesStringResource+Localized.swift */; }; + 2C42EFB327F2EA7B00B4648B /* StringResource+Localized.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2C42EFB227F2EA7B00B4648B /* StringResource+Localized.swift */; }; 2C42EFB527F2EAB300B4648B /* GraphicsColor+UIColor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2C42EFB427F2EAB300B4648B /* GraphicsColor+UIColor.swift */; }; - 2C42EFB727F2EAF800B4648B /* ResourcesColorResourceThemed+UIColor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2C42EFB627F2EAF800B4648B /* ResourcesColorResourceThemed+UIColor.swift */; }; 2C43CDF928B55CC600E74762 /* HyperskillLogoView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2C43CDF828B55CC600E74762 /* HyperskillLogoView.swift */; }; 2C43CDFB28B56AE600E74762 /* AuthPreviews.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2C43CDFA28B56AE600E74762 /* AuthPreviews.swift */; }; 2C45E7BD2A0FD9D600DFF32D /* ProjectSelectionListGridCellProjectLevelView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2C45E7BC2A0FD9D600DFF32D /* ProjectSelectionListGridCellProjectLevelView.swift */; }; @@ -446,6 +445,7 @@ 2CE31F4D27F1E0C8008EEE66 /* AppAssembly.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2CE31F4C27F1E0C8008EEE66 /* AppAssembly.swift */; }; 2CE58C5A2B07662300E5EBBE /* ChallengeWidgetContentStateProgressGridView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2CE58C592B07662300E5EBBE /* ChallengeWidgetContentStateProgressGridView.swift */; }; 2CE58C5C2B0768F300E5EBBE /* ChallengeWidgetContentStateProgressGridItemView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2CE58C5B2B0768F300E5EBBE /* ChallengeWidgetContentStateProgressGridItemView.swift */; }; + 2CE601362B3345DD00E9CC46 /* ColorResource+UIColor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2CE601352B3345DD00E9CC46 /* ColorResource+UIColor.swift */; }; 2CE7B4842AB0593F00DCBE4D /* AttributedTextLabelWrapper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2CE7B4832AB0593F00DCBE4D /* AttributedTextLabelWrapper.swift */; }; 2CE7B4872AB05D0400DCBE4D /* StepQuizParsonsViewDataMapper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2CE7B4862AB05D0400DCBE4D /* StepQuizParsonsViewDataMapper.swift */; }; 2CE7B48A2AB0973D00DCBE4D /* HTMLString.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2CE7B4892AB0973D00DCBE4D /* HTMLString.swift */; }; @@ -821,9 +821,8 @@ 2C41127728376F6D004948A3 /* StepQuizDiscussionsButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StepQuizDiscussionsButton.swift; sourceTree = ""; }; 2C42EFA927F2C03D00B4648B /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/Localizable.strings; sourceTree = ""; }; 2C42EFAC27F2C29500B4648B /* Strings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Strings.swift; sourceTree = ""; }; - 2C42EFB227F2EA7B00B4648B /* ResourcesStringResource+Localized.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ResourcesStringResource+Localized.swift"; sourceTree = ""; }; + 2C42EFB227F2EA7B00B4648B /* StringResource+Localized.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "StringResource+Localized.swift"; sourceTree = ""; }; 2C42EFB427F2EAB300B4648B /* GraphicsColor+UIColor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "GraphicsColor+UIColor.swift"; sourceTree = ""; }; - 2C42EFB627F2EAF800B4648B /* ResourcesColorResourceThemed+UIColor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ResourcesColorResourceThemed+UIColor.swift"; sourceTree = ""; }; 2C43CDF828B55CC600E74762 /* HyperskillLogoView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HyperskillLogoView.swift; sourceTree = ""; }; 2C43CDFA28B56AE600E74762 /* AuthPreviews.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AuthPreviews.swift; sourceTree = ""; }; 2C45E7BC2A0FD9D600DFF32D /* ProjectSelectionListGridCellProjectLevelView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProjectSelectionListGridCellProjectLevelView.swift; sourceTree = ""; }; @@ -1118,6 +1117,7 @@ 2CE31F4C27F1E0C8008EEE66 /* AppAssembly.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppAssembly.swift; sourceTree = ""; }; 2CE58C592B07662300E5EBBE /* ChallengeWidgetContentStateProgressGridView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChallengeWidgetContentStateProgressGridView.swift; sourceTree = ""; }; 2CE58C5B2B0768F300E5EBBE /* ChallengeWidgetContentStateProgressGridItemView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChallengeWidgetContentStateProgressGridItemView.swift; sourceTree = ""; }; + 2CE601352B3345DD00E9CC46 /* ColorResource+UIColor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ColorResource+UIColor.swift"; sourceTree = ""; }; 2CE7B4832AB0593F00DCBE4D /* AttributedTextLabelWrapper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AttributedTextLabelWrapper.swift; sourceTree = ""; }; 2CE7B4862AB05D0400DCBE4D /* StepQuizParsonsViewDataMapper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StepQuizParsonsViewDataMapper.swift; sourceTree = ""; }; 2CE7B4892AB0973D00DCBE4D /* HTMLString.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HTMLString.swift; sourceTree = ""; }; @@ -2067,9 +2067,9 @@ 2C42EFB127F2EA5000B4648B /* MokoResources */ = { isa = PBXGroup; children = ( + 2CE601352B3345DD00E9CC46 /* ColorResource+UIColor.swift */, 2C42EFB427F2EAB300B4648B /* GraphicsColor+UIColor.swift */, - 2C42EFB627F2EAF800B4648B /* ResourcesColorResourceThemed+UIColor.swift */, - 2C42EFB227F2EA7B00B4648B /* ResourcesStringResource+Localized.swift */, + 2C42EFB227F2EA7B00B4648B /* StringResource+Localized.swift */, ); path = MokoResources; sourceTree = ""; @@ -4366,7 +4366,7 @@ E9A3435029113FA400E0C0A4 /* StepQuizHintReactionButtonView.swift in Sources */, 2C05AC512A0EACF50039C7EF /* ProjectSelectionListSkeletonView.swift in Sources */, 2CAE8CF0280525BE00E6C83D /* StepViewModel.swift in Sources */, - 2C42EFB327F2EA7B00B4648B /* ResourcesStringResource+Localized.swift in Sources */, + 2C42EFB327F2EA7B00B4648B /* StringResource+Localized.swift in Sources */, 2C2FD622281920B1004E7AF6 /* SentryInfo.swift in Sources */, 2C0DB91C28645ADA001EA35E /* CodeCompletionKeywords.swift in Sources */, 2CA8E093281039EB00154088 /* OutlineButtonStyle.swift in Sources */, @@ -4386,6 +4386,7 @@ 2CBFB94A28897DBB0044D1BA /* StepQuizCodeFullScreenView.swift in Sources */, 2C0DB90A2864515B001EA35E /* CodeEditorViewDelegate.swift in Sources */, 2C336D132865C47900C91342 /* ApplicationTheme.swift in Sources */, + 2CE601362B3345DD00E9CC46 /* ColorResource+UIColor.swift in Sources */, E9AB310F29DECC7500645376 /* StudyPlanSectionHeaderStatisticsView.swift in Sources */, 2C7CB66D2ADFB951006F78DA /* StepQuizFillBlanksViewModel.swift in Sources */, E99B21812887E535006A6154 /* StepQuizSkeletonViewFactory.swift in Sources */, @@ -4824,7 +4825,6 @@ 2C25BFD52851F8F00036C689 /* UIColor+DesignSystem.swift in Sources */, 2C023C8D285DCA4300D2D5A9 /* DatasetExtensions.swift in Sources */, E91017152832975C002E70F5 /* CheckboxButton.swift in Sources */, - 2C42EFB727F2EAF800B4648B /* ResourcesColorResourceThemed+UIColor.swift in Sources */, 2C99B1002A14255F0018627B /* StudyPlanWidgetViewStateSectionItemStateWrapper.swift in Sources */, E9E964872A0B8D8200841DF6 /* StepQuizProblemsLimitView.swift in Sources */, 2C8CD9AE2994EFC5008DC09D /* DebugFeatureViewStateKsExtensions.swift in Sources */, @@ -4897,7 +4897,7 @@ buildSettings = { CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 272; + CURRENT_PROJECT_VERSION = 285; DEBUG_INFORMATION_FORMAT = dwarf; DEVELOPMENT_TEAM = 3DWS674B2M; GENERATE_INFOPLIST_FILE = NO; @@ -4918,7 +4918,7 @@ buildSettings = { CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 272; + CURRENT_PROJECT_VERSION = 285; DEVELOPMENT_TEAM = 3DWS674B2M; GENERATE_INFOPLIST_FILE = NO; INFOPLIST_FILE = iosHyperskillAppUITests/Info.plist; @@ -4939,7 +4939,7 @@ BUNDLE_LOADER = "$(TEST_HOST)"; CLANG_CXX_LANGUAGE_STANDARD = "gnu++17"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 272; + CURRENT_PROJECT_VERSION = 285; DEBUG_INFORMATION_FORMAT = dwarf; DEVELOPMENT_TEAM = 3DWS674B2M; INFOPLIST_FILE = iosHyperskillAppTests/Info.plist; @@ -4960,7 +4960,7 @@ BUNDLE_LOADER = "$(TEST_HOST)"; CLANG_CXX_LANGUAGE_STANDARD = "gnu++17"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 272; + CURRENT_PROJECT_VERSION = 285; DEVELOPMENT_TEAM = 3DWS674B2M; INFOPLIST_FILE = iosHyperskillAppTests/Info.plist; IPHONEOS_DEPLOYMENT_TARGET = 14.0; @@ -4981,7 +4981,7 @@ CODE_SIGN_ENTITLEMENTS = NotificationServiceExtension/NotificationServiceExtension.entitlements; CODE_SIGN_IDENTITY = "iPhone Developer"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 272; + CURRENT_PROJECT_VERSION = 285; DEBUG_INFORMATION_FORMAT = dwarf; DEVELOPMENT_TEAM = 3DWS674B2M; INFOPLIST_FILE = NotificationServiceExtension/Info.plist; @@ -5009,7 +5009,7 @@ CODE_SIGN_ENTITLEMENTS = NotificationServiceExtension/NotificationServiceExtension.entitlements; CODE_SIGN_IDENTITY = "iPhone Developer"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 272; + CURRENT_PROJECT_VERSION = 285; DEVELOPMENT_TEAM = 3DWS674B2M; INFOPLIST_FILE = NotificationServiceExtension/Info.plist; IPHONEOS_DEPLOYMENT_TARGET = 14.0; @@ -5154,7 +5154,7 @@ CODE_SIGN_ENTITLEMENTS = iosHyperskillApp/iosHyperskillApp.entitlements; CODE_SIGN_IDENTITY = "iPhone Developer"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 272; + CURRENT_PROJECT_VERSION = 285; DEVELOPMENT_ASSET_PATHS = "\"iosHyperskillApp/Preview Content\""; DEVELOPMENT_TEAM = 3DWS674B2M; ENABLE_PREVIEWS = YES; @@ -5190,7 +5190,7 @@ CODE_SIGN_ENTITLEMENTS = iosHyperskillApp/iosHyperskillApp.entitlements; CODE_SIGN_IDENTITY = "iPhone Developer"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 272; + CURRENT_PROJECT_VERSION = 285; DEVELOPMENT_ASSET_PATHS = "\"iosHyperskillApp/Preview Content\""; DEVELOPMENT_TEAM = 3DWS674B2M; ENABLE_PREVIEWS = YES; diff --git a/iosHyperskillApp/iosHyperskillApp/Info.plist b/iosHyperskillApp/iosHyperskillApp/Info.plist index 2e1c46f557..9a2c2456f2 100644 --- a/iosHyperskillApp/iosHyperskillApp/Info.plist +++ b/iosHyperskillApp/iosHyperskillApp/Info.plist @@ -23,7 +23,7 @@ CFBundlePackageType $(PRODUCT_BUNDLE_PACKAGE_TYPE) CFBundleShortVersionString - 1.45 + 1.46 CFBundleURLTypes @@ -36,7 +36,7 @@ CFBundleVersion - 272 + 285 FirebaseAppDelegateProxyEnabled FirebaseMessagingAutoInitEnabled diff --git a/iosHyperskillApp/iosHyperskillApp/Sources/Extensions/ThirdParty/MokoResources/ColorResource+UIColor.swift b/iosHyperskillApp/iosHyperskillApp/Sources/Extensions/ThirdParty/MokoResources/ColorResource+UIColor.swift new file mode 100644 index 0000000000..20484be3c1 --- /dev/null +++ b/iosHyperskillApp/iosHyperskillApp/Sources/Extensions/ThirdParty/MokoResources/ColorResource+UIColor.swift @@ -0,0 +1,20 @@ +import shared +import UIKit + +extension shared.ColorResource { + var uiColor: UIColor { + switch self.asColorDesc() { + case let descResource as ColorDescResource: + descResource.resource.getUIColor() // Color from the named asset + case let singleColor as ColorDescSingle: + singleColor.color.uiColor + case let themedColor as ColorDescThemed: + UIColor.dynamic( + light: themedColor.lightColor.uiColor, + dark: themedColor.darkColor.uiColor + ) + default: + fatalError("Unknown color description for name = \(self.name)") + } + } +} diff --git a/iosHyperskillApp/iosHyperskillApp/Sources/Extensions/ThirdParty/MokoResources/ResourcesColorResourceThemed+UIColor.swift b/iosHyperskillApp/iosHyperskillApp/Sources/Extensions/ThirdParty/MokoResources/ResourcesColorResourceThemed+UIColor.swift deleted file mode 100644 index 11231474f4..0000000000 --- a/iosHyperskillApp/iosHyperskillApp/Sources/Extensions/ThirdParty/MokoResources/ResourcesColorResourceThemed+UIColor.swift +++ /dev/null @@ -1,8 +0,0 @@ -import shared -import UIKit - -extension ResourcesColorResource.Themed { - var dynamicUIColor: UIColor { - UIColor.dynamic(light: self.light.uiColor, dark: self.dark.uiColor) - } -} diff --git a/iosHyperskillApp/iosHyperskillApp/Sources/Extensions/ThirdParty/MokoResources/ResourcesStringResource+Localized.swift b/iosHyperskillApp/iosHyperskillApp/Sources/Extensions/ThirdParty/MokoResources/StringResource+Localized.swift similarity index 85% rename from iosHyperskillApp/iosHyperskillApp/Sources/Extensions/ThirdParty/MokoResources/ResourcesStringResource+Localized.swift rename to iosHyperskillApp/iosHyperskillApp/Sources/Extensions/ThirdParty/MokoResources/StringResource+Localized.swift index 34363b7b9b..bd5da0a4ea 100644 --- a/iosHyperskillApp/iosHyperskillApp/Sources/Extensions/ThirdParty/MokoResources/ResourcesStringResource+Localized.swift +++ b/iosHyperskillApp/iosHyperskillApp/Sources/Extensions/ThirdParty/MokoResources/StringResource+Localized.swift @@ -1,7 +1,7 @@ import Foundation import shared -extension ResourcesStringResource { +extension shared.StringResource { func localized() -> String { // swiftlint:disable:next nslocalizedstring_key NSLocalizedString(self.resourceId, bundle: self.bundle, comment: "") diff --git a/iosHyperskillApp/iosHyperskillApp/Sources/Modules/StepQuizSubmodules/StepQuizFillBlanks/Views/FillBlanksQuizViewWrapper.swift b/iosHyperskillApp/iosHyperskillApp/Sources/Modules/StepQuizSubmodules/StepQuizFillBlanks/Views/FillBlanksQuizViewWrapper.swift index 0406575aa0..b47237162c 100644 --- a/iosHyperskillApp/iosHyperskillApp/Sources/Modules/StepQuizSubmodules/StepQuizFillBlanks/Views/FillBlanksQuizViewWrapper.swift +++ b/iosHyperskillApp/iosHyperskillApp/Sources/Modules/StepQuizSubmodules/StepQuizFillBlanks/Views/FillBlanksQuizViewWrapper.swift @@ -38,6 +38,14 @@ struct FillBlanksQuizViewWrapper: UIViewRepresentable { delegate: collectionViewAdapter, dataSource: collectionViewAdapter ) + + // ALTAPPS-1062: Fixes incorrect collection view layout on first update + if context.coordinator.isFirstCollectionViewDataUpdate { + context.coordinator.isFirstCollectionViewDataUpdate = false + DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) { + uiView.invalidateCollectionViewLayout() + } + } } context.coordinator.onInputDidChange = { [weak collectionViewAdapter, weak uiView] inputText, component in @@ -76,6 +84,8 @@ extension FillBlanksQuizViewWrapper { class Coordinator: NSObject, FillBlanksQuizCollectionViewAdapterDelegate { private(set) var collectionViewAdapter = FillBlanksQuizCollectionViewAdapter() + fileprivate var isFirstCollectionViewDataUpdate = true + var onInputDidChange: ((String, StepQuizFillBlankComponent) -> Void)? var onDidSelectComponent: ((IndexPath) -> Void)? diff --git a/iosHyperskillApp/iosHyperskillApp/Sources/Systems/Sentry/SentryManager.swift b/iosHyperskillApp/iosHyperskillApp/Sources/Systems/Sentry/SentryManager.swift index 4c02f50e1f..b576fdef67 100644 --- a/iosHyperskillApp/iosHyperskillApp/Sources/Systems/Sentry/SentryManager.swift +++ b/iosHyperskillApp/iosHyperskillApp/Sources/Systems/Sentry/SentryManager.swift @@ -16,18 +16,15 @@ final class SentryManager: shared.SentryManager { options.dsn = SentryInfo.dsn options.environment = "\(ApplicationInfo.flavor)-\(BuildVariant.current.value)" - - let userAgentInfo = UserAgentBuilder.userAgentInfo - options.releaseName = "\(userAgentInfo.versionName) (\(userAgentInfo.versionCode))" + options.releaseName = Self.makeReleaseName() #if DEBUG options.debug = true - options.diagnosticLevel = .debug + options.diagnosticLevel = .info options.tracesSampleRate = 1 #else options.tracesSampleRate = 0.3 - options.diagnosticLevel = .info #endif // HTTP Client Errors @@ -64,6 +61,19 @@ final class SentryManager: shared.SentryManager { } } + /// Makes Sentry release id using Semantic Versioning naming strategy `package@version+build`. + /// For example `org.hyperskill.App@1.45+272` + /// - Returns: Sentry release id + private static func makeReleaseName() -> String { + guard let identifier = MainBundleInfo.identifier, + let shortVersionString = MainBundleInfo.shortVersionString, + let buildNumberString = MainBundleInfo.buildNumberString else { + fatalError("Can't make Sentry release name. Check main bundle.") + } + + return "\(identifier)@\(shortVersionString)+\(buildNumberString)" + } + // MARK: Breadcrumbs func addBreadcrumb(breadcrumb: HyperskillSentryBreadcrumb) { diff --git a/iosHyperskillApp/iosHyperskillApp/Theme/ColorPalette.swift b/iosHyperskillApp/iosHyperskillApp/Theme/ColorPalette.swift index 81d11bcdc3..f38be075bc 100644 --- a/iosHyperskillApp/iosHyperskillApp/Theme/ColorPalette.swift +++ b/iosHyperskillApp/iosHyperskillApp/Theme/ColorPalette.swift @@ -6,194 +6,194 @@ enum ColorPalette { // MARK: Blue - static let blue600 = sharedColors.color_blue_600.color.uiColor - static let blue400 = sharedColors.color_blue_400.color.uiColor - static let blue200 = sharedColors.color_blue_200.color.uiColor - static let blue400Alpha60 = sharedColors.color_blue_400_alpha_60.color.uiColor - static let blue400Alpha38 = sharedColors.color_blue_400_alpha_38.color.uiColor - static let blue400Alpha12 = sharedColors.color_blue_400_alpha_12.color.uiColor - static let blue400Alpha7 = sharedColors.color_blue_400_alpha_7.color.uiColor - static let blue200Alpha60 = sharedColors.color_blue_200_alpha_60.color.uiColor - static let blue200Alpha38 = sharedColors.color_blue_200_alpha_38.color.uiColor - static let blue200Alpha12 = sharedColors.color_blue_200_alpha_12.color.uiColor - static let blue200Alpha7 = sharedColors.color_blue_200_alpha_7.color.uiColor + static let blue600 = sharedColors.color_blue_600.uiColor + static let blue400 = sharedColors.color_blue_400.uiColor + static let blue200 = sharedColors.color_blue_200.uiColor + static let blue400Alpha60 = sharedColors.color_blue_400_alpha_60.uiColor + static let blue400Alpha38 = sharedColors.color_blue_400_alpha_38.uiColor + static let blue400Alpha12 = sharedColors.color_blue_400_alpha_12.uiColor + static let blue400Alpha7 = sharedColors.color_blue_400_alpha_7.uiColor + static let blue200Alpha60 = sharedColors.color_blue_200_alpha_60.uiColor + static let blue200Alpha38 = sharedColors.color_blue_200_alpha_38.uiColor + static let blue200Alpha12 = sharedColors.color_blue_200_alpha_12.uiColor + static let blue200Alpha7 = sharedColors.color_blue_200_alpha_7.uiColor // MARK: White - static let white50 = sharedColors.color_white_50.color.uiColor - static let white50Alpha87 = sharedColors.color_white_50_alpha_87.color.uiColor - static let white50Alpha60 = sharedColors.color_white_50_alpha_60.color.uiColor - static let white50Alpha38 = sharedColors.color_white_50_alpha_38.color.uiColor - static let white50Alpha12 = sharedColors.color_white_50_alpha_12.color.uiColor + static let white50 = sharedColors.color_white_50.uiColor + static let white50Alpha87 = sharedColors.color_white_50_alpha_87.uiColor + static let white50Alpha60 = sharedColors.color_white_50_alpha_60.uiColor + static let white50Alpha38 = sharedColors.color_white_50_alpha_38.uiColor + static let white50Alpha12 = sharedColors.color_white_50_alpha_12.uiColor // MARK: Brown - static let brown = sharedColors.color_brown.color.uiColor + static let brown = sharedColors.color_brown.uiColor // MARK: Gray - static let gray50 = sharedColors.color_gray_50.color.uiColor + static let gray50 = sharedColors.color_gray_50.uiColor // MARK: Red - static let colorRed500 = sharedColors.color_red_500.color.uiColor - static let colorRed500Alpha12 = sharedColors.color_red_500_alpha_12.color.uiColor - static let colorRed500Alpha7 = sharedColors.color_red_500_alpha_7.color.uiColor - static let colorRed300 = sharedColors.color_red_300.color.uiColor - static let colorRed300Alpha12 = sharedColors.color_red_300_alpha_12.color.uiColor - static let colorRed300Alpha7 = sharedColors.color_red_300_alpha_7.color.uiColor + static let colorRed500 = sharedColors.color_red_500.uiColor + static let colorRed500Alpha12 = sharedColors.color_red_500_alpha_12.uiColor + static let colorRed500Alpha7 = sharedColors.color_red_500_alpha_7.uiColor + static let colorRed300 = sharedColors.color_red_300.uiColor + static let colorRed300Alpha12 = sharedColors.color_red_300_alpha_12.uiColor + static let colorRed300Alpha7 = sharedColors.color_red_300_alpha_7.uiColor // MARK: Orange - static let orange900 = sharedColors.color_orange_900.color.uiColor - static let orange900Alpha12 = sharedColors.color_orange_900_alpha_12.color.uiColor - static let orange900Alpha7 = sharedColors.color_orange_900_alpha_7.color.uiColor - static let orange300 = sharedColors.color_orange_300.color.uiColor - static let orange300Alpha12 = sharedColors.color_orange_300_alpha_12.color.uiColor - static let orange300Alpha7 = sharedColors.color_orange_300_alpha_7.color.uiColor + static let orange900 = sharedColors.color_orange_900.uiColor + static let orange900Alpha12 = sharedColors.color_orange_900_alpha_12.uiColor + static let orange900Alpha7 = sharedColors.color_orange_900_alpha_7.uiColor + static let orange300 = sharedColors.color_orange_300.uiColor + static let orange300Alpha12 = sharedColors.color_orange_300_alpha_12.uiColor + static let orange300Alpha7 = sharedColors.color_orange_300_alpha_7.uiColor // MARK: Yellow - static let yellow300 = sharedColors.color_yellow_300.color.uiColor - static let yellow300Alpha12 = sharedColors.color_yellow_300_alpha_12.color.uiColor - static let yellow300Alpha7 = sharedColors.color_yellow_300_alpha_7.color.uiColor - static let yellow200 = sharedColors.color_yellow_200.color.uiColor - static let yellow200Alpha12 = sharedColors.color_yellow_200_alpha_12.color.uiColor - static let yellow200Alpha7 = sharedColors.color_yellow_200_alpha_7.color.uiColor + static let yellow300 = sharedColors.color_yellow_300.uiColor + static let yellow300Alpha12 = sharedColors.color_yellow_300_alpha_12.uiColor + static let yellow300Alpha7 = sharedColors.color_yellow_300_alpha_7.uiColor + static let yellow200 = sharedColors.color_yellow_200.uiColor + static let yellow200Alpha12 = sharedColors.color_yellow_200_alpha_12.uiColor + static let yellow200Alpha7 = sharedColors.color_yellow_200_alpha_7.uiColor // MARK: Green - static let green400 = sharedColors.color_green_400.color.uiColor - static let green200 = sharedColors.color_green_200.color.uiColor - static let green400Alpha60 = sharedColors.color_green_400_alpha_60.color.uiColor - static let green400Alpha38 = sharedColors.color_green_400_alpha_38.color.uiColor - static let green400Alpha12 = sharedColors.color_green_400_alpha_12.color.uiColor - static let green400Alpha7 = sharedColors.color_green_400_alpha_7.color.uiColor - static let green200Alpha60 = sharedColors.color_green_200_alpha_60.color.uiColor - static let green200Alpha38 = sharedColors.color_green_200_alpha_38.color.uiColor - static let green200Alpha12 = sharedColors.color_green_200_alpha_12.color.uiColor - static let green200Alpha7 = sharedColors.color_green_200_alpha_7.color.uiColor + static let green400 = sharedColors.color_green_400.uiColor + static let green200 = sharedColors.color_green_200.uiColor + static let green400Alpha60 = sharedColors.color_green_400_alpha_60.uiColor + static let green400Alpha38 = sharedColors.color_green_400_alpha_38.uiColor + static let green400Alpha12 = sharedColors.color_green_400_alpha_12.uiColor + static let green400Alpha7 = sharedColors.color_green_400_alpha_7.uiColor + static let green200Alpha60 = sharedColors.color_green_200_alpha_60.uiColor + static let green200Alpha38 = sharedColors.color_green_200_alpha_38.uiColor + static let green200Alpha12 = sharedColors.color_green_200_alpha_12.uiColor + static let green200Alpha7 = sharedColors.color_green_200_alpha_7.uiColor // MARK: Black - static let black900 = sharedColors.color_black_900.color.uiColor - static let black850 = sharedColors.color_black_850.color.uiColor - static let black300 = sharedColors.color_black_300.color.uiColor - static let black900Alpha87 = sharedColors.color_black_900_alpha_87.color.uiColor - static let black900Alpha60 = sharedColors.color_black_900_alpha_60.color.uiColor - static let black900Alpha38 = sharedColors.color_black_900_alpha_38.color.uiColor - static let black900Alpha12 = sharedColors.color_black_900_alpha_12.color.uiColor - static let black900Alpha7 = sharedColors.color_black_900_alpha_7.color.uiColor + static let black900 = sharedColors.color_black_900.uiColor + static let black850 = sharedColors.color_black_850.uiColor + static let black300 = sharedColors.color_black_300.uiColor + static let black900Alpha87 = sharedColors.color_black_900_alpha_87.uiColor + static let black900Alpha60 = sharedColors.color_black_900_alpha_60.uiColor + static let black900Alpha38 = sharedColors.color_black_900_alpha_38.uiColor + static let black900Alpha12 = sharedColors.color_black_900_alpha_12.uiColor + static let black900Alpha7 = sharedColors.color_black_900_alpha_7.uiColor // MARK: Violet - static let violet400 = sharedColors.color_violet_400.color.uiColor - static let violet400Alpha12 = sharedColors.color_violet_400_alpha_12.color.uiColor - static let violet400Alpha7 = sharedColors.color_violet_400_alpha_7.color.uiColor - static let violet200 = sharedColors.color_violet_200.color.uiColor - static let violet200Alpha12 = sharedColors.color_violet_200_alpha_12.color.uiColor - static let violet200Alpha7 = sharedColors.color_violet_200_alpha_7.color.uiColor + static let violet400 = sharedColors.color_violet_400.uiColor + static let violet400Alpha12 = sharedColors.color_violet_400_alpha_12.uiColor + static let violet400Alpha7 = sharedColors.color_violet_400_alpha_7.uiColor + static let violet200 = sharedColors.color_violet_200.uiColor + static let violet200Alpha12 = sharedColors.color_violet_200_alpha_12.uiColor + static let violet200Alpha7 = sharedColors.color_violet_200_alpha_7.uiColor // MARK: - AppTheme - // MARK: Background - static let background = sharedColors.color_background.dynamicUIColor + static let background = sharedColors.color_background.uiColor - static let onBackground = sharedColors.color_on_background.dynamicUIColor + static let onBackground = sharedColors.color_on_background.uiColor // MARK: Primary - static let primary = sharedColors.color_primary.dynamicUIColor - static let primaryAlpha60 = sharedColors.color_primary_alpha_60.dynamicUIColor - static let primaryAlpha38 = sharedColors.color_primary_alpha_38.dynamicUIColor + static let primary = sharedColors.color_primary.uiColor + static let primaryAlpha60 = sharedColors.color_primary_alpha_60.uiColor + static let primaryAlpha38 = sharedColors.color_primary_alpha_38.uiColor - static let primaryVariant = sharedColors.color_primary_variant.color.uiColor + static let primaryVariant = sharedColors.color_primary_variant.uiColor - static let onPrimary = sharedColors.color_on_primary.dynamicUIColor - static let onPrimaryAlpha87 = sharedColors.color_on_primary_alpha_87.dynamicUIColor - static let onPrimaryAlpha60 = sharedColors.color_on_primary_alpha_60.dynamicUIColor + static let onPrimary = sharedColors.color_on_primary.uiColor + static let onPrimaryAlpha87 = sharedColors.color_on_primary_alpha_87.uiColor + static let onPrimaryAlpha60 = sharedColors.color_on_primary_alpha_60.uiColor // MARK: Secondary - static let secondary = sharedColors.color_secondary.dynamicUIColor - static let secondaryAlpha60 = sharedColors.color_secondary_alpha_60.dynamicUIColor - static let secondaryAlpha38 = sharedColors.color_secondary_alpha_38.dynamicUIColor + static let secondary = sharedColors.color_secondary.uiColor + static let secondaryAlpha60 = sharedColors.color_secondary_alpha_60.uiColor + static let secondaryAlpha38 = sharedColors.color_secondary_alpha_38.uiColor - static let onSecondary = sharedColors.color_on_secondary.dynamicUIColor + static let onSecondary = sharedColors.color_on_secondary.uiColor // MARK: Surface - static let surface = sharedColors.color_surface.dynamicUIColor + static let surface = sharedColors.color_surface.uiColor - static let onSurface = sharedColors.color_on_surface.dynamicUIColor - static let onSurfaceAlpha87 = sharedColors.color_on_surface_alpha_87.dynamicUIColor - static let onSurfaceAlpha60 = sharedColors.color_on_surface_alpha_60.dynamicUIColor - static let onSurfaceAlpha38 = sharedColors.color_on_surface_alpha_38.dynamicUIColor - static let onSurfaceAlpha12 = sharedColors.color_on_surface_alpha_12.dynamicUIColor - static let onSurfaceAlpha9 = sharedColors.color_on_surface_alpha_9.dynamicUIColor + static let onSurface = sharedColors.color_on_surface.uiColor + static let onSurfaceAlpha87 = sharedColors.color_on_surface_alpha_87.uiColor + static let onSurfaceAlpha60 = sharedColors.color_on_surface_alpha_60.uiColor + static let onSurfaceAlpha38 = sharedColors.color_on_surface_alpha_38.uiColor + static let onSurfaceAlpha12 = sharedColors.color_on_surface_alpha_12.uiColor + static let onSurfaceAlpha9 = sharedColors.color_on_surface_alpha_9.uiColor // MARK: Error - static let error = sharedColors.color_error.dynamicUIColor + static let error = sharedColors.color_error.uiColor - static let onError = sharedColors.color_on_error.dynamicUIColor + static let onError = sharedColors.color_on_error.uiColor // MARK: - ThemeOverlay - // MARK: Blue - static let overlayBlue = sharedColors.color_overlay_blue.dynamicUIColor - static let overlayBlueAlpha12 = sharedColors.color_overlay_blue_alpha_12.dynamicUIColor - static let overlayBlueAlpha7 = sharedColors.color_overlay_blue_alpha_7.dynamicUIColor - static let overlayBlueBrand = sharedColors.color_overlay_blue_brand.color.uiColor + static let overlayBlue = sharedColors.color_overlay_blue.uiColor + static let overlayBlueAlpha12 = sharedColors.color_overlay_blue_alpha_12.uiColor + static let overlayBlueAlpha7 = sharedColors.color_overlay_blue_alpha_7.uiColor + static let overlayBlueBrand = sharedColors.color_overlay_blue_brand.uiColor // MARK: Orange - static let overlayOrange = sharedColors.color_overlay_orange.dynamicUIColor - static let overlayOrangeAlpha12 = sharedColors.color_overlay_orange_alpha_12.dynamicUIColor - static let overlayOrangeAlpha7 = sharedColors.color_overlay_orange_alpha_7.dynamicUIColor + static let overlayOrange = sharedColors.color_overlay_orange.uiColor + static let overlayOrangeAlpha12 = sharedColors.color_overlay_orange_alpha_12.uiColor + static let overlayOrangeAlpha7 = sharedColors.color_overlay_orange_alpha_7.uiColor // MARK: Green - static let overlayGreen = sharedColors.color_overlay_green.dynamicUIColor - static let overlayGreenAlpha12 = sharedColors.color_overlay_green_alpha_12.dynamicUIColor - static let overlayGreenAlpha7 = sharedColors.color_overlay_green_alpha_7.dynamicUIColor + static let overlayGreen = sharedColors.color_overlay_green.uiColor + static let overlayGreenAlpha12 = sharedColors.color_overlay_green_alpha_12.uiColor + static let overlayGreenAlpha7 = sharedColors.color_overlay_green_alpha_7.uiColor // MARK: Yellow - static let overlayYellow = sharedColors.color_overlay_yellow.dynamicUIColor - static let overlayYellowAlpha12 = sharedColors.color_overlay_yellow_alpha_12.dynamicUIColor - static let overlayYellowAlpha7 = sharedColors.color_overlay_yellow_alpha_7.dynamicUIColor + static let overlayYellow = sharedColors.color_overlay_yellow.uiColor + static let overlayYellowAlpha12 = sharedColors.color_overlay_yellow_alpha_12.uiColor + static let overlayYellowAlpha7 = sharedColors.color_overlay_yellow_alpha_7.uiColor // MARK: Violet - static let overlayViolet = sharedColors.color_overlay_violet.dynamicUIColor - static let overlayVioletAlpha12 = sharedColors.color_overlay_violet_alpha_12.dynamicUIColor - static let overlayVioletAlpha7 = sharedColors.color_overlay_violet_alpha_7.dynamicUIColor + static let overlayViolet = sharedColors.color_overlay_violet.uiColor + static let overlayVioletAlpha12 = sharedColors.color_overlay_violet_alpha_12.uiColor + static let overlayVioletAlpha7 = sharedColors.color_overlay_violet_alpha_7.uiColor // MARK: Red - static let overlayRed = sharedColors.color_overlay_red.dynamicUIColor - static let overlayRedAlpha12 = sharedColors.color_overlay_red_alpha_12.dynamicUIColor - static let overlayRedAlpha7 = sharedColors.color_overlay_red_alpha_7.dynamicUIColor + static let overlayRed = sharedColors.color_overlay_red.uiColor + static let overlayRedAlpha12 = sharedColors.color_overlay_red_alpha_12.uiColor + static let overlayRedAlpha7 = sharedColors.color_overlay_red_alpha_7.uiColor // MARK: - New Colors - - static let newTextPrimary = sharedColors.text_primary.dynamicUIColor - static let newTextSecondary = sharedColors.text_secondary.dynamicUIColor - static let newTextOnColor = sharedColors.text_on_color.color.uiColor + static let newTextPrimary = sharedColors.text_primary.uiColor + static let newTextSecondary = sharedColors.text_secondary.uiColor + static let newTextOnColor = sharedColors.text_on_color.uiColor - static let newButtonPrimary = sharedColors.button_primary.color.uiColor - static let newButtonPrimaryActive = sharedColors.button_primary_active.color.uiColor + static let newButtonPrimary = sharedColors.button_primary.uiColor + static let newButtonPrimaryActive = sharedColors.button_primary_active.uiColor - static let newButtonTertiary = sharedColors.button_tertiary.dynamicUIColor - static let newButtonTertiaryActive = sharedColors.button_tertiary_active.dynamicUIColor + static let newButtonTertiary = sharedColors.button_tertiary.uiColor + static let newButtonTertiaryActive = sharedColors.button_tertiary_active.uiColor - static let newButtonGhost = sharedColors.button_ghost.dynamicUIColor - static let newButtonGhostActive = sharedColors.button_ghost_active.dynamicUIColor + static let newButtonGhost = sharedColors.button_ghost.uiColor + static let newButtonGhostActive = sharedColors.button_ghost_active.uiColor - static let newButtonDisabled = sharedColors.button_disabled.dynamicUIColor + static let newButtonDisabled = sharedColors.button_disabled.uiColor - static let newLayer1 = sharedColors.layer_1.dynamicUIColor + static let newLayer1 = sharedColors.layer_1.uiColor } diff --git a/iosHyperskillApp/iosHyperskillAppTests/Info.plist b/iosHyperskillApp/iosHyperskillAppTests/Info.plist index a139fb5cba..690fdfe212 100644 --- a/iosHyperskillApp/iosHyperskillAppTests/Info.plist +++ b/iosHyperskillApp/iosHyperskillAppTests/Info.plist @@ -13,8 +13,8 @@ CFBundlePackageType BNDL CFBundleShortVersionString - 1.45 + 1.46 CFBundleVersion - 272 + 285 diff --git a/iosHyperskillApp/iosHyperskillAppUITests/Info.plist b/iosHyperskillApp/iosHyperskillAppUITests/Info.plist index a433de1dce..bac10b5d28 100644 --- a/iosHyperskillApp/iosHyperskillAppUITests/Info.plist +++ b/iosHyperskillApp/iosHyperskillAppUITests/Info.plist @@ -13,8 +13,8 @@ CFBundlePackageType $(PRODUCT_BUNDLE_PACKAGE_TYPE) CFBundleShortVersionString - 1.45 + 1.46 CFBundleVersion - 272 + 285 diff --git a/shared/build.gradle.kts b/shared/build.gradle.kts index 711e01247c..b7ec66c98f 100644 --- a/shared/build.gradle.kts +++ b/shared/build.gradle.kts @@ -46,6 +46,8 @@ kotlin { // Delete options passed to a system linker after upgrading to the Kotlin 1.9.10 // https://youtrack.jetbrains.com/issue/KT-60230 linkerOpts += "-ld64" + // Add export declarations to use moko-resources iOS extensions from Swift side + export(libs.mokoResources.main) } } } @@ -100,39 +102,41 @@ kotlin { } } - val iosX64Main by getting - val iosArm64Main by getting - val iosSimulatorArm64Main by getting val iosMain by creating { - dependsOn(commonMain) - - iosX64Main.dependsOn(this) - iosArm64Main.dependsOn(this) - iosSimulatorArm64Main.dependsOn(this) - dependencies { implementation(libs.ktor.ios) } } - val iosX64Test by getting - val iosArm64Test by getting - val iosSimulatorArm64Test by getting + val iosX64Main by getting { + dependsOn(iosMain) + } + val iosArm64Main by getting { + dependsOn(iosMain) + } + val iosSimulatorArm64Main by getting { + dependsOn(iosMain) + } + val iosTest by creating { dependsOn(commonTest) - - iosX64Test.dependsOn(this) - iosArm64Test.dependsOn(this) - iosSimulatorArm64Test.dependsOn(this) + } + val iosX64Test by getting { + dependsOn(iosTest) + } + val iosArm64Test by getting { + dependsOn(iosTest) + } + val iosSimulatorArm64Test by getting { + dependsOn(iosTest) } } } android { - compileSdkVersion(appVersions.versions.compileSdk.get().toInt()) + compileSdk = appVersions.versions.compileSdk.get().toInt() sourceSets["main"].manifest.srcFile("src/androidMain/AndroidManifest.xml") defaultConfig { - minSdkVersion(appVersions.versions.minSdk.get().toInt()) - targetSdkVersion(appVersions.versions.targetSdk.get().toInt()) + minSdk = appVersions.versions.minSdk.get().toInt() testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" } @@ -143,6 +147,11 @@ android { excludes += "META-INF/AL2.0" } } + namespace = "org.hyperskill.app" + + sourceSets { + getByName("main").java.srcDirs("build/generated/moko/androidMain/src") + } } buildkonfig { diff --git a/shared/src/androidMain/AndroidManifest.xml b/shared/src/androidMain/AndroidManifest.xml index 00bcc6825e..ecebd576e6 100644 --- a/shared/src/androidMain/AndroidManifest.xml +++ b/shared/src/androidMain/AndroidManifest.xml @@ -1,4 +1,4 @@ - + \ No newline at end of file diff --git a/shared/src/commonMain/kotlin/org/hyperskill/app/analytic/domain/model/hyperskill/HyperskillAnalyticEngine.kt b/shared/src/commonMain/kotlin/org/hyperskill/app/analytic/domain/model/hyperskill/HyperskillAnalyticEngine.kt index 4743f7dc8b..7edbf5fe42 100644 --- a/shared/src/commonMain/kotlin/org/hyperskill/app/analytic/domain/model/hyperskill/HyperskillAnalyticEngine.kt +++ b/shared/src/commonMain/kotlin/org/hyperskill/app/analytic/domain/model/hyperskill/HyperskillAnalyticEngine.kt @@ -13,6 +13,7 @@ import org.hyperskill.app.analytic.domain.model.AnalyticSource import org.hyperskill.app.analytic.domain.processor.AnalyticHyperskillEventProcessor import org.hyperskill.app.analytic.domain.repository.AnalyticHyperskillRepository import org.hyperskill.app.auth.domain.interactor.AuthInteractor +import org.hyperskill.app.config.BuildKonfig import org.hyperskill.app.core.domain.model.ScreenOrientation import org.hyperskill.app.notification.local.domain.interactor.NotificationInteractor import org.hyperskill.app.profile.domain.model.Profile @@ -36,6 +37,7 @@ internal class HyperskillAnalyticEngineImpl( private var screenOrientation: ScreenOrientation? = null private var isATTPermissionGranted: Boolean = false + private val isInternalTesting: Boolean = BuildKonfig.IS_INTERNAL_TESTING ?: false override val targetSource: AnalyticSource = AnalyticSource.HYPERSKILL_API @@ -70,7 +72,8 @@ internal class HyperskillAnalyticEngineImpl( userId = currentProfile.id, isNotificationsPermissionGranted = notificationInteractor.isNotificationsPermissionGranted(), isATTPermissionGranted = isATTPermissionGranted, - screenOrientation = screenOrientation ?: ScreenOrientation.PORTRAIT + screenOrientation = screenOrientation ?: ScreenOrientation.PORTRAIT, + isInternalTesting = isInternalTesting ) analyticHyperskillRepository.logEvent(processedEvent) diff --git a/shared/src/commonMain/kotlin/org/hyperskill/app/analytic/domain/processor/AnalyticHyperskillEventProcessor.kt b/shared/src/commonMain/kotlin/org/hyperskill/app/analytic/domain/processor/AnalyticHyperskillEventProcessor.kt index f8865acfd2..d2b71bc6f7 100644 --- a/shared/src/commonMain/kotlin/org/hyperskill/app/analytic/domain/processor/AnalyticHyperskillEventProcessor.kt +++ b/shared/src/commonMain/kotlin/org/hyperskill/app/analytic/domain/processor/AnalyticHyperskillEventProcessor.kt @@ -4,7 +4,6 @@ import org.hyperskill.app.analytic.domain.model.AnalyticEvent import org.hyperskill.app.analytic.domain.model.hyperskill.HyperskillProcessedAnalyticEvent import org.hyperskill.app.core.domain.model.ScreenOrientation import org.hyperskill.app.core.domain.platform.Platform -import ru.nobird.app.core.model.mapOfNotNull import ru.nobird.app.core.model.safeCast class AnalyticHyperskillEventProcessor( @@ -19,6 +18,7 @@ class AnalyticHyperskillEventProcessor( private const val PARAM_SCREEN_ORIENTATION = "screen_orientation" private const val SCREEN_ORIENTATION_VALUE_PORTRAIT = "portrait" private const val SCREEN_ORIENTATION_VALUE_LANDSCAPE = "landscape" + private const val PARAM_IS_INTERNAL_TESTING = "is_internal_testing" } fun processEvent( @@ -26,36 +26,45 @@ class AnalyticHyperskillEventProcessor( userId: Long, isNotificationsPermissionGranted: Boolean, isATTPermissionGranted: Boolean, - screenOrientation: ScreenOrientation + screenOrientation: ScreenOrientation, + isInternalTesting: Boolean ): HyperskillProcessedAnalyticEvent { val resultParams = event.params.toMutableMap() - if (resultParams.containsKey(PARAM_CONTEXT)) { - val contextMap = resultParams[PARAM_CONTEXT] - .safeCast>() - ?.toMutableMap() - - if (contextMap != null) { - contextMap[PARAM_PLATFORM] = platform.analyticName - contextMap[PARAM_IS_NOTIFICATIONS_ALLOW] = isNotificationsPermissionGranted - contextMap[PARAM_IS_ATT_ALLOW] = isATTPermissionGranted - contextMap[PARAM_SCREEN_ORIENTATION] = getScreenOrientationValue(screenOrientation) - resultParams[PARAM_CONTEXT] = contextMap - } - } else { - resultParams[PARAM_CONTEXT] = mapOfNotNull( - PARAM_PLATFORM to platform.analyticName, - PARAM_IS_NOTIFICATIONS_ALLOW to isNotificationsPermissionGranted, - PARAM_IS_ATT_ALLOW to isATTPermissionGranted, - PARAM_SCREEN_ORIENTATION to getScreenOrientationValue(screenOrientation) - ) - } + val contextMap = when (resultParams.containsKey(PARAM_CONTEXT)) { + true -> resultParams[PARAM_CONTEXT].safeCast>()?.toMutableMap() + false -> mutableMapOf() + } ?: mutableMapOf() + updateContextMap( + contextMap, + isNotificationsPermissionGranted = isNotificationsPermissionGranted, + isATTPermissionGranted = isATTPermissionGranted, + screenOrientation = screenOrientation, + isInternalTesting = isInternalTesting + ) + resultParams[PARAM_CONTEXT] = contextMap resultParams[PARAM_USER] = userId return HyperskillProcessedAnalyticEvent(name = event.name, params = resultParams.toMap()) } + private fun updateContextMap( + contextMap: MutableMap, + isNotificationsPermissionGranted: Boolean, + isATTPermissionGranted: Boolean, + screenOrientation: ScreenOrientation, + isInternalTesting: Boolean + ) { + contextMap.apply { + this[PARAM_PLATFORM] = platform.analyticName + this[PARAM_IS_NOTIFICATIONS_ALLOW] = isNotificationsPermissionGranted + this[PARAM_IS_ATT_ALLOW] = isATTPermissionGranted + this[PARAM_SCREEN_ORIENTATION] = getScreenOrientationValue(screenOrientation) + this[PARAM_IS_INTERNAL_TESTING] = isInternalTesting + } + } + private fun getScreenOrientationValue(screenOrientation: ScreenOrientation): String = when (screenOrientation) { ScreenOrientation.PORTRAIT -> SCREEN_ORIENTATION_VALUE_PORTRAIT diff --git a/shared/src/commonMain/kotlin/org/hyperskill/app/code/domain/model/ProgrammingLanguage.kt b/shared/src/commonMain/kotlin/org/hyperskill/app/code/domain/model/ProgrammingLanguage.kt new file mode 100644 index 0000000000..2d9b29a462 --- /dev/null +++ b/shared/src/commonMain/kotlin/org/hyperskill/app/code/domain/model/ProgrammingLanguage.kt @@ -0,0 +1,91 @@ +package org.hyperskill.app.code.domain.model + +enum class ProgrammingLanguage(val languageName: String) { + PYTHON("python"), + PYTHON3("python3"), + PYTHON3_1("python3.10"), + PYTHON3_11("python3.11"), + CPP("c++"), + CPP11("c++11"), + CPP20("c++20"), + C("c"), + C_VALGRIND("c_valgrind"), + HASKELL("haskell"), + HASKELL7("haskell 7.10"), + HASKELL8("haskell 8.0"), + HASKELL8_8("haskell 8.8"), + JAVA("java"), + JAVA8("java8"), + JAVA9("java9"), + JAVA11("java11"), + JAVA17("java17"), + OCTAVE("octave"), + ASM32("asm32"), + ASM64("asm64"), + SHELL("shell"), + RUST("rust"), + R("r"), + RUBY("ruby"), + CLOJURE("clojure"), + CS("c#"), + CS_MONO("mono c#"), + JAVASCRIPT("javascript"), + TYPESCRIPT("typescript"), + SCALA("scala"), + SCALA3("scala3"), + KOTLIN("kotlin"), + GO("go"), + PASCAL("pascalabc"), + PERL("perl"), + SQL("sql"), + SWIFT("swift"), + PHP("php"), + JULIA("julia"), + DART("dart"); + + val fileExtension: String + get() = when (this) { + PYTHON, PYTHON3, PYTHON3_1, PYTHON3_11 -> "py" + CPP, CPP11, CPP20, C, C_VALGRIND -> "cpp" + HASKELL, HASKELL7, HASKELL8, HASKELL8_8 -> "hs" + JAVA, JAVA8, JAVA9, JAVA11, JAVA17 -> "java" + OCTAVE -> "matlab" + ASM32, ASM64 -> "asm" + SHELL -> "sh" + RUST -> "rust" + R -> "r" + RUBY -> "rb" + CLOJURE -> "clj" + CS, CS_MONO -> "cs" + JAVASCRIPT -> "js" + TYPESCRIPT -> "ts" + SCALA, SCALA3 -> "scala" + KOTLIN -> "kt" + GO -> "go" + PASCAL -> "pascal" + PERL -> "perl" + SQL -> "sql" + SWIFT -> "swift" + PHP -> "php" + JULIA -> "julia" + DART -> "dart" + } + + companion object { + fun of(languageName: String): ProgrammingLanguage? = + ProgrammingLanguage.values() + .find { + it.languageName.equals(languageName, ignoreCase = true) + } + } +} + +/** + * Returns the file extension for a given programming language. + * + * @param lang The name of the programming language. + * @return The file extension for the given programming language, or an empty string if the language is not found. + * Empty string for not found language is used for compatibility with Android code. + */ +fun fileExtensionForLanguage(lang: String): String = + ProgrammingLanguage.of(lang)?.fileExtension ?: "" \ No newline at end of file diff --git a/shared/src/commonMain/kotlin/org/hyperskill/app/logging/presentation/WrapWithLogger.kt b/shared/src/commonMain/kotlin/org/hyperskill/app/logging/presentation/WrapWithLogger.kt index 58f2c2569e..6e2d14934a 100644 --- a/shared/src/commonMain/kotlin/org/hyperskill/app/logging/presentation/WrapWithLogger.kt +++ b/shared/src/commonMain/kotlin/org/hyperskill/app/logging/presentation/WrapWithLogger.kt @@ -29,7 +29,7 @@ private class LoggableStateReducer( private val origin: StateReducer, private val logger: Logger, private val tag: String, - private val severity: Severity = Severity.Info + private val severity: Severity ) : StateReducer { override fun reduce(state: State, message: Message): Pair> { val (newState, actions) = origin.reduce(state, message) diff --git a/shared/src/commonMain/kotlin/org/hyperskill/app/notification/click_handling/presentation/NotificationClickHandlingReducer.kt b/shared/src/commonMain/kotlin/org/hyperskill/app/notification/click_handling/presentation/NotificationClickHandlingReducer.kt index c3871fbd38..57dfad4e21 100644 --- a/shared/src/commonMain/kotlin/org/hyperskill/app/notification/click_handling/presentation/NotificationClickHandlingReducer.kt +++ b/shared/src/commonMain/kotlin/org/hyperskill/app/notification/click_handling/presentation/NotificationClickHandlingReducer.kt @@ -48,6 +48,7 @@ class NotificationClickHandlingReducer : StateReducer { PushNotificationType.STREAK_RECORD_START, PushNotificationType.STREAK_RECORD_NEAR, PushNotificationType.STREAK_RECORD_COMPLETE, + PushNotificationType.DAILY_REMINDER, PushNotificationType.STREAK_NEW -> setOf( Action.ViewAction.SetLoadingShowed(true), diff --git a/shared/src/commonMain/kotlin/org/hyperskill/app/notification/remote/domain/model/PushNotificationType.kt b/shared/src/commonMain/kotlin/org/hyperskill/app/notification/remote/domain/model/PushNotificationType.kt index 635d913c4f..81be3529f2 100644 --- a/shared/src/commonMain/kotlin/org/hyperskill/app/notification/remote/domain/model/PushNotificationType.kt +++ b/shared/src/commonMain/kotlin/org/hyperskill/app/notification/remote/domain/model/PushNotificationType.kt @@ -18,6 +18,7 @@ enum class PushNotificationType { REMIND_MEDIUM, USER_BADGE_UPDATED, USER_BADGE_UNLOCKED, + DAILY_REMINDER, UNKNOWN } \ 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 09708f1bd5..c90128ae12 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 @@ -2,6 +2,7 @@ package org.hyperskill.app.step.domain.model import kotlinx.serialization.SerialName import kotlinx.serialization.Serializable +import org.hyperskill.app.code.domain.model.ProgrammingLanguage @Serializable data class Block( @@ -39,4 +40,7 @@ data class Block( val text: String ) } -} \ No newline at end of file +} + +val Block.Options.programmingLanguage: ProgrammingLanguage? + get() = language?.let(ProgrammingLanguage::of) \ No newline at end of file diff --git a/shared/src/commonMain/resources/MR/base/strings.xml b/shared/src/commonMain/resources/MR/base/strings.xml index 48976bdfe3..5a62a80486 100644 --- a/shared/src/commonMain/resources/MR/base/strings.xml +++ b/shared/src/commonMain/resources/MR/base/strings.xml @@ -394,15 +394,9 @@ Start for free I already have an account - - - Track your progress Choose a time for your daily practice and we\'ll remind you to stay on track - Enable notifications to keep your streak alive and stay - consistently on top of your learning - Set time Allow notifications Not now