Add verified updates and unverified banner.

This commit is contained in:
Cody Henthorne
2023-05-25 12:47:33 -04:00
parent 7318e676f7
commit e565de0724
10 changed files with 246 additions and 38 deletions

View File

@@ -2,7 +2,6 @@ package org.thoughtcrime.securesms.components.identity;
import android.content.Context;
import android.os.Build;
import android.util.AttributeSet;
import android.view.LayoutInflater;
import android.view.View;
@@ -12,7 +11,6 @@ import android.widget.TextView;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.RequiresApi;
import org.signal.core.util.logging.Log;
import org.thoughtcrime.securesms.R;
@@ -24,9 +22,10 @@ public class UnverifiedBannerView extends LinearLayout {
private static final String TAG = Log.tag(UnverifiedBannerView.class);
private View container;
private TextView text;
private ImageView closeButton;
private View container;
private TextView text;
private ImageView closeButton;
private OnHideListener onHideListener;
public UnverifiedBannerView(Context context) {
super(context);
@@ -38,13 +37,11 @@ public class UnverifiedBannerView extends LinearLayout {
initialize();
}
@RequiresApi(api = Build.VERSION_CODES.HONEYCOMB)
public UnverifiedBannerView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
initialize();
}
@RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
public UnverifiedBannerView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
super(context, attrs, defStyleAttr, defStyleRes);
initialize();
@@ -82,16 +79,27 @@ public class UnverifiedBannerView extends LinearLayout {
});
}
public void setOnHideListener(@Nullable OnHideListener onHideListener) {
this.onHideListener = onHideListener;
}
public void hide() {
if (onHideListener != null && onHideListener.onHide()) {
return;
}
setVisibility(View.GONE);
}
public interface DismissListener {
public void onDismissed(List<IdentityRecord> unverifiedIdentities);
void onDismissed(List<IdentityRecord> unverifiedIdentities);
}
public interface ClickListener {
public void onClicked(List<IdentityRecord> unverifiedIdentities);
void onClicked(List<IdentityRecord> unverifiedIdentities);
}
public interface OnHideListener {
boolean onHide();
}
}

View File

@@ -4393,7 +4393,7 @@ public class ConversationParentFragment extends Fragment
AlertDialog.Builder builder = new MaterialAlertDialogBuilder(requireContext());
builder.setIcon(R.drawable.ic_warning);
builder.setTitle("No longer verified");
builder.setTitle(R.string.ConversationFragment__no_longer_verified);
builder.setItems(unverifiedNames, (dialog, which) -> {
startActivity(VerifyIdentityActivity.newIntent(requireContext(), unverifiedIdentities.get(which), false));
});

View File

@@ -71,7 +71,6 @@ public class ConversationTitleView extends ConstraintLayout {
ViewUtil.setTextViewGravityStart(this.subtitle, getContext());
}
@Override
protected @NonNull Parcelable onSaveInstanceState() {
Bundle bundle = new Bundle();
@@ -94,7 +93,6 @@ public class ConversationTitleView extends ConstraintLayout {
}
}
public void showExpiring(@NonNull Recipient recipient) {
isSelf = recipient.isSelf();

View File

@@ -6,8 +6,10 @@
package org.thoughtcrime.securesms.conversation.v2
import android.content.Context
import android.transition.ChangeBounds
import android.transition.Slide
import android.transition.TransitionManager
import android.transition.TransitionSet
import android.util.AttributeSet
import android.view.Gravity
import android.view.LayoutInflater
@@ -15,8 +17,12 @@ import android.view.View
import androidx.appcompat.widget.LinearLayoutCompat
import androidx.core.transition.addListener
import org.thoughtcrime.securesms.R
import org.thoughtcrime.securesms.components.identity.UnverifiedBannerView
import org.thoughtcrime.securesms.components.reminder.Reminder
import org.thoughtcrime.securesms.components.reminder.ReminderView
import org.thoughtcrime.securesms.database.identity.IdentityRecordList
import org.thoughtcrime.securesms.database.model.IdentityRecord
import org.thoughtcrime.securesms.util.IdentityUtil
/**
* Responsible for showing the various "banner" views at the top of a conversation
@@ -37,7 +43,7 @@ class ConversationBannerView @JvmOverloads constructor(
private val inflater: LayoutInflater by lazy { LayoutInflater.from(context) }
private var reminderView: ReminderView? = null
private var currentView: View? = null
private var unverifiedBannerView: UnverifiedBannerView? = null
var listener: Listener? = null
@@ -45,8 +51,9 @@ class ConversationBannerView @JvmOverloads constructor(
orientation = VERTICAL
}
fun showAsReminder(reminder: Reminder) {
fun showReminder(reminder: Reminder) {
reminderView = show(
position = -1,
existingView = reminderView,
create = { ReminderView(context) },
bind = {
@@ -63,39 +70,60 @@ class ConversationBannerView @JvmOverloads constructor(
}
}
setOnHideListener {
removeIfNotNull(reminderView)
reminderView = null
clearReminder()
true
}
}
)
}
fun clear() {
removeAllViews()
fun clearReminder() {
removeIfNotNull(reminderView)
reminderView = null
currentView = null
}
private fun <V : View> show(existingView: V?, create: () -> V, bind: V.() -> Unit = {}): V {
if (existingView != currentView) {
removeIfNotNull(currentView)
}
fun showUnverifiedBanner(identityRecords: IdentityRecordList) {
unverifiedBannerView = show(
position = 0,
existingView = null,
create = { UnverifiedBannerView(context) },
bind = {
setOnHideListener {
clearUnverifiedBanner()
true
}
display(
IdentityUtil.getUnverifiedBannerDescription(context, identityRecords.unverifiedRecipients)!!,
identityRecords.unverifiedRecords,
{ listener?.onUnverifiedBannerClicked(identityRecords.unverifiedRecords) },
{ listener?.onUnverifiedBannerDismissed(identityRecords.unverifiedRecords) }
)
}
)
}
fun clearUnverifiedBanner() {
removeIfNotNull(unverifiedBannerView)
unverifiedBannerView = null
}
private fun <V : View> show(position: Int, existingView: V?, create: () -> V, bind: V.() -> Unit = {}): V {
val view: V = if (existingView != null) {
existingView
} else {
val newView: V = create()
TransitionManager.beginDelayedTransition(this, Slide(Gravity.TOP))
addView(newView, defaultLayoutParams())
if (position in 0..childCount) {
addView(newView, position, defaultLayoutParams())
} else {
addView(newView, defaultLayoutParams())
}
newView
}
view.bind()
currentView = view
return view
}
@@ -105,13 +133,29 @@ class ConversationBannerView @JvmOverloads constructor(
private fun removeIfNotNull(view: View?) {
if (view != null) {
val transition = Slide(Gravity.TOP).apply {
val slideTransition = Slide(Gravity.TOP).apply {
addListener(
onEnd = {
layoutParams = layoutParams.apply { height = LayoutParams.WRAP_CONTENT }
}
)
}
val changeTransition = ChangeBounds().apply {
if (reminderView != null) {
addTarget(reminderView)
}
if (unverifiedBannerView != null) {
addTarget(unverifiedBannerView)
}
}
val transition = TransitionSet().apply {
addTransition(slideTransition)
addTransition(changeTransition)
}
layoutParams = layoutParams.apply { height = this@ConversationBannerView.height }
TransitionManager.beginDelayedTransition(this, transition)
removeView(view)
@@ -124,5 +168,7 @@ class ConversationBannerView @JvmOverloads constructor(
fun reviewJoinRequestsAction()
fun gv1SuggestionsAction(actionId: Int)
fun changeBubbleSettingAction(disableSetting: Boolean)
fun onUnverifiedBannerClicked(unverifiedIdentities: List<IdentityRecord>)
fun onUnverifiedBannerDismissed(unverifiedIdentities: List<IdentityRecord>)
}
}

View File

@@ -60,6 +60,8 @@ import io.reactivex.rxjava3.disposables.Disposable
import io.reactivex.rxjava3.kotlin.subscribeBy
import io.reactivex.rxjava3.schedulers.Schedulers
import org.greenrobot.eventbus.EventBus
import org.greenrobot.eventbus.Subscribe
import org.greenrobot.eventbus.ThreadMode
import org.signal.core.util.Result
import org.signal.core.util.ThreadUtil
import org.signal.core.util.concurrent.LifecycleDisposable
@@ -125,6 +127,7 @@ import org.thoughtcrime.securesms.conversation.ui.error.EnableCallNotificationSe
import org.thoughtcrime.securesms.conversation.v2.groups.ConversationGroupCallViewModel
import org.thoughtcrime.securesms.conversation.v2.groups.ConversationGroupViewModel
import org.thoughtcrime.securesms.conversation.v2.keyboard.AttachmentKeyboardFragment
import org.thoughtcrime.securesms.database.model.IdentityRecord
import org.thoughtcrime.securesms.database.model.InMemoryMessageRecord
import org.thoughtcrime.securesms.database.model.MediaMmsMessageRecord
import org.thoughtcrime.securesms.database.model.MessageId
@@ -198,6 +201,7 @@ import org.thoughtcrime.securesms.util.hasGiftBadge
import org.thoughtcrime.securesms.util.viewModel
import org.thoughtcrime.securesms.util.views.Stub
import org.thoughtcrime.securesms.util.visible
import org.thoughtcrime.securesms.verify.VerifyIdentityActivity
import org.thoughtcrime.securesms.wallpaper.ChatWallpaper
import org.thoughtcrime.securesms.wallpaper.ChatWallpaperDimLevelUtil
import java.util.Locale
@@ -345,6 +349,9 @@ class ConversationFragment : LoggingFragment(R.layout.v2_conversation_fragment)
WindowUtil.setLightNavigationBarFromTheme(requireActivity())
WindowUtil.setLightStatusBarFromTheme(requireActivity())
EventBus.getDefault().register(this)
groupCallViewModel.peekGroupCall()
if (!args.conversationScreenType.isInBubble) {
@@ -352,12 +359,20 @@ class ConversationFragment : LoggingFragment(R.layout.v2_conversation_fragment)
}
motionEventRelay.setDrain(MotionEventRelayDrain())
viewModel.updateIdentityRecords()
}
override fun onPause() {
super.onPause()
ApplicationDependencies.getMessageNotifier().clearVisibleThread()
motionEventRelay.setDrain(null)
EventBus.getDefault().unregister(this)
}
override fun onStop() {
super.onStop()
EventBus.getDefault().unregister(this)
}
private fun observeConversationThread() {
@@ -485,12 +500,30 @@ class ConversationFragment : LoggingFragment(R.layout.v2_conversation_fragment)
.reminder
.subscribeBy { reminder ->
if (reminder.isPresent) {
binding.conversationBanner.showAsReminder(reminder.get())
binding.conversationBanner.showReminder(reminder.get())
} else {
binding.conversationBanner.clear()
binding.conversationBanner.clearReminder()
}
}
.addTo(disposables)
viewModel
.identityRecords
.observeOn(AndroidSchedulers.mainThread())
.subscribeBy { presentIdentityRecordsState(it) }
.addTo(disposables)
}
private fun presentIdentityRecordsState(identityRecordsState: IdentityRecordsState) {
if (!identityRecordsState.isGroup) {
binding.conversationTitleView.root.setVerified(identityRecordsState.isVerified)
}
if (identityRecordsState.isUnverified) {
binding.conversationBanner.showUnverifiedBanner(identityRecordsState.identityRecords)
} else {
binding.conversationBanner.clearUnverifiedBanner()
}
}
private fun presentInputReadyState(inputReadyState: InputReadyState) {
@@ -589,7 +622,14 @@ class ConversationFragment : LoggingFragment(R.layout.v2_conversation_fragment)
return
}
binding.conversationTitleView.root.setTitle(GlideApp.with(this), recipient)
val titleView = binding.conversationTitleView.root
titleView.setTitle(GlideApp.with(this), recipient)
if (recipient.expiresInSeconds > 0) {
titleView.showExpiring(recipient)
} else {
titleView.clearExpiring()
}
}
private fun presentWallpaper(chatWallpaper: ChatWallpaper?) {
@@ -1714,13 +1754,8 @@ class ConversationFragment : LoggingFragment(R.layout.v2_conversation_fragment)
// TODO [alex] - ("Not yet implemented")
}
override fun showExpiring(recipient: Recipient) {
binding.conversationTitleView.root.showExpiring(recipient)
}
override fun clearExpiring() {
binding.conversationTitleView.root.clearExpiring()
}
override fun showExpiring(recipient: Recipient) = Unit
override fun clearExpiring() = Unit
override fun showGroupCallingTooltip() {
conversationTooltips.displayGroupCallingTooltip(requireView().findViewById(R.id.menu_video_secure))
@@ -1841,6 +1876,26 @@ class ConversationFragment : LoggingFragment(R.layout.v2_conversation_fragment)
startActivity(intent)
}
}
override fun onUnverifiedBannerClicked(unverifiedIdentities: List<IdentityRecord>) {
if (unverifiedIdentities.size == 1) {
startActivity(VerifyIdentityActivity.newIntent(requireContext(), unverifiedIdentities[0], false))
} else {
val unverifiedNames = unverifiedIdentities
.map { Recipient.resolved(it.recipientId).getDisplayName(requireContext()) }
.toTypedArray()
MaterialAlertDialogBuilder(requireContext())
.setIcon(R.drawable.ic_warning)
.setTitle(R.string.ConversationFragment__no_longer_verified)
.setItems(unverifiedNames) { _, which: Int -> startActivity(VerifyIdentityActivity.newIntent(requireContext(), unverifiedIdentities[which], false)) }
.show()
}
}
override fun onUnverifiedBannerDismissed(unverifiedIdentities: List<IdentityRecord>) {
viewModel.resetVerifiedStatusToDefault(unverifiedIdentities)
}
}
//endregion
@@ -2100,4 +2155,13 @@ class ConversationFragment : LoggingFragment(R.layout.v2_conversation_fragment)
}
//endregion
//region Event Bus
@Subscribe(threadMode = ThreadMode.POSTING)
fun onIdentityRecordUpdate(event: IdentityRecord?) {
viewModel.updateIdentityRecords()
}
//endregion
}

View File

@@ -28,9 +28,14 @@ import org.thoughtcrime.securesms.components.reminder.UnauthorizedReminder
import org.thoughtcrime.securesms.conversation.colors.GroupAuthorNameColorHelper
import org.thoughtcrime.securesms.conversation.colors.NameColor
import org.thoughtcrime.securesms.conversation.v2.data.ConversationDataSource
import org.thoughtcrime.securesms.crypto.ReentrantSessionLock
import org.thoughtcrime.securesms.database.GroupTable
import org.thoughtcrime.securesms.database.IdentityTable.VerifiedStatus
import org.thoughtcrime.securesms.database.RecipientTable
import org.thoughtcrime.securesms.database.RxDatabaseObserver
import org.thoughtcrime.securesms.database.SignalDatabase
import org.thoughtcrime.securesms.database.model.GroupRecord
import org.thoughtcrime.securesms.database.model.IdentityRecord
import org.thoughtcrime.securesms.database.model.Mention
import org.thoughtcrime.securesms.database.model.MessageId
import org.thoughtcrime.securesms.database.model.Quote
@@ -226,6 +231,36 @@ class ConversationRepository(
}
}
@Suppress("IfThenToElvis")
fun getIdentityRecords(recipient: Recipient, groupRecord: GroupRecord?): Single<IdentityRecordsState> {
return Single.fromCallable {
val recipients = if (groupRecord == null) {
listOf(recipient)
} else {
groupRecord.requireV2GroupProperties().getMemberRecipients(GroupTable.MemberSet.FULL_MEMBERS_EXCLUDING_SELF)
}
val records = ApplicationDependencies.getProtocolStore().aci().identities().getIdentityRecords(recipients)
val isVerified = recipient.registered == RecipientTable.RegisteredState.REGISTERED &&
Recipient.self().isRegistered &&
records.isVerified &&
!recipient.isSelf
IdentityRecordsState(isVerified, records, isGroup = groupRecord != null)
}.subscribeOn(Schedulers.io())
}
fun resetVerifiedStatusToDefault(unverifiedIdentities: List<IdentityRecord>): Completable {
return Completable.fromCallable {
ReentrantSessionLock.INSTANCE.acquire().use {
val identityStore = ApplicationDependencies.getProtocolStore().aci().identities()
for ((recipientId, identityKey) in unverifiedIdentities) {
identityStore.setVerified(recipientId, identityKey, VerifiedStatus.DEFAULT)
}
}
}.subscribeOn(Schedulers.io())
}
data class MessageCounts(
val unread: Int,
val mentions: Int

View File

@@ -27,6 +27,7 @@ import org.thoughtcrime.securesms.conversation.colors.GroupAuthorNameColorHelper
import org.thoughtcrime.securesms.conversation.colors.NameColor
import org.thoughtcrime.securesms.conversation.v2.data.ConversationElementKey
import org.thoughtcrime.securesms.database.DatabaseObserver
import org.thoughtcrime.securesms.database.model.IdentityRecord
import org.thoughtcrime.securesms.database.model.Mention
import org.thoughtcrime.securesms.database.model.MessageId
import org.thoughtcrime.securesms.database.model.MessageRecord
@@ -95,9 +96,11 @@ class ConversationViewModel(
get() = hasMessageRequestStateSubject.value ?: false
private val refreshReminder: Subject<Unit> = PublishSubject.create()
val reminder: Observable<Optional<Reminder>>
private val refreshIdentityRecords: Subject<Unit> = PublishSubject.create()
val identityRecords: Observable<IdentityRecordsState>
init {
disposables += recipient
.subscribeBy {
@@ -170,6 +173,14 @@ class ConversationViewModel(
.subscribeOn(Schedulers.io())
.flatMapMaybe { groupRecord -> repository.getReminder(groupRecord.orNull()) }
.observeOn(AndroidSchedulers.mainThread())
identityRecords = Observable.combineLatest(
refreshIdentityRecords.startWithItem(Unit).observeOn(Schedulers.io()),
recipient,
recipientRepository.groupRecord
) { _, r, g -> Pair(r, g) }
.flatMapSingle { (r, g) -> repository.getIdentityRecords(r, g.orNull()) }
.distinctUntilChanged()
}
override fun onCleared() {
@@ -229,4 +240,15 @@ class ConversationViewModel(
bodyRanges = bodyRanges
).observeOn(AndroidSchedulers.mainThread())
}
fun resetVerifiedStatusToDefault(unverifiedIdentities: List<IdentityRecord>) {
disposables += repository.resetVerifiedStatusToDefault(unverifiedIdentities)
.subscribe {
refreshIdentityRecords.onNext(Unit)
}
}
fun updateIdentityRecords() {
refreshIdentityRecords.onNext(Unit)
}
}

View File

@@ -0,0 +1,19 @@
/*
* Copyright 2023 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.thoughtcrime.securesms.conversation.v2
import org.thoughtcrime.securesms.database.identity.IdentityRecordList
/**
* Current state for all participants identity keys in a conversation excluding self.
*/
data class IdentityRecordsState(
val isVerified: Boolean,
val identityRecords: IdentityRecordList,
val isGroup: Boolean
) {
val isUnverified: Boolean = identityRecords.isUnverified
}

View File

@@ -10,6 +10,7 @@ import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.Objects;
import java.util.concurrent.TimeUnit;
public final class IdentityRecordList {
@@ -145,4 +146,16 @@ public final class IdentityRecordList {
System.currentTimeMillis() - identityRecord.getTimestamp() < untrustedWindowMillis;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
final IdentityRecordList that = (IdentityRecordList) o;
return Objects.equals(identityRecords, that.identityRecords);
}
@Override
public int hashCode() {
return Objects.hash(identityRecords);
}
}