Add support for arbitrary rows in contact search.

This commit is contained in:
Alex Hart
2023-01-23 13:11:28 -04:00
committed by Greyson Parrelli
parent d76d13f76c
commit 5d14166a27
16 changed files with 598 additions and 480 deletions

View File

@@ -0,0 +1,20 @@
package org.thoughtcrime.securesms.contacts.paged
import org.thoughtcrime.securesms.util.adapter.mapping.MappingModel
interface ArbitraryRepository {
/**
* Get the count of arbitrary rows to include for the given query from the given section.
*/
fun getSize(section: ContactSearchConfiguration.Section.Arbitrary, query: String?): Int
/**
* Get the data for the given arbitrary rows within the start and end index.
*/
fun getData(section: ContactSearchConfiguration.Section.Arbitrary, query: String?, startIndex: Int, endIndex: Int): List<ContactSearchData.Arbitrary>
/**
* Map an arbitrary object to a mapping model
*/
fun getMappingModel(arbitrary: ContactSearchData.Arbitrary): MappingModel<*>
}

View File

@@ -1,23 +1,463 @@
package org.thoughtcrime.securesms.contacts.paged
import android.view.View
import android.view.ViewGroup
import android.widget.CheckBox
import android.widget.TextView
import com.google.android.material.button.MaterialButton
import org.signal.core.util.dp
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.contacts.LetterHeaderDecoration
import org.thoughtcrime.securesms.contacts.avatars.FallbackContactPhoto
import org.thoughtcrime.securesms.contacts.avatars.GeneratedContactPhoto
import org.thoughtcrime.securesms.database.model.DistributionListPrivacyMode
import org.thoughtcrime.securesms.keyvalue.SignalStore
import org.thoughtcrime.securesms.recipients.Recipient
import org.thoughtcrime.securesms.util.adapter.mapping.LayoutFactory
import org.thoughtcrime.securesms.util.adapter.mapping.MappingAdapter
import org.thoughtcrime.securesms.util.adapter.mapping.MappingModel
import org.thoughtcrime.securesms.util.adapter.mapping.MappingModelList
import org.thoughtcrime.securesms.util.adapter.mapping.MappingViewHolder
import org.thoughtcrime.securesms.util.adapter.mapping.PagingMappingAdapter
import org.thoughtcrime.securesms.util.visible
/**
* Default contact search adapter, using the models defined in `ContactSearchItems`
*/
class ContactSearchAdapter(
@Suppress("LeakingThis")
open class ContactSearchAdapter(
displayCheckBox: Boolean,
displaySmsTag: ContactSearchItems.DisplaySmsTag,
displaySmsTag: DisplaySmsTag,
recipientListener: (View, ContactSearchData.KnownRecipient, Boolean) -> Unit,
storyListener: (View, ContactSearchData.Story, Boolean) -> Unit,
storyContextMenuCallbacks: ContactSearchItems.StoryContextMenuCallbacks,
storyContextMenuCallbacks: StoryContextMenuCallbacks,
expandListener: (ContactSearchData.Expand) -> Unit
) : PagingMappingAdapter<ContactSearchKey>() {
init {
ContactSearchItems.registerStoryItems(this, displayCheckBox, storyListener, storyContextMenuCallbacks)
ContactSearchItems.registerKnownRecipientItems(this, displayCheckBox, displaySmsTag, recipientListener)
ContactSearchItems.registerHeaders(this)
ContactSearchItems.registerExpands(this, expandListener)
registerStoryItems(this, displayCheckBox, storyListener, storyContextMenuCallbacks)
registerKnownRecipientItems(this, displayCheckBox, displaySmsTag, recipientListener)
registerHeaders(this)
registerExpands(this, expandListener)
}
companion object {
fun registerStoryItems(
mappingAdapter: MappingAdapter,
displayCheckBox: Boolean = false,
storyListener: (View, ContactSearchData.Story, Boolean) -> Unit,
storyContextMenuCallbacks: StoryContextMenuCallbacks? = null
) {
mappingAdapter.registerFactory(
StoryModel::class.java,
LayoutFactory({ StoryViewHolder(it, displayCheckBox, storyListener, storyContextMenuCallbacks) }, R.layout.contact_search_item)
)
}
fun registerKnownRecipientItems(
mappingAdapter: MappingAdapter,
displayCheckBox: Boolean,
displaySmsTag: DisplaySmsTag,
recipientListener: (View, ContactSearchData.KnownRecipient, Boolean) -> Unit
) {
mappingAdapter.registerFactory(
RecipientModel::class.java,
LayoutFactory({ KnownRecipientViewHolder(it, displayCheckBox, displaySmsTag, recipientListener) }, R.layout.contact_search_item)
)
}
fun registerHeaders(mappingAdapter: MappingAdapter) {
mappingAdapter.registerFactory(
HeaderModel::class.java,
LayoutFactory({ HeaderViewHolder(it) }, R.layout.contact_search_section_header)
)
}
fun registerExpands(mappingAdapter: MappingAdapter, expandListener: (ContactSearchData.Expand) -> Unit) {
mappingAdapter.registerFactory(
ExpandModel::class.java,
LayoutFactory({ ExpandViewHolder(it, expandListener) }, R.layout.contacts_expand_item)
)
}
fun toMappingModelList(contactSearchData: List<ContactSearchData?>, selection: Set<ContactSearchKey>, arbitraryRepository: ArbitraryRepository?): MappingModelList {
return MappingModelList(
contactSearchData.filterNotNull().map {
when (it) {
is ContactSearchData.Story -> StoryModel(it, selection.contains(it.contactSearchKey), SignalStore.storyValues().userHasBeenNotifiedAboutStories)
is ContactSearchData.KnownRecipient -> RecipientModel(it, selection.contains(it.contactSearchKey), it.shortSummary)
is ContactSearchData.Expand -> ExpandModel(it)
is ContactSearchData.Header -> HeaderModel(it)
is ContactSearchData.TestRow -> error("This row exists for testing only.")
is ContactSearchData.Arbitrary -> arbitraryRepository?.getMappingModel(it) ?: error("This row must be handled manually")
}
}
)
}
}
/**
* Story Model
*/
class StoryModel(val story: ContactSearchData.Story, val isSelected: Boolean, val hasBeenNotified: Boolean) : MappingModel<StoryModel> {
override fun areItemsTheSame(newItem: StoryModel): Boolean {
return newItem.story == story
}
override fun areContentsTheSame(newItem: StoryModel): Boolean {
return story.recipient.hasSameContent(newItem.story.recipient) &&
isSelected == newItem.isSelected &&
hasBeenNotified == newItem.hasBeenNotified
}
override fun getChangePayload(newItem: StoryModel): Any? {
return if (story.recipient.hasSameContent(newItem.story.recipient) &&
hasBeenNotified == newItem.hasBeenNotified &&
newItem.isSelected != isSelected
) {
0
} else {
null
}
}
}
private class StoryViewHolder(
itemView: View,
displayCheckBox: Boolean,
onClick: (View, ContactSearchData.Story, Boolean) -> Unit,
private val storyContextMenuCallbacks: StoryContextMenuCallbacks?
) : BaseRecipientViewHolder<StoryModel, ContactSearchData.Story>(itemView, displayCheckBox, DisplaySmsTag.NEVER, 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
override fun bindNumberField(model: StoryModel) {
number.visible = true
val count = if (model.story.recipient.isGroup) {
model.story.recipient.participantIds.size
} else {
model.story.count
}
if (model.story.recipient.isMyStory && !model.hasBeenNotified) {
number.setText(R.string.ContactSearchItems__tap_to_choose_your_viewers)
} else {
number.text = when {
model.story.recipient.isGroup -> context.resources.getQuantityString(R.plurals.ContactSearchItems__group_story_d_viewers, count, count)
model.story.recipient.isMyStory -> {
if (model.story.privacyMode == DistributionListPrivacyMode.ALL_EXCEPT) {
context.resources.getQuantityString(R.plurals.ContactSearchItems__my_story_s_dot_d_excluded, count, presentPrivacyMode(DistributionListPrivacyMode.ALL), count)
} else {
context.resources.getQuantityString(R.plurals.ContactSearchItems__my_story_s_dot_d_viewers, count, presentPrivacyMode(model.story.privacyMode), count)
}
}
else -> context.resources.getQuantityString(R.plurals.ContactSearchItems__custom_story_d_viewers, count, count)
}
}
}
override fun bindAvatar(model: StoryModel) {
if (model.story.recipient.isMyStory) {
avatar.setFallbackPhotoProvider(MyStoryFallbackPhotoProvider(Recipient.self().getDisplayName(context), 40.dp))
avatar.setAvatarUsingProfile(Recipient.self())
} else {
avatar.setFallbackPhotoProvider(Recipient.DEFAULT_FALLBACK_PHOTO_PROVIDER)
super.bindAvatar(model)
}
}
override fun bindLongPress(model: StoryModel) {
if (storyContextMenuCallbacks == null) {
return
}
itemView.setOnLongClickListener {
val actions: List<ActionItem> = when {
model.story.recipient.isMyStory -> getMyStoryContextMenuActions(model, storyContextMenuCallbacks)
model.story.recipient.isGroup -> getGroupStoryContextMenuActions(model, storyContextMenuCallbacks)
model.story.recipient.isDistributionList -> getPrivateStoryContextMenuActions(model, storyContextMenuCallbacks)
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, callbacks: StoryContextMenuCallbacks): List<ActionItem> {
return listOf(
ActionItem(R.drawable.ic_settings_24, context.getString(R.string.ContactSearchItems__story_settings)) {
callbacks.onOpenStorySettings(model.story)
}
)
}
private fun getGroupStoryContextMenuActions(model: StoryModel, callbacks: StoryContextMenuCallbacks): List<ActionItem> {
return listOf(
ActionItem(R.drawable.ic_minus_circle_20, context.getString(R.string.ContactSearchItems__remove_story)) {
callbacks.onRemoveGroupStory(model.story, model.isSelected)
}
)
}
private fun getPrivateStoryContextMenuActions(model: StoryModel, callbacks: StoryContextMenuCallbacks): List<ActionItem> {
return listOf(
ActionItem(R.drawable.ic_settings_24, context.getString(R.string.ContactSearchItems__story_settings)) {
callbacks.onOpenStorySettings(model.story)
},
ActionItem(R.drawable.ic_delete_24, context.getString(R.string.ContactSearchItems__delete_story), R.color.signal_colorError) {
callbacks.onDeletePrivateStory(model.story, model.isSelected)
}
)
}
private fun presentPrivacyMode(privacyMode: DistributionListPrivacyMode): String {
return when (privacyMode) {
DistributionListPrivacyMode.ONLY_WITH -> context.getString(R.string.ContactSearchItems__only_share_with)
DistributionListPrivacyMode.ALL_EXCEPT -> context.getString(R.string.ChooseInitialMyStoryMembershipFragment__all_except)
DistributionListPrivacyMode.ALL -> context.getString(R.string.ChooseInitialMyStoryMembershipFragment__all_signal_connections)
}
}
private class MyStoryFallbackPhotoProvider(private val name: String, private val targetSize: Int) : Recipient.FallbackPhotoProvider() {
override fun getPhotoForLocalNumber(): FallbackContactPhoto {
return GeneratedContactPhoto(name, R.drawable.ic_profile_outline_40, targetSize)
}
}
}
/**
* Recipient model
*/
class RecipientModel(val knownRecipient: ContactSearchData.KnownRecipient, val isSelected: Boolean, val shortSummary: Boolean) : MappingModel<RecipientModel> {
override fun areItemsTheSame(newItem: RecipientModel): Boolean {
return newItem.knownRecipient == knownRecipient
}
override fun areContentsTheSame(newItem: RecipientModel): Boolean {
return knownRecipient.recipient.hasSameContent(newItem.knownRecipient.recipient) && isSelected == newItem.isSelected
}
override fun getChangePayload(newItem: RecipientModel): Any? {
return if (knownRecipient.recipient.hasSameContent(newItem.knownRecipient.recipient) && newItem.isSelected != isSelected) {
0
} else {
null
}
}
}
private class KnownRecipientViewHolder(
itemView: View,
displayCheckBox: Boolean,
displaySmsTag: DisplaySmsTag,
onClick: (View, ContactSearchData.KnownRecipient, Boolean) -> Unit
) : BaseRecipientViewHolder<RecipientModel, ContactSearchData.KnownRecipient>(itemView, displayCheckBox, displaySmsTag, onClick), LetterHeaderDecoration.LetterHeaderItem {
private var headerLetter: String? = null
override fun isSelected(model: RecipientModel): Boolean = model.isSelected
override fun getData(model: RecipientModel): ContactSearchData.KnownRecipient = model.knownRecipient
override fun getRecipient(model: RecipientModel): Recipient = model.knownRecipient.recipient
override fun bindNumberField(model: RecipientModel) {
val recipient = getRecipient(model)
if (model.shortSummary && recipient.isGroup) {
val count = recipient.participantIds.size
number.text = context.resources.getQuantityString(R.plurals.ContactSearchItems__group_d_members, count, count)
} else {
super.bindNumberField(model)
}
headerLetter = model.knownRecipient.headerLetter
}
override fun getHeaderLetter(): String? {
return headerLetter
}
}
/**
* Base Recipient View Holder
*/
private abstract class BaseRecipientViewHolder<T : MappingModel<T>, D : ContactSearchData>(
itemView: View,
private val displayCheckBox: Boolean,
private val displaySmsTag: DisplaySmsTag,
val onClick: (View, D, Boolean) -> Unit
) : MappingViewHolder<T>(itemView) {
protected val avatar: AvatarImageView = itemView.findViewById(R.id.contact_photo_image)
protected val badge: BadgeImageView = itemView.findViewById(R.id.contact_badge)
protected val checkbox: CheckBox = itemView.findViewById(R.id.check_box)
protected val name: FromTextView = itemView.findViewById(R.id.name)
protected val number: TextView = itemView.findViewById(R.id.number)
protected val label: TextView = itemView.findViewById(R.id.label)
protected val smsTag: View = itemView.findViewById(R.id.sms_tag)
override fun bind(model: T) {
checkbox.visible = displayCheckBox
checkbox.isChecked = isSelected(model)
itemView.setOnClickListener { onClick(avatar, getData(model), isSelected(model)) }
bindLongPress(model)
if (payload.isNotEmpty()) {
return
}
name.setText(getRecipient(model))
badge.setBadgeFromRecipient(getRecipient(model))
bindAvatar(model)
bindNumberField(model)
bindLabelField(model)
bindSmsTagField(model)
}
protected open fun bindAvatar(model: T) {
avatar.setAvatar(getRecipient(model))
}
protected open fun bindNumberField(model: T) {
number.visible = getRecipient(model).isGroup
if (getRecipient(model).isGroup) {
number.text = getRecipient(model).participantIds
.take(10)
.map { id -> Recipient.resolved(id) }
.sortedWith(IsSelfComparator()).joinToString(", ") {
if (it.isSelf) {
context.getString(R.string.ConversationTitleView_you)
} else {
it.getShortDisplayName(context)
}
}
}
}
protected open fun bindLabelField(model: T) {
label.visible = false
}
protected open fun bindSmsTagField(model: T) {
smsTag.visible = when (displaySmsTag) {
DisplaySmsTag.DEFAULT -> isSmsContact(model)
DisplaySmsTag.IF_NOT_REGISTERED -> isNotRegistered(model)
DisplaySmsTag.NEVER -> false
}
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
}
private fun isNotRegistered(model: T): Boolean {
return getRecipient(model).isUnregistered && !getRecipient(model).isDistributionList
}
abstract fun isSelected(model: T): Boolean
abstract fun getData(model: T): D
abstract fun getRecipient(model: T): Recipient
}
/**
* Mapping Model for section headers
*/
class HeaderModel(val header: ContactSearchData.Header) : MappingModel<HeaderModel> {
override fun areItemsTheSame(newItem: HeaderModel): Boolean {
return header.sectionKey == newItem.header.sectionKey
}
override fun areContentsTheSame(newItem: HeaderModel): Boolean {
return areItemsTheSame(newItem) &&
header.action?.icon == newItem.header.action?.icon &&
header.action?.label == newItem.header.action?.label
}
}
/**
* View Holder for section headers
*/
private class HeaderViewHolder(itemView: View) : MappingViewHolder<HeaderModel>(itemView) {
private val headerTextView: TextView = itemView.findViewById(R.id.section_header)
private val headerActionView: MaterialButton = itemView.findViewById(R.id.section_header_action)
override fun bind(model: HeaderModel) {
headerTextView.setText(
when (model.header.sectionKey) {
ContactSearchConfiguration.SectionKey.STORIES -> R.string.ContactsCursorLoader_my_stories
ContactSearchConfiguration.SectionKey.RECENTS -> R.string.ContactsCursorLoader_recent_chats
ContactSearchConfiguration.SectionKey.INDIVIDUALS -> R.string.ContactsCursorLoader_contacts
ContactSearchConfiguration.SectionKey.GROUPS -> R.string.ContactsCursorLoader_groups
ContactSearchConfiguration.SectionKey.ARBITRARY -> error("This section does not support HEADER")
}
)
if (model.header.action != null) {
headerActionView.visible = true
headerActionView.setIconResource(model.header.action.icon)
headerActionView.setText(model.header.action.label)
headerActionView.setOnClickListener { model.header.action.action.run() }
} else {
headerActionView.visible = false
}
}
}
/**
* Mapping Model for expandable content rows.
*/
class ExpandModel(val expand: ContactSearchData.Expand) : MappingModel<ExpandModel> {
override fun areItemsTheSame(newItem: ExpandModel): Boolean {
return expand.contactSearchKey == newItem.expand.contactSearchKey
}
override fun areContentsTheSame(newItem: ExpandModel): Boolean {
return areItemsTheSame(newItem)
}
}
/**
* View Holder for expandable content rows.
*/
private class ExpandViewHolder(itemView: View, private val expandListener: (ContactSearchData.Expand) -> Unit) : MappingViewHolder<ExpandModel>(itemView) {
override fun bind(model: ExpandModel) {
itemView.setOnClickListener { expandListener.invoke(model.expand) }
}
}
private class IsSelfComparator : Comparator<Recipient> {
override fun compare(lhs: Recipient?, rhs: Recipient?): Int {
val isLeftSelf = lhs?.isSelf == true
val isRightSelf = rhs?.isSelf == true
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)
}
enum class DisplaySmsTag {
DEFAULT,
IF_NOT_REGISTERED,
NEVER
}
}

View File

@@ -69,6 +69,13 @@ class ContactSearchConfiguration private constructor(
override val includeHeader: Boolean,
override val expandConfig: ExpandConfig? = null
) : Section(SectionKey.GROUPS)
data class Arbitrary(
val types: Set<String>
) : Section(SectionKey.ARBITRARY) {
override val includeHeader: Boolean = false
override val expandConfig: ExpandConfig? = null
}
}
/**
@@ -78,7 +85,8 @@ class ContactSearchConfiguration private constructor(
STORIES,
RECENTS,
INDIVIDUALS,
GROUPS
GROUPS,
ARBITRARY
}
/**
@@ -139,6 +147,10 @@ class ContactSearchConfiguration private constructor(
*/
interface Builder {
var query: String?
fun arbitrary(first: String, vararg rest: String) {
addSection(Section.Arbitrary(setOf(first) + rest.toSet()))
}
fun addSection(section: Section)
}
}

View File

@@ -1,5 +1,6 @@
package org.thoughtcrime.securesms.contacts.paged
import android.os.Bundle
import androidx.annotation.VisibleForTesting
import org.thoughtcrime.securesms.contacts.HeaderAction
import org.thoughtcrime.securesms.database.model.DistributionListPrivacyMode
@@ -42,6 +43,11 @@ sealed class ContactSearchData(val contactSearchKey: ContactSearchKey) {
*/
class Expand(val sectionKey: ContactSearchConfiguration.SectionKey) : ContactSearchData(ContactSearchKey.Expand(sectionKey))
/**
* A row representing arbitrary data tied to a specific section.
*/
class Arbitrary(val type: String, val data: Bundle? = null) : ContactSearchData(ContactSearchKey.Arbitrary(type))
/**
* A row which contains an integer, for testing.
*/

View File

@@ -1,447 +0,0 @@
package org.thoughtcrime.securesms.contacts.paged
import android.view.View
import android.view.ViewGroup
import android.widget.CheckBox
import android.widget.TextView
import com.google.android.material.button.MaterialButton
import org.signal.core.util.dp
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.contacts.LetterHeaderDecoration
import org.thoughtcrime.securesms.contacts.avatars.FallbackContactPhoto
import org.thoughtcrime.securesms.contacts.avatars.GeneratedContactPhoto
import org.thoughtcrime.securesms.database.model.DistributionListPrivacyMode
import org.thoughtcrime.securesms.keyvalue.SignalStore
import org.thoughtcrime.securesms.recipients.Recipient
import org.thoughtcrime.securesms.recipients.Recipient.FallbackPhotoProvider
import org.thoughtcrime.securesms.util.adapter.mapping.LayoutFactory
import org.thoughtcrime.securesms.util.adapter.mapping.MappingAdapter
import org.thoughtcrime.securesms.util.adapter.mapping.MappingModel
import org.thoughtcrime.securesms.util.adapter.mapping.MappingModelList
import org.thoughtcrime.securesms.util.adapter.mapping.MappingViewHolder
import org.thoughtcrime.securesms.util.visible
private typealias StoryClickListener = (View, ContactSearchData.Story, Boolean) -> Unit
private typealias RecipientClickListener = (View, ContactSearchData.KnownRecipient, Boolean) -> Unit
/**
* Mapping Models and View Holders for ContactSearchData
*/
object ContactSearchItems {
fun registerStoryItems(
mappingAdapter: MappingAdapter,
displayCheckBox: Boolean = false,
storyListener: StoryClickListener,
storyContextMenuCallbacks: StoryContextMenuCallbacks? = null
) {
mappingAdapter.registerFactory(
StoryModel::class.java,
LayoutFactory({ StoryViewHolder(it, displayCheckBox, storyListener, storyContextMenuCallbacks) }, R.layout.contact_search_item)
)
}
fun registerKnownRecipientItems(
mappingAdapter: MappingAdapter,
displayCheckBox: Boolean,
displaySmsTag: DisplaySmsTag,
recipientListener: (View, ContactSearchData.KnownRecipient, Boolean) -> Unit
) {
mappingAdapter.registerFactory(
RecipientModel::class.java,
LayoutFactory({ KnownRecipientViewHolder(it, displayCheckBox, displaySmsTag, recipientListener) }, R.layout.contact_search_item)
)
}
fun registerHeaders(mappingAdapter: MappingAdapter) {
mappingAdapter.registerFactory(
HeaderModel::class.java,
LayoutFactory({ HeaderViewHolder(it) }, R.layout.contact_search_section_header)
)
}
fun registerExpands(mappingAdapter: MappingAdapter, expandListener: (ContactSearchData.Expand) -> Unit) {
mappingAdapter.registerFactory(
ExpandModel::class.java,
LayoutFactory({ ExpandViewHolder(it, expandListener) }, R.layout.contacts_expand_item)
)
}
fun toMappingModelList(contactSearchData: List<ContactSearchData?>, selection: Set<ContactSearchKey>): MappingModelList {
return MappingModelList(
contactSearchData.filterNotNull().map {
when (it) {
is ContactSearchData.Story -> StoryModel(it, selection.contains(it.contactSearchKey), SignalStore.storyValues().userHasBeenNotifiedAboutStories)
is ContactSearchData.KnownRecipient -> RecipientModel(it, selection.contains(it.contactSearchKey), it.shortSummary)
is ContactSearchData.Expand -> ExpandModel(it)
is ContactSearchData.Header -> HeaderModel(it)
is ContactSearchData.TestRow -> error("This row exists for testing only.")
}
}
)
}
/**
* Story Model
*/
class StoryModel(val story: ContactSearchData.Story, val isSelected: Boolean, val hasBeenNotified: Boolean) : MappingModel<StoryModel> {
override fun areItemsTheSame(newItem: StoryModel): Boolean {
return newItem.story == story
}
override fun areContentsTheSame(newItem: StoryModel): Boolean {
return story.recipient.hasSameContent(newItem.story.recipient) &&
isSelected == newItem.isSelected &&
hasBeenNotified == newItem.hasBeenNotified
}
override fun getChangePayload(newItem: StoryModel): Any? {
return if (story.recipient.hasSameContent(newItem.story.recipient) &&
hasBeenNotified == newItem.hasBeenNotified &&
newItem.isSelected != isSelected
) {
0
} else {
null
}
}
}
private class StoryViewHolder(
itemView: View,
displayCheckBox: Boolean,
onClick: StoryClickListener,
private val storyContextMenuCallbacks: StoryContextMenuCallbacks?
) : BaseRecipientViewHolder<StoryModel, ContactSearchData.Story>(itemView, displayCheckBox, DisplaySmsTag.NEVER, 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
override fun bindNumberField(model: StoryModel) {
number.visible = true
val count = if (model.story.recipient.isGroup) {
model.story.recipient.participantIds.size
} else {
model.story.count
}
if (model.story.recipient.isMyStory && !model.hasBeenNotified) {
number.setText(R.string.ContactSearchItems__tap_to_choose_your_viewers)
} else {
number.text = when {
model.story.recipient.isGroup -> context.resources.getQuantityString(R.plurals.ContactSearchItems__group_story_d_viewers, count, count)
model.story.recipient.isMyStory -> {
if (model.story.privacyMode == DistributionListPrivacyMode.ALL_EXCEPT) {
context.resources.getQuantityString(R.plurals.ContactSearchItems__my_story_s_dot_d_excluded, count, presentPrivacyMode(DistributionListPrivacyMode.ALL), count)
} else {
context.resources.getQuantityString(R.plurals.ContactSearchItems__my_story_s_dot_d_viewers, count, presentPrivacyMode(model.story.privacyMode), count)
}
}
else -> context.resources.getQuantityString(R.plurals.ContactSearchItems__custom_story_d_viewers, count, count)
}
}
}
override fun bindAvatar(model: StoryModel) {
if (model.story.recipient.isMyStory) {
avatar.setFallbackPhotoProvider(MyStoryFallbackPhotoProvider(Recipient.self().getDisplayName(context), 40.dp))
avatar.setAvatarUsingProfile(Recipient.self())
} else {
avatar.setFallbackPhotoProvider(Recipient.DEFAULT_FALLBACK_PHOTO_PROVIDER)
super.bindAvatar(model)
}
}
override fun bindLongPress(model: StoryModel) {
if (storyContextMenuCallbacks == null) {
return
}
itemView.setOnLongClickListener {
val actions: List<ActionItem> = when {
model.story.recipient.isMyStory -> getMyStoryContextMenuActions(model, storyContextMenuCallbacks)
model.story.recipient.isGroup -> getGroupStoryContextMenuActions(model, storyContextMenuCallbacks)
model.story.recipient.isDistributionList -> getPrivateStoryContextMenuActions(model, storyContextMenuCallbacks)
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, callbacks: StoryContextMenuCallbacks): List<ActionItem> {
return listOf(
ActionItem(R.drawable.ic_settings_24, context.getString(R.string.ContactSearchItems__story_settings)) {
callbacks.onOpenStorySettings(model.story)
}
)
}
private fun getGroupStoryContextMenuActions(model: StoryModel, callbacks: StoryContextMenuCallbacks): List<ActionItem> {
return listOf(
ActionItem(R.drawable.ic_minus_circle_20, context.getString(R.string.ContactSearchItems__remove_story)) {
callbacks.onRemoveGroupStory(model.story, model.isSelected)
}
)
}
private fun getPrivateStoryContextMenuActions(model: StoryModel, callbacks: StoryContextMenuCallbacks): List<ActionItem> {
return listOf(
ActionItem(R.drawable.ic_settings_24, context.getString(R.string.ContactSearchItems__story_settings)) {
callbacks.onOpenStorySettings(model.story)
},
ActionItem(R.drawable.ic_delete_24, context.getString(R.string.ContactSearchItems__delete_story), R.color.signal_colorError) {
callbacks.onDeletePrivateStory(model.story, model.isSelected)
}
)
}
private fun presentPrivacyMode(privacyMode: DistributionListPrivacyMode): String {
return when (privacyMode) {
DistributionListPrivacyMode.ONLY_WITH -> context.getString(R.string.ContactSearchItems__only_share_with)
DistributionListPrivacyMode.ALL_EXCEPT -> context.getString(R.string.ChooseInitialMyStoryMembershipFragment__all_except)
DistributionListPrivacyMode.ALL -> context.getString(R.string.ChooseInitialMyStoryMembershipFragment__all_signal_connections)
}
}
private class MyStoryFallbackPhotoProvider(private val name: String, private val targetSize: Int) : FallbackPhotoProvider() {
override fun getPhotoForLocalNumber(): FallbackContactPhoto {
return GeneratedContactPhoto(name, R.drawable.ic_profile_outline_40, targetSize)
}
}
}
/**
* Recipient model
*/
class RecipientModel(val knownRecipient: ContactSearchData.KnownRecipient, val isSelected: Boolean, val shortSummary: Boolean) : MappingModel<RecipientModel> {
override fun areItemsTheSame(newItem: RecipientModel): Boolean {
return newItem.knownRecipient == knownRecipient
}
override fun areContentsTheSame(newItem: RecipientModel): Boolean {
return knownRecipient.recipient.hasSameContent(newItem.knownRecipient.recipient) && isSelected == newItem.isSelected
}
override fun getChangePayload(newItem: RecipientModel): Any? {
return if (knownRecipient.recipient.hasSameContent(newItem.knownRecipient.recipient) && newItem.isSelected != isSelected) {
0
} else {
null
}
}
}
private class KnownRecipientViewHolder(
itemView: View,
displayCheckBox: Boolean,
displaySmsTag: DisplaySmsTag,
onClick: RecipientClickListener
) : BaseRecipientViewHolder<RecipientModel, ContactSearchData.KnownRecipient>(itemView, displayCheckBox, displaySmsTag, onClick), LetterHeaderDecoration.LetterHeaderItem {
private var headerLetter: String? = null
override fun isSelected(model: RecipientModel): Boolean = model.isSelected
override fun getData(model: RecipientModel): ContactSearchData.KnownRecipient = model.knownRecipient
override fun getRecipient(model: RecipientModel): Recipient = model.knownRecipient.recipient
override fun bindNumberField(model: RecipientModel) {
val recipient = getRecipient(model)
if (model.shortSummary && recipient.isGroup) {
val count = recipient.participantIds.size
number.text = context.resources.getQuantityString(R.plurals.ContactSearchItems__group_d_members, count, count)
} else {
super.bindNumberField(model)
}
headerLetter = model.knownRecipient.headerLetter
}
override fun getHeaderLetter(): String? {
return headerLetter
}
}
/**
* Base Recipient View Holder
*/
private abstract class BaseRecipientViewHolder<T : MappingModel<T>, D : ContactSearchData>(
itemView: View,
private val displayCheckBox: Boolean,
private val displaySmsTag: DisplaySmsTag,
val onClick: (View, D, Boolean) -> Unit
) : MappingViewHolder<T>(itemView) {
protected val avatar: AvatarImageView = itemView.findViewById(R.id.contact_photo_image)
protected val badge: BadgeImageView = itemView.findViewById(R.id.contact_badge)
protected val checkbox: CheckBox = itemView.findViewById(R.id.check_box)
protected val name: FromTextView = itemView.findViewById(R.id.name)
protected val number: TextView = itemView.findViewById(R.id.number)
protected val label: TextView = itemView.findViewById(R.id.label)
protected val smsTag: View = itemView.findViewById(R.id.sms_tag)
override fun bind(model: T) {
checkbox.visible = displayCheckBox
checkbox.isChecked = isSelected(model)
itemView.setOnClickListener { onClick(avatar, getData(model), isSelected(model)) }
bindLongPress(model)
if (payload.isNotEmpty()) {
return
}
name.setText(getRecipient(model))
badge.setBadgeFromRecipient(getRecipient(model))
bindAvatar(model)
bindNumberField(model)
bindLabelField(model)
bindSmsTagField(model)
}
protected open fun bindAvatar(model: T) {
avatar.setAvatar(getRecipient(model))
}
protected open fun bindNumberField(model: T) {
number.visible = getRecipient(model).isGroup
if (getRecipient(model).isGroup) {
number.text = getRecipient(model).participantIds
.take(10)
.map { id -> Recipient.resolved(id) }
.sortedWith(IsSelfComparator()).joinToString(", ") {
if (it.isSelf) {
context.getString(R.string.ConversationTitleView_you)
} else {
it.getShortDisplayName(context)
}
}
}
}
protected open fun bindLabelField(model: T) {
label.visible = false
}
protected open fun bindSmsTagField(model: T) {
smsTag.visible = when (displaySmsTag) {
DisplaySmsTag.DEFAULT -> isSmsContact(model)
DisplaySmsTag.IF_NOT_REGISTERED -> isNotRegistered(model)
DisplaySmsTag.NEVER -> false
}
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
}
private fun isNotRegistered(model: T): Boolean {
return getRecipient(model).isUnregistered && !getRecipient(model).isDistributionList
}
abstract fun isSelected(model: T): Boolean
abstract fun getData(model: T): D
abstract fun getRecipient(model: T): Recipient
}
/**
* Mapping Model for section headers
*/
class HeaderModel(val header: ContactSearchData.Header) : MappingModel<HeaderModel> {
override fun areItemsTheSame(newItem: HeaderModel): Boolean {
return header.sectionKey == newItem.header.sectionKey
}
override fun areContentsTheSame(newItem: HeaderModel): Boolean {
return areItemsTheSame(newItem) &&
header.action?.icon == newItem.header.action?.icon &&
header.action?.label == newItem.header.action?.label
}
}
/**
* View Holder for section headers
*/
private class HeaderViewHolder(itemView: View) : MappingViewHolder<HeaderModel>(itemView) {
private val headerTextView: TextView = itemView.findViewById(R.id.section_header)
private val headerActionView: MaterialButton = itemView.findViewById(R.id.section_header_action)
override fun bind(model: HeaderModel) {
headerTextView.setText(
when (model.header.sectionKey) {
ContactSearchConfiguration.SectionKey.STORIES -> R.string.ContactsCursorLoader_my_stories
ContactSearchConfiguration.SectionKey.RECENTS -> R.string.ContactsCursorLoader_recent_chats
ContactSearchConfiguration.SectionKey.INDIVIDUALS -> R.string.ContactsCursorLoader_contacts
ContactSearchConfiguration.SectionKey.GROUPS -> R.string.ContactsCursorLoader_groups
}
)
if (model.header.action != null) {
headerActionView.visible = true
headerActionView.setIconResource(model.header.action.icon)
headerActionView.setText(model.header.action.label)
headerActionView.setOnClickListener { model.header.action.action.run() }
} else {
headerActionView.visible = false
}
}
}
/**
* Mapping Model for expandable content rows.
*/
class ExpandModel(val expand: ContactSearchData.Expand) : MappingModel<ExpandModel> {
override fun areItemsTheSame(newItem: ExpandModel): Boolean {
return expand.contactSearchKey == newItem.expand.contactSearchKey
}
override fun areContentsTheSame(newItem: ExpandModel): Boolean {
return areItemsTheSame(newItem)
}
}
/**
* View Holder for expandable content rows.
*/
private class ExpandViewHolder(itemView: View, private val expandListener: (ContactSearchData.Expand) -> Unit) : MappingViewHolder<ExpandModel>(itemView) {
override fun bind(model: ExpandModel) {
itemView.setOnClickListener { expandListener.invoke(model.expand) }
}
}
private class IsSelfComparator : Comparator<Recipient> {
override fun compare(lhs: Recipient?, rhs: Recipient?): Int {
val isLeftSelf = lhs?.isSelf == true
val isRightSelf = rhs?.isSelf == true
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)
}
enum class DisplaySmsTag {
DEFAULT,
IF_NOT_REGISTERED,
NEVER
}
}

View File

@@ -35,4 +35,11 @@ sealed class ContactSearchKey {
* Key to an expand button for a given section
*/
data class Expand(val sectionKey: ContactSearchConfiguration.SectionKey) : ContactSearchKey()
/**
* Arbitrary takes a string type and will map to exactly one ArbitraryData object.
*
* This is used to allow arbitrary extra data to be added to the contact search system.
*/
data class Arbitrary(val type: String) : ContactSearchKey()
}

View File

@@ -24,14 +24,18 @@ class ContactSearchMediator(
recyclerView: RecyclerView,
selectionLimits: SelectionLimits,
displayCheckBox: Boolean,
displaySmsTag: ContactSearchItems.DisplaySmsTag,
displaySmsTag: ContactSearchAdapter.DisplaySmsTag,
mapStateToConfiguration: (ContactSearchState) -> ContactSearchConfiguration,
private val contactSelectionPreFilter: (View?, Set<ContactSearchKey>) -> Set<ContactSearchKey> = { _, s -> s },
performSafetyNumberChecks: Boolean = true,
adapterFactory: AdapterFactory = DefaultAdapterFactory
adapterFactory: AdapterFactory = DefaultAdapterFactory,
arbitraryRepository: ArbitraryRepository? = null
) {
private val viewModel: ContactSearchViewModel = ViewModelProvider(fragment, ContactSearchViewModel.Factory(selectionLimits, ContactSearchRepository(), performSafetyNumberChecks)).get(ContactSearchViewModel::class.java)
private val viewModel: ContactSearchViewModel = ViewModelProvider(
fragment,
ContactSearchViewModel.Factory(selectionLimits, ContactSearchRepository(), performSafetyNumberChecks, arbitraryRepository)
)[ContactSearchViewModel::class.java]
init {
val adapter = adapterFactory.create(
@@ -51,7 +55,7 @@ class ContactSearchMediator(
)
dataAndSelection.observe(fragment.viewLifecycleOwner) { (data, selection) ->
adapter.submitList(ContactSearchItems.toMappingModelList(data, selection))
adapter.submitList(ContactSearchAdapter.toMappingModelList(data, selection, arbitraryRepository))
}
viewModel.controller.observe(fragment.viewLifecycleOwner) { controller ->
@@ -111,7 +115,7 @@ class ContactSearchMediator(
}
}
private inner class StoryContextMenuCallbacks : ContactSearchItems.StoryContextMenuCallbacks {
private inner class StoryContextMenuCallbacks : ContactSearchAdapter.StoryContextMenuCallbacks {
override fun onOpenStorySettings(story: ContactSearchData.Story) {
if (story.recipient.isMyStory) {
MyStorySettingsFragment.createAsDialog()
@@ -148,10 +152,10 @@ class ContactSearchMediator(
fun interface AdapterFactory {
fun create(
displayCheckBox: Boolean,
displaySmsTag: ContactSearchItems.DisplaySmsTag,
displaySmsTag: ContactSearchAdapter.DisplaySmsTag,
recipientListener: (View, ContactSearchData.KnownRecipient, Boolean) -> Unit,
storyListener: (View, ContactSearchData.Story, Boolean) -> Unit,
storyContextMenuCallbacks: ContactSearchItems.StoryContextMenuCallbacks,
storyContextMenuCallbacks: ContactSearchAdapter.StoryContextMenuCallbacks,
expandListener: (ContactSearchData.Expand) -> Unit
): PagingMappingAdapter<ContactSearchKey>
}
@@ -159,10 +163,10 @@ class ContactSearchMediator(
private object DefaultAdapterFactory : AdapterFactory {
override fun create(
displayCheckBox: Boolean,
displaySmsTag: ContactSearchItems.DisplaySmsTag,
displaySmsTag: ContactSearchAdapter.DisplaySmsTag,
recipientListener: (View, ContactSearchData.KnownRecipient, Boolean) -> Unit,
storyListener: (View, ContactSearchData.Story, Boolean) -> Unit,
storyContextMenuCallbacks: ContactSearchItems.StoryContextMenuCallbacks,
storyContextMenuCallbacks: ContactSearchAdapter.StoryContextMenuCallbacks,
expandListener: (ContactSearchData.Expand) -> Unit
): PagingMappingAdapter<ContactSearchKey> {
return ContactSearchAdapter(displayCheckBox, displaySmsTag, recipientListener, storyListener, storyContextMenuCallbacks, expandListener)

View File

@@ -19,7 +19,8 @@ import java.util.concurrent.TimeUnit
*/
class ContactSearchPagedDataSource(
private val contactConfiguration: ContactSearchConfiguration,
private val contactSearchPagedDataSourceRepository: ContactSearchPagedDataSourceRepository = ContactSearchPagedDataSourceRepository(ApplicationDependencies.getApplication())
private val contactSearchPagedDataSourceRepository: ContactSearchPagedDataSourceRepository = ContactSearchPagedDataSourceRepository(ApplicationDependencies.getApplication()),
private val arbitraryRepository: ArbitraryRepository? = null
) : PagedDataSource<ContactSearchKey, ContactSearchData> {
companion object {
@@ -89,6 +90,7 @@ class ContactSearchPagedDataSource(
is ContactSearchConfiguration.Section.Groups -> contactSearchPagedDataSourceRepository.getGroupSearchIterator(section, query).getCollectionSize(section, query, this::canSendToGroup)
is ContactSearchConfiguration.Section.Recents -> getRecentsSearchIterator(section, query).getCollectionSize(section, query, null)
is ContactSearchConfiguration.Section.Stories -> getStoriesSearchIterator(query).getCollectionSize(section, query, null)
is ContactSearchConfiguration.Section.Arbitrary -> arbitraryRepository?.getSize(section, query) ?: error("Invalid arbitrary section.")
}
}
@@ -119,6 +121,7 @@ class ContactSearchPagedDataSource(
is ContactSearchConfiguration.Section.Individuals -> getNonGroupContactsData(section, query, startIndex, endIndex)
is ContactSearchConfiguration.Section.Recents -> getRecentsContactData(section, query, startIndex, endIndex)
is ContactSearchConfiguration.Section.Stories -> getStoriesContactData(section, query, startIndex, endIndex)
is ContactSearchConfiguration.Section.Arbitrary -> arbitraryRepository?.getData(section, query, startIndex, endIndex) ?: error("Invalid arbitrary section.")
}
}

View File

@@ -22,6 +22,7 @@ class ContactSearchRepository {
is ContactSearchKey.Expand -> false
is ContactSearchKey.Header -> false
is ContactSearchKey.RecipientSearchKey -> canSelectRecipient(it.recipientId)
is ContactSearchKey.Arbitrary -> false
}
ContactSearchSelectionResult(it, isSelectable)
}.toSet()

View File

@@ -27,6 +27,7 @@ class ContactSearchViewModel(
private val contactSearchRepository: ContactSearchRepository,
private val performSafetyNumberChecks: Boolean,
private val safetyNumberRepository: SafetyNumberRepository = SafetyNumberRepository(),
private val arbitraryRepository: ArbitraryRepository?
) : ViewModel() {
private val disposables = CompositeDisposable()
@@ -53,7 +54,7 @@ class ContactSearchViewModel(
}
fun setConfiguration(contactSearchConfiguration: ContactSearchConfiguration) {
val pagedDataSource = ContactSearchPagedDataSource(contactSearchConfiguration)
val pagedDataSource = ContactSearchPagedDataSource(contactSearchConfiguration, arbitraryRepository = arbitraryRepository)
pagedData.value = PagedData.createForLiveData(pagedDataSource, pagingConfig)
}
@@ -139,10 +140,11 @@ class ContactSearchViewModel(
class Factory(
private val selectionLimits: SelectionLimits,
private val repository: ContactSearchRepository,
private val performSafetyNumberChecks: Boolean
private val performSafetyNumberChecks: Boolean,
private val arbitraryRepository: ArbitraryRepository?
) : ViewModelProvider.Factory {
override fun <T : ViewModel> create(modelClass: Class<T>): T {
return modelClass.cast(ContactSearchViewModel(selectionLimits, repository, performSafetyNumberChecks)) as T
return modelClass.cast(ContactSearchViewModel(selectionLimits, repository, performSafetyNumberChecks, arbitraryRepository = arbitraryRepository)) as T
}
}
}

View File

@@ -33,9 +33,9 @@ import org.thoughtcrime.securesms.color.ViewColorSet
import org.thoughtcrime.securesms.components.ContactFilterView
import org.thoughtcrime.securesms.components.TooltipPopup
import org.thoughtcrime.securesms.components.WrapperDialogFragment
import org.thoughtcrime.securesms.contacts.paged.ContactSearchAdapter
import org.thoughtcrime.securesms.contacts.paged.ContactSearchConfiguration
import org.thoughtcrime.securesms.contacts.paged.ContactSearchError
import org.thoughtcrime.securesms.contacts.paged.ContactSearchItems
import org.thoughtcrime.securesms.contacts.paged.ContactSearchKey
import org.thoughtcrime.securesms.contacts.paged.ContactSearchMediator
import org.thoughtcrime.securesms.contacts.paged.ContactSearchState
@@ -118,7 +118,15 @@ class MultiselectForwardFragment :
view.minimumHeight = resources.displayMetrics.heightPixels
contactSearchRecycler = view.findViewById(R.id.contact_selection_list)
contactSearchMediator = ContactSearchMediator(this, contactSearchRecycler, FeatureFlags.shareSelectionLimit(), !args.selectSingleRecipient, ContactSearchItems.DisplaySmsTag.DEFAULT, this::getConfiguration, this::filterContacts)
contactSearchMediator = ContactSearchMediator(
this,
contactSearchRecycler,
FeatureFlags.shareSelectionLimit(),
!args.selectSingleRecipient,
ContactSearchAdapter.DisplaySmsTag.DEFAULT,
this::getConfiguration,
this::filterContacts
)
callback = findListener()!!
disposables.bindTo(viewLifecycleOwner.lifecycle)

View File

@@ -1,6 +1,7 @@
package org.thoughtcrime.securesms.conversation.mutiselect.forward
import androidx.fragment.app.FragmentManager
import org.thoughtcrime.securesms.contacts.paged.ArbitraryRepository
import org.thoughtcrime.securesms.contacts.paged.ContactSearchConfiguration
import org.thoughtcrime.securesms.contacts.paged.ContactSearchState
@@ -15,4 +16,9 @@ interface SearchConfigurationProvider {
* @return A configuration or null. Returning null will result in MultiselectForwardFragment using it's default configuration.
*/
fun getSearchConfiguration(fragmentManager: FragmentManager, contactSearchState: ContactSearchState): ContactSearchConfiguration? = null
/**
* @return An ArbitraryRepository or null. Returning null will result in not being able to use the Arbitrary section, keys, or data.
*/
fun getArbitraryRepository(): ArbitraryRepository? = null
}

View File

@@ -15,8 +15,8 @@ import androidx.recyclerview.widget.RecyclerView
import org.signal.core.util.DimensionUnit
import org.thoughtcrime.securesms.R
import org.thoughtcrime.securesms.components.FixedRoundedCornerBottomSheetDialogFragment
import org.thoughtcrime.securesms.contacts.paged.ContactSearchAdapter
import org.thoughtcrime.securesms.contacts.paged.ContactSearchConfiguration
import org.thoughtcrime.securesms.contacts.paged.ContactSearchItems
import org.thoughtcrime.securesms.contacts.paged.ContactSearchKey
import org.thoughtcrime.securesms.contacts.paged.ContactSearchMediator
import org.thoughtcrime.securesms.contacts.paged.ContactSearchSortOrder
@@ -67,7 +67,7 @@ class ChooseGroupStoryBottomSheet : FixedRoundedCornerBottomSheetDialogFragment(
recyclerView = contactRecycler,
selectionLimits = FeatureFlags.shareSelectionLimit(),
displayCheckBox = true,
displaySmsTag = ContactSearchItems.DisplaySmsTag.DEFAULT,
displaySmsTag = ContactSearchAdapter.DisplaySmsTag.DEFAULT,
mapStateToConfiguration = { state ->
ContactSearchConfiguration.build {
query = state.query

View File

@@ -8,8 +8,8 @@ import org.thoughtcrime.securesms.R
import org.thoughtcrime.securesms.components.ViewBinderDelegate
import org.thoughtcrime.securesms.components.WrapperDialogFragment
import org.thoughtcrime.securesms.contacts.LetterHeaderDecoration
import org.thoughtcrime.securesms.contacts.paged.ContactSearchAdapter
import org.thoughtcrime.securesms.contacts.paged.ContactSearchConfiguration
import org.thoughtcrime.securesms.contacts.paged.ContactSearchItems
import org.thoughtcrime.securesms.contacts.paged.ContactSearchMediator
import org.thoughtcrime.securesms.databinding.ViewAllSignalConnectionsFragmentBinding
import org.thoughtcrime.securesms.groups.SelectionLimits
@@ -29,7 +29,7 @@ class ViewAllSignalConnectionsFragment : Fragment(R.layout.view_all_signal_conne
recyclerView = binding.recycler,
selectionLimits = SelectionLimits(0, 0),
displayCheckBox = false,
displaySmsTag = ContactSearchItems.DisplaySmsTag.IF_NOT_REGISTERED,
displaySmsTag = ContactSearchAdapter.DisplaySmsTag.IF_NOT_REGISTERED,
mapStateToConfiguration = { getConfiguration() },
performSafetyNumberChecks = false
)

View File

@@ -13,7 +13,7 @@ import org.thoughtcrime.securesms.components.settings.DSLSettingsAdapter
import org.thoughtcrime.securesms.components.settings.DSLSettingsFragment
import org.thoughtcrime.securesms.components.settings.DSLSettingsText
import org.thoughtcrime.securesms.components.settings.configure
import org.thoughtcrime.securesms.contacts.paged.ContactSearchItems
import org.thoughtcrime.securesms.contacts.paged.ContactSearchAdapter
import org.thoughtcrime.securesms.contacts.paged.ContactSearchKey
import org.thoughtcrime.securesms.groups.ParcelableGroupId
import org.thoughtcrime.securesms.keyvalue.SignalStore
@@ -62,7 +62,7 @@ class StoriesPrivacySettingsFragment :
}
@Suppress("UNCHECKED_CAST")
ContactSearchItems.registerStoryItems(
ContactSearchAdapter.registerStoryItems(
mappingAdapter = middle as PagingMappingAdapter<ContactSearchKey>,
storyListener = { _, story, _ ->
when {
@@ -136,9 +136,10 @@ class StoriesPrivacySettingsFragment :
private fun getMiddleConfiguration(state: StoriesPrivacySettingsState): DSLConfiguration {
return if (state.areStoriesEnabled) {
configure {
ContactSearchItems.toMappingModelList(
ContactSearchAdapter.toMappingModelList(
state.storyContactItems,
emptySet()
emptySet(),
null
).forEach {
customPref(it)
}