mirror of
https://github.com/signalapp/Signal-Android.git
synced 2026-04-25 19:29:54 +01:00
Add context menus to story contacts in contact selection.
This commit is contained in:
committed by
Cody Henthorne
parent
7bd34d2b99
commit
c64be82710
@@ -1,12 +1,15 @@
|
||||
package org.thoughtcrime.securesms.contacts.paged
|
||||
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.CheckBox
|
||||
import android.widget.TextView
|
||||
import org.thoughtcrime.securesms.R
|
||||
import org.thoughtcrime.securesms.badges.BadgeImageView
|
||||
import org.thoughtcrime.securesms.components.AvatarImageView
|
||||
import org.thoughtcrime.securesms.components.FromTextView
|
||||
import org.thoughtcrime.securesms.components.menu.ActionItem
|
||||
import org.thoughtcrime.securesms.components.menu.SignalContextMenu
|
||||
import org.thoughtcrime.securesms.recipients.Recipient
|
||||
import org.thoughtcrime.securesms.util.adapter.mapping.LayoutFactory
|
||||
import org.thoughtcrime.securesms.util.adapter.mapping.MappingAdapter
|
||||
@@ -27,11 +30,12 @@ object ContactSearchItems {
|
||||
displayCheckBox: Boolean,
|
||||
recipientListener: RecipientClickListener,
|
||||
storyListener: StoryClickListener,
|
||||
storyContextMenuCallbacks: StoryContextMenuCallbacks,
|
||||
expandListener: (ContactSearchData.Expand) -> Unit
|
||||
) {
|
||||
mappingAdapter.registerFactory(
|
||||
StoryModel::class.java,
|
||||
LayoutFactory({ StoryViewHolder(it, displayCheckBox, storyListener) }, R.layout.contact_search_item)
|
||||
LayoutFactory({ StoryViewHolder(it, displayCheckBox, storyListener, storyContextMenuCallbacks) }, R.layout.contact_search_item)
|
||||
)
|
||||
mappingAdapter.registerFactory(
|
||||
RecipientModel::class.java,
|
||||
@@ -82,7 +86,7 @@ object ContactSearchItems {
|
||||
}
|
||||
}
|
||||
|
||||
private class StoryViewHolder(itemView: View, displayCheckBox: Boolean, onClick: StoryClickListener) : BaseRecipientViewHolder<StoryModel, ContactSearchData.Story>(itemView, displayCheckBox, onClick) {
|
||||
private class StoryViewHolder(itemView: View, displayCheckBox: Boolean, onClick: StoryClickListener, private val storyContextMenuCallbacks: StoryContextMenuCallbacks) : BaseRecipientViewHolder<StoryModel, ContactSearchData.Story>(itemView, displayCheckBox, onClick) {
|
||||
override fun isSelected(model: StoryModel): Boolean = model.isSelected
|
||||
override fun getData(model: StoryModel): ContactSearchData.Story = model.story
|
||||
override fun getRecipient(model: StoryModel): Recipient = model.story.recipient
|
||||
@@ -104,6 +108,50 @@ object ContactSearchItems {
|
||||
|
||||
number.text = context.resources.getQuantityString(pluralId, count, count)
|
||||
}
|
||||
|
||||
override fun bindLongPress(model: StoryModel) {
|
||||
itemView.setOnLongClickListener {
|
||||
val actions: List<ActionItem> = when {
|
||||
model.story.recipient.isMyStory -> getMyStoryContextMenuActions(model)
|
||||
model.story.recipient.isGroup -> getGroupStoryContextMenuActions(model)
|
||||
model.story.recipient.isDistributionList -> getPrivateStoryContextMenuActions(model)
|
||||
else -> error("Unsupported story target. Not a group or distribution list.")
|
||||
}
|
||||
|
||||
SignalContextMenu.Builder(itemView, itemView.rootView as ViewGroup)
|
||||
.offsetX(context.resources.getDimensionPixelSize(R.dimen.dsl_settings_gutter))
|
||||
.show(actions)
|
||||
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
private fun getMyStoryContextMenuActions(model: StoryModel): List<ActionItem> {
|
||||
return listOf(
|
||||
ActionItem(R.drawable.ic_settings_24, context.getString(R.string.ContactSearchItems__story_settings)) {
|
||||
storyContextMenuCallbacks.onOpenStorySettings(model.story)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
private fun getGroupStoryContextMenuActions(model: StoryModel): List<ActionItem> {
|
||||
return listOf(
|
||||
ActionItem(R.drawable.ic_minus_circle_20, context.getString(R.string.ContactSearchItems__remove_story)) {
|
||||
storyContextMenuCallbacks.onRemoveGroupStory(model.story, model.isSelected)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
private fun getPrivateStoryContextMenuActions(model: StoryModel): List<ActionItem> {
|
||||
return listOf(
|
||||
ActionItem(R.drawable.ic_settings_24, context.getString(R.string.ContactSearchItems__story_settings)) {
|
||||
storyContextMenuCallbacks.onOpenStorySettings(model.story)
|
||||
},
|
||||
ActionItem(R.drawable.ic_delete_24, context.getString(R.string.ContactSearchItems__delete_story), R.color.signal_colorError) {
|
||||
storyContextMenuCallbacks.onDeletePrivateStory(model.story, model.isSelected)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -151,6 +199,7 @@ object ContactSearchItems {
|
||||
checkbox.visible = displayCheckBox
|
||||
checkbox.isChecked = isSelected(model)
|
||||
itemView.setOnClickListener { onClick(itemView, getData(model), isSelected(model)) }
|
||||
bindLongPress(model)
|
||||
|
||||
if (payload.isNotEmpty()) {
|
||||
return
|
||||
@@ -188,6 +237,8 @@ object ContactSearchItems {
|
||||
smsTag.visible = isSmsContact(model)
|
||||
}
|
||||
|
||||
protected open fun bindLongPress(model: T) = Unit
|
||||
|
||||
private fun isSmsContact(model: T): Boolean {
|
||||
return (getRecipient(model).isForceSmsSelection || getRecipient(model).isUnregistered) && !getRecipient(model).isDistributionList
|
||||
}
|
||||
@@ -271,4 +322,10 @@ object ContactSearchItems {
|
||||
return if (isLeftSelf == isRightSelf) 0 else if (isLeftSelf) 1 else -1
|
||||
}
|
||||
}
|
||||
|
||||
interface StoryContextMenuCallbacks {
|
||||
fun onOpenStorySettings(story: ContactSearchData.Story)
|
||||
fun onRemoveGroupStory(story: ContactSearchData.Story, isSelected: Boolean)
|
||||
fun onDeletePrivateStory(story: ContactSearchData.Story, isSelected: Boolean)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,16 +1,22 @@
|
||||
package org.thoughtcrime.securesms.contacts.paged
|
||||
|
||||
import android.view.View
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.lifecycle.LiveData
|
||||
import androidx.lifecycle.ViewModelProvider
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||
import org.thoughtcrime.securesms.R
|
||||
import org.thoughtcrime.securesms.groups.SelectionLimits
|
||||
import org.thoughtcrime.securesms.stories.settings.custom.PrivateStorySettingsFragment
|
||||
import org.thoughtcrime.securesms.stories.settings.my.MyStorySettingsFragment
|
||||
import org.thoughtcrime.securesms.util.SpanUtil
|
||||
import org.thoughtcrime.securesms.util.adapter.mapping.PagingMappingAdapter
|
||||
import org.thoughtcrime.securesms.util.livedata.LiveDataUtil
|
||||
|
||||
class ContactSearchMediator(
|
||||
fragment: Fragment,
|
||||
private val fragment: Fragment,
|
||||
recyclerView: RecyclerView,
|
||||
selectionLimits: SelectionLimits,
|
||||
displayCheckBox: Boolean,
|
||||
@@ -30,6 +36,7 @@ class ContactSearchMediator(
|
||||
displayCheckBox = displayCheckBox,
|
||||
recipientListener = this::toggleSelection,
|
||||
storyListener = this::toggleSelection,
|
||||
storyContextMenuCallbacks = StoryContextMenuCallbacks(),
|
||||
expandListener = { viewModel.expandSection(it.sectionKey) }
|
||||
)
|
||||
|
||||
@@ -76,6 +83,10 @@ class ContactSearchMediator(
|
||||
viewModel.addToVisibleGroupStories(groupStories)
|
||||
}
|
||||
|
||||
fun refresh() {
|
||||
viewModel.refresh()
|
||||
}
|
||||
|
||||
private fun toggleSelection(view: View, contactSearchData: ContactSearchData, isSelected: Boolean) {
|
||||
return if (isSelected) {
|
||||
viewModel.setKeysNotSelected(setOf(contactSearchData.contactSearchKey))
|
||||
@@ -83,4 +94,34 @@ class ContactSearchMediator(
|
||||
viewModel.setKeysSelected(contactSelectionPreFilter(view, setOf(contactSearchData.contactSearchKey)))
|
||||
}
|
||||
}
|
||||
|
||||
private inner class StoryContextMenuCallbacks : ContactSearchItems.StoryContextMenuCallbacks {
|
||||
override fun onOpenStorySettings(story: ContactSearchData.Story) {
|
||||
if (story.recipient.isMyStory) {
|
||||
MyStorySettingsFragment.createAsDialog()
|
||||
.show(fragment.childFragmentManager, null)
|
||||
} else {
|
||||
PrivateStorySettingsFragment.createAsDialog(story.recipient.requireDistributionListId())
|
||||
.show(fragment.childFragmentManager, null)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onRemoveGroupStory(story: ContactSearchData.Story, isSelected: Boolean) {
|
||||
MaterialAlertDialogBuilder(fragment.requireContext())
|
||||
.setTitle(R.string.ContactSearchMediator__remove_group_story)
|
||||
.setMessage(R.string.ContactSearchMediator__this_will_remove)
|
||||
.setPositiveButton(R.string.ContactSearchMediator__remove) { _, _ -> viewModel.removeGroupStory(story) }
|
||||
.setNegativeButton(android.R.string.cancel) { _, _ -> }
|
||||
.show()
|
||||
}
|
||||
|
||||
override fun onDeletePrivateStory(story: ContactSearchData.Story, isSelected: Boolean) {
|
||||
MaterialAlertDialogBuilder(fragment.requireContext())
|
||||
.setTitle(R.string.ContactSearchMediator__delete_story)
|
||||
.setMessage(fragment.getString(R.string.ContactSearchMediator__delete_the_private, story.recipient.getDisplayName(fragment.requireContext())))
|
||||
.setPositiveButton(SpanUtil.color(ContextCompat.getColor(fragment.requireContext(), R.color.signal_colorError), fragment.getString(R.string.ContactSearchMediator__delete))) { _, _ -> viewModel.deletePrivateStory(story) }
|
||||
.setNegativeButton(android.R.string.cancel) { _, _ -> }
|
||||
.show()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,9 +1,14 @@
|
||||
package org.thoughtcrime.securesms.contacts.paged
|
||||
|
||||
import io.reactivex.rxjava3.core.Completable
|
||||
import io.reactivex.rxjava3.core.Single
|
||||
import io.reactivex.rxjava3.schedulers.Schedulers
|
||||
import org.thoughtcrime.securesms.database.SignalDatabase
|
||||
import org.thoughtcrime.securesms.database.model.DistributionListId
|
||||
import org.thoughtcrime.securesms.groups.GroupId
|
||||
import org.thoughtcrime.securesms.recipients.Recipient
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId
|
||||
import org.thoughtcrime.securesms.stories.Stories
|
||||
|
||||
class ContactSearchRepository {
|
||||
fun filterOutUnselectableContactSearchKeys(contactSearchKeys: Set<ContactSearchKey>): Single<Set<ContactSearchSelectionResult>> {
|
||||
@@ -29,4 +34,17 @@ class ContactSearchRepository {
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
fun unmarkDisplayAsStory(groupId: GroupId): Completable {
|
||||
return Completable.fromAction {
|
||||
SignalDatabase.groups.markDisplayAsStory(groupId, false)
|
||||
}.subscribeOn(Schedulers.io())
|
||||
}
|
||||
|
||||
fun deletePrivateStory(distributionListId: DistributionListId): Completable {
|
||||
return Completable.fromAction {
|
||||
SignalDatabase.distributionLists.deleteList(distributionListId)
|
||||
Stories.onStorySettingsChanged(distributionListId)
|
||||
}.subscribeOn(Schedulers.io())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,6 +14,7 @@ import org.signal.paging.PagingController
|
||||
import org.thoughtcrime.securesms.groups.SelectionLimits
|
||||
import org.thoughtcrime.securesms.recipients.Recipient
|
||||
import org.thoughtcrime.securesms.util.livedata.Store
|
||||
import org.whispersystems.signalservice.api.util.Preconditions
|
||||
|
||||
/**
|
||||
* Simple, reusable view model that manages a ContactSearchPagedDataSource as well as filter and expansion state.
|
||||
@@ -97,6 +98,31 @@ class ContactSearchViewModel(
|
||||
}
|
||||
}
|
||||
|
||||
fun removeGroupStory(story: ContactSearchData.Story) {
|
||||
Preconditions.checkArgument(story.recipient.isGroup)
|
||||
setKeysNotSelected(setOf(story.contactSearchKey))
|
||||
disposables += contactSearchRepository.unmarkDisplayAsStory(story.recipient.requireGroupId()).subscribe {
|
||||
configurationStore.update { state ->
|
||||
state.copy(
|
||||
groupStories = state.groupStories.filter { it.recipient.id == story.recipient.id }.toSet()
|
||||
)
|
||||
}
|
||||
refresh()
|
||||
}
|
||||
}
|
||||
|
||||
fun deletePrivateStory(story: ContactSearchData.Story) {
|
||||
Preconditions.checkArgument(story.recipient.isDistributionList && !story.recipient.isMyStory)
|
||||
setKeysNotSelected(setOf(story.contactSearchKey))
|
||||
disposables += contactSearchRepository.deletePrivateStory(story.recipient.requireDistributionListId()).subscribe {
|
||||
refresh()
|
||||
}
|
||||
}
|
||||
|
||||
fun refresh() {
|
||||
controller.value?.onDataInvalidated()
|
||||
}
|
||||
|
||||
class Factory(private val selectionLimits: SelectionLimits, private val repository: ContactSearchRepository) : ViewModelProvider.Factory {
|
||||
override fun <T : ViewModel> create(modelClass: Class<T>): T {
|
||||
return modelClass.cast(ContactSearchViewModel(selectionLimits, repository)) as T
|
||||
|
||||
Reference in New Issue
Block a user