Use compose

This commit is contained in:
andrew 2023-08-24 22:37:25 +09:30
parent 5142c45643
commit 71b2544c31
10 changed files with 414 additions and 442 deletions

View File

@ -166,14 +166,14 @@ dependencies {
testImplementation 'org.robolectric:shadows-multidex:4.4'
implementation 'com.github.bumptech.glide:compose:1.0.0-alpha.1'
implementation 'androidx.compose.ui:ui:1.4.3'
implementation 'androidx.compose.ui:ui-tooling:1.4.3'
implementation 'androidx.compose.ui:ui:1.5.0'
implementation 'androidx.compose.ui:ui-tooling:1.5.0'
implementation "com.google.accompanist:accompanist-themeadapter-appcompat:0.31.5-beta"
implementation "com.google.accompanist:accompanist-pager-indicators:0.31.5-beta"
implementation "androidx.compose.runtime:runtime-livedata:1.4.3"
implementation "androidx.compose.runtime:runtime-livedata:1.5.0"
implementation 'androidx.compose.foundation:foundation-layout:1.5.0-alpha02'
implementation 'androidx.compose.material:material:1.5.0-alpha02'
implementation 'androidx.compose.foundation:foundation-layout:1.5.0'
implementation 'androidx.compose.material:material:1.5.0'
}
def canonicalVersionCode = 354

View File

@ -1,33 +1,58 @@
package org.thoughtcrime.securesms.conversation.expiration
import android.os.Bundle
import android.os.Parcelable
import android.util.SparseArray
import android.widget.Toast
import androidx.activity.viewModels
import androidx.core.text.HtmlCompat
import androidx.core.view.isVisible
import androidx.compose.foundation.BorderStroke
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.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.items
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.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Brush
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.lifecycleScope
import androidx.lifecycle.repeatOnLifecycle
import androidx.recyclerview.widget.RecyclerView
import androidx.recyclerview.widget.SimpleItemAnimator
import com.google.android.material.divider.MaterialDividerItemDecoration
import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.launch
import network.loki.messenger.BuildConfig
import network.loki.messenger.R
import network.loki.messenger.databinding.ActivityExpirationSettingsBinding
import network.loki.messenger.libsession_util.util.ExpiryMode
import org.thoughtcrime.securesms.PassphraseRequiredActionBarActivity
import org.thoughtcrime.securesms.database.RecipientDatabase
import org.thoughtcrime.securesms.database.ThreadDatabase
import org.thoughtcrime.securesms.preferences.ExpirationRadioOption
import org.thoughtcrime.securesms.preferences.RadioOptionAdapter
import org.thoughtcrime.securesms.preferences.radioOption
import org.thoughtcrime.securesms.ui.AppTheme
import org.thoughtcrime.securesms.ui.CellNoMargin
import org.thoughtcrime.securesms.ui.GetString
import org.thoughtcrime.securesms.ui.PreviewTheme
import org.thoughtcrime.securesms.ui.ThemeResPreviewParameterProvider
import org.thoughtcrime.securesms.util.ConfigurationMessageUtilities
import javax.inject.Inject
import kotlin.math.max
@AndroidEntryPoint
class ExpirationSettingsActivity: PassphraseRequiredActionBarActivity() {
@ -43,34 +68,16 @@ class ExpirationSettingsActivity: PassphraseRequiredActionBarActivity() {
}
private val viewModel: ExpirationSettingsViewModel by viewModels {
val afterReadOptions = resources.getIntArray(R.array.read_expiration_time_values)
.zip(resources.getStringArray(R.array.read_expiration_time_names)) { value, name ->
radioOption(ExpiryMode.AfterRead(value.toLong()), name) { contentDescription(R.string.AccessibilityId_time_option) }
}
val afterSendOptions = resources.getIntArray(R.array.send_expiration_time_values)
.zip(resources.getStringArray(R.array.send_expiration_time_names)) { value, name ->
radioOption(ExpiryMode.AfterSend(value.toLong()), name) { contentDescription(R.string.AccessibilityId_time_option) }
}
viewModelFactory.create(threadId, mayAddTestExpiryOption(afterReadOptions), mayAddTestExpiryOption(afterSendOptions))
viewModelFactory.create(threadId)
}
private fun mayAddTestExpiryOption(expiryOptions: List<ExpirationRadioOption>): List<ExpirationRadioOption> =
if (BuildConfig.DEBUG) {
when (expiryOptions.first().value) {
is ExpiryMode.AfterRead -> ExpiryMode.AfterRead(60)
is ExpiryMode.AfterSend -> ExpiryMode.AfterSend(60)
is ExpiryMode.Legacy -> ExpiryMode.Legacy(60)
ExpiryMode.NONE -> ExpiryMode.NONE // shouldn't happen
}.let { radioOption(it, "1 Minute (for testing purposes)") }
.let { expiryOptions.toMutableList().apply { add(1, it) } }
} else expiryOptions
override fun onSaveInstanceState(outState: Bundle) {
super.onSaveInstanceState(outState)
val scrollParcelArray = SparseArray<Parcelable>()
binding.scrollView.saveHierarchyState(scrollParcelArray)
outState.putSparseParcelableArray(SCROLL_PARCEL, scrollParcelArray)
}
// override fun onSaveInstanceState(outState: Bundle) {
// super.onSaveInstanceState(outState)
// val scrollParcelArray = SparseArray<Parcelable>()
// binding.scrollView.saveHierarchyState(scrollParcelArray)
// outState.putSparseParcelableArray(SCROLL_PARCEL, scrollParcelArray)
// }
override fun onCreate(savedInstanceState: Bundle?, ready: Boolean) {
super.onCreate(savedInstanceState, ready)
@ -79,45 +86,35 @@ class ExpirationSettingsActivity: PassphraseRequiredActionBarActivity() {
setUpToolbar()
savedInstanceState?.let { bundle ->
val scrollStateParcel = bundle.getSparseParcelableArray<Parcelable>(SCROLL_PARCEL)
if (scrollStateParcel != null) {
binding.scrollView.restoreHierarchyState(scrollStateParcel)
}
}
binding.container.setContent { DisappearingMessagesScreen() }
val deleteTypeOptions = viewModel.getDeleteOptions()
val deleteTypeOptionAdapter = RadioOptionAdapter {
viewModel.onExpirationTypeSelected(it)
}
binding.recyclerViewDeleteTypes.apply {
(itemAnimator as SimpleItemAnimator).supportsChangeAnimations = false
adapter = deleteTypeOptionAdapter
addDividers()
setHasFixedSize(true)
}
deleteTypeOptionAdapter.submitList(deleteTypeOptions)
// savedInstanceState?.let { bundle ->
// val scrollStateParcel = bundle.getSparseParcelableArray<Parcelable>(SCROLL_PARCEL)
// if (scrollStateParcel != null) {
// binding.scrollView.restoreHierarchyState(scrollStateParcel)
// }
// }
val timerOptionAdapter = RadioOptionAdapter {
viewModel.onExpirationTimerSelected(it)
}
binding.recyclerViewTimerOptions.apply {
(itemAnimator as SimpleItemAnimator).supportsChangeAnimations = false
adapter = timerOptionAdapter
addDividers()
}
binding.buttonSet.setOnClickListener {
viewModel.onSetClick()
}
// val deleteTypeOptions = viewModel.getDeleteOptions()
// binding.buttonSet.setOnClickListener {
// viewModel.onSetClick()
// }
lifecycleScope.launch {
repeatOnLifecycle(Lifecycle.State.STARTED) {
viewModel.uiState.collect { uiState ->
binding.textViewDeleteType.isVisible = uiState.showExpirationTypeSelector
binding.layoutDeleteTypes.isVisible = uiState.showExpirationTypeSelector
binding.textViewFooter.isVisible = uiState.recipient?.isClosedGroupRecipient == true
binding.textViewFooter.text = HtmlCompat.fromHtml(getString(R.string.activity_expiration_settings_group_footer), HtmlCompat.FROM_HTML_MODE_COMPACT)
viewModel.state.collect { state ->
// actionBar?.subtitle = if (state.selectedExpirationType.value is ExpiryMode.AfterSend) {
// getString(R.string.activity_expiration_settings_subtitle_sent)
// } else {
// getString(R.string.activity_expiration_settings_subtitle)
// }
when (uiState.settingsSaved) {
// binding.textViewDeleteType.isVisible = state.showExpirationTypeSelector
// binding.layoutDeleteTypes.isVisible = state.showExpirationTypeSelector
// binding.textViewFooter.isVisible = state.recipient?.isClosedGroupRecipient == true
// binding.textViewFooter.text = HtmlCompat.fromHtml(getString(R.string.activity_expiration_settings_group_footer), HtmlCompat.FROM_HTML_MODE_COMPACT)
when (state.settingsSaved) {
true -> {
ConfigurationMessageUtilities.forceSyncConfigurationNowIfNeeded(this@ExpirationSettingsActivity)
finish()
@ -125,47 +122,39 @@ class ExpirationSettingsActivity: PassphraseRequiredActionBarActivity() {
false -> showToast(getString(R.string.ExpirationSettingsActivity_settings_not_updated))
else -> {}
}
}
}
}
lifecycleScope.launch {
repeatOnLifecycle(Lifecycle.State.STARTED) {
viewModel.selectedExpirationType.collect { type ->
val position = deleteTypeOptions.indexOfFirst { it.value == type }
deleteTypeOptionAdapter.setSelectedPosition(max(0, position))
}
}
}
lifecycleScope.launch {
repeatOnLifecycle(Lifecycle.State.STARTED) {
viewModel.selectedExpirationTimer.collect { option ->
val position =
viewModel.expirationTimerOptions.value.indexOfFirst { it.value == option?.value }
timerOptionAdapter.setSelectedPosition(max(0, position))
}
}
}
lifecycleScope.launch {
repeatOnLifecycle(Lifecycle.State.STARTED) {
viewModel.expirationTimerOptions.collect { options ->
binding.textViewTimer.isVisible =
options.isNotEmpty() && viewModel.uiState.value.showExpirationTypeSelector
binding.layoutTimer.isVisible = options.isNotEmpty()
timerOptionAdapter.submitList(options)
}
}
}
}
private fun RecyclerView.addDividers() {
addItemDecoration(
MaterialDividerItemDecoration(
this@ExpirationSettingsActivity,
RecyclerView.VERTICAL
).apply {
isLastItemDecorated = false
// val position = deleteTypeOptions.indexOfFirst { it.value == state.selectedExpirationType }
// deleteTypeOptionAdapter.setSelectedPosition(max(0, position))
}
}
)
}
// lifecycleScope.launch {
// repeatOnLifecycle(Lifecycle.State.STARTED) {
// viewModel.selectedExpirationType.collect { type ->
// val position = deleteTypeOptions.indexOfFirst { it.value == type }
// deleteTypeOptionAdapter.setSelectedPosition(max(0, position))
// }
// }
// }
// lifecycleScope.launch {
// repeatOnLifecycle(Lifecycle.State.STARTED) {
// viewModel.selectedExpirationTimer.collect { option ->
// val position =
// viewModel.expirationTimerOptions.value.indexOfFirst { it.value == option?.value }
// timerOptionAdapter.setSelectedPosition(max(0, position))
// }
// }
// }
// lifecycleScope.launch {
// repeatOnLifecycle(Lifecycle.State.STARTED) {
// viewModel.expirationTimerOptions.collect { options ->
// binding.textViewTimer.isVisible =
// options.isNotEmpty() && viewModel.uiState.value.showExpirationTypeSelector
// binding.layoutTimer.isVisible = options.isNotEmpty()
// timerOptionAdapter.submitList(options)
// }
// }
// }
}
private fun showToast(message: String) {
@ -176,11 +165,6 @@ class ExpirationSettingsActivity: PassphraseRequiredActionBarActivity() {
setSupportActionBar(binding.toolbar)
val actionBar = supportActionBar ?: return
actionBar.title = getString(R.string.activity_expiration_settings_title)
actionBar.subtitle = if (viewModel.selectedExpirationType.value is ExpiryMode.AfterSend) {
getString(R.string.activity_expiration_settings_subtitle_sent)
} else {
getString(R.string.activity_expiration_settings_subtitle)
}
actionBar.setDisplayHomeAsUpEnabled(true)
actionBar.setHomeButtonEnabled(true)
}
@ -190,4 +174,139 @@ class ExpirationSettingsActivity: PassphraseRequiredActionBarActivity() {
const val THREAD_ID = "thread_id"
}
}
@Composable
fun DisappearingMessagesScreen() {
val uiState by viewModel.uiState.collectAsState(UiState())
AppTheme {
DisappearingMessages(uiState, onSetClick = viewModel::onSetClick)
}
}
}
@Composable
fun DisappearingMessages(
state: UiState,
modifier: Modifier = Modifier,
onSetClick: () -> Unit = {}
) {
Column(modifier = modifier) {
Box(modifier = Modifier.weight(1f)) {
Column(
modifier = Modifier
.padding(horizontal = 32.dp)
.verticalScroll(rememberScrollState()),
verticalArrangement = Arrangement.spacedBy(16.dp)
) {
state.cards.filter { it.options.isNotEmpty() }.forEach { OptionsCard(it) }
}
Gradient(100.dp, modifier = Modifier.align(Alignment.BottomCenter))
}
OutlineButton(
stringResource(R.string.expiration_settings_set_button_title),
modifier = Modifier
.align(Alignment.CenterHorizontally)
.padding(bottom = 20.dp),
onClick = onSetClick
)
}
}
@Composable
fun OptionsCard(card: CardModel) {
Text(text = card.title())
CellNoMargin {
LazyColumn(
modifier = Modifier.heightIn(max = 5000.dp)
) {
items(card.options) {
TitledRadioButton(it)
}
}
}
}
@Composable
fun Gradient(height: Dp, modifier: Modifier = Modifier) {
Box(
modifier = modifier
.fillMaxWidth()
.height(height)
.background(
brush = Brush.verticalGradient(
colors = listOf(Color.Transparent, MaterialTheme.colors.primary),
startY = 0f,
endY = height.value
)
)
)
}
@Composable
fun TitledRadioButton(option: OptionModel) {
Row(modifier = Modifier
.heightIn(min = 60.dp)
.padding(horizontal = 34.dp)) {
Column(modifier = Modifier
.weight(1f)
.align(Alignment.CenterVertically)) {
Column {
Text(text = option.title())
option.subtitle?.let { Text(text = it()) }
}
}
RadioButton(
selected = option.selected,
onClick = option.onClick,
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, MaterialTheme.colors.secondary),
shape = RoundedCornerShape(50), // = 50% percent
colors = ButtonDefaults.outlinedButtonColors(contentColor = MaterialTheme.colors.secondary)
){
Text(text = text)
}
}
@Preview
@Composable
fun PreviewMessageDetails(
@PreviewParameter(ThemeResPreviewParameterProvider::class) themeResId: Int
) {
PreviewTheme(themeResId) {
DisappearingMessages(
UiState(
cards = listOf(
CardModel(GetString(R.string.activity_expiration_settings_delete_type), typeOptions()),
CardModel(GetString(R.string.activity_expiration_settings_timer), timeOptions())
)
),
modifier = Modifier.size(400.dp, 600.dp)
)
}
}
fun typeOptions() = listOf(
OptionModel(GetString(R.string.expiration_off)),
OptionModel(GetString(R.string.expiration_type_disappear_legacy)),
OptionModel(GetString(R.string.expiration_type_disappear_after_read)),
OptionModel(GetString(R.string.expiration_type_disappear_after_send))
)
fun timeOptions() = listOf(
OptionModel(GetString("1 Minute")),
OptionModel(GetString("5 Minutes")),
OptionModel(GetString("1 Week")),
OptionModel(GetString("2 Weeks")),
)

View File

@ -1,5 +1,6 @@
package org.thoughtcrime.securesms.conversation.expiration
import androidx.annotation.StringRes
import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.viewModelScope
@ -7,36 +8,31 @@ import dagger.assisted.Assisted
import dagger.assisted.AssistedInject
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.mapLatest
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.map
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.GroupRecord
import org.session.libsession.utilities.ExpirationUtil
import org.session.libsession.utilities.SSKEnvironment.MessageExpirationManagerProtocol
import org.session.libsession.utilities.TextSecurePreferences
import org.session.libsession.utilities.recipients.Recipient
import org.session.libsignal.utilities.guava.Optional
import org.thoughtcrime.securesms.database.GroupDatabase
import org.thoughtcrime.securesms.database.Storage
import org.thoughtcrime.securesms.database.ThreadDatabase
import org.thoughtcrime.securesms.preferences.ExpirationRadioOption
import org.thoughtcrime.securesms.preferences.RadioOption
import org.thoughtcrime.securesms.preferences.radioOption
import kotlin.reflect.KClass
import org.thoughtcrime.securesms.ui.GetString
import kotlin.time.Duration
import kotlin.time.Duration.Companion.days
import kotlin.time.Duration.Companion.hours
import kotlin.time.Duration.Companion.minutes
@OptIn(ExperimentalCoroutinesApi::class)
class ExpirationSettingsViewModel(
private val threadId: Long,
private val afterReadOptions: List<ExpirationRadioOption>,
private val afterSendOptions: List<ExpirationRadioOption>,
private val textSecurePreferences: TextSecurePreferences,
private val messageExpirationManager: MessageExpirationManagerProtocol,
private val threadDb: ThreadDatabase,
@ -44,47 +40,38 @@ class ExpirationSettingsViewModel(
private val storage: Storage
) : ViewModel() {
private val _uiState = MutableStateFlow(ExpirationSettingsUiState())
val uiState: StateFlow<ExpirationSettingsUiState> = _uiState
private val _state = MutableStateFlow(State())
val state = _state.asStateFlow()
val uiState = _state.map {
UiState(
cards = listOf(
CardModel(GetString(R.string.activity_expiration_settings_delete_type), typeOptions()),
CardModel(GetString(R.string.activity_expiration_settings_timer), timeOptions(it))
)
)
}
private var expirationConfig: ExpirationConfiguration? = null
private val _selectedExpirationType: MutableStateFlow<ExpiryMode> = MutableStateFlow(ExpiryMode.NONE)
val selectedExpirationType: StateFlow<ExpiryMode> = _selectedExpirationType
private val _selectedExpirationTimer = MutableStateFlow(afterSendOptions.firstOrNull())
val selectedExpirationTimer: StateFlow<RadioOption<ExpiryMode>?> = _selectedExpirationTimer
private val _expirationTimerOptions = MutableStateFlow<List<RadioOption<ExpiryMode>>>(emptyList())
val expirationTimerOptions: StateFlow<List<RadioOption<ExpiryMode>>> = _expirationTimerOptions
init {
// SETUP
viewModelScope.launch {
expirationConfig = storage.getExpirationConfiguration(threadId)
val expirationType = expirationConfig?.expiryMode
val expiryMode = expirationConfig?.expiryMode ?: ExpiryMode.NONE
val recipient = threadDb.getRecipientForThreadId(threadId)
val groupInfo = recipient?.takeIf { it.isClosedGroupRecipient }
?.run { address.toGroupString().let(groupDb::getGroup).orNull() }
_uiState.update { currentUiState ->
currentUiState.copy(
_state.update { state ->
state.copy(
isSelfAdmin = groupInfo == null || groupInfo.admins.any{ it.serialize() == textSecurePreferences.getLocalNumber() },
showExpirationTypeSelector = true,
recipient = recipient
recipient = recipient,
expiryType = expiryMode.type,
expiryMode = expiryMode
)
}
_selectedExpirationType.value = if (ExpirationConfiguration.isNewConfigEnabled) {
expirationType ?: ExpiryMode.NONE
} else {
if (expirationType != null && expirationType != ExpiryMode.NONE)
ExpiryMode.Legacy(expirationType.expirySeconds)
else ExpiryMode.NONE
}
_selectedExpirationTimer.value = when(expirationType) {
is ExpiryMode.AfterSend -> afterSendOptions.find { it.value == expirationType }
is ExpiryMode.AfterRead -> afterReadOptions.find { it.value == expirationType }
else -> afterSendOptions.firstOrNull()
}
}
// selectedExpirationType.mapLatest {
// when (it) {
@ -100,146 +87,99 @@ class ExpirationSettingsViewModel(
// }.launchIn(viewModelScope)
}
fun onExpirationTypeSelected(option: RadioOption<ExpiryMode>) {
_selectedExpirationType.value = option.value
_selectedExpirationTimer.value = _expirationTimerOptions.value.firstOrNull()
fun typeOption(
type: ExpiryType,
@StringRes title: Int,
@StringRes subtitle: Int,
// @StringRes contentDescription: Int
) = OptionModel(GetString(title), GetString(subtitle)) { setType(type) }
private fun typeOptions() = listOf(
typeOption(ExpiryType.NONE, R.string.expiration_off, R.string.AccessibilityId_disable_disappearing_messages),
typeOption(ExpiryType.LEGACY, R.string.expiration_type_disappear_legacy, R.string.expiration_type_disappear_legacy_description),
typeOption(ExpiryType.AFTER_READ, R.string.expiration_type_disappear_after_read, R.string.expiration_type_disappear_after_read_description),
typeOption(ExpiryType.AFTER_SEND, R.string.expiration_type_disappear_after_send, R.string.expiration_type_disappear_after_send_description),
)
private fun setType(type: ExpiryType) {
_state.update { it.copy(expiryType = type, expiryMode = type.mode(0)) }
}
fun onExpirationTimerSelected(option: RadioOption<ExpiryMode>) {
_selectedExpirationTimer.value = option
private fun setTime(seconds: Long) {
_state.update { it.copy(
expiryMode = it.expiryType.mode(seconds)
) }
}
private fun KClass<out ExpiryMode>?.withTime(expirationTimer: Long) = when(this) {
ExpiryMode.AfterRead::class -> ExpiryMode.AfterRead(expirationTimer)
ExpiryMode.AfterSend::class -> ExpiryMode.AfterSend(expirationTimer)
else -> ExpiryMode.NONE
private fun setMode(mode: ExpiryMode) {
_state.update { it.copy(
expiryMode = mode
) }
}
fun timeOption(seconds: Long, @StringRes id: Int) = OptionModel(GetString(id), selected = false, onClick = { setTime(seconds) })
fun timeOption(seconds: Long, title: String, subtitle: String) = OptionModel(GetString(title), GetString(subtitle), selected = false, onClick = { setTime(seconds) })
// private fun timeOptions(state: State) = timeOptions(state.types.isEmpty(), state.expiryType == ExpiryType.AFTER_SEND)
private fun timeOptions(state: State) = noteToSelfOptions()
val afterReadTimes = listOf(12.hours, 1.days, 7.days, 14.days)
val afterSendTimes = listOf(5.minutes, 1.hours) + afterReadTimes
private fun noteToSelfOptions() = listOfNotNull(
typeOption(ExpiryType.NONE, R.string.arrays__off, R.string.arrays__off),
noteToSelfOption(1.minutes, subtitle = "for testing purposes").takeIf { BuildConfig.DEBUG },
) + afterSendTimes.map(::noteToSelfOption)
private fun noteToSelfOption(
duration: Duration,
title: GetString = GetString { ExpirationUtil.getExpirationDisplayValue(it, duration.inWholeSeconds.toInt()) },
subtitle: String? = null
) = OptionModel(
title = title,
subtitle = subtitle?.let(::GetString),
selected = false,
onClick = { setMode(ExpiryMode.AfterSend(duration.inWholeSeconds)) }
)
fun onSetClick() = viewModelScope.launch {
val state = uiState.value
val expiryMode = _selectedExpirationTimer.value?.value ?: ExpiryMode.NONE
val typeValue = expiryMode.let {
if (it is ExpiryMode.Legacy) ExpiryMode.AfterRead(it.expirySeconds)
else it
}
val state = _state.value
// val expiryMode = _selectedExpirationTimer.value?.value ?: ExpiryMode.NONE
// val typeValue = expiryMode.let {
// if (it is ExpiryMode.Legacy) ExpiryMode.AfterRead(it.expirySeconds)
// else it
// }
val address = state.recipient?.address
if (address == null || expirationConfig?.expiryMode == typeValue) {
_uiState.update {
it.copy(settingsSaved = false)
}
return@launch
}
// if (address == null || expirationConfig?.expiryMode == typeValue) {
// _state.update {
// it.copy(settingsSaved = false)
// }
// return@launch
// }
val expiryChangeTimestampMs = SnodeAPI.nowWithOffset
storage.setExpirationConfiguration(ExpirationConfiguration(threadId, typeValue, expiryChangeTimestampMs))
val message = ExpirationTimerUpdate(typeValue.expirySeconds.toInt())
message.sender = textSecurePreferences.getLocalNumber()
message.recipient = address.serialize()
message.sentTimestamp = expiryChangeTimestampMs
messageExpirationManager.setExpirationTimer(message, typeValue)
MessageSender.send(message, address)
_uiState.update {
it.copy(settingsSaved = true)
}
// val expiryChangeTimestampMs = SnodeAPI.nowWithOffset
// storage.setExpirationConfiguration(ExpirationConfiguration(threadId, typeValue, expiryChangeTimestampMs))
//
// val message = ExpirationTimerUpdate(typeValue.expirySeconds.toInt())
// message.sender = textSecurePreferences.getLocalNumber()
// message.recipient = address.serialize()
// message.sentTimestamp = expiryChangeTimestampMs
// messageExpirationManager.setExpirationTimer(message, typeValue)
//
// MessageSender.send(message, address)
// state.update {
// it.copy(settingsSaved = true)
// }
}
fun getDeleteOptions(): List<ExpirationRadioOption> {
if (!uiState.value.showExpirationTypeSelector) return emptyList()
val recipient = uiState.value.recipient ?: return emptyList()
return if (ExpirationConfiguration.isNewConfigEnabled) when {
recipient.isLocalNumber -> noteToSelfOptions()
recipient.isContactRecipient -> contactRecipientOptions()
recipient.isClosedGroupRecipient -> closedGroupRecipientOptions()
else -> emptyList()
} else when {
recipient.isContactRecipient && !recipient.isLocalNumber -> oldConfigContactRecipientOptions()
else -> oldConfigDefaultOptions()
}
}
private fun oldConfigDefaultOptions() = listOf(
radioOption(ExpiryMode.NONE, R.string.expiration_off),
radioOption(ExpiryMode.Legacy(0), R.string.expiration_type_disappear_legacy) {
subtitle(R.string.expiration_type_disappear_legacy_description)
},
radioOption(ExpiryMode.AfterSend(0), R.string.expiration_type_disappear_after_send) {
subtitle(R.string.expiration_type_disappear_after_send_description)
enabled = false
contentDescription(R.string.AccessibilityId_disappear_after_send_option)
}
)
private fun oldConfigContactRecipientOptions() = listOf(
radioOption(ExpiryMode.NONE, R.string.expiration_off) {
contentDescription(R.string.AccessibilityId_disable_disappearing_messages)
},
radioOption(ExpiryMode.Legacy(0), R.string.expiration_type_disappear_legacy) {
subtitle(R.string.expiration_type_disappear_legacy_description)
},
radioOption(ExpiryMode.AfterRead(0), R.string.expiration_type_disappear_after_read) {
subtitle(R.string.expiration_type_disappear_after_read_description)
enabled = false
contentDescription(R.string.AccessibilityId_disappear_after_read_option)
},
radioOption(ExpiryMode.AfterSend(0), R.string.expiration_type_disappear_after_send) {
subtitle(R.string.expiration_type_disappear_after_send_description)
enabled = false
contentDescription(R.string.AccessibilityId_disappear_after_send_option)
}
)
private fun contactRecipientOptions() = listOf(
radioOption(ExpiryMode.NONE, R.string.expiration_off) {
contentDescription(R.string.AccessibilityId_disable_disappearing_messages)
},
radioOption(ExpiryMode.AfterRead(0), R.string.expiration_type_disappear_after_read) {
subtitle(R.string.expiration_type_disappear_after_read_description)
contentDescription(R.string.AccessibilityId_disappear_after_read_option)
},
radioOption(ExpiryMode.AfterSend(0), R.string.expiration_type_disappear_after_send) {
subtitle(R.string.expiration_type_disappear_after_send_description)
contentDescription(R.string.AccessibilityId_disappear_after_send_option)
}
)
private fun closedGroupRecipientOptions() = listOf(
radioOption(ExpiryMode.NONE, R.string.expiration_off) {
contentDescription(R.string.AccessibilityId_disable_disappearing_messages)
},
radioOption(ExpiryMode.AfterSend(0), R.string.expiration_type_disappear_after_send) {
subtitle(R.string.expiration_type_disappear_after_send_description)
contentDescription(R.string.AccessibilityId_disappear_after_send_option)
}
)
private fun noteToSelfOptions() = listOf(
radioOption(ExpiryMode.NONE, R.string.expiration_off) {
contentDescription(R.string.AccessibilityId_disable_disappearing_messages)
},
radioOption(ExpiryMode.AfterSend(0), R.string.expiration_type_disappear_after_send) {
subtitle(R.string.expiration_type_disappear_after_send_description)
contentDescription(R.string.AccessibilityId_disappear_after_send_option)
}
)
@dagger.assisted.AssistedFactory
interface AssistedFactory {
fun create(
threadId: Long,
@Assisted("afterRead") afterReadOptions: List<ExpirationRadioOption>,
@Assisted("afterSend") afterSendOptions: List<ExpirationRadioOption>
): Factory
fun create(threadId: Long): Factory
}
@Suppress("UNCHECKED_CAST")
class Factory @AssistedInject constructor(
@Assisted private val threadId: Long,
@Assisted("afterRead") private val afterReadOptions: List<ExpirationRadioOption>,
@Assisted("afterSend") private val afterSendOptions: List<ExpirationRadioOption>,
private val textSecurePreferences: TextSecurePreferences,
private val messageExpirationManager: MessageExpirationManagerProtocol,
private val threadDb: ThreadDatabase,
@ -247,24 +187,54 @@ class ExpirationSettingsViewModel(
private val storage: Storage
) : ViewModelProvider.Factory {
override fun <T : ViewModel> create(modelClass: Class<T>): T {
return ExpirationSettingsViewModel(
threadId,
afterReadOptions,
afterSendOptions,
textSecurePreferences,
messageExpirationManager,
threadDb,
groupDb,
storage
) as T
}
override fun <T : ViewModel> create(modelClass: Class<T>): T = ExpirationSettingsViewModel(
threadId,
textSecurePreferences,
messageExpirationManager,
threadDb,
groupDb,
storage
) as T
}
}
data class ExpirationSettingsUiState(
data class State(
val isSelfAdmin: Boolean = false,
val showExpirationTypeSelector: Boolean = false,
val settingsSaved: Boolean? = null,
val recipient: Recipient? = null
val recipient: Recipient? = null,
val expiryType: ExpiryType = ExpiryType.NONE,
val expiryMode: ExpiryMode? = null,
val types: List<ExpiryType> = emptyList()
) {
val isSelf = recipient?.isLocalNumber == true
}
data class UiState(
val cards: List<CardModel> = emptyList()
)
data class CardModel(
val title: GetString,
val options: List<OptionModel>
)
data class OptionModel(
val title: GetString,
val subtitle: GetString? = null,
val selected: Boolean = false,
val onClick: () -> Unit = {}
)
enum class ExpiryType(val mode: (Long) -> ExpiryMode) {
NONE({ ExpiryMode.NONE }),
LEGACY(ExpiryMode::Legacy),
AFTER_SEND(ExpiryMode::AfterSend),
AFTER_READ(ExpiryMode::AfterRead)
}
private val ExpiryMode.type: ExpiryType get() = when(this) {
is ExpiryMode.Legacy -> ExpiryType.LEGACY
is ExpiryMode.AfterSend -> ExpiryType.AFTER_SEND
is ExpiryMode.AfterRead -> ExpiryType.AFTER_READ
else -> ExpiryType.NONE
}

View File

@ -41,7 +41,7 @@ class AlbumThumbnailView : RelativeLayout {
private var slides: List<Slide> = listOf()
private var slideSize: Int = 0
override fun dispatchDraw(canvas: Canvas?) {
override fun dispatchDraw(canvas: Canvas) {
super.dispatchDraw(canvas)
cornerMask.mask(canvas)
}

View File

@ -30,9 +30,7 @@ class ThumbnailProgressBar: View {
private val objectRect = Rect()
private val drawingRect = Rect()
override fun dispatchDraw(canvas: Canvas?) {
if (canvas == null) return
override fun dispatchDraw(canvas: Canvas) {
getDrawingRect(objectRect)
drawingRect.set(objectRect)

View File

@ -3,12 +3,18 @@ package org.thoughtcrime.securesms.ui
import android.content.Context
import androidx.annotation.StringRes
import androidx.compose.runtime.Composable
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
/**
* Compatibility class to allow ViewModels to use strings and string resources interchangeably.
*/
sealed class GetString {
@Composable
operator fun invoke() = string()
operator fun invoke(context: Context) = string(context)
@Composable
abstract fun string(): String
@ -22,12 +28,17 @@ sealed class GetString {
@Composable
override fun string(): String = stringResource(resId)
override fun string(context: Context): String = context.getString(resId)
}
data class FromFun(val function: (Context) -> String): GetString() {
@Composable
override fun string(): String = function(LocalContext.current)
override fun string(context: Context): String = function(context)
}
}
fun GetString(@StringRes resId: Int) = GetString.FromResId(resId)
fun GetString(string: String) = GetString.FromString(string)
fun GetString(function: (Context) -> String) = GetString.FromFun(function)
/**

View File

@ -22,101 +22,11 @@
app:subtitleTextAppearance="@style/TextAppearance.Session.ToolbarSubtitle"
app:title="@string/activity_expiration_settings_title" />
<LinearLayout
android:id="@+id/layout_container"
<androidx.compose.ui.platform.ComposeView
android:id="@+id/container"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_below="@id/toolbar"
android:layout_marginBottom="@dimen/massive_spacing"
android:orientation="vertical">
<TextView
android:id="@+id/text_view_delete_type"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingHorizontal="@dimen/very_large_spacing"
android:paddingVertical="@dimen/small_spacing"
android:text="@string/activity_expiration_settings_delete_type"
android:textColor="?android:textColorTertiary"
android:textSize="@dimen/medium_font_size" />
<LinearLayout
android:id="@+id/layout_delete_types"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginHorizontal="@dimen/small_spacing"
android:background="@drawable/preference_single"
android:paddingHorizontal="@dimen/large_spacing"
android:paddingVertical="@dimen/medium_spacing">
<androidx.recyclerview.widget.RecyclerView
android:overScrollMode="never"
android:id="@+id/recycler_view_delete_types"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:clipToPadding="false"
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
tools:itemCount="3"
tools:listitem="@layout/item_selectable" />
</LinearLayout>
<TextView
android:id="@+id/text_view_timer"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingHorizontal="@dimen/very_large_spacing"
android:paddingVertical="@dimen/small_spacing"
android:text="@string/activity_expiration_settings_timer"
android:textColor="?android:textColorTertiary"
android:textSize="@dimen/medium_font_size" />
<LinearLayout
android:id="@+id/layout_timer"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:contentDescription="@string/AccessibilityId_disappearing_messages_timer"
android:layout_marginHorizontal="@dimen/small_spacing"
android:background="@drawable/preference_single"
android:paddingHorizontal="@dimen/large_spacing"
android:paddingVertical="@dimen/medium_spacing">
<androidx.recyclerview.widget.RecyclerView
android:overScrollMode="never"
android:id="@+id/recycler_view_timer_options"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:clipToPadding="false"
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
tools:itemCount="3"
tools:listitem="@layout/item_selectable" />
</LinearLayout>
<TextView
android:id="@+id/text_view_footer"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingHorizontal="@dimen/very_large_spacing"
android:paddingVertical="@dimen/small_spacing"
android:gravity="center_horizontal"
android:text="@string/activity_expiration_settings_group_footer"
android:textColor="?android:textColorTertiary"
android:textSize="@dimen/very_small_font_size"
android:visibility="gone"
tools:visibility="visible"/>
</LinearLayout>
<Button
android:id="@+id/button_set"
style="@style/Widget.Session.Button.Common.ProminentOutline"
android:layout_width="196dp"
android:layout_height="@dimen/medium_button_height"
android:contentDescription="@string/AccessibilityId_set_button"
android:layout_alignParentBottom="true"
android:layout_centerHorizontal="true"
android:layout_marginTop="@dimen/medium_spacing"
android:layout_marginBottom="@dimen/very_large_spacing"
android:text="@string/expiration_settings_set_button_title" />
android:layout_height="match_parent"
android:layout_below="@id/toolbar"/>
</RelativeLayout>

View File

@ -228,40 +228,4 @@
<item>@string/notify_type_mentions</item>
</string-array>
<integer-array name="read_expiration_time_names">
<item>@string/arrays__off</item>
<item>@string/arrays__five_minutes</item>
<item>@string/arrays__one_hour</item>
<item>@string/arrays__twelve_hours</item>
<item>@string/arrays__one_day</item>
<item>@string/arrays__one_week</item>
<item>@string/arrays__two_weeks</item>
</integer-array>
<integer-array name="read_expiration_time_values">
<item>0</item>
<item>300</item>
<item>3600</item>
<item>43200</item>
<item>86400</item>
<item>604800</item>
<item>1209600</item>
</integer-array>
<integer-array name="send_expiration_time_names">
<item>@string/arrays__off</item>
<item>@string/arrays__twelve_hours</item>
<item>@string/arrays__one_day</item>
<item>@string/arrays__one_week</item>
<item>@string/arrays__two_weeks</item>
</integer-array>
<integer-array name="send_expiration_time_values">
<item>0</item>
<item>43200</item>
<item>86400</item>
<item>604800</item>
<item>1209600</item>
</integer-array>
</resources>

View File

@ -57,6 +57,6 @@ allprojects {
project.ext {
androidMinimumSdkVersion = 23
androidTargetSdkVersion = 33
androidCompileSdkVersion = 33
androidCompileSdkVersion = 34
}
}

View File

@ -161,7 +161,7 @@ private fun MessageReceiver.handleExpirationTimerUpdate(message: ExpirationTimer
val type = when {
recipient.isContactRecipient -> ExpiryMode.AfterRead(message.duration!!.toLong())
recipient.isGroupRecipient -> ExpiryMode.AfterSend(message.duration!!.toLong())
else -> null
else -> ExpiryMode.NONE
}
try {
var threadId: Long = module.storage.getOrCreateThreadIdFor(fromSerialized(message.sender!!))
@ -295,7 +295,7 @@ fun MessageReceiver.updateExpiryIfNeeded(
val durationSeconds = if (proto.hasExpirationTimer()) proto.expirationTimer else 0
val type = if (proto.hasExpirationType()) proto.expirationType else null
val expiryMode = type?.expiryMode(durationSeconds.toLong())
val expiryMode = type?.expiryMode(durationSeconds.toLong()) ?: ExpiryMode.NONE
val remoteConfig = ExpirationConfiguration(
threadID,
@ -325,12 +325,12 @@ fun MessageReceiver.updateExpiryIfNeeded(
// handle a delete after send expired fetch
if (type == ExpirationType.DELETE_AFTER_SEND
&& sentTime + (configToUse.expiryMode?.expirySeconds ?: 0) <= SnodeAPI.nowWithOffset) {
&& sentTime + configToUse.expiryMode.expirySeconds <= SnodeAPI.nowWithOffset) {
throw MessageReceiver.Error.ExpiredMessage
}
// handle a delete after read last known config value
if (type == ExpirationType.DELETE_AFTER_READ
&& sentTime + (configToUse.expiryMode?.expirySeconds ?: 0) <= storage.getLastSeen(threadID)) {
&& sentTime + configToUse.expiryMode.expirySeconds <= storage.getLastSeen(threadID)) {
throw MessageReceiver.Error.ExpiredMessage
}