mirror of
https://github.com/oxen-io/session-android.git
synced 2025-02-21 11:38:27 +00:00
Use compose
This commit is contained in:
parent
5142c45643
commit
71b2544c31
@ -166,14 +166,14 @@ dependencies {
|
|||||||
testImplementation 'org.robolectric:shadows-multidex:4.4'
|
testImplementation 'org.robolectric:shadows-multidex:4.4'
|
||||||
|
|
||||||
implementation 'com.github.bumptech.glide:compose:1.0.0-alpha.1'
|
implementation 'com.github.bumptech.glide:compose:1.0.0-alpha.1'
|
||||||
implementation 'androidx.compose.ui:ui:1.4.3'
|
implementation 'androidx.compose.ui:ui:1.5.0'
|
||||||
implementation 'androidx.compose.ui:ui-tooling:1.4.3'
|
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-themeadapter-appcompat:0.31.5-beta"
|
||||||
implementation "com.google.accompanist:accompanist-pager-indicators: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.foundation:foundation-layout:1.5.0'
|
||||||
implementation 'androidx.compose.material:material:1.5.0-alpha02'
|
implementation 'androidx.compose.material:material:1.5.0'
|
||||||
}
|
}
|
||||||
|
|
||||||
def canonicalVersionCode = 354
|
def canonicalVersionCode = 354
|
||||||
|
@ -1,33 +1,58 @@
|
|||||||
package org.thoughtcrime.securesms.conversation.expiration
|
package org.thoughtcrime.securesms.conversation.expiration
|
||||||
|
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.os.Parcelable
|
|
||||||
import android.util.SparseArray
|
|
||||||
import android.widget.Toast
|
import android.widget.Toast
|
||||||
import androidx.activity.viewModels
|
import androidx.activity.viewModels
|
||||||
import androidx.core.text.HtmlCompat
|
import androidx.compose.foundation.BorderStroke
|
||||||
import androidx.core.view.isVisible
|
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.Lifecycle
|
||||||
import androidx.lifecycle.lifecycleScope
|
import androidx.lifecycle.lifecycleScope
|
||||||
import androidx.lifecycle.repeatOnLifecycle
|
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 dagger.hilt.android.AndroidEntryPoint
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import network.loki.messenger.BuildConfig
|
|
||||||
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.preferences.ExpirationRadioOption
|
import org.thoughtcrime.securesms.ui.AppTheme
|
||||||
import org.thoughtcrime.securesms.preferences.RadioOptionAdapter
|
import org.thoughtcrime.securesms.ui.CellNoMargin
|
||||||
import org.thoughtcrime.securesms.preferences.radioOption
|
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 org.thoughtcrime.securesms.util.ConfigurationMessageUtilities
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
import kotlin.math.max
|
|
||||||
|
|
||||||
@AndroidEntryPoint
|
@AndroidEntryPoint
|
||||||
class ExpirationSettingsActivity: PassphraseRequiredActionBarActivity() {
|
class ExpirationSettingsActivity: PassphraseRequiredActionBarActivity() {
|
||||||
@ -43,34 +68,16 @@ class ExpirationSettingsActivity: PassphraseRequiredActionBarActivity() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private val viewModel: ExpirationSettingsViewModel by viewModels {
|
private val viewModel: ExpirationSettingsViewModel by viewModels {
|
||||||
val afterReadOptions = resources.getIntArray(R.array.read_expiration_time_values)
|
viewModelFactory.create(threadId)
|
||||||
.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))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
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) {
|
// override fun onSaveInstanceState(outState: Bundle) {
|
||||||
super.onSaveInstanceState(outState)
|
// super.onSaveInstanceState(outState)
|
||||||
val scrollParcelArray = SparseArray<Parcelable>()
|
// val scrollParcelArray = SparseArray<Parcelable>()
|
||||||
binding.scrollView.saveHierarchyState(scrollParcelArray)
|
// binding.scrollView.saveHierarchyState(scrollParcelArray)
|
||||||
outState.putSparseParcelableArray(SCROLL_PARCEL, scrollParcelArray)
|
// outState.putSparseParcelableArray(SCROLL_PARCEL, scrollParcelArray)
|
||||||
}
|
// }
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?, ready: Boolean) {
|
override fun onCreate(savedInstanceState: Bundle?, ready: Boolean) {
|
||||||
super.onCreate(savedInstanceState, ready)
|
super.onCreate(savedInstanceState, ready)
|
||||||
@ -79,45 +86,35 @@ class ExpirationSettingsActivity: PassphraseRequiredActionBarActivity() {
|
|||||||
|
|
||||||
setUpToolbar()
|
setUpToolbar()
|
||||||
|
|
||||||
savedInstanceState?.let { bundle ->
|
binding.container.setContent { DisappearingMessagesScreen() }
|
||||||
val scrollStateParcel = bundle.getSparseParcelableArray<Parcelable>(SCROLL_PARCEL)
|
|
||||||
if (scrollStateParcel != null) {
|
|
||||||
binding.scrollView.restoreHierarchyState(scrollStateParcel)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
val deleteTypeOptions = viewModel.getDeleteOptions()
|
// savedInstanceState?.let { bundle ->
|
||||||
val deleteTypeOptionAdapter = RadioOptionAdapter {
|
// val scrollStateParcel = bundle.getSparseParcelableArray<Parcelable>(SCROLL_PARCEL)
|
||||||
viewModel.onExpirationTypeSelected(it)
|
// if (scrollStateParcel != null) {
|
||||||
}
|
// binding.scrollView.restoreHierarchyState(scrollStateParcel)
|
||||||
binding.recyclerViewDeleteTypes.apply {
|
// }
|
||||||
(itemAnimator as SimpleItemAnimator).supportsChangeAnimations = false
|
// }
|
||||||
adapter = deleteTypeOptionAdapter
|
|
||||||
addDividers()
|
|
||||||
setHasFixedSize(true)
|
|
||||||
}
|
|
||||||
deleteTypeOptionAdapter.submitList(deleteTypeOptions)
|
|
||||||
|
|
||||||
val timerOptionAdapter = RadioOptionAdapter {
|
// val deleteTypeOptions = viewModel.getDeleteOptions()
|
||||||
viewModel.onExpirationTimerSelected(it)
|
|
||||||
}
|
// binding.buttonSet.setOnClickListener {
|
||||||
binding.recyclerViewTimerOptions.apply {
|
// viewModel.onSetClick()
|
||||||
(itemAnimator as SimpleItemAnimator).supportsChangeAnimations = false
|
// }
|
||||||
adapter = timerOptionAdapter
|
|
||||||
addDividers()
|
|
||||||
}
|
|
||||||
binding.buttonSet.setOnClickListener {
|
|
||||||
viewModel.onSetClick()
|
|
||||||
}
|
|
||||||
lifecycleScope.launch {
|
lifecycleScope.launch {
|
||||||
repeatOnLifecycle(Lifecycle.State.STARTED) {
|
repeatOnLifecycle(Lifecycle.State.STARTED) {
|
||||||
viewModel.uiState.collect { uiState ->
|
viewModel.state.collect { state ->
|
||||||
binding.textViewDeleteType.isVisible = uiState.showExpirationTypeSelector
|
// actionBar?.subtitle = if (state.selectedExpirationType.value is ExpiryMode.AfterSend) {
|
||||||
binding.layoutDeleteTypes.isVisible = uiState.showExpirationTypeSelector
|
// getString(R.string.activity_expiration_settings_subtitle_sent)
|
||||||
binding.textViewFooter.isVisible = uiState.recipient?.isClosedGroupRecipient == true
|
// } else {
|
||||||
binding.textViewFooter.text = HtmlCompat.fromHtml(getString(R.string.activity_expiration_settings_group_footer), HtmlCompat.FROM_HTML_MODE_COMPACT)
|
// 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 -> {
|
true -> {
|
||||||
ConfigurationMessageUtilities.forceSyncConfigurationNowIfNeeded(this@ExpirationSettingsActivity)
|
ConfigurationMessageUtilities.forceSyncConfigurationNowIfNeeded(this@ExpirationSettingsActivity)
|
||||||
finish()
|
finish()
|
||||||
@ -125,47 +122,39 @@ class ExpirationSettingsActivity: PassphraseRequiredActionBarActivity() {
|
|||||||
false -> showToast(getString(R.string.ExpirationSettingsActivity_settings_not_updated))
|
false -> showToast(getString(R.string.ExpirationSettingsActivity_settings_not_updated))
|
||||||
else -> {}
|
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() {
|
// val position = deleteTypeOptions.indexOfFirst { it.value == state.selectedExpirationType }
|
||||||
addItemDecoration(
|
// deleteTypeOptionAdapter.setSelectedPosition(max(0, position))
|
||||||
MaterialDividerItemDecoration(
|
}
|
||||||
this@ExpirationSettingsActivity,
|
|
||||||
RecyclerView.VERTICAL
|
|
||||||
).apply {
|
|
||||||
isLastItemDecorated = false
|
|
||||||
}
|
}
|
||||||
)
|
}
|
||||||
|
// 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) {
|
private fun showToast(message: String) {
|
||||||
@ -176,11 +165,6 @@ class ExpirationSettingsActivity: PassphraseRequiredActionBarActivity() {
|
|||||||
setSupportActionBar(binding.toolbar)
|
setSupportActionBar(binding.toolbar)
|
||||||
val actionBar = supportActionBar ?: return
|
val actionBar = supportActionBar ?: return
|
||||||
actionBar.title = getString(R.string.activity_expiration_settings_title)
|
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.setDisplayHomeAsUpEnabled(true)
|
||||||
actionBar.setHomeButtonEnabled(true)
|
actionBar.setHomeButtonEnabled(true)
|
||||||
}
|
}
|
||||||
@ -190,4 +174,139 @@ class ExpirationSettingsActivity: PassphraseRequiredActionBarActivity() {
|
|||||||
const val THREAD_ID = "thread_id"
|
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")),
|
||||||
|
)
|
@ -1,5 +1,6 @@
|
|||||||
package org.thoughtcrime.securesms.conversation.expiration
|
package org.thoughtcrime.securesms.conversation.expiration
|
||||||
|
|
||||||
|
import androidx.annotation.StringRes
|
||||||
import androidx.lifecycle.ViewModel
|
import androidx.lifecycle.ViewModel
|
||||||
import androidx.lifecycle.ViewModelProvider
|
import androidx.lifecycle.ViewModelProvider
|
||||||
import androidx.lifecycle.viewModelScope
|
import androidx.lifecycle.viewModelScope
|
||||||
@ -7,36 +8,31 @@ import dagger.assisted.Assisted
|
|||||||
import dagger.assisted.AssistedInject
|
import dagger.assisted.AssistedInject
|
||||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
import kotlinx.coroutines.flow.StateFlow
|
import kotlinx.coroutines.flow.asStateFlow
|
||||||
import kotlinx.coroutines.flow.launchIn
|
import kotlinx.coroutines.flow.map
|
||||||
import kotlinx.coroutines.flow.mapLatest
|
|
||||||
import kotlinx.coroutines.flow.onEach
|
|
||||||
import kotlinx.coroutines.flow.update
|
import kotlinx.coroutines.flow.update
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
import network.loki.messenger.BuildConfig
|
||||||
import network.loki.messenger.R
|
import network.loki.messenger.R
|
||||||
import network.loki.messenger.libsession_util.util.ExpiryMode
|
import network.loki.messenger.libsession_util.util.ExpiryMode
|
||||||
import org.session.libsession.messaging.messages.ExpirationConfiguration
|
import org.session.libsession.messaging.messages.ExpirationConfiguration
|
||||||
import org.session.libsession.messaging.messages.control.ExpirationTimerUpdate
|
import org.session.libsession.utilities.ExpirationUtil
|
||||||
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.SSKEnvironment.MessageExpirationManagerProtocol
|
import org.session.libsession.utilities.SSKEnvironment.MessageExpirationManagerProtocol
|
||||||
import org.session.libsession.utilities.TextSecurePreferences
|
import org.session.libsession.utilities.TextSecurePreferences
|
||||||
import org.session.libsession.utilities.recipients.Recipient
|
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.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.preferences.ExpirationRadioOption
|
import org.thoughtcrime.securesms.ui.GetString
|
||||||
import org.thoughtcrime.securesms.preferences.RadioOption
|
import kotlin.time.Duration
|
||||||
import org.thoughtcrime.securesms.preferences.radioOption
|
import kotlin.time.Duration.Companion.days
|
||||||
import kotlin.reflect.KClass
|
import kotlin.time.Duration.Companion.hours
|
||||||
|
import kotlin.time.Duration.Companion.minutes
|
||||||
|
|
||||||
|
|
||||||
@OptIn(ExperimentalCoroutinesApi::class)
|
@OptIn(ExperimentalCoroutinesApi::class)
|
||||||
class ExpirationSettingsViewModel(
|
class ExpirationSettingsViewModel(
|
||||||
private val threadId: Long,
|
private val threadId: Long,
|
||||||
private val afterReadOptions: List<ExpirationRadioOption>,
|
|
||||||
private val afterSendOptions: List<ExpirationRadioOption>,
|
|
||||||
private val textSecurePreferences: TextSecurePreferences,
|
private val textSecurePreferences: TextSecurePreferences,
|
||||||
private val messageExpirationManager: MessageExpirationManagerProtocol,
|
private val messageExpirationManager: MessageExpirationManagerProtocol,
|
||||||
private val threadDb: ThreadDatabase,
|
private val threadDb: ThreadDatabase,
|
||||||
@ -44,47 +40,38 @@ class ExpirationSettingsViewModel(
|
|||||||
private val storage: Storage
|
private val storage: Storage
|
||||||
) : ViewModel() {
|
) : ViewModel() {
|
||||||
|
|
||||||
private val _uiState = MutableStateFlow(ExpirationSettingsUiState())
|
private val _state = MutableStateFlow(State())
|
||||||
val uiState: StateFlow<ExpirationSettingsUiState> = _uiState
|
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 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 {
|
init {
|
||||||
// SETUP
|
// SETUP
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
expirationConfig = storage.getExpirationConfiguration(threadId)
|
expirationConfig = storage.getExpirationConfiguration(threadId)
|
||||||
val expirationType = expirationConfig?.expiryMode
|
val expiryMode = expirationConfig?.expiryMode ?: ExpiryMode.NONE
|
||||||
val recipient = threadDb.getRecipientForThreadId(threadId)
|
val recipient = threadDb.getRecipientForThreadId(threadId)
|
||||||
val groupInfo = recipient?.takeIf { it.isClosedGroupRecipient }
|
val groupInfo = recipient?.takeIf { it.isClosedGroupRecipient }
|
||||||
?.run { address.toGroupString().let(groupDb::getGroup).orNull() }
|
?.run { address.toGroupString().let(groupDb::getGroup).orNull() }
|
||||||
_uiState.update { currentUiState ->
|
_state.update { state ->
|
||||||
currentUiState.copy(
|
state.copy(
|
||||||
isSelfAdmin = groupInfo == null || groupInfo.admins.any{ it.serialize() == textSecurePreferences.getLocalNumber() },
|
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 {
|
// selectedExpirationType.mapLatest {
|
||||||
// when (it) {
|
// when (it) {
|
||||||
@ -100,146 +87,99 @@ class ExpirationSettingsViewModel(
|
|||||||
// }.launchIn(viewModelScope)
|
// }.launchIn(viewModelScope)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun onExpirationTypeSelected(option: RadioOption<ExpiryMode>) {
|
fun typeOption(
|
||||||
_selectedExpirationType.value = option.value
|
type: ExpiryType,
|
||||||
_selectedExpirationTimer.value = _expirationTimerOptions.value.firstOrNull()
|
@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>) {
|
private fun setTime(seconds: Long) {
|
||||||
_selectedExpirationTimer.value = option
|
_state.update { it.copy(
|
||||||
|
expiryMode = it.expiryType.mode(seconds)
|
||||||
|
) }
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun KClass<out ExpiryMode>?.withTime(expirationTimer: Long) = when(this) {
|
private fun setMode(mode: ExpiryMode) {
|
||||||
ExpiryMode.AfterRead::class -> ExpiryMode.AfterRead(expirationTimer)
|
_state.update { it.copy(
|
||||||
ExpiryMode.AfterSend::class -> ExpiryMode.AfterSend(expirationTimer)
|
expiryMode = mode
|
||||||
else -> ExpiryMode.NONE
|
) }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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 {
|
fun onSetClick() = viewModelScope.launch {
|
||||||
val state = uiState.value
|
val state = _state.value
|
||||||
val expiryMode = _selectedExpirationTimer.value?.value ?: ExpiryMode.NONE
|
// val expiryMode = _selectedExpirationTimer.value?.value ?: ExpiryMode.NONE
|
||||||
val typeValue = expiryMode.let {
|
// val typeValue = expiryMode.let {
|
||||||
if (it is ExpiryMode.Legacy) ExpiryMode.AfterRead(it.expirySeconds)
|
// if (it is ExpiryMode.Legacy) ExpiryMode.AfterRead(it.expirySeconds)
|
||||||
else it
|
// else it
|
||||||
}
|
// }
|
||||||
val address = state.recipient?.address
|
val address = state.recipient?.address
|
||||||
if (address == null || expirationConfig?.expiryMode == typeValue) {
|
// if (address == null || expirationConfig?.expiryMode == typeValue) {
|
||||||
_uiState.update {
|
// _state.update {
|
||||||
it.copy(settingsSaved = false)
|
// it.copy(settingsSaved = false)
|
||||||
}
|
// }
|
||||||
return@launch
|
// return@launch
|
||||||
}
|
// }
|
||||||
|
|
||||||
val expiryChangeTimestampMs = SnodeAPI.nowWithOffset
|
// val expiryChangeTimestampMs = SnodeAPI.nowWithOffset
|
||||||
storage.setExpirationConfiguration(ExpirationConfiguration(threadId, typeValue, expiryChangeTimestampMs))
|
// storage.setExpirationConfiguration(ExpirationConfiguration(threadId, typeValue, expiryChangeTimestampMs))
|
||||||
|
//
|
||||||
val message = ExpirationTimerUpdate(typeValue.expirySeconds.toInt())
|
// val message = ExpirationTimerUpdate(typeValue.expirySeconds.toInt())
|
||||||
message.sender = textSecurePreferences.getLocalNumber()
|
// message.sender = textSecurePreferences.getLocalNumber()
|
||||||
message.recipient = address.serialize()
|
// message.recipient = address.serialize()
|
||||||
message.sentTimestamp = expiryChangeTimestampMs
|
// message.sentTimestamp = expiryChangeTimestampMs
|
||||||
messageExpirationManager.setExpirationTimer(message, typeValue)
|
// messageExpirationManager.setExpirationTimer(message, typeValue)
|
||||||
|
//
|
||||||
MessageSender.send(message, address)
|
// MessageSender.send(message, address)
|
||||||
_uiState.update {
|
// state.update {
|
||||||
it.copy(settingsSaved = true)
|
// 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
|
@dagger.assisted.AssistedFactory
|
||||||
interface AssistedFactory {
|
interface AssistedFactory {
|
||||||
fun create(
|
fun create(threadId: Long): Factory
|
||||||
threadId: Long,
|
|
||||||
@Assisted("afterRead") afterReadOptions: List<ExpirationRadioOption>,
|
|
||||||
@Assisted("afterSend") afterSendOptions: List<ExpirationRadioOption>
|
|
||||||
): Factory
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Suppress("UNCHECKED_CAST")
|
@Suppress("UNCHECKED_CAST")
|
||||||
class Factory @AssistedInject constructor(
|
class Factory @AssistedInject constructor(
|
||||||
@Assisted private val threadId: Long,
|
@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 textSecurePreferences: TextSecurePreferences,
|
||||||
private val messageExpirationManager: MessageExpirationManagerProtocol,
|
private val messageExpirationManager: MessageExpirationManagerProtocol,
|
||||||
private val threadDb: ThreadDatabase,
|
private val threadDb: ThreadDatabase,
|
||||||
@ -247,24 +187,54 @@ class ExpirationSettingsViewModel(
|
|||||||
private val storage: Storage
|
private val storage: Storage
|
||||||
) : ViewModelProvider.Factory {
|
) : ViewModelProvider.Factory {
|
||||||
|
|
||||||
override fun <T : ViewModel> create(modelClass: Class<T>): T {
|
override fun <T : ViewModel> create(modelClass: Class<T>): T = ExpirationSettingsViewModel(
|
||||||
return ExpirationSettingsViewModel(
|
threadId,
|
||||||
threadId,
|
textSecurePreferences,
|
||||||
afterReadOptions,
|
messageExpirationManager,
|
||||||
afterSendOptions,
|
threadDb,
|
||||||
textSecurePreferences,
|
groupDb,
|
||||||
messageExpirationManager,
|
storage
|
||||||
threadDb,
|
) as T
|
||||||
groupDb,
|
|
||||||
storage
|
|
||||||
) as T
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
data class ExpirationSettingsUiState(
|
data class State(
|
||||||
val isSelfAdmin: Boolean = false,
|
val isSelfAdmin: Boolean = false,
|
||||||
val showExpirationTypeSelector: Boolean = false,
|
|
||||||
val settingsSaved: Boolean? = null,
|
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
|
||||||
|
}
|
||||||
|
@ -41,7 +41,7 @@ class AlbumThumbnailView : RelativeLayout {
|
|||||||
private var slides: List<Slide> = listOf()
|
private var slides: List<Slide> = listOf()
|
||||||
private var slideSize: Int = 0
|
private var slideSize: Int = 0
|
||||||
|
|
||||||
override fun dispatchDraw(canvas: Canvas?) {
|
override fun dispatchDraw(canvas: Canvas) {
|
||||||
super.dispatchDraw(canvas)
|
super.dispatchDraw(canvas)
|
||||||
cornerMask.mask(canvas)
|
cornerMask.mask(canvas)
|
||||||
}
|
}
|
||||||
|
@ -30,9 +30,7 @@ class ThumbnailProgressBar: View {
|
|||||||
private val objectRect = Rect()
|
private val objectRect = Rect()
|
||||||
private val drawingRect = Rect()
|
private val drawingRect = Rect()
|
||||||
|
|
||||||
override fun dispatchDraw(canvas: Canvas?) {
|
override fun dispatchDraw(canvas: Canvas) {
|
||||||
if (canvas == null) return
|
|
||||||
|
|
||||||
getDrawingRect(objectRect)
|
getDrawingRect(objectRect)
|
||||||
drawingRect.set(objectRect)
|
drawingRect.set(objectRect)
|
||||||
|
|
||||||
|
@ -3,12 +3,18 @@ package org.thoughtcrime.securesms.ui
|
|||||||
import android.content.Context
|
import android.content.Context
|
||||||
import androidx.annotation.StringRes
|
import androidx.annotation.StringRes
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.platform.LocalContext
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Compatibility class to allow ViewModels to use strings and string resources interchangeably.
|
* Compatibility class to allow ViewModels to use strings and string resources interchangeably.
|
||||||
*/
|
*/
|
||||||
sealed class GetString {
|
sealed class GetString {
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
operator fun invoke() = string()
|
||||||
|
operator fun invoke(context: Context) = string(context)
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
abstract fun string(): String
|
abstract fun string(): String
|
||||||
|
|
||||||
@ -22,12 +28,17 @@ sealed class GetString {
|
|||||||
@Composable
|
@Composable
|
||||||
override fun string(): String = stringResource(resId)
|
override fun string(): String = stringResource(resId)
|
||||||
override fun string(context: Context): String = context.getString(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(@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)
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -22,101 +22,11 @@
|
|||||||
app:subtitleTextAppearance="@style/TextAppearance.Session.ToolbarSubtitle"
|
app:subtitleTextAppearance="@style/TextAppearance.Session.ToolbarSubtitle"
|
||||||
app:title="@string/activity_expiration_settings_title" />
|
app:title="@string/activity_expiration_settings_title" />
|
||||||
|
|
||||||
<LinearLayout
|
<androidx.compose.ui.platform.ComposeView
|
||||||
android:id="@+id/layout_container"
|
android:id="@+id/container"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="match_parent"
|
||||||
android:layout_below="@id/toolbar"
|
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" />
|
|
||||||
|
|
||||||
</RelativeLayout>
|
</RelativeLayout>
|
||||||
|
|
||||||
|
@ -228,40 +228,4 @@
|
|||||||
<item>@string/notify_type_mentions</item>
|
<item>@string/notify_type_mentions</item>
|
||||||
</string-array>
|
</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>
|
</resources>
|
||||||
|
@ -57,6 +57,6 @@ allprojects {
|
|||||||
project.ext {
|
project.ext {
|
||||||
androidMinimumSdkVersion = 23
|
androidMinimumSdkVersion = 23
|
||||||
androidTargetSdkVersion = 33
|
androidTargetSdkVersion = 33
|
||||||
androidCompileSdkVersion = 33
|
androidCompileSdkVersion = 34
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -161,7 +161,7 @@ private fun MessageReceiver.handleExpirationTimerUpdate(message: ExpirationTimer
|
|||||||
val type = when {
|
val type = when {
|
||||||
recipient.isContactRecipient -> ExpiryMode.AfterRead(message.duration!!.toLong())
|
recipient.isContactRecipient -> ExpiryMode.AfterRead(message.duration!!.toLong())
|
||||||
recipient.isGroupRecipient -> ExpiryMode.AfterSend(message.duration!!.toLong())
|
recipient.isGroupRecipient -> ExpiryMode.AfterSend(message.duration!!.toLong())
|
||||||
else -> null
|
else -> ExpiryMode.NONE
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
var threadId: Long = module.storage.getOrCreateThreadIdFor(fromSerialized(message.sender!!))
|
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 durationSeconds = if (proto.hasExpirationTimer()) proto.expirationTimer else 0
|
||||||
val type = if (proto.hasExpirationType()) proto.expirationType else null
|
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(
|
val remoteConfig = ExpirationConfiguration(
|
||||||
threadID,
|
threadID,
|
||||||
@ -325,12 +325,12 @@ fun MessageReceiver.updateExpiryIfNeeded(
|
|||||||
|
|
||||||
// handle a delete after send expired fetch
|
// handle a delete after send expired fetch
|
||||||
if (type == ExpirationType.DELETE_AFTER_SEND
|
if (type == ExpirationType.DELETE_AFTER_SEND
|
||||||
&& sentTime + (configToUse.expiryMode?.expirySeconds ?: 0) <= SnodeAPI.nowWithOffset) {
|
&& sentTime + configToUse.expiryMode.expirySeconds <= SnodeAPI.nowWithOffset) {
|
||||||
throw MessageReceiver.Error.ExpiredMessage
|
throw MessageReceiver.Error.ExpiredMessage
|
||||||
}
|
}
|
||||||
// handle a delete after read last known config value
|
// handle a delete after read last known config value
|
||||||
if (type == ExpirationType.DELETE_AFTER_READ
|
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
|
throw MessageReceiver.Error.ExpiredMessage
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user