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.compose.foundation.BorderStroke
import androidx.compose.foundation.ScrollState
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.heightIn
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.lazy.itemsIndexed
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.shape.RoundedCornerShape
@ -33,27 +29,22 @@ import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.alpha
import androidx.compose.ui.draw.drawWithContent
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.graphics.BlendMode
import androidx.compose.ui.graphics.Brush
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.CompositingStrategy
import androidx.compose.ui.graphics.graphicsLayer
import androidx.compose.ui.layout.ModifierLocalBeyondBoundsLayout
import androidx.compose.ui.res.stringResource
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.style.TextAlign
import androidx.compose.ui.tooling.preview.Preview
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.sp
import androidx.core.text.HtmlCompat
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.lifecycleScope
import androidx.lifecycle.repeatOnLifecycle
@ -61,6 +52,8 @@ import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.launch
import network.loki.messenger.R
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.database.RecipientDatabase
import org.thoughtcrime.securesms.database.ThreadDatabase
@ -279,8 +272,18 @@ fun TitledRadioButton(option: OptionModel) {
.weight(1f)
.align(Alignment.CenterVertically)) {
Column {
Text(text = option.title())
option.subtitle?.let { Text(text = it()) }
Text(
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(
@ -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
@Composable
fun PreviewMessageDetails(
@ -314,27 +352,8 @@ fun PreviewMessageDetails(
) {
PreviewTheme(themeResId) {
DisappearingMessages(
UiState(
cards = listOf(
CardModel(GetString(R.string.activity_expiration_settings_delete_type), previewTypeOptions()),
CardModel(GetString(R.string.activity_expiration_settings_timer), previewTimeOptions())
)
),
UiState(State(expiryMode = ExpiryMode.AfterSend(43200))),
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 dagger.assisted.Assisted
import dagger.assisted.AssistedInject
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.flow.MutableStateFlow
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.sending_receiving.MessageSender
import org.session.libsession.snode.SnodeAPI
import org.session.libsession.utilities.Address
import org.session.libsession.utilities.ExpirationUtil
import org.session.libsession.utilities.SSKEnvironment.MessageExpirationManagerProtocol
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.Storage
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.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(
private val threadId: Long,
private val textSecurePreferences: TextSecurePreferences,
@ -43,24 +73,19 @@ class ExpirationSettingsViewModel(
private val threadDb: ThreadDatabase,
private val groupDb: GroupDatabase,
private val storage: Storage,
private val isNewConfigEnabled: Boolean
) : ViewModel() {
isNewConfigEnabled: Boolean
) : ViewModel(), Callbacks {
private val _event = Channel<Event>()
val event = _event.receiveAsFlow()
private val _state = MutableStateFlow(State())
private val _state = MutableStateFlow(State(
isNewConfigEnabled = isNewConfigEnabled,
callbacks = this@ExpirationSettingsViewModel
))
val state = _state.asStateFlow()
val uiState = _state.map {
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
)
}
val uiState = _state.map { UiState(it) }
private var expirationConfig: ExpirationConfiguration? = null
@ -71,101 +96,41 @@ class ExpirationSettingsViewModel(
val recipient = threadDb.getRecipientForThreadId(threadId)
val groupInfo = recipient?.takeIf { it.isClosedGroupRecipient }
?.run { address.toGroupString().let(groupDb::getGroup).orNull() }
_state.update { state ->
state.copy(
isGroup = groupInfo != null,
isSelfAdmin = groupInfo == null || groupInfo.admins.any{ it.serialize() == textSecurePreferences.getLocalNumber() },
recipient = recipient,
expiryMode = expiryMode
)
}
}
}
private fun typeOption(
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) {
override fun setType(type: ExpiryType) {
_state.update { it.copy(expiryMode = type.mode(0)) }
}
private fun setTime(seconds: Long) {
override fun setTime(seconds: Long) {
_state.update { it.copy(
expiryMode = it.expiryType?.mode(seconds)
) }
}
private fun setMode(mode: ExpiryMode) {
override fun setMode(mode: ExpiryMode) {
_state.update { it.copy(
expiryMode = mode
) }
}
fun timeOption(seconds: Long, @StringRes id: Int) = OptionModel(GetString(id), selected = false, onClick = { setTime(seconds) })
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 {
override fun onSetClick() = viewModelScope.launch {
val state = _state.value
val expiryMode = state.expiryMode ?: ExpiryMode.NONE
val typeValue = expiryMode.let {
if (it is ExpiryMode.Legacy) ExpiryMode.AfterRead(it.expirySeconds)
else it
}
val address = state.recipient?.address
val address = state.address
if (address == null || expirationConfig?.expiryMode == typeValue) {
_event.send(Event(false))
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(
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(
val title: GetString,
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(
val title: GetString,
val subtitle: GetString? = null,
@ -259,8 +294,8 @@ enum class ExpiryType(private val createMode: (Long) -> ExpiryMode) {
}
private val ExpiryMode.type: ExpiryType get() = when(this) {
is ExpiryMode.Legacy -> ExpiryType.LEGACY
is ExpiryMode.AfterSend -> ExpiryType.AFTER_SEND
is ExpiryMode.AfterRead -> ExpiryType.AFTER_READ
else -> ExpiryType.NONE
}
is ExpiryMode.Legacy -> ExpiryType.LEGACY
is ExpiryMode.AfterSend -> ExpiryType.AFTER_SEND
is ExpiryMode.AfterRead -> ExpiryType.AFTER_READ
else -> ExpiryType.NONE
}