This commit is contained in:
andrew 2023-08-31 17:51:14 +09:30
parent 58c4467749
commit ef24fb0fd1
4 changed files with 185 additions and 51 deletions

View File

@ -153,7 +153,7 @@ fun DisappearingMessages(
.fadingEdges(scrollState), .fadingEdges(scrollState),
verticalArrangement = Arrangement.spacedBy(16.dp) verticalArrangement = Arrangement.spacedBy(16.dp)
) { ) {
state.cards.filter { it.options.isNotEmpty() }.forEach { state.cards.forEach {
OptionsCard(it) OptionsCard(it)
} }

View File

@ -1,7 +1,10 @@
package org.thoughtcrime.securesms.conversation.expiration package org.thoughtcrime.securesms.conversation.expiration
import android.app.Application
import android.content.Context import android.content.Context
import androidx.annotation.StringRes import androidx.annotation.StringRes
import androidx.compose.runtime.collectAsState
import androidx.lifecycle.AndroidViewModel
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
@ -10,9 +13,11 @@ import dagger.assisted.AssistedInject
import dagger.hilt.android.qualifiers.ApplicationContext import dagger.hilt.android.qualifiers.ApplicationContext
import kotlinx.coroutines.channels.Channel import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.receiveAsFlow import kotlinx.coroutines.flow.receiveAsFlow
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.flow.update import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import network.loki.messenger.BuildConfig import network.loki.messenger.BuildConfig
@ -71,14 +76,14 @@ object NoOpCallbacks: Callbacks
class ExpirationSettingsViewModel( class ExpirationSettingsViewModel(
private val threadId: Long, private val threadId: Long,
private val context: Context, private val application: Application,
private val textSecurePreferences: TextSecurePreferences, private val textSecurePreferences: TextSecurePreferences,
private val messageExpirationManager: MessageExpirationManagerProtocol, private val messageExpirationManager: MessageExpirationManagerProtocol,
private val threadDb: ThreadDatabase, private val threadDb: ThreadDatabase,
private val groupDb: GroupDatabase, private val groupDb: GroupDatabase,
private val storage: Storage, private val storage: Storage,
isNewConfigEnabled: Boolean isNewConfigEnabled: Boolean
) : ViewModel(), Callbacks { ) : AndroidViewModel(application), Callbacks {
private val _event = Channel<Event>() private val _event = Channel<Event>()
val event = _event.receiveAsFlow() val event = _event.receiveAsFlow()
@ -89,7 +94,9 @@ class ExpirationSettingsViewModel(
)) ))
val state = _state.asStateFlow() val state = _state.asStateFlow()
val uiState = _state.map { UiState(it) } val uiState = _state
.map(::UiState)
.stateIn(viewModelScope, SharingStarted.Eagerly, UiState())
private var expirationConfig: ExpirationConfiguration? = null private var expirationConfig: ExpirationConfiguration? = null
@ -161,7 +168,7 @@ class ExpirationSettingsViewModel(
MessageSender.send(message, address) MessageSender.send(message, address)
} }
ConfigurationMessageUtilities.forceSyncConfigurationNowIfNeeded(context) ConfigurationMessageUtilities.forceSyncConfigurationNowIfNeeded(application)
_event.send(Event.SUCCESS) _event.send(Event.SUCCESS)
} }
@ -174,7 +181,7 @@ class ExpirationSettingsViewModel(
@Suppress("UNCHECKED_CAST") @Suppress("UNCHECKED_CAST")
class Factory @AssistedInject constructor( class Factory @AssistedInject constructor(
@Assisted private val threadId: Long, @Assisted private val threadId: Long,
@ApplicationContext private val context: Context, private val application: Application,
private val textSecurePreferences: TextSecurePreferences, private val textSecurePreferences: TextSecurePreferences,
private val messageExpirationManager: MessageExpirationManagerProtocol, private val messageExpirationManager: MessageExpirationManagerProtocol,
private val threadDb: ThreadDatabase, private val threadDb: ThreadDatabase,
@ -184,7 +191,7 @@ class ExpirationSettingsViewModel(
override fun <T : ViewModel> create(modelClass: Class<T>): T = ExpirationSettingsViewModel( override fun <T : ViewModel> create(modelClass: Class<T>): T = ExpirationSettingsViewModel(
threadId, threadId,
context, application,
textSecurePreferences, textSecurePreferences,
messageExpirationManager, messageExpirationManager,
threadDb, threadDb,
@ -201,9 +208,9 @@ data class UiState(
val callbacks: Callbacks = NoOpCallbacks val callbacks: Callbacks = NoOpCallbacks
) { ) {
constructor(state: State): this( constructor(state: State): this(
cards = listOf( cards = listOfNotNull(
CardModel(GetString(R.string.activity_expiration_settings_delete_type), typeOptions(state)), typeOptions(state)?.let { CardModel(GetString(R.string.activity_expiration_settings_delete_type), it) },
CardModel(GetString(R.string.activity_expiration_settings_timer), timeOptions(state)) timeOptions(state)?.let { CardModel(GetString(R.string.activity_expiration_settings_timer), it) }
), ),
showGroupFooter = state.isGroup && state.isNewConfigEnabled, showGroupFooter = state.isGroup && state.isNewConfigEnabled,
callbacks = state.callbacks callbacks = state.callbacks
@ -216,39 +223,42 @@ data class CardModel(
) )
private fun typeOptions(state: State) = private fun typeOptions(state: State) =
if (state.isNoteToSelf || (state.isGroup && state.isNewConfigEnabled)) emptyList() state.takeUnless {
else listOfNotNull( state.isNoteToSelf || state.isGroup && state.isNewConfigEnabled
typeOption( }?.run {
ExpiryType.NONE, listOfNotNull(
state, typeOption(
R.string.expiration_off, ExpiryType.NONE,
contentDescription = R.string.AccessibilityId_disable_disappearing_messages, state,
enabled = state.isSelfAdmin R.string.expiration_off,
), contentDescription = R.string.AccessibilityId_disable_disappearing_messages,
if (!state.isNewConfigEnabled) typeOption( enabled = state.isSelfAdmin
ExpiryType.LEGACY, ),
state, if (!state.isNewConfigEnabled) typeOption(
R.string.expiration_type_disappear_legacy, ExpiryType.LEGACY,
contentDescription = R.string.expiration_type_disappear_legacy_description, state,
enabled = state.isSelfAdmin R.string.expiration_type_disappear_legacy,
) else null, contentDescription = R.string.expiration_type_disappear_legacy_description,
if (!state.isGroup) typeOption( enabled = state.isSelfAdmin
ExpiryType.AFTER_READ, ) else null,
state, if (!state.isGroup) typeOption(
R.string.expiration_type_disappear_after_read, ExpiryType.AFTER_READ,
R.string.expiration_type_disappear_after_read_description, state,
contentDescription = R.string.expiration_type_disappear_after_read_description, R.string.expiration_type_disappear_after_read,
enabled = state.isNewConfigEnabled && state.isSelfAdmin R.string.expiration_type_disappear_after_read_description,
) else null, contentDescription = R.string.expiration_type_disappear_after_read_description,
typeOption( enabled = state.isNewConfigEnabled && state.isSelfAdmin
ExpiryType.AFTER_SEND, ) else null,
state, typeOption(
R.string.expiration_type_disappear_after_send, ExpiryType.AFTER_SEND,
R.string.expiration_type_disappear_after_read_description, state,
contentDescription = R.string.expiration_type_disappear_after_send_description, R.string.expiration_type_disappear_after_send,
enabled = state.isNewConfigEnabled && state.isSelfAdmin R.string.expiration_type_disappear_after_read_description,
), contentDescription = R.string.expiration_type_disappear_after_send_description,
) enabled = state.isNewConfigEnabled && state.isSelfAdmin
)
)
}
private fun typeOption( private fun typeOption(
type: ExpiryType, type: ExpiryType,
@ -266,17 +276,17 @@ private fun typeOption(
onClick = onClick onClick = onClick
) )
private fun timeOptions(state: State): List<OptionModel> = private fun timeOptions(state: State) =
if (state.isNoteToSelf || (state.isGroup && state.isNewConfigEnabled)) timeOptionsOnly(state) if (state.isNoteToSelf || (state.isGroup && state.isNewConfigEnabled)) timeOptionsOnly(state)
else when (state.expiryMode) { else when (state.expiryMode) {
is ExpiryMode.Legacy -> afterReadTimes is ExpiryMode.Legacy -> afterReadTimes
is ExpiryMode.AfterRead -> afterReadTimes is ExpiryMode.AfterRead -> afterReadTimes
is ExpiryMode.AfterSend -> afterSendTimes is ExpiryMode.AfterSend -> afterSendTimes
else -> emptyList() else -> null
}.map { timeOption(it, state) } }?.map { timeOption(it, state) }
private val afterSendTimes = listOf(12.hours, 1.days, 7.days, 14.days) val afterSendTimes = listOf(12.hours, 1.days, 7.days, 14.days)
private val afterReadTimes = listOf(5.minutes, 1.hours) + afterSendTimes val afterReadTimes = listOf(5.minutes, 1.hours) + afterSendTimes
private fun timeOptionsOnly(state: State) = listOfNotNull( private fun timeOptionsOnly(state: State) = listOfNotNull(
typeOption(ExpiryType.NONE, state, R.string.arrays__off, enabled = state.isSelfAdmin), typeOption(ExpiryType.NONE, state, R.string.arrays__off, enabled = state.isSelfAdmin),
@ -312,9 +322,9 @@ enum class ExpiryType(private val createMode: (Long) -> ExpiryMode) {
fun mode(seconds: Long) = createMode(seconds) fun mode(seconds: Long) = createMode(seconds)
fun defaultMode() = when(this) { fun defaultMode() = when(this) {
AFTER_READ -> 43200L AFTER_READ -> 12.hours
else -> 86400L else -> 1.days
}.let { mode(it) } }.inWholeSeconds.let(::mode)
} }
private val ExpiryMode.type: ExpiryType get() = when(this) { private val ExpiryMode.type: ExpiryType get() = when(this) {

View File

@ -0,0 +1,25 @@
package org.thoughtcrime.securesms
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.StandardTestDispatcher
import kotlinx.coroutines.test.TestDispatcher
import kotlinx.coroutines.test.resetMain
import kotlinx.coroutines.test.setMain
import org.junit.rules.TestWatcher
import org.junit.runner.Description
@ExperimentalCoroutinesApi
class MainCoroutineRule(private val dispatcher: TestDispatcher = StandardTestDispatcher()) :
TestWatcher() {
override fun starting(description: Description) {
super.starting(description)
Dispatchers.setMain(dispatcher)
}
override fun finished(description: Description) {
super.finished(description)
Dispatchers.resetMain()
}
}

View File

@ -0,0 +1,99 @@
package org.thoughtcrime.securesms.conversation.expiration
import android.app.Application
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.advanceUntilIdle
import kotlinx.coroutines.test.runTest
import network.loki.messenger.libsession_util.util.ExpiryMode
import org.hamcrest.CoreMatchers
import org.hamcrest.MatcherAssert
import org.junit.Rule
import org.junit.Test
import org.mockito.Mockito
import org.mockito.Mockito.mock
import org.mockito.kotlin.whenever
import org.session.libsession.messaging.messages.ExpirationConfiguration
import org.session.libsession.utilities.Address
import org.session.libsession.utilities.GroupRecord
import org.session.libsession.utilities.GroupUtil.CLOSED_GROUP_PREFIX
import org.session.libsession.utilities.SSKEnvironment
import org.session.libsession.utilities.TextSecurePreferences
import org.session.libsession.utilities.recipients.Recipient
import org.session.libsignal.utilities.guava.Optional
import org.thoughtcrime.securesms.MainCoroutineRule
import org.thoughtcrime.securesms.database.GroupDatabase
import org.thoughtcrime.securesms.database.Storage
import org.thoughtcrime.securesms.database.ThreadDatabase
import kotlin.time.Duration.Companion.hours
@OptIn(ExperimentalCoroutinesApi::class)
class ExpirationSettingsViewModelTest {
@ExperimentalCoroutinesApi
@get:Rule
var mainCoroutineRule = MainCoroutineRule()
val application: Application = mock(Application::class.java)
val textSecurePreferences: TextSecurePreferences = mock(TextSecurePreferences::class.java)
val messageExpirationManager: SSKEnvironment.MessageExpirationManagerProtocol = mock(SSKEnvironment.MessageExpirationManagerProtocol::class.java)
val threadDb: ThreadDatabase = mock(ThreadDatabase::class.java)
val groupDb: GroupDatabase = mock(GroupDatabase::class.java)
val storage: Storage = mock(Storage::class.java)
val recipient = mock(Recipient::class.java)
val groupRecord = mock(GroupRecord::class.java)
val optionalGroupRecord = Optional.of(groupRecord)
@Test
fun `UI should show a list of times and an Off option`() = runTest {
val threadId = 1L
val expirationConfig = ExpirationConfiguration(
threadId = threadId,
expiryMode = ExpiryMode.AfterSend(12.hours.inWholeSeconds),
updatedTimestampMs = 0
)
whenever(threadDb.getRecipientForThreadId(Mockito.anyLong())).thenReturn(recipient)
whenever(storage.getExpirationConfiguration(Mockito.anyLong())).thenReturn(expirationConfig)
val address = Address.fromSerialized("${CLOSED_GROUP_PREFIX}94198734289")
whenever(recipient.isClosedGroupRecipient).thenReturn(true)
whenever(recipient.address).thenReturn(address)
whenever(groupDb.getGroup(Mockito.anyString())).thenReturn(optionalGroupRecord)
val viewModel = createViewModel()
advanceUntilIdle()
val state = viewModel.state.value
MatcherAssert.assertThat(
state.isGroup,
CoreMatchers.equalTo(true)
)
MatcherAssert.assertThat(
viewModel.uiState.value.cards.count(),
CoreMatchers.equalTo(1)
)
MatcherAssert.assertThat(
viewModel.uiState.value.cards[0].options.count(),
CoreMatchers.equalTo(6)
)
}
private fun createViewModel(isNewConfigEnabled: Boolean = true) = ExpirationSettingsViewModel(
1L,
application,
textSecurePreferences,
messageExpirationManager,
threadDb,
groupDb,
storage,
isNewConfigEnabled
)
}