mirror of
https://github.com/oxen-io/session-android.git
synced 2025-02-17 13:38:26 +00:00
Refactor
This commit is contained in:
parent
5ce100a4ec
commit
885df1e22b
@ -0,0 +1,141 @@
|
|||||||
|
package org.thoughtcrime.securesms.conversation.expiration
|
||||||
|
|
||||||
|
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
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
|
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.ui.CellNoMargin
|
||||||
|
import org.thoughtcrime.securesms.ui.Divider
|
||||||
|
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.fadingEdges
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun DisappearingMessages(
|
||||||
|
state: UiState,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
callbacks: Callbacks = NoOpCallbacks
|
||||||
|
) {
|
||||||
|
val scrollState = rememberScrollState()
|
||||||
|
|
||||||
|
Column(modifier = modifier) {
|
||||||
|
Box(modifier = Modifier.weight(1f)) {
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.padding(horizontal = 32.dp)
|
||||||
|
.padding(bottom = 20.dp)
|
||||||
|
.verticalScroll(scrollState)
|
||||||
|
.fadingEdges(scrollState),
|
||||||
|
verticalArrangement = Arrangement.spacedBy(16.dp)
|
||||||
|
) {
|
||||||
|
state.cards.forEach {
|
||||||
|
OptionsCard(it, callbacks)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (state.showGroupFooter) Text(text = stringResource(R.string.activity_expiration_settings_group_footer),
|
||||||
|
style = TextStyle(
|
||||||
|
fontSize = 11.sp,
|
||||||
|
fontWeight = FontWeight(400),
|
||||||
|
color = Color(0xFFA1A2A1),
|
||||||
|
textAlign = TextAlign.Center),
|
||||||
|
modifier = Modifier.fillMaxWidth())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
OutlineButton(
|
||||||
|
stringResource(R.string.expiration_settings_set_button_title),
|
||||||
|
modifier = Modifier
|
||||||
|
.align(Alignment.CenterHorizontally)
|
||||||
|
.padding(bottom = 20.dp),
|
||||||
|
onClick = callbacks::onSetClick
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@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) { it.onClick(callbacks) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Preview(widthDp = 450, heightDp = 700)
|
||||||
|
@Composable
|
||||||
|
fun PreviewStates(
|
||||||
|
@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 PreviewThemes(
|
||||||
|
@PreviewParameter(ThemeResPreviewParameterProvider::class) themeResId: Int
|
||||||
|
) {
|
||||||
|
PreviewTheme(themeResId) {
|
||||||
|
DisappearingMessages(
|
||||||
|
UiState(State(expiryMode = ExpiryMode.AfterSend(43200))),
|
||||||
|
modifier = Modifier.size(400.dp, 600.dp)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
@ -3,49 +3,9 @@ package org.thoughtcrime.securesms.conversation.expiration
|
|||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.widget.Toast
|
import android.widget.Toast
|
||||||
import androidx.activity.viewModels
|
import androidx.activity.viewModels
|
||||||
import androidx.compose.foundation.BorderStroke
|
|
||||||
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.Column
|
|
||||||
import androidx.compose.foundation.layout.Row
|
|
||||||
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.lazy.LazyColumn
|
|
||||||
import androidx.compose.foundation.lazy.itemsIndexed
|
|
||||||
import androidx.compose.foundation.rememberScrollState
|
|
||||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
|
||||||
import androidx.compose.foundation.verticalScroll
|
|
||||||
import androidx.compose.material.ButtonDefaults
|
|
||||||
import androidx.compose.material.MaterialTheme
|
|
||||||
import androidx.compose.material.OutlinedButton
|
|
||||||
import androidx.compose.material.RadioButton
|
|
||||||
import androidx.compose.material.Text
|
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.collectAsState
|
import androidx.compose.runtime.collectAsState
|
||||||
import androidx.compose.runtime.getValue
|
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.graphics.BlendMode
|
|
||||||
import androidx.compose.ui.graphics.Brush
|
|
||||||
import androidx.compose.ui.graphics.Color
|
|
||||||
import androidx.compose.ui.graphics.graphicsLayer
|
|
||||||
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.dp
|
|
||||||
import androidx.compose.ui.unit.sp
|
|
||||||
import androidx.lifecycle.Lifecycle
|
import androidx.lifecycle.Lifecycle
|
||||||
import androidx.lifecycle.lifecycleScope
|
import androidx.lifecycle.lifecycleScope
|
||||||
import androidx.lifecycle.repeatOnLifecycle
|
import androidx.lifecycle.repeatOnLifecycle
|
||||||
@ -53,18 +13,11 @@ 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.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
|
||||||
import org.thoughtcrime.securesms.ui.AppTheme
|
import org.thoughtcrime.securesms.ui.AppTheme
|
||||||
import org.thoughtcrime.securesms.ui.CellNoMargin
|
|
||||||
import org.thoughtcrime.securesms.ui.Divider
|
|
||||||
import org.thoughtcrime.securesms.ui.LocalExtraColors
|
|
||||||
import org.thoughtcrime.securesms.ui.PreviewTheme
|
|
||||||
import org.thoughtcrime.securesms.ui.ThemeResPreviewParameterProvider
|
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
import kotlin.math.min
|
|
||||||
|
|
||||||
@AndroidEntryPoint
|
@AndroidEntryPoint
|
||||||
class ExpirationSettingsActivity: PassphraseRequiredActionBarActivity() {
|
class ExpirationSettingsActivity: PassphraseRequiredActionBarActivity() {
|
||||||
@ -132,200 +85,7 @@ class ExpirationSettingsActivity: PassphraseRequiredActionBarActivity() {
|
|||||||
fun DisappearingMessagesScreen() {
|
fun DisappearingMessagesScreen() {
|
||||||
val uiState by viewModel.uiState.collectAsState(UiState())
|
val uiState by viewModel.uiState.collectAsState(UiState())
|
||||||
AppTheme {
|
AppTheme {
|
||||||
DisappearingMessages(uiState)
|
DisappearingMessages(uiState, callbacks = viewModel)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
|
||||||
fun DisappearingMessages(
|
|
||||||
state: UiState,
|
|
||||||
modifier: Modifier = Modifier
|
|
||||||
) {
|
|
||||||
val scrollState = rememberScrollState()
|
|
||||||
|
|
||||||
Column(modifier = modifier) {
|
|
||||||
Box(modifier = Modifier.weight(1f)) {
|
|
||||||
Column(
|
|
||||||
modifier = Modifier
|
|
||||||
.padding(horizontal = 32.dp)
|
|
||||||
.padding(bottom = 20.dp)
|
|
||||||
.verticalScroll(scrollState)
|
|
||||||
.fadingEdges(scrollState),
|
|
||||||
verticalArrangement = Arrangement.spacedBy(16.dp)
|
|
||||||
) {
|
|
||||||
state.cards.forEach {
|
|
||||||
OptionsCard(it)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (state.showGroupFooter) Text(text = stringResource(R.string.activity_expiration_settings_group_footer),
|
|
||||||
style = TextStyle(
|
|
||||||
fontSize = 11.sp,
|
|
||||||
fontWeight = FontWeight(400),
|
|
||||||
color = Color(0xFFA1A2A1),
|
|
||||||
textAlign = TextAlign.Center),
|
|
||||||
modifier = Modifier.fillMaxWidth())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
OutlineButton(
|
|
||||||
stringResource(R.string.expiration_settings_set_button_title),
|
|
||||||
modifier = Modifier
|
|
||||||
.align(Alignment.CenterHorizontally)
|
|
||||||
.padding(bottom = 20.dp),
|
|
||||||
onClick = state.callbacks::onSetClick
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun Modifier.fadingEdges(
|
|
||||||
scrollState: ScrollState,
|
|
||||||
topEdgeHeight: Dp = 0.dp,
|
|
||||||
bottomEdgeHeight: Dp = 20.dp
|
|
||||||
): Modifier = this.then(
|
|
||||||
Modifier
|
|
||||||
// adding layer fixes issue with blending gradient and content
|
|
||||||
.graphicsLayer { alpha = 0.99F }
|
|
||||||
.drawWithContent {
|
|
||||||
drawContent()
|
|
||||||
|
|
||||||
val topColors = listOf(Color.Transparent, Color.Black)
|
|
||||||
val topStartY = scrollState.value.toFloat()
|
|
||||||
val topGradientHeight = min(topEdgeHeight.toPx(), topStartY)
|
|
||||||
drawRect(
|
|
||||||
brush = Brush.verticalGradient(
|
|
||||||
colors = topColors,
|
|
||||||
startY = topStartY,
|
|
||||||
endY = topStartY + topGradientHeight
|
|
||||||
),
|
|
||||||
blendMode = BlendMode.DstIn
|
|
||||||
)
|
|
||||||
|
|
||||||
val bottomColors = listOf(Color.Black, Color.Transparent)
|
|
||||||
val bottomEndY = size.height - scrollState.maxValue + scrollState.value
|
|
||||||
val bottomGradientHeight =
|
|
||||||
min(bottomEdgeHeight.toPx(), scrollState.maxValue.toFloat() - scrollState.value)
|
|
||||||
if (bottomGradientHeight != 0f) drawRect(
|
|
||||||
brush = Brush.verticalGradient(
|
|
||||||
colors = bottomColors,
|
|
||||||
startY = bottomEndY - bottomGradientHeight,
|
|
||||||
endY = bottomEndY
|
|
||||||
),
|
|
||||||
blendMode = BlendMode.DstIn
|
|
||||||
)
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
fun OptionsCard(card: CardModel) {
|
|
||||||
Text(text = card.title())
|
|
||||||
CellNoMargin {
|
|
||||||
LazyColumn(
|
|
||||||
modifier = Modifier.heightIn(max = 5000.dp)
|
|
||||||
) {
|
|
||||||
itemsIndexed(card.options) { i, it ->
|
|
||||||
if (i != 0) Divider()
|
|
||||||
TitledRadioButton(it)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
fun TitledRadioButton(option: OptionModel) {
|
|
||||||
Row(modifier = Modifier
|
|
||||||
.clickable { option.onClick() }
|
|
||||||
.heightIn(min = 60.dp)
|
|
||||||
.padding(horizontal = 34.dp)) {
|
|
||||||
Column(modifier = Modifier
|
|
||||||
.weight(1f)
|
|
||||||
.align(Alignment.CenterVertically)) {
|
|
||||||
Column {
|
|
||||||
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(
|
|
||||||
selected = option.selected,
|
|
||||||
onClick = null,
|
|
||||||
enabled = option.enabled,
|
|
||||||
modifier = Modifier
|
|
||||||
.height(26.dp)
|
|
||||||
.align(Alignment.CenterVertically)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
fun OutlineButton(text: String, modifier: Modifier = Modifier, onClick: () -> Unit) {
|
|
||||||
OutlinedButton(
|
|
||||||
modifier = modifier.size(108.dp, 34.dp),
|
|
||||||
onClick = onClick,
|
|
||||||
border = BorderStroke(1.dp, LocalExtraColors.current.prominentButtonColor),
|
|
||||||
shape = RoundedCornerShape(50), // = 50% percent
|
|
||||||
colors = ButtonDefaults.outlinedButtonColors(
|
|
||||||
contentColor = LocalExtraColors.current.prominentButtonColor,
|
|
||||||
backgroundColor = MaterialTheme.colors.background
|
|
||||||
)
|
|
||||||
){
|
|
||||||
Text(text = text)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Preview(widthDp = 450, heightDp = 700)
|
|
||||||
@Composable
|
|
||||||
fun PreviewStates(
|
|
||||||
@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 PreviewThemes(
|
|
||||||
@PreviewParameter(ThemeResPreviewParameterProvider::class) themeResId: Int
|
|
||||||
) {
|
|
||||||
PreviewTheme(themeResId) {
|
|
||||||
DisappearingMessages(
|
|
||||||
UiState(State(expiryMode = ExpiryMode.AfterSend(43200))),
|
|
||||||
modifier = Modifier.size(400.dp, 600.dp)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
@ -50,27 +50,32 @@ data class State(
|
|||||||
val isNoteToSelf: Boolean = false,
|
val isNoteToSelf: Boolean = false,
|
||||||
val expiryMode: ExpiryMode? = ExpiryMode.NONE,
|
val expiryMode: ExpiryMode? = ExpiryMode.NONE,
|
||||||
val isNewConfigEnabled: Boolean = true,
|
val isNewConfigEnabled: Boolean = true,
|
||||||
val callbacks: Callbacks = NoOpCallbacks,
|
val persistedMode: ExpiryMode? = null,
|
||||||
val persistedMode: ExpiryMode? = null
|
val showDebugOptions: Boolean = false
|
||||||
) {
|
) {
|
||||||
val subtitle get() = when {
|
val subtitle get() = when {
|
||||||
isGroup || isNoteToSelf -> GetString(R.string.activity_expiration_settings_subtitle_sent)
|
isGroup || isNoteToSelf -> GetString(R.string.activity_expiration_settings_subtitle_sent)
|
||||||
else -> GetString(R.string.activity_expiration_settings_subtitle)
|
else -> GetString(R.string.activity_expiration_settings_subtitle)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
val modeOptionsHidden get() = isNoteToSelf || (isGroup && isNewConfigEnabled)
|
||||||
|
|
||||||
val duration get() = expiryMode?.duration
|
val duration get() = expiryMode?.duration
|
||||||
val expiryType get() = expiryMode?.type
|
val expiryType get() = expiryMode?.type
|
||||||
|
val expiryTypeOrNone: ExpiryType = expiryType ?: ExpiryType.NONE
|
||||||
|
|
||||||
val isTimeOptionsEnabled = isNoteToSelf || isSelfAdmin && (isNewConfigEnabled || expiryType == ExpiryType.LEGACY)
|
val isTimeOptionsEnabled = isNoteToSelf || isSelfAdmin && (isNewConfigEnabled || expiryType == ExpiryType.LEGACY)
|
||||||
}
|
}
|
||||||
|
|
||||||
interface Callbacks {
|
interface Callbacks {
|
||||||
fun onSetClick(): Any = Unit
|
fun onSetClick(): Any?
|
||||||
fun setType(type: ExpiryType) {}
|
fun setMode(mode: ExpiryMode)
|
||||||
fun setTime(seconds: Long) {}
|
|
||||||
fun setMode(mode: ExpiryMode) {}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
object NoOpCallbacks: Callbacks
|
object NoOpCallbacks: Callbacks {
|
||||||
|
override fun onSetClick() {}
|
||||||
|
override fun setMode(mode: ExpiryMode) {}
|
||||||
|
}
|
||||||
|
|
||||||
class ExpirationSettingsViewModel(
|
class ExpirationSettingsViewModel(
|
||||||
private val threadId: Long,
|
private val threadId: Long,
|
||||||
@ -80,16 +85,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,
|
||||||
isNewConfigEnabled: Boolean
|
isNewConfigEnabled: Boolean,
|
||||||
|
showDebugOptions: Boolean
|
||||||
) : AndroidViewModel(application), Callbacks {
|
) : AndroidViewModel(application), 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(
|
||||||
isNewConfigEnabled = isNewConfigEnabled,
|
State(
|
||||||
callbacks = this@ExpirationSettingsViewModel
|
isNewConfigEnabled = isNewConfigEnabled,
|
||||||
))
|
showDebugOptions = showDebugOptions
|
||||||
|
)
|
||||||
|
)
|
||||||
val state = _state.asStateFlow()
|
val state = _state.asStateFlow()
|
||||||
|
|
||||||
val uiState = _state
|
val uiState = _state
|
||||||
@ -119,28 +127,6 @@ class ExpirationSettingsViewModel(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* When enabling Disappearing Messages (for screens which provide the `Delete Type` options) the default `Timer` selection should be:
|
|
||||||
* Disappear After Read: `12 Hours`
|
|
||||||
* Disappear After Send: `1 Day`
|
|
||||||
* Legacy: `1 Day`
|
|
||||||
* */
|
|
||||||
override fun setType(type: ExpiryType) {
|
|
||||||
val state = state.value
|
|
||||||
|
|
||||||
if (state.expiryType == type) return
|
|
||||||
|
|
||||||
_state.update {
|
|
||||||
it.copy(expiryMode = type.defaultMode(state.persistedMode))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun setTime(seconds: Long) {
|
|
||||||
_state.update { it.copy(
|
|
||||||
expiryMode = it.expiryType?.mode(seconds)
|
|
||||||
) }
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun setMode(mode: ExpiryMode) {
|
override fun setMode(mode: ExpiryMode) {
|
||||||
_state.update { it.copy(
|
_state.update { it.copy(
|
||||||
expiryMode = mode
|
expiryMode = mode
|
||||||
@ -202,29 +188,28 @@ class ExpirationSettingsViewModel(
|
|||||||
threadDb,
|
threadDb,
|
||||||
groupDb,
|
groupDb,
|
||||||
storage,
|
storage,
|
||||||
ExpirationConfiguration.isNewConfigEnabled
|
ExpirationConfiguration.isNewConfigEnabled,
|
||||||
|
BuildConfig.DEBUG
|
||||||
) as T
|
) as T
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private fun ExpiryType.defaultMode(persistedMode: ExpiryMode?) = when(this) {
|
private fun ExpiryType.defaultMode(persistedMode: ExpiryMode?) = when(this) {
|
||||||
persistedMode?.type -> persistedMode
|
persistedMode?.type -> persistedMode
|
||||||
ExpiryType.AFTER_READ -> mode(12.hours)
|
ExpiryType.AFTER_READ -> mode(12.hours)
|
||||||
else -> mode(1.days)
|
else -> mode(1.days)
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
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(
|
constructor(state: State): this(
|
||||||
cards = listOfNotNull(
|
cards = listOfNotNull(
|
||||||
typeOptions(state)?.let { CardModel(GetString(R.string.activity_expiration_settings_delete_type), it) },
|
typeOptions(state)?.let { CardModel(GetString(R.string.activity_expiration_settings_delete_type), it) },
|
||||||
timeOptions(state)?.let { CardModel(GetString(R.string.activity_expiration_settings_timer), it) }
|
timeOptions(state)?.let { CardModel(GetString(R.string.activity_expiration_settings_timer), it) }
|
||||||
),
|
),
|
||||||
showGroupFooter = state.isGroup && state.isNewConfigEnabled,
|
showGroupFooter = state.isGroup && state.isNewConfigEnabled
|
||||||
callbacks = state.callbacks
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -234,31 +219,29 @@ data class CardModel(
|
|||||||
)
|
)
|
||||||
|
|
||||||
private fun typeOptions(state: State) =
|
private fun typeOptions(state: State) =
|
||||||
state.takeUnless {
|
state.takeUnless { it.modeOptionsHidden }?.run {
|
||||||
state.isNoteToSelf || state.isGroup && state.isNewConfigEnabled
|
|
||||||
}?.run {
|
|
||||||
listOfNotNull(
|
listOfNotNull(
|
||||||
typeOption(
|
typeOption(
|
||||||
ExpiryType.NONE,
|
ExpiryType.NONE,
|
||||||
state,
|
state,
|
||||||
R.string.expiration_off,
|
R.string.expiration_off,
|
||||||
contentDescription = R.string.AccessibilityId_disable_disappearing_messages,
|
contentDescription = R.string.AccessibilityId_disable_disappearing_messages,
|
||||||
enabled = state.isSelfAdmin
|
enabled = isSelfAdmin
|
||||||
),
|
),
|
||||||
if (!state.isNewConfigEnabled) typeOption(
|
if (!isNewConfigEnabled) typeOption(
|
||||||
ExpiryType.LEGACY,
|
ExpiryType.LEGACY,
|
||||||
state,
|
state,
|
||||||
R.string.expiration_type_disappear_legacy,
|
R.string.expiration_type_disappear_legacy,
|
||||||
contentDescription = R.string.expiration_type_disappear_legacy_description,
|
contentDescription = R.string.expiration_type_disappear_legacy_description,
|
||||||
enabled = state.isSelfAdmin
|
enabled = isSelfAdmin
|
||||||
) else null,
|
) else null,
|
||||||
if (!state.isGroup) typeOption(
|
if (!isGroup) typeOption(
|
||||||
ExpiryType.AFTER_READ,
|
ExpiryType.AFTER_READ,
|
||||||
state,
|
state,
|
||||||
R.string.expiration_type_disappear_after_read,
|
R.string.expiration_type_disappear_after_read,
|
||||||
R.string.expiration_type_disappear_after_read_description,
|
R.string.expiration_type_disappear_after_read_description,
|
||||||
contentDescription = R.string.expiration_type_disappear_after_read_description,
|
contentDescription = R.string.expiration_type_disappear_after_read_description,
|
||||||
enabled = state.isNewConfigEnabled && state.isSelfAdmin
|
enabled = isNewConfigEnabled && isSelfAdmin
|
||||||
) else null,
|
) else null,
|
||||||
typeOption(
|
typeOption(
|
||||||
ExpiryType.AFTER_SEND,
|
ExpiryType.AFTER_SEND,
|
||||||
@ -266,7 +249,7 @@ private fun typeOptions(state: State) =
|
|||||||
R.string.expiration_type_disappear_after_send,
|
R.string.expiration_type_disappear_after_send,
|
||||||
R.string.expiration_type_disappear_after_read_description,
|
R.string.expiration_type_disappear_after_read_description,
|
||||||
contentDescription = R.string.expiration_type_disappear_after_send_description,
|
contentDescription = R.string.expiration_type_disappear_after_send_description,
|
||||||
enabled = state.isNewConfigEnabled && state.isSelfAdmin
|
enabled = isNewConfigEnabled && isSelfAdmin
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@ -278,70 +261,89 @@ private fun typeOption(
|
|||||||
@StringRes subtitle: Int? = null,
|
@StringRes subtitle: Int? = null,
|
||||||
@StringRes contentDescription: Int = title,
|
@StringRes contentDescription: Int = title,
|
||||||
enabled: Boolean = true,
|
enabled: Boolean = true,
|
||||||
onClick: () -> Unit = { state.callbacks.setType(type) }
|
) = typeOption(
|
||||||
|
mode = type.defaultMode(state.persistedMode),
|
||||||
|
state = state,
|
||||||
|
title = title,
|
||||||
|
subtitle = subtitle,
|
||||||
|
contentDescription = contentDescription,
|
||||||
|
enabled = enabled
|
||||||
|
)
|
||||||
|
private fun typeOption(
|
||||||
|
mode: ExpiryMode,
|
||||||
|
state: State,
|
||||||
|
@StringRes title: Int,
|
||||||
|
@StringRes subtitle: Int? = null,
|
||||||
|
@StringRes contentDescription: Int = title,
|
||||||
|
enabled: Boolean = true,
|
||||||
|
onClick: Action = Action.SelectMode(mode)
|
||||||
) = OptionModel(
|
) = OptionModel(
|
||||||
GetString(title),
|
GetString(title),
|
||||||
subtitle?.let(::GetString),
|
subtitle?.let(::GetString),
|
||||||
selected = state.expiryType == type,
|
selected = state.expiryType == mode.type,
|
||||||
enabled = enabled,
|
enabled = enabled,
|
||||||
onClick = onClick
|
onClick = onClick
|
||||||
)
|
)
|
||||||
|
|
||||||
private fun timeOptions(state: State) =
|
private fun debugTimes(isDebug: Boolean) = if (isDebug) listOf(10.seconds, 1.minutes) else emptyList()
|
||||||
if (state.isNoteToSelf || (state.isGroup && state.isNewConfigEnabled)) timeOptionsOnly(state)
|
private fun debugModes(isDebug: Boolean, type: ExpiryType) =
|
||||||
else when (state.expiryMode) {
|
debugTimes(isDebug).map { type.mode(it.inWholeSeconds) }
|
||||||
is ExpiryMode.Legacy -> afterReadTimes
|
private fun debugOptions(state: State): List<OptionModel> =
|
||||||
is ExpiryMode.AfterRead -> afterReadTimes
|
debugModes(state.showDebugOptions, state.expiryType.takeIf { it != ExpiryType.NONE } ?: ExpiryType.AFTER_SEND)
|
||||||
is ExpiryMode.AfterSend -> afterSendTimes
|
.map { timeOption(it, state, subtitle = GetString("for testing purposes")) }
|
||||||
else -> null
|
|
||||||
}?.map { timeOption(it, state) }
|
|
||||||
|
|
||||||
private val DEBUG_TIMES = if (BuildConfig.DEBUG) listOf(10.seconds, 1.minutes) else emptyList()
|
|
||||||
|
|
||||||
val defaultTimes = listOf(12.hours, 1.days, 7.days, 14.days)
|
val defaultTimes = listOf(12.hours, 1.days, 7.days, 14.days)
|
||||||
|
|
||||||
val afterSendTimes = buildList {
|
val afterSendTimes = defaultTimes
|
||||||
addAll(DEBUG_TIMES)
|
val afterSendModes = afterSendTimes.map { it.inWholeSeconds }.map(ExpiryMode::AfterSend)
|
||||||
addAll(defaultTimes)
|
|
||||||
}
|
|
||||||
|
|
||||||
val afterReadTimes = buildList {
|
val afterReadTimes = buildList {
|
||||||
addAll(DEBUG_TIMES)
|
|
||||||
add(5.minutes)
|
add(5.minutes)
|
||||||
add(1.hours)
|
add(1.hours)
|
||||||
addAll(defaultTimes)
|
addAll(defaultTimes)
|
||||||
}
|
}
|
||||||
|
val afterReadModes = afterReadTimes.map { it.inWholeSeconds }.map(ExpiryMode::AfterRead)
|
||||||
|
|
||||||
private fun timeOptionsOnly(state: State) = listOfNotNull(
|
private fun timeOptions(state: State): List<OptionModel>? =
|
||||||
typeOption(ExpiryType.NONE, state, R.string.arrays__off, enabled = state.isSelfAdmin),
|
if (state.modeOptionsHidden) timeOptionsAfterSend(state)
|
||||||
) + afterSendTimes.map { timeOptionOnly(it, state) }
|
else when (state.expiryMode) {
|
||||||
|
is ExpiryMode.Legacy, is ExpiryMode.AfterRead -> debugOptions(state) + afterReadModes.map { timeOption(it, state) }
|
||||||
|
is ExpiryMode.AfterSend -> debugOptions(state) + afterSendModes.map { timeOption(it, state) }
|
||||||
|
else -> null
|
||||||
|
}
|
||||||
|
|
||||||
private fun timeOptionOnly(
|
private fun timeOptionsAfterSend(state: State) = listOfNotNull(
|
||||||
duration: Duration,
|
typeOption(ExpiryType.NONE, state, R.string.expiration_off, enabled = state.isSelfAdmin),
|
||||||
state: State,
|
) + debugOptions(state) + afterSendModes.map { timeOption(it, state) }
|
||||||
title: GetString = GetString { ExpirationUtil.getExpirationDisplayValue(it, duration.inWholeSeconds.toInt()) }
|
|
||||||
) = timeOption(duration, state, title) { state.callbacks.setMode(ExpiryMode.AfterSend(duration.inWholeSeconds)) }
|
|
||||||
|
|
||||||
private fun timeOption(
|
private fun timeOption(
|
||||||
duration: Duration,
|
mode: ExpiryMode,
|
||||||
state: State,
|
state: State,
|
||||||
title: GetString = GetString { ExpirationUtil.getExpirationDisplayValue(it, duration.inWholeSeconds.toInt()) },
|
title: GetString = GetString(mode.duration, ExpirationUtil::getExpirationDisplayValue),
|
||||||
subtitle: GetString? = if (duration in DEBUG_TIMES) GetString("for testing purposes") else null,
|
subtitle: GetString? = null,
|
||||||
onClick: () -> Unit = { state.callbacks.setTime(duration.inWholeSeconds) }
|
onClick: Action = Action.SelectMode(mode)
|
||||||
) = OptionModel(
|
) = OptionModel(
|
||||||
title = title,
|
title = title,
|
||||||
subtitle = subtitle,
|
subtitle = subtitle,
|
||||||
selected = state.expiryMode?.duration == duration,
|
selected = state.expiryMode == mode,
|
||||||
enabled = state.isTimeOptionsEnabled,
|
enabled = state.isTimeOptionsEnabled,
|
||||||
onClick = onClick
|
onClick = onClick
|
||||||
)
|
)
|
||||||
|
|
||||||
|
sealed interface Action {
|
||||||
|
operator fun invoke(callbacks: Callbacks) {}
|
||||||
|
|
||||||
|
data class SelectMode(val mode: ExpiryMode): Action {
|
||||||
|
override operator fun invoke(callbacks: Callbacks) = callbacks.setMode(mode)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
data class OptionModel(
|
data class OptionModel(
|
||||||
val title: GetString,
|
val title: GetString,
|
||||||
val subtitle: GetString? = null,
|
val subtitle: GetString? = null,
|
||||||
val selected: Boolean = false,
|
val selected: Boolean = false,
|
||||||
val enabled: Boolean = true,
|
val enabled: Boolean = true,
|
||||||
val onClick: () -> Unit = {}
|
val onClick: Action
|
||||||
)
|
)
|
||||||
|
|
||||||
enum class ExpiryType(private val createMode: (Long) -> ExpiryMode) {
|
enum class ExpiryType(private val createMode: (Long) -> ExpiryMode) {
|
||||||
|
@ -1,42 +1,60 @@
|
|||||||
package org.thoughtcrime.securesms.ui
|
package org.thoughtcrime.securesms.ui
|
||||||
|
|
||||||
import androidx.annotation.DrawableRes
|
import androidx.annotation.DrawableRes
|
||||||
|
import androidx.compose.foundation.BorderStroke
|
||||||
import androidx.compose.foundation.ExperimentalFoundationApi
|
import androidx.compose.foundation.ExperimentalFoundationApi
|
||||||
|
import androidx.compose.foundation.ScrollState
|
||||||
|
import androidx.compose.foundation.clickable
|
||||||
import androidx.compose.foundation.layout.Box
|
import androidx.compose.foundation.layout.Box
|
||||||
import androidx.compose.foundation.layout.BoxScope
|
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.RowScope
|
||||||
import androidx.compose.foundation.layout.Spacer
|
import androidx.compose.foundation.layout.Spacer
|
||||||
import androidx.compose.foundation.layout.fillMaxHeight
|
import androidx.compose.foundation.layout.fillMaxHeight
|
||||||
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.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.layout.size
|
||||||
import androidx.compose.foundation.layout.width
|
import androidx.compose.foundation.layout.width
|
||||||
import androidx.compose.foundation.layout.wrapContentHeight
|
import androidx.compose.foundation.layout.wrapContentHeight
|
||||||
import androidx.compose.foundation.pager.PagerState
|
import androidx.compose.foundation.pager.PagerState
|
||||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
import androidx.compose.material.ButtonColors
|
import androidx.compose.material.ButtonColors
|
||||||
|
import androidx.compose.material.ButtonDefaults
|
||||||
import androidx.compose.material.Card
|
import androidx.compose.material.Card
|
||||||
import androidx.compose.material.Colors
|
import androidx.compose.material.Colors
|
||||||
import androidx.compose.material.Icon
|
import androidx.compose.material.Icon
|
||||||
import androidx.compose.material.IconButton
|
import androidx.compose.material.IconButton
|
||||||
import androidx.compose.material.MaterialTheme
|
import androidx.compose.material.MaterialTheme
|
||||||
|
import androidx.compose.material.OutlinedButton
|
||||||
|
import androidx.compose.material.RadioButton
|
||||||
import androidx.compose.material.Text
|
import androidx.compose.material.Text
|
||||||
import androidx.compose.material.TextButton
|
import androidx.compose.material.TextButton
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.rememberCoroutineScope
|
import androidx.compose.runtime.rememberCoroutineScope
|
||||||
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.graphics.BlendMode
|
||||||
|
import androidx.compose.ui.graphics.Brush
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
import androidx.compose.ui.graphics.RectangleShape
|
import androidx.compose.ui.graphics.RectangleShape
|
||||||
|
import androidx.compose.ui.graphics.graphicsLayer
|
||||||
import androidx.compose.ui.res.painterResource
|
import androidx.compose.ui.res.painterResource
|
||||||
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.viewinterop.AndroidView
|
import androidx.compose.ui.viewinterop.AndroidView
|
||||||
import com.google.accompanist.pager.HorizontalPagerIndicator
|
import com.google.accompanist.pager.HorizontalPagerIndicator
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import network.loki.messenger.R
|
import network.loki.messenger.R
|
||||||
import org.session.libsession.utilities.recipients.Recipient
|
import org.session.libsession.utilities.recipients.Recipient
|
||||||
import org.thoughtcrime.securesms.components.ProfilePictureView
|
import org.thoughtcrime.securesms.components.ProfilePictureView
|
||||||
|
import org.thoughtcrime.securesms.conversation.expiration.OptionModel
|
||||||
|
import kotlin.math.min
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun ItemButton(
|
fun ItemButton(
|
||||||
@ -95,10 +113,99 @@ fun CellWithPaddingAndMargin(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun TitledRadioButton(option: OptionModel, onClick: () -> Unit) {
|
||||||
|
Row(modifier = Modifier
|
||||||
|
.clickable { if (!option.selected) onClick() }
|
||||||
|
.heightIn(min = 60.dp)
|
||||||
|
.padding(horizontal = 34.dp)) {
|
||||||
|
Column(modifier = Modifier
|
||||||
|
.weight(1f)
|
||||||
|
.align(Alignment.CenterVertically)) {
|
||||||
|
Column {
|
||||||
|
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(
|
||||||
|
selected = option.selected,
|
||||||
|
onClick = null,
|
||||||
|
enabled = option.enabled,
|
||||||
|
modifier = Modifier
|
||||||
|
.height(26.dp)
|
||||||
|
.align(Alignment.CenterVertically)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun OutlineButton(text: String, modifier: Modifier = Modifier, onClick: () -> Unit) {
|
||||||
|
OutlinedButton(
|
||||||
|
modifier = modifier.size(108.dp, 34.dp),
|
||||||
|
onClick = onClick,
|
||||||
|
border = BorderStroke(1.dp, LocalExtraColors.current.prominentButtonColor),
|
||||||
|
shape = RoundedCornerShape(50), // = 50% percent
|
||||||
|
colors = ButtonDefaults.outlinedButtonColors(
|
||||||
|
contentColor = LocalExtraColors.current.prominentButtonColor,
|
||||||
|
backgroundColor = MaterialTheme.colors.background
|
||||||
|
)
|
||||||
|
){
|
||||||
|
Text(text = text)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private val Colors.cellColor: Color
|
private val Colors.cellColor: Color
|
||||||
@Composable
|
@Composable
|
||||||
get() = LocalExtraColors.current.settingsBackground
|
get() = LocalExtraColors.current.settingsBackground
|
||||||
|
|
||||||
|
fun Modifier.fadingEdges(
|
||||||
|
scrollState: ScrollState,
|
||||||
|
topEdgeHeight: Dp = 0.dp,
|
||||||
|
bottomEdgeHeight: Dp = 20.dp
|
||||||
|
): Modifier = this.then(
|
||||||
|
Modifier
|
||||||
|
// adding layer fixes issue with blending gradient and content
|
||||||
|
.graphicsLayer { alpha = 0.99F }
|
||||||
|
.drawWithContent {
|
||||||
|
drawContent()
|
||||||
|
|
||||||
|
val topColors = listOf(Color.Transparent, Color.Black)
|
||||||
|
val topStartY = scrollState.value.toFloat()
|
||||||
|
val topGradientHeight = min(topEdgeHeight.toPx(), topStartY)
|
||||||
|
drawRect(
|
||||||
|
brush = Brush.verticalGradient(
|
||||||
|
colors = topColors,
|
||||||
|
startY = topStartY,
|
||||||
|
endY = topStartY + topGradientHeight
|
||||||
|
),
|
||||||
|
blendMode = BlendMode.DstIn
|
||||||
|
)
|
||||||
|
|
||||||
|
val bottomColors = listOf(Color.Black, Color.Transparent)
|
||||||
|
val bottomEndY = size.height - scrollState.maxValue + scrollState.value
|
||||||
|
val bottomGradientHeight =
|
||||||
|
min(bottomEdgeHeight.toPx(), scrollState.maxValue.toFloat() - scrollState.value)
|
||||||
|
if (bottomGradientHeight != 0f) drawRect(
|
||||||
|
brush = Brush.verticalGradient(
|
||||||
|
colors = bottomColors,
|
||||||
|
startY = bottomEndY - bottomGradientHeight,
|
||||||
|
endY = bottomEndY
|
||||||
|
),
|
||||||
|
blendMode = BlendMode.DstIn
|
||||||
|
)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
@OptIn(ExperimentalFoundationApi::class)
|
@OptIn(ExperimentalFoundationApi::class)
|
||||||
@Composable
|
@Composable
|
||||||
fun BoxScope.HorizontalPagerIndicator(pagerState: PagerState) {
|
fun BoxScope.HorizontalPagerIndicator(pagerState: PagerState) {
|
||||||
|
@ -5,6 +5,8 @@ import androidx.annotation.StringRes
|
|||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.ui.platform.LocalContext
|
import androidx.compose.ui.platform.LocalContext
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
|
import org.session.libsession.utilities.ExpirationUtil
|
||||||
|
import kotlin.time.Duration
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Compatibility class to allow ViewModels to use strings and string resources interchangeably.
|
* Compatibility class to allow ViewModels to use strings and string resources interchangeably.
|
||||||
@ -34,11 +36,20 @@ sealed class GetString {
|
|||||||
override fun string(): String = function(LocalContext.current)
|
override fun string(): String = function(LocalContext.current)
|
||||||
override fun string(context: Context): String = function(context)
|
override fun string(context: Context): String = function(context)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
data class FromMap<T>(val value: T, val function: (Context, T) -> String): GetString() {
|
||||||
|
@Composable
|
||||||
|
override fun string(): String = function(LocalContext.current, value)
|
||||||
|
|
||||||
|
override fun string(context: Context): String = function(context, value)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun GetString(@StringRes resId: Int) = GetString.FromResId(resId)
|
fun GetString(@StringRes resId: Int) = GetString.FromResId(resId)
|
||||||
fun GetString(string: String) = GetString.FromString(string)
|
fun GetString(string: String) = GetString.FromString(string)
|
||||||
fun GetString(function: (Context) -> String) = GetString.FromFun(function)
|
fun GetString(function: (Context) -> String) = GetString.FromFun(function)
|
||||||
|
fun <T> GetString(value: T, function: (Context, T) -> String) = GetString.FromMap(value, function)
|
||||||
|
fun GetString(duration: Duration) = GetString.FromMap(duration, ExpirationUtil::getExpirationDisplayValue)
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -5,6 +5,7 @@ import kotlinx.coroutines.ExperimentalCoroutinesApi
|
|||||||
import kotlinx.coroutines.test.advanceUntilIdle
|
import kotlinx.coroutines.test.advanceUntilIdle
|
||||||
import kotlinx.coroutines.test.runTest
|
import kotlinx.coroutines.test.runTest
|
||||||
import network.loki.messenger.libsession_util.util.ExpiryMode
|
import network.loki.messenger.libsession_util.util.ExpiryMode
|
||||||
|
import network.loki.messenger.R
|
||||||
import org.hamcrest.CoreMatchers
|
import org.hamcrest.CoreMatchers
|
||||||
import org.hamcrest.MatcherAssert
|
import org.hamcrest.MatcherAssert
|
||||||
import org.junit.Rule
|
import org.junit.Rule
|
||||||
@ -24,6 +25,8 @@ import org.thoughtcrime.securesms.MainCoroutineRule
|
|||||||
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
|
||||||
|
import org.thoughtcrime.securesms.ui.GetString
|
||||||
|
import kotlin.time.Duration.Companion.days
|
||||||
import kotlin.time.Duration.Companion.hours
|
import kotlin.time.Duration.Companion.hours
|
||||||
|
|
||||||
@OptIn(ExperimentalCoroutinesApi::class)
|
@OptIn(ExperimentalCoroutinesApi::class)
|
||||||
@ -33,17 +36,17 @@ class ExpirationSettingsViewModelTest {
|
|||||||
@get:Rule
|
@get:Rule
|
||||||
var mainCoroutineRule = MainCoroutineRule()
|
var mainCoroutineRule = MainCoroutineRule()
|
||||||
|
|
||||||
val application: Application = mock(Application::class.java)
|
private val application: Application = mock(Application::class.java)
|
||||||
val textSecurePreferences: TextSecurePreferences = mock(TextSecurePreferences::class.java)
|
private val textSecurePreferences: TextSecurePreferences = mock(TextSecurePreferences::class.java)
|
||||||
val messageExpirationManager: SSKEnvironment.MessageExpirationManagerProtocol = mock(SSKEnvironment.MessageExpirationManagerProtocol::class.java)
|
private val messageExpirationManager: SSKEnvironment.MessageExpirationManagerProtocol = mock(SSKEnvironment.MessageExpirationManagerProtocol::class.java)
|
||||||
val threadDb: ThreadDatabase = mock(ThreadDatabase::class.java)
|
private val threadDb: ThreadDatabase = mock(ThreadDatabase::class.java)
|
||||||
val groupDb: GroupDatabase = mock(GroupDatabase::class.java)
|
private val groupDb: GroupDatabase = mock(GroupDatabase::class.java)
|
||||||
val storage: Storage = mock(Storage::class.java)
|
private val storage: Storage = mock(Storage::class.java)
|
||||||
|
|
||||||
val recipient = mock(Recipient::class.java)
|
private val recipient = mock(Recipient::class.java)
|
||||||
|
|
||||||
val groupRecord = mock(GroupRecord::class.java)
|
private val groupRecord = mock(GroupRecord::class.java)
|
||||||
val optionalGroupRecord = Optional.of(groupRecord)
|
private val optionalGroupRecord = Optional.of(groupRecord)
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `UI should show a list of times and an Off option`() = runTest {
|
fun `UI should show a list of times and an Off option`() = runTest {
|
||||||
@ -80,9 +83,18 @@ class ExpirationSettingsViewModelTest {
|
|||||||
CoreMatchers.equalTo(1)
|
CoreMatchers.equalTo(1)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
val options = viewModel.uiState.value.cards[0].options
|
||||||
MatcherAssert.assertThat(
|
MatcherAssert.assertThat(
|
||||||
viewModel.uiState.value.cards[0].options.count(),
|
options.map { it.title },
|
||||||
CoreMatchers.equalTo(6)
|
CoreMatchers.equalTo(
|
||||||
|
listOf(
|
||||||
|
GetString(R.string.expiration_off),
|
||||||
|
GetString(12.hours),
|
||||||
|
GetString(1.days),
|
||||||
|
GetString(7.days),
|
||||||
|
GetString(14.days)
|
||||||
|
)
|
||||||
|
)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -94,6 +106,7 @@ class ExpirationSettingsViewModelTest {
|
|||||||
threadDb,
|
threadDb,
|
||||||
groupDb,
|
groupDb,
|
||||||
storage,
|
storage,
|
||||||
isNewConfigEnabled
|
isNewConfigEnabled,
|
||||||
|
false
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -1,50 +0,0 @@
|
|||||||
package org.session.libsession.utilities;
|
|
||||||
|
|
||||||
import android.content.Context;
|
|
||||||
|
|
||||||
import org.session.libsession.R;
|
|
||||||
|
|
||||||
import java.util.concurrent.TimeUnit;
|
|
||||||
|
|
||||||
public class ExpirationUtil {
|
|
||||||
|
|
||||||
public static String getExpirationDisplayValue(Context context, int expirationTime) {
|
|
||||||
if (expirationTime <= 0) {
|
|
||||||
return context.getString(R.string.expiration_off);
|
|
||||||
} else if (expirationTime < TimeUnit.MINUTES.toSeconds(1)) {
|
|
||||||
return context.getResources().getQuantityString(R.plurals.expiration_seconds, expirationTime, expirationTime);
|
|
||||||
} else if (expirationTime < TimeUnit.HOURS.toSeconds(1)) {
|
|
||||||
int minutes = expirationTime / (int)TimeUnit.MINUTES.toSeconds(1);
|
|
||||||
return context.getResources().getQuantityString(R.plurals.expiration_minutes, minutes, minutes);
|
|
||||||
} else if (expirationTime < TimeUnit.DAYS.toSeconds(1)) {
|
|
||||||
int hours = expirationTime / (int)TimeUnit.HOURS.toSeconds(1);
|
|
||||||
return context.getResources().getQuantityString(R.plurals.expiration_hours, hours, hours);
|
|
||||||
} else if (expirationTime < TimeUnit.DAYS.toSeconds(7)) {
|
|
||||||
int days = expirationTime / (int)TimeUnit.DAYS.toSeconds(1);
|
|
||||||
return context.getResources().getQuantityString(R.plurals.expiration_days, days, days);
|
|
||||||
} else {
|
|
||||||
int weeks = expirationTime / (int)TimeUnit.DAYS.toSeconds(7);
|
|
||||||
return context.getResources().getQuantityString(R.plurals.expiration_weeks, weeks, weeks);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public static String getExpirationAbbreviatedDisplayValue(Context context, long expirationTime) {
|
|
||||||
if (expirationTime < TimeUnit.MINUTES.toSeconds(1)) {
|
|
||||||
return context.getResources().getString(R.string.expiration_seconds_abbreviated, expirationTime);
|
|
||||||
} else if (expirationTime < TimeUnit.HOURS.toSeconds(1)) {
|
|
||||||
long minutes = expirationTime / TimeUnit.MINUTES.toSeconds(1);
|
|
||||||
return context.getResources().getString(R.string.expiration_minutes_abbreviated, minutes);
|
|
||||||
} else if (expirationTime < TimeUnit.DAYS.toSeconds(1)) {
|
|
||||||
long hours = expirationTime / TimeUnit.HOURS.toSeconds(1);
|
|
||||||
return context.getResources().getString(R.string.expiration_hours_abbreviated, hours);
|
|
||||||
} else if (expirationTime < TimeUnit.DAYS.toSeconds(7)) {
|
|
||||||
long days = expirationTime / TimeUnit.DAYS.toSeconds(1);
|
|
||||||
return context.getResources().getString(R.string.expiration_days_abbreviated, days);
|
|
||||||
} else {
|
|
||||||
long weeks = expirationTime / TimeUnit.DAYS.toSeconds(7);
|
|
||||||
return context.getResources().getString(R.string.expiration_weeks_abbreviated, weeks);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
}
|
|
@ -0,0 +1,54 @@
|
|||||||
|
package org.session.libsession.utilities
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import org.session.libsession.R
|
||||||
|
import java.util.concurrent.TimeUnit
|
||||||
|
import kotlin.time.Duration
|
||||||
|
|
||||||
|
object ExpirationUtil {
|
||||||
|
@JvmStatic
|
||||||
|
fun getExpirationDisplayValue(context: Context, duration: Duration): String = getExpirationDisplayValue(context, duration.inWholeSeconds.toInt())
|
||||||
|
|
||||||
|
@JvmStatic
|
||||||
|
fun getExpirationDisplayValue(context: Context, expirationTime: Int): String {
|
||||||
|
return if (expirationTime <= 0) {
|
||||||
|
context.getString(R.string.expiration_off)
|
||||||
|
} else if (expirationTime < TimeUnit.MINUTES.toSeconds(1)) {
|
||||||
|
context.resources.getQuantityString(
|
||||||
|
R.plurals.expiration_seconds,
|
||||||
|
expirationTime,
|
||||||
|
expirationTime
|
||||||
|
)
|
||||||
|
} else if (expirationTime < TimeUnit.HOURS.toSeconds(1)) {
|
||||||
|
val minutes = expirationTime / TimeUnit.MINUTES.toSeconds(1).toInt()
|
||||||
|
context.resources.getQuantityString(R.plurals.expiration_minutes, minutes, minutes)
|
||||||
|
} else if (expirationTime < TimeUnit.DAYS.toSeconds(1)) {
|
||||||
|
val hours = expirationTime / TimeUnit.HOURS.toSeconds(1).toInt()
|
||||||
|
context.resources.getQuantityString(R.plurals.expiration_hours, hours, hours)
|
||||||
|
} else if (expirationTime < TimeUnit.DAYS.toSeconds(7)) {
|
||||||
|
val days = expirationTime / TimeUnit.DAYS.toSeconds(1).toInt()
|
||||||
|
context.resources.getQuantityString(R.plurals.expiration_days, days, days)
|
||||||
|
} else {
|
||||||
|
val weeks = expirationTime / TimeUnit.DAYS.toSeconds(7).toInt()
|
||||||
|
context.resources.getQuantityString(R.plurals.expiration_weeks, weeks, weeks)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getExpirationAbbreviatedDisplayValue(context: Context, expirationTime: Long): String {
|
||||||
|
return if (expirationTime < TimeUnit.MINUTES.toSeconds(1)) {
|
||||||
|
context.resources.getString(R.string.expiration_seconds_abbreviated, expirationTime)
|
||||||
|
} else if (expirationTime < TimeUnit.HOURS.toSeconds(1)) {
|
||||||
|
val minutes = expirationTime / TimeUnit.MINUTES.toSeconds(1)
|
||||||
|
context.resources.getString(R.string.expiration_minutes_abbreviated, minutes)
|
||||||
|
} else if (expirationTime < TimeUnit.DAYS.toSeconds(1)) {
|
||||||
|
val hours = expirationTime / TimeUnit.HOURS.toSeconds(1)
|
||||||
|
context.resources.getString(R.string.expiration_hours_abbreviated, hours)
|
||||||
|
} else if (expirationTime < TimeUnit.DAYS.toSeconds(7)) {
|
||||||
|
val days = expirationTime / TimeUnit.DAYS.toSeconds(1)
|
||||||
|
context.resources.getString(R.string.expiration_days_abbreviated, days)
|
||||||
|
} else {
|
||||||
|
val weeks = expirationTime / TimeUnit.DAYS.toSeconds(7)
|
||||||
|
context.resources.getString(R.string.expiration_weeks_abbreviated, weeks)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
Loading…
x
Reference in New Issue
Block a user