Improve ui vm separation

This commit is contained in:
andrew 2023-10-09 16:33:54 +10:30
parent b211c8bffb
commit 17f41d76b8
10 changed files with 377 additions and 351 deletions

View File

@ -15,6 +15,7 @@ import network.loki.messenger.R
import network.loki.messenger.databinding.ActivityDisappearingMessagesBinding
import org.thoughtcrime.securesms.PassphraseRequiredActionBarActivity
import org.thoughtcrime.securesms.conversation.disappearingmessages.ui.DisappearingMessages
import org.thoughtcrime.securesms.conversation.disappearingmessages.ui.UiState
import org.thoughtcrime.securesms.database.RecipientDatabase
import org.thoughtcrime.securesms.database.ThreadDatabase
import org.thoughtcrime.securesms.ui.AppTheme

View File

@ -1,7 +1,6 @@
package org.thoughtcrime.securesms.conversation.disappearingmessages
import android.app.Application
import androidx.annotation.StringRes
import androidx.lifecycle.AndroidViewModel
import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider
@ -18,62 +17,20 @@ import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
import network.loki.messenger.BuildConfig
import network.loki.messenger.R
import network.loki.messenger.libsession_util.util.ExpiryMode
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.SSKEnvironment.MessageExpirationManagerProtocol
import org.session.libsession.utilities.TextSecurePreferences
import org.thoughtcrime.securesms.conversation.disappearingmessages.ui.ExpiryCallbacks
import org.thoughtcrime.securesms.conversation.disappearingmessages.ui.UiState
import org.thoughtcrime.securesms.conversation.disappearingmessages.ui.toUiState
import org.thoughtcrime.securesms.database.GroupDatabase
import org.thoughtcrime.securesms.database.Storage
import org.thoughtcrime.securesms.database.ThreadDatabase
import org.thoughtcrime.securesms.ui.GetString
import org.thoughtcrime.securesms.util.ConfigurationMessageUtilities
import kotlin.time.Duration
import kotlin.time.Duration.Companion.days
import kotlin.time.Duration.Companion.hours
import kotlin.time.Duration.Companion.minutes
import kotlin.time.Duration.Companion.seconds
enum class Event {
SUCCESS, FAIL
}
data class State(
val isGroup: Boolean = false,
val isSelfAdmin: Boolean = true,
val address: Address? = null,
val isNoteToSelf: Boolean = false,
val expiryMode: ExpiryMode? = null,
val isNewConfigEnabled: Boolean = true,
val persistedMode: ExpiryMode? = null,
val showDebugOptions: Boolean = false
) {
val subtitle get() = when {
isGroup || isNoteToSelf -> GetString(R.string.activity_disappearing_messages_subtitle_sent)
else -> GetString(R.string.activity_disappearing_messages_subtitle)
}
val typeOptionsHidden get() = isNoteToSelf || (isGroup && isNewConfigEnabled)
val duration get() = expiryMode?.duration
val expiryType get() = expiryMode?.type
val isTimeOptionsEnabled = isNoteToSelf || isSelfAdmin && (isNewConfigEnabled || expiryType == ExpiryType.LEGACY)
}
interface Callbacks {
fun onSetClick(): Any?
fun setMode(mode: ExpiryMode)
}
object NoOpCallbacks: Callbacks {
override fun onSetClick() {}
override fun setMode(mode: ExpiryMode) {}
}
class DisappearingMessagesViewModel(
private val threadId: Long,
@ -85,7 +42,7 @@ class DisappearingMessagesViewModel(
private val storage: Storage,
isNewConfigEnabled: Boolean,
showDebugOptions: Boolean
) : AndroidViewModel(application), Callbacks {
) : AndroidViewModel(application), ExpiryCallbacks {
private val _event = Channel<Event>()
val event = _event.receiveAsFlow()
@ -99,7 +56,7 @@ class DisappearingMessagesViewModel(
val state = _state.asStateFlow()
val uiState = _state
.map(::UiState)
.map(State::toUiState)
.stateIn(viewModelScope, SharingStarted.Eagerly, UiState())
init {
@ -122,7 +79,7 @@ class DisappearingMessagesViewModel(
}
}
override fun setMode(mode: ExpiryMode) = _state.update { it.copy(expiryMode = mode) }
override fun setValue(value: ExpiryMode) = _state.update { it.copy(expiryMode = value) }
override fun onSetClick() = viewModelScope.launch {
val state = _state.value
@ -178,177 +135,3 @@ class DisappearingMessagesViewModel(
) as T
}
}
data class UiState(
val cards: List<CardModel> = emptyList(),
val showGroupFooter: Boolean = false,
val showSetButton: Boolean = true
) {
constructor(state: State): this(
cards = listOfNotNull(
typeOptions(state)?.let { CardModel(GetString(R.string.activity_disappearing_messages_delete_type), it) },
timeOptions(state)?.let { CardModel(GetString(R.string.activity_disappearing_messages_timer), it) }
),
showGroupFooter = state.isGroup && state.isNewConfigEnabled,
showSetButton = state.isSelfAdmin
)
constructor(
vararg cards: CardModel,
showGroupFooter: Boolean = false,
showSetButton: Boolean = true,
): this(
cards.asList(),
showGroupFooter,
showSetButton
)
}
data class CardModel(
val title: GetString,
val options: List<OptionModel>
) {
constructor(title: GetString, vararg options: OptionModel): this(title, options.asList())
constructor(@StringRes title: Int, vararg options: OptionModel): this(GetString(title), options.asList())
}
fun offTypeOption(state: State) = typeOption(ExpiryType.NONE, state)
fun legacyTypeOption(state: State) = typeOption(ExpiryType.LEGACY, state)
fun afterReadTypeOption(state: State) = newTypeOption(ExpiryType.AFTER_READ, state)
fun afterSendTypeOption(state: State) = newTypeOption(ExpiryType.AFTER_SEND, state)
private fun newTypeOption(type: ExpiryType, state: State) = typeOption(type, state, state.run { isNewConfigEnabled && isSelfAdmin })
private fun typeOptions(state: State) = state.takeUnless { it.typeOptionsHidden }?.run {
listOfNotNull(
offTypeOption(state),
takeUnless { isNewConfigEnabled }?.let(::legacyTypeOption),
takeUnless { isGroup }?.let(::afterReadTypeOption),
afterSendTypeOption(state)
)
}
private fun typeOption(
type: ExpiryType,
state: State,
enabled: Boolean = state.isSelfAdmin,
) = OptionModel(
value = type.defaultMode(state.persistedMode),
title = GetString(type.title),
subtitle = type.subtitle?.let(::GetString),
contentDescription = GetString(type.contentDescription),
selected = state.expiryType == type,
enabled = enabled
)
private fun debugTimes(isDebug: Boolean) = if (isDebug) listOf(10.seconds, 1.minutes) else emptyList()
private fun debugModes(isDebug: Boolean, type: ExpiryType) =
debugTimes(isDebug).map { type.mode(it.inWholeSeconds) }
private fun debugOptions(state: State): List<OptionModel> =
debugModes(state.showDebugOptions, state.expiryType.takeIf { it == ExpiryType.AFTER_READ } ?: ExpiryType.AFTER_SEND)
.map { timeOption(it, state, subtitle = GetString("for testing purposes")) }
val defaultTimes = listOf(12.hours, 1.days, 7.days, 14.days)
val afterSendTimes = defaultTimes
val afterSendModes = afterSendTimes.map { it.inWholeSeconds }.map(ExpiryMode::AfterSend)
val legacyModes = afterSendTimes.map { it.inWholeSeconds }.map(ExpiryMode::Legacy)
fun afterSendOptions(state: State) = afterSendModes.map { timeOption(it, state) }
fun legacyOptions(state: State) = legacyModes.map { timeOption(it, state) }
val afterReadTimes = buildList {
add(5.minutes)
add(1.hours)
addAll(defaultTimes)
}
val afterReadModes = afterReadTimes.map { it.inWholeSeconds }.map(ExpiryMode::AfterRead)
fun afterReadOptions(state: State) = afterReadModes.map { timeOption(it, state) }
private fun timeOptions(
state: State
): List<OptionModel>? {
val type = state.takeUnless {
it.typeOptionsHidden
}?.expiryType ?: if (state.isNewConfigEnabled) ExpiryType.AFTER_SEND else ExpiryType.LEGACY
return when (type) {
ExpiryType.AFTER_READ -> afterReadOptions(state)
ExpiryType.AFTER_SEND -> afterSendOptions(state)
ExpiryType.LEGACY -> legacyOptions(state)
else -> null
}?.let {
buildList {
if (state.typeOptionsHidden) add(offTypeOption(state))
addAll(debugOptions(state))
addAll(it)
}
}
}
fun timeOption(
mode: ExpiryMode,
state: State,
title: GetString = GetString(mode.duration),
subtitle: GetString? = null,
) = OptionModel(
value = mode,
title = title,
subtitle = subtitle,
contentDescription = title,
selected = state.expiryMode == mode,
enabled = state.isTimeOptionsEnabled
)
data class OptionModel(
val value: ExpiryMode,
val title: GetString,
val subtitle: GetString? = null,
val contentDescription: GetString = title,
val selected: Boolean = false,
val enabled: Boolean = true,
)
enum class ExpiryType(
private val createMode: (Long) -> ExpiryMode,
@StringRes val title: Int,
@StringRes val subtitle: Int? = null,
@StringRes val contentDescription: Int = title,
) {
NONE(
{ ExpiryMode.NONE },
R.string.expiration_off,
contentDescription = R.string.AccessibilityId_disable_disappearing_messages,
),
LEGACY(
ExpiryMode::Legacy,
R.string.expiration_type_disappear_legacy,
contentDescription = R.string.expiration_type_disappear_legacy_description
),
AFTER_READ(
ExpiryMode::AfterRead,
R.string.expiration_type_disappear_after_read,
R.string.expiration_type_disappear_after_read_description,
R.string.expiration_type_disappear_after_read_description
),
AFTER_SEND(
ExpiryMode::AfterSend,
R.string.expiration_type_disappear_after_send,
R.string.expiration_type_disappear_after_read_description,
R.string.expiration_type_disappear_after_send_description
);
fun mode(seconds: Long) = if (seconds != 0L) createMode(seconds) else ExpiryMode.NONE
fun mode(duration: Duration) = mode(duration.inWholeSeconds)
fun defaultMode(persistedMode: ExpiryMode?) = when(this) {
persistedMode?.type -> persistedMode
AFTER_READ -> mode(12.hours)
else -> mode(1.days)
}
}
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
}

View File

@ -0,0 +1,84 @@
package org.thoughtcrime.securesms.conversation.disappearingmessages
import androidx.annotation.StringRes
import network.loki.messenger.R
import network.loki.messenger.libsession_util.util.ExpiryMode
import org.session.libsession.utilities.Address
import org.thoughtcrime.securesms.ui.GetString
import kotlin.time.Duration
import kotlin.time.Duration.Companion.days
import kotlin.time.Duration.Companion.hours
enum class Event {
SUCCESS, FAIL
}
data class State(
val isGroup: Boolean = false,
val isSelfAdmin: Boolean = true,
val address: Address? = null,
val isNoteToSelf: Boolean = false,
val expiryMode: ExpiryMode? = null,
val isNewConfigEnabled: Boolean = true,
val persistedMode: ExpiryMode? = null,
val showDebugOptions: Boolean = false
) {
val subtitle get() = when {
isGroup || isNoteToSelf -> GetString(R.string.activity_disappearing_messages_subtitle_sent)
else -> GetString(R.string.activity_disappearing_messages_subtitle)
}
val typeOptionsHidden get() = isNoteToSelf || (isGroup && isNewConfigEnabled)
val duration get() = expiryMode?.duration
val expiryType get() = expiryMode?.type
val isTimeOptionsEnabled = isNoteToSelf || isSelfAdmin && (isNewConfigEnabled || expiryType == ExpiryType.LEGACY)
}
enum class ExpiryType(
private val createMode: (Long) -> ExpiryMode,
@StringRes val title: Int,
@StringRes val subtitle: Int? = null,
@StringRes val contentDescription: Int = title,
) {
NONE(
{ ExpiryMode.NONE },
R.string.expiration_off,
contentDescription = R.string.AccessibilityId_disable_disappearing_messages,
),
LEGACY(
ExpiryMode::Legacy,
R.string.expiration_type_disappear_legacy,
contentDescription = R.string.expiration_type_disappear_legacy_description
),
AFTER_READ(
ExpiryMode::AfterRead,
R.string.expiration_type_disappear_after_read,
R.string.expiration_type_disappear_after_read_description,
R.string.expiration_type_disappear_after_read_description
),
AFTER_SEND(
ExpiryMode::AfterSend,
R.string.expiration_type_disappear_after_send,
R.string.expiration_type_disappear_after_read_description,
R.string.expiration_type_disappear_after_send_description
);
fun mode(seconds: Long) = if (seconds != 0L) createMode(seconds) else ExpiryMode.NONE
fun mode(duration: Duration) = mode(duration.inWholeSeconds)
fun defaultMode(persistedMode: ExpiryMode?) = when(this) {
persistedMode?.type -> persistedMode
AFTER_READ -> mode(12.hours)
else -> mode(1.days)
}
}
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
}

View File

@ -0,0 +1,98 @@
package org.thoughtcrime.securesms.conversation.disappearingmessages.ui
import network.loki.messenger.R
import network.loki.messenger.libsession_util.util.ExpiryMode
import org.thoughtcrime.securesms.conversation.disappearingmessages.ExpiryType
import org.thoughtcrime.securesms.conversation.disappearingmessages.State
import org.thoughtcrime.securesms.ui.GetString
import kotlin.time.Duration.Companion.days
import kotlin.time.Duration.Companion.hours
import kotlin.time.Duration.Companion.minutes
import kotlin.time.Duration.Companion.seconds
fun State.toUiState() = UiState(
cards = listOfNotNull(
typeOptions()?.let { ExpiryOptionsCard(GetString(R.string.activity_disappearing_messages_delete_type), it) },
timeOptions()?.let { ExpiryOptionsCard(GetString(R.string.activity_disappearing_messages_timer), it) }
),
showGroupFooter = isGroup && isNewConfigEnabled,
showSetButton = isSelfAdmin
)
private fun State.typeOptions(): List<ExpiryRadioOption>? = if (typeOptionsHidden) null else {
buildList {
add(offTypeOption())
if (!isNewConfigEnabled) add(legacyTypeOption())
if (!isGroup) add(afterReadTypeOption())
add(afterSendTypeOption())
}
}
private fun State.timeOptions(): List<ExpiryRadioOption>? {
val type = takeUnless {
it.typeOptionsHidden
}?.expiryType ?: if (isNewConfigEnabled) ExpiryType.AFTER_SEND else ExpiryType.LEGACY
return when (type) {
ExpiryType.AFTER_READ -> afterReadModes
ExpiryType.AFTER_SEND -> afterSendModes
ExpiryType.LEGACY -> legacyModes
else -> null
}?.map { timeOption(it) }?.let {
buildList {
if (typeOptionsHidden) add(offTypeOption())
addAll(debugOptions())
addAll(it)
}
}
}
private fun State.offTypeOption() = typeOption(ExpiryType.NONE)
private fun State.legacyTypeOption() = typeOption(ExpiryType.LEGACY)
private fun State.afterReadTypeOption() = newTypeOption(ExpiryType.AFTER_READ)
private fun State.afterSendTypeOption() = newTypeOption(ExpiryType.AFTER_SEND)
private fun State.newTypeOption(type: ExpiryType) = typeOption(type, isNewConfigEnabled && isSelfAdmin)
private fun State.typeOption(
type: ExpiryType,
enabled: Boolean = isSelfAdmin,
) = ExpiryRadioOption(
value = type.defaultMode(persistedMode),
title = GetString(type.title),
subtitle = type.subtitle?.let(::GetString),
contentDescription = GetString(type.contentDescription),
selected = expiryType == type,
enabled = enabled
)
private fun debugTimes(isDebug: Boolean) = if (isDebug) listOf(10.seconds, 1.minutes) else emptyList()
private fun debugModes(isDebug: Boolean, type: ExpiryType) =
debugTimes(isDebug).map { type.mode(it.inWholeSeconds) }
private fun State.debugOptions(): List<ExpiryRadioOption> =
debugModes(showDebugOptions, expiryType.takeIf { it == ExpiryType.AFTER_READ } ?: ExpiryType.AFTER_SEND)
.map { timeOption(it, subtitle = GetString("for testing purposes")) }
private val afterSendTimes = listOf(12.hours, 1.days, 7.days, 14.days)
private val afterSendModes = afterSendTimes.map { it.inWholeSeconds }.map(ExpiryMode::AfterSend)
private val legacyModes = afterSendTimes.map { it.inWholeSeconds }.map(ExpiryMode::Legacy)
private val afterReadTimes = buildList {
add(5.minutes)
add(1.hours)
addAll(afterSendTimes)
}
private val afterReadModes = afterReadTimes.map { it.inWholeSeconds }.map(ExpiryMode::AfterRead)
private fun State.timeOption(
mode: ExpiryMode,
title: GetString = GetString(mode.duration),
subtitle: GetString? = null,
) = ExpiryRadioOption(
value = mode,
title = title,
subtitle = subtitle,
contentDescription = title,
selected = expiryMode == mode,
enabled = isTimeOptionsEnabled
)

View File

@ -4,11 +4,7 @@ import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.heightIn
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.itemsIndexed
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.Text
@ -20,32 +16,25 @@ import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.TextStyle
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.sp
import network.loki.messenger.R
import network.loki.messenger.libsession_util.util.ExpiryMode
import org.thoughtcrime.securesms.conversation.disappearingmessages.Callbacks
import org.thoughtcrime.securesms.conversation.disappearingmessages.CardModel
import org.thoughtcrime.securesms.conversation.disappearingmessages.ExpiryType
import org.thoughtcrime.securesms.conversation.disappearingmessages.NoOpCallbacks
import org.thoughtcrime.securesms.conversation.disappearingmessages.State
import org.thoughtcrime.securesms.conversation.disappearingmessages.UiState
import org.thoughtcrime.securesms.ui.CellNoMargin
import org.thoughtcrime.securesms.ui.Divider
import org.thoughtcrime.securesms.ui.Callbacks
import org.thoughtcrime.securesms.ui.NoOpCallbacks
import org.thoughtcrime.securesms.ui.OptionsCard
import org.thoughtcrime.securesms.ui.OutlineButton
import org.thoughtcrime.securesms.ui.PreviewTheme
import org.thoughtcrime.securesms.ui.ThemeResPreviewParameterProvider
import org.thoughtcrime.securesms.ui.TitledRadioButton
import org.thoughtcrime.securesms.ui.RadioOption
import org.thoughtcrime.securesms.ui.fadingEdges
typealias ExpiryCallbacks = Callbacks<ExpiryMode>
typealias ExpiryRadioOption = RadioOption<ExpiryMode>
@Composable
fun DisappearingMessages(
state: UiState,
modifier: Modifier = Modifier,
callbacks: Callbacks = NoOpCallbacks
callbacks: ExpiryCallbacks = NoOpCallbacks
) {
val scrollState = rememberScrollState()
@ -81,18 +70,3 @@ fun DisappearingMessages(
)
}
}
@Composable
fun OptionsCard(card: CardModel, callbacks: Callbacks) {
Text(text = card.title())
CellNoMargin {
LazyColumn(
modifier = Modifier.heightIn(max = 5000.dp)
) {
itemsIndexed(card.options) { i, it ->
if (i != 0) Divider()
TitledRadioButton(it) { callbacks.setMode(it.value) }
}
}
}
}

View File

@ -11,7 +11,6 @@ import network.loki.messenger.R
import network.loki.messenger.libsession_util.util.ExpiryMode
import org.thoughtcrime.securesms.conversation.disappearingmessages.ExpiryType
import org.thoughtcrime.securesms.conversation.disappearingmessages.State
import org.thoughtcrime.securesms.conversation.disappearingmessages.UiState
import org.thoughtcrime.securesms.ui.PreviewTheme
import org.thoughtcrime.securesms.ui.ThemeResPreviewParameterProvider
@ -22,7 +21,7 @@ fun PreviewStates(
) {
PreviewTheme(R.style.Classic_Dark) {
DisappearingMessages(
UiState(state)
state.toUiState()
)
}
}
@ -56,7 +55,7 @@ fun PreviewThemes(
) {
PreviewTheme(themeResId) {
DisappearingMessages(
UiState(State(expiryMode = ExpiryMode.AfterSend(43200))),
State(expiryMode = ExpiryMode.AfterSend(43200)).toUiState(),
modifier = Modifier.size(400.dp, 600.dp)
)
}

View File

@ -0,0 +1,32 @@
package org.thoughtcrime.securesms.conversation.disappearingmessages.ui
import androidx.annotation.StringRes
import network.loki.messenger.libsession_util.util.ExpiryMode
import org.thoughtcrime.securesms.ui.GetString
import org.thoughtcrime.securesms.ui.RadioOption
typealias ExpiryOptionsCard = OptionsCard<ExpiryMode>
data class UiState(
val cards: List<ExpiryOptionsCard> = emptyList(),
val showGroupFooter: Boolean = false,
val showSetButton: Boolean = true
) {
constructor(
vararg cards: ExpiryOptionsCard,
showGroupFooter: Boolean = false,
showSetButton: Boolean = true,
): this(
cards.asList(),
showGroupFooter,
showSetButton
)
}
data class OptionsCard<T>(
val title: GetString,
val options: List<RadioOption<T>>
) {
constructor(title: GetString, vararg options: RadioOption<T>): this(title, options.asList())
constructor(@StringRes title: Int, vararg options: RadioOption<T>): this(GetString(title), options.asList())
}

View File

@ -0,0 +1,81 @@
package org.thoughtcrime.securesms.ui
import androidx.annotation.DrawableRes
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.BoxScope
import androidx.compose.foundation.layout.RowScope
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.pager.PagerState
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.Card
import androidx.compose.material.Icon
import androidx.compose.material.IconButton
import androidx.compose.runtime.Composable
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.unit.dp
import kotlinx.coroutines.launch
import network.loki.messenger.R
@OptIn(ExperimentalFoundationApi::class)
@Composable
fun BoxScope.HorizontalPagerIndicator(pagerState: PagerState) {
if (pagerState.pageCount >= 2) Card(
shape = RoundedCornerShape(50.dp),
backgroundColor = Color.Black.copy(alpha = 0.4f),
modifier = Modifier
.align(Alignment.BottomCenter)
.padding(8.dp)
) {
Box(modifier = Modifier.padding(8.dp)) {
com.google.accompanist.pager.HorizontalPagerIndicator(
pagerState = pagerState,
pageCount = pagerState.pageCount,
activeColor = Color.White,
inactiveColor = classicDarkColors[5])
}
}
}
@OptIn(ExperimentalFoundationApi::class)
@Composable
fun RowScope.CarouselPrevButton(pagerState: PagerState) {
CarouselButton(pagerState, pagerState.canScrollBackward, R.drawable.ic_prev, -1)
}
@OptIn(ExperimentalFoundationApi::class)
@Composable
fun RowScope.CarouselNextButton(pagerState: PagerState) {
CarouselButton(pagerState, pagerState.canScrollForward, R.drawable.ic_next, 1)
}
@OptIn(ExperimentalFoundationApi::class)
@Composable
fun RowScope.CarouselButton(
pagerState: PagerState,
enabled: Boolean,
@DrawableRes id: Int,
delta: Int
) {
if (pagerState.pageCount <= 1) Spacer(modifier = Modifier.width(32.dp))
else {
val animationScope = rememberCoroutineScope()
IconButton(
modifier = Modifier
.width(40.dp)
.align(Alignment.CenterVertically),
enabled = enabled,
onClick = { animationScope.launch { pagerState.animateScrollToPage(pagerState.currentPage + delta) } }) {
Icon(
painter = painterResource(id = id),
contentDescription = null,
)
}
}
}

View File

@ -2,16 +2,13 @@ package org.thoughtcrime.securesms.ui
import androidx.annotation.DrawableRes
import androidx.compose.foundation.BorderStroke
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.ScrollState
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.BoxScope
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.RowScope
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
@ -20,21 +17,20 @@ import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.layout.wrapContentHeight
import androidx.compose.foundation.pager.PagerState
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.itemsIndexed
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.ButtonColors
import androidx.compose.material.ButtonDefaults
import androidx.compose.material.Card
import androidx.compose.material.Colors
import androidx.compose.material.Icon
import androidx.compose.material.IconButton
import androidx.compose.material.MaterialTheme
import androidx.compose.material.OutlinedButton
import androidx.compose.material.RadioButton
import androidx.compose.material.Text
import androidx.compose.material.TextButton
import androidx.compose.runtime.Composable
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.alpha
@ -49,14 +45,46 @@ import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.compose.ui.viewinterop.AndroidView
import com.google.accompanist.pager.HorizontalPagerIndicator
import kotlinx.coroutines.launch
import network.loki.messenger.R
import org.session.libsession.utilities.recipients.Recipient
import org.thoughtcrime.securesms.components.ProfilePictureView
import org.thoughtcrime.securesms.conversation.disappearingmessages.OptionModel
import org.thoughtcrime.securesms.conversation.disappearingmessages.ui.OptionsCard
import kotlin.math.min
interface Callbacks<in T> {
fun onSetClick(): Any?
fun setValue(value: T)
}
object NoOpCallbacks: Callbacks<Any> {
override fun onSetClick() {}
override fun setValue(value: Any) {}
}
data class RadioOption<T>(
val value: T,
val title: GetString,
val subtitle: GetString? = null,
val contentDescription: GetString = title,
val selected: Boolean = false,
val enabled: Boolean = true,
)
@Composable
fun <T> OptionsCard(card: OptionsCard<T>, callbacks: Callbacks<T>) {
Text(text = card.title())
CellNoMargin {
LazyColumn(
modifier = Modifier.heightIn(max = 5000.dp)
) {
itemsIndexed(card.options) { i, it ->
if (i != 0) Divider()
TitledRadioButton(it) { callbacks.setValue(it.value) }
}
}
}
}
@Composable
fun ItemButton(
text: String,
@ -115,7 +143,7 @@ fun CellWithPaddingAndMargin(
}
@Composable
fun TitledRadioButton(option: OptionModel, onClick: () -> Unit) {
fun <T> TitledRadioButton(option: RadioOption<T>, onClick: () -> Unit) {
Row(
horizontalArrangement = Arrangement.spacedBy(16.dp),
modifier = Modifier
@ -210,63 +238,6 @@ fun Modifier.fadingEdges(
}
)
@OptIn(ExperimentalFoundationApi::class)
@Composable
fun BoxScope.HorizontalPagerIndicator(pagerState: PagerState) {
if (pagerState.pageCount >= 2) Card(
shape = RoundedCornerShape(50.dp),
backgroundColor = Color.Black.copy(alpha = 0.4f),
modifier = Modifier
.align(Alignment.BottomCenter)
.padding(8.dp)
) {
Box(modifier = Modifier.padding(8.dp)) {
HorizontalPagerIndicator(
pagerState = pagerState,
pageCount = pagerState.pageCount,
activeColor = Color.White,
inactiveColor = classicDarkColors[5])
}
}
}
@OptIn(ExperimentalFoundationApi::class)
@Composable
fun RowScope.CarouselPrevButton(pagerState: PagerState) {
CarouselButton(pagerState, pagerState.canScrollBackward, R.drawable.ic_prev, -1)
}
@OptIn(ExperimentalFoundationApi::class)
@Composable
fun RowScope.CarouselNextButton(pagerState: PagerState) {
CarouselButton(pagerState, pagerState.canScrollForward, R.drawable.ic_next, 1)
}
@OptIn(ExperimentalFoundationApi::class)
@Composable
fun RowScope.CarouselButton(
pagerState: PagerState,
enabled: Boolean,
@DrawableRes id: Int,
delta: Int
) {
if (pagerState.pageCount <= 1) Spacer(modifier = Modifier.width(32.dp))
else {
val animationScope = rememberCoroutineScope()
IconButton(
modifier = Modifier
.width(40.dp)
.align(Alignment.CenterVertically),
enabled = enabled,
onClick = { animationScope.launch { pagerState.animateScrollToPage(pagerState.currentPage + delta) } }) {
Icon(
painter = painterResource(id = id),
contentDescription = null,
)
}
}
}
@Composable
fun Divider() {
androidx.compose.material.Divider(

View File

@ -24,6 +24,9 @@ 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.conversation.disappearingmessages.ui.ExpiryRadioOption
import org.thoughtcrime.securesms.conversation.disappearingmessages.ui.OptionsCard
import org.thoughtcrime.securesms.conversation.disappearingmessages.ui.UiState
import org.thoughtcrime.securesms.database.GroupDatabase
import org.thoughtcrime.securesms.database.Storage
import org.thoughtcrime.securesms.database.ThreadDatabase
@ -83,7 +86,7 @@ class DisappearingMessagesViewModelTest {
viewModel.uiState.value
).isEqualTo(
UiState(
CardModel(
OptionsCard(
R.string.activity_disappearing_messages_timer,
typeOption(ExpiryMode.NONE, selected = true),
timeOption(ExpiryType.AFTER_SEND, 12.hours),
@ -122,7 +125,7 @@ class DisappearingMessagesViewModelTest {
viewModel.uiState.value
).isEqualTo(
UiState(
CardModel(
OptionsCard(
R.string.activity_disappearing_messages_timer,
typeOption(ExpiryMode.NONE, selected = true),
timeOption(ExpiryType.LEGACY, 12.hours),
@ -161,7 +164,7 @@ class DisappearingMessagesViewModelTest {
viewModel.uiState.value
).isEqualTo(
UiState(
CardModel(
OptionsCard(
R.string.activity_disappearing_messages_timer,
typeOption(ExpiryMode.NONE, selected = true),
timeOption(ExpiryType.AFTER_SEND, 12.hours),
@ -201,7 +204,7 @@ class DisappearingMessagesViewModelTest {
viewModel.uiState.value
).isEqualTo(
UiState(
CardModel(
OptionsCard(
R.string.activity_disappearing_messages_timer,
typeOption(ExpiryMode.NONE, enabled = false, selected = true),
timeOption(ExpiryType.AFTER_SEND, 12.hours, enabled = false),
@ -243,7 +246,7 @@ class DisappearingMessagesViewModelTest {
viewModel.uiState.value
).isEqualTo(
UiState(
CardModel(
OptionsCard(
R.string.activity_disappearing_messages_delete_type,
typeOption(ExpiryMode.NONE, selected = true),
typeOption(12.hours, ExpiryType.AFTER_READ),
@ -282,14 +285,14 @@ class DisappearingMessagesViewModelTest {
viewModel.uiState.value
).isEqualTo(
UiState(
CardModel(
OptionsCard(
R.string.activity_disappearing_messages_delete_type,
typeOption(ExpiryMode.NONE),
typeOption(time, ExpiryType.AFTER_READ),
typeOption(time, ExpiryType.AFTER_SEND, selected = true)
),
CardModel(
GetString(R.string.activity_disappearing_messages_timer),
OptionsCard(
R.string.activity_disappearing_messages_timer,
timeOption(ExpiryType.AFTER_SEND, 12.hours, selected = true),
timeOption(ExpiryType.AFTER_SEND, 1.days),
timeOption(ExpiryType.AFTER_SEND, 7.days),
@ -328,15 +331,15 @@ class DisappearingMessagesViewModelTest {
viewModel.uiState.value
).isEqualTo(
UiState(
CardModel(
OptionsCard(
R.string.activity_disappearing_messages_delete_type,
typeOption(ExpiryMode.NONE),
typeOption(time, ExpiryType.LEGACY, selected = true),
typeOption(12.hours, ExpiryType.AFTER_READ, enabled = false),
typeOption(1.days, ExpiryType.AFTER_SEND, enabled = false)
),
CardModel(
GetString(R.string.activity_disappearing_messages_timer),
OptionsCard(
R.string.activity_disappearing_messages_timer,
timeOption(ExpiryType.LEGACY, 12.hours, selected = true),
timeOption(ExpiryType.LEGACY, 1.days),
timeOption(ExpiryType.LEGACY, 7.days),
@ -375,14 +378,14 @@ class DisappearingMessagesViewModelTest {
viewModel.uiState.value
).isEqualTo(
UiState(
CardModel(
OptionsCard(
R.string.activity_disappearing_messages_delete_type,
typeOption(ExpiryMode.NONE),
typeOption(12.hours, ExpiryType.AFTER_READ),
typeOption(time, ExpiryType.AFTER_SEND, selected = true)
),
CardModel(
GetString(R.string.activity_disappearing_messages_timer),
OptionsCard(
R.string.activity_disappearing_messages_timer,
timeOption(ExpiryType.AFTER_SEND, 12.hours),
timeOption(ExpiryType.AFTER_SEND, 1.days, selected = true),
timeOption(ExpiryType.AFTER_SEND, 7.days),
@ -422,14 +425,14 @@ class DisappearingMessagesViewModelTest {
viewModel.uiState.value
).isEqualTo(
UiState(
CardModel(
OptionsCard(
R.string.activity_disappearing_messages_delete_type,
typeOption(ExpiryMode.NONE),
typeOption(1.days, ExpiryType.AFTER_READ, selected = true),
typeOption(time, ExpiryType.AFTER_SEND)
),
CardModel(
GetString(R.string.activity_disappearing_messages_timer),
OptionsCard(
R.string.activity_disappearing_messages_timer,
timeOption(ExpiryType.AFTER_READ, 5.minutes),
timeOption(ExpiryType.AFTER_READ, 1.hours),
timeOption(ExpiryType.AFTER_READ, 12.hours),
@ -452,7 +455,7 @@ class DisappearingMessagesViewModelTest {
advanceUntilIdle()
viewModel.setMode(afterSendMode(1.days))
viewModel.setValue(afterSendMode(1.days))
advanceUntilIdle()
@ -475,14 +478,14 @@ class DisappearingMessagesViewModelTest {
viewModel.uiState.value
).isEqualTo(
UiState(
CardModel(
OptionsCard(
R.string.activity_disappearing_messages_delete_type,
typeOption(ExpiryMode.NONE),
typeOption(12.hours, ExpiryType.AFTER_READ),
typeOption(1.days, ExpiryType.AFTER_SEND, selected = true)
),
CardModel(
GetString(R.string.activity_disappearing_messages_timer),
OptionsCard(
R.string.activity_disappearing_messages_timer,
timeOption(ExpiryType.AFTER_SEND, 12.hours),
timeOption(ExpiryType.AFTER_SEND, 1.days, selected = true),
timeOption(ExpiryType.AFTER_SEND, 7.days),
@ -497,7 +500,7 @@ class DisappearingMessagesViewModelTest {
time: Duration,
enabled: Boolean = true,
selected: Boolean = false
) = OptionModel(
) = ExpiryRadioOption(
value = type.mode(time),
title = GetString(time),
enabled = enabled,
@ -564,7 +567,7 @@ fun typeOption(time: Duration, type: ExpiryType, selected: Boolean = false, enab
typeOption(type.mode(time), selected, enabled)
fun typeOption(mode: ExpiryMode, selected: Boolean = false, enabled: Boolean = true) =
OptionModel(
ExpiryRadioOption(
mode,
GetString(mode.type.title),
mode.type.subtitle?.let(::GetString),