Add preview for all states

This commit is contained in:
andrew 2023-08-31 12:09:06 +09:30
parent 0b11e182ff
commit e95c842051
3 changed files with 196 additions and 142 deletions

View File

@ -5,20 +5,16 @@ import android.widget.Toast
import androidx.activity.viewModels import androidx.activity.viewModels
import androidx.compose.foundation.BorderStroke import androidx.compose.foundation.BorderStroke
import androidx.compose.foundation.ScrollState import androidx.compose.foundation.ScrollState
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.heightIn import androidx.compose.foundation.layout.heightIn
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.lazy.itemsIndexed import androidx.compose.foundation.lazy.itemsIndexed
import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.shape.RoundedCornerShape
@ -33,27 +29,22 @@ import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.alpha
import androidx.compose.ui.draw.drawWithContent import androidx.compose.ui.draw.drawWithContent
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.graphics.BlendMode import androidx.compose.ui.graphics.BlendMode
import androidx.compose.ui.graphics.Brush import androidx.compose.ui.graphics.Brush
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.CompositingStrategy
import androidx.compose.ui.graphics.graphicsLayer import androidx.compose.ui.graphics.graphicsLayer
import androidx.compose.ui.layout.ModifierLocalBeyondBoundsLayout
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.TextStyle import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.buildAnnotatedString
import androidx.compose.ui.text.font.Font
import androidx.compose.ui.text.font.FontFamily
import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp import androidx.compose.ui.unit.sp
import androidx.core.text.HtmlCompat
import androidx.lifecycle.Lifecycle import androidx.lifecycle.Lifecycle
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
import androidx.lifecycle.repeatOnLifecycle import androidx.lifecycle.repeatOnLifecycle
@ -61,6 +52,8 @@ import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import network.loki.messenger.R import network.loki.messenger.R
import network.loki.messenger.databinding.ActivityExpirationSettingsBinding import network.loki.messenger.databinding.ActivityExpirationSettingsBinding
import network.loki.messenger.libsession_util.util.ExpiryMode
import org.session.libsession.utilities.recipients.Recipient
import org.thoughtcrime.securesms.PassphraseRequiredActionBarActivity import org.thoughtcrime.securesms.PassphraseRequiredActionBarActivity
import org.thoughtcrime.securesms.database.RecipientDatabase import org.thoughtcrime.securesms.database.RecipientDatabase
import org.thoughtcrime.securesms.database.ThreadDatabase import org.thoughtcrime.securesms.database.ThreadDatabase
@ -279,8 +272,18 @@ fun TitledRadioButton(option: OptionModel) {
.weight(1f) .weight(1f)
.align(Alignment.CenterVertically)) { .align(Alignment.CenterVertically)) {
Column { Column {
Text(text = option.title()) Text(
option.subtitle?.let { Text(text = it()) } text = option.title(),
fontSize = 16.sp,
modifier = Modifier.alpha(if (option.enabled) 1f else 0.5f)
)
option.subtitle?.let {
Text(
text = it(),
fontSize = 11.sp,
modifier = Modifier.alpha(if (option.enabled) 1f else 0.5f)
)
}
} }
} }
RadioButton( RadioButton(
@ -307,6 +310,41 @@ fun OutlineButton(text: String, modifier: Modifier = Modifier, onClick: () -> Un
} }
} }
@Preview(widthDp = 450, heightDp = 700)
@Composable
fun x(
@PreviewParameter(StatePreviewParameterProvider::class) state: State
) {
PreviewTheme(R.style.Classic_Dark) {
DisappearingMessages(
UiState(state)
)
}
}
class StatePreviewParameterProvider : PreviewParameterProvider<State> {
override val values = newConfigValues.filter { it.expiryType != ExpiryType.LEGACY } + newConfigValues.map { it.copy(isNewConfigEnabled = false) }
private val newConfigValues get() = sequenceOf(
// new 1-1
State(expiryMode = ExpiryMode.NONE),
State(expiryMode = ExpiryMode.Legacy(43200)),
State(expiryMode = ExpiryMode.AfterRead(300)),
State(expiryMode = ExpiryMode.AfterSend(43200)),
// new group non-admin
State(isGroup = true, isSelfAdmin = false),
State(isGroup = true, isSelfAdmin = false, expiryMode = ExpiryMode.Legacy(43200)),
State(isGroup = true, isSelfAdmin = false, expiryMode = ExpiryMode.AfterSend(43200)),
// new group admin
State(isGroup = true),
State(isGroup = true, expiryMode = ExpiryMode.Legacy(43200)),
State(isGroup = true, expiryMode = ExpiryMode.AfterSend(43200)),
// new note-to-self
State(isNoteToSelf = true),
)
}
@Preview @Preview
@Composable @Composable
fun PreviewMessageDetails( fun PreviewMessageDetails(
@ -314,27 +352,8 @@ fun PreviewMessageDetails(
) { ) {
PreviewTheme(themeResId) { PreviewTheme(themeResId) {
DisappearingMessages( DisappearingMessages(
UiState( UiState(State(expiryMode = ExpiryMode.AfterSend(43200))),
cards = listOf(
CardModel(GetString(R.string.activity_expiration_settings_delete_type), previewTypeOptions()),
CardModel(GetString(R.string.activity_expiration_settings_timer), previewTimeOptions())
)
),
modifier = Modifier.size(400.dp, 600.dp) modifier = Modifier.size(400.dp, 600.dp)
) )
} }
} }
fun previewTypeOptions() = listOf(
OptionModel(GetString(R.string.expiration_off)),
OptionModel(GetString(R.string.expiration_type_disappear_legacy)),
OptionModel(GetString(R.string.expiration_type_disappear_after_read)),
OptionModel(GetString(R.string.expiration_type_disappear_after_send))
)
fun previewTimeOptions() = listOf(
OptionModel(GetString("1 Minute")),
OptionModel(GetString("5 Minutes")),
OptionModel(GetString("1 Week")),
OptionModel(GetString("2 Weeks")),
)

View File

@ -6,7 +6,6 @@ import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import dagger.assisted.Assisted import dagger.assisted.Assisted
import dagger.assisted.AssistedInject import dagger.assisted.AssistedInject
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.channels.Channel import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.asStateFlow
@ -21,10 +20,11 @@ import org.session.libsession.messaging.messages.ExpirationConfiguration
import org.session.libsession.messaging.messages.control.ExpirationTimerUpdate import org.session.libsession.messaging.messages.control.ExpirationTimerUpdate
import org.session.libsession.messaging.sending_receiving.MessageSender import org.session.libsession.messaging.sending_receiving.MessageSender
import org.session.libsession.snode.SnodeAPI import org.session.libsession.snode.SnodeAPI
import org.session.libsession.utilities.Address
import org.session.libsession.utilities.ExpirationUtil import org.session.libsession.utilities.ExpirationUtil
import org.session.libsession.utilities.SSKEnvironment.MessageExpirationManagerProtocol import org.session.libsession.utilities.SSKEnvironment.MessageExpirationManagerProtocol
import org.session.libsession.utilities.TextSecurePreferences import org.session.libsession.utilities.TextSecurePreferences
import org.session.libsession.utilities.recipients.Recipient import org.thoughtcrime.securesms.conversation.expiration.ExpiryType
import org.thoughtcrime.securesms.database.GroupDatabase import org.thoughtcrime.securesms.database.GroupDatabase
import org.thoughtcrime.securesms.database.Storage import org.thoughtcrime.securesms.database.Storage
import org.thoughtcrime.securesms.database.ThreadDatabase import org.thoughtcrime.securesms.database.ThreadDatabase
@ -34,8 +34,38 @@ import kotlin.time.Duration.Companion.days
import kotlin.time.Duration.Companion.hours import kotlin.time.Duration.Companion.hours
import kotlin.time.Duration.Companion.minutes import kotlin.time.Duration.Companion.minutes
data class Event(
val saveSuccess: Boolean
)
data class State(
val isGroup: Boolean = false,
val isSelfAdmin: Boolean = true,
val address: Address? = null,
val isNoteToSelf: Boolean = false,
val expiryMode: ExpiryMode? = ExpiryMode.NONE,
val isNewConfigEnabled: Boolean = true,
val callbacks: Callbacks = NoOpCallbacks
) {
val subtitle get() = when {
isGroup || isNoteToSelf -> GetString(R.string.activity_expiration_settings_subtitle_sent)
else -> GetString(R.string.activity_expiration_settings_subtitle)
}
val duration get() = expiryMode?.duration
val expiryType get() = expiryMode?.type
val isTimeOptionsEnabled = isNoteToSelf || isSelfAdmin && (isNewConfigEnabled || expiryType == ExpiryType.LEGACY)
}
interface Callbacks {
fun onSetClick(): Any = Unit
fun setType(type: ExpiryType) {}
fun setTime(seconds: Long) {}
fun setMode(mode: ExpiryMode) {}
}
object NoOpCallbacks: Callbacks
@OptIn(ExperimentalCoroutinesApi::class)
class ExpirationSettingsViewModel( class ExpirationSettingsViewModel(
private val threadId: Long, private val threadId: Long,
private val textSecurePreferences: TextSecurePreferences, private val textSecurePreferences: TextSecurePreferences,
@ -43,24 +73,19 @@ class ExpirationSettingsViewModel(
private val threadDb: ThreadDatabase, private val threadDb: ThreadDatabase,
private val groupDb: GroupDatabase, private val groupDb: GroupDatabase,
private val storage: Storage, private val storage: Storage,
private val isNewConfigEnabled: Boolean isNewConfigEnabled: Boolean
) : ViewModel() { ) : ViewModel(), Callbacks {
private val _event = Channel<Event>() private val _event = Channel<Event>()
val event = _event.receiveAsFlow() val event = _event.receiveAsFlow()
private val _state = MutableStateFlow(State()) private val _state = MutableStateFlow(State(
isNewConfigEnabled = isNewConfigEnabled,
callbacks = this@ExpirationSettingsViewModel
))
val state = _state.asStateFlow() val state = _state.asStateFlow()
val uiState = _state.map { val uiState = _state.map { UiState(it) }
UiState(
cards = listOf(
CardModel(GetString(R.string.activity_expiration_settings_delete_type), typeOptions(it)),
CardModel(GetString(R.string.activity_expiration_settings_timer), timeOptions(it))
),
showGroupFooter = it.isGroup
)
}
private var expirationConfig: ExpirationConfiguration? = null private var expirationConfig: ExpirationConfiguration? = null
@ -71,101 +96,41 @@ class ExpirationSettingsViewModel(
val recipient = threadDb.getRecipientForThreadId(threadId) val recipient = threadDb.getRecipientForThreadId(threadId)
val groupInfo = recipient?.takeIf { it.isClosedGroupRecipient } val groupInfo = recipient?.takeIf { it.isClosedGroupRecipient }
?.run { address.toGroupString().let(groupDb::getGroup).orNull() } ?.run { address.toGroupString().let(groupDb::getGroup).orNull() }
_state.update { state -> _state.update { state ->
state.copy( state.copy(
isGroup = groupInfo != null, isGroup = groupInfo != null,
isSelfAdmin = groupInfo == null || groupInfo.admins.any{ it.serialize() == textSecurePreferences.getLocalNumber() }, isSelfAdmin = groupInfo == null || groupInfo.admins.any{ it.serialize() == textSecurePreferences.getLocalNumber() },
recipient = recipient,
expiryMode = expiryMode expiryMode = expiryMode
) )
} }
} }
} }
private fun typeOption( override fun setType(type: ExpiryType) {
type: ExpiryType,
state: State,
@StringRes title: Int,
@StringRes subtitle: Int? = null,
@StringRes contentDescription: Int = title
) = OptionModel(GetString(title), subtitle?.let(::GetString), selected = state.expiryType == type) { setType(type) }
private fun typeOptions(state: State) =
if (state.isSelf || state.isGroup) emptyList()
else listOf(
typeOption(ExpiryType.NONE, state, R.string.expiration_off, contentDescription = R.string.AccessibilityId_disable_disappearing_messages),
typeOption(ExpiryType.LEGACY, state, R.string.expiration_type_disappear_legacy, contentDescription = R.string.expiration_type_disappear_legacy_description),
typeOption(ExpiryType.AFTER_READ, state, R.string.expiration_type_disappear_after_read, contentDescription = R.string.expiration_type_disappear_after_read_description),
typeOption(ExpiryType.AFTER_SEND, state, R.string.expiration_type_disappear_after_send, contentDescription = R.string.expiration_type_disappear_after_send_description),
)
private fun setType(type: ExpiryType) {
_state.update { it.copy(expiryMode = type.mode(0)) } _state.update { it.copy(expiryMode = type.mode(0)) }
} }
private fun setTime(seconds: Long) { override fun setTime(seconds: Long) {
_state.update { it.copy( _state.update { it.copy(
expiryMode = it.expiryType?.mode(seconds) expiryMode = it.expiryType?.mode(seconds)
) } ) }
} }
private fun setMode(mode: ExpiryMode) { override fun setMode(mode: ExpiryMode) {
_state.update { it.copy( _state.update { it.copy(
expiryMode = mode expiryMode = mode
) } ) }
} }
fun timeOption(seconds: Long, @StringRes id: Int) = OptionModel(GetString(id), selected = false, onClick = { setTime(seconds) }) override fun onSetClick() = viewModelScope.launch {
fun timeOption(seconds: Long, title: String, subtitle: String) = OptionModel(GetString(title), GetString(subtitle), selected = false, onClick = { setTime(seconds) })
// private fun timeOptions(state: State) = timeOptions(state.types.isEmpty(), state.expiryType == ExpiryType.AFTER_SEND)
private fun timeOptions(state: State): List<OptionModel> =
if (state.isSelf || state.isGroup) timeOptionsOnly(state)
else when (state.expiryMode) {
is ExpiryMode.Legacy -> afterReadTimes
is ExpiryMode.AfterRead -> afterReadTimes
is ExpiryMode.AfterSend -> afterSendTimes
else -> emptyList()
}.map { timeOption(it, state) }
private val afterReadTimes = listOf(12.hours, 1.days, 7.days, 14.days)
private val afterSendTimes = listOf(5.minutes, 1.hours) + afterReadTimes
private fun timeOptionsOnly(state: State) = listOfNotNull(
typeOption(ExpiryType.NONE, state, R.string.arrays__off),
noteToSelfOption(1.minutes, state, subtitle = "for testing purposes").takeIf { BuildConfig.DEBUG },
) + afterSendTimes.map { noteToSelfOption(it, state) }
private fun timeOption(
duration: Duration,
state: State,
title: GetString = GetString { ExpirationUtil.getExpirationDisplayValue(it, duration.inWholeSeconds.toInt()) },
) = OptionModel(
title = title,
selected = state.expiryMode?.duration == duration,
enabled = state.isSelfAdmin
) { setTime(duration.inWholeSeconds) }
private fun noteToSelfOption(
duration: Duration,
state: State,
title: GetString = GetString { ExpirationUtil.getExpirationDisplayValue(it, duration.inWholeSeconds.toInt()) },
subtitle: String? = null
) = OptionModel(
title = title,
subtitle = subtitle?.let(::GetString),
selected = state.duration == duration,
onClick = { setMode(ExpiryMode.AfterSend(duration.inWholeSeconds)) }
)
fun onSetClick() = viewModelScope.launch {
val state = _state.value val state = _state.value
val expiryMode = state.expiryMode ?: ExpiryMode.NONE val expiryMode = state.expiryMode ?: ExpiryMode.NONE
val typeValue = expiryMode.let { val typeValue = expiryMode.let {
if (it is ExpiryMode.Legacy) ExpiryMode.AfterRead(it.expirySeconds) if (it is ExpiryMode.Legacy) ExpiryMode.AfterRead(it.expirySeconds)
else it else it
} }
val address = state.recipient?.address val address = state.address
if (address == null || expirationConfig?.expiryMode == typeValue) { if (address == null || expirationConfig?.expiryMode == typeValue) {
_event.send(Event(false)) _event.send(Event(false))
return@launch return@launch
@ -211,36 +176,106 @@ class ExpirationSettingsViewModel(
} }
} }
data class Event(
val saveSuccess: Boolean
)
data class State(
val isGroup: Boolean = false,
val isSelfAdmin: Boolean = false,
val recipient: Recipient? = null,
val expiryMode: ExpiryMode? = null,
val types: List<ExpiryType> = emptyList()
) {
val subtitle get() = when {
isGroup || isSelf -> GetString(R.string.activity_expiration_settings_subtitle_sent)
else -> GetString(R.string.activity_expiration_settings_subtitle)
}
val duration get() = expiryMode?.duration
val isSelf = recipient?.isLocalNumber == true
val expiryType get() = expiryMode?.type
}
data class UiState( data class UiState(
val cards: List<CardModel> = emptyList(), val cards: List<CardModel> = emptyList(),
val showGroupFooter: Boolean = false val showGroupFooter: Boolean = false,
) 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))
),
showGroupFooter = state.isGroup && state.isNewConfigEnabled,
callbacks = state.callbacks
)
}
data class CardModel( data class CardModel(
val title: GetString, val title: GetString,
val options: List<OptionModel> val options: List<OptionModel>
) )
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
),
)
private fun typeOption(
type: ExpiryType,
state: State,
@StringRes title: Int,
@StringRes subtitle: Int? = null,
@StringRes contentDescription: Int = title,
enabled: Boolean = true,
onClick: () -> Unit = { state.callbacks.setType(type) }
) = OptionModel(
GetString(title),
subtitle?.let(::GetString),
selected = state.expiryType == type,
enabled = enabled,
onClick = onClick
)
private fun timeOptions(state: State): List<OptionModel> =
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) }
private val afterSendTimes = listOf(12.hours, 1.days, 7.days, 14.days)
private 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),
timeOption(1.minutes, state, subtitle = GetString("for testing purposes")).takeIf { BuildConfig.DEBUG },
) + afterSendTimes.map { timeOption(it, state) }
private fun timeOption(
duration: Duration,
state: State,
title: GetString = GetString { ExpirationUtil.getExpirationDisplayValue(it, duration.inWholeSeconds.toInt()) },
subtitle: GetString? = null
) = OptionModel(
title = title,
subtitle = subtitle,
selected = state.expiryMode?.duration == duration,
enabled = state.isTimeOptionsEnabled
) { state.callbacks.setTime(duration.inWholeSeconds) }
data class OptionModel( data class OptionModel(
val title: GetString, val title: GetString,
val subtitle: GetString? = null, val subtitle: GetString? = null,
@ -259,8 +294,8 @@ enum class ExpiryType(private val createMode: (Long) -> ExpiryMode) {
} }
private val ExpiryMode.type: ExpiryType get() = when(this) { private val ExpiryMode.type: ExpiryType get() = when(this) {
is ExpiryMode.Legacy -> ExpiryType.LEGACY is ExpiryMode.Legacy -> ExpiryType.LEGACY
is ExpiryMode.AfterSend -> ExpiryType.AFTER_SEND is ExpiryMode.AfterSend -> ExpiryType.AFTER_SEND
is ExpiryMode.AfterRead -> ExpiryType.AFTER_READ is ExpiryMode.AfterRead -> ExpiryType.AFTER_READ
else -> ExpiryType.NONE else -> ExpiryType.NONE
} }

View File

@ -34,4 +34,4 @@ class GroupRecord(
this.admins = Address.fromSerializedList(admins!!, ',') this.admins = Address.fromSerializedList(admins!!, ',')
} }
} }
} }