feat: add more conversation settings options and notification settings

pull/1403/head
0x330a 3 years ago
parent 880a3f603c
commit f08ae7a874

@ -228,6 +228,10 @@
<activity android:name="org.thoughtcrime.securesms.conversation.settings.ConversationSettingsActivity"
android:screenOrientation="portrait"
android:theme="@style/Theme.Session.DayNight.NoActionBar"/>
<activity android:name="org.thoughtcrime.securesms.conversation.settings.ConversationNotificationSettingsActivity"
android:label="@string/activity_notification_settings_title"
android:screenOrientation="portrait"
android:theme="@style/Theme.Session.DayNight"/>
<activity
android:name="org.thoughtcrime.securesms.conversation.v2.MessageDetailActivity"
android:screenOrientation="portrait"

@ -0,0 +1,59 @@
package org.thoughtcrime.securesms.conversation.settings
import android.os.Bundle
import android.view.View
import dagger.hilt.android.AndroidEntryPoint
import network.loki.messenger.databinding.ActivityConversationNotificationSettingsBinding
import org.thoughtcrime.securesms.PassphraseRequiredActionBarActivity
import org.thoughtcrime.securesms.conversation.v2.ConversationActivityV2
import org.thoughtcrime.securesms.database.RecipientDatabase
import org.thoughtcrime.securesms.database.ThreadDatabase
import javax.inject.Inject
@AndroidEntryPoint
class ConversationNotificationSettingsActivity: PassphraseRequiredActionBarActivity(), View.OnClickListener {
lateinit var binding: ActivityConversationNotificationSettingsBinding
@Inject lateinit var threadDb: ThreadDatabase
@Inject lateinit var recipientDb: RecipientDatabase
val recipient by lazy {
if (threadId == -1L) null
else threadDb.getRecipientForThreadId(threadId)
}
var threadId: Long = -1
override fun onClick(v: View?) {
val recipient = recipient ?: return
if (v === binding.notifyAll) {
// set notify type
recipientDb.setNotifyType(recipient, RecipientDatabase.NOTIFY_TYPE_ALL)
} else if (v === binding.notifyMentions) {
recipientDb.setNotifyType(recipient, RecipientDatabase.NOTIFY_TYPE_MENTIONS)
} else if (v === binding.notifyMute) {
recipientDb.setNotifyType(recipient, RecipientDatabase.NOTIFY_TYPE_NONE)
}
updateValues()
}
override fun onCreate(savedInstanceState: Bundle?, ready: Boolean) {
super.onCreate(savedInstanceState, ready)
binding = ActivityConversationNotificationSettingsBinding.inflate(layoutInflater)
setContentView(binding.root)
threadId = intent.getLongExtra(ConversationActivityV2.THREAD_ID, -1L)
if (threadId == -1L) finish()
updateValues()
with (binding) {
notifyAll.setOnClickListener(this@ConversationNotificationSettingsActivity)
notifyMentions.setOnClickListener(this@ConversationNotificationSettingsActivity)
notifyMute.setOnClickListener(this@ConversationNotificationSettingsActivity)
}
}
private fun updateValues() {
val notifyType = recipient?.notifyType ?: return
binding.notifyAllButton.isSelected = notifyType == RecipientDatabase.NOTIFY_TYPE_ALL
binding.notifyMentionsButton.isSelected = notifyType == RecipientDatabase.NOTIFY_TYPE_MENTIONS
binding.notifyMuteButton.isSelected = notifyType == RecipientDatabase.NOTIFY_TYPE_NONE
}
}

@ -0,0 +1,16 @@
package org.thoughtcrime.securesms.conversation.settings
import android.content.Context
import android.content.Intent
import androidx.activity.result.contract.ActivityResultContract
import org.thoughtcrime.securesms.conversation.v2.ConversationActivityV2
class ConversationNotificationSettingsActivityContract: ActivityResultContract<Long, Unit>() {
override fun createIntent(context: Context, input: Long?): Intent =
Intent(context, ConversationNotificationSettingsActivity::class.java).apply {
putExtra(ConversationActivityV2.THREAD_ID, input)
}
override fun parseResult(resultCode: Int, intent: Intent?) { /* do nothing */ }
}

@ -27,6 +27,18 @@ class ConversationSettingsActivity: PassphraseRequiredActionBarActivity(), View.
lateinit var binding: ActivityConversationSettingsBinding
private val groupOptions: List<View>
get() = with(binding) {
listOf(
groupMembers,
groupMembersDivider.root,
editGroup,
editGroupDivider.root,
leaveGroup,
leaveGroupDivider.root
)
}
@Inject lateinit var threadDb: ThreadDatabase
@Inject lateinit var lokiThreadDb: LokiThreadDatabase
@Inject lateinit var viewModelFactory: ConversationSettingsViewModel.AssistedFactory
@ -38,6 +50,10 @@ class ConversationSettingsActivity: PassphraseRequiredActionBarActivity(), View.
viewModelFactory.create(threadId)
}
private val notificationActivityCallback = registerForActivityResult(ConversationNotificationSettingsActivityContract()) {
updateRecipientDisplay()
}
override fun onCreate(savedInstanceState: Bundle?, ready: Boolean) {
super.onCreate(savedInstanceState, ready)
binding = ActivityConversationSettingsBinding.inflate(layoutInflater)
@ -47,16 +63,12 @@ class ConversationSettingsActivity: PassphraseRequiredActionBarActivity(), View.
binding.searchConversation.setOnClickListener(this)
binding.allMedia.setOnClickListener(this)
binding.pinConversation.setOnClickListener(this)
}
override fun onResume() {
super.onResume()
}
override fun onPause() {
super.onPause()
binding.notificationSettings.setOnClickListener(this)
binding.back.setOnClickListener(this)
binding.autoDownloadMediaSwitch.setOnCheckedChangeListener { _, isChecked ->
viewModel.setTrusted(isChecked)
updateRecipientDisplay()
}
}
private fun updateRecipientDisplay() {
@ -72,35 +84,66 @@ class ConversationSettingsActivity: PassphraseRequiredActionBarActivity(), View.
binding.conversationSubtitle.isVisible = recipient.isClosedGroupRecipient.apply {
binding.conversationSubtitle.text = "TODO: This is a test for group descriptions"
}
// Toggle group-specific settings
val areGroupOptionsVisible = recipient.isClosedGroupRecipient
groupOptions.forEach { v ->
v.isVisible = areGroupOptionsVisible
}
// Group admin settings
val isUserGroupAdmin = areGroupOptionsVisible && viewModel.isUserGroupAdmin()
with (binding) {
groupMembersDivider.root.isVisible = areGroupOptionsVisible && !isUserGroupAdmin
groupMembers.isVisible = areGroupOptionsVisible && !isUserGroupAdmin
adminControlsGroup.isVisible = isUserGroupAdmin
deleteGroup.isVisible = isUserGroupAdmin
clearMessagesDivider.root.isVisible = isUserGroupAdmin
}
// Set pinned state
binding.pinConversation.setText(
if (viewModel.isPinned()) R.string.conversation_settings_unpin_conversation
else R.string.conversation_settings_pin_conversation
)
// Set auto-download state
val trusted = viewModel.isTrusted()
binding.autoDownloadMediaSwitch.isChecked = trusted
// Set notification type
val notifyTypes = resources.getStringArray(R.array.notify_types)
val summary = notifyTypes.getOrNull(recipient.notifyType)
binding.notificationsValue.text = summary
}
override fun onClick(v: View?) {
if (v === binding.searchConversation) {
setResult(RESULT_SEARCH)
finish()
} else if (v === binding.allMedia) {
val threadRecipient = viewModel.recipient ?: return
val intent = Intent(this, MediaOverviewActivity::class.java).apply {
putExtra(MediaOverviewActivity.ADDRESS_EXTRA, threadRecipient.address)
when {
v === binding.searchConversation -> {
setResult(RESULT_SEARCH)
finish()
}
startActivity(intent)
} else if (v === binding.pinConversation) {
viewModel.togglePin().invokeOnCompletion { e ->
if (e != null) {
// something happened
Log.e("ConversationSettings", "Failed to toggle pin on thread", e)
} else {
updateRecipientDisplay()
v === binding.allMedia -> {
val threadRecipient = viewModel.recipient ?: return
val intent = Intent(this, MediaOverviewActivity::class.java).apply {
putExtra(MediaOverviewActivity.ADDRESS_EXTRA, threadRecipient.address)
}
startActivity(intent)
}
v === binding.pinConversation -> {
viewModel.togglePin().invokeOnCompletion { e ->
if (e != null) {
// something happened
Log.e("ConversationSettings", "Failed to toggle pin on thread", e)
} else {
updateRecipientDisplay()
}
}
}
v === binding.notificationSettings -> {
notificationActivityCallback.launch(viewModel.threadId)
}
v === binding.back -> onBackPressed()
}
}
}

@ -6,11 +6,15 @@ import androidx.lifecycle.viewModelScope
import dagger.assisted.Assisted
import dagger.assisted.AssistedInject
import kotlinx.coroutines.launch
import org.session.libsession.database.StorageProtocol
import org.session.libsession.utilities.Address
import org.session.libsession.utilities.TextSecurePreferences
import org.thoughtcrime.securesms.database.Storage
class ConversationSettingsViewModel(
val threadId: Long,
private val storage: Storage
private val storage: StorageProtocol,
private val prefs: TextSecurePreferences
): ViewModel() {
val recipient get() = storage.getRecipientForThread(threadId)
@ -22,6 +26,22 @@ class ConversationSettingsViewModel(
storage.setThreadPinned(threadId, !isPinned)
}
fun isTrusted() = recipient?.let { recipient ->
storage.isContactTrusted(recipient)
} ?: false
fun setTrusted(isTrusted: Boolean) {
val recipient = recipient ?: return
storage.setContactTrusted(recipient, isTrusted)
}
fun isUserGroupAdmin(): Boolean = recipient?.let { recipient ->
if (!recipient.isGroupRecipient) return@let false
val localUserAddress = prefs.getLocalNumber() ?: return@let false
val group = storage.getGroup(recipient.address.toGroupString())
group?.admins?.contains(Address.fromSerialized(localUserAddress)) ?: false // this will have to be replaced for new closed groups
} ?: false
// DI-related
@dagger.assisted.AssistedFactory
interface AssistedFactory {
@ -29,11 +49,12 @@ class ConversationSettingsViewModel(
}
class Factory @AssistedInject constructor(
@Assisted private val threadId: Long,
private val storage: Storage
private val storage: Storage,
private val prefs: TextSecurePreferences
) : ViewModelProvider.Factory {
@Suppress("UNCHECKED_CAST")
override fun <T : ViewModel> create(modelClass: Class<T>): T {
return ConversationSettingsViewModel(threadId, storage) as T
return ConversationSettingsViewModel(threadId, storage, prefs) as T
}
}

@ -685,6 +685,21 @@ class Storage(context: Context, helper: SQLCipherOpenHelper) : Database(context,
}
}
override fun isContactTrusted(recipient: Recipient): Boolean {
val sessionID = recipient.address.toString()
val contactDb = DatabaseComponent.get(context).sessionContactDatabase()
val contact = contactDb.getContactWithSessionID(sessionID) ?: return false
return contact.isTrusted
}
override fun setContactTrusted(recipient: Recipient, isTrusted: Boolean) {
val sessionID = recipient.address.toString()
val contactDb = DatabaseComponent.get(context).sessionContactDatabase()
val contact = contactDb.getContactWithSessionID(sessionID) ?: return
val threadID = DatabaseComponent.get(context).threadDatabase().getThreadIdIfExistsFor(recipient)
contactDb.setContactIsTrusted(contact, isTrusted, threadID)
}
override fun getLastUpdated(threadID: Long): Long {
val threadDB = DatabaseComponent.get(context).threadDatabase()
return threadDB.getLastUpdated(threadID)

@ -0,0 +1,73 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
xmlns:app="http://schemas.android.com/apk/res-auto">
<TextView
android:background="@drawable/preference_top"
android:paddingTop="@dimen/small_spacing"
android:id="@+id/notifyAll"
style="@style/TextAppearance.Session.ConversationSettings.Option"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
android:text="@string/notify_type_all"
android:layout_width="0dp"
android:layout_height="72dp"/>
<View
android:layout_marginTop="@dimen/small_spacing"
app:layout_constraintTop_toTopOf="@+id/notifyAll"
app:layout_constraintBottom_toBottomOf="@+id/notifyAll"
app:layout_constraintEnd_toEndOf="@+id/notifyAll"
android:layout_marginEnd="54dp"
android:id="@+id/notifyAllButton"
android:padding="@dimen/small_spacing"
android:layout_width="@dimen/small_radial_size"
android:layout_height="@dimen/small_radial_size"
android:background="@drawable/padded_circle_accent_select"
android:foreground="@drawable/radial_multi_select"/>
<TextView
app:layout_constraintTop_toBottomOf="@+id/notifyAll"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
android:id="@+id/notifyMentions"
style="@style/TextAppearance.Session.ConversationSettings.Option"
android:background="@drawable/preference_middle"
android:text="@string/notify_type_mentions"
android:layout_width="0dp"
android:layout_height="@dimen/setting_button_height"/>
<View
app:layout_constraintTop_toTopOf="@+id/notifyMentions"
app:layout_constraintBottom_toBottomOf="@+id/notifyMentions"
app:layout_constraintEnd_toEndOf="@+id/notifyMentions"
android:layout_marginEnd="54dp"
android:id="@+id/notifyMentionsButton"
android:layout_width="@dimen/small_radial_size"
android:layout_height="@dimen/small_radial_size"
android:background="@drawable/padded_circle_accent_select"
android:foreground="@drawable/radial_multi_select"/>
<TextView
android:paddingBottom="@dimen/small_spacing"
app:layout_constraintTop_toBottomOf="@+id/notifyMentions"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
android:id="@+id/notifyMute"
style="@style/TextAppearance.Session.ConversationSettings.Option"
android:background="@drawable/preference_bottom"
android:text="@string/notify_type_mute"
android:layout_width="0dp"
android:layout_height="72dp"/>
<View
android:layout_marginBottom="@dimen/small_spacing"
app:layout_constraintTop_toTopOf="@+id/notifyMute"
app:layout_constraintBottom_toBottomOf="@+id/notifyMute"
app:layout_constraintEnd_toEndOf="@+id/notifyMute"
android:layout_marginEnd="54dp"
android:id="@+id/notifyMuteButton"
android:layout_width="@dimen/small_radial_size"
android:layout_height="@dimen/small_radial_size"
android:background="@drawable/padded_circle_accent_select"
android:foreground="@drawable/radial_multi_select"/>
</androidx.constraintlayout.widget.ConstraintLayout>

@ -9,6 +9,16 @@
android:layout_width="match_parent"
android:layout_height="wrap_content">
<ImageView
android:id="@+id/back"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintStart_toStartOf="parent"
android:src="@drawable/ic_baseline_arrow_back_24"
android:scaleType="centerInside"
android:layout_width="?android:actionBarSize"
android:layout_height="?android:actionBarSize"
app:tint="?android:textColorPrimary" />
<include
android:id="@+id/profilePictureView"
layout="@layout/view_large_profile_picture"
@ -71,6 +81,21 @@
android:layout_width="match_parent"
android:layout_height="wrap_content"
/>
<TextView
android:background="?selectableItemBackground"
app:drawableStartCompat="@drawable/ic_edit_group"
style="@style/TextAppearance.Session.ConversationSettings.Option"
android:text="@string/conversation_settings_group_members"
android:id="@+id/groupMembers"
android:layout_width="match_parent"
android:layout_height="@dimen/setting_button_height"/>
<include
android:id="@+id/groupMembersDivider"
android:layout_marginHorizontal="@dimen/large_spacing"
layout="@layout/preference_divider"
android:layout_width="match_parent"
android:layout_height="wrap_content"
/>
<TextView
android:background="?selectableItemBackground"
app:drawableStartCompat="@drawable/ic_all_media"
@ -235,6 +260,7 @@
android:layout_width="match_parent"
android:layout_height="@dimen/setting_button_height"/>
<include
android:id="@+id/editGroupDivider"
android:layout_marginHorizontal="@dimen/large_spacing"
layout="@layout/preference_divider"
android:layout_width="match_parent"
@ -319,6 +345,7 @@
android:layout_width="match_parent"
android:layout_height="@dimen/setting_button_height"/>
<include
android:id="@+id/clearMessagesDivider"
android:layout_marginHorizontal="@dimen/large_spacing"
layout="@layout/preference_divider"
android:layout_width="match_parent"
@ -333,6 +360,7 @@
android:layout_width="match_parent"
android:layout_height="@dimen/setting_button_height"/>
<include
android:id="@+id/leaveGroupDivider"
android:layout_marginHorizontal="@dimen/large_spacing"
layout="@layout/preference_divider"
android:layout_width="match_parent"
@ -342,7 +370,7 @@
android:background="?selectableItemBackground"
app:drawableStartCompat="@drawable/ic_delete"
style="@style/TextAppearance.Session.ConversationSettings.Option.Destructive"
android:text="@string/conversation_settings_edit_group"
android:text="@string/conversation_settings_delete_group"
android:id="@+id/deleteGroup"
android:layout_width="match_parent"
android:layout_height="@dimen/setting_button_height"/>

@ -7,24 +7,26 @@
<RelativeLayout
android:id="@+id/doubleModeImageViewContainer"
android:layout_centerInParent="true"
android:layout_width="@dimen/large_profile_picture_size"
android:layout_height="@dimen/large_profile_picture_size">
android:layout_width="@dimen/extra_large_profile_picture_size"
android:layout_height="@dimen/extra_large_profile_picture_size">
<ImageView
android:layout_margin="@dimen/small_spacing"
android:id="@+id/doubleModeImageView1"
android:layout_width="@dimen/medium_profile_picture_size"
android:layout_height="@dimen/medium_profile_picture_size"
android:layout_width="@dimen/large_profile_picture_size"
android:layout_height="@dimen/large_profile_picture_size"
android:layout_alignParentLeft="true"
android:layout_alignParentTop="true"
android:background="@drawable/profile_picture_view_medium_background" />
android:background="@drawable/profile_picture_view_large_background" />
<ImageView
android:layout_margin="@dimen/small_spacing"
android:id="@+id/doubleModeImageView2"
android:layout_width="@dimen/medium_profile_picture_size"
android:layout_height="@dimen/medium_profile_picture_size"
android:layout_width="@dimen/large_profile_picture_size"
android:layout_height="@dimen/large_profile_picture_size"
android:layout_alignParentRight="true"
android:layout_alignParentBottom="true"
android:background="@drawable/profile_picture_view_medium_background" />
android:background="@drawable/profile_picture_view_large_background" />
</RelativeLayout>

@ -234,6 +234,7 @@
<string-array name="notify_types">
<item>@string/notify_type_all</item>
<item>@string/notify_type_mentions</item>
<item>@string/notify_type_mute</item>
</string-array>
</resources>

@ -748,8 +748,9 @@
<string name="dialog_send_seed_title">Warning</string>
<string name="dialog_send_seed_explanation">This is your recovery phrase. If you send it to someone they\'ll have full access to your account.</string>
<string name="dialog_send_seed_send_button_title">Send</string>
<string name="notify_type_all">All</string>
<string name="notify_type_mentions">Mentions</string>
<string name="notify_type_all">All Messages</string>
<string name="notify_type_mentions">Mentions Only</string>
<string name="notify_type_mute">Mute</string>
<string name="deleted_message">This message has been deleted</string>
<string name="delete_message_for_me">Delete just for me</string>
<string name="delete_message_for_everyone">Delete for everyone</string>
@ -877,4 +878,6 @@
<string name="conversation_settings_add_admins">Add Admins</string>
<string name="conversation_settings_clear_messages">Clear Messages</string>
<string name="conversation_settings_leave_group">Leave Group</string>
<string name="conversation_settings_group_members">Group Members</string>
<string name="conversation_settings_delete_group">Delete Group</string>
</resources>

@ -163,6 +163,8 @@ interface StorageProtocol {
fun getRecipientForThread(threadId: Long): Recipient?
fun getRecipientSettings(address: Address): RecipientSettings?
fun addContacts(contacts: List<ConfigurationMessage.Contact>)
fun isContactTrusted(recipient: Recipient): Boolean
fun setContactTrusted(recipient: Recipient, isTrusted: Boolean)
// Attachments
fun getAttachmentDataUri(attachmentId: AttachmentId): Uri

Loading…
Cancel
Save