diff --git a/app/src/main/java/org/thoughtcrime/securesms/ContactSelectionListFragment.java b/app/src/main/java/org/thoughtcrime/securesms/ContactSelectionListFragment.java index aebef2833f..b35a6bff0b 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/ContactSelectionListFragment.java +++ b/app/src/main/java/org/thoughtcrime/securesms/ContactSelectionListFragment.java @@ -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() { diff --git a/app/src/main/java/org/thoughtcrime/securesms/avatar/view/AvatarView.kt b/app/src/main/java/org/thoughtcrime/securesms/avatar/view/AvatarView.kt index cc190feaa8..c039737f7b 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/avatar/view/AvatarView.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/avatar/view/AvatarView.kt @@ -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(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() { diff --git a/app/src/main/java/org/thoughtcrime/securesms/contacts/paged/ContactSearchAdapter.kt b/app/src/main/java/org/thoughtcrime/securesms/contacts/paged/ContactSearchAdapter.kt index 12a93c5c5a..8ff6c9aa8f 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/contacts/paged/ContactSearchAdapter.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/contacts/paged/ContactSearchAdapter.kt @@ -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(), 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, - 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, - private val storyContextMenuCallbacks: StoryContextMenuCallbacks? - ) : BaseRecipientViewHolder(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, + private val storyContextMenuCallbacks: StoryContextMenuCallbacks?, + private val showStoryRing: Boolean = false + ) : MappingViewHolder(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? = 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 { diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/mutiselect/forward/MultiselectForwardFragment.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/mutiselect/forward/MultiselectForwardFragment.kt index 4ca963d5a7..d9affdb13c 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/mutiselect/forward/MultiselectForwardFragment.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/mutiselect/forward/MultiselectForwardFragment.kt @@ -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()!! diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversationlist/ConversationListFragment.java b/app/src/main/java/org/thoughtcrime/securesms/conversationlist/ConversationListFragment.java index 2d55566b51..29b36c9708 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversationlist/ConversationListFragment.java +++ b/app/src/main/java/org/thoughtcrime/securesms/conversationlist/ConversationListFragment.java @@ -304,6 +304,7 @@ public class ConversationListFragment extends MainFragment implements ActionMode false, ContactSearchAdapter.DisplaySmsTag.DEFAULT, ContactSearchAdapter.DisplaySecondaryInformation.NEVER, + false, false ), this::mapSearchStateToConfiguration, diff --git a/app/src/main/res/color/group_story_indicator_tint_selector.xml b/app/src/main/res/color/group_story_indicator_tint_selector.xml new file mode 100644 index 0000000000..b09f186984 --- /dev/null +++ b/app/src/main/res/color/group_story_indicator_tint_selector.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/group_story_indicator_background.xml b/app/src/main/res/drawable/group_story_indicator_background.xml new file mode 100644 index 0000000000..987f1df7ab --- /dev/null +++ b/app/src/main/res/drawable/group_story_indicator_background.xml @@ -0,0 +1,13 @@ + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/contact_search_story_item.xml b/app/src/main/res/layout/contact_search_story_item.xml new file mode 100644 index 0000000000..5d245c91f9 --- /dev/null +++ b/app/src/main/res/layout/contact_search_story_item.xml @@ -0,0 +1,108 @@ + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/values/attrs.xml b/app/src/main/res/values/attrs.xml index be473fc81c..d58e369fac 100644 --- a/app/src/main/res/values/attrs.xml +++ b/app/src/main/res/values/attrs.xml @@ -57,6 +57,10 @@ + + + +