Show the multi-account announcement on the HomeScreen (#6265)

* Present the multi-account announcement when nothing else is showing

The user session flow owns onboarding, verification and settings, so it presents the announcement in the tab sheet slot with the lowest priority: once the verification state is known, and whenever onboarding, settings or a verification sheet is dismissed. Routes and sign-out dismiss settings without triggering it, and it is marked as seen when presented.

* Show the multi-account announcement on the home screen

Following review, the announcement is a best-effort sheet on the home
screen instead of a UserSessionFlowCoordinator state. It's presented when
the room list appears with the flag on, the announcement armed and the
session verified, and counts as seen once its content appears.

- Restore UserSessionFlowCoordinator to develop.
- HomeScreenViewModel presents the announcement through a binding and
  waits for a known verification state, so it never competes with the
  identity confirmation onboarding.
- Add account only closes the sheet until the add account flow exists.
- Rename MultiAccountAnnouncementScreen to MultiAccountAnnouncementView
  and drop its unused coordinator and view model protocol.
- Size the sheet from the measured navigation bar height and match the
  Figma spacing; the hard-coded 44pt pushed the content under the button.

* Present the multi-account announcement from the home screen view model

The announcement no longer needs its own view model: the view takes the
home screen's context and presents through a boolean binding, like the
home screen banners.

- Drop MultiAccountAnnouncementViewModel and its models. The view sends
  home screen actions to close or add an account and builds its own
  attributed subtitle.
- Check the verification state when the room list appears instead of
  waiting for it. Restored sessions already know it by then, unverified
  ones get the identity confirmation cover first and the room list
  appears again once it's dismissed. Nothing is left subscribed, so the
  sheet can no longer turn up later wherever the user is.
This commit is contained in:
Mauro
2026-10-07 12:29:51 +02:00
committed by GitHub
parent 82a933d1d8
commit f2eaa8e80d
22 changed files with 221 additions and 248 deletions
@@ -42,6 +42,10 @@ enum HomeScreenViewAction {
case resetEncryption
case skipRecoveryKeyConfirmation
case dismissNewSoundBanner
case screenAppeared
case multiAccountAnnouncementAppeared
case dismissMultiAccountAnnouncement
case addAccount
case updateVisibleItemRange(Range<Int>)
case spaceFilters
case markRoomAsUnread(roomIdentifier: String)
@@ -161,6 +165,7 @@ struct HomeScreenViewStateBindings {
var leaveRoomAlertItem: LeaveRoomAlertItem?
var spaceFiltersViewModel: ChatsSpaceFiltersScreenViewModel?
var isPresentingMultiAccountAnnouncement = false
}
enum CallBadgeType {
@@ -218,6 +218,14 @@ class HomeScreenViewModel: HomeScreenViewModelType, HomeScreenViewModelProtocol
state.securityBannerMode = .dismissed
case .dismissNewSoundBanner:
userSettings.hasSeenNewSoundBanner = true
case .screenAppeared:
// Use a task otherwise the presentation isn't animated.
Task { presentMultiAccountAnnouncementIfNeeded() }
case .multiAccountAnnouncementAppeared:
MXLog.info("The multi-account announcement has been seen.")
userSettings.hasSeenMultiAccountAnnouncement = true
case .dismissMultiAccountAnnouncement, .addAccount: // There is no add account flow to start yet.
state.bindings.isPresentingMultiAccountAnnouncement = false
case .updateVisibleItemRange(let range):
roomSummaryProvider?.updateVisibleRange(range)
case .startChat:
@@ -476,6 +484,21 @@ class HomeScreenViewModel: HomeScreenViewModelType, HomeScreenViewModelProtocol
actionsSubject.send(.roomLeft(roomIdentifier: roomID))
}
// MARK: Multi-account announcement
private func presentMultiAccountAnnouncementIfNeeded() {
// An unverified session gets the identity confirmation cover first, the room list appears again once it's dismissed.
guard userSettings.multiAccountEnabled,
!userSettings.hasSeenMultiAccountAnnouncement,
userSession.sessionSecurityStatePublisher.value.verificationState == .verified,
!state.bindings.isPresentingMultiAccountAnnouncement else {
return
}
MXLog.info("Presenting the multi-account announcement.")
state.bindings.isPresentingMultiAccountAnnouncement = true
}
// MARK: Invites
private func acceptInvite(roomID: String) async {
@@ -42,6 +42,11 @@ struct HomeScreen: View {
.navigationTransition(.zoom(sourceID: NavigationTransitionSourceID.spaceFilters,
in: navigationTransitionNamespace))
}
.sheet(isPresented: $context.isPresentingMultiAccountAnnouncement) {
MultiAccountAnnouncementView(context: context)
.onAppear { context.send(viewAction: .multiAccountAnnouncementAppeared) }
}
.onAppear { context.send(viewAction: .screenAppeared) }
}
// MARK: - Private
@@ -0,0 +1,85 @@
//
// Copyright 2026 Element Creations Ltd.
//
// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
// Please see LICENSE files in the repository root for full details.
//
import Compound
import SwiftUI
struct MultiAccountAnnouncementView: View {
let context: HomeScreenViewModel.Context
@State private var navigationBarHeight: CGFloat = .zero
@State private var contentHeight: CGFloat = .zero
@State private var buttonHeight: CGFloat = .zero
private var subtitle: AttributedString {
let boldPlaceholder = "{bold}"
var subtitle = AttributedString(L10n.screenMultiAccountAnnouncementDescription(boldPlaceholder))
var boldString = AttributedString("\(L10n.commonSettings) > \(L10n.screenSettingsAddAccount)")
boldString.bold()
subtitle.replace(boldPlaceholder, with: boldString)
return subtitle
}
var body: some View {
GeometryReader { geometry in
ElementNavigationStack {
ScrollView {
content
.readHeight($contentHeight)
}
.scrollBounceBehavior(.basedOnSize)
.backportSafeAreaBar(edge: .bottom, spacing: 0) {
addAccountButton
.padding(.bottom, geometry.safeAreaInsets.bottom > 0 ? 0 : 16)
.readHeight($buttonHeight)
}
.onGeometryChange(for: CGFloat.self) { $0.safeAreaInsets.top } action: { navigationBarHeight = $0 }
.toolbar {
ToolbarItem(placement: .primaryAction) {
ToolbarButton(role: .close) { context.send(viewAction: .dismissMultiAccountAnnouncement) }
}
}
}
}
.presentationDetents([.height(navigationBarHeight + contentHeight + buttonHeight)])
.presentationDragIndicator(.hidden)
.presentationBackground(.compound.bgCanvasDefault)
}
private var content: some View {
TitleAndIcon(title: L10n.screenMultiAccountAnnouncementTitle,
subtitle: subtitle,
icon: \.userProfileSolid,
iconStyle: .defaultSolid)
.frame(maxWidth: .infinity)
.padding(.horizontal, 24)
.padding(.top, 8)
.padding(.bottom, 24)
}
private var addAccountButton: some View {
Button {
context.send(viewAction: .addAccount)
} label: {
Label(L10n.screenMultiAccountAnnouncementAction, icon: \.plus)
}
.buttonStyle(.compound(.primary))
.padding(.horizontal, 16)
.padding(.top, 16)
}
}
// MARK: - Previews
struct MultiAccountAnnouncementView_Previews: PreviewProvider, TestablePreview {
static let viewModel = HomeScreen_Previews.viewModel(.rooms)
static var previews: some View {
MultiAccountAnnouncementView(context: viewModel.context)
.previewDisplayName("Default")
}
}