From 68ca04826725d3c2b3ddb0a4b3376fa15e26bb77 Mon Sep 17 00:00:00 2001 From: charles Date: Thu, 27 Oct 2022 09:07:50 +1100 Subject: [PATCH] Add timer selector --- .../expiration/ExpirationSettingsActivity.kt | 93 +++++++++++++++---- .../expiration/ExpirationSettingsViewModel.kt | 84 +++++++++++++++++ .../conversation/v2/ConversationActivityV2.kt | 3 +- .../preferences/RadioOptionAdapter.kt | 13 ++- .../layout/activity_expiration_settings.xml | 44 ++++----- app/src/main/res/layout/item_selectable.xml | 42 +++++++-- app/src/main/res/values/strings.xml | 2 + 7 files changed, 223 insertions(+), 58 deletions(-) create mode 100644 app/src/main/java/org/thoughtcrime/securesms/conversation/expiration/ExpirationSettingsViewModel.kt 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 9b944394b3..95a0e4cf50 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 @@ -3,25 +3,48 @@ package org.thoughtcrime.securesms.conversation.expiration import android.os.Bundle import android.os.Parcelable import android.util.SparseArray +import androidx.activity.viewModels import androidx.core.content.ContextCompat import androidx.core.view.isVisible +import androidx.lifecycle.lifecycleScope import androidx.recyclerview.widget.DividerItemDecoration import androidx.recyclerview.widget.RecyclerView import dagger.hilt.android.AndroidEntryPoint +import kotlinx.coroutines.launch import network.loki.messenger.R import network.loki.messenger.databinding.ActivityExpirationSettingsBinding import org.session.libsignal.protos.SignalServiceProtos.Content.ExpirationType import org.thoughtcrime.securesms.PassphraseRequiredActionBarActivity +import org.thoughtcrime.securesms.database.RecipientDatabase +import org.thoughtcrime.securesms.database.ThreadDatabase import org.thoughtcrime.securesms.preferences.RadioOption import org.thoughtcrime.securesms.preferences.RadioOptionAdapter +import javax.inject.Inject +import kotlin.math.max @AndroidEntryPoint class ExpirationSettingsActivity: PassphraseRequiredActionBarActivity() { private lateinit var binding : ActivityExpirationSettingsBinding + @Inject lateinit var recipientDb: RecipientDatabase + @Inject lateinit var threadDb: ThreadDatabase + @Inject lateinit var viewModelFactory: ExpirationSettingsViewModel.AssistedFactory + + private val threadId: Long by lazy { + intent.getLongExtra(THREAD_ID, -1) + } + private val expirationType: ExpirationType? by lazy { - ExpirationType.valueOf(intent.getIntExtra(EXTRA_EXPIRATION_TYPE, -1)) + ExpirationType.valueOf(intent.getIntExtra(EXPIRATION_TYPE, -1)) + } + + private val viewModel: ExpirationSettingsViewModel by viewModels { + val afterReadOptions = resources.getIntArray(R.array.read_expiration_time_values).map(Int::toString) + .zip(resources.getStringArray(R.array.read_expiration_time_names)) { value, name -> RadioOption(value, name)} + val afterSendOptions = resources.getIntArray(R.array.send_expiration_time_values).map(Int::toString) + .zip(resources.getStringArray(R.array.send_expiration_time_names)) { value, name -> RadioOption(value, name)} + viewModelFactory.create(threadId, expirationType, afterReadOptions, afterSendOptions) } override fun onSaveInstanceState(outState: Bundle) { @@ -45,25 +68,26 @@ class ExpirationSettingsActivity: PassphraseRequiredActionBarActivity() { } } - val options = if (expirationType == ExpirationType.DELETE_AFTER_SEND) { - val values = resources.getIntArray(R.array.send_expiration_time_values).map(Int::toString) - val names = resources.getStringArray(R.array.send_expiration_time_names) - values.zip(names) { value, name -> RadioOption(value, name)} - } else { - listOf( - RadioOption("off", getString(R.string.expiration_off)), - RadioOption("read", getString(R.string.expiration_type_disappear_after_read)), - RadioOption("send", getString(R.string.expiration_type_disappear_after_send)) + val deleteTypeOptions = listOf( + RadioOption("off", getString(R.string.expiration_off)), + RadioOption( + value = ExpirationType.DELETE_AFTER_READ_VALUE.toString(), + title = getString(R.string.expiration_type_disappear_after_read), + subtitle = getString(R.string.expiration_type_disappear_after_read_description) + ), + RadioOption( + value = ExpirationType.DELETE_AFTER_SEND_VALUE.toString(), + title = getString(R.string.expiration_type_disappear_after_send), + subtitle = getString(R.string.expiration_type_disappear_after_send_description) ) - } - val optionAdapter = RadioOptionAdapter { - + ) + val deleteTypeOptionAdapter = RadioOptionAdapter { + viewModel.onExpirationTypeSelected(it) } binding.textViewDeleteType.isVisible = expirationType == null - binding.textViewTimer.isVisible = expirationType == null - binding.layoutTimer.isVisible = expirationType == null - binding.recyclerView.apply { - adapter = optionAdapter + binding.layoutDeleteTypes.isVisible = expirationType == null + binding.recyclerViewDeleteTypes.apply { + adapter = deleteTypeOptionAdapter addItemDecoration(ContextCompat.getDrawable(this@ExpirationSettingsActivity, R.drawable.conversation_menu_divider)!!.let { DividerItemDecoration(this@ExpirationSettingsActivity, RecyclerView.VERTICAL).apply { setDrawable(it) @@ -71,7 +95,34 @@ class ExpirationSettingsActivity: PassphraseRequiredActionBarActivity() { }) setHasFixedSize(true) } - optionAdapter.submitList(options) + deleteTypeOptionAdapter.submitList(deleteTypeOptions) + + val timerOptionAdapter = RadioOptionAdapter { + viewModel.onExpirationTimerSelected(it) + } + binding.recyclerViewTimerOptions.apply { + adapter = timerOptionAdapter + addItemDecoration(ContextCompat.getDrawable(this@ExpirationSettingsActivity, R.drawable.conversation_menu_divider)!!.let { + DividerItemDecoration(this@ExpirationSettingsActivity, RecyclerView.VERTICAL).apply { + setDrawable(it) + } + }) + } + lifecycleScope.launchWhenStarted { + launch { + viewModel.selectedExpirationType.collect { type -> + val position = deleteTypeOptions.indexOfFirst { it.value.toIntOrNull() == type?.number } + deleteTypeOptionAdapter.setSelectedPosition(max(0, position)) + } + } + launch { + viewModel.expirationTimerOptions.collect { options -> + binding.textViewTimer.isVisible = options.isNotEmpty() && expirationType == null + binding.layoutTimer.isVisible = options.isNotEmpty() + timerOptionAdapter.submitList(options) + } + } + } } @@ -87,10 +138,12 @@ class ExpirationSettingsActivity: PassphraseRequiredActionBarActivity() { actionBar.setDisplayHomeAsUpEnabled(true) actionBar.setHomeButtonEnabled(true) } + companion object { private const val SCROLL_PARCEL = "scroll_parcel" - const val EXTRA_EXPIRATION_TYPE = "expiration_type" - const val EXTRA_READ_ONLY = "read_only" + const val THREAD_ID = "thread_id" + const val EXPIRATION_TYPE = "expiration_type" + const val READ_ONLY = "read_only" } } \ 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 new file mode 100644 index 0000000000..e7397c19c7 --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/expiration/ExpirationSettingsViewModel.kt @@ -0,0 +1,84 @@ +package org.thoughtcrime.securesms.conversation.expiration + +import androidx.lifecycle.ViewModel +import androidx.lifecycle.ViewModelProvider +import androidx.lifecycle.viewModelScope +import dagger.assisted.Assisted +import dagger.assisted.AssistedInject +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 org.session.libsession.utilities.recipients.Recipient +import org.session.libsignal.protos.SignalServiceProtos.Content.ExpirationType +import org.thoughtcrime.securesms.database.ThreadDatabase +import org.thoughtcrime.securesms.preferences.RadioOption + +class ExpirationSettingsViewModel( + private val threadId: Long, + private val expirationType: ExpirationType?, + private val afterReadOptions: List, + private val afterSendOptions: List, + private val threadDb: ThreadDatabase +) : ViewModel() { + + val recipient: Recipient? + get() = threadDb.getRecipientForThreadId(threadId) + + private val _selectedExpirationType = MutableStateFlow(expirationType) + val selectedExpirationType: StateFlow = _selectedExpirationType + + private val _expirationTimerOptions = MutableStateFlow>(emptyList()) + val expirationTimerOptions: StateFlow> = _expirationTimerOptions + + init { + selectedExpirationType.mapLatest { + when (it) { + ExpirationType.DELETE_AFTER_SEND -> afterSendOptions + ExpirationType.DELETE_AFTER_READ -> afterReadOptions + else -> emptyList() + } + }.onEach { + _expirationTimerOptions.value = it + }.launchIn(viewModelScope) + } + + fun onExpirationTypeSelected(option: RadioOption) { + _selectedExpirationType.value = option.value.toIntOrNull()?.let { ExpirationType.valueOf(it) } + } + + fun onExpirationTimerSelected(option: RadioOption) { + + } + + @dagger.assisted.AssistedFactory + interface AssistedFactory { + fun create( + threadId: Long, + expirationType: ExpirationType?, + @Assisted("afterRead") afterReadOptions: List, + @Assisted("afterSend") afterSendOptions: List + ): Factory + } + + @Suppress("UNCHECKED_CAST") + class Factory @AssistedInject constructor( + @Assisted private val threadId: Long, + @Assisted private val expirationType: ExpirationType?, + @Assisted("afterRead") private val afterReadOptions: List, + @Assisted("afterSend") private val afterSendOptions: List, + private val threadDb: ThreadDatabase + ) : ViewModelProvider.Factory { + + override fun create(modelClass: Class): T { + return ExpirationSettingsViewModel( + threadId, + expirationType, + afterReadOptions, + afterSendOptions, + threadDb + ) as T + } + } +} \ No newline at end of file diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationActivityV2.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationActivityV2.kt index 685939c4ad..a55fa496d4 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationActivityV2.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationActivityV2.kt @@ -990,8 +990,9 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe if (group?.isActive == false) { return } } val expirationIntent = Intent(this, ExpirationSettingsActivity::class.java) + expirationIntent.putExtra(ExpirationSettingsActivity.THREAD_ID, viewModel.threadId) if (thread.isLocalNumber || thread.isClosedGroupRecipient) { - expirationIntent.putExtra(ExpirationSettingsActivity.EXTRA_EXPIRATION_TYPE, ExpirationType.DELETE_AFTER_SEND_VALUE) + expirationIntent.putExtra(ExpirationSettingsActivity.EXPIRATION_TYPE, ExpirationType.DELETE_AFTER_SEND_VALUE) } show(expirationIntent, true) } diff --git a/app/src/main/java/org/thoughtcrime/securesms/preferences/RadioOptionAdapter.kt b/app/src/main/java/org/thoughtcrime/securesms/preferences/RadioOptionAdapter.kt index 2cb61a0e82..cb6a09257c 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/preferences/RadioOptionAdapter.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/preferences/RadioOptionAdapter.kt @@ -3,6 +3,7 @@ package org.thoughtcrime.securesms.preferences import android.view.LayoutInflater import android.view.View import android.view.ViewGroup +import androidx.core.view.isVisible import androidx.recyclerview.widget.DiffUtil import androidx.recyclerview.widget.ListAdapter import androidx.recyclerview.widget.RecyclerView @@ -11,7 +12,7 @@ import network.loki.messenger.databinding.ItemSelectableBinding import org.thoughtcrime.securesms.mms.GlideApp class RadioOptionAdapter( - var selectedOptionPosition: Int = 0, + private var selectedOptionPosition: Int = 0, private val onClickListener: (RadioOption) -> Unit ) : ListAdapter(RadioOptionDiffer()) { @@ -35,6 +36,11 @@ class RadioOptionAdapter( } } + fun setSelectedPosition(selectedPosition: Int) { + selectedOptionPosition = selectedPosition + notifyDataSetChanged() + } + class ViewHolder(itemView: View): RecyclerView.ViewHolder(itemView) { val glide = GlideApp.with(itemView) @@ -42,6 +48,8 @@ class RadioOptionAdapter( fun bind(option: RadioOption, isSelected: Boolean, toggleSelection: (RadioOption) -> Unit) { binding.titleTextView.text = option.title + binding.subtitleTextView.text = option.subtitle + binding.subtitleTextView.isVisible = !option.subtitle.isNullOrEmpty() binding.root.setOnClickListener { toggleSelection(option) } binding.selectButton.isSelected = isSelected } @@ -51,5 +59,6 @@ class RadioOptionAdapter( data class RadioOption( val value: String, - val title: String + val title: String, + val subtitle: String? = null ) diff --git a/app/src/main/res/layout/activity_expiration_settings.xml b/app/src/main/res/layout/activity_expiration_settings.xml index 59d62fcc5c..287becaa35 100644 --- a/app/src/main/res/layout/activity_expiration_settings.xml +++ b/app/src/main/res/layout/activity_expiration_settings.xml @@ -32,28 +32,28 @@ android:id="@+id/text_view_delete_type" android:layout_width="match_parent" android:layout_height="wrap_content" - android:paddingHorizontal="@dimen/large_spacing" + 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" /> @@ -61,7 +61,7 @@ android:id="@+id/text_view_timer" android:layout_width="match_parent" android:layout_height="wrap_content" - android:paddingHorizontal="@dimen/large_spacing" + android:paddingHorizontal="@dimen/very_large_spacing" android:paddingVertical="@dimen/small_spacing" android:text="@string/activity_expiration_settings_timer" android:textColor="?android:textColorTertiary" @@ -71,27 +71,20 @@ android:id="@+id/layout_timer" android:layout_width="match_parent" android:layout_height="wrap_content" + android:layout_marginHorizontal="@dimen/small_spacing" android:layout_marginBottom="@dimen/massive_spacing" android:background="@drawable/preference_single" - android:gravity="center" - android:padding="@dimen/medium_spacing"> + android:paddingHorizontal="@dimen/large_spacing" + android:paddingVertical="@dimen/medium_spacing"> - - - + android:clipToPadding="false" + app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager" + tools:itemCount="3" + tools:listitem="@layout/item_selectable" /> @@ -104,10 +97,9 @@ android:layout_alignParentBottom="true" android:layout_centerHorizontal="true" android:layout_marginTop="@dimen/medium_spacing" - android:layout_marginBottom="@dimen/massive_spacing" + android:layout_marginBottom="@dimen/very_large_spacing" android:text="@string/expiration_settings_set_button_title" /> - \ No newline at end of file diff --git a/app/src/main/res/layout/item_selectable.xml b/app/src/main/res/layout/item_selectable.xml index 886bde7332..0dcb4d526a 100644 --- a/app/src/main/res/layout/item_selectable.xml +++ b/app/src/main/res/layout/item_selectable.xml @@ -1,12 +1,10 @@ - @@ -14,18 +12,44 @@ android:id="@+id/titleTextView" android:layout_width="0dp" android:layout_height="wrap_content" - android:layout_margin="@dimen/medium_spacing" - android:layout_weight="1" + android:layout_marginTop="@dimen/small_spacing" android:ellipsize="end" android:lines="1" android:textSize="@dimen/text_size" - tools:text="@tools:sample/full_names" /> + android:textStyle="bold" + app:layout_goneMarginBottom="@dimen/small_spacing" + app:layout_constraintEnd_toStartOf="@id/selectButton" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toTopOf="parent" + app:layout_constraintBottom_toTopOf="@id/subtitleTextView" + tools:text="@tools:sample/cities" /> + + + android:foreground="@drawable/radial_multi_select" + app:layout_constraintHorizontal_bias="1" + app:layout_constraintBottom_toBottomOf="parent" + app:layout_constraintStart_toEndOf="@id/titleTextView" + app:layout_constraintTop_toTopOf="parent" + app:layout_constraintEnd_toEndOf="parent"/> - \ No newline at end of file + \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 56d11244fb..d21bf05166 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -875,7 +875,9 @@ This setting applies to everyone in this conversation. Messages disappear after they have been sent. Disappear After Read + Messages delete after they have been read. Disappear After Send + Messages delete after they have been sent. Set Delete Type Timer