diff --git a/app/build.gradle b/app/build.gradle index 74b9f84f0..95c8e4df8 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -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 diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/expiration/ExpirationSettingsActivity.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/expiration/ExpirationSettingsActivity.kt index 6dbeb92c5..33ed55b01 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/expiration/ExpirationSettingsActivity.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/expiration/ExpirationSettingsActivity.kt @@ -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): List = - 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() - binding.scrollView.saveHierarchyState(scrollParcelArray) - outState.putSparseParcelableArray(SCROLL_PARCEL, scrollParcelArray) - } +// override fun onSaveInstanceState(outState: Bundle) { +// super.onSaveInstanceState(outState) +// val scrollParcelArray = SparseArray() +// 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(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(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" } -} \ No newline at end of file + @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")), +) \ No newline at end of file diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/expiration/ExpirationSettingsViewModel.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/expiration/ExpirationSettingsViewModel.kt index 21e28e157..f4f1aeb41 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/expiration/ExpirationSettingsViewModel.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/expiration/ExpirationSettingsViewModel.kt @@ -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, - private val afterSendOptions: List, 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 = _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 = MutableStateFlow(ExpiryMode.NONE) - val selectedExpirationType: StateFlow = _selectedExpirationType - - private val _selectedExpirationTimer = MutableStateFlow(afterSendOptions.firstOrNull()) - val selectedExpirationTimer: StateFlow?> = _selectedExpirationTimer - - private val _expirationTimerOptions = MutableStateFlow>>(emptyList()) - val expirationTimerOptions: StateFlow>> = _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) { - _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) { - _selectedExpirationTimer.value = option + private fun setTime(seconds: Long) { + _state.update { it.copy( + expiryMode = it.expiryType.mode(seconds) + ) } } - private fun KClass?.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 { - 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, - @Assisted("afterSend") afterSendOptions: List - ): Factory + fun create(threadId: Long): Factory } @Suppress("UNCHECKED_CAST") class Factory @AssistedInject constructor( @Assisted private val threadId: Long, - @Assisted("afterRead") private val afterReadOptions: List, - @Assisted("afterSend") private val afterSendOptions: List, 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 create(modelClass: Class): T { - return ExpirationSettingsViewModel( - threadId, - afterReadOptions, - afterSendOptions, - textSecurePreferences, - messageExpirationManager, - threadDb, - groupDb, - storage - ) as T - } + override fun create(modelClass: Class): 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 = emptyList() +) { + val isSelf = recipient?.isLocalNumber == true +} + +data class UiState( + val cards: List = emptyList() ) + +data class CardModel( + val title: GetString, + val options: List +) + +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 + } diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/components/AlbumThumbnailView.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/components/AlbumThumbnailView.kt index 330534e23..1a7420005 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/components/AlbumThumbnailView.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/components/AlbumThumbnailView.kt @@ -41,7 +41,7 @@ class AlbumThumbnailView : RelativeLayout { private var slides: List = listOf() private var slideSize: Int = 0 - override fun dispatchDraw(canvas: Canvas?) { + override fun dispatchDraw(canvas: Canvas) { super.dispatchDraw(canvas) cornerMask.mask(canvas) } diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/utilities/ThumbnailProgressBar.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/utilities/ThumbnailProgressBar.kt index 3abfd235c..d5ef6434e 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/utilities/ThumbnailProgressBar.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/utilities/ThumbnailProgressBar.kt @@ -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) diff --git a/app/src/main/java/org/thoughtcrime/securesms/ui/Data.kt b/app/src/main/java/org/thoughtcrime/securesms/ui/Data.kt index 601f2a9dc..fa6c7761b 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/ui/Data.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/ui/Data.kt @@ -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) /** diff --git a/app/src/main/res/layout/activity_expiration_settings.xml b/app/src/main/res/layout/activity_expiration_settings.xml index 8a9668b87..30c93b6db 100644 --- a/app/src/main/res/layout/activity_expiration_settings.xml +++ b/app/src/main/res/layout/activity_expiration_settings.xml @@ -22,101 +22,11 @@ app:subtitleTextAppearance="@style/TextAppearance.Session.ToolbarSubtitle" app:title="@string/activity_expiration_settings_title" /> - - - - - - - - - - - - - - - - - - - - -