Merge branch 'pr/1451' into feature/compose-cleanup

This commit is contained in:
ThomasSession 2024-07-12 08:53:07 +10:00
commit 666a04c432
14 changed files with 93 additions and 45 deletions

View File

@ -125,7 +125,7 @@
<activity <activity
android:name="org.thoughtcrime.securesms.home.HomeActivity" android:name="org.thoughtcrime.securesms.home.HomeActivity"
android:screenOrientation="portrait" android:screenOrientation="portrait"
android:launchMode="singleTask" android:launchMode="standard"
android:theme="@style/Theme.Session.DayNight.NoActionBar" /> android:theme="@style/Theme.Session.DayNight.NoActionBar" />
<activity <activity
android:name="org.thoughtcrime.securesms.messagerequests.MessageRequestsActivity" android:name="org.thoughtcrime.securesms.messagerequests.MessageRequestsActivity"

View File

@ -49,17 +49,20 @@ internal fun StartConversationScreen(
ItemButton( ItemButton(
textId = R.string.messageNew, textId = R.string.messageNew,
icon = R.drawable.ic_message, icon = R.drawable.ic_message,
modifier = Modifier.contentDescription(R.string.AccessibilityId_new_direct_message),
onClick = delegate::onNewMessageSelected) onClick = delegate::onNewMessageSelected)
Divider(startIndent = LocalDimensions.current.dividerIndent) Divider(startIndent = LocalDimensions.current.dividerIndent)
ItemButton( ItemButton(
textId = R.string.activity_create_group_title, textId = R.string.activity_create_group_title,
icon = R.drawable.ic_group, icon = R.drawable.ic_group,
modifier = Modifier.contentDescription(R.string.AccessibilityId_create_group),
onClick = delegate::onCreateGroupSelected onClick = delegate::onCreateGroupSelected
) )
Divider(startIndent = LocalDimensions.current.dividerIndent) Divider(startIndent = LocalDimensions.current.dividerIndent)
ItemButton( ItemButton(
textId = R.string.dialog_join_community_title, textId = R.string.dialog_join_community_title,
icon = R.drawable.ic_globe, icon = R.drawable.ic_globe,
modifier = Modifier.contentDescription(R.string.AccessibilityId_join_community),
onClick = delegate::onJoinCommunitySelected onClick = delegate::onJoinCommunitySelected
) )
Divider(startIndent = LocalDimensions.current.dividerIndent) Divider(startIndent = LocalDimensions.current.dividerIndent)

View File

@ -138,6 +138,9 @@ class HomeActivity : PassphraseRequiredActionBarActivity(),
// region Lifecycle // region Lifecycle
override fun onCreate(savedInstanceState: Bundle?, isReady: Boolean) { override fun onCreate(savedInstanceState: Bundle?, isReady: Boolean) {
super.onCreate(savedInstanceState, isReady) super.onCreate(savedInstanceState, isReady)
if (!isTaskRoot) { finish(); return }
// Set content view // Set content view
binding = ActivityHomeBinding.inflate(layoutInflater) binding = ActivityHomeBinding.inflate(layoutInflater)
setContentView(binding.root) setContentView(binding.root)

View File

@ -1,5 +1,6 @@
package org.thoughtcrime.securesms.onboarding package org.thoughtcrime.securesms.onboarding
import androidx.annotation.StringRes
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import network.loki.messenger.R import network.loki.messenger.R
@ -11,12 +12,13 @@ import org.thoughtcrime.securesms.ui.theme.LocalColors
@Composable @Composable
fun OnboardingBackPressAlertDialog( fun OnboardingBackPressAlertDialog(
dismissDialog: () -> Unit, 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 quit: () -> Unit
) { ) {
AlertDialog( AlertDialog(
onDismissRequest = dismissDialog, onDismissRequest = dismissDialog,
title = stringResource(R.string.warning), 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( buttons = listOf(
DialogButtonModel( DialogButtonModel(
GetString(stringResource(R.string.quit)), GetString(stringResource(R.string.quit)),

View File

@ -1,14 +1,10 @@
package org.thoughtcrime.securesms.onboarding.loading 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.Column
import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.height
import androidx.compose.material.Text import androidx.compose.material.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
@ -20,21 +16,11 @@ import org.thoughtcrime.securesms.ui.contentDescription
import org.thoughtcrime.securesms.ui.theme.h7 import org.thoughtcrime.securesms.ui.theme.h7
@Composable @Composable
internal fun LoadingScreen(state: State) { internal fun LoadingScreen(progress: Float) {
val animatable = remember { Animatable(initialValue = 0f, visibilityThreshold = 0.005f) }
LaunchedEffect(state) {
animatable.stop()
animatable.animateTo(
targetValue = 1f,
animationSpec = TweenSpec(durationMillis = state.duration.inWholeMilliseconds.toInt())
)
}
Column(horizontalAlignment = Alignment.CenterHorizontally) { Column(horizontalAlignment = Alignment.CenterHorizontally) {
Spacer(modifier = Modifier.weight(1f)) Spacer(modifier = Modifier.weight(1f))
ProgressArc( ProgressArc(
animatable.value, progress,
modifier = Modifier.contentDescription(R.string.AccessibilityId_loading_animation) modifier = Modifier.contentDescription(R.string.AccessibilityId_loading_animation)
) )
Text( Text(

View File

@ -47,8 +47,8 @@ class LoadingActivity: BaseActionBarActivity() {
ApplicationContext.getInstance(this).newAccount = false ApplicationContext.getInstance(this).newAccount = false
setComposeContent { setComposeContent {
val state by viewModel.states.collectAsState() val progress by viewModel.progress.collectAsState()
LoadingScreen(state) LoadingScreen(progress)
} }
lifecycleScope.launch { lifecycleScope.launch {

View File

@ -4,15 +4,20 @@ import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import dagger.hilt.android.lifecycle.HiltViewModel import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.FlowPreview import kotlinx.coroutines.FlowPreview
import kotlinx.coroutines.channels.BufferOverflow
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asSharedFlow import kotlinx.coroutines.flow.asSharedFlow
import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.buffer
import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.flow.filter 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.onStart
import kotlinx.coroutines.flow.timeout import kotlinx.coroutines.flow.timeout
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
@ -23,25 +28,43 @@ import kotlin.time.Duration
import kotlin.time.Duration.Companion.milliseconds import kotlin.time.Duration.Companion.milliseconds
import kotlin.time.Duration.Companion.seconds 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 ANIMATE_TO_DONE_TIME = 500.milliseconds
private val IDLE_DONE_TIME = 1.seconds private val IDLE_DONE_TIME = 1.seconds
private val TIMEOUT_TIME = 15.seconds private val TIMEOUT_TIME = 15.seconds
@OptIn(FlowPreview::class) private val REFRESH_TIME = 50.milliseconds
@OptIn(FlowPreview::class, ExperimentalCoroutinesApi::class)
@HiltViewModel @HiltViewModel
internal class LoadingViewModel @Inject constructor( internal class LoadingViewModel @Inject constructor(
val prefs: TextSecurePreferences val prefs: TextSecurePreferences
): ViewModel() { ): ViewModel() {
private val _states = MutableStateFlow(State(TIMEOUT_TIME)) private val state = MutableStateFlow(State.LOADING)
val states = _states.asStateFlow()
private val _progress = MutableStateFlow(0f)
val progress = _progress.asStateFlow()
private val _events = MutableSharedFlow<Event>() private val _events = MutableSharedFlow<Event>()
val events = _events.asSharedFlow() val events = _events.asSharedFlow()
init { 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) { viewModelScope.launch(Dispatchers.IO) {
try { try {
TextSecurePreferences.events TextSecurePreferences.events
@ -58,7 +81,7 @@ internal class LoadingViewModel @Inject constructor(
private suspend fun onSuccess() { private suspend fun onSuccess() {
withContext(Dispatchers.Main) { withContext(Dispatchers.Main) {
_states.value = State(ANIMATE_TO_DONE_TIME) state.value = State.SUCCESS
delay(IDLE_DONE_TIME) delay(IDLE_DONE_TIME)
_events.emit(Event.SUCCESS) _events.emit(Event.SUCCESS)
} }
@ -66,6 +89,8 @@ internal class LoadingViewModel @Inject constructor(
private suspend fun onFail() { private suspend fun onFail() {
withContext(Dispatchers.Main) { withContext(Dispatchers.Main) {
state.value = State.FAIL
delay(IDLE_DONE_TIME)
_events.emit(Event.TIMEOUT) _events.emit(Event.TIMEOUT)
} }
} }
@ -75,3 +100,22 @@ sealed interface Event {
object SUCCESS: Event object SUCCESS: Event
object TIMEOUT: 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)
}

View File

@ -55,7 +55,7 @@ internal fun MessageNotificationsScreen(
return return
} }
if (state.showDialog) OnboardingBackPressAlertDialog(dismissDialog, quit) if (state.showDialog) OnboardingBackPressAlertDialog(dismissDialog, quit = quit)
Column { Column {
Spacer(Modifier.weight(1f)) Spacer(Modifier.weight(1f))

View File

@ -40,7 +40,11 @@ internal fun PickDisplayName(
quit: () -> Unit = {} 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( Column(
modifier = Modifier modifier = Modifier

View File

@ -293,6 +293,7 @@ class SettingsActivity : PassphraseRequiredActionBarActivity() {
val userConfig = configFactory.user val userConfig = configFactory.user
AvatarHelper.setAvatar(this, Address.fromSerialized(TextSecurePreferences.getLocalNumber(this)!!), profilePicture) AvatarHelper.setAvatar(this, Address.fromSerialized(TextSecurePreferences.getLocalNumber(this)!!), profilePicture)
prefs.setProfileAvatarId(profilePicture?.let { SecureRandom().nextInt() } ?: 0 ) prefs.setProfileAvatarId(profilePicture?.let { SecureRandom().nextInt() } ?: 0 )
ProfileKeyUtil.setEncodedProfileKey(this, encodedProfileKey) ProfileKeyUtil.setEncodedProfileKey(this, encodedProfileKey)
// new config // new config
val url = TextSecurePreferences.getProfilePictureURL(this) val url = TextSecurePreferences.getProfilePictureURL(this)

View File

@ -49,7 +49,7 @@
<string name="AccessibilityId_new_conversation_button">New conversation button</string> <string name="AccessibilityId_new_conversation_button">New conversation button</string>
<string name="AccessibilityId_new_direct_message">New direct message</string> <string name="AccessibilityId_new_direct_message">New direct message</string>
<string name="AccessibilityId_create_group">Create group</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 --> <!-- Join community pop up -->
<string name="AccessibilityId_community_input_box">Community input</string> <string name="AccessibilityId_community_input_box">Community input</string>
<string name="AccessibilityId_join_community_button">Join community button</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="AccessibilityId_qr_code">QR code</string>
<string name="warning">Warning</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_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> <string name="quit">Quit</string>
</resources> </resources>

View File

@ -21,8 +21,8 @@ googleServicesVersion=4.3.12
kotlinVersion=1.8.21 kotlinVersion=1.8.21
android.useAndroidX=true android.useAndroidX=true
appcompatVersion=1.6.1 appcompatVersion=1.6.1
composeVersion=1.6.4
coreVersion=1.13.1 coreVersion=1.13.1
composeVersion=1.6.4
coroutinesVersion=1.6.4 coroutinesVersion=1.6.4
curve25519Version=0.6.0 curve25519Version=0.6.0
daggerVersion=2.46.1 daggerVersion=2.46.1

View File

@ -35,14 +35,14 @@ class MnemonicCodecTest {
@Test @Test
fun `decode one invalid word that is too short`() { fun `decode one invalid word that is too short`() {
assertThrows(InvalidWord::class.java) { assertThrows(InputTooShort::class.java) {
codec.decode("a") codec.decode("a")
} }
} }
@Test @Test
fun `decode one invalid word`() { fun `decode one invalid word`() {
assertThrows(InvalidWord::class.java) { assertThrows(InputTooShort::class.java) {
codec.decode("abcd") codec.decode("abcd")
} }
} }
@ -96,12 +96,20 @@ class MnemonicCodecTest {
assertEquals("0f2ccde528622876b8f16e14db97dafc", result) assertEquals("0f2ccde528622876b8f16e14db97dafc", result)
} }
@Test
fun `decodeMnemonicOrHexAsByteArray with account id throws`() {
assertThrows(InputTooShort::class.java) {
codec.decodeMnemonicOrHexAsByteArray("0582e1421da6f584a4795d30b654b4f25fed860afdf081075cb26a2b997e492f14").let(Hex::toStringCondensed)
}
}
@Test @Test
fun `decodeMnemonicOrHexAsByteArray with bad hex`() { fun `decodeMnemonicOrHexAsByteArray with bad hex`() {
// throws InvalidWord as 0f2ccde528622876b8f16e14db97dafcg is not a valid word on the english wordlist. // 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. // 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) codec.decodeMnemonicOrHexAsByteArray("0f2ccde528622876b8f16e14db97dafcg").let(Hex::toStringCondensed)
} }
} }

View File

@ -83,8 +83,8 @@ class MnemonicCodec(private val loadFileContents: (String) -> String) {
val prefixLength = languageConfiguration.prefixLength val prefixLength = languageConfiguration.prefixLength
val n = truncatedWordSet.size.toLong() val n = truncatedWordSet.size.toLong()
if (mnemonic.isEmpty()) throw DecodingError.InputTooShort // Check preconditions
if (words.isEmpty()) throw DecodingError.InputTooShort if (words.size < 13) throw DecodingError.InputTooShort
fun String.prefix() = substring(0 until prefixLength) 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) } val wordIndexes = wordPrefixes.map { truncatedWordSet.indexOf(it) }
.onEach { if (it < 0) throw DecodingError.InvalidWord } .onEach { if (it < 0) throw DecodingError.InvalidWord }
// Check preconditions
if (words.size < 13) throw DecodingError.InputTooShort
// Verify checksum // Verify checksum
val checksumIndex = determineChecksumIndex(words.dropLast(1), prefixLength) val checksumIndex = determineChecksumIndex(words.dropLast(1), prefixLength)
val expectedChecksumWord = words[checksumIndex] val expectedChecksumWord = words[checksumIndex]
@ -128,13 +125,12 @@ class MnemonicCodec(private val loadFileContents: (String) -> String) {
decode(mnemonic = mnemonicOrHex).let(Hex::fromStringCondensed) decode(mnemonic = mnemonicOrHex).let(Hex::fromStringCondensed)
} catch (decodeException: Exception) { } catch (decodeException: Exception) {
// It's not a valid mnemonic, if it's pure-hexadecimal then we'll interpret it as a // It's not a valid mnemonic, if it's pure-hexadecimal then we'll interpret it as a
// hexadecimal-byte encoded mnemonic. // hexadecimal-byte encoded mnemonic... unless it's 66 chars or longer, then it could be
if (!mnemonicOrHex.isHex()) throw decodeException // an account id.
try { mnemonicOrHex.takeIf { it.length < 66 && it.isHex() }
Hex.fromStringCondensed(mnemonicOrHex) .runCatching { Hex.fromStringCondensed(this) }
} catch (_: Exception) { .getOrNull()
throw decodeException ?: throw decodeException
}
} }
} }