From e2e69534e36ab4949c1bdc3722922203ce283f13 Mon Sep 17 00:00:00 2001 From: Gil Eluard Date: Mon, 30 Jan 2023 12:51:07 +0100 Subject: [PATCH] 430 - Design review for first public TestFlight Part2 (#496) * 430 - Design review for first public TestFlight Part2 --------- Co-authored-by: Doug --- .../Buttons/ElementCapsuleButtonStyle.swift | 2 +- ElementX.xcodeproj/project.pbxproj | 4 + .../en.lproj/Untranslated.strings | 55 +++++- .../Generated/Strings+Untranslated.swift | 32 +++- ElementX/Sources/Other/AvatarSize.swift | 2 +- .../SwiftUI/Views/SettingsPickerRow.swift | 53 ++++++ .../AuthenticationIconImage.swift | 3 +- .../Screens/HomeScreen/View/HomeScreen.swift | 49 +++--- .../HomeScreen/View/HomeScreenRoomCell.swift | 30 ++-- .../Other/SplashScreenCoordinator.swift | 3 +- .../SessionVerificationModels.swift | 37 +++- .../SessionVerificationViewModel.swift | 6 + .../View/SessionVerificationScreen.swift | 166 +++++++++++------- .../Settings/SettingsScreenCoordinator.swift | 19 ++ .../Settings/SettingsScreenModels.swift | 3 + .../Settings/SettingsScreenViewModel.swift | 21 ++- .../Settings/View/SettingsScreen.swift | 113 +++++------- .../UserSessionFlowCoordinator.swift | 4 +- ...erSessionFlowCoordinatorStateMachine.swift | 2 +- 19 files changed, 412 insertions(+), 192 deletions(-) create mode 100644 ElementX/Sources/Other/SwiftUI/Views/SettingsPickerRow.swift diff --git a/DesignKit/Sources/Buttons/ElementCapsuleButtonStyle.swift b/DesignKit/Sources/Buttons/ElementCapsuleButtonStyle.swift index a358d0db1..9711ced4f 100644 --- a/DesignKit/Sources/Buttons/ElementCapsuleButtonStyle.swift +++ b/DesignKit/Sources/Buttons/ElementCapsuleButtonStyle.swift @@ -35,7 +35,7 @@ public struct ElementCapsuleButtonStyle: ButtonStyle { configuration.label .padding(7) .frame(maxWidth: .infinity) - .font(.element.footnote) + .font(.element.headline) .foregroundColor(fontColor) .multilineTextAlignment(.center) .background(background) diff --git a/ElementX.xcodeproj/project.pbxproj b/ElementX.xcodeproj/project.pbxproj index 1d141e69a..3d51426ba 100644 --- a/ElementX.xcodeproj/project.pbxproj +++ b/ElementX.xcodeproj/project.pbxproj @@ -426,6 +426,7 @@ CF82143AA4A4F7BD11D22946 /* RoomTimelineViewProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = ACB6C5E4950B6C9842F35A38 /* RoomTimelineViewProvider.swift */; }; D034A195A3494E38BF060485 /* MockSessionVerificationControllerProxy.swift in Sources */ = {isa = PBXBuildFile; fileRef = D1A9CCCF53495CF3D7B19FCE /* MockSessionVerificationControllerProxy.swift */; }; D05A193AE63030F2CFCE2E9C /* UITestScreenIdentifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = CC6FE34A0A47D010BBB4D4D4 /* UITestScreenIdentifier.swift */; }; + D1DF52739551A5B2655C2D2E /* SettingsPickerRow.swift in Sources */ = {isa = PBXBuildFile; fileRef = D7428970E7B5EA449806CFA3 /* SettingsPickerRow.swift */; }; D2D70B5DB1A5E4AF0CD88330 /* target.yml in Resources */ = {isa = PBXBuildFile; fileRef = 033DB41C51865A2E83174E87 /* target.yml */; }; D33AC79A50DFC26D2498DD28 /* FileRoomTimelineItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5098DA7799946A61E34A2373 /* FileRoomTimelineItem.swift */; }; D59F046B15AA8E971053C1A6 /* RoomDetailsCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 813B198AE8833FD12E5A9C78 /* RoomDetailsCoordinator.swift */; }; @@ -1014,6 +1015,7 @@ D6CA5F386C7701C129398945 /* AuthenticationCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AuthenticationCoordinator.swift; sourceTree = ""; }; D6D094C15E8DB424F1C6FC94 /* hr */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = hr; path = hr.lproj/Localizable.strings; sourceTree = ""; }; D6DC38E64A5ED3FDB201029A /* BugReportService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BugReportService.swift; sourceTree = ""; }; + D7428970E7B5EA449806CFA3 /* SettingsPickerRow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsPickerRow.swift; sourceTree = ""; }; D751BB69BB7C38FD247517B4 /* UITestsRootCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UITestsRootCoordinator.swift; sourceTree = ""; }; D77B3D4950F1707E66E4A45A /* AnalyticsConfiguration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AnalyticsConfiguration.swift; sourceTree = ""; }; D8F5F9E02B1AB5350B1815E7 /* TimelineStartRoomTimelineItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelineStartRoomTimelineItem.swift; sourceTree = ""; }; @@ -1335,6 +1337,7 @@ 839E2C35DF3F9C7B54C3CE49 /* RoundedCornerShape.swift */, F3F8435052B2488947B35942 /* SettingsActionButton.swift */, BFB3D0B0E16FEC93175ABC2D /* SettingsDefaultRow.swift */, + D7428970E7B5EA449806CFA3 /* SettingsPickerRow.swift */, ); path = Views; sourceTree = ""; @@ -3241,6 +3244,7 @@ D6417E5A799C3C7F14F9EC0A /* SessionVerificationViewModelProtocol.swift in Sources */, 052BE25E8C466D3D60558DA3 /* SettingsActionButton.swift in Sources */, A2DDFA5033B535AB2BA51F5C /* SettingsDefaultRow.swift in Sources */, + D1DF52739551A5B2655C2D2E /* SettingsPickerRow.swift in Sources */, 7FED310F6AB7A70CBFB7C8A3 /* SettingsScreen.swift in Sources */, 233221E32DA045018D3D3050 /* SettingsScreenCoordinator.swift in Sources */, DBAA69CC2CE4D44BC8E20105 /* SettingsScreenModels.swift in Sources */, diff --git a/ElementX/Resources/Localizations/en.lproj/Untranslated.strings b/ElementX/Resources/Localizations/en.lproj/Untranslated.strings index 2bb6fb59d..328756c4e 100644 --- a/ElementX/Resources/Localizations/en.lproj/Untranslated.strings +++ b/ElementX/Resources/Localizations/en.lproj/Untranslated.strings @@ -8,10 +8,11 @@ "screenshot_detected_message" = "Would you like to submit a bug report?"; "settings_appearance" = "Appearance"; -"settings_timeline_style" = "Timeline Style"; +"settings_timeline_style" = "Message layout"; +"settings_session_verification" = "Complete verification"; -"room_timeline_style_plain_long_description" = "Plain Timeline"; -"room_timeline_style_bubbled_long_description" = "Bubbled Timeline"; +"room_timeline_style_plain_long_description" = "Modern"; +"room_timeline_style_bubbled_long_description" = "Bubbles"; "room_timeline_permalink_creation_failure" = "Failed creating the permalink"; "room_timeline_backpagination_failure" = "Failed loading messages"; @@ -38,8 +39,9 @@ "noticeRoomUnknownMembershipChange" = "%1$@ made an unknown change to their membership"; -"session_verification_banner_title" = "Help keep your messages secure"; -"session_verification_banner_message" = "Looks like you’re using a new device. Verify its you."; +"session_verification_banner_title" = "Access your message history"; +"session_verification_banner_message" = "Looks like you’re using a new device. Verify it’s you to access your encrypted messages."; +"session_verification_start" = "Start"; "server_selection_server_footer" = "You can only connect to an existing server"; @@ -70,3 +72,46 @@ "bug_report_screen_description" = "Please describe the bug. What did you do? What did you expect to happen? What actually happened. Please go into as much detail as you can."; "bug_report_screen_include_logs" = "Send logs to help"; "bug_report_screen_logs_description" = "To check things work as intended, logs will be sent with your message. These will be private. To just send your message, turn off this setting."; + + +// Session verification +/* + "verification_open_other_to_verify" = "Use an existing session to verify this one, granting it access to encrypted messages."; + "verification_profile_verify" = "Verify"; + "verification_profile_verified" = "Verified"; + "verification_profile_warning" = "Warning"; +"verification_conclusion_warning" = "Untrusted sign in"; +"verification_sas_match" = "They match"; +"verification_sas_do_not_match" = "They don\'t match"; +"verification_conclusion_not_secure" = "Not secure"; +"verification_conclusion_compromised" = "One of the following may be compromised:\n\n - Your homeserver\n - The homeserver the user you’re verifying is connected to\n - Yours, or the other users’ internet connection\n - Yours, or the other users’ device"; +"verification_request_waiting" = "Waiting…"; +"verification_request_other_cancelled" = "%@ cancelled"; +"verification_request_you_cancelled" = "You cancelled"; +"verification_request_other_accepted" = "%@ accepted"; +"verification_request_you_accepted" = "You accepted"; +"verification_sent" = "Verification Sent"; +"verification_request" = "Verification Request"; +"verification_verify_device" = "Verify this session"; +"verification_scan_notice" = "Scan the code with the other user\'s device to securely verify each other"; +"verification_scan_self_notice" = "Scan the code with your other device or switch and scan with this device"; +"verification_scan_their_code" = "Scan their code"; +"verification_scan_with_this_device" = "Scan with this device"; +"verification_scan_emoji_title" = "Can\'t scan"; +"verification_scan_emoji_subtitle" = "If you\'re not in person, compare emoji instead"; +"verification_scan_self_emoji_subtitle" = "Verify by comparing emoji instead"; +"verification_no_scan_emoji_title" = "Verify by comparing emojis"; +"verification_verify_user" = "Verify %@"; +"verification_verified_user" = "Verified %@"; +"verification_request_waiting_for" = "Waiting for %@…"; +*/ + +"verification_open_other_to_verify_title" = "Open an existing session"; +"verification_open_other_to_verify_detail" = "Prove it’s you in order to access your encrypted message history."; +"verification_request_waiting_accept_request_title" = "Waiting to accept request"; +"verification_request_waiting_accept_request_detail" = "Accept the request to start the verification process in your other session to continue."; +"verification_request_cancelled_title" = "Verification cancelled"; +"verification_request_cancelled_detail" = "Something doesn’t seem right. Either the request timed out or the request was denied."; +"verification_compare_emojis_title" = "Compare emojis"; +"verification_compare_emojis_detail" = "Confirm that the emojis below match those shown on your other session."; +"verification_conclusion_ok_self_notice_title" = "Verification complete"; diff --git a/ElementX/Sources/Generated/Strings+Untranslated.swift b/ElementX/Sources/Generated/Strings+Untranslated.swift index c2860ed6c..57c72dd68 100644 --- a/ElementX/Sources/Generated/Strings+Untranslated.swift +++ b/ElementX/Sources/Generated/Strings+Untranslated.swift @@ -104,9 +104,9 @@ extension ElementL10n { public static func roomTimelineReplyingTo(_ p1: Any) -> String { return ElementL10n.tr("Untranslated", "room_timeline_replying_to", String(describing: p1)) } - /// Bubbled Timeline + /// Bubbles public static let roomTimelineStyleBubbledLongDescription = ElementL10n.tr("Untranslated", "room_timeline_style_bubbled_long_description") - /// Plain Timeline + /// Modern public static let roomTimelineStylePlainLongDescription = ElementL10n.tr("Untranslated", "room_timeline_style_plain_long_description") /// Syncing public static let roomTimelineSyncing = ElementL10n.tr("Untranslated", "room_timeline_syncing") @@ -116,13 +116,17 @@ extension ElementL10n { public static let screenshotDetectedTitle = ElementL10n.tr("Untranslated", "screenshot_detected_title") /// You can only connect to an existing server public static let serverSelectionServerFooter = ElementL10n.tr("Untranslated", "server_selection_server_footer") - /// Looks like you’re using a new device. Verify its you. + /// Looks like you’re using a new device. Verify it’s you to access your encrypted messages. public static let sessionVerificationBannerMessage = ElementL10n.tr("Untranslated", "session_verification_banner_message") - /// Help keep your messages secure + /// Access your message history public static let sessionVerificationBannerTitle = ElementL10n.tr("Untranslated", "session_verification_banner_title") + /// Start + public static let sessionVerificationStart = ElementL10n.tr("Untranslated", "session_verification_start") /// Appearance public static let settingsAppearance = ElementL10n.tr("Untranslated", "settings_appearance") - /// Timeline Style + /// Complete verification + public static let settingsSessionVerification = ElementL10n.tr("Untranslated", "settings_session_verification") + /// Message layout public static let settingsTimelineStyle = ElementL10n.tr("Untranslated", "settings_timeline_style") /// Untranslated public static let untranslated = ElementL10n.tr("Untranslated", "untranslated") @@ -130,6 +134,24 @@ extension ElementL10n { public static func untranslatedPlural(_ p1: Int) -> String { return ElementL10n.tr("Untranslated", "untranslated_plural", p1) } + /// Confirm that the emojis below match those shown on your other session. + public static let verificationCompareEmojisDetail = ElementL10n.tr("Untranslated", "verification_compare_emojis_detail") + /// Compare emojis + public static let verificationCompareEmojisTitle = ElementL10n.tr("Untranslated", "verification_compare_emojis_title") + /// Verification complete + public static let verificationConclusionOkSelfNoticeTitle = ElementL10n.tr("Untranslated", "verification_conclusion_ok_self_notice_title") + /// Prove it’s you in order to access your encrypted message history. + public static let verificationOpenOtherToVerifyDetail = ElementL10n.tr("Untranslated", "verification_open_other_to_verify_detail") + /// Open an existing session + public static let verificationOpenOtherToVerifyTitle = ElementL10n.tr("Untranslated", "verification_open_other_to_verify_title") + /// Something doesn’t seem right. Either the request timed out or the request was denied. + public static let verificationRequestCancelledDetail = ElementL10n.tr("Untranslated", "verification_request_cancelled_detail") + /// Verification cancelled + public static let verificationRequestCancelledTitle = ElementL10n.tr("Untranslated", "verification_request_cancelled_title") + /// Accept the request to start the verification process in your other session to continue. + public static let verificationRequestWaitingAcceptRequestDetail = ElementL10n.tr("Untranslated", "verification_request_waiting_accept_request_detail") + /// Waiting to accept request + public static let verificationRequestWaitingAcceptRequestTitle = ElementL10n.tr("Untranslated", "verification_request_waiting_accept_request_title") } // swiftlint:enable explicit_type_interface function_parameter_count identifier_name line_length // swiftlint:enable nesting type_body_length type_name vertical_whitespace_opening_braces diff --git a/ElementX/Sources/Other/AvatarSize.swift b/ElementX/Sources/Other/AvatarSize.swift index 293f26fab..6c551297f 100644 --- a/ElementX/Sources/Other/AvatarSize.swift +++ b/ElementX/Sources/Other/AvatarSize.swift @@ -71,7 +71,7 @@ enum RoomAvatarSizeOnScreen { case .timeline: return 32 case .home: - return 44 + return 52 case .details: return 70 } diff --git a/ElementX/Sources/Other/SwiftUI/Views/SettingsPickerRow.swift b/ElementX/Sources/Other/SwiftUI/Views/SettingsPickerRow.swift new file mode 100644 index 000000000..e0b04cdc9 --- /dev/null +++ b/ElementX/Sources/Other/SwiftUI/Views/SettingsPickerRow.swift @@ -0,0 +1,53 @@ +// +// 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 SwiftUI + +/// Picker row that can be reused for settings screens +struct SettingsPickerRow: View { + // MARK: Public + + let title: String + let image: Image + @Binding var selection: SelectionValue + let content: () -> Content + + // MARK: Private + + @ScaledMetric private var menuIconSize = 30.0 + private let listRowInsets = EdgeInsets(top: 8, leading: 16, bottom: 8, trailing: 16) + + // MARK: Views + + var body: some View { + Picker(selection: $selection, content: content) { + HStack(spacing: 16) { + image + .foregroundColor(.element.systemGray) + .padding(4) + .background(Color.element.systemGray6) + .clipShape(RoundedRectangle(cornerRadius: 8)) + .frame(width: menuIconSize, height: menuIconSize) + + Text(title) + .font(.element.body) + .foregroundColor(.element.primaryContent) + } + } + .listRowInsets(listRowInsets) + .listRowSeparator(.hidden) + } +} diff --git a/ElementX/Sources/Screens/Authentication/AuthenticationIconImage.swift b/ElementX/Sources/Screens/Authentication/AuthenticationIconImage.swift index 3058ee3fe..d021531c4 100644 --- a/ElementX/Sources/Screens/Authentication/AuthenticationIconImage.swift +++ b/ElementX/Sources/Screens/Authentication/AuthenticationIconImage.swift @@ -25,8 +25,9 @@ struct AuthenticationIconImage: View { .resizable() .renderingMode(.template) .foregroundColor(.element.secondaryContent) + .aspectRatio(contentMode: .fit) .accessibilityHidden(true) - .padding(20) + .padding(16) .frame(width: 72, height: 72) .background(RoundedRectangle(cornerRadius: 14).fill(Color.element.quinaryContent)) } diff --git a/ElementX/Sources/Screens/HomeScreen/View/HomeScreen.swift b/ElementX/Sources/Screens/HomeScreen/View/HomeScreen.swift index 28dd5ab8c..b5dc226a5 100644 --- a/ElementX/Sources/Screens/HomeScreen/View/HomeScreen.swift +++ b/ElementX/Sources/Screens/HomeScreen/View/HomeScreen.swift @@ -76,6 +76,7 @@ struct HomeScreen: View { userMenuButton } } + .background(Color.element.background) } @ViewBuilder @@ -83,15 +84,15 @@ struct HomeScreen: View { Menu { Section { Button(action: settings) { - Label(ElementL10n.settingsUserSettings, systemImage: "gearshape") + Label(ElementL10n.settings, systemImage: "gearshape") } } Section { Button(action: inviteFriends) { - Label(ElementL10n.inviteFriends, systemImage: "square.and.arrow.up") + Label(ElementL10n.actionInvite, systemImage: "square.and.arrow.up") } Button(action: feedback) { - Label(ElementL10n.feedback, systemImage: "questionmark.circle") + Label(ElementL10n.bugReportScreenTitle, systemImage: "questionmark.circle") } } Section { @@ -122,31 +123,35 @@ struct HomeScreen: View { private var sessionVerificationBanner: some View { VStack(alignment: .leading, spacing: 16) { - VStack(alignment: .leading, spacing: 2) { - Text(ElementL10n.sessionVerificationBannerTitle) - .font(.element.subheadlineBold) - .foregroundColor(.element.systemPrimaryLabel) + VStack(alignment: .leading, spacing: 4) { + HStack(spacing: 16) { + Text(ElementL10n.sessionVerificationBannerTitle) + .font(.element.headline) + .foregroundColor(.element.systemPrimaryLabel) + + Spacer() + + Button { + context.send(viewAction: .skipSessionVerification) + } label: { + Image(systemName: "xmark") + .foregroundColor(.element.secondaryContent) + .frame(width: 12, height: 12) + } + } Text(ElementL10n.sessionVerificationBannerMessage) - .font(.element.footnote) - .foregroundColor(.element.systemSecondaryLabel) + .font(.element.subheadline) + .foregroundColor(.element.secondaryContent) } - HStack(spacing: 16) { - Button(ElementL10n.actionSkip) { - context.send(viewAction: .skipSessionVerification) - } - .frame(maxWidth: .infinity) - .buttonStyle(.elementCapsule) - - Button(ElementL10n.continue) { - context.send(viewAction: .verifySession) - } - .frame(maxWidth: .infinity) - .buttonStyle(.elementCapsuleProminent) + Button(ElementL10n.continue) { + context.send(viewAction: .verifySession) } + .frame(maxWidth: .infinity) + .buttonStyle(.elementCapsuleProminent) } .padding(16) - .background(Color.element.systemSecondaryBackground) + .background(Color.element.system) .cornerRadius(14) .padding(.horizontal, 16) } diff --git a/ElementX/Sources/Screens/HomeScreen/View/HomeScreenRoomCell.swift b/ElementX/Sources/Screens/HomeScreen/View/HomeScreenRoomCell.swift index 6d69d4fe0..838174a55 100644 --- a/ElementX/Sources/Screens/HomeScreen/View/HomeScreenRoomCell.swift +++ b/ElementX/Sources/Screens/HomeScreen/View/HomeScreenRoomCell.swift @@ -34,11 +34,17 @@ struct HomeScreenRoomCell: View { footer } } - .frame(minHeight: 64.0) + .frame(minHeight: 84.0) .accessibilityElement(children: .combine) } .buttonStyle(HomeScreenRoomCellButtonStyle()) .accessibilityIdentifier("roomName:\(room.name)") + .overlay(alignment: .bottom) { + Divider() + .frame(height: 0.5) + .background(Color.element.quinaryContent) + .padding(.leading, 84) + } } @ViewBuilder @@ -53,17 +59,17 @@ struct HomeScreenRoomCell: View { @ViewBuilder var header: some View { - HStack(alignment: .firstTextBaseline) { + HStack(alignment: .top, spacing: 16) { Text(room.name) - .font(.element.callout.bold()) + .font(.element.headline) .foregroundColor(.element.primaryContent) .lineLimit(1) .frame(maxWidth: .infinity, alignment: .leading) if let timestamp = room.timestamp { Text(timestamp) - .font(.element.caption1) - .foregroundColor(.element.secondaryContent) + .font(.element.footnote) + .foregroundColor(room.hasUnreads ? .element.brand : .element.tertiaryContent) .id(timestamp) .transition(.opacity.animation(.elementDefault)) } @@ -91,9 +97,15 @@ struct HomeScreenRoomCell: View { if room.hasUnreads { Rectangle() .frame(width: 12, height: 12) - .foregroundColor(.element.primaryContent) + .foregroundColor(.element.brand) .clipShape(Circle()) .transition(.opacity.animation(.elementDefault)) + .padding(.leading, 12) + } else { + // Force extra padding between last message text and the right border of the screen if there is no unread dot + Rectangle() + .fill(Color.clear) + .frame(width: 12, height: 12) } } .animation(.elementDefault, value: room) @@ -111,18 +123,16 @@ struct HomeScreenRoomCellButtonStyle: ButtonStyle { private extension View { func lastMessageFormatting() -> some View { font(.element.subheadline) - .foregroundColor(.element.secondaryContent) + .foregroundColor(.element.tertiaryContent) .lineLimit(2) .multilineTextAlignment(.leading) - .padding(.top, 2) } // To be used to indicate the selected room too func roomCellBackground(_ background: Color) -> some View { padding(.horizontal, 8) - .padding(.vertical, 6) - .background { background.clipShape(RoundedRectangle(cornerRadius: 12)) } .padding(.horizontal, 8) + .background { background } } } diff --git a/ElementX/Sources/Screens/Other/SplashScreenCoordinator.swift b/ElementX/Sources/Screens/Other/SplashScreenCoordinator.swift index 922ae819b..a2ea2b9c8 100644 --- a/ElementX/Sources/Screens/Other/SplashScreenCoordinator.swift +++ b/ElementX/Sources/Screens/Other/SplashScreenCoordinator.swift @@ -20,7 +20,8 @@ struct SplashScreenCoordinator: CoordinatorProtocol { func toPresentable() -> AnyView { AnyView( ZStack { - Image(asset: Asset.Images.appLogo) + Image(asset: Asset.Images.onboardingAppLogo) + .renderingMode(.original) } .frame(maxWidth: .infinity, maxHeight: .infinity) ) diff --git a/ElementX/Sources/Screens/SessionVerification/SessionVerificationModels.swift b/ElementX/Sources/Screens/SessionVerification/SessionVerificationModels.swift index 8e49db30d..5c366abaa 100644 --- a/ElementX/Sources/Screens/SessionVerification/SessionVerificationModels.swift +++ b/ElementX/Sources/Screens/SessionVerification/SessionVerificationModels.swift @@ -22,13 +22,40 @@ enum SessionVerificationViewModelAction { struct SessionVerificationViewState: BindableState { var verificationState: SessionVerificationStateMachine.State = .initial + + var title: String? { + switch verificationState { + case .initial: + return ElementL10n.verificationOpenOtherToVerifyTitle + case .requestingVerification: + return ElementL10n.verificationRequestWaitingAcceptRequestTitle + case .verificationRequestAccepted: + return ElementL10n.verificationCompareEmojisTitle + case .startingSasVerification: + return nil + case .sasVerificationStarted: + return nil + case .showingChallenge: + return ElementL10n.verificationCompareEmojisTitle + case .acceptingChallenge: + return ElementL10n.verificationCompareEmojisTitle + case .decliningChallenge: + return nil + case .verified: + return ElementL10n.verificationConclusionOkSelfNoticeTitle + case .cancelling: + return nil + case .cancelled: + return ElementL10n.verificationCancelled + } + } var message: String { switch verificationState { case .initial: - return ElementL10n.verificationOpenOtherToVerify + return ElementL10n.verificationOpenOtherToVerifyDetail case .requestingVerification: - return ElementL10n.verificationRequestWaiting + return ElementL10n.verificationRequestWaitingAcceptRequestDetail case .verificationRequestAccepted: return ElementL10n.verificationEmojiNotice case .startingSasVerification: @@ -36,17 +63,17 @@ struct SessionVerificationViewState: BindableState { case .sasVerificationStarted: return ElementL10n.verificationRequestWaiting case .acceptingChallenge: - return ElementL10n.verificationRequestWaiting + return ElementL10n.verificationCompareEmojisDetail case .decliningChallenge: return ElementL10n.verificationRequestWaiting case .cancelling: return ElementL10n.verificationRequestWaiting case .showingChallenge: - return ElementL10n.verificationCodeNotice + return ElementL10n.verificationCompareEmojisDetail case .verified: return ElementL10n.verificationConclusionOkSelfNotice case .cancelled: - return ElementL10n.verificationCancelled + return ElementL10n.verificationRequestCancelledDetail } } } diff --git a/ElementX/Sources/Screens/SessionVerification/SessionVerificationViewModel.swift b/ElementX/Sources/Screens/SessionVerification/SessionVerificationViewModel.swift index 59d6c312c..d1c60337c 100644 --- a/ElementX/Sources/Screens/SessionVerification/SessionVerificationViewModel.swift +++ b/ElementX/Sources/Screens/SessionVerification/SessionVerificationViewModel.swift @@ -106,6 +106,12 @@ class SessionVerificationViewModel: SessionVerificationViewModelType, SessionVer self.declineChallenge() case (_, .cancel, .cancelling): self.cancelVerification() + case (_, _, .verified): + // Dismiss the success screen automatically. + Task { + try await Task.sleep(for: .seconds(2)) + self.callback?(.finished) + } default: break } diff --git a/ElementX/Sources/Screens/SessionVerification/View/SessionVerificationScreen.swift b/ElementX/Sources/Screens/SessionVerification/View/SessionVerificationScreen.swift index a51bae9ab..aaf8dee04 100644 --- a/ElementX/Sources/Screens/SessionVerification/View/SessionVerificationScreen.swift +++ b/ElementX/Sources/Screens/SessionVerification/View/SessionVerificationScreen.swift @@ -22,23 +22,18 @@ struct SessionVerificationScreen: View { var body: some View { NavigationView { ScrollView { - VStack(spacing: 32.0) { - Text(context.viewState.message) - .font(.body) - .multilineTextAlignment(.center) - .foregroundColor(.element.systemPrimaryLabel) - .accessibilityIdentifier("titleLabel") - + VStack(spacing: 32) { + screenHeader + Spacer() mainContent } - .padding() - .padding(.top, 64) + .padding(.horizontal, 16) + .padding(.top, 24) .frame(maxWidth: .infinity) .navigationBarTitleDisplayMode(.inline) - .navigationTitle(ElementL10n.verificationProfileVerify) .toolbar { toolbarContent } } - .background(Color.element.systemSecondaryBackground) + .background(Color.element.background) .safeAreaInset(edge: .bottom) { actionButtons.padding() } } .navigationViewStyle(.stack) @@ -46,52 +41,82 @@ struct SessionVerificationScreen: View { // MARK: - Private + private var headerImageName: String { + switch context.viewState.verificationState { + case .initial: + return "macbook.and.iphone" + case .cancelled: + return "exclamationmark.shield" + case .requestingVerification: + return "hourglass" + case .verificationRequestAccepted: + return "face.smiling" + case .startingSasVerification: + return "hourglass" + case .sasVerificationStarted: + return "hourglass" + case .cancelling: + return "hourglass" + case .acceptingChallenge: + return "hourglass" + case .decliningChallenge: + return "hourglass" + case .showingChallenge: + return "face.smiling" + case .verified: + return "checkmark.shield" + } + } + + @ViewBuilder + private var screenHeader: some View { + VStack(spacing: 0) { + AuthenticationIconImage(image: Image(systemName: headerImageName)) + .padding(.bottom, 16) + + Text(context.viewState.title ?? "") + .font(.title2.bold()) + .multilineTextAlignment(.center) + .foregroundColor(.element.primaryContent) + .accessibilityIdentifier("titleLabel") + .padding(.bottom, 8) + + Text(context.viewState.message) + .font(.subheadline) + .multilineTextAlignment(.center) + .foregroundColor(.element.tertiaryContent) + .accessibilityIdentifier("detailLabel") + } + } + @ViewBuilder private var mainContent: some View { switch context.viewState.verificationState { - case .initial: - StateIcon(systemName: "lock.shield") - - case .cancelled: - StateIcon(systemName: "xmark.shield") - .accessibilityIdentifier("sessionVerificationFailedIcon") - + case .showingChallenge(let emojis): + emojisPanel(with: emojis) + case .acceptingChallenge(let emojis): + emojisPanel(with: emojis) case .requestingVerification: ProgressView() - .accessibilityIdentifier("requestingVerificationProgressView") - - case .verificationRequestAccepted: - StateIcon(systemName: "lock.shield") - case .startingSasVerification: - ProgressView() - .accessibilityIdentifier("startingSasVerification") - case .sasVerificationStarted: - ProgressView() - .accessibilityIdentifier("startedSasVerification") - case .cancelling: - ProgressView() - .accessibilityIdentifier("cancellingVerificationProgressView") - case .acceptingChallenge: - ProgressView() - .accessibilityIdentifier("acceptingChallengeProgressView") - case .decliningChallenge: - ProgressView() - .accessibilityIdentifier("decliningChallengeProgressView") - - case .showingChallenge(let emojis): - HStack(spacing: 16) { - ForEach(emojis.prefix(4), id: \.self) { emoji in - EmojiView(emoji: emoji) - } + .tint(.element.secondaryContent) + .scaleEffect(2) + default: + // In All other cases, we just want an empty view + EmptyView() + } + } + + @ViewBuilder + private func emojisPanel(with emojis: [SessionVerificationEmoji]) -> some View { + HStack(spacing: 16) { + ForEach(emojis.prefix(4), id: \.self) { emoji in + EmojiView(emoji: emoji) } - HStack(spacing: 16) { - ForEach(emojis.suffix(from: 4), id: \.self) { emoji in - EmojiView(emoji: emoji) - } + } + HStack(spacing: 16) { + ForEach(emojis.suffix(from: 4), id: \.self) { emoji in + EmojiView(emoji: emoji) } - case .verified: - StateIcon(systemName: "checkmark.shield") - .accessibilityIdentifier("sessionVerificationSucceededIcon") } } @@ -113,7 +138,7 @@ struct SessionVerificationScreen: View { .accessibilityIdentifier("restartButton") case .verificationRequestAccepted: - Button(ElementL10n.startVerification) { + Button(ElementL10n.sessionVerificationStart) { context.send(viewAction: .startSasVerification) } .buttonStyle(.elementAction(.xLarge)) @@ -133,14 +158,28 @@ struct SessionVerificationScreen: View { .font(.element.bodyBold) .accessibilityIdentifier("challengeDeclineButton") } - - case .verified: - Button(ElementL10n.finish) { - context.send(viewAction: .close) + + case .acceptingChallenge: + VStack(spacing: 30) { + Button { context.send(viewAction: .accept) } label: { + HStack(spacing: 16) { + ProgressView() + .tint(.element.background) + Label(ElementL10n.verificationSasMatch, systemImage: "checkmark") + } + } + .buttonStyle(.elementAction(.xLarge)) + .accessibilityIdentifier("challengeAcceptButton") + .disabled(true) + + Button(ElementL10n.verificationSasDoNotMatch) { + context.send(viewAction: .decline) + } + .font(.element.bodyBold) + .accessibilityIdentifier("challengeDeclineButton") + .disabled(true) } - .buttonStyle(.elementAction(.xLarge)) - .accessibilityIdentifier("finishButton") - + default: EmptyView() } @@ -149,11 +188,10 @@ struct SessionVerificationScreen: View { @ToolbarContentBuilder private var toolbarContent: some ToolbarContent { ToolbarItem(placement: .cancellationAction) { - Button { context.send(viewAction: .close) } label: { - Image(systemName: "xmark") + Button(ElementL10n.actionCancel) { + context.send(viewAction: .close) } - .font(.element.bodyBold) - .foregroundColor(.element.systemSecondaryLabel) + .foregroundColor(.element.accent) .accessibilityIdentifier("closeButton") } } @@ -166,8 +204,8 @@ struct SessionVerificationScreen: View { Text(emoji.symbol) .font(.element.largeTitleBold) Text(emoji.description) - .font(.element.caption2) - .foregroundColor(.element.systemSecondaryLabel) + .font(.element.subheadline) + .foregroundColor(.element.secondaryContent) } .padding(8.0) } diff --git a/ElementX/Sources/Screens/Settings/SettingsScreenCoordinator.swift b/ElementX/Sources/Screens/Settings/SettingsScreenCoordinator.swift index d649b0ab1..db1d2e384 100644 --- a/ElementX/Sources/Screens/Settings/SettingsScreenCoordinator.swift +++ b/ElementX/Sources/Screens/Settings/SettingsScreenCoordinator.swift @@ -50,6 +50,8 @@ final class SettingsScreenCoordinator: CoordinatorProtocol { self.toggleAnalytics() case .reportBug: self.presentBugReportScreen() + case .sessionVerification: + self.verifySession() case .logout: self.callback?(.logout) } @@ -91,6 +93,23 @@ final class SettingsScreenCoordinator: CoordinatorProtocol { parameters.navigationStackCoordinator.push(coordinator) } + + private func verifySession() { + guard let sessionVerificationController = parameters.userSession.sessionVerificationController else { + fatalError("The sessionVerificationController should aways be valid at this point") + } + + let verificationParameters = SessionVerificationCoordinatorParameters(sessionVerificationControllerProxy: sessionVerificationController) + let coordinator = SessionVerificationCoordinator(parameters: verificationParameters) + + coordinator.callback = { [weak self] in + self?.parameters.navigationStackCoordinator.setSheetCoordinator(nil) + } + + parameters.navigationStackCoordinator.setSheetCoordinator(coordinator) { [weak self] in + self?.parameters.navigationStackCoordinator.setSheetCoordinator(nil) + } + } private func showSuccess(label: String) { parameters.userNotificationController.submitNotification(UserNotification(title: label)) diff --git a/ElementX/Sources/Screens/Settings/SettingsScreenModels.swift b/ElementX/Sources/Screens/Settings/SettingsScreenModels.swift index 6901bb127..32d43c6f7 100644 --- a/ElementX/Sources/Screens/Settings/SettingsScreenModels.swift +++ b/ElementX/Sources/Screens/Settings/SettingsScreenModels.swift @@ -21,6 +21,7 @@ enum SettingsScreenViewModelAction { case close case toggleAnalytics case reportBug + case sessionVerification case logout } @@ -30,6 +31,7 @@ struct SettingsScreenViewState: BindableState { var userID: String var userAvatarURL: URL? var userDisplayName: String? + var showSessionVerificationSection: Bool } struct SettingsScreenViewStateBindings { @@ -40,5 +42,6 @@ enum SettingsScreenViewAction { case close case toggleAnalytics case reportBug + case sessionVerification case logout } diff --git a/ElementX/Sources/Screens/Settings/SettingsScreenViewModel.swift b/ElementX/Sources/Screens/Settings/SettingsScreenViewModel.swift index a7ccbbbe9..0302af43e 100644 --- a/ElementX/Sources/Screens/Settings/SettingsScreenViewModel.swift +++ b/ElementX/Sources/Screens/Settings/SettingsScreenViewModel.swift @@ -26,7 +26,10 @@ class SettingsScreenViewModel: SettingsScreenViewModelType, SettingsScreenViewMo init(withUserSession userSession: UserSessionProtocol) { self.userSession = userSession let bindings = SettingsScreenViewStateBindings() - super.init(initialViewState: .init(bindings: bindings, deviceID: userSession.deviceId, userID: userSession.userID), + super.init(initialViewState: .init(bindings: bindings, + deviceID: userSession.deviceId, + userID: userSession.userID, + showSessionVerificationSection: !(userSession.sessionVerificationController?.isVerified ?? false)), imageProvider: userSession.mediaProvider) Task { @@ -40,6 +43,20 @@ class SettingsScreenViewModel: SettingsScreenViewModelType, SettingsScreenViewMo state.userDisplayName = userDisplayName } } + + userSession.callbacks + .receive(on: DispatchQueue.main) + .sink { [weak self] callback in + switch callback { + case .sessionVerificationNeeded: + self?.state.showSessionVerificationSection = true + case .didVerifySession: + self?.state.showSessionVerificationSection = false + default: + break + } + } + .store(in: &cancellables) } override func process(viewAction: SettingsScreenViewAction) async { @@ -52,6 +69,8 @@ class SettingsScreenViewModel: SettingsScreenViewModelType, SettingsScreenViewMo callback?(.reportBug) case .logout: callback?(.logout) + case .sessionVerification: + callback?(.sessionVerification) } } } diff --git a/ElementX/Sources/Screens/Settings/View/SettingsScreen.swift b/ElementX/Sources/Screens/Settings/View/SettingsScreen.swift index 8e81423ca..bdf100deb 100644 --- a/ElementX/Sources/Screens/Settings/View/SettingsScreen.swift +++ b/ElementX/Sources/Screens/Settings/View/SettingsScreen.swift @@ -32,17 +32,16 @@ struct SettingsScreen: View { userSection .listRowBackground(rowBackgroundColor) + if context.viewState.showSessionVerificationSection { + sessionVerificationSection + .listRowBackground(rowBackgroundColor) + } + simplifiedSection .listRowBackground(rowBackgroundColor) - -// analyticsSection -// .listRowBackground(rowBackgroundColor) - -// userInterfaceSection -// .listRowBackground(rowBackgroundColor) - -// logoutSection -// .listRowBackground(rowBackgroundColor) + + signOutSection + .listRowBackground(rowBackgroundColor) } .introspectTableView { tableView in tableView.backgroundColor = .clear @@ -80,10 +79,10 @@ struct SettingsScreen: View { VStack(alignment: .leading, spacing: 4) { Text(context.viewState.userDisplayName ?? "") - .font(.title3) + .font(.element.title3) .foregroundColor(.element.primaryContent) Text(context.viewState.userID) - .font(.subheadline) + .font(.element.subheadline) .foregroundColor(.element.primaryContent) } } @@ -91,14 +90,38 @@ struct SettingsScreen: View { } } + private var sessionVerificationSection: some View { + Section { + SettingsDefaultRow(title: ElementL10n.settingsSessionVerification, + image: Image(systemName: "checkmark.shield")) { + context.send(viewAction: .sessionVerification) + } + .accessibilityIdentifier("sessionVerificationButton") + } + } + private var simplifiedSection: some View { Section { + SettingsPickerRow(title: ElementL10n.settingsTimelineStyle, + image: Image(systemName: "rectangle.grid.1x2"), + selection: $settings.timelineStyle) { + ForEach(TimelineStyle.allCases, id: \.self) { style in + Text(style.description) + .tag(style) + } + } + .accessibilityIdentifier("timelineStylePicker") + SettingsDefaultRow(title: ElementL10n.sendBugReport, image: Image(systemName: "questionmark.circle")) { context.send(viewAction: .reportBug) } .accessibilityIdentifier("reportBugButton") - + } + } + + private var signOutSection: some View { + Section { SettingsDefaultRow(title: ElementL10n.actionSignOut, image: Image(systemName: "rectangle.portrait.and.arrow.right")) { showingLogoutConfirmation = true @@ -112,76 +135,20 @@ struct SettingsScreen: View { } message: { Text(ElementL10n.actionSignOutConfirmationSimple) } - } - } - - private var analyticsSection: some View { - Section { - Button { context.send(viewAction: .reportBug) } label: { - HStack { - Text(ElementL10n.sendBugReport) - Spacer() - Image(systemName: "chevron.forward") - .foregroundColor(.element.tertiaryContent) - } - } - .listRowInsets(listRowInsets) - .listRowSeparator(.hidden) - .foregroundColor(.element.primaryContent) - .accessibilityIdentifier("reportBugButton") - } - } - - @ViewBuilder - private var userInterfaceSection: some View { - Section { - Picker(ElementL10n.settingsTimelineStyle, selection: $settings.timelineStyle) { - ForEach(TimelineStyle.allCases, id: \.self) { style in - Text(style.description) - .tag(style) - } - } - .listRowInsets(listRowInsets) - .accessibilityIdentifier("timelineStylePicker") - } - } - - private var logoutSection: some View { - Section { - Button { showingLogoutConfirmation = true } label: { - HStack { - Image(systemName: "rectangle.portrait.and.arrow.right") - .foregroundColor(.element.systemGray) - .padding(4) - .background(Color.element.systemGray6) - .clipShape(Circle()) - .frame(width: menuIconSize, height: menuIconSize) - Text(ElementL10n.actionSignOut) - Spacer() - Image(systemName: "chevron.forward") - .foregroundColor(.element.tertiaryContent) - } - } - .listRowInsets(listRowInsets) - .foregroundColor(.element.primaryContent) - .accessibilityIdentifier("logoutButton") - .alert(ElementL10n.actionSignOut, - isPresented: $showingLogoutConfirmation) { - Button(ElementL10n.actionSignOut, - role: .destructive, - action: logout) - } message: { - Text(ElementL10n.actionSignOutConfirmationSimple) - } } footer: { VStack { versionText + .font(.element.caption1) + .foregroundColor(.element.tertiaryContent) .frame(maxWidth: .infinity) if let deviceId = context.viewState.deviceID { Text(deviceId) + .font(.element.caption1) + .foregroundColor(.element.tertiaryContent) } } + .padding(.top, 24) } } diff --git a/ElementX/Sources/Services/UserSession/UserSessionFlowCoordinator.swift b/ElementX/Sources/Services/UserSession/UserSessionFlowCoordinator.swift index 8b85c769a..964b8b8ae 100644 --- a/ElementX/Sources/Services/UserSession/UserSessionFlowCoordinator.swift +++ b/ElementX/Sources/Services/UserSession/UserSessionFlowCoordinator.swift @@ -31,7 +31,7 @@ class UserSessionFlowCoordinator: CoordinatorProtocol { private let sidebarNavigationStackCoordinator: NavigationStackCoordinator private let detailNavigationStackCoordinator: NavigationStackCoordinator - + var callback: ((UserSessionFlowCoordinatorAction) -> Void)? init(userSession: UserSessionProtocol, @@ -91,7 +91,7 @@ class UserSessionFlowCoordinator: CoordinatorProtocol { self.presentSessionVerification() case (.sessionVerificationScreen, .dismissedSessionVerificationScreen, .roomList): break - + case (.roomList, .showSettingsScreen, .settingsScreen): self.presentSettingsScreen() case (.settingsScreen, .dismissedSettingsScreen, .roomList): diff --git a/ElementX/Sources/Services/UserSession/UserSessionFlowCoordinatorStateMachine.swift b/ElementX/Sources/Services/UserSession/UserSessionFlowCoordinatorStateMachine.swift index 12d2ad5e8..fe3ea300e 100644 --- a/ElementX/Sources/Services/UserSession/UserSessionFlowCoordinatorStateMachine.swift +++ b/ElementX/Sources/Services/UserSession/UserSessionFlowCoordinatorStateMachine.swift @@ -28,7 +28,7 @@ class UserSessionFlowCoordinatorStateMachine { /// Showing the session verification flows case sessionVerificationScreen(selectedRoomId: String?) - + /// Showing the session verification flows case feedbackScreen(selectedRoomId: String?)