Add indicator and story ring for stories in chat selection.

This commit is contained in:
Clark
2023-03-16 16:46:25 -04:00
committed by Greyson Parrelli
parent 7c8de901f1
commit 17fc0dc0a1
9 changed files with 221 additions and 26 deletions

View File

@@ -344,7 +344,8 @@ public final class ContactSelectionListFragment extends LoggingFragment {
isMulti,
ContactSearchAdapter.DisplaySmsTag.DEFAULT,
ContactSearchAdapter.DisplaySecondaryInformation.ALWAYS,
newCallCallback != null
newCallCallback != null,
false
),
this::mapStateToConfiguration,
new ContactSearchMediator.SimpleCallbacks() {

View File

@@ -4,6 +4,7 @@ import android.content.Context
import android.util.AttributeSet
import android.view.View
import android.widget.FrameLayout
import androidx.core.content.res.use
import org.thoughtcrime.securesms.R
import org.thoughtcrime.securesms.components.AvatarImageView
import org.thoughtcrime.securesms.database.model.StoryViewState
@@ -20,10 +21,12 @@ class AvatarView @JvmOverloads constructor(
attrs: AttributeSet? = null
) : FrameLayout(context, attrs) {
private var storyRingScale = 0.8f
init {
inflate(context, R.layout.avatar_view, this)
isClickable = false
storyRingScale = context.theme.obtainStyledAttributes(attrs, R.styleable.AvatarView, 0, 0).use { it.getFloat(R.styleable.AvatarView_storyRingScale, storyRingScale) }
}
private val avatar: AvatarImageView = findViewById<AvatarImageView>(R.id.avatar_image_view).apply {
@@ -40,8 +43,8 @@ class AvatarView @JvmOverloads constructor(
storyRing.visible = true
storyRing.isActivated = hasUnreadStory
avatar.scaleX = 0.8f
avatar.scaleY = 0.8f
avatar.scaleX = storyRingScale
avatar.scaleY = storyRingScale
}
private fun hideStoryRing() {

View File

@@ -5,10 +5,15 @@ import android.view.View
import android.view.ViewGroup
import android.widget.CheckBox
import android.widget.TextView
import androidx.appcompat.widget.AppCompatImageView
import com.google.android.material.button.MaterialButton
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
import io.reactivex.rxjava3.core.Observable
import io.reactivex.rxjava3.disposables.Disposable
import org.signal.core.util.BreakIteratorCompat
import org.signal.core.util.dp
import org.thoughtcrime.securesms.R
import org.thoughtcrime.securesms.avatar.view.AvatarView
import org.thoughtcrime.securesms.badges.BadgeImageView
import org.thoughtcrime.securesms.components.AvatarImageView
import org.thoughtcrime.securesms.components.FromTextView
@@ -19,6 +24,7 @@ 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.database.model.StoryViewState
import org.thoughtcrime.securesms.keyvalue.SignalStore
import org.thoughtcrime.securesms.phonenumbers.PhoneNumberFormatter
import org.thoughtcrime.securesms.recipients.Recipient
@@ -45,7 +51,7 @@ open class ContactSearchAdapter(
) : PagingMappingAdapter<ContactSearchKey>(), FastScrollAdapter {
init {
registerStoryItems(this, displayOptions.displayCheckBox, onClickCallbacks::onStoryClicked, storyContextMenuCallbacks)
registerStoryItems(this, displayOptions.displayCheckBox, onClickCallbacks::onStoryClicked, storyContextMenuCallbacks, displayOptions.displayStoryRing)
registerKnownRecipientItems(this, fixedContacts, displayOptions, onClickCallbacks::onKnownRecipientClicked, longClickCallbacks::onKnownRecipientLongClick, callButtonClickCallbacks)
registerHeaders(this)
registerExpands(this, onClickCallbacks::onExpandClicked)
@@ -70,11 +76,12 @@ open class ContactSearchAdapter(
mappingAdapter: MappingAdapter,
displayCheckBox: Boolean = false,
storyListener: OnClickedCallback<ContactSearchData.Story>,
storyContextMenuCallbacks: StoryContextMenuCallbacks? = null
storyContextMenuCallbacks: StoryContextMenuCallbacks? = null,
showStoryRing: Boolean = false
) {
mappingAdapter.registerFactory(
StoryModel::class.java,
LayoutFactory({ StoryViewHolder(it, displayCheckBox, storyListener, storyContextMenuCallbacks) }, R.layout.contact_search_item)
LayoutFactory({ StoryViewHolder(it, displayCheckBox, storyListener, storyContextMenuCallbacks, showStoryRing) }, R.layout.contact_search_story_item)
)
}
@@ -158,15 +165,47 @@ open class ContactSearchAdapter(
private class StoryViewHolder(
itemView: View,
displayCheckBox: Boolean,
onClick: OnClickedCallback<ContactSearchData.Story>,
private val storyContextMenuCallbacks: StoryContextMenuCallbacks?
) : BaseRecipientViewHolder<StoryModel, ContactSearchData.Story>(itemView, DisplayOptions(displayCheckBox = displayCheckBox), onClick, EmptyCallButtonClickCallbacks) {
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
val displayCheckBox: Boolean,
val onClick: OnClickedCallback<ContactSearchData.Story>,
private val storyContextMenuCallbacks: StoryContextMenuCallbacks?,
private val showStoryRing: Boolean = false
) : MappingViewHolder<StoryModel>(itemView) {
override fun bindNumberField(model: StoryModel) {
val avatar: AvatarView = itemView.findViewById(R.id.contact_photo_image)
val badge: BadgeImageView = itemView.findViewById(R.id.contact_badge)
val checkbox: CheckBox = itemView.findViewById(R.id.check_box)
val name: FromTextView = itemView.findViewById(R.id.name)
val number: TextView = itemView.findViewById(R.id.number)
val groupStoryIndicator: AppCompatImageView = itemView.findViewById(R.id.group_story_indicator)
var storyViewState: Observable<StoryViewState>? = null
var storyDisposable: Disposable? = null
override fun bind(model: StoryModel) {
itemView.setOnClickListener { onClick.onClicked(avatar, getData(model), isSelected(model)) }
bindLongPress(model)
bindCheckbox(model)
if (payload.isNotEmpty()) {
return
}
storyViewState = if (showStoryRing) StoryViewState.getForRecipientId(getRecipient(model).id) else null
avatar.setStoryRingFromState(StoryViewState.NONE)
groupStoryIndicator.isActivated = false
name.setText(getRecipient(model))
badge.setBadgeFromRecipient(getRecipient(model))
bindAvatar(model)
bindNumberField(model)
}
fun isSelected(model: StoryModel): Boolean = model.isSelected
fun getData(model: StoryModel): ContactSearchData.Story = model.story
fun getRecipient(model: StoryModel): Recipient = model.story.recipient
fun bindNumberField(model: StoryModel) {
number.visible = true
val count = if (model.story.recipient.isGroup) {
@@ -193,17 +232,23 @@ open class ContactSearchAdapter(
}
}
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)
}
fun bindCheckbox(model: StoryModel) {
checkbox.visible = displayCheckBox
checkbox.isChecked = isSelected(model)
}
override fun bindLongPress(model: StoryModel) {
fun bindAvatar(model: StoryModel) {
if (model.story.recipient.isMyStory) {
avatar.setFallbackPhotoProvider(MyStoryFallbackPhotoProvider(Recipient.self().getDisplayName(context), 40.dp))
avatar.displayProfileAvatar(Recipient.self())
} else {
avatar.setFallbackPhotoProvider(Recipient.DEFAULT_FALLBACK_PHOTO_PROVIDER)
avatar.displayChatAvatar(getRecipient(model))
}
groupStoryIndicator.visible = showStoryRing && model.story.recipient.isGroup
}
fun bindLongPress(model: StoryModel) {
if (storyContextMenuCallbacks == null) {
return
}
@@ -264,6 +309,20 @@ open class ContactSearchAdapter(
return GeneratedContactPhoto(name, R.drawable.symbol_person_40, targetSize)
}
}
override fun onAttachedToWindow() {
storyDisposable = storyViewState?.observeOn(AndroidSchedulers.mainThread())?.subscribe {
avatar.setStoryRingFromState(it)
when (it) {
StoryViewState.UNVIEWED -> groupStoryIndicator.isActivated = true
else -> groupStoryIndicator.isActivated = false
}
}
}
override fun onDetachedFromWindow() {
storyDisposable?.dispose()
}
}
/**
@@ -661,7 +720,8 @@ open class ContactSearchAdapter(
val displayCheckBox: Boolean = false,
val displaySmsTag: DisplaySmsTag = DisplaySmsTag.NEVER,
val displaySecondaryInformation: DisplaySecondaryInformation = DisplaySecondaryInformation.NEVER,
val displayCallButtons: Boolean = false
val displayCallButtons: Boolean = false,
val displayStoryRing: Boolean = false
)
fun interface OnClickedCallback<D : ContactSearchData> {

View File

@@ -127,7 +127,8 @@ class MultiselectForwardFragment :
ContactSearchAdapter.DisplayOptions(
displayCheckBox = !args.selectSingleRecipient,
displaySmsTag = ContactSearchAdapter.DisplaySmsTag.DEFAULT,
displaySecondaryInformation = ContactSearchAdapter.DisplaySecondaryInformation.NEVER
displaySecondaryInformation = ContactSearchAdapter.DisplaySecondaryInformation.NEVER,
displayStoryRing = true
),
this::getConfiguration,
object : ContactSearchMediator.SimpleCallbacks() {
@@ -136,7 +137,6 @@ class MultiselectForwardFragment :
}
}
)
contactSearchRecycler.adapter = contactSearchMediator.adapter
callback = findListener()!!

View File

@@ -304,6 +304,7 @@ public class ConversationListFragment extends MainFragment implements ActionMode
false,
ContactSearchAdapter.DisplaySmsTag.DEFAULT,
ContactSearchAdapter.DisplaySecondaryInformation.NEVER,
false,
false
),
this::mapSearchStateToConfiguration,