diff --git a/library/ui-strings/src/main/res/values/strings.xml b/library/ui-strings/src/main/res/values/strings.xml index cf74250ab5e..6bcca18d660 100644 --- a/library/ui-strings/src/main/res/values/strings.xml +++ b/library/ui-strings/src/main/res/values/strings.xml @@ -2717,6 +2717,8 @@ Review to ensure your account is safe Verify the new login accessing your account: %1$s + As of April 2026 unverified devices will not be able to send and receive messages. + Verify now Manually Verify by Text diff --git a/vector/src/main/java/im/vector/app/core/ui/views/VerifyDeviceBanner.kt b/vector/src/main/java/im/vector/app/core/ui/views/VerifyDeviceBanner.kt new file mode 100755 index 00000000000..249a3617ee6 --- /dev/null +++ b/vector/src/main/java/im/vector/app/core/ui/views/VerifyDeviceBanner.kt @@ -0,0 +1,38 @@ +/* + * Copyright 2026 New Vector Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial + * Please see LICENSE files in the repository root for full details. + */ + +package im.vector.app.core.ui.views + +import android.content.Context +import android.util.AttributeSet +import androidx.constraintlayout.widget.ConstraintLayout +import im.vector.app.R +import im.vector.app.databinding.ViewVerifySessionBannerBinding + +class VerifyDeviceBanner @JvmOverloads constructor( + context: Context, + attrs: AttributeSet? = null, + defStyleAttr: Int = 0 +) : ConstraintLayout(context, attrs, defStyleAttr) { + var delegate: Delegate? = null + + init { + setupView() + } + + private fun setupView() { + inflate(context, R.layout.view_verify_session_banner, this) + val views = ViewVerifySessionBannerBinding.bind(this) + views.learnMore.setOnClickListener { delegate?.onVerificationLearnMoreClicked() } + views.verifyButton.setOnClickListener { delegate?.onVerifySession() } + } + + interface Delegate { + fun onVerificationLearnMoreClicked() + fun onVerifySession() + } +} diff --git a/vector/src/main/java/im/vector/app/features/home/HomeActivityViewModel.kt b/vector/src/main/java/im/vector/app/features/home/HomeActivityViewModel.kt index ca17b95a48c..473d73ab438 100644 --- a/vector/src/main/java/im/vector/app/features/home/HomeActivityViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/home/HomeActivityViewModel.kt @@ -362,11 +362,13 @@ class HomeActivityViewModel @AssistedInject constructor( // cross signing keys have been reset // Trigger a popup to re-verify // Note: user can be unknown in case of logout - session.getUserOrDefault(session.myUserId) - .toMatrixItem() - .let { user -> - _viewEvents.post(HomeActivityViewEvents.OnCrossSignedInvalidated(user)) - } + + // This is now handled by the banner in the HomeFragment + // session.getUserOrDefault(session.myUserId) + // .toMatrixItem() + // .let { user -> + // _viewEvents.post(HomeActivityViewEvents.OnCrossSignedInvalidated(user)) + // } } } @@ -441,12 +443,13 @@ class HomeActivityViewModel @AssistedInject constructor( val is4Ssetup = session.sharedSecretStorageService().isRecoverySetup() if (hasTargetDeviceToVerifyAgainst || is4Ssetup) { // New session - _viewEvents.post( - HomeActivityViewEvents.CurrentSessionNotVerified( - session.getUserOrDefault(session.myUserId).toMatrixItem(), - vectorPreferences.isOnRustCrypto() && vectorPreferences.hadExistingLegacyData() - ) - ) + // This is now handled by the banner in the HomeFragment + // _viewEvents.post( + // HomeActivityViewEvents.CurrentSessionNotVerified( + // session.getUserOrDefault(session.myUserId).toMatrixItem(), + // vectorPreferences.isOnRustCrypto() && vectorPreferences.hadExistingLegacyData() + // ) + // ) } else { _viewEvents.post( HomeActivityViewEvents.CurrentSessionCannotBeVerified( diff --git a/vector/src/main/java/im/vector/app/features/home/HomeDetailViewModel.kt b/vector/src/main/java/im/vector/app/features/home/HomeDetailViewModel.kt index 0a720f29d3d..5cc55f2e781 100644 --- a/vector/src/main/java/im/vector/app/features/home/HomeDetailViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/home/HomeDetailViewModel.kt @@ -93,6 +93,7 @@ class HomeDetailViewModel @AssistedInject constructor( observeRoomSummaries() updatePstnSupportFlag() observeDataStore() + observeCrossSigningState() callManager.addProtocolsCheckerListener(this) session.flow().liveUser(session.myUserId).execute { copy( @@ -101,6 +102,21 @@ class HomeDetailViewModel @AssistedInject constructor( } } + private fun observeCrossSigningState() { + session + .flow() + .liveCrossSigningInfo(session.myUserId) + .onEach { info -> + val isVerified = info.getOrNull()?.isTrusted() ?: false + setState { + copy( + isSessionVerified = isVerified, + ) + } + } + .launchIn(viewModelScope) + } + private fun observeDataStore() { vectorDataStore.pushCounterFlow.setOnEach { nbOfPush -> copy( diff --git a/vector/src/main/java/im/vector/app/features/home/HomeDetailViewState.kt b/vector/src/main/java/im/vector/app/features/home/HomeDetailViewState.kt index 7b9284afc1d..352f378d5d8 100644 --- a/vector/src/main/java/im/vector/app/features/home/HomeDetailViewState.kt +++ b/vector/src/main/java/im/vector/app/features/home/HomeDetailViewState.kt @@ -33,7 +33,9 @@ data class HomeDetailViewState( val incrementalSyncRequestState: SyncRequestState.IncrementalSyncRequestState? = null, val pushCounter: Int = 0, val pstnSupportFlag: Boolean = false, - val forceDialPadTab: Boolean = false + val forceDialPadTab: Boolean = false, + // Set to true by default to avoid glitch + val isSessionVerified: Boolean = true, ) : MavericksState { val showDialPadTab = forceDialPadTab || pstnSupportFlag } diff --git a/vector/src/main/java/im/vector/app/features/home/NewHomeDetailFragment.kt b/vector/src/main/java/im/vector/app/features/home/NewHomeDetailFragment.kt index fbc3604e58e..082e8b65244 100644 --- a/vector/src/main/java/im/vector/app/features/home/NewHomeDetailFragment.kt +++ b/vector/src/main/java/im/vector/app/features/home/NewHomeDetailFragment.kt @@ -14,6 +14,7 @@ import android.view.Menu import android.view.MenuItem import android.view.View import android.view.ViewGroup +import androidx.core.view.isGone import androidx.core.view.isVisible import androidx.lifecycle.lifecycleScope import com.airbnb.mvrx.activityViewModel @@ -33,6 +34,8 @@ import im.vector.app.core.resources.ColorProvider import im.vector.app.core.ui.views.CurrentCallsView import im.vector.app.core.ui.views.CurrentCallsViewPresenter import im.vector.app.core.ui.views.KeysBackupBanner +import im.vector.app.core.ui.views.VerifyDeviceBanner +import im.vector.app.core.utils.openUrlInChromeCustomTab import im.vector.app.databinding.FragmentNewHomeDetailBinding import im.vector.app.features.call.SharedKnownCallsViewModel import im.vector.app.features.call.VectorCallActivity @@ -66,7 +69,8 @@ class NewHomeDetailFragment : KeysBackupBanner.Delegate, CurrentCallsView.Callback, OnBackPressed, - VectorMenuProvider { + VectorMenuProvider, + VerifyDeviceBanner.Delegate { @Inject lateinit var avatarRenderer: AvatarRenderer @Inject lateinit var colorProvider: ColorProvider @@ -132,6 +136,7 @@ class NewHomeDetailFragment : sharedCallActionViewModel = activityViewModelProvider.get(SharedKnownCallsViewModel::class.java) setupToolbar() setupKeysBackupBanner() + setupVerificationBanner() setupActiveCallView() setupDebugButton() setupFabs() @@ -323,6 +328,10 @@ class NewHomeDetailFragment : views.homeKeysBackupBanner.delegate = this } + private fun setupVerificationBanner() { + views.homeVerifyDevice.delegate = this + } + private fun setupActiveCallView() { currentCallsViewPresenter.bind(views.currentCallsView, this) } @@ -355,9 +364,9 @@ class NewHomeDetailFragment : }) } -/* ========================================================================================== - * KeysBackupBanner Listener - * ========================================================================================== */ + /* ========================================================================================== + * KeysBackupBanner Listener + * ========================================================================================== */ override fun onCloseClicked() { serverBackupStatusViewModel.handle(ServerBackupStatusAction.OnBannerClosed) @@ -378,7 +387,7 @@ class NewHomeDetailFragment : it.pushCounter, vectorPreferences.developerShowDebugInfo() ) - + views.homeVerifyDevice.isGone = it.isSessionVerified refreshAvatar() hasUnreadRooms = it.hasUnreadMessages } @@ -419,6 +428,21 @@ class NewHomeDetailFragment : private fun SpaceStateHandler.isRoot() = getSpaceBackstack().isEmpty() + override fun onVerificationLearnMoreClicked() { + openUrlInChromeCustomTab( + context = requireContext(), + session = null, + url = "https://docs.element.io/latest/element-support/device-verification/how-to-verify-devices", + ) + } + + override fun onVerifySession() { + (activity as? VectorBaseActivity<*>)?.let { vectorBaseActivity -> + vectorBaseActivity.navigator + .requestSelfSessionVerification(vectorBaseActivity) + } + } + companion object { private const val HOME_ROOM_LIST_FRAGMENT_TAG = "TAG_HOME_ROOM_LIST" } diff --git a/vector/src/main/res/drawable/banner_background.xml b/vector/src/main/res/drawable/banner_background.xml new file mode 100644 index 00000000000..d66d089d918 --- /dev/null +++ b/vector/src/main/res/drawable/banner_background.xml @@ -0,0 +1,9 @@ + + + + + + + + diff --git a/vector/src/main/res/layout/fragment_new_home_detail.xml b/vector/src/main/res/layout/fragment_new_home_detail.xml index 32d1dfc5d21..4621156a77e 100644 --- a/vector/src/main/res/layout/fragment_new_home_detail.xml +++ b/vector/src/main/res/layout/fragment_new_home_detail.xml @@ -44,12 +44,11 @@ + android:layout_height="wrap_content"> @@ -89,13 +88,26 @@ - + android:orientation="vertical" + app:layout_behavior="com.google.android.material.appbar.AppBarLayout$ScrollingViewBehavior"> + + + + + + diff --git a/vector/src/main/res/layout/view_verify_session_banner.xml b/vector/src/main/res/layout/view_verify_session_banner.xml new file mode 100644 index 00000000000..8d57407e1c2 --- /dev/null +++ b/vector/src/main/res/layout/view_verify_session_banner.xml @@ -0,0 +1,62 @@ + + + + + + + + + + + +