Add timer selector

This commit is contained in:
charles 2022-10-27 09:07:50 +11:00
parent 63f372b45c
commit 68ca048267
7 changed files with 223 additions and 58 deletions

View File

@ -3,25 +3,48 @@ package org.thoughtcrime.securesms.conversation.expiration
import android.os.Bundle import android.os.Bundle
import android.os.Parcelable import android.os.Parcelable
import android.util.SparseArray import android.util.SparseArray
import androidx.activity.viewModels
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
import androidx.core.view.isVisible import androidx.core.view.isVisible
import androidx.lifecycle.lifecycleScope
import androidx.recyclerview.widget.DividerItemDecoration import androidx.recyclerview.widget.DividerItemDecoration
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import dagger.hilt.android.AndroidEntryPoint import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.launch
import network.loki.messenger.R import network.loki.messenger.R
import network.loki.messenger.databinding.ActivityExpirationSettingsBinding import network.loki.messenger.databinding.ActivityExpirationSettingsBinding
import org.session.libsignal.protos.SignalServiceProtos.Content.ExpirationType import org.session.libsignal.protos.SignalServiceProtos.Content.ExpirationType
import org.thoughtcrime.securesms.PassphraseRequiredActionBarActivity 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.RadioOption
import org.thoughtcrime.securesms.preferences.RadioOptionAdapter import org.thoughtcrime.securesms.preferences.RadioOptionAdapter
import javax.inject.Inject
import kotlin.math.max
@AndroidEntryPoint @AndroidEntryPoint
class ExpirationSettingsActivity: PassphraseRequiredActionBarActivity() { class ExpirationSettingsActivity: PassphraseRequiredActionBarActivity() {
private lateinit var binding : ActivityExpirationSettingsBinding 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 { 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) { override fun onSaveInstanceState(outState: Bundle) {
@ -45,25 +68,26 @@ class ExpirationSettingsActivity: PassphraseRequiredActionBarActivity() {
} }
} }
val options = if (expirationType == ExpirationType.DELETE_AFTER_SEND) { val deleteTypeOptions = listOf(
val values = resources.getIntArray(R.array.send_expiration_time_values).map(Int::toString) RadioOption("off", getString(R.string.expiration_off)),
val names = resources.getStringArray(R.array.send_expiration_time_names) RadioOption(
values.zip(names) { value, name -> RadioOption(value, name)} value = ExpirationType.DELETE_AFTER_READ_VALUE.toString(),
} else { title = getString(R.string.expiration_type_disappear_after_read),
listOf( subtitle = getString(R.string.expiration_type_disappear_after_read_description)
RadioOption("off", getString(R.string.expiration_off)), ),
RadioOption("read", getString(R.string.expiration_type_disappear_after_read)), RadioOption(
RadioOption("send", getString(R.string.expiration_type_disappear_after_send)) 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.textViewDeleteType.isVisible = expirationType == null
binding.textViewTimer.isVisible = expirationType == null binding.layoutDeleteTypes.isVisible = expirationType == null
binding.layoutTimer.isVisible = expirationType == null binding.recyclerViewDeleteTypes.apply {
binding.recyclerView.apply { adapter = deleteTypeOptionAdapter
adapter = optionAdapter
addItemDecoration(ContextCompat.getDrawable(this@ExpirationSettingsActivity, R.drawable.conversation_menu_divider)!!.let { addItemDecoration(ContextCompat.getDrawable(this@ExpirationSettingsActivity, R.drawable.conversation_menu_divider)!!.let {
DividerItemDecoration(this@ExpirationSettingsActivity, RecyclerView.VERTICAL).apply { DividerItemDecoration(this@ExpirationSettingsActivity, RecyclerView.VERTICAL).apply {
setDrawable(it) setDrawable(it)
@ -71,7 +95,34 @@ class ExpirationSettingsActivity: PassphraseRequiredActionBarActivity() {
}) })
setHasFixedSize(true) 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.setDisplayHomeAsUpEnabled(true)
actionBar.setHomeButtonEnabled(true) actionBar.setHomeButtonEnabled(true)
} }
companion object { companion object {
private const val SCROLL_PARCEL = "scroll_parcel" private const val SCROLL_PARCEL = "scroll_parcel"
const val EXTRA_EXPIRATION_TYPE = "expiration_type" const val THREAD_ID = "thread_id"
const val EXTRA_READ_ONLY = "read_only" const val EXPIRATION_TYPE = "expiration_type"
const val READ_ONLY = "read_only"
} }
} }

View File

@ -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<RadioOption>,
private val afterSendOptions: List<RadioOption>,
private val threadDb: ThreadDatabase
) : ViewModel() {
val recipient: Recipient?
get() = threadDb.getRecipientForThreadId(threadId)
private val _selectedExpirationType = MutableStateFlow(expirationType)
val selectedExpirationType: StateFlow<ExpirationType?> = _selectedExpirationType
private val _expirationTimerOptions = MutableStateFlow<List<RadioOption>>(emptyList())
val expirationTimerOptions: StateFlow<List<RadioOption>> = _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<RadioOption>,
@Assisted("afterSend") afterSendOptions: List<RadioOption>
): Factory
}
@Suppress("UNCHECKED_CAST")
class Factory @AssistedInject constructor(
@Assisted private val threadId: Long,
@Assisted private val expirationType: ExpirationType?,
@Assisted("afterRead") private val afterReadOptions: List<RadioOption>,
@Assisted("afterSend") private val afterSendOptions: List<RadioOption>,
private val threadDb: ThreadDatabase
) : ViewModelProvider.Factory {
override fun <T : ViewModel> create(modelClass: Class<T>): T {
return ExpirationSettingsViewModel(
threadId,
expirationType,
afterReadOptions,
afterSendOptions,
threadDb
) as T
}
}
}

View File

@ -990,8 +990,9 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
if (group?.isActive == false) { return } if (group?.isActive == false) { return }
} }
val expirationIntent = Intent(this, ExpirationSettingsActivity::class.java) val expirationIntent = Intent(this, ExpirationSettingsActivity::class.java)
expirationIntent.putExtra(ExpirationSettingsActivity.THREAD_ID, viewModel.threadId)
if (thread.isLocalNumber || thread.isClosedGroupRecipient) { 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) show(expirationIntent, true)
} }

View File

@ -3,6 +3,7 @@ package org.thoughtcrime.securesms.preferences
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import androidx.core.view.isVisible
import androidx.recyclerview.widget.DiffUtil import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.ListAdapter import androidx.recyclerview.widget.ListAdapter
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
@ -11,7 +12,7 @@ import network.loki.messenger.databinding.ItemSelectableBinding
import org.thoughtcrime.securesms.mms.GlideApp import org.thoughtcrime.securesms.mms.GlideApp
class RadioOptionAdapter( class RadioOptionAdapter(
var selectedOptionPosition: Int = 0, private var selectedOptionPosition: Int = 0,
private val onClickListener: (RadioOption) -> Unit private val onClickListener: (RadioOption) -> Unit
) : ListAdapter<RadioOption, RadioOptionAdapter.ViewHolder>(RadioOptionDiffer()) { ) : ListAdapter<RadioOption, RadioOptionAdapter.ViewHolder>(RadioOptionDiffer()) {
@ -35,6 +36,11 @@ class RadioOptionAdapter(
} }
} }
fun setSelectedPosition(selectedPosition: Int) {
selectedOptionPosition = selectedPosition
notifyDataSetChanged()
}
class ViewHolder(itemView: View): RecyclerView.ViewHolder(itemView) { class ViewHolder(itemView: View): RecyclerView.ViewHolder(itemView) {
val glide = GlideApp.with(itemView) val glide = GlideApp.with(itemView)
@ -42,6 +48,8 @@ class RadioOptionAdapter(
fun bind(option: RadioOption, isSelected: Boolean, toggleSelection: (RadioOption) -> Unit) { fun bind(option: RadioOption, isSelected: Boolean, toggleSelection: (RadioOption) -> Unit) {
binding.titleTextView.text = option.title binding.titleTextView.text = option.title
binding.subtitleTextView.text = option.subtitle
binding.subtitleTextView.isVisible = !option.subtitle.isNullOrEmpty()
binding.root.setOnClickListener { toggleSelection(option) } binding.root.setOnClickListener { toggleSelection(option) }
binding.selectButton.isSelected = isSelected binding.selectButton.isSelected = isSelected
} }
@ -51,5 +59,6 @@ class RadioOptionAdapter(
data class RadioOption( data class RadioOption(
val value: String, val value: String,
val title: String val title: String,
val subtitle: String? = null
) )

View File

@ -32,28 +32,28 @@
android:id="@+id/text_view_delete_type" android:id="@+id/text_view_delete_type"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:paddingHorizontal="@dimen/large_spacing" android:paddingHorizontal="@dimen/very_large_spacing"
android:paddingVertical="@dimen/small_spacing" android:paddingVertical="@dimen/small_spacing"
android:text="@string/activity_expiration_settings_delete_type" android:text="@string/activity_expiration_settings_delete_type"
android:textColor="?android:textColorTertiary" android:textColor="?android:textColorTertiary"
android:textSize="@dimen/medium_font_size" /> android:textSize="@dimen/medium_font_size" />
<LinearLayout <LinearLayout
android:id="@+id/theme_option_classic_dark" android:id="@+id/layout_delete_types"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:background="@drawable/preference_top" android:layout_marginHorizontal="@dimen/small_spacing"
android:orientation="horizontal" android:background="@drawable/preference_single"
android:paddingHorizontal="@dimen/large_spacing" android:paddingHorizontal="@dimen/large_spacing"
android:paddingVertical="@dimen/medium_spacing"> android:paddingVertical="@dimen/medium_spacing">
<androidx.recyclerview.widget.RecyclerView <androidx.recyclerview.widget.RecyclerView
android:id="@+id/recyclerView" android:id="@+id/recycler_view_delete_types"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:clipToPadding="false" android:clipToPadding="false"
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager" app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
tools:itemCount="6" tools:itemCount="3"
tools:listitem="@layout/item_selectable" /> tools:listitem="@layout/item_selectable" />
</LinearLayout> </LinearLayout>
@ -61,7 +61,7 @@
android:id="@+id/text_view_timer" android:id="@+id/text_view_timer"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:paddingHorizontal="@dimen/large_spacing" android:paddingHorizontal="@dimen/very_large_spacing"
android:paddingVertical="@dimen/small_spacing" android:paddingVertical="@dimen/small_spacing"
android:text="@string/activity_expiration_settings_timer" android:text="@string/activity_expiration_settings_timer"
android:textColor="?android:textColorTertiary" android:textColor="?android:textColorTertiary"
@ -71,27 +71,20 @@
android:id="@+id/layout_timer" android:id="@+id/layout_timer"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginHorizontal="@dimen/small_spacing"
android:layout_marginBottom="@dimen/massive_spacing" android:layout_marginBottom="@dimen/massive_spacing"
android:background="@drawable/preference_single" android:background="@drawable/preference_single"
android:gravity="center" android:paddingHorizontal="@dimen/large_spacing"
android:padding="@dimen/medium_spacing"> android:paddingVertical="@dimen/medium_spacing">
<TextView <androidx.recyclerview.widget.RecyclerView
android:layout_width="0dp" android:id="@+id/recycler_view_timer_options"
android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_weight="1" android:clipToPadding="false"
android:paddingHorizontal="@dimen/large_spacing" app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
android:paddingVertical="@dimen/small_spacing" tools:itemCount="3"
android:text="@string/activity_appearance_follow_system_explanation" tools:listitem="@layout/item_selectable" />
android:textColor="?android:textColorPrimary"
android:textSize="@dimen/medium_font_size"
android:textStyle="bold" />
<androidx.appcompat.widget.SwitchCompat
android:id="@+id/system_settings_switch"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:paddingHorizontal="@dimen/large_spacing" />
</LinearLayout> </LinearLayout>
</LinearLayout> </LinearLayout>
@ -104,10 +97,9 @@
android:layout_alignParentBottom="true" android:layout_alignParentBottom="true"
android:layout_centerHorizontal="true" android:layout_centerHorizontal="true"
android:layout_marginTop="@dimen/medium_spacing" 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" /> android:text="@string/expiration_settings_set_button_title" />
</RelativeLayout> </RelativeLayout>
</ScrollView> </ScrollView>

View File

@ -1,12 +1,10 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" <androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools" xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/backgroundContainer" xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:background="?selectableItemBackground" android:background="?selectableItemBackground"
android:gravity="center_vertical"
android:orientation="horizontal"
android:paddingHorizontal="@dimen/medium_spacing" android:paddingHorizontal="@dimen/medium_spacing"
android:paddingVertical="@dimen/small_spacing"> android:paddingVertical="@dimen/small_spacing">
@ -14,18 +12,44 @@
android:id="@+id/titleTextView" android:id="@+id/titleTextView"
android:layout_width="0dp" android:layout_width="0dp"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_margin="@dimen/medium_spacing" android:layout_marginTop="@dimen/small_spacing"
android:layout_weight="1"
android:ellipsize="end" android:ellipsize="end"
android:lines="1" android:lines="1"
android:textSize="@dimen/text_size" 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" />
<TextView
android:id="@+id/subtitleTextView"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginBottom="@dimen/small_spacing"
android:ellipsize="end"
android:lines="1"
android:textSize="@dimen/very_small_font_size"
android:visibility="gone"
app:layout_constraintEnd_toStartOf="@id/selectButton"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/titleTextView"
tools:text="@tools:sample/full_names"
tools:visibility="visible"/>
<View <View
android:id="@+id/selectButton" android:id="@+id/selectButton"
android:layout_width="@dimen/small_radial_size" android:layout_width="@dimen/small_radial_size"
android:layout_height="@dimen/small_radial_size" android:layout_height="@dimen/small_radial_size"
android:background="@drawable/padded_circle_accent_select" android:background="@drawable/padded_circle_accent_select"
android:foreground="@drawable/radial_multi_select" /> 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"/>
</LinearLayout> </androidx.constraintlayout.widget.ConstraintLayout>

View File

@ -875,7 +875,9 @@
<string name="activity_expiration_settings_subtitle">This setting applies to everyone in this conversation.</string> <string name="activity_expiration_settings_subtitle">This setting applies to everyone in this conversation.</string>
<string name="activity_expiration_settings_subtitle_sent">Messages disappear after they have been sent.</string> <string name="activity_expiration_settings_subtitle_sent">Messages disappear after they have been sent.</string>
<string name="expiration_type_disappear_after_read">Disappear After Read</string> <string name="expiration_type_disappear_after_read">Disappear After Read</string>
<string name="expiration_type_disappear_after_read_description">Messages delete after they have been read.</string>
<string name="expiration_type_disappear_after_send">Disappear After Send</string> <string name="expiration_type_disappear_after_send">Disappear After Send</string>
<string name="expiration_type_disappear_after_send_description">Messages delete after they have been sent.</string>
<string name="expiration_settings_set_button_title">Set</string> <string name="expiration_settings_set_button_title">Set</string>
<string name="activity_expiration_settings_delete_type">Delete Type</string> <string name="activity_expiration_settings_delete_type">Delete Type</string>
<string name="activity_expiration_settings_timer">Timer</string> <string name="activity_expiration_settings_timer">Timer</string>