mirror of
https://github.com/oxen-io/session-android.git
synced 2025-02-17 12:08:26 +00:00
Merge branch 'pr/1451' into feature/compose-cleanup
This commit is contained in:
commit
666a04c432
@ -125,7 +125,7 @@
|
||||
<activity
|
||||
android:name="org.thoughtcrime.securesms.home.HomeActivity"
|
||||
android:screenOrientation="portrait"
|
||||
android:launchMode="singleTask"
|
||||
android:launchMode="standard"
|
||||
android:theme="@style/Theme.Session.DayNight.NoActionBar" />
|
||||
<activity
|
||||
android:name="org.thoughtcrime.securesms.messagerequests.MessageRequestsActivity"
|
||||
|
@ -49,17 +49,20 @@ internal fun StartConversationScreen(
|
||||
ItemButton(
|
||||
textId = R.string.messageNew,
|
||||
icon = R.drawable.ic_message,
|
||||
modifier = Modifier.contentDescription(R.string.AccessibilityId_new_direct_message),
|
||||
onClick = delegate::onNewMessageSelected)
|
||||
Divider(startIndent = LocalDimensions.current.dividerIndent)
|
||||
ItemButton(
|
||||
textId = R.string.activity_create_group_title,
|
||||
icon = R.drawable.ic_group,
|
||||
modifier = Modifier.contentDescription(R.string.AccessibilityId_create_group),
|
||||
onClick = delegate::onCreateGroupSelected
|
||||
)
|
||||
Divider(startIndent = LocalDimensions.current.dividerIndent)
|
||||
ItemButton(
|
||||
textId = R.string.dialog_join_community_title,
|
||||
icon = R.drawable.ic_globe,
|
||||
modifier = Modifier.contentDescription(R.string.AccessibilityId_join_community),
|
||||
onClick = delegate::onJoinCommunitySelected
|
||||
)
|
||||
Divider(startIndent = LocalDimensions.current.dividerIndent)
|
||||
|
@ -138,6 +138,9 @@ class HomeActivity : PassphraseRequiredActionBarActivity(),
|
||||
// region Lifecycle
|
||||
override fun onCreate(savedInstanceState: Bundle?, isReady: Boolean) {
|
||||
super.onCreate(savedInstanceState, isReady)
|
||||
|
||||
if (!isTaskRoot) { finish(); return }
|
||||
|
||||
// Set content view
|
||||
binding = ActivityHomeBinding.inflate(layoutInflater)
|
||||
setContentView(binding.root)
|
||||
|
@ -1,5 +1,6 @@
|
||||
package org.thoughtcrime.securesms.onboarding
|
||||
|
||||
import androidx.annotation.StringRes
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import network.loki.messenger.R
|
||||
@ -11,12 +12,13 @@ import org.thoughtcrime.securesms.ui.theme.LocalColors
|
||||
@Composable
|
||||
fun OnboardingBackPressAlertDialog(
|
||||
dismissDialog: () -> Unit,
|
||||
@StringRes textId: Int = R.string.you_cannot_go_back_further_in_order_to_stop_loading_your_account_session_needs_to_quit,
|
||||
quit: () -> Unit
|
||||
) {
|
||||
AlertDialog(
|
||||
onDismissRequest = dismissDialog,
|
||||
title = stringResource(R.string.warning),
|
||||
text = stringResource(R.string.you_cannot_go_back_further_in_order_to_stop_loading_your_account_session_needs_to_quit),
|
||||
text = stringResource(textId),
|
||||
buttons = listOf(
|
||||
DialogButtonModel(
|
||||
GetString(stringResource(R.string.quit)),
|
||||
|
@ -1,14 +1,10 @@
|
||||
package org.thoughtcrime.securesms.onboarding.loading
|
||||
|
||||
import androidx.compose.animation.core.Animatable
|
||||
import androidx.compose.animation.core.TweenSpec
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.material.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.res.stringResource
|
||||
@ -20,21 +16,11 @@ import org.thoughtcrime.securesms.ui.contentDescription
|
||||
import org.thoughtcrime.securesms.ui.theme.h7
|
||||
|
||||
@Composable
|
||||
internal fun LoadingScreen(state: State) {
|
||||
val animatable = remember { Animatable(initialValue = 0f, visibilityThreshold = 0.005f) }
|
||||
|
||||
LaunchedEffect(state) {
|
||||
animatable.stop()
|
||||
animatable.animateTo(
|
||||
targetValue = 1f,
|
||||
animationSpec = TweenSpec(durationMillis = state.duration.inWholeMilliseconds.toInt())
|
||||
)
|
||||
}
|
||||
|
||||
internal fun LoadingScreen(progress: Float) {
|
||||
Column(horizontalAlignment = Alignment.CenterHorizontally) {
|
||||
Spacer(modifier = Modifier.weight(1f))
|
||||
ProgressArc(
|
||||
animatable.value,
|
||||
progress,
|
||||
modifier = Modifier.contentDescription(R.string.AccessibilityId_loading_animation)
|
||||
)
|
||||
Text(
|
||||
|
@ -47,8 +47,8 @@ class LoadingActivity: BaseActionBarActivity() {
|
||||
ApplicationContext.getInstance(this).newAccount = false
|
||||
|
||||
setComposeContent {
|
||||
val state by viewModel.states.collectAsState()
|
||||
LoadingScreen(state)
|
||||
val progress by viewModel.progress.collectAsState()
|
||||
LoadingScreen(progress)
|
||||
}
|
||||
|
||||
lifecycleScope.launch {
|
||||
|
@ -4,15 +4,20 @@ import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.FlowPreview
|
||||
import kotlinx.coroutines.channels.BufferOverflow
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.MutableSharedFlow
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.asSharedFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
import kotlinx.coroutines.flow.buffer
|
||||
import kotlinx.coroutines.flow.collectLatest
|
||||
import kotlinx.coroutines.flow.filter
|
||||
import kotlinx.coroutines.flow.flowOn
|
||||
import kotlinx.coroutines.flow.flatMapLatest
|
||||
import kotlinx.coroutines.flow.flow
|
||||
import kotlinx.coroutines.flow.onStart
|
||||
import kotlinx.coroutines.flow.timeout
|
||||
import kotlinx.coroutines.launch
|
||||
@ -23,25 +28,43 @@ import kotlin.time.Duration
|
||||
import kotlin.time.Duration.Companion.milliseconds
|
||||
import kotlin.time.Duration.Companion.seconds
|
||||
|
||||
data class State(val duration: Duration)
|
||||
enum class State {
|
||||
LOADING,
|
||||
SUCCESS,
|
||||
FAIL
|
||||
}
|
||||
|
||||
private val ANIMATE_TO_DONE_TIME = 500.milliseconds
|
||||
private val IDLE_DONE_TIME = 1.seconds
|
||||
private val TIMEOUT_TIME = 15.seconds
|
||||
|
||||
@OptIn(FlowPreview::class)
|
||||
private val REFRESH_TIME = 50.milliseconds
|
||||
|
||||
@OptIn(FlowPreview::class, ExperimentalCoroutinesApi::class)
|
||||
@HiltViewModel
|
||||
internal class LoadingViewModel @Inject constructor(
|
||||
val prefs: TextSecurePreferences
|
||||
): ViewModel() {
|
||||
|
||||
private val _states = MutableStateFlow(State(TIMEOUT_TIME))
|
||||
val states = _states.asStateFlow()
|
||||
private val state = MutableStateFlow(State.LOADING)
|
||||
|
||||
private val _progress = MutableStateFlow(0f)
|
||||
val progress = _progress.asStateFlow()
|
||||
|
||||
private val _events = MutableSharedFlow<Event>()
|
||||
val events = _events.asSharedFlow()
|
||||
|
||||
init {
|
||||
viewModelScope.launch(Dispatchers.IO) {
|
||||
state.flatMapLatest {
|
||||
when (it) {
|
||||
State.LOADING -> progress(0f, 1f, TIMEOUT_TIME)
|
||||
else -> progress(progress.value, 1f, ANIMATE_TO_DONE_TIME)
|
||||
}
|
||||
}.buffer(0, BufferOverflow.DROP_OLDEST)
|
||||
.collectLatest { _progress.value = it }
|
||||
}
|
||||
|
||||
viewModelScope.launch(Dispatchers.IO) {
|
||||
try {
|
||||
TextSecurePreferences.events
|
||||
@ -58,7 +81,7 @@ internal class LoadingViewModel @Inject constructor(
|
||||
|
||||
private suspend fun onSuccess() {
|
||||
withContext(Dispatchers.Main) {
|
||||
_states.value = State(ANIMATE_TO_DONE_TIME)
|
||||
state.value = State.SUCCESS
|
||||
delay(IDLE_DONE_TIME)
|
||||
_events.emit(Event.SUCCESS)
|
||||
}
|
||||
@ -66,6 +89,8 @@ internal class LoadingViewModel @Inject constructor(
|
||||
|
||||
private suspend fun onFail() {
|
||||
withContext(Dispatchers.Main) {
|
||||
state.value = State.FAIL
|
||||
delay(IDLE_DONE_TIME)
|
||||
_events.emit(Event.TIMEOUT)
|
||||
}
|
||||
}
|
||||
@ -75,3 +100,22 @@ sealed interface Event {
|
||||
object SUCCESS: Event
|
||||
object TIMEOUT: Event
|
||||
}
|
||||
|
||||
private fun progress(
|
||||
init: Float,
|
||||
target: Float,
|
||||
time: Duration,
|
||||
refreshRate: Duration = REFRESH_TIME
|
||||
): Flow<Float> = flow {
|
||||
val startMs = System.currentTimeMillis()
|
||||
val timeMs = time.inWholeMilliseconds
|
||||
val finishMs = startMs + timeMs
|
||||
val range = target - init
|
||||
|
||||
generateSequence { System.currentTimeMillis() }.takeWhile { it < finishMs }.forEach {
|
||||
emit((it - startMs) * range / timeMs + init)
|
||||
delay(refreshRate)
|
||||
}
|
||||
|
||||
emit(target)
|
||||
}
|
||||
|
@ -55,7 +55,7 @@ internal fun MessageNotificationsScreen(
|
||||
return
|
||||
}
|
||||
|
||||
if (state.showDialog) OnboardingBackPressAlertDialog(dismissDialog, quit)
|
||||
if (state.showDialog) OnboardingBackPressAlertDialog(dismissDialog, quit = quit)
|
||||
|
||||
Column {
|
||||
Spacer(Modifier.weight(1f))
|
||||
|
@ -40,7 +40,11 @@ internal fun PickDisplayName(
|
||||
quit: () -> Unit = {}
|
||||
) {
|
||||
|
||||
if (state.showDialog) OnboardingBackPressAlertDialog(dismissDialog, quit)
|
||||
if (state.showDialog) OnboardingBackPressAlertDialog(
|
||||
dismissDialog,
|
||||
R.string.you_cannot_go_back_further_cancel_account_creation,
|
||||
quit
|
||||
)
|
||||
|
||||
Column(
|
||||
modifier = Modifier
|
||||
|
@ -293,6 +293,7 @@ class SettingsActivity : PassphraseRequiredActionBarActivity() {
|
||||
val userConfig = configFactory.user
|
||||
AvatarHelper.setAvatar(this, Address.fromSerialized(TextSecurePreferences.getLocalNumber(this)!!), profilePicture)
|
||||
prefs.setProfileAvatarId(profilePicture?.let { SecureRandom().nextInt() } ?: 0 )
|
||||
|
||||
ProfileKeyUtil.setEncodedProfileKey(this, encodedProfileKey)
|
||||
// new config
|
||||
val url = TextSecurePreferences.getProfilePictureURL(this)
|
||||
|
@ -49,7 +49,7 @@
|
||||
<string name="AccessibilityId_new_conversation_button">New conversation button</string>
|
||||
<string name="AccessibilityId_new_direct_message">New direct message</string>
|
||||
<string name="AccessibilityId_create_group">Create group</string>
|
||||
<string name="AccessibilityId_join_community">Join community button</string>
|
||||
<string name="AccessibilityId_join_community">Join community</string>
|
||||
<!-- Join community pop up -->
|
||||
<string name="AccessibilityId_community_input_box">Community input</string>
|
||||
<string name="AccessibilityId_join_community_button">Join community button</string>
|
||||
@ -1140,5 +1140,6 @@
|
||||
<string name="AccessibilityId_qr_code">QR code</string>
|
||||
<string name="warning">Warning</string>
|
||||
<string name="you_cannot_go_back_further_in_order_to_stop_loading_your_account_session_needs_to_quit">You cannot go back further. In order to stop loading your account, Session needs to quit.</string>
|
||||
<string name="you_cannot_go_back_further_cancel_account_creation">You cannot go back further. In order to cancel your account creation, Session needs to quit.</string>
|
||||
<string name="quit">Quit</string>
|
||||
</resources>
|
||||
|
@ -21,8 +21,8 @@ googleServicesVersion=4.3.12
|
||||
kotlinVersion=1.8.21
|
||||
android.useAndroidX=true
|
||||
appcompatVersion=1.6.1
|
||||
composeVersion=1.6.4
|
||||
coreVersion=1.13.1
|
||||
composeVersion=1.6.4
|
||||
coroutinesVersion=1.6.4
|
||||
curve25519Version=0.6.0
|
||||
daggerVersion=2.46.1
|
||||
|
@ -35,14 +35,14 @@ class MnemonicCodecTest {
|
||||
|
||||
@Test
|
||||
fun `decode one invalid word that is too short`() {
|
||||
assertThrows(InvalidWord::class.java) {
|
||||
assertThrows(InputTooShort::class.java) {
|
||||
codec.decode("a")
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `decode one invalid word`() {
|
||||
assertThrows(InvalidWord::class.java) {
|
||||
assertThrows(InputTooShort::class.java) {
|
||||
codec.decode("abcd")
|
||||
}
|
||||
}
|
||||
@ -96,12 +96,20 @@ class MnemonicCodecTest {
|
||||
|
||||
assertEquals("0f2ccde528622876b8f16e14db97dafc", result)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `decodeMnemonicOrHexAsByteArray with account id throws`() {
|
||||
assertThrows(InputTooShort::class.java) {
|
||||
codec.decodeMnemonicOrHexAsByteArray("0582e1421da6f584a4795d30b654b4f25fed860afdf081075cb26a2b997e492f14").let(Hex::toStringCondensed)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `decodeMnemonicOrHexAsByteArray with bad hex`() {
|
||||
// throws InvalidWord as 0f2ccde528622876b8f16e14db97dafcg is not a valid word on the english wordlist.
|
||||
// It is also not a valid hex string, but we assume that a non-hex string is a recovery password.
|
||||
|
||||
assertThrows(InvalidWord::class.java) {
|
||||
assertThrows(InputTooShort::class.java) {
|
||||
codec.decodeMnemonicOrHexAsByteArray("0f2ccde528622876b8f16e14db97dafcg").let(Hex::toStringCondensed)
|
||||
}
|
||||
}
|
||||
|
@ -83,8 +83,8 @@ class MnemonicCodec(private val loadFileContents: (String) -> String) {
|
||||
val prefixLength = languageConfiguration.prefixLength
|
||||
val n = truncatedWordSet.size.toLong()
|
||||
|
||||
if (mnemonic.isEmpty()) throw DecodingError.InputTooShort
|
||||
if (words.isEmpty()) throw DecodingError.InputTooShort
|
||||
// Check preconditions
|
||||
if (words.size < 13) throw DecodingError.InputTooShort
|
||||
|
||||
fun String.prefix() = substring(0 until prefixLength)
|
||||
|
||||
@ -96,9 +96,6 @@ class MnemonicCodec(private val loadFileContents: (String) -> String) {
|
||||
val wordIndexes = wordPrefixes.map { truncatedWordSet.indexOf(it) }
|
||||
.onEach { if (it < 0) throw DecodingError.InvalidWord }
|
||||
|
||||
// Check preconditions
|
||||
if (words.size < 13) throw DecodingError.InputTooShort
|
||||
|
||||
// Verify checksum
|
||||
val checksumIndex = determineChecksumIndex(words.dropLast(1), prefixLength)
|
||||
val expectedChecksumWord = words[checksumIndex]
|
||||
@ -128,13 +125,12 @@ class MnemonicCodec(private val loadFileContents: (String) -> String) {
|
||||
decode(mnemonic = mnemonicOrHex).let(Hex::fromStringCondensed)
|
||||
} catch (decodeException: Exception) {
|
||||
// It's not a valid mnemonic, if it's pure-hexadecimal then we'll interpret it as a
|
||||
// hexadecimal-byte encoded mnemonic.
|
||||
if (!mnemonicOrHex.isHex()) throw decodeException
|
||||
try {
|
||||
Hex.fromStringCondensed(mnemonicOrHex)
|
||||
} catch (_: Exception) {
|
||||
throw decodeException
|
||||
}
|
||||
// hexadecimal-byte encoded mnemonic... unless it's 66 chars or longer, then it could be
|
||||
// an account id.
|
||||
mnemonicOrHex.takeIf { it.length < 66 && it.isHex() }
|
||||
.runCatching { Hex.fromStringCondensed(this) }
|
||||
.getOrNull()
|
||||
?: throw decodeException
|
||||
}
|
||||
}
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user