Skip to content

Commit

Permalink
Merge branch 'develop' into minor_fixes/new_gpt_generated_code_with_e…
Browse files Browse the repository at this point in the history
…rrors_onboarding_cache_flag
  • Loading branch information
XanderZhu authored May 31, 2024
2 parents 5ab3042 + 7d6ac45 commit 0337e89
Show file tree
Hide file tree
Showing 246 changed files with 4,045 additions and 1,305 deletions.
2 changes: 2 additions & 0 deletions androidHyperskillApp/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,8 @@ dependencies {
implementation(libs.compose.viewbinding)

coreLibraryDesugaring(libs.android.desugar.jdk)

implementation(libs.android.analytic.clarity)
}

android {
Expand Down
Binary file modified androidHyperskillApp/keys/debug.properties
Binary file not shown.
Binary file modified androidHyperskillApp/keys/release.properties
Binary file not shown.
1 change: 1 addition & 0 deletions androidHyperskillApp/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
<uses-permission android:name="android.permission.DETECT_SCREEN_CAPTURE" />

<uses-permission
android:name="android.permission.WAKE_LOCK"
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
package org.hyperskill.app.android.clarity

import android.content.Context
import androidx.core.os.bundleOf
import androidx.savedstate.SavedStateRegistry
import com.microsoft.clarity.Clarity
import com.microsoft.clarity.ClarityConfig
import com.microsoft.clarity.models.LogLevel
import com.russhwolf.settings.Settings
import org.hyperskill.app.android.BuildConfig

class ClarityDelegate(
private val settings: Settings
) {

companion object {
private const val SAVED_STATE_PROVIDER_KEY = "CLARITY_MANAGER"
private const val IS_FIRST_SESSION_KEY = "IS_ANDROID_FIRST_SESSION"
private const val IS_DEBUG_KEY = "IS_DEBUG"
private val clarityConfig = ClarityConfig(
projectId = BuildConfig.CLARITY_PROJECT_ID,
logLevel = if (BuildConfig.DEBUG) LogLevel.Debug else LogLevel.None,
allowMeteredNetworkUsage = false,
allowedDomains = emptyList(),
disableOnLowEndDevices = true
)
}

private var isFirstSession: Boolean = false

fun init(context: Context, savedStateRegistry: SavedStateRegistry) {
registerSavedStateProvider(savedStateRegistry)
isFirstSession = isFirstSession(savedStateRegistry, settings)
setNotFirstSession(settings)
if (isFirstSession) {
Clarity.initialize(context, clarityConfig)
Clarity.setCustomTag(IS_DEBUG_KEY, BuildConfig.DEBUG.toString())
}
}

fun setUserId(userId: Long) {
if (isFirstSession) {
Clarity.setCustomUserId(userId.toString())
}
}

private fun isFirstSession(savedStateRegistry: SavedStateRegistry, settings: Settings): Boolean =
getIsFirstSession(savedStateRegistry) ?: getIsFirstSession(settings)

private fun getIsFirstSession(savedStateRegistry: SavedStateRegistry): Boolean? =
savedStateRegistry
.consumeRestoredStateForKey(SAVED_STATE_PROVIDER_KEY)
?.getBoolean(IS_FIRST_SESSION_KEY)

private fun getIsFirstSession(settings: Settings): Boolean =
settings.getBoolean(IS_FIRST_SESSION_KEY, defaultValue = true)

private fun setNotFirstSession(settings: Settings) {
settings.putBoolean(IS_FIRST_SESSION_KEY, false)
}

private fun registerSavedStateProvider(savedStateRegistry: SavedStateRegistry) {
savedStateRegistry.registerSavedStateProvider(SAVED_STATE_PROVIDER_KEY) {
bundleOf(IS_FIRST_SESSION_KEY to isFirstSession)
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ class CodeToolbarAdapter(private val context: Context) :
ignoreCase = true
)
) {
onSymbolClickListener?.onSymbolClick("$word ", autocomplete.prefix.length)
onSymbolClickListener?.onSymbolClick("$word", autocomplete.prefix.length)
} else {
onSymbolClickListener?.onSymbolClick(word, 0)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,6 @@ class CodeAnalyzer(private val autocompleteContainer: AutocompleteContainer) {

private val quotes = hashMapOf(
"\"" to "\"",
"'" to "'",
"`" to "`"
)

Expand Down Expand Up @@ -85,10 +84,9 @@ class CodeAnalyzer(private val autocompleteContainer: AutocompleteContainer) {
}

in quotes -> {
val next = getNextSymbolAsString(start + 1, text)
val prev = getPrevSymbolAsString(start, text)
// don't want auto quote if there is a statement next
if ((next == null || Character.isWhitespace(next[0])) && prev != inserted) {
if (prev != inserted) {
insertTextAfterCursor(start, count, codeEditor, inserted)
} else {
onClosingSymbolInserted(start, count, codeEditor, inserted, text)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,13 +13,13 @@ open class DefaultTapUpListener : GestureDetector.OnGestureListener {
override fun onSingleTapUp(e: MotionEvent): Boolean =
false

override fun onScroll(e1: MotionEvent, e2: MotionEvent, distanceX: Float, distanceY: Float): Boolean =
override fun onScroll(e1: MotionEvent?, e2: MotionEvent, distanceX: Float, distanceY: Float): Boolean =
false

override fun onLongPress(e: MotionEvent) {
// no op
}

override fun onFling(e1: MotionEvent, e2: MotionEvent, velocityX: Float, velocityY: Float): Boolean =
override fun onFling(e1: MotionEvent?, e2: MotionEvent, velocityX: Float, velocityY: Float): Boolean =
false
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
package org.hyperskill.app.android.core.extensions

import android.app.Activity
import android.os.Build
import androidx.fragment.app.Fragment
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.LifecycleEventObserver

/**
* Sets a callback to be invoked when a screenshot is captured on the screen.
* Works only on Android 14 and higher.
*
* @param block The code block to be executed when a screenshot is captured.
*/
inline fun Fragment.doOnScreenShootCaptured(
crossinline block: () -> Unit
) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
val callBack = Activity.ScreenCaptureCallback {
block()
}
lifecycle.addObserver(
LifecycleEventObserver { _, event ->
when (event) {
Lifecycle.Event.ON_START -> {
requireActivity().registerScreenCaptureCallback(
/* executor = */ requireActivity().mainExecutor,
/* callback = */ callBack
)
}
Lifecycle.Event.ON_STOP -> {
requireActivity().unregisterScreenCaptureCallback(callBack)
}
else -> {
// no op
}
}
}
)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -55,4 +55,13 @@ object ShareUtils {
file
)
}

fun getShareTextIntent(text: String): Intent {
val shareIntent = Intent().apply {
action = Intent.ACTION_SEND
putExtra(Intent.EXTRA_TEXT, text)
type = "text/plain"
}
return Intent.createChooser(shareIntent, null)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
package org.hyperskill.app.android.core.view.ui.widget.compose

import androidx.compose.animation.core.AnimationSpec
import androidx.compose.animation.core.Easing
import androidx.compose.animation.core.FastOutSlowInEasing
import androidx.compose.animation.core.animateFloatAsState
import androidx.compose.animation.core.tween
import androidx.compose.runtime.Stable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.composed
import androidx.compose.ui.draw.drawWithContent
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.graphics.Brush
import androidx.compose.ui.graphics.Color

/**
* Applies shimmer animation to the target Composable.
* Animation is playing one time.
* To start animation call [ShimmerState.runShimmerAnimation] on the [ShimmerState] instance.
*/
fun Modifier.shimmerShot(shimmerState: ShimmerState): Modifier =
composed {
val startOffsetX by animateFloatAsState(
targetValue = shimmerState.targetValue,
animationSpec = shimmerState.startOffsetXAnimationSpec,
label = "shimmer"
)
drawWithContent {
val width = size.width
val height = size.height
val offset = startOffsetX * width

drawContent()
val brush = Brush.linearGradient(
colors = shimmerState.colors,
start = Offset(offset, 0f),
end = Offset(offset + width, height)
)
drawRect(brush)
}
}

@Stable
class ShimmerState(
val colors: List<Color> = listOf(
Color.Transparent,
Color.White.copy(alpha = 0.7f),
Color.Transparent
),
durationMillis: Int = 1200,
easing: Easing = FastOutSlowInEasing
) {

companion object {
private const val INITIAL_VALUE = -2f
private const val TARGET_VALUE = 2f
}

var targetValue: Float by mutableStateOf(INITIAL_VALUE)
private set

val startOffsetXAnimationSpec: AnimationSpec<Float> = tween(
durationMillis = durationMillis,
easing = easing
)

fun runShimmerAnimation() {
targetValue = TARGET_VALUE
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
package org.hyperskill.app.android.core.view.ui.widget.compose

import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberUpdatedState
import androidx.compose.runtime.setValue
import kotlin.random.Random
import kotlin.time.Duration.Companion.milliseconds
import kotlinx.coroutines.delay

/**
* A composable function that displays a text with a typewriter-like effect, revealing characters in chunks.
*
* @param text The input text to be displayed with the typewriter effect.
* @param minDelayInMillis The minimum delay in milliseconds between revealing character chunks, defaults to 30ms.
* @param maxDelayInMillis The maximum delay in milliseconds between revealing character chunks, defaults to 80ms.
* @param minCharacterChunk The minimum number of characters to reveal at once, defaults to 1.
* @param maxCharacterChunk The maximum number of characters to reveal at once, defaults to 3.
* @param onEffectCompleted A callback function invoked when the entire text has been revealed.
* @param displayTextComposable A composable function that receives the text to display with the typewriter effect.
*
* @throws IllegalArgumentException if [minDelayInMillis] is greater than [maxDelayInMillis].
* @throws IllegalArgumentException if [minCharacterChunk] is greater than [maxCharacterChunk].
*/
@Suppress("MaxLineLength")
@Composable
fun TypewriterTextEffect(
text: String,
startTypingDelayInMillis: Int? = null,
minDelayInMillis: Long = 30,
maxDelayInMillis: Long = 80,
minCharacterChunk: Int = 1,
maxCharacterChunk: Int = 3,
onEffectCompleted: () -> Unit = {},
displayTextComposable: @Composable (displayedText: String) -> Unit
) {
// Ensure minDelayInMillis is less than or equal to maxDelayInMillis
require(minDelayInMillis <= maxDelayInMillis) {
"TypewriterTextEffect: Invalid delay range. minDelayInMillis ($minDelayInMillis) must be less than or equal to maxDelayInMillis ($maxDelayInMillis)." //ktlint-disable
}

// Ensure minCharacterChunk is less than or equal to maxCharacterChunk
require(minCharacterChunk <= maxCharacterChunk) {
"TypewriterTextEffect: Invalid character chunk range. minCharacterChunk ($minCharacterChunk) must be less than or equal to maxCharacterChunk ($maxCharacterChunk)." //ktlint-disable
}

val currentOnEffectCompleted by rememberUpdatedState(newValue = onEffectCompleted)

// Initialize and remember the displayedText
var displayedText by remember { mutableStateOf("") }

// Call the displayTextComposable with the current displayedText value
displayTextComposable(displayedText)

// Launch the effect to update the displayedText value over time
LaunchedEffect(text) {
if (startTypingDelayInMillis != null) {
delay(startTypingDelayInMillis.milliseconds)
}

val textLength = text.length
var endIndex = 0

while (endIndex < textLength) {
endIndex = minOf(
endIndex + Random.nextInt(minCharacterChunk, maxCharacterChunk + 1),
textLength
)
displayedText = text.substring(startIndex = 0, endIndex = endIndex)
delay(Random.nextLong(minDelayInMillis, maxDelayInMillis))
}
currentOnEffectCompleted()
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ import org.hyperskill.app.android.R
import org.hyperskill.app.android.core.view.ui.navigation.requireRouter
import org.hyperskill.app.android.core.view.ui.widget.compose.HyperskillTheme
import org.hyperskill.app.android.debug.ui.DebugScreen
import org.hyperskill.app.android.stage_implementation.view.navigation.StageImplementationScreen
import org.hyperskill.app.android.stage_implementation.navigation.StageImplementationScreen
import org.hyperskill.app.android.step.view.navigation.StepScreen
import org.hyperskill.app.core.view.handleActions
import org.hyperskill.app.debug.presentation.DebugFeature
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,13 @@
package org.hyperskill.app.android.latex.view.model.block

object DataMobileHiddenBlock : ContentBlock {

private const val DATA_MOBILE_HIDDEN_TAG = "data-mobile-hidden"

override val header: String = """
<script type="text/javascript" src="file:///android_asset/scripts/remove_data_mobile_hidden_elements.js"></script>
""".trimIndent()

override fun isEnabled(content: String): Boolean =
DATA_MOBILE_HIDDEN_TAG in content
}
Original file line number Diff line number Diff line change
@@ -1,7 +1,13 @@
package org.hyperskill.app.android.latex.view.model.block

object RemoveIFrameElementsInjection : ContentBlock {

private const val I_FRAME_TAG = "iframe"

override val header: String = """
<script type="text/javascript" src="file:///android_asset/scripts/remove_iframes.js"></script>
""".trimIndent()

override fun isEnabled(content: String): Boolean =
I_FRAME_TAG in content
}
Loading

0 comments on commit 0337e89

Please sign in to comment.