diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/disappearingmessages/DisappearingMessagesActivity.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/disappearingmessages/DisappearingMessagesActivity.kt index 86e6515746..d37d42fb9f 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/disappearingmessages/DisappearingMessagesActivity.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/disappearingmessages/DisappearingMessagesActivity.kt @@ -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 diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/disappearingmessages/DisappearingMessagesViewModel.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/disappearingmessages/DisappearingMessagesViewModel.kt index 8babbb55f5..761d6cd07e 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/disappearingmessages/DisappearingMessagesViewModel.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/disappearingmessages/DisappearingMessagesViewModel.kt @@ -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() 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 = 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 -) { - 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 = - 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? { - 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 -} diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/disappearingmessages/State.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/disappearingmessages/State.kt new file mode 100644 index 0000000000..fb60e48ab1 --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/disappearingmessages/State.kt @@ -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 +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/disappearingmessages/ui/Adapter.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/disappearingmessages/ui/Adapter.kt new file mode 100644 index 0000000000..32bd5bfb96 --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/disappearingmessages/ui/Adapter.kt @@ -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? = if (typeOptionsHidden) null else { + buildList { + add(offTypeOption()) + if (!isNewConfigEnabled) add(legacyTypeOption()) + if (!isGroup) add(afterReadTypeOption()) + add(afterSendTypeOption()) + } +} + +private fun State.timeOptions(): List? { + 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 = + 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 +) diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/disappearingmessages/ui/DisappearingMessages.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/disappearingmessages/ui/DisappearingMessages.kt index 997be53824..fa6a67a1fe 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/disappearingmessages/ui/DisappearingMessages.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/disappearingmessages/ui/DisappearingMessages.kt @@ -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 +typealias ExpiryRadioOption = RadioOption + @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) } - } - } - } -} diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/disappearingmessages/ui/DisappearingMessagesPreview.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/disappearingmessages/ui/DisappearingMessagesPreview.kt index 5a166f74d4..c2524bf261 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/disappearingmessages/ui/DisappearingMessagesPreview.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/disappearingmessages/ui/DisappearingMessagesPreview.kt @@ -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) ) } diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/disappearingmessages/ui/UiState.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/disappearingmessages/ui/UiState.kt new file mode 100644 index 0000000000..40f917427c --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/disappearingmessages/ui/UiState.kt @@ -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 + +data class UiState( + val cards: List = 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( + val title: GetString, + val options: List> +) { + constructor(title: GetString, vararg options: RadioOption): this(title, options.asList()) + constructor(@StringRes title: Int, vararg options: RadioOption): this(GetString(title), options.asList()) +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/ui/Carousel.kt b/app/src/main/java/org/thoughtcrime/securesms/ui/Carousel.kt new file mode 100644 index 0000000000..0b7b6d6b4c --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/ui/Carousel.kt @@ -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, + ) + } + } +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/ui/Components.kt b/app/src/main/java/org/thoughtcrime/securesms/ui/Components.kt index 9c72cbb78b..f0810ea61f 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/ui/Components.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/ui/Components.kt @@ -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 { + fun onSetClick(): Any? + fun setValue(value: T) +} + +object NoOpCallbacks: Callbacks { + override fun onSetClick() {} + override fun setValue(value: Any) {} +} + +data class RadioOption( + val value: T, + val title: GetString, + val subtitle: GetString? = null, + val contentDescription: GetString = title, + val selected: Boolean = false, + val enabled: Boolean = true, +) + +@Composable +fun OptionsCard(card: OptionsCard, 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.setValue(it.value) } + } + } + } +} + + @Composable fun ItemButton( text: String, @@ -115,7 +143,7 @@ fun CellWithPaddingAndMargin( } @Composable -fun TitledRadioButton(option: OptionModel, onClick: () -> Unit) { +fun TitledRadioButton(option: RadioOption, 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( diff --git a/app/src/test/java/org/thoughtcrime/securesms/conversation/disappearingmessages/DisappearingMessagesViewModelTest.kt b/app/src/test/java/org/thoughtcrime/securesms/conversation/disappearingmessages/DisappearingMessagesViewModelTest.kt index 2666ac8463..a40d67a5a6 100644 --- a/app/src/test/java/org/thoughtcrime/securesms/conversation/disappearingmessages/DisappearingMessagesViewModelTest.kt +++ b/app/src/test/java/org/thoughtcrime/securesms/conversation/disappearingmessages/DisappearingMessagesViewModelTest.kt @@ -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),