From 41c3a0573d5d681c07d3456ae96c0a06417aa431 Mon Sep 17 00:00:00 2001 From: Valentin Perignon Date: Wed, 29 Mar 2023 10:00:30 +0200 Subject: [PATCH 01/21] feat: Add contextMenu (without preview) --- Mail/Components/RecipientChip.swift | 76 ++++++++++++++++++++++++++++ Mail/Components/RecipientField.swift | 19 ------- 2 files changed, 76 insertions(+), 19 deletions(-) create mode 100644 Mail/Components/RecipientChip.swift diff --git a/Mail/Components/RecipientChip.swift b/Mail/Components/RecipientChip.swift new file mode 100644 index 000000000..e8240dc0a --- /dev/null +++ b/Mail/Components/RecipientChip.swift @@ -0,0 +1,76 @@ +/* + Infomaniak Mail - iOS App + Copyright (C) 2022 Infomaniak Network SA + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + */ + +import MailCore +import MailResources +import SwiftUI + +struct RecipientChip: View { + let recipient: Recipient + let removeButtonTapped: () -> Void + + @AppStorage(UserDefaults.shared.key(.accentColor)) private var accentColor = DefaultPreferences.accentColor + + var body: some View { + Text(recipient.name.isEmpty ? recipient.email : recipient.name) + .textStyle(.bodyAccent) + .lineLimit(1) + .padding(.horizontal, 8) + .padding(.vertical, 4) + .background(Capsule().fill(accentColor.secondary.swiftUIColor)) + .modifier(RecipientDetailsModifier()) + } +} + +struct RecipientContextMenu: View { + var body: some View { + Button { + // Copy mail address + } label: { + Label(MailResourcesStrings.Localizable.contactActionCopyEmailAddress, image: MailResourcesAsset.duplicate.name) + } + + Button { + // Remove recipient + } label: { + Label(MailResourcesStrings.Localizable.actionDelete, image: MailResourcesAsset.bin.name) + } + } +} + +struct RecipientDetailsModifier: ViewModifier { + func body(content: Content) -> some View { + if #available(iOS 16.0, *) { + content + .contextMenu { + RecipientContextMenu() + } preview: { + Text("Contact Preview") + } + } else { + content + .contextMenu { RecipientContextMenu() } + } + } +} + +struct RecipientChip_Previews: PreviewProvider { + static var previews: some View { + RecipientChip(recipient: PreviewHelper.sampleRecipient1) { /* Preview */ } + } +} diff --git a/Mail/Components/RecipientField.swift b/Mail/Components/RecipientField.swift index 1815874d8..35646cf3c 100644 --- a/Mail/Components/RecipientField.swift +++ b/Mail/Components/RecipientField.swift @@ -25,25 +25,6 @@ import RealmSwift import SwiftUI import WrappingHStack -struct RecipientChip: View { - let recipient: Recipient - let removeButtonTapped: () -> Void - - @AppStorage(UserDefaults.shared.key(.accentColor)) private var accentColor = DefaultPreferences.accentColor - - var body: some View { - Button(action: removeButtonTapped) { - Text(recipient.name.isEmpty ? recipient.email : recipient.name) - .textStyle(.bodyAccent) - .padding(.vertical, 6) - .lineLimit(1) - } - .padding(.leading, 12) - .padding(.trailing, 12) - .background(Capsule().fill(accentColor.secondary.swiftUIColor)) - } -} - struct RecipientField: View { @Binding var recipients: RealmSwift.List @Binding var autocompletion: [Recipient] From fc440907d8bd1295504bea91d5d48a269a705ed1 Mon Sep 17 00:00:00 2001 From: Valentin Perignon Date: Wed, 29 Mar 2023 10:10:57 +0200 Subject: [PATCH 02/21] refactor: RecipientCell --- ...mpletionCell.swift => RecipientCell.swift} | 10 +++---- .../New Message/AutocompletionView.swift | 2 +- Mail/Views/New Message/NewMessageCell.swift | 26 ++++++++++--------- 3 files changed, 20 insertions(+), 18 deletions(-) rename Mail/Components/{RecipientAutocompletionCell.swift => RecipientCell.swift} (86%) diff --git a/Mail/Components/RecipientAutocompletionCell.swift b/Mail/Components/RecipientCell.swift similarity index 86% rename from Mail/Components/RecipientAutocompletionCell.swift rename to Mail/Components/RecipientCell.swift index ab8926bbd..fb637b42c 100644 --- a/Mail/Components/RecipientAutocompletionCell.swift +++ b/Mail/Components/RecipientCell.swift @@ -19,11 +19,11 @@ import MailCore import SwiftUI -struct RecipientAutocompletionCell: View { +struct RecipientCell: View { let recipient: Recipient var body: some View { - HStack { + HStack(spacing: 8) { AvatarView(avatarDisplayable: recipient, size: 40) .accessibilityHidden(true) @@ -38,9 +38,9 @@ struct RecipientAutocompletionCell: View { .textStyle(.bodySecondary) } } - Spacer() } .lineLimit(1) + .frame(maxWidth: .infinity, alignment: .leading) .accessibilityElement(children: .combine) .accessibilityAddTraits(.isButton) } @@ -48,7 +48,7 @@ struct RecipientAutocompletionCell: View { struct RecipientAutocompletionCell_Previews: PreviewProvider { static var previews: some View { - RecipientAutocompletionCell(recipient: PreviewHelper.sampleRecipient1) - RecipientAutocompletionCell(recipient: PreviewHelper.sampleRecipient3) + RecipientCell(recipient: PreviewHelper.sampleRecipient1) + RecipientCell(recipient: PreviewHelper.sampleRecipient3) } } diff --git a/Mail/Views/New Message/AutocompletionView.swift b/Mail/Views/New Message/AutocompletionView.swift index 4fb834bee..4acecb730 100644 --- a/Mail/Views/New Message/AutocompletionView.swift +++ b/Mail/Views/New Message/AutocompletionView.swift @@ -33,7 +33,7 @@ struct AutocompletionView: View { Button { onSelect(recipient) } label: { - RecipientAutocompletionCell(recipient: recipient) + RecipientCell(recipient: recipient) } .padding(.horizontal, 8) diff --git a/Mail/Views/New Message/NewMessageCell.swift b/Mail/Views/New Message/NewMessageCell.swift index c64b07b01..fdd8f1520 100644 --- a/Mail/Views/New Message/NewMessageCell.swift +++ b/Mail/Views/New Message/NewMessageCell.swift @@ -77,19 +77,21 @@ struct NewMessageCell: View where Content: View { } } -struct RecipientCellView_Previews: PreviewProvider { +struct NewMessageCell_Previews: PreviewProvider { static var previews: some View { - NewMessageCell(type: .to, - showCc: .constant(false)) { - RecipientField(recipients: .constant([PreviewHelper.sampleRecipient1].toRealmList()), - autocompletion: .constant([]), - unknownRecipientAutocompletion: .constant(""), - addRecipientHandler: .constant { _ in /* Preview */ }, - focusedField: .init(), - type: .to) - } - NewMessageCell(type: .subject) { - TextField("", text: .constant("")) + VStack { + NewMessageCell(type: .to, + showCc: .constant(false)) { + RecipientField(recipients: .constant([PreviewHelper.sampleRecipient1].toRealmList()), + autocompletion: .constant([]), + unknownRecipientAutocompletion: .constant(""), + addRecipientHandler: .constant { _ in /* Preview */ }, + focusedField: .init(), + type: .to) + } + NewMessageCell(type: .subject) { + TextField("", text: .constant("")) + } } } } From 687e8e6f23f38d7b7fd6be4e9c2d8ec8f900bf51 Mon Sep 17 00:00:00 2001 From: Valentin Perignon Date: Mon, 3 Apr 2023 16:22:02 +0200 Subject: [PATCH 03/21] feat: Show preview --- Mail/Components/RecipientChip.swift | 24 +++++++++++++++++------- 1 file changed, 17 insertions(+), 7 deletions(-) diff --git a/Mail/Components/RecipientChip.swift b/Mail/Components/RecipientChip.swift index e8240dc0a..8e4ba9afd 100644 --- a/Mail/Components/RecipientChip.swift +++ b/Mail/Components/RecipientChip.swift @@ -16,13 +16,14 @@ along with this program. If not, see . */ +import InfomaniakCoreUI import MailCore import MailResources import SwiftUI struct RecipientChip: View { let recipient: Recipient - let removeButtonTapped: () -> Void + let removeHandler: () -> Void @AppStorage(UserDefaults.shared.key(.accentColor)) private var accentColor = DefaultPreferences.accentColor @@ -33,20 +34,24 @@ struct RecipientChip: View { .padding(.horizontal, 8) .padding(.vertical, 4) .background(Capsule().fill(accentColor.secondary.swiftUIColor)) - .modifier(RecipientDetailsModifier()) + .modifier(RecipientDetailsModifier(recipient: recipient, removeHandler: removeHandler)) } } struct RecipientContextMenu: View { + let recipient: Recipient + let removeHandler: () -> Void + var body: some View { Button { - // Copy mail address + UIPasteboard.general.string = recipient.email + IKSnackBar.showSnackBar(message: MailResourcesStrings.Localizable.snackbarEmailCopiedToClipboard) } label: { Label(MailResourcesStrings.Localizable.contactActionCopyEmailAddress, image: MailResourcesAsset.duplicate.name) } Button { - // Remove recipient + removeHandler() } label: { Label(MailResourcesStrings.Localizable.actionDelete, image: MailResourcesAsset.bin.name) } @@ -54,17 +59,22 @@ struct RecipientContextMenu: View { } struct RecipientDetailsModifier: ViewModifier { + let recipient: Recipient + let removeHandler: () -> Void + func body(content: Content) -> some View { if #available(iOS 16.0, *) { content .contextMenu { - RecipientContextMenu() + RecipientContextMenu(recipient: recipient, removeHandler: removeHandler) } preview: { - Text("Contact Preview") + RecipientCell(recipient: recipient) + .padding(.vertical, 8) + .padding(.horizontal, 16) } } else { content - .contextMenu { RecipientContextMenu() } + .contextMenu { RecipientContextMenu(recipient: recipient, removeHandler: removeHandler) } } } } From 9b39386ce47141b3a284de6421a1d55499784ce0 Mon Sep 17 00:00:00 2001 From: Valentin Perignon Date: Wed, 12 Apr 2023 11:23:33 +0200 Subject: [PATCH 04/21] feat: Try popover library --- .package.resolved | 9 +++ Mail/Components/RecipientChip.swift | 60 ++++++++++++++++--- .../Search/SearchContactsSectionView.swift | 2 +- Project.swift | 6 +- 4 files changed, 67 insertions(+), 10 deletions(-) diff --git a/.package.resolved b/.package.resolved index aff26dd01..8f2d1d3c8 100644 --- a/.package.resolved +++ b/.package.resolved @@ -143,6 +143,15 @@ "version" : "12.1.0" } }, + { + "identity" : "popovers", + "kind" : "remoteSourceControl", + "location" : "https://github.com/aheze/Popovers", + "state" : { + "revision" : "de44c4dd7271ec6413fe350f7efadb14e5e18dce", + "version" : "1.3.2" + } + }, { "identity" : "realm-core", "kind" : "remoteSourceControl", diff --git a/Mail/Components/RecipientChip.swift b/Mail/Components/RecipientChip.swift index 8e4ba9afd..2fa6628c2 100644 --- a/Mail/Components/RecipientChip.swift +++ b/Mail/Components/RecipientChip.swift @@ -21,20 +21,66 @@ import MailCore import MailResources import SwiftUI +import Popovers + struct RecipientChip: View { + @State private var showPopover = false + let recipient: Recipient let removeHandler: () -> Void @AppStorage(UserDefaults.shared.key(.accentColor)) private var accentColor = DefaultPreferences.accentColor var body: some View { - Text(recipient.name.isEmpty ? recipient.email : recipient.name) - .textStyle(.bodyAccent) - .lineLimit(1) - .padding(.horizontal, 8) - .padding(.vertical, 4) - .background(Capsule().fill(accentColor.secondary.swiftUIColor)) - .modifier(RecipientDetailsModifier(recipient: recipient, removeHandler: removeHandler)) + Templates.Menu { + RecipientCell(recipient: recipient) + .padding(.vertical, 8) + .padding(.horizontal, 16) + + Templates.MenuDivider() + + Templates.MenuButton(text: Text(MailResourcesStrings.Localizable.contactActionCopyEmailAddress), + image: MailResourcesAsset.duplicate.swiftUIImage) { + UIPasteboard.general.string = recipient.email + IKSnackBar.showSnackBar(message: MailResourcesStrings.Localizable.snackbarEmailCopiedToClipboard) + } + + Templates.MenuButton(text: Text(MailResourcesStrings.Localizable.actionDelete), + image: MailResourcesAsset.bin.swiftUIImage) { + removeHandler() + } + } label: { _ in + Text(recipient.name.isEmpty ? recipient.email : recipient.name) + .textStyle(.bodyAccent) + .lineLimit(1) + .padding(.horizontal, 8) + .padding(.vertical, 4) + .background(Capsule().fill(accentColor.secondary.swiftUIColor)) + } + + +// Text(recipient.name.isEmpty ? recipient.email : recipient.name) +// .textStyle(.bodyAccent) +// .lineLimit(1) +// .padding(.horizontal, 8) +// .padding(.vertical, 4) +// .background(Capsule().fill(accentColor.secondary.swiftUIColor)) +// .onTapGesture { +// showPopover = true +// } +// .popover(present: $showPopover) { +// Templates.MenuItem { +// print("hello") +// } label: { _ in +// Text("Hello") +// } +// +// +// RecipientContextMenu(recipient: recipient) { +// showPopover = false +// removeHandler() +// } +// } } } diff --git a/Mail/Views/Search/SearchContactsSectionView.swift b/Mail/Views/Search/SearchContactsSectionView.swift index 7b1e0a210..86488a46f 100644 --- a/Mail/Views/Search/SearchContactsSectionView.swift +++ b/Mail/Views/Search/SearchContactsSectionView.swift @@ -28,7 +28,7 @@ struct SearchContactsSectionView: View { var body: some View { Section { ForEach(viewModel.contacts) { contact in - RecipientAutocompletionCell(recipient: contact) + RecipientCell(recipient: contact) .onTapGesture { viewModel.matomo.track(eventWithCategory: .search, name: "selectContact") Constants.globallyResignFirstResponder() diff --git a/Project.swift b/Project.swift index b14aae396..ddb50d720 100644 --- a/Project.swift +++ b/Project.swift @@ -47,7 +47,8 @@ let project = Project(name: "Mail", .package(url: "https://github.com/SCENEE/FloatingPanel", .upToNextMajor(from: "2.0.0")), .package(url: "https://github.com/kean/Nuke", .upToNextMajor(from: "12.0.0")), .package(url: "https://github.com/airbnb/lottie-ios", .exact("3.5.0")), - .package(url: "https://github.com/scinfu/SwiftSoup", .upToNextMajor(from: "2.5.3")) + .package(url: "https://github.com/scinfu/SwiftSoup", .upToNextMajor(from: "2.5.3")), + .package(url: "https://github.com/aheze/Popovers", .upToNextMajor(from: "1.3.2")) ], targets: [ Target(name: "Mail", @@ -77,7 +78,8 @@ let project = Project(name: "Mail", .package(product: "Shimmer"), .package(product: "WrappingHStack"), .package(product: "FloatingPanel"), - .package(product: "Lottie") + .package(product: "Lottie"), + .package(product: "Popovers") ], settings: .settings(base: baseSettings), environment: ["hostname": "\(ProcessInfo.processInfo.hostName)."]), From f970e2923a717e470fbe5dce9b5b6fe18b4176a9 Mon Sep 17 00:00:00 2001 From: Valentin Perignon Date: Wed, 12 Apr 2023 14:47:27 +0200 Subject: [PATCH 05/21] feat: iOS-like menu --- Mail/Components/RecipientChip.swift | 79 +++-------------------------- 1 file changed, 8 insertions(+), 71 deletions(-) diff --git a/Mail/Components/RecipientChip.swift b/Mail/Components/RecipientChip.swift index 2fa6628c2..e747f512f 100644 --- a/Mail/Components/RecipientChip.swift +++ b/Mail/Components/RecipientChip.swift @@ -19,12 +19,11 @@ import InfomaniakCoreUI import MailCore import MailResources -import SwiftUI - import Popovers +import SwiftUI struct RecipientChip: View { - @State private var showPopover = false + @Environment(\.window) private var window let recipient: Recipient let removeHandler: () -> Void @@ -33,11 +32,13 @@ struct RecipientChip: View { var body: some View { Templates.Menu { + $0.width = nil + $0.originAnchor = .topLeft + } content: { RecipientCell(recipient: recipient) .padding(.vertical, 8) .padding(.horizontal, 16) - - Templates.MenuDivider() + .frame(maxWidth: min(300, window?.screen.bounds.width ?? 300)) Templates.MenuButton(text: Text(MailResourcesStrings.Localizable.contactActionCopyEmailAddress), image: MailResourcesAsset.duplicate.swiftUIImage) { @@ -49,78 +50,14 @@ struct RecipientChip: View { image: MailResourcesAsset.bin.swiftUIImage) { removeHandler() } - } label: { _ in + } label: { isSelected in Text(recipient.name.isEmpty ? recipient.email : recipient.name) .textStyle(.bodyAccent) .lineLimit(1) .padding(.horizontal, 8) .padding(.vertical, 4) .background(Capsule().fill(accentColor.secondary.swiftUIColor)) - } - - -// Text(recipient.name.isEmpty ? recipient.email : recipient.name) -// .textStyle(.bodyAccent) -// .lineLimit(1) -// .padding(.horizontal, 8) -// .padding(.vertical, 4) -// .background(Capsule().fill(accentColor.secondary.swiftUIColor)) -// .onTapGesture { -// showPopover = true -// } -// .popover(present: $showPopover) { -// Templates.MenuItem { -// print("hello") -// } label: { _ in -// Text("Hello") -// } -// -// -// RecipientContextMenu(recipient: recipient) { -// showPopover = false -// removeHandler() -// } -// } - } -} - -struct RecipientContextMenu: View { - let recipient: Recipient - let removeHandler: () -> Void - - var body: some View { - Button { - UIPasteboard.general.string = recipient.email - IKSnackBar.showSnackBar(message: MailResourcesStrings.Localizable.snackbarEmailCopiedToClipboard) - } label: { - Label(MailResourcesStrings.Localizable.contactActionCopyEmailAddress, image: MailResourcesAsset.duplicate.name) - } - - Button { - removeHandler() - } label: { - Label(MailResourcesStrings.Localizable.actionDelete, image: MailResourcesAsset.bin.name) - } - } -} - -struct RecipientDetailsModifier: ViewModifier { - let recipient: Recipient - let removeHandler: () -> Void - - func body(content: Content) -> some View { - if #available(iOS 16.0, *) { - content - .contextMenu { - RecipientContextMenu(recipient: recipient, removeHandler: removeHandler) - } preview: { - RecipientCell(recipient: recipient) - .padding(.vertical, 8) - .padding(.horizontal, 16) - } - } else { - content - .contextMenu { RecipientContextMenu(recipient: recipient, removeHandler: removeHandler) } + .opacity(isSelected ? 0.8 : 1) } } } From 4e488c39a16bbb7708c88ac60f50dfc286f62855 Mon Sep 17 00:00:00 2001 From: Valentin Perignon Date: Thu, 13 Apr 2023 13:32:02 +0200 Subject: [PATCH 06/21] refactor: Replace TextField with UITextField --- Mail/Components/RecipientField.swift | 34 ++++--- .../New Message/RecipientsTextField.swift | 95 +++++++++++++++++++ 2 files changed, 115 insertions(+), 14 deletions(-) create mode 100644 Mail/Views/New Message/RecipientsTextField.swift diff --git a/Mail/Components/RecipientField.swift b/Mail/Components/RecipientField.swift index 35646cf3c..0f44e0bfd 100644 --- a/Mail/Components/RecipientField.swift +++ b/Mail/Components/RecipientField.swift @@ -46,20 +46,9 @@ struct RecipientField: View { } .alignmentGuide(.newMessageCellAlignment) { d in d[.top] + 21 } } - TextField("", text: $currentText) - .textContentType(.emailAddress) - .keyboardType(.emailAddress) - .autocapitalization(.none) - .disableAutocorrection(true) - .multilineTextAlignment(.leading) + + RecipientsTextFieldView(text: $currentText, onSubmit: submitTextField, onBackspace: handleBackspaceTextField) .focused($focusedField, equals: type) - .onSubmit { - guard let recipient = autocompletion.first else { return } - add(recipient: recipient) - focusedField = type - @InjectService var matomo: MatomoUtils - matomo.track(eventWithCategory: .newMessage, action: .input, name: "addNewRecipient") - } } .onChange(of: currentText) { _ in updateAutocompletion() @@ -76,12 +65,29 @@ struct RecipientField: View { } } + @MainActor private func submitTextField() { + guard let recipient = autocompletion.first else { + IKSnackBar.showSnackBar( + message: MailResourcesStrings.Localizable.addUnknownRecipientInvalidEmail, + anchor: keyboardHeight + ) + return + } + add(recipient: recipient) + @InjectService var matomo: MatomoUtils + matomo.track(eventWithCategory: .newMessage, action: .input, name: "addNewRecipient") + } + + private func handleBackspaceTextField() { + print("Backspace") + } + private func updateAutocompletion() { let trimmedCurrentText = currentText.trimmingCharacters(in: .whitespacesAndNewlines) let contactManager = AccountManager.instance.currentContactManager let autocompleteContacts = contactManager?.contacts(matching: trimmedCurrentText) ?? [] - var autocompleteRecipients = autocompleteContacts.map { Recipient(email: $0.email, name: $0.name) } + let autocompleteRecipients = autocompleteContacts.map { Recipient(email: $0.email, name: $0.name) } withAnimation { autocompletion = autocompleteRecipients.filter { !recipients.map(\.email).contains($0.email) } diff --git a/Mail/Views/New Message/RecipientsTextField.swift b/Mail/Views/New Message/RecipientsTextField.swift new file mode 100644 index 000000000..5f2f90ab8 --- /dev/null +++ b/Mail/Views/New Message/RecipientsTextField.swift @@ -0,0 +1,95 @@ +/* + Infomaniak Mail - iOS App + Copyright (C) 2022 Infomaniak Network SA + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + */ + +import SwiftUI +import UIKit + +struct RecipientsTextFieldView: UIViewRepresentable { + @Binding var text: String + + let onSubmit: () -> Void + let onBackspace: () -> Void + + func makeUIView(context: Context) -> UITextField { + let textField = RecipientsTextField() + textField.delegate = context.coordinator + textField.addTarget(context.coordinator, action: #selector(context.coordinator.textDidChanged(_:)), for: .editingChanged) + textField.onBackspace = onBackspace + return textField + } + + func updateUIView(_ textField: UITextField, context: Context) { + guard textField.text != text else { return } + textField.text = text + } + + func makeCoordinator() -> Coordinator { + return Coordinator(self) + } + + class Coordinator: NSObject, UITextFieldDelegate { + let parent: RecipientsTextFieldView + + init(_ parent: RecipientsTextFieldView) { + self.parent = parent + } + + func textFieldShouldReturn(_ textField: UITextField) -> Bool { + guard textField.text?.isEmpty == false else { + textField.resignFirstResponder() + return true + } + + parent.onSubmit() + return true + } + + @objc func textDidChanged(_ textField: UITextField) { + parent.text = textField.text ?? "" + } + } +} + +/* + * We need to create our own UITextField to benefit from the `deleteBackward()` function + */ +class RecipientsTextField: UITextField { + var onBackspace: (() -> Void)? + + override init(frame: CGRect) { + super.init(frame: frame) + setUpView() + } + + required init?(coder: NSCoder) { + super.init(coder: coder) + setUpView() + } + + private func setUpView() { + textContentType = .emailAddress + keyboardType = .emailAddress + autocapitalizationType = .none + autocorrectionType = .no + } + + override func deleteBackward() { + super.deleteBackward() + onBackspace?() + } +} From 659c9b5937579799f219613f351821f0ad74463c Mon Sep 17 00:00:00 2001 From: Valentin Perignon Date: Thu, 13 Apr 2023 16:02:20 +0200 Subject: [PATCH 07/21] feat: Focus last chip on backspace --- Mail/Components/RecipientChip.swift | 7 ++++--- Mail/Components/RecipientField.swift | 10 ++++++++-- MailCore/Utils/MailTextStyle.swift | 5 +++++ 3 files changed, 17 insertions(+), 5 deletions(-) diff --git a/Mail/Components/RecipientChip.swift b/Mail/Components/RecipientChip.swift index e747f512f..fb98ef89e 100644 --- a/Mail/Components/RecipientChip.swift +++ b/Mail/Components/RecipientChip.swift @@ -26,6 +26,7 @@ struct RecipientChip: View { @Environment(\.window) private var window let recipient: Recipient + let isFocused: Bool let removeHandler: () -> Void @AppStorage(UserDefaults.shared.key(.accentColor)) private var accentColor = DefaultPreferences.accentColor @@ -52,11 +53,11 @@ struct RecipientChip: View { } } label: { isSelected in Text(recipient.name.isEmpty ? recipient.email : recipient.name) - .textStyle(.bodyAccent) + .textStyle(isFocused ? .bodyAccentSecondary : .bodyAccent) .lineLimit(1) .padding(.horizontal, 8) .padding(.vertical, 4) - .background(Capsule().fill(accentColor.secondary.swiftUIColor)) + .background(Capsule().fill(isFocused ? Color.accentColor : accentColor.secondary.swiftUIColor)) .opacity(isSelected ? 0.8 : 1) } } @@ -64,6 +65,6 @@ struct RecipientChip: View { struct RecipientChip_Previews: PreviewProvider { static var previews: some View { - RecipientChip(recipient: PreviewHelper.sampleRecipient1) { /* Preview */ } + RecipientChip(recipient: PreviewHelper.sampleRecipient1, isFocused: false) { /* Preview */ } } } diff --git a/Mail/Components/RecipientField.swift b/Mail/Components/RecipientField.swift index 0f44e0bfd..6a7d0500a 100644 --- a/Mail/Components/RecipientField.swift +++ b/Mail/Components/RecipientField.swift @@ -35,12 +35,13 @@ struct RecipientField: View { @State private var currentText = "" @State private var keyboardHeight: CGFloat = 0 + @State private var lastChipIsFocused = false var body: some View { VStack { if !recipients.isEmpty { WrappingHStack(recipients.indices, spacing: .constant(8), lineSpacing: 8) { i in - RecipientChip(recipient: recipients[i]) { + RecipientChip(recipient: recipients[i], isFocused: i == recipients.count - 1 && lastChipIsFocused) { remove(recipientAt: i) } } @@ -79,7 +80,12 @@ struct RecipientField: View { } private func handleBackspaceTextField() { - print("Backspace") + if lastChipIsFocused { + remove(recipientAt: recipients.count - 1) + lastChipIsFocused = false + } else { + lastChipIsFocused = true + } } private func updateAutocompletion() { diff --git a/MailCore/Utils/MailTextStyle.swift b/MailCore/Utils/MailTextStyle.swift index 51f886cc5..d5036a39e 100644 --- a/MailCore/Utils/MailTextStyle.swift +++ b/MailCore/Utils/MailTextStyle.swift @@ -105,6 +105,11 @@ public struct MailTextStyle { color: .accentColor ) + public static let bodyAccentSecondary = MailTextStyle( + font: .system(size: 16), + color: UserDefaults.shared.accentColor.secondary + ) + public static let bodySecondary = MailTextStyle( font: .system(size: 16), color: MailResourcesAsset.textSecondaryColor From 65d205b1c9b2827f5716a9ddbe4e969544bab38f Mon Sep 17 00:00:00 2001 From: Valentin Perignon Date: Thu, 13 Apr 2023 16:17:17 +0200 Subject: [PATCH 08/21] feat: Focus is lost when typing/switching textfield --- Mail/Components/RecipientField.swift | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/Mail/Components/RecipientField.swift b/Mail/Components/RecipientField.swift index 6a7d0500a..9d335a3a7 100644 --- a/Mail/Components/RecipientField.swift +++ b/Mail/Components/RecipientField.swift @@ -52,9 +52,13 @@ struct RecipientField: View { .focused($focusedField, equals: type) } .onChange(of: currentText) { _ in + lastChipIsFocused = false updateAutocompletion() addRecipientHandler = add(recipient:) } + .onChange(of: focusedField) { _ in + lastChipIsFocused = false + } .onReceive(NotificationCenter.default.publisher(for: UIResponder.keyboardDidShowNotification)) { output in if let userInfo = output.userInfo, let keyboardFrame = userInfo[UIResponder.keyboardFrameEndUserInfoKey] as? CGRect { From 541273c9be525d0dfe669cb7870d95a1193539e7 Mon Sep 17 00:00:00 2001 From: Valentin Perignon Date: Mon, 17 Apr 2023 16:58:39 +0200 Subject: [PATCH 09/21] feat: Create custom UIKit chip --- Mail/Components/RecipientChip.swift | 12 ++- Mail/Components/RecipientChipLabel.swift | 98 +++++++++++++++++++++ Mail/Components/RecipientField.swift | 5 ++ Mail/Views/Thread List/ThreadListView.swift | 36 +++++--- MailCore/UI/UIConstants.swift | 2 + 5 files changed, 136 insertions(+), 17 deletions(-) create mode 100644 Mail/Components/RecipientChipLabel.swift diff --git a/Mail/Components/RecipientChip.swift b/Mail/Components/RecipientChip.swift index fb98ef89e..8e10546fe 100644 --- a/Mail/Components/RecipientChip.swift +++ b/Mail/Components/RecipientChip.swift @@ -24,12 +24,13 @@ import SwiftUI struct RecipientChip: View { @Environment(\.window) private var window + @AppStorage(UserDefaults.shared.key(.accentColor)) private var accentColor = DefaultPreferences.accentColor let recipient: Recipient let isFocused: Bool let removeHandler: () -> Void - @AppStorage(UserDefaults.shared.key(.accentColor)) private var accentColor = DefaultPreferences.accentColor + @FocusState var isLastChipFocused: Bool var body: some View { Templates.Menu { @@ -52,12 +53,9 @@ struct RecipientChip: View { removeHandler() } } label: { isSelected in - Text(recipient.name.isEmpty ? recipient.email : recipient.name) - .textStyle(isFocused ? .bodyAccentSecondary : .bodyAccent) - .lineLimit(1) - .padding(.horizontal, 8) - .padding(.vertical, 4) - .background(Capsule().fill(isFocused ? Color.accentColor : accentColor.secondary.swiftUIColor)) + RecipientChipLabelView(recipient: recipient, removeHandler: removeHandler) + .fixedSize() + .focused($isLastChipFocused) .opacity(isSelected ? 0.8 : 1) } } diff --git a/Mail/Components/RecipientChipLabel.swift b/Mail/Components/RecipientChipLabel.swift new file mode 100644 index 000000000..26c2814ca --- /dev/null +++ b/Mail/Components/RecipientChipLabel.swift @@ -0,0 +1,98 @@ +/* + Infomaniak Mail - iOS App + Copyright (C) 2022 Infomaniak Network SA + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + */ + +import Foundation +import MailCore +import SwiftUI +import UIKit + +struct RecipientChipLabelView: UIViewRepresentable { + let recipient: Recipient + let removeHandler: () -> Void + + func makeUIView(context: Context) -> RecipientChipLabel { + let label = RecipientChipLabel(recipient: recipient) + label.removeHandler = removeHandler + return label + } + + func updateUIView(_ uiView: RecipientChipLabel, context: Context) { + /* Not implemented on purpose */ + } +} + +class RecipientChipLabel: UILabel, UIKeyInput { + var removeHandler: (() -> Void)? + + override var intrinsicContentSize: CGSize { + var contentSize = super.intrinsicContentSize + contentSize.height += UIConstants.chipInsets.top + UIConstants.chipInsets.bottom + contentSize.width += UIConstants.chipInsets.left + UIConstants.chipInsets.right + return contentSize + } + + override var canBecomeFirstResponder: Bool { return true } + + var hasText = false + + init(recipient: Recipient) { + super.init(frame: .zero) + + text = recipient.name.isEmpty ? recipient.email : recipient.name + textAlignment = .center + numberOfLines = 1 + + font = .systemFont(ofSize: 16) + updateColors(isFirstResponder: false) + + layer.cornerRadius = intrinsicContentSize.height / 2 + layer.masksToBounds = true + } + + @available(*, unavailable) + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override func drawText(in rect: CGRect) { + super.drawText(in: rect.inset(by: UIConstants.chipInsets)) + } + + override func becomeFirstResponder() -> Bool { + updateColors(isFirstResponder: true) + return super.becomeFirstResponder() + } + + override func resignFirstResponder() -> Bool { + updateColors(isFirstResponder: false) + return super.resignFirstResponder() + } + + func insertText(_ text: String) { + /* Not implemented on purpose */ + } + + func deleteBackward() { + removeHandler?() + } + + private func updateColors(isFirstResponder: Bool) { + textColor = isFirstResponder ? UserDefaults.shared.accentColor.secondary.color : .tintColor + backgroundColor = isFirstResponder ? .tintColor : UserDefaults.shared.accentColor.secondary.color + } +} diff --git a/Mail/Components/RecipientField.swift b/Mail/Components/RecipientField.swift index 9d335a3a7..22cb50af6 100644 --- a/Mail/Components/RecipientField.swift +++ b/Mail/Components/RecipientField.swift @@ -31,6 +31,8 @@ struct RecipientField: View { @Binding var unknownRecipientAutocompletion: String @Binding var addRecipientHandler: ((Recipient) -> Void)? @FocusState var focusedField: ComposeViewFieldType? + @FocusState var isLastChipFocused: Bool + @FocusState var focusChip: Bool let type: ComposeViewFieldType @State private var currentText = "" @@ -44,6 +46,7 @@ struct RecipientField: View { RecipientChip(recipient: recipients[i], isFocused: i == recipients.count - 1 && lastChipIsFocused) { remove(recipientAt: i) } + .focused(i == recipients.count - 1 ? $isLastChipFocused : $focusChip) } .alignmentGuide(.newMessageCellAlignment) { d in d[.top] + 21 } } @@ -87,8 +90,10 @@ struct RecipientField: View { if lastChipIsFocused { remove(recipientAt: recipients.count - 1) lastChipIsFocused = false + isLastChipFocused = false } else { lastChipIsFocused = true + isLastChipFocused = true } } diff --git a/Mail/Views/Thread List/ThreadListView.swift b/Mail/Views/Thread List/ThreadListView.swift index 2d0d07376..8c191b90c 100644 --- a/Mail/Views/Thread List/ThreadListView.swift +++ b/Mail/Views/Thread List/ThreadListView.swift @@ -200,12 +200,11 @@ struct ThreadListView: View { flushAlert: $flushAlert, bottomSheet: bottomSheet, viewModel: viewModel, - multipleSelectionViewModel: multipleSelectionViewModel, - selectAll: { - withAnimation(.default.speed(2)) { - multipleSelectionViewModel.selectAll(threads: viewModel.filteredThreads) - } - })) + multipleSelectionViewModel: multipleSelectionViewModel) { + withAnimation(.default.speed(2)) { + multipleSelectionViewModel.selectAll(threads: viewModel.filteredThreads) + } + }) .floatingActionButton(isEnabled: !multipleSelectionViewModel.isEnabled, icon: MailResourcesAsset.pencilPlain, title: MailResourcesStrings.Localizable.buttonNewMessage) { @@ -213,7 +212,7 @@ struct ThreadListView: View { isShowingComposeNewMessageView.toggle() } .floatingPanel(state: bottomSheet, halfOpening: true) { - if case .actions(let target) = bottomSheet.state, !target.isInvalidated { + if case let .actions(target) = bottomSheet.state, !target.isInvalidated { ActionsView(mailboxManager: viewModel.mailboxManager, target: target, state: bottomSheet, @@ -246,7 +245,7 @@ struct ThreadListView: View { ComposeMessageView.newMessage(mailboxManager: viewModel.mailboxManager) } .sheet(isPresented: $moveSheet.isShowing) { - if case .move(let folderId, let handler) = moveSheet.state { + if case let .move(folderId, handler) = moveSheet.state { MoveEmailView.sheetView(mailboxManager: viewModel.mailboxManager, from: folderId, moveHandler: handler) } } @@ -310,8 +309,25 @@ private struct ThreadListToolbar: ViewModifier { multipleSelectionViewModel.isEnabled = false } } - } else { - if isCompact { + } + + ToolbarItem(placement: .principal) { + if !multipleSelectionViewModel.isEnabled { + Text(splitViewManager.selectedFolder?.localizedName ?? "") + .textStyle(.header1) + .frame(maxWidth: .infinity, alignment: .leading) + } + } + + ToolbarItemGroup(placement: .navigationBarTrailing) { + if multipleSelectionViewModel.isEnabled { + Button(multipleSelectionViewModel.selectedItems.count == viewModel.filteredThreads.count + ? MailResourcesStrings.Localizable.buttonUnselectAll + : MailResourcesStrings.Localizable.buttonSelectAll) { + selectAll() + } + .animation(nil, value: multipleSelectionViewModel.selectedItems) + } else { Button { matomo.track(eventWithCategory: .menuDrawer, name: "openByButton") navigationDrawerState.open() diff --git a/MailCore/UI/UIConstants.swift b/MailCore/UI/UIConstants.swift index 1c2b32a15..3881adc45 100644 --- a/MailCore/UI/UIConstants.swift +++ b/MailCore/UI/UIConstants.swift @@ -96,4 +96,6 @@ public enum UIConstants { public static let bottomSheetHorizontalPadding: CGFloat = 24 public static let componentsMaxWidth: CGFloat = 496 + + public static let chipInsets = UIEdgeInsets(top: 4, left: 8, bottom: 4, right: 8) } From 5c588f998d0f333662c4c8326074778fdf54289e Mon Sep 17 00:00:00 2001 From: Valentin Perignon Date: Mon, 17 Apr 2023 17:10:29 +0200 Subject: [PATCH 10/21] refactor: Clean code --- Mail/Components/RecipientChip.swift | 5 ++--- Mail/Components/RecipientField.swift | 20 ++++++-------------- 2 files changed, 8 insertions(+), 17 deletions(-) diff --git a/Mail/Components/RecipientChip.swift b/Mail/Components/RecipientChip.swift index 8e10546fe..baca916ea 100644 --- a/Mail/Components/RecipientChip.swift +++ b/Mail/Components/RecipientChip.swift @@ -27,7 +27,6 @@ struct RecipientChip: View { @AppStorage(UserDefaults.shared.key(.accentColor)) private var accentColor = DefaultPreferences.accentColor let recipient: Recipient - let isFocused: Bool let removeHandler: () -> Void @FocusState var isLastChipFocused: Bool @@ -40,7 +39,7 @@ struct RecipientChip: View { RecipientCell(recipient: recipient) .padding(.vertical, 8) .padding(.horizontal, 16) - .frame(maxWidth: min(300, window?.screen.bounds.width ?? 300)) + .frame(maxWidth: min(304, window?.screen.bounds.width ?? 304)) Templates.MenuButton(text: Text(MailResourcesStrings.Localizable.contactActionCopyEmailAddress), image: MailResourcesAsset.duplicate.swiftUIImage) { @@ -63,6 +62,6 @@ struct RecipientChip: View { struct RecipientChip_Previews: PreviewProvider { static var previews: some View { - RecipientChip(recipient: PreviewHelper.sampleRecipient1, isFocused: false) { /* Preview */ } + RecipientChip(recipient: PreviewHelper.sampleRecipient1) { /* Preview */ } } } diff --git a/Mail/Components/RecipientField.swift b/Mail/Components/RecipientField.swift index 22cb50af6..868300b79 100644 --- a/Mail/Components/RecipientField.swift +++ b/Mail/Components/RecipientField.swift @@ -30,20 +30,21 @@ struct RecipientField: View { @Binding var autocompletion: [Recipient] @Binding var unknownRecipientAutocompletion: String @Binding var addRecipientHandler: ((Recipient) -> Void)? + @FocusState var focusedField: ComposeViewFieldType? - @FocusState var isLastChipFocused: Bool - @FocusState var focusChip: Bool + @FocusState private var isLastChipFocused: Bool + @FocusState private var focusChip: Bool + let type: ComposeViewFieldType @State private var currentText = "" @State private var keyboardHeight: CGFloat = 0 - @State private var lastChipIsFocused = false var body: some View { VStack { if !recipients.isEmpty { WrappingHStack(recipients.indices, spacing: .constant(8), lineSpacing: 8) { i in - RecipientChip(recipient: recipients[i], isFocused: i == recipients.count - 1 && lastChipIsFocused) { + RecipientChip(recipient: recipients[i]) { remove(recipientAt: i) } .focused(i == recipients.count - 1 ? $isLastChipFocused : $focusChip) @@ -55,13 +56,9 @@ struct RecipientField: View { .focused($focusedField, equals: type) } .onChange(of: currentText) { _ in - lastChipIsFocused = false updateAutocompletion() addRecipientHandler = add(recipient:) } - .onChange(of: focusedField) { _ in - lastChipIsFocused = false - } .onReceive(NotificationCenter.default.publisher(for: UIResponder.keyboardDidShowNotification)) { output in if let userInfo = output.userInfo, let keyboardFrame = userInfo[UIResponder.keyboardFrameEndUserInfoKey] as? CGRect { @@ -87,12 +84,7 @@ struct RecipientField: View { } private func handleBackspaceTextField() { - if lastChipIsFocused { - remove(recipientAt: recipients.count - 1) - lastChipIsFocused = false - isLastChipFocused = false - } else { - lastChipIsFocused = true + if currentText.isEmpty { isLastChipFocused = true } } From 9333984eee0e101e1415f09a4c532c087ecbacd3 Mon Sep 17 00:00:00 2001 From: Valentin Perignon Date: Tue, 18 Apr 2023 11:31:57 +0200 Subject: [PATCH 11/21] feat: Update focus when deleted --- Mail/Components/RecipientChip.swift | 13 ++++++++++--- Mail/Components/RecipientField.swift | 2 +- 2 files changed, 11 insertions(+), 4 deletions(-) diff --git a/Mail/Components/RecipientChip.swift b/Mail/Components/RecipientChip.swift index baca916ea..e7a4d3830 100644 --- a/Mail/Components/RecipientChip.swift +++ b/Mail/Components/RecipientChip.swift @@ -27,6 +27,8 @@ struct RecipientChip: View { @AppStorage(UserDefaults.shared.key(.accentColor)) private var accentColor = DefaultPreferences.accentColor let recipient: Recipient + let fieldType: ComposeViewFieldType + @FocusState var focusedField: ComposeViewFieldType? let removeHandler: () -> Void @FocusState var isLastChipFocused: Bool @@ -39,7 +41,7 @@ struct RecipientChip: View { RecipientCell(recipient: recipient) .padding(.vertical, 8) .padding(.horizontal, 16) - .frame(maxWidth: min(304, window?.screen.bounds.width ?? 304)) + .frame(maxWidth: min(304, 0.8 * (window?.screen.bounds.width ?? 304))) Templates.MenuButton(text: Text(MailResourcesStrings.Localizable.contactActionCopyEmailAddress), image: MailResourcesAsset.duplicate.swiftUIImage) { @@ -52,16 +54,21 @@ struct RecipientChip: View { removeHandler() } } label: { isSelected in - RecipientChipLabelView(recipient: recipient, removeHandler: removeHandler) + RecipientChipLabelView(recipient: recipient, removeHandler: removeAndFocus) .fixedSize() .focused($isLastChipFocused) .opacity(isSelected ? 0.8 : 1) } } + + private func removeAndFocus() { + removeHandler() + focusedField = fieldType + } } struct RecipientChip_Previews: PreviewProvider { static var previews: some View { - RecipientChip(recipient: PreviewHelper.sampleRecipient1) { /* Preview */ } + RecipientChip(recipient: PreviewHelper.sampleRecipient1, fieldType: .to) { /* Preview */ } } } diff --git a/Mail/Components/RecipientField.swift b/Mail/Components/RecipientField.swift index 868300b79..c83acb48d 100644 --- a/Mail/Components/RecipientField.swift +++ b/Mail/Components/RecipientField.swift @@ -44,7 +44,7 @@ struct RecipientField: View { VStack { if !recipients.isEmpty { WrappingHStack(recipients.indices, spacing: .constant(8), lineSpacing: 8) { i in - RecipientChip(recipient: recipients[i]) { + RecipientChip(recipient: recipients[i], fieldType: type, focusedField: _focusedField, isLastChipFocused: _isLastChipFocused) { remove(recipientAt: i) } .focused(i == recipients.count - 1 ? $isLastChipFocused : $focusChip) From 8c6f150c8c44b61983a5a830a2ba654965459374 Mon Sep 17 00:00:00 2001 From: Valentin Perignon Date: Tue, 18 Apr 2023 14:47:35 +0200 Subject: [PATCH 12/21] feat: Use only one @FocusState --- Mail/Components/RecipientChip.swift | 3 --- Mail/Components/RecipientChipLabel.swift | 4 ++-- Mail/Components/RecipientField.swift | 13 +++++++------ Mail/Views/New Message/ComposeMessageView.swift | 3 +++ 4 files changed, 12 insertions(+), 11 deletions(-) diff --git a/Mail/Components/RecipientChip.swift b/Mail/Components/RecipientChip.swift index e7a4d3830..9cd48897b 100644 --- a/Mail/Components/RecipientChip.swift +++ b/Mail/Components/RecipientChip.swift @@ -31,8 +31,6 @@ struct RecipientChip: View { @FocusState var focusedField: ComposeViewFieldType? let removeHandler: () -> Void - @FocusState var isLastChipFocused: Bool - var body: some View { Templates.Menu { $0.width = nil @@ -56,7 +54,6 @@ struct RecipientChip: View { } label: { isSelected in RecipientChipLabelView(recipient: recipient, removeHandler: removeAndFocus) .fixedSize() - .focused($isLastChipFocused) .opacity(isSelected ? 0.8 : 1) } } diff --git a/Mail/Components/RecipientChipLabel.swift b/Mail/Components/RecipientChipLabel.swift index 26c2814ca..05c1ad5ac 100644 --- a/Mail/Components/RecipientChipLabel.swift +++ b/Mail/Components/RecipientChipLabel.swift @@ -31,8 +31,8 @@ struct RecipientChipLabelView: UIViewRepresentable { return label } - func updateUIView(_ uiView: RecipientChipLabel, context: Context) { - /* Not implemented on purpose */ + func updateUIView(_ uiLabel: RecipientChipLabel, context: Context) { + uiLabel.text = recipient.name.isEmpty ? recipient.email : recipient.name } } diff --git a/Mail/Components/RecipientField.swift b/Mail/Components/RecipientField.swift index c83acb48d..145571ce8 100644 --- a/Mail/Components/RecipientField.swift +++ b/Mail/Components/RecipientField.swift @@ -32,8 +32,6 @@ struct RecipientField: View { @Binding var addRecipientHandler: ((Recipient) -> Void)? @FocusState var focusedField: ComposeViewFieldType? - @FocusState private var isLastChipFocused: Bool - @FocusState private var focusChip: Bool let type: ComposeViewFieldType @@ -44,10 +42,13 @@ struct RecipientField: View { VStack { if !recipients.isEmpty { WrappingHStack(recipients.indices, spacing: .constant(8), lineSpacing: 8) { i in - RecipientChip(recipient: recipients[i], fieldType: type, focusedField: _focusedField, isLastChipFocused: _isLastChipFocused) { + RecipientChip( + recipient: recipients[i], + fieldType: type, + focusedField: _focusedField) { remove(recipientAt: i) } - .focused(i == recipients.count - 1 ? $isLastChipFocused : $focusChip) + .focused($focusedField, equals: .chip(type.hashValue, recipients[i])) } .alignmentGuide(.newMessageCellAlignment) { d in d[.top] + 21 } } @@ -84,8 +85,8 @@ struct RecipientField: View { } private func handleBackspaceTextField() { - if currentText.isEmpty { - isLastChipFocused = true + if let recipient = recipients.last, currentText.isEmpty { + focusedField = .chip(type.hashValue, recipient) } } diff --git a/Mail/Views/New Message/ComposeMessageView.swift b/Mail/Views/New Message/ComposeMessageView.swift index e5d613ca5..7fa30697e 100644 --- a/Mail/Views/New Message/ComposeMessageView.swift +++ b/Mail/Views/New Message/ComposeMessageView.swift @@ -28,6 +28,7 @@ import SwiftUI enum ComposeViewFieldType: Hashable { case from, to, cc, bcc, subject + case chip(Int, Recipient) var title: String { switch self { @@ -41,6 +42,8 @@ enum ComposeViewFieldType: Hashable { return MailResourcesStrings.Localizable.bccTitle case .subject: return MailResourcesStrings.Localizable.subjectTitle + case .chip: + return "Recipient Chip" } } } From beb5a486dc2a649d8ce707daf23665319ee3278f Mon Sep 17 00:00:00 2001 From: Valentin Perignon Date: Tue, 18 Apr 2023 16:44:08 +0200 Subject: [PATCH 13/21] feat: Switch next and previous with tab key --- Mail/Components/RecipientChip.swift | 11 +++++-- Mail/Components/RecipientChipLabel.swift | 7 ++++- Mail/Components/RecipientField.swift | 30 +++++++++++++++---- .../New Message/ComposeMessageView.swift | 20 ++++++++++++- Mail/Views/New Message/NewMessageCell.swift | 2 +- 5 files changed, 58 insertions(+), 12 deletions(-) diff --git a/Mail/Components/RecipientChip.swift b/Mail/Components/RecipientChip.swift index 9cd48897b..d72f42344 100644 --- a/Mail/Components/RecipientChip.swift +++ b/Mail/Components/RecipientChip.swift @@ -30,6 +30,7 @@ struct RecipientChip: View { let fieldType: ComposeViewFieldType @FocusState var focusedField: ComposeViewFieldType? let removeHandler: () -> Void + let switchFocusHandler: (Bool) -> Void var body: some View { Templates.Menu { @@ -52,20 +53,24 @@ struct RecipientChip: View { removeHandler() } } label: { isSelected in - RecipientChipLabelView(recipient: recipient, removeHandler: removeAndFocus) + RecipientChipLabelView(recipient: recipient, removeHandler: removeAndFocus, switchFocusHandler: switchFocusHandler) .fixedSize() .opacity(isSelected ? 0.8 : 1) } } private func removeAndFocus() { - removeHandler() focusedField = fieldType + removeHandler() } } struct RecipientChip_Previews: PreviewProvider { static var previews: some View { - RecipientChip(recipient: PreviewHelper.sampleRecipient1, fieldType: .to) { /* Preview */ } + RecipientChip(recipient: PreviewHelper.sampleRecipient1, fieldType: .to) { + /* Preview */ + } switchFocusHandler: { _ in + /* Preview */ + } } } diff --git a/Mail/Components/RecipientChipLabel.swift b/Mail/Components/RecipientChipLabel.swift index 05c1ad5ac..5fa655c6b 100644 --- a/Mail/Components/RecipientChipLabel.swift +++ b/Mail/Components/RecipientChipLabel.swift @@ -24,10 +24,12 @@ import UIKit struct RecipientChipLabelView: UIViewRepresentable { let recipient: Recipient let removeHandler: () -> Void + let switchFocusHandler: (Bool) -> Void func makeUIView(context: Context) -> RecipientChipLabel { let label = RecipientChipLabel(recipient: recipient) label.removeHandler = removeHandler + label.switchFocusHandler = switchFocusHandler return label } @@ -38,6 +40,7 @@ struct RecipientChipLabelView: UIViewRepresentable { class RecipientChipLabel: UILabel, UIKeyInput { var removeHandler: (() -> Void)? + var switchFocusHandler: ((Bool) -> Void)? override var intrinsicContentSize: CGSize { var contentSize = super.intrinsicContentSize @@ -84,7 +87,9 @@ class RecipientChipLabel: UILabel, UIKeyInput { } func insertText(_ text: String) { - /* Not implemented on purpose */ + if text == "\t" { + switchFocusHandler?(true) + } } func deleteBackward() { diff --git a/Mail/Components/RecipientField.swift b/Mail/Components/RecipientField.swift index 145571ce8..64c172fc1 100644 --- a/Mail/Components/RecipientField.swift +++ b/Mail/Components/RecipientField.swift @@ -34,6 +34,7 @@ struct RecipientField: View { @FocusState var focusedField: ComposeViewFieldType? let type: ComposeViewFieldType + let switchField: (ComposeViewFieldType, Bool) -> ComposeViewFieldType @State private var currentText = "" @State private var keyboardHeight: CGFloat = 0 @@ -42,13 +43,12 @@ struct RecipientField: View { VStack { if !recipients.isEmpty { WrappingHStack(recipients.indices, spacing: .constant(8), lineSpacing: 8) { i in - RecipientChip( - recipient: recipients[i], - fieldType: type, - focusedField: _focusedField) { + RecipientChip(recipient: recipients[i], fieldType: type, focusedField: _focusedField) { remove(recipientAt: i) + } switchFocusHandler: { next in + switchFocus(next: next) } - .focused($focusedField, equals: .chip(type.hashValue, recipients[i])) + .focused($focusedField, equals: .chip(type.hashValue, recipients[i])) } .alignmentGuide(.newMessageCellAlignment) { d in d[.top] + 21 } } @@ -128,6 +128,24 @@ struct RecipientField: View { $recipients.remove(at: recipientAt) } } + + private func switchFocus(next: Bool) { + guard case let .chip(hash, recipient) = focusedField else { return } + + if next { + if recipient == recipients.last { + focusedField = type + } else if let recipientIndex = recipients.firstIndex(of: recipient) { + focusedField = .chip(hash, recipients[recipientIndex + 1]) + } + } else { + if recipient == recipients.first { + focusedField = switchField(type, false) + } else if let recipientIndex = recipients.firstIndex(of: recipient) { + focusedField = .chip(hash, recipients[recipientIndex - 1]) + } + } + } } struct RecipientField_Previews: PreviewProvider { @@ -139,6 +157,6 @@ struct RecipientField_Previews: PreviewProvider { unknownRecipientAutocompletion: .constant(""), addRecipientHandler: .constant { _ in /* Preview */ }, focusedField: .init(), - type: .to) + type: .to) { _, _ in .to } } } diff --git a/Mail/Views/New Message/ComposeMessageView.swift b/Mail/Views/New Message/ComposeMessageView.swift index 7fa30697e..d15a8df1a 100644 --- a/Mail/Views/New Message/ComposeMessageView.swift +++ b/Mail/Views/New Message/ComposeMessageView.swift @@ -46,6 +46,10 @@ enum ComposeViewFieldType: Hashable { return "Recipient Chip" } } + + static let focusableFields: [Self] = [.to, .cc, .bcc, .subject] + static let minimizedFocusableFields: [Self] = [.to, .subject] + } class NewMessageAlert: SheetState { @@ -258,7 +262,8 @@ struct ComposeMessageView: View { unknownRecipientAutocompletion: $unknownRecipientAutocompletion, addRecipientHandler: $addRecipientHandler, focusedField: _focusedField, - type: type) + type: type, + switchField: switchField) } } } @@ -365,6 +370,19 @@ struct ComposeMessageView: View { } attachmentsManager.completeUploadedAttachments() } + + private func switchField(from field: ComposeViewFieldType, next: Bool) -> ComposeViewFieldType { + let fields = showCc ? ComposeViewFieldType.focusableFields : ComposeViewFieldType.minimizedFocusableFields + + let currentIndex = Int(fields.firstIndex(of: field) ?? 0) + let newIndex: Int + if next { + newIndex = (currentIndex + 1) % fields.count + } else { + newIndex = currentIndex > 0 ? currentIndex - 1 : fields.count - 1 + } + return fields[newIndex] + } } extension ComposeMessageView { diff --git a/Mail/Views/New Message/NewMessageCell.swift b/Mail/Views/New Message/NewMessageCell.swift index fdd8f1520..7226ae3ca 100644 --- a/Mail/Views/New Message/NewMessageCell.swift +++ b/Mail/Views/New Message/NewMessageCell.swift @@ -87,7 +87,7 @@ struct NewMessageCell_Previews: PreviewProvider { unknownRecipientAutocompletion: .constant(""), addRecipientHandler: .constant { _ in /* Preview */ }, focusedField: .init(), - type: .to) + type: .to) { _, _ in return .to } } NewMessageCell(type: .subject) { TextField("", text: .constant("")) From 971bacde72a0994d10465c429d765477103003e8 Mon Sep 17 00:00:00 2001 From: Valentin Perignon Date: Wed, 19 Apr 2023 09:19:29 +0200 Subject: [PATCH 14/21] refactor: Clean code --- Mail/Components/RecipientChip.swift | 5 ++-- Mail/Components/RecipientChipLabel.swift | 6 ++--- Mail/Components/RecipientField.swift | 25 ++++++------------- .../New Message/ComposeMessageView.swift | 24 +++--------------- Mail/Views/New Message/NewMessageCell.swift | 2 +- 5 files changed, 18 insertions(+), 44 deletions(-) diff --git a/Mail/Components/RecipientChip.swift b/Mail/Components/RecipientChip.swift index d72f42344..3789554a0 100644 --- a/Mail/Components/RecipientChip.swift +++ b/Mail/Components/RecipientChip.swift @@ -30,12 +30,13 @@ struct RecipientChip: View { let fieldType: ComposeViewFieldType @FocusState var focusedField: ComposeViewFieldType? let removeHandler: () -> Void - let switchFocusHandler: (Bool) -> Void + let switchFocusHandler: () -> Void var body: some View { Templates.Menu { $0.width = nil $0.originAnchor = .topLeft + $0.popoverAnchor = .topLeft } content: { RecipientCell(recipient: recipient) .padding(.vertical, 8) @@ -69,7 +70,7 @@ struct RecipientChip_Previews: PreviewProvider { static var previews: some View { RecipientChip(recipient: PreviewHelper.sampleRecipient1, fieldType: .to) { /* Preview */ - } switchFocusHandler: { _ in + } switchFocusHandler: { /* Preview */ } } diff --git a/Mail/Components/RecipientChipLabel.swift b/Mail/Components/RecipientChipLabel.swift index 5fa655c6b..aae845c04 100644 --- a/Mail/Components/RecipientChipLabel.swift +++ b/Mail/Components/RecipientChipLabel.swift @@ -24,7 +24,7 @@ import UIKit struct RecipientChipLabelView: UIViewRepresentable { let recipient: Recipient let removeHandler: () -> Void - let switchFocusHandler: (Bool) -> Void + let switchFocusHandler: () -> Void func makeUIView(context: Context) -> RecipientChipLabel { let label = RecipientChipLabel(recipient: recipient) @@ -40,7 +40,7 @@ struct RecipientChipLabelView: UIViewRepresentable { class RecipientChipLabel: UILabel, UIKeyInput { var removeHandler: (() -> Void)? - var switchFocusHandler: ((Bool) -> Void)? + var switchFocusHandler: (() -> Void)? override var intrinsicContentSize: CGSize { var contentSize = super.intrinsicContentSize @@ -88,7 +88,7 @@ class RecipientChipLabel: UILabel, UIKeyInput { func insertText(_ text: String) { if text == "\t" { - switchFocusHandler?(true) + switchFocusHandler?() } } diff --git a/Mail/Components/RecipientField.swift b/Mail/Components/RecipientField.swift index 64c172fc1..39ae76c40 100644 --- a/Mail/Components/RecipientField.swift +++ b/Mail/Components/RecipientField.swift @@ -34,7 +34,6 @@ struct RecipientField: View { @FocusState var focusedField: ComposeViewFieldType? let type: ComposeViewFieldType - let switchField: (ComposeViewFieldType, Bool) -> ComposeViewFieldType @State private var currentText = "" @State private var keyboardHeight: CGFloat = 0 @@ -45,8 +44,8 @@ struct RecipientField: View { WrappingHStack(recipients.indices, spacing: .constant(8), lineSpacing: 8) { i in RecipientChip(recipient: recipients[i], fieldType: type, focusedField: _focusedField) { remove(recipientAt: i) - } switchFocusHandler: { next in - switchFocus(next: next) + } switchFocusHandler: { + switchFocus() } .focused($focusedField, equals: .chip(type.hashValue, recipients[i])) } @@ -129,21 +128,13 @@ struct RecipientField: View { } } - private func switchFocus(next: Bool) { + private func switchFocus() { guard case let .chip(hash, recipient) = focusedField else { return } - if next { - if recipient == recipients.last { - focusedField = type - } else if let recipientIndex = recipients.firstIndex(of: recipient) { - focusedField = .chip(hash, recipients[recipientIndex + 1]) - } - } else { - if recipient == recipients.first { - focusedField = switchField(type, false) - } else if let recipientIndex = recipients.firstIndex(of: recipient) { - focusedField = .chip(hash, recipients[recipientIndex - 1]) - } + if recipient == recipients.last { + focusedField = type + } else if let recipientIndex = recipients.firstIndex(of: recipient) { + focusedField = .chip(hash, recipients[recipientIndex + 1]) } } } @@ -157,6 +148,6 @@ struct RecipientField_Previews: PreviewProvider { unknownRecipientAutocompletion: .constant(""), addRecipientHandler: .constant { _ in /* Preview */ }, focusedField: .init(), - type: .to) { _, _ in .to } + type: .to) } } diff --git a/Mail/Views/New Message/ComposeMessageView.swift b/Mail/Views/New Message/ComposeMessageView.swift index d15a8df1a..c6deb66c5 100644 --- a/Mail/Views/New Message/ComposeMessageView.swift +++ b/Mail/Views/New Message/ComposeMessageView.swift @@ -46,10 +46,6 @@ enum ComposeViewFieldType: Hashable { return "Recipient Chip" } } - - static let focusableFields: [Self] = [.to, .cc, .bcc, .subject] - static let minimizedFocusableFields: [Self] = [.to, .subject] - } class NewMessageAlert: SheetState { @@ -226,9 +222,9 @@ struct ComposeMessageView: View { } .customAlert(isPresented: $alert.isShowing) { switch alert.state { - case .link(let handler): + case let .link(handler): AddLinkView(actionHandler: handler) - case .emptySubject(let handler): + case let .emptySubject(handler): EmptySubjectView(actionHandler: handler) case .none: EmptyView() @@ -262,8 +258,7 @@ struct ComposeMessageView: View { unknownRecipientAutocompletion: $unknownRecipientAutocompletion, addRecipientHandler: $addRecipientHandler, focusedField: _focusedField, - type: type, - switchField: switchField) + type: type) } } } @@ -370,19 +365,6 @@ struct ComposeMessageView: View { } attachmentsManager.completeUploadedAttachments() } - - private func switchField(from field: ComposeViewFieldType, next: Bool) -> ComposeViewFieldType { - let fields = showCc ? ComposeViewFieldType.focusableFields : ComposeViewFieldType.minimizedFocusableFields - - let currentIndex = Int(fields.firstIndex(of: field) ?? 0) - let newIndex: Int - if next { - newIndex = (currentIndex + 1) % fields.count - } else { - newIndex = currentIndex > 0 ? currentIndex - 1 : fields.count - 1 - } - return fields[newIndex] - } } extension ComposeMessageView { diff --git a/Mail/Views/New Message/NewMessageCell.swift b/Mail/Views/New Message/NewMessageCell.swift index 7226ae3ca..fdd8f1520 100644 --- a/Mail/Views/New Message/NewMessageCell.swift +++ b/Mail/Views/New Message/NewMessageCell.swift @@ -87,7 +87,7 @@ struct NewMessageCell_Previews: PreviewProvider { unknownRecipientAutocompletion: .constant(""), addRecipientHandler: .constant { _ in /* Preview */ }, focusedField: .init(), - type: .to) { _, _ in return .to } + type: .to) } NewMessageCell(type: .subject) { TextField("", text: .constant("")) From eaf97f5465098677a54dfe60a06324c884996984 Mon Sep 17 00:00:00 2001 From: Valentin Perignon Date: Wed, 26 Apr 2023 10:45:51 +0200 Subject: [PATCH 15/21] fix: Check if textField is empty --- Mail/Components/RecipientField.swift | 4 +-- .../New Message/RecipientsTextField.swift | 6 ++-- Mail/Views/Thread List/ThreadListView.swift | 32 +++++-------------- 3 files changed, 13 insertions(+), 29 deletions(-) diff --git a/Mail/Components/RecipientField.swift b/Mail/Components/RecipientField.swift index 39ae76c40..581a6445a 100644 --- a/Mail/Components/RecipientField.swift +++ b/Mail/Components/RecipientField.swift @@ -83,8 +83,8 @@ struct RecipientField: View { matomo.track(eventWithCategory: .newMessage, action: .input, name: "addNewRecipient") } - private func handleBackspaceTextField() { - if let recipient = recipients.last, currentText.isEmpty { + private func handleBackspaceTextField(isTextEmpty: Bool) { + if let recipient = recipients.last, isTextEmpty { focusedField = .chip(type.hashValue, recipient) } } diff --git a/Mail/Views/New Message/RecipientsTextField.swift b/Mail/Views/New Message/RecipientsTextField.swift index 5f2f90ab8..b32f52f02 100644 --- a/Mail/Views/New Message/RecipientsTextField.swift +++ b/Mail/Views/New Message/RecipientsTextField.swift @@ -23,7 +23,7 @@ struct RecipientsTextFieldView: UIViewRepresentable { @Binding var text: String let onSubmit: () -> Void - let onBackspace: () -> Void + let onBackspace: (Bool) -> Void func makeUIView(context: Context) -> UITextField { let textField = RecipientsTextField() @@ -69,7 +69,7 @@ struct RecipientsTextFieldView: UIViewRepresentable { * We need to create our own UITextField to benefit from the `deleteBackward()` function */ class RecipientsTextField: UITextField { - var onBackspace: (() -> Void)? + var onBackspace: ((Bool) -> Void)? override init(frame: CGRect) { super.init(frame: frame) @@ -89,7 +89,7 @@ class RecipientsTextField: UITextField { } override func deleteBackward() { + onBackspace?(text?.isEmpty == true) super.deleteBackward() - onBackspace?() } } diff --git a/Mail/Views/Thread List/ThreadListView.swift b/Mail/Views/Thread List/ThreadListView.swift index 8c191b90c..aa6ba203d 100644 --- a/Mail/Views/Thread List/ThreadListView.swift +++ b/Mail/Views/Thread List/ThreadListView.swift @@ -201,10 +201,10 @@ struct ThreadListView: View { bottomSheet: bottomSheet, viewModel: viewModel, multipleSelectionViewModel: multipleSelectionViewModel) { - withAnimation(.default.speed(2)) { - multipleSelectionViewModel.selectAll(threads: viewModel.filteredThreads) - } - }) + withAnimation(.default.speed(2)) { + multipleSelectionViewModel.selectAll(threads: viewModel.filteredThreads) + } + }) .floatingActionButton(isEnabled: !multipleSelectionViewModel.isEnabled, icon: MailResourcesAsset.pencilPlain, title: MailResourcesStrings.Localizable.buttonNewMessage) { @@ -309,25 +309,8 @@ private struct ThreadListToolbar: ViewModifier { multipleSelectionViewModel.isEnabled = false } } - } - - ToolbarItem(placement: .principal) { - if !multipleSelectionViewModel.isEnabled { - Text(splitViewManager.selectedFolder?.localizedName ?? "") - .textStyle(.header1) - .frame(maxWidth: .infinity, alignment: .leading) - } - } - - ToolbarItemGroup(placement: .navigationBarTrailing) { - if multipleSelectionViewModel.isEnabled { - Button(multipleSelectionViewModel.selectedItems.count == viewModel.filteredThreads.count - ? MailResourcesStrings.Localizable.buttonUnselectAll - : MailResourcesStrings.Localizable.buttonSelectAll) { - selectAll() - } - .animation(nil, value: multipleSelectionViewModel.selectedItems) - } else { + } else { + if isCompact { Button { matomo.track(eventWithCategory: .menuDrawer, name: "openByButton") navigationDrawerState.open() @@ -383,7 +366,8 @@ private struct ThreadListToolbar: ViewModifier { ForEach(multipleSelectionViewModel.toolbarActions) { action in ToolbarButton( text: action.shortTitle ?? action.title, - icon: action.icon) { + icon: action.icon + ) { Task { await tryOrDisplayError { try await multipleSelectionViewModel.didTap( From 2e525db79a9dc8923a1b525b7e4737563dc59330 Mon Sep 17 00:00:00 2001 From: Philippe Weidmann Date: Wed, 19 Apr 2023 14:57:49 +0200 Subject: [PATCH 16/21] feat: iOS 16 navigation backport --- .package.resolved | 13 +++++++++-- Mail/Views/SplitView.swift | 21 +++++++++++++++-- Mail/Views/Thread List/ThreadListCell.swift | 25 +++++++-------------- Project.swift | 2 ++ 4 files changed, 40 insertions(+), 21 deletions(-) diff --git a/.package.resolved b/.package.resolved index 8f2d1d3c8..3a0482a0e 100644 --- a/.package.resolved +++ b/.package.resolved @@ -134,6 +134,15 @@ "version" : "7.5.2" } }, + { + "identity" : "navigationbackport", + "kind" : "remoteSourceControl", + "location" : "https://github.com/johnpatrickmorgan/NavigationBackport", + "state" : { + "revision" : "5096dda355148dd40162810e7f56292ce0a2b09b", + "version" : "0.7.5" + } + }, { "identity" : "nuke", "kind" : "remoteSourceControl", @@ -175,8 +184,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/getsentry/sentry-cocoa", "state" : { - "revision" : "ac224c437a3070ffe34460137ac8761eddaf2852", - "version" : "8.3.3" + "revision" : "92a6472efc750a4e18bdee21c204942ab0bc4dcd", + "version" : "8.4.0" } }, { diff --git a/Mail/Views/SplitView.swift b/Mail/Views/SplitView.swift index 70fa00756..055cf351b 100644 --- a/Mail/Views/SplitView.swift +++ b/Mail/Views/SplitView.swift @@ -21,9 +21,21 @@ import InfomaniakCore import Introspect import MailCore import MailResources +import NavigationBackport import RealmSwift import SwiftUI +struct MailNavigationPathKey: EnvironmentKey { + static var defaultValue: Binding? +} + +extension EnvironmentValues { + var mailNavigationPath: Binding? { + get { self[MailNavigationPathKey.self] } + set { self[MailNavigationPathKey.self] = newValue } + } +} + class GlobalBottomSheet: DisplayedFloatingPanelState { enum State { case getMoreStorage @@ -62,6 +74,7 @@ struct SplitView: View { @StateObject private var alert = GlobalAlert() @StateObject private var splitViewManager: SplitViewManager + @State var path = NBNavigationPath() var isCompact: Bool { sizeClass == .compact || verticalSizeClass == .compact @@ -77,9 +90,13 @@ struct SplitView: View { Group { if isCompact { ZStack { - NavigationView { + NBNavigationStack(path: $path) { ThreadListManagerView(isCompact: isCompact) - .accessibilityHidden(navigationDrawerController.isOpen) + .accessibilityHidden(navigationDrawerController.isOpen) + .nbNavigationDestination(for: Thread.self) { thread in + ThreadView(thread: thread) + } + .environment(\.mailNavigationPath, $path) } .navigationViewStyle(.stack) diff --git a/Mail/Views/Thread List/ThreadListCell.swift b/Mail/Views/Thread List/ThreadListCell.swift index a95996504..59e2165f8 100644 --- a/Mail/Views/Thread List/ThreadListCell.swift +++ b/Mail/Views/Thread List/ThreadListCell.swift @@ -25,6 +25,7 @@ import SwiftUI struct ThreadListCell: View { @EnvironmentObject var splitViewManager: SplitViewManager + @Environment(\.mailNavigationPath) var path let thread: Thread @@ -53,22 +54,12 @@ struct ThreadListCell: View { } var body: some View { - ZStack { - if !thread.shouldPresentAsDraft { - NavigationLink(destination: ThreadView(thread: thread, - onDismiss: { viewModel.selectedThread = nil }), - isActive: $shouldNavigateToThreadList) { EmptyView() } - .opacity(0) - .disabled(multipleSelectionViewModel.isEnabled) - } - - ThreadCell( - thread: thread, - density: threadDensity, - isMultipleSelectionEnabled: multipleSelectionViewModel.isEnabled, - isSelected: isSelected - ) - } + ThreadCell( + thread: thread, + density: threadDensity, + isMultipleSelectionEnabled: multipleSelectionViewModel.isEnabled, + isSelected: isSelected + ) .background(SelectionBackground(selectionType: selectionType, paddingLeading: 4)) .onTapGesture { didTapCell() } .onLongPressGesture(minimumDuration: 0.3) { didLongPressCell() } @@ -101,7 +92,7 @@ struct ThreadListCell: View { splitViewManager.splitViewController?.hide(.supplementary) } viewModel.selectedThread = thread - shouldNavigateToThreadList = true + path?.wrappedValue.append(thread) } } } diff --git a/Project.swift b/Project.swift index ddb50d720..43ec46ebf 100644 --- a/Project.swift +++ b/Project.swift @@ -48,6 +48,7 @@ let project = Project(name: "Mail", .package(url: "https://github.com/kean/Nuke", .upToNextMajor(from: "12.0.0")), .package(url: "https://github.com/airbnb/lottie-ios", .exact("3.5.0")), .package(url: "https://github.com/scinfu/SwiftSoup", .upToNextMajor(from: "2.5.3")), + .package(url: "https://github.com/johnpatrickmorgan/NavigationBackport", .upToNextMajor(from: "0.7.2")), .package(url: "https://github.com/aheze/Popovers", .upToNextMajor(from: "1.3.2")) ], targets: [ @@ -79,6 +80,7 @@ let project = Project(name: "Mail", .package(product: "WrappingHStack"), .package(product: "FloatingPanel"), .package(product: "Lottie"), + .package(product: "NavigationBackport"), .package(product: "Popovers") ], settings: .settings(base: baseSettings), From 576dbc3e6a692671687230ef1eaf56958f4a79f5 Mon Sep 17 00:00:00 2001 From: Ambroise Decouttere Date: Tue, 25 Apr 2023 10:12:31 +0200 Subject: [PATCH 17/21] fix(Navigation): iPad navigation done --- Mail/Views/SplitView.swift | 17 ++++++++---- Mail/Views/Thread List/ThreadListCell.swift | 22 +++++----------- Mail/Views/Thread List/ThreadListView.swift | 11 +++++++- .../Thread List/ThreadListViewModel.swift | 17 +++++++----- Mail/Views/Thread/ThreadView.swift | 26 +++++++++---------- 5 files changed, 51 insertions(+), 42 deletions(-) diff --git a/Mail/Views/SplitView.swift b/Mail/Views/SplitView.swift index 055cf351b..0b8335594 100644 --- a/Mail/Views/SplitView.swift +++ b/Mail/Views/SplitView.swift @@ -26,11 +26,11 @@ import RealmSwift import SwiftUI struct MailNavigationPathKey: EnvironmentKey { - static var defaultValue: Binding? + static var defaultValue: Binding<[Thread]>? } extension EnvironmentValues { - var mailNavigationPath: Binding? { + var mailNavigationPath: Binding<[Thread]>? { get { self[MailNavigationPathKey.self] } set { self[MailNavigationPathKey.self] = newValue } } @@ -74,7 +74,7 @@ struct SplitView: View { @StateObject private var alert = GlobalAlert() @StateObject private var splitViewManager: SplitViewManager - @State var path = NBNavigationPath() + @State var path = [Thread]() var isCompact: Bool { sizeClass == .compact || verticalSizeClass == .compact @@ -96,7 +96,6 @@ struct SplitView: View { .nbNavigationDestination(for: Thread.self) { thread in ThreadView(thread: thread) } - .environment(\.mailNavigationPath, $path) } .navigationViewStyle(.stack) @@ -112,7 +111,11 @@ struct SplitView: View { ThreadListManagerView(isCompact: isCompact) - EmptyStateView.emptyThread(from: splitViewManager.selectedFolder) + if let thread = path.last { + ThreadView(mailboxManager: mailboxManager, thread: thread) + } else { + EmptyStateView.emptyThread(from: splitViewManager.selectedFolder) + } } } } @@ -121,6 +124,10 @@ struct SplitView: View { try await mailboxManager.folders() } } + .environment(\.mailNavigationPath, $path) + .environmentObject(splitViewManager) + .environmentObject(navigationDrawerController) + .defaultAppStorage(.shared) .onAppear { AppDelegate.orientationLock = .all } diff --git a/Mail/Views/Thread List/ThreadListCell.swift b/Mail/Views/Thread List/ThreadListCell.swift index 59e2165f8..bed120f4d 100644 --- a/Mail/Views/Thread List/ThreadListCell.swift +++ b/Mail/Views/Thread List/ThreadListCell.swift @@ -35,22 +35,17 @@ struct ThreadListCell: View { let threadDensity: ThreadDensity let isSelected: Bool + let isMultiSelected: Bool @Binding var editedMessageDraft: Draft? @State private var shouldNavigateToThreadList = false private var selectionType: SelectionBackgroundKind { - if isSelected { - return .multiple - } else if !multipleSelectionViewModel.isEnabled && viewModel.selectedThread?.uid == thread.uid { - return .single + if multipleSelectionViewModel.isEnabled { + return isMultiSelected ? .multiple : .none } - return .none - } - - private var selectedThreadBackground: Bool { - return !multipleSelectionViewModel.isEnabled && (viewModel.selectedThread?.uid == thread.uid) + return isSelected ? .single : .none } var body: some View { @@ -58,7 +53,7 @@ struct ThreadListCell: View { thread: thread, density: threadDensity, isMultipleSelectionEnabled: multipleSelectionViewModel.isEnabled, - isSelected: isSelected + isSelected: isMultiSelected ) .background(SelectionBackground(selectionType: selectionType, paddingLeading: 4)) .onTapGesture { didTapCell() } @@ -67,11 +62,6 @@ struct ThreadListCell: View { .listRowInsets(.init(top: 0, leading: 0, bottom: 0, trailing: 0)) .listRowSeparator(.hidden) .listRowBackground(MailResourcesAsset.backgroundColor.swiftUIColor) - .onChange(of: viewModel.selectedThread) { newThread in - if newThread?.uid == thread.uid { - shouldNavigateToThreadList = true - } - } } private func didTapCell() { @@ -92,7 +82,6 @@ struct ThreadListCell: View { splitViewManager.splitViewController?.hide(.supplementary) } viewModel.selectedThread = thread - path?.wrappedValue.append(thread) } } } @@ -123,6 +112,7 @@ struct ThreadListCell_Previews: PreviewProvider { multipleSelectionViewModel: ThreadListMultipleSelectionViewModel(mailboxManager: PreviewHelper.sampleMailboxManager), threadDensity: .large, isSelected: false, + isMultiSelected: false, editedMessageDraft: .constant(nil) ) } diff --git a/Mail/Views/Thread List/ThreadListView.swift b/Mail/Views/Thread List/ThreadListView.swift index aa6ba203d..de57d2edb 100644 --- a/Mail/Views/Thread List/ThreadListView.swift +++ b/Mail/Views/Thread List/ThreadListView.swift @@ -53,6 +53,7 @@ struct ThreadListView: View { @EnvironmentObject var splitViewManager: SplitViewManager @EnvironmentObject var globalBottomSheet: GlobalBottomSheet + @Environment(\.mailNavigationPath) var path @AppStorage(UserDefaults.shared.key(.threadDensity)) private var threadDensity = DefaultPreferences.threadDensity @AppStorage(UserDefaults.shared.key(.accentColor)) private var accentColor = DefaultPreferences.accentColor @@ -142,7 +143,8 @@ struct ThreadListView: View { viewModel: viewModel, multipleSelectionViewModel: multipleSelectionViewModel, threadDensity: threadDensity, - isSelected: multipleSelectionViewModel.selectedItems + isSelected: viewModel.selectedThread?.uid == thread.uid, + isMultiSelected: multipleSelectionViewModel.selectedItems .contains { $0.id == thread.id }, editedMessageDraft: $editedMessageDraft) .id(thread.id) @@ -232,6 +234,13 @@ struct ThreadListView: View { .onChange(of: splitViewManager.selectedFolder) { newFolder in changeFolder(newFolder: newFolder) } + .onChange(of: viewModel.selectedThread) { newThread in + if let newThread = newThread { + path?.wrappedValue = [newThread] + } else { + path?.wrappedValue = [] + } + } .onReceive(NotificationCenter.default.publisher(for: UIApplication.willEnterForegroundNotification)) { _ in updateFetchingTask() } diff --git a/Mail/Views/Thread List/ThreadListViewModel.swift b/Mail/Views/Thread List/ThreadListViewModel.swift index 54961c349..2911b3d3c 100644 --- a/Mail/Views/Thread List/ThreadListViewModel.swift +++ b/Mail/Views/Thread List/ThreadListViewModel.swift @@ -93,13 +93,7 @@ class DateSection: Identifiable { @Published var sections = [DateSection]() @Published var selectedThread: Thread? { didSet { - guard !isCompact, !filteredThreads.isEmpty else { return } - if selectedThread == nil, let index = selectedThreadIndex { - let realIndex = min(index, filteredThreads.count - 1) - selectedThread = filteredThreads[realIndex] - } else if let thread = selectedThread, let index = filteredThreads.firstIndex(where: { $0.uid == thread.uid }) { - selectedThreadIndex = index - } + selectedThreadIndex = filteredThreads.firstIndex { $0.uid == selectedThread?.uid } } } @@ -235,6 +229,7 @@ class DateSection: Identifiable { } case .update(let results, _, _, _): let filteredThreads = Array(results.freezeIfNeeded()) + self?.nextThreadIfNeeded(from: filteredThreads) guard let newSections = self?.sortThreadsIntoSections(threads: filteredThreads) else { return } DispatchQueue.main.sync { @@ -263,6 +258,14 @@ class DateSection: Identifiable { } } + func nextThreadIfNeeded(from threads: [Thread]) { + guard !isCompact, + !threads.contains(where: { $0.uid == selectedThread?.uid }), + let lastIndex = selectedThreadIndex else { return } + let validIndex = min(lastIndex, threads.count - 1) + selectedThread = threads[validIndex] + } + private func sortThreadsIntoSections(threads: [Thread]) -> [DateSection]? { var newSections = [DateSection]() diff --git a/Mail/Views/Thread/ThreadView.swift b/Mail/Views/Thread/ThreadView.swift index d8427c718..13caa523a 100644 --- a/Mail/Views/Thread/ThreadView.swift +++ b/Mail/Views/Thread/ThreadView.swift @@ -41,10 +41,10 @@ class MessageBottomSheet: DisplayedFloatingPanelState struct ThreadView: View { @EnvironmentObject private var splitViewManager: SplitViewManager + @Environment(\.mailNavigationPath) var path @EnvironmentObject private var mailboxManager: MailboxManager @ObservedRealmObject var thread: Thread - var onDismiss: (() -> Void)? @State private var headerHeight: CGFloat = 0 @State private var displayNavigationTitle = false @@ -58,9 +58,14 @@ struct ThreadView: View { @EnvironmentObject var globalBottomSheet: GlobalBottomSheet @EnvironmentObject var globalAlert: GlobalAlert - @Environment(\.verticalSizeClass) var sizeClass + @Environment(\.horizontalSizeClass) var sizeClass + @Environment(\.verticalSizeClass) var verticalSizeClass @Environment(\.dismiss) var dismiss + var isCompact: Bool { + sizeClass == .compact || verticalSizeClass == .compact + } + @LazyInjectService private var matomo: MatomoUtils private let toolbarActions: [Action] = [.reply, .forward, .archive, .delete] @@ -176,19 +181,14 @@ struct ThreadView: View { } } .onChange(of: thread.messages) { newMessagesList in - if newMessagesList.isEmpty { - showEmptyView = true - onDismiss?() - dismiss() - } - if thread.messageInFolderCount == 0 { - onDismiss?() - dismiss() + if newMessagesList.isEmpty || thread.messageInFolderCount == 0 { + if isCompact { + dismiss() // For iPhone + } else { + path?.wrappedValue = [] // For iPad + } } } - .emptyState(isEmpty: showEmptyView) { - EmptyStateView.emptyThread(from: splitViewManager.selectedFolder) - } .matomoView(view: [MatomoUtils.View.threadView.displayName, "Main"]) } From f67b81bdc790cf862cdbba9411045f81b43b2cb4 Mon Sep 17 00:00:00 2001 From: Ambroise Decouttere Date: Tue, 25 Apr 2023 12:08:54 +0200 Subject: [PATCH 18/21] fix: CI --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index c3372a42f..e77d09edf 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -7,7 +7,7 @@ on: jobs: build: name: Build and Test project - runs-on: self-hosted + runs-on: [ self-hosted, iOS ] steps: - name: Cancel Previous Runs From 4d7b432f3b430f74d6c183e2471d798224016a1e Mon Sep 17 00:00:00 2001 From: Ambroise Decouttere Date: Wed, 26 Apr 2023 10:26:07 +0200 Subject: [PATCH 19/21] fix: Rebase master --- Mail/Views/SplitView.swift | 15 ++++++--------- Mail/Views/Thread List/ThreadListViewModel.swift | 2 +- 2 files changed, 7 insertions(+), 10 deletions(-) diff --git a/Mail/Views/SplitView.swift b/Mail/Views/SplitView.swift index 0b8335594..32000da53 100644 --- a/Mail/Views/SplitView.swift +++ b/Mail/Views/SplitView.swift @@ -92,10 +92,10 @@ struct SplitView: View { ZStack { NBNavigationStack(path: $path) { ThreadListManagerView(isCompact: isCompact) - .accessibilityHidden(navigationDrawerController.isOpen) - .nbNavigationDestination(for: Thread.self) { thread in - ThreadView(thread: thread) - } + .accessibilityHidden(navigationDrawerController.isOpen) + .nbNavigationDestination(for: Thread.self) { thread in + ThreadView(thread: thread) + } } .navigationViewStyle(.stack) @@ -112,7 +112,7 @@ struct SplitView: View { ThreadListManagerView(isCompact: isCompact) if let thread = path.last { - ThreadView(mailboxManager: mailboxManager, thread: thread) + ThreadView(thread: thread) } else { EmptyStateView.emptyThread(from: splitViewManager.selectedFolder) } @@ -124,10 +124,6 @@ struct SplitView: View { try await mailboxManager.folders() } } - .environment(\.mailNavigationPath, $path) - .environmentObject(splitViewManager) - .environmentObject(navigationDrawerController) - .defaultAppStorage(.shared) .onAppear { AppDelegate.orientationLock = .all } @@ -181,6 +177,7 @@ struct SplitView: View { EmptyView() } } + .environment(\.mailNavigationPath, $path) .environment(\.realmConfiguration, mailboxManager.realmConfiguration) .environmentObject(mailboxManager) .environmentObject(splitViewManager) diff --git a/Mail/Views/Thread List/ThreadListViewModel.swift b/Mail/Views/Thread List/ThreadListViewModel.swift index 2911b3d3c..5f3fe1bf7 100644 --- a/Mail/Views/Thread List/ThreadListViewModel.swift +++ b/Mail/Views/Thread List/ThreadListViewModel.swift @@ -229,10 +229,10 @@ class DateSection: Identifiable { } case .update(let results, _, _, _): let filteredThreads = Array(results.freezeIfNeeded()) - self?.nextThreadIfNeeded(from: filteredThreads) guard let newSections = self?.sortThreadsIntoSections(threads: filteredThreads) else { return } DispatchQueue.main.sync { + self?.nextThreadIfNeeded(from: filteredThreads) self?.filteredThreads = filteredThreads if self?.filter != .all && filteredThreads.count == 1 && self?.filter.accepts(thread: filteredThreads[0]) != true { From 7e72aba15fc479f3109bef458a474df02de8bc89 Mon Sep 17 00:00:00 2001 From: Ambroise Decouttere Date: Wed, 26 Apr 2023 13:17:09 +0200 Subject: [PATCH 20/21] fix: PR feedback --- Mail/Views/SplitView.swift | 2 +- Mail/Views/Thread List/ThreadListCell.swift | 2 +- Mail/Views/Thread List/ThreadListView.swift | 2 +- Mail/Views/Thread/ThreadView.swift | 4 ++-- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/Mail/Views/SplitView.swift b/Mail/Views/SplitView.swift index 32000da53..5a1b92fce 100644 --- a/Mail/Views/SplitView.swift +++ b/Mail/Views/SplitView.swift @@ -74,7 +74,7 @@ struct SplitView: View { @StateObject private var alert = GlobalAlert() @StateObject private var splitViewManager: SplitViewManager - @State var path = [Thread]() + @State private var path = [Thread]() var isCompact: Bool { sizeClass == .compact || verticalSizeClass == .compact diff --git a/Mail/Views/Thread List/ThreadListCell.swift b/Mail/Views/Thread List/ThreadListCell.swift index bed120f4d..e791ba606 100644 --- a/Mail/Views/Thread List/ThreadListCell.swift +++ b/Mail/Views/Thread List/ThreadListCell.swift @@ -25,7 +25,7 @@ import SwiftUI struct ThreadListCell: View { @EnvironmentObject var splitViewManager: SplitViewManager - @Environment(\.mailNavigationPath) var path + @Environment(\.mailNavigationPath) private var path let thread: Thread diff --git a/Mail/Views/Thread List/ThreadListView.swift b/Mail/Views/Thread List/ThreadListView.swift index de57d2edb..bb4519af6 100644 --- a/Mail/Views/Thread List/ThreadListView.swift +++ b/Mail/Views/Thread List/ThreadListView.swift @@ -235,7 +235,7 @@ struct ThreadListView: View { changeFolder(newFolder: newFolder) } .onChange(of: viewModel.selectedThread) { newThread in - if let newThread = newThread { + if let newThread { path?.wrappedValue = [newThread] } else { path?.wrappedValue = [] diff --git a/Mail/Views/Thread/ThreadView.swift b/Mail/Views/Thread/ThreadView.swift index 13caa523a..79368d474 100644 --- a/Mail/Views/Thread/ThreadView.swift +++ b/Mail/Views/Thread/ThreadView.swift @@ -58,8 +58,8 @@ struct ThreadView: View { @EnvironmentObject var globalBottomSheet: GlobalBottomSheet @EnvironmentObject var globalAlert: GlobalAlert - @Environment(\.horizontalSizeClass) var sizeClass - @Environment(\.verticalSizeClass) var verticalSizeClass + @Environment(\.horizontalSizeClass) private var sizeClass + @Environment(\.verticalSizeClass) private var verticalSizeClass @Environment(\.dismiss) var dismiss var isCompact: Bool { From 7a78ec55f424a948135f420135158db51f60d3ff Mon Sep 17 00:00:00 2001 From: Ambroise Decouttere Date: Wed, 26 Apr 2023 16:48:01 +0200 Subject: [PATCH 21/21] fix: PR feedback --- Mail/Views/Thread List/ThreadListView.swift | 2 +- Mail/Views/Thread/ThreadView.swift | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Mail/Views/Thread List/ThreadListView.swift b/Mail/Views/Thread List/ThreadListView.swift index bb4519af6..f91aaf5a8 100644 --- a/Mail/Views/Thread List/ThreadListView.swift +++ b/Mail/Views/Thread List/ThreadListView.swift @@ -53,7 +53,7 @@ struct ThreadListView: View { @EnvironmentObject var splitViewManager: SplitViewManager @EnvironmentObject var globalBottomSheet: GlobalBottomSheet - @Environment(\.mailNavigationPath) var path + @Environment(\.mailNavigationPath) private var path @AppStorage(UserDefaults.shared.key(.threadDensity)) private var threadDensity = DefaultPreferences.threadDensity @AppStorage(UserDefaults.shared.key(.accentColor)) private var accentColor = DefaultPreferences.accentColor diff --git a/Mail/Views/Thread/ThreadView.swift b/Mail/Views/Thread/ThreadView.swift index 79368d474..479462b98 100644 --- a/Mail/Views/Thread/ThreadView.swift +++ b/Mail/Views/Thread/ThreadView.swift @@ -41,7 +41,7 @@ class MessageBottomSheet: DisplayedFloatingPanelState struct ThreadView: View { @EnvironmentObject private var splitViewManager: SplitViewManager - @Environment(\.mailNavigationPath) var path + @Environment(\.mailNavigationPath) private var path @EnvironmentObject private var mailboxManager: MailboxManager @ObservedRealmObject var thread: Thread