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 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+