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),
verticalArrangement = Arrangement.spacedBy(16.dp)
) {
state.cards.filter { it.options.isNotEmpty() }.forEach {
state.cards.forEach {
OptionsCard(it)
}

View File

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