diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index c0e06f0a89..1ab19508d5 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -109,6 +109,11 @@
android:screenOrientation="portrait"
android:windowSoftInputMode="adjustResize"
android:theme="@style/Theme.Session.DayNight.FlatActionBar" />
+
register(skipped = true)
+ Event.SUCCESS -> register(skipped = false)
+ }
+ }
+ }
+ }
+
+ @Composable
+ fun LoadingScreen() {
+ val state by viewModel.stateFlow.collectAsState()
+
+ val animatable = remember { Animatable(initialValue = 0f, visibilityThreshold = 0.005f) }
+
+ LaunchedEffect(state) {
+ animatable.stop()
+ animatable.animateTo(
+ targetValue = 1f,
+ animationSpec = TweenSpec(durationMillis = state.duration.inWholeMilliseconds.toInt())
+ )
+ }
+
+ AppTheme {
+ Column {
+ Spacer(modifier = Modifier.weight(1f))
+ ProgressArc(animatable.value, modifier = Modifier.align(Alignment.CenterHorizontally))
+ Text("One moment please..", modifier = Modifier.align(Alignment.CenterHorizontally), style = MaterialTheme.typography.h6)
+ Text("Loading your account", modifier = Modifier.align(Alignment.CenterHorizontally))
+ Spacer(modifier = Modifier.weight(2f))
+ }
+ }
+ }
+}
+
+fun Context.startLoadingActivity(mnemonic: ByteArray) {
+ Intent(this, LoadingActivity::class.java)
+ .apply { putExtra(EXTRA_MNEMONIC, mnemonic) }
+ .also(::startActivity)
+}
diff --git a/app/src/main/java/org/thoughtcrime/securesms/onboarding/LoadingViewModel.kt b/app/src/main/java/org/thoughtcrime/securesms/onboarding/LoadingViewModel.kt
new file mode 100644
index 0000000000..68bc5cc127
--- /dev/null
+++ b/app/src/main/java/org/thoughtcrime/securesms/onboarding/LoadingViewModel.kt
@@ -0,0 +1,102 @@
+package org.thoughtcrime.securesms.onboarding
+
+import android.content.Context
+import androidx.lifecycle.ViewModel
+import androidx.lifecycle.viewModelScope
+import dagger.hilt.android.lifecycle.HiltViewModel
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.Job
+import kotlinx.coroutines.channels.Channel
+import kotlinx.coroutines.delay
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.asStateFlow
+import kotlinx.coroutines.flow.filter
+import kotlinx.coroutines.flow.receiveAsFlow
+import kotlinx.coroutines.launch
+import org.session.libsession.snode.SnodeModule
+import org.session.libsession.utilities.TextSecurePreferences
+import org.session.libsignal.database.LokiAPIDatabaseProtocol
+import org.session.libsignal.utilities.KeyHelper
+import org.session.libsignal.utilities.hexEncodedPublicKey
+import org.thoughtcrime.securesms.ApplicationContext
+import org.thoughtcrime.securesms.crypto.KeyPairUtilities
+import org.thoughtcrime.securesms.dependencies.ConfigFactory
+import javax.inject.Inject
+import kotlin.time.Duration
+import kotlin.time.Duration.Companion.hours
+import kotlin.time.Duration.Companion.milliseconds
+import kotlin.time.Duration.Companion.seconds
+
+data class State(val duration: Duration)
+
+private val DONE_TIME = 1.seconds
+private val DONE_ANIMATE_TIME = 500.milliseconds
+
+private val TOTAL_ANIMATE_TIME = 14.seconds
+private val TOTAL_TIME = 15.seconds
+
+@HiltViewModel
+class LoadingViewModel @Inject constructor(
+ private val configFactory: ConfigFactory,
+ private val prefs: TextSecurePreferences,
+) : ViewModel() {
+
+ private val state = MutableStateFlow(State(TOTAL_ANIMATE_TIME))
+ val stateFlow = state.asStateFlow()
+
+ private val event = Channel()
+ val eventFlow = event.receiveAsFlow()
+
+ private var restoreJob: Job? = null
+
+ internal val database: LokiAPIDatabaseProtocol
+ get() = SnodeModule.shared.storage
+
+ fun restore(context: Context, seed: ByteArray) {
+
+ // only have one sync job running at a time (prevent QR from trying to spawn a new job)
+ if (restoreJob?.isActive == true) return
+
+ restoreJob = viewModelScope.launch(Dispatchers.IO) {
+ // This is here to resolve a case where the app restarts before a user completes onboarding
+ // which can result in an invalid database state
+ database.clearAllLastMessageHashes()
+ database.clearReceivedMessageHashValues()
+
+ // RestoreActivity handles seed this way
+ val keyPairGenerationResult = KeyPairUtilities.generate(seed)
+ val x25519KeyPair = keyPairGenerationResult.x25519KeyPair
+ KeyPairUtilities.store(context, seed, keyPairGenerationResult.ed25519KeyPair, x25519KeyPair)
+ configFactory.keyPairChanged()
+ val userHexEncodedPublicKey = x25519KeyPair.hexEncodedPublicKey
+ val registrationID = KeyHelper.generateRegistrationId(false)
+ prefs.apply {
+ setLocalRegistrationId(registrationID)
+ setLocalNumber(userHexEncodedPublicKey)
+ setRestorationTime(System.currentTimeMillis())
+ setHasViewedSeed(true)
+ }
+
+ val skipJob = launch(Dispatchers.IO) {
+ delay(TOTAL_TIME)
+ event.send(Event.TIMEOUT)
+ }
+
+ // start polling and wait for updated message
+ ApplicationContext.getInstance(context).apply { startPollingIfNeeded() }
+ TextSecurePreferences.events.filter { it == TextSecurePreferences.CONFIGURATION_SYNCED }.collect {
+ // handle we've synced
+ skipJob.cancel()
+
+ state.value = State(DONE_ANIMATE_TIME)
+ delay(DONE_TIME)
+ event.send(Event.SUCCESS)
+ }
+ }
+ }
+}
+
+sealed interface Event {
+ object SUCCESS: Event
+ object TIMEOUT: Event
+}
diff --git a/app/src/main/java/org/thoughtcrime/securesms/ui/Colors.kt b/app/src/main/java/org/thoughtcrime/securesms/ui/Colors.kt
index 55bc1be62e..32c375e54d 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/ui/Colors.kt
+++ b/app/src/main/java/org/thoughtcrime/securesms/ui/Colors.kt
@@ -42,6 +42,7 @@ const val oceanLight5 = 0xffE7F3F4
const val oceanLight6 = 0xffECFAFB
const val oceanLight7 = 0xffFCFFFF
+val session_accent = Color(0xFF31F196)
val ocean_accent = Color(0xff57C9FA)
val oceanLights = arrayOf(oceanLight0, oceanLight1, oceanLight2, oceanLight3, oceanLight4, oceanLight5, oceanLight6, oceanLight7)
diff --git a/app/src/main/java/org/thoughtcrime/securesms/ui/Components.kt b/app/src/main/java/org/thoughtcrime/securesms/ui/Components.kt
index 1724bde8a6..26949241dc 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/ui/Components.kt
+++ b/app/src/main/java/org/thoughtcrime/securesms/ui/Components.kt
@@ -1,6 +1,7 @@
package org.thoughtcrime.securesms.ui
import androidx.annotation.DrawableRes
+import androidx.compose.foundation.Canvas
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.BoxScope
@@ -10,6 +11,7 @@ import androidx.compose.foundation.layout.fillMaxHeight
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.layout.wrapContentHeight
import androidx.compose.foundation.pager.PagerState
@@ -26,9 +28,13 @@ import androidx.compose.runtime.Composable
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
+import androidx.compose.ui.geometry.Size
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.RectangleShape
+import androidx.compose.ui.graphics.StrokeCap
+import androidx.compose.ui.graphics.drawscope.Stroke
import androidx.compose.ui.res.painterResource
+import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import androidx.compose.ui.viewinterop.AndroidView
@@ -37,6 +43,7 @@ import kotlinx.coroutines.launch
import network.loki.messenger.R
import org.session.libsession.utilities.recipients.Recipient
import org.thoughtcrime.securesms.components.ProfilePictureView
+import kotlin.math.roundToInt
@Composable
fun ItemButton(
@@ -180,3 +187,49 @@ fun RowScope.Avatar(recipient: Recipient) {
)
}
}
+
+@Composable
+fun ProgressArc(progress: Float, modifier: Modifier = Modifier) {
+ val text = (progress * 100).roundToInt()
+
+ Box(modifier = modifier) {
+ Arc(percentage = progress, modifier = Modifier.align(Alignment.Center))
+ Text("${text}%", color = Color.White, modifier = Modifier.align(Alignment.Center), style = MaterialTheme.typography.h2)
+ }
+}
+
+@Composable
+fun Arc(
+ modifier: Modifier = Modifier,
+ percentage: Float = 0.25f,
+ fillColor: Color = session_accent,
+ backgroundColor: Color = classicDarkColors[3],
+ strokeWidth: Dp = 18.dp,
+ sweepAngle: Float = 310f,
+ startAngle: Float = (360f - sweepAngle) / 2 + 90f
+) {
+ Canvas(
+ modifier = modifier
+ .padding(strokeWidth)
+ .size(186.dp)
+ ) {
+ // Background Line
+ drawArc(
+ color = backgroundColor,
+ startAngle,
+ sweepAngle,
+ false,
+ style = Stroke(strokeWidth.toPx(), cap = StrokeCap.Round),
+ size = Size(size.width, size.height)
+ )
+
+ drawArc(
+ color = fillColor,
+ startAngle,
+ percentage * sweepAngle,
+ false,
+ style = Stroke(strokeWidth.toPx(), cap = StrokeCap.Round),
+ size = Size(size.width, size.height)
+ )
+ }
+}
diff --git a/app/src/main/res/layout/activity_link_device.xml b/app/src/main/res/layout/activity_link_device.xml
index b267c08ac8..5c93e251c9 100644
--- a/app/src/main/res/layout/activity_link_device.xml
+++ b/app/src/main/res/layout/activity_link_device.xml
@@ -1,7 +1,6 @@
@@ -18,28 +17,4 @@
-
-
-
-
-
-
-
-
-
-
\ No newline at end of file