Key backup Rust API adoption (#1952)

* Adopt Rust side secure backup APIs (incomplete, based on poljar/backup-pr)

* Implement the home screen recovery key confirmation banner.

* Update with the latest Rust changes.

The loading button for .unknown was incorrect. It should be treated as disabled.

---------

Co-authored-by: Doug <douglase@element.io>
This commit is contained in:
Stefan Ceriu
2023-10-27 22:16:51 +03:00
committed by GitHub
parent 4077f5f4bb
commit a837ce5cff
22 changed files with 444 additions and 166 deletions
@@ -32,6 +32,7 @@ enum HomeScreenCoordinatorAction {
case presentSettingsScreen
case presentFeedbackScreen
case presentSessionVerificationScreen
case presentSecureBackupSettings
case presentStartChatScreen
case presentInvitesScreen
case logout
@@ -75,6 +76,8 @@ final class HomeScreenCoordinator: CoordinatorProtocol {
actionsSubject.send(.presentSettingsScreen)
case .presentSessionVerificationScreen:
actionsSubject.send(.presentSessionVerificationScreen)
case .presentSecureBackupSettings:
actionsSubject.send(.presentSecureBackupSettings)
case .logout:
actionsSubject.send(.logout)
case .presentStartChatScreen:
@@ -23,6 +23,7 @@ enum HomeScreenViewModelAction {
case presentRoomDetails(roomIdentifier: String)
case roomLeft(roomIdentifier: String)
case presentSessionVerificationScreen
case presentSecureBackupSettings
case presentSettingsScreen
case presentFeedbackScreen
case presentStartChatScreen
@@ -44,7 +45,9 @@ enum HomeScreenViewAction {
case userMenu(action: HomeScreenViewUserMenuAction)
case startChat
case verifySession
case confirmRecoveryKey
case skipSessionVerification
case skipRecoveryKeyConfirmation
case updateVisibleItemRange(range: Range<Int>, isScrolling: Bool)
case selectInvites
}
@@ -70,7 +73,8 @@ struct HomeScreenViewState: BindableState {
let userID: String
var userDisplayName: String?
var userAvatarURL: URL?
var showSessionVerificationBanner = false
var needsSessionVerification = false
var needsRecoveryKeyConfirmation = false
var showUserMenuBadge = false
var showSettingsMenuOptionBadge = false
var rooms: [HomeScreenRoom] = []
@@ -61,9 +61,9 @@ class HomeScreenViewModel: HomeScreenViewModelType, HomeScreenViewModelProtocol
.sink { [weak self] callback in
switch callback {
case .sessionVerificationNeeded:
self?.state.showSessionVerificationBanner = true
self?.state.needsSessionVerification = true
case .didVerifySession:
self?.state.showSessionVerificationBanner = false
self?.state.needsSessionVerification = false
default:
break
}
@@ -82,14 +82,15 @@ class HomeScreenViewModel: HomeScreenViewModelType, HomeScreenViewModelProtocol
userSession.clientProxy.secureBackupController.recoveryKeyState
.receive(on: DispatchQueue.main)
.map { state in
state == .unknown || state == .disabled
}
.sink { [weak self] requiresSecureBackupSetup in
guard let self else { return }
.sink { [weak self] recoveryKeyState in
guard let self, appSettings.chatBackupEnabled else { return }
state.showUserMenuBadge = requiresSecureBackupSetup && appSettings.chatBackupEnabled
state.showSettingsMenuOptionBadge = requiresSecureBackupSetup && appSettings.chatBackupEnabled
let requiresSecureBackupSetup = recoveryKeyState == .unknown || recoveryKeyState == .disabled || recoveryKeyState == .incomplete
state.showUserMenuBadge = requiresSecureBackupSetup
state.showSettingsMenuOptionBadge = requiresSecureBackupSetup
state.needsRecoveryKeyConfirmation = recoveryKeyState == .incomplete
}
.store(in: &cancellables)
@@ -143,8 +144,12 @@ class HomeScreenViewModel: HomeScreenViewModelType, HomeScreenViewModelProtocol
}
case .verifySession:
actionsSubject.send(.presentSessionVerificationScreen)
case .confirmRecoveryKey:
actionsSubject.send(.presentSecureBackupSettings)
case .skipSessionVerification:
state.showSessionVerificationBanner = false
state.needsSessionVerification = false
case .skipRecoveryKeyConfirmation:
state.needsRecoveryKeyConfirmation = false
case .updateVisibleItemRange(let range, let isScrolling):
visibleItemRangePublisher.send((range, isScrolling))
case .startChat:
@@ -191,8 +191,10 @@ struct HomeScreen: View {
@ViewBuilder
/// The session verification banner and invites button if either are needed.
private var topSection: some View {
if context.viewState.showSessionVerificationBanner {
sessionVerificationBanner
if context.viewState.needsSessionVerification {
HomeScreenSessionVerificationBanner(context: context)
} else if context.viewState.needsRecoveryKeyConfirmation {
HomeScreenRecoveryKeyConfirmationBanner(context: context)
}
if context.viewState.hasPendingInvitations, !context.isSearchFieldFocused {
@@ -204,43 +206,7 @@ struct HomeScreen: View {
.padding(.vertical, -8.0)
}
}
private var sessionVerificationBanner: some View {
VStack(alignment: .leading, spacing: 16) {
VStack(alignment: .leading, spacing: 4) {
HStack(spacing: 16) {
Text(L10n.sessionVerificationBannerTitle)
.font(.compound.bodyLGSemibold)
.foregroundColor(.compound.textPrimary)
Spacer()
Button {
context.send(viewAction: .skipSessionVerification)
} label: {
Image(systemName: "xmark")
.foregroundColor(.compound.iconSecondary)
.frame(width: 12, height: 12)
}
}
Text(L10n.sessionVerificationBannerMessage)
.font(.compound.bodyMD)
.foregroundColor(.compound.textSecondary)
}
Button(L10n.actionContinue) {
context.send(viewAction: .verifySession)
}
.frame(maxWidth: .infinity)
.buttonStyle(.compound(.primary, size: .medium))
.accessibilityIdentifier(A11yIdentifiers.homeScreen.verificationBannerContinue)
}
.padding(16)
.background(Color.compound.bgSubtleSecondary)
.cornerRadius(14)
.padding(.horizontal, 16)
}
@ToolbarContentBuilder
private var toolbar: some ToolbarContent {
ToolbarItem(placement: .navigationBarLeading) {
@@ -333,13 +299,18 @@ struct HomeScreen_Previews: PreviewProvider, TestablePreview {
}
static func viewModel(_ state: MockRoomSummaryProviderState) -> HomeScreenViewModel {
let userSession = MockUserSession(clientProxy: MockClientProxy(userID: "@alice:example.com",
roomSummaryProvider: MockRoomSummaryProvider(state: state)),
let clientProxy = MockClientProxy(userID: "@alice:example.com",
roomSummaryProvider: MockRoomSummaryProvider(state: state))
let userSession = MockUserSession(clientProxy: clientProxy,
mediaProvider: MockMediaProvider(),
voiceMessageMediaManager: VoiceMessageMediaManagerMock())
let attributedStringBuilder = AttributedStringBuilder(permalinkBaseURL: ServiceLocator.shared.settings.permalinkBaseURL,
mentionBuilder: MentionBuilder())
return HomeScreenViewModel(userSession: userSession,
attributedStringBuilder: AttributedStringBuilder(permalinkBaseURL: ServiceLocator.shared.settings.permalinkBaseURL, mentionBuilder: MentionBuilder()),
attributedStringBuilder: attributedStringBuilder,
selectedRoomPublisher: CurrentValueSubject<String?, Never>(nil).asCurrentValuePublisher(),
appSettings: ServiceLocator.shared.settings,
analytics: ServiceLocator.shared.analytics,
@@ -0,0 +1,85 @@
//
// Copyright 2023 New Vector Ltd
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
import Combine
import SwiftUI
struct HomeScreenRecoveryKeyConfirmationBanner: View {
@ObservedObject var context: HomeScreenViewModel.Context
var body: some View {
VStack(alignment: .leading, spacing: 16) {
VStack(alignment: .leading, spacing: 4) {
HStack(spacing: 16) {
Text(L10n.confirmRecoveryKeyBannerTitle)
.font(.compound.bodyLGSemibold)
.foregroundColor(.compound.textPrimary)
Spacer()
Button {
context.send(viewAction: .skipRecoveryKeyConfirmation)
} label: {
Image(systemName: "xmark")
.foregroundColor(.compound.iconSecondary)
.frame(width: 12, height: 12)
}
}
Text(L10n.confirmRecoveryKeyBannerMessage)
.font(.compound.bodyMD)
.foregroundColor(.compound.textSecondary)
}
Button(L10n.actionContinue) {
context.send(viewAction: .confirmRecoveryKey)
}
.frame(maxWidth: .infinity)
.buttonStyle(.compound(.primary, size: .medium))
.accessibilityIdentifier(A11yIdentifiers.homeScreen.recoveryKeyConfirmationBannerContinue)
}
.padding(16)
.background(Color.compound.bgSubtleSecondary)
.cornerRadius(14)
.padding(.horizontal, 16)
}
}
struct HomeScreenRecoveryKeyConfirmationBanner_Previews: PreviewProvider, TestablePreview {
static let viewModel = buildViewModel()
static var previews: some View {
HomeScreenRecoveryKeyConfirmationBanner(context: viewModel.context)
}
static func buildViewModel() -> HomeScreenViewModel {
let clientProxy = MockClientProxy(userID: "@alice:example.com",
roomSummaryProvider: MockRoomSummaryProvider(state: .loading))
let userSession = MockUserSession(clientProxy: clientProxy,
mediaProvider: MockMediaProvider(),
voiceMessageMediaManager: VoiceMessageMediaManagerMock())
let attributedStringBuilder = AttributedStringBuilder(permalinkBaseURL: ServiceLocator.shared.settings.permalinkBaseURL,
mentionBuilder: MentionBuilder())
return HomeScreenViewModel(userSession: userSession,
attributedStringBuilder: attributedStringBuilder,
selectedRoomPublisher: CurrentValueSubject<String?, Never>(nil).asCurrentValuePublisher(),
appSettings: ServiceLocator.shared.settings,
analytics: ServiceLocator.shared.analytics,
userIndicatorController: ServiceLocator.shared.userIndicatorController)
}
}
@@ -0,0 +1,85 @@
//
// Copyright 2023 New Vector Ltd
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
import Combine
import SwiftUI
struct HomeScreenSessionVerificationBanner: View {
@ObservedObject var context: HomeScreenViewModel.Context
var body: some View {
VStack(alignment: .leading, spacing: 16) {
VStack(alignment: .leading, spacing: 4) {
HStack(spacing: 16) {
Text(L10n.sessionVerificationBannerTitle)
.font(.compound.bodyLGSemibold)
.foregroundColor(.compound.textPrimary)
Spacer()
Button {
context.send(viewAction: .skipSessionVerification)
} label: {
Image(systemName: "xmark")
.foregroundColor(.compound.iconSecondary)
.frame(width: 12, height: 12)
}
}
Text(L10n.sessionVerificationBannerMessage)
.font(.compound.bodyMD)
.foregroundColor(.compound.textSecondary)
}
Button(L10n.actionContinue) {
context.send(viewAction: .verifySession)
}
.frame(maxWidth: .infinity)
.buttonStyle(.compound(.primary, size: .medium))
.accessibilityIdentifier(A11yIdentifiers.homeScreen.verificationBannerContinue)
}
.padding(16)
.background(Color.compound.bgSubtleSecondary)
.cornerRadius(14)
.padding(.horizontal, 16)
}
}
struct HomeScreenSessionVerificationBanner_Previews: PreviewProvider, TestablePreview {
static let viewModel = buildViewModel()
static var previews: some View {
HomeScreenSessionVerificationBanner(context: viewModel.context)
}
static func buildViewModel() -> HomeScreenViewModel {
let clientProxy = MockClientProxy(userID: "@alice:example.com",
roomSummaryProvider: MockRoomSummaryProvider(state: .loading))
let userSession = MockUserSession(clientProxy: clientProxy,
mediaProvider: MockMediaProvider(),
voiceMessageMediaManager: VoiceMessageMediaManagerMock())
let attributedStringBuilder = AttributedStringBuilder(permalinkBaseURL: ServiceLocator.shared.settings.permalinkBaseURL,
mentionBuilder: MentionBuilder())
return HomeScreenViewModel(userSession: userSession,
attributedStringBuilder: attributedStringBuilder,
selectedRoomPublisher: CurrentValueSubject<String?, Never>(nil).asCurrentValuePublisher(),
appSettings: ServiceLocator.shared.settings,
analytics: ServiceLocator.shared.analytics,
userIndicatorController: ServiceLocator.shared.userIndicatorController)
}
}