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
diff --git a/.package.resolved b/.package.resolved
index aff26dd01..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",
@@ -143,6 +152,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",
@@ -166,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/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/Components/RecipientChip.swift b/Mail/Components/RecipientChip.swift
new file mode 100644
index 000000000..3789554a0
--- /dev/null
+++ b/Mail/Components/RecipientChip.swift
@@ -0,0 +1,77 @@
+/*
+ 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 InfomaniakCoreUI
+import MailCore
+import MailResources
+import Popovers
+import SwiftUI
+
+struct RecipientChip: View {
+ @Environment(\.window) private var window
+ @AppStorage(UserDefaults.shared.key(.accentColor)) private var accentColor = DefaultPreferences.accentColor
+
+ let recipient: Recipient
+ let fieldType: ComposeViewFieldType
+ @FocusState var focusedField: ComposeViewFieldType?
+ let removeHandler: () -> 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)
+ .padding(.horizontal, 16)
+ .frame(maxWidth: min(304, 0.8 * (window?.screen.bounds.width ?? 304)))
+
+ 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: { isSelected in
+ RecipientChipLabelView(recipient: recipient, removeHandler: removeAndFocus, switchFocusHandler: switchFocusHandler)
+ .fixedSize()
+ .opacity(isSelected ? 0.8 : 1)
+ }
+ }
+
+ private func removeAndFocus() {
+ focusedField = fieldType
+ removeHandler()
+ }
+}
+
+struct RecipientChip_Previews: PreviewProvider {
+ static var previews: some View {
+ RecipientChip(recipient: PreviewHelper.sampleRecipient1, fieldType: .to) {
+ /* Preview */
+ } switchFocusHandler: {
+ /* Preview */
+ }
+ }
+}
diff --git a/Mail/Components/RecipientChipLabel.swift b/Mail/Components/RecipientChipLabel.swift
new file mode 100644
index 000000000..aae845c04
--- /dev/null
+++ b/Mail/Components/RecipientChipLabel.swift
@@ -0,0 +1,103 @@
+/*
+ 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
+ let switchFocusHandler: () -> Void
+
+ func makeUIView(context: Context) -> RecipientChipLabel {
+ let label = RecipientChipLabel(recipient: recipient)
+ label.removeHandler = removeHandler
+ label.switchFocusHandler = switchFocusHandler
+ return label
+ }
+
+ func updateUIView(_ uiLabel: RecipientChipLabel, context: Context) {
+ uiLabel.text = recipient.name.isEmpty ? recipient.email : recipient.name
+ }
+}
+
+class RecipientChipLabel: UILabel, UIKeyInput {
+ var removeHandler: (() -> Void)?
+ var switchFocusHandler: (() -> 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) {
+ if text == "\t" {
+ switchFocusHandler?()
+ }
+ }
+
+ 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 1815874d8..581a6445a 100644
--- a/Mail/Components/RecipientField.swift
+++ b/Mail/Components/RecipientField.swift
@@ -25,31 +25,14 @@ 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]
@Binding var unknownRecipientAutocompletion: String
@Binding var addRecipientHandler: ((Recipient) -> Void)?
+
@FocusState var focusedField: ComposeViewFieldType?
+
let type: ComposeViewFieldType
@State private var currentText = ""
@@ -59,26 +42,18 @@ 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) {
remove(recipientAt: i)
+ } switchFocusHandler: {
+ switchFocus()
}
+ .focused($focusedField, equals: .chip(type.hashValue, recipients[i]))
}
.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()
@@ -95,12 +70,31 @@ 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(isTextEmpty: Bool) {
+ if let recipient = recipients.last, isTextEmpty {
+ focusedField = .chip(type.hashValue, recipient)
+ }
+ }
+
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) }
@@ -133,6 +127,16 @@ struct RecipientField: View {
$recipients.remove(at: recipientAt)
}
}
+
+ private func switchFocus() {
+ guard case let .chip(hash, recipient) = focusedField else { return }
+
+ if recipient == recipients.last {
+ focusedField = type
+ } else if let recipientIndex = recipients.firstIndex(of: recipient) {
+ focusedField = .chip(hash, recipients[recipientIndex + 1])
+ }
+ }
}
struct RecipientField_Previews: PreviewProvider {
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/ComposeMessageView.swift b/Mail/Views/New Message/ComposeMessageView.swift
index e5d613ca5..c6deb66c5 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"
}
}
}
@@ -219,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()
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(""))
+ }
}
}
}
diff --git a/Mail/Views/New Message/RecipientsTextField.swift b/Mail/Views/New Message/RecipientsTextField.swift
new file mode 100644
index 000000000..b32f52f02
--- /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: (Bool) -> 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: ((Bool) -> 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() {
+ onBackspace?(text?.isEmpty == true)
+ super.deleteBackward()
+ }
+}
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/Mail/Views/SplitView.swift b/Mail/Views/SplitView.swift
index f2f7277cf..72ed51fd2 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<[Thread]>?
+}
+
+extension EnvironmentValues {
+ var mailNavigationPath: Binding<[Thread]>? {
+ 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 private var path = [Thread]()
var isCompact: Bool {
UIConstants.isCompact(horizontalSizeClass: horizontalSizeClass, verticalSizeClass: verticalSizeClass)
@@ -77,9 +90,12 @@ struct SplitView: View {
Group {
if isCompact {
ZStack {
- NavigationView {
+ NBNavigationStack(path: $path) {
ThreadListManagerView(isCompact: isCompact)
.accessibilityHidden(navigationDrawerController.isOpen)
+ .nbNavigationDestination(for: Thread.self) { thread in
+ ThreadView(thread: thread)
+ }
}
.navigationViewStyle(.stack)
@@ -95,7 +111,11 @@ struct SplitView: View {
ThreadListManagerView(isCompact: isCompact)
- EmptyStateView.emptyThread(from: splitViewManager.selectedFolder)
+ if let thread = path.last {
+ ThreadView(thread: thread)
+ } else {
+ EmptyStateView.emptyThread(from: splitViewManager.selectedFolder)
+ }
}
}
}
@@ -140,6 +160,7 @@ struct SplitView: View {
EmptyView()
}
}
+ .environment(\.mailNavigationPath, $path)
.environment(\.realmConfiguration, mailboxManager.realmConfiguration)
.environmentObject(mailboxManager)
.environmentObject(splitViewManager)
diff --git a/Mail/Views/Thread List/ThreadListCell.swift b/Mail/Views/Thread List/ThreadListCell.swift
index a95996504..e791ba606 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) private var path
let thread: Thread
@@ -34,41 +35,26 @@ 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 {
- 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: isMultiSelected
+ )
.background(SelectionBackground(selectionType: selectionType, paddingLeading: 4))
.onTapGesture { didTapCell() }
.onLongPressGesture(minimumDuration: 0.3) { didLongPressCell() }
@@ -76,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() {
@@ -101,7 +82,6 @@ struct ThreadListCell: View {
splitViewManager.splitViewController?.hide(.supplementary)
}
viewModel.selectedThread = thread
- shouldNavigateToThreadList = true
}
}
}
@@ -132,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 f123a0940..afba54167 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) private 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)
@@ -200,12 +202,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) {
@@ -230,6 +231,13 @@ struct ThreadListView: View {
.onChange(of: splitViewManager.selectedFolder) { newFolder in
changeFolder(newFolder: newFolder)
}
+ .onChange(of: viewModel.selectedThread) { newThread in
+ if let newThread {
+ path?.wrappedValue = [newThread]
+ } else {
+ path?.wrappedValue = []
+ }
+ }
.onReceive(NotificationCenter.default.publisher(for: UIApplication.willEnterForegroundNotification)) { _ in
updateFetchingTask()
}
@@ -243,7 +251,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)
}
}
diff --git a/Mail/Views/Thread List/ThreadListViewModel.swift b/Mail/Views/Thread List/ThreadListViewModel.swift
index aeeed7908..6a5314b10 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 }
}
}
@@ -240,6 +234,7 @@ class DateSection: Identifiable {
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 {
@@ -265,6 +260,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 c271c7e72..4c35bdadf 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) private 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
@@ -56,9 +56,14 @@ struct ThreadView: View {
@State private var showEmptyView = false
@EnvironmentObject var globalAlert: GlobalAlert
- @Environment(\.verticalSizeClass) var sizeClass
+ @Environment(\.horizontalSizeClass) private var sizeClass
+ @Environment(\.verticalSizeClass) private 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]
@@ -179,19 +184,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"])
}
diff --git a/MailCore/UI/UIConstants.swift b/MailCore/UI/UIConstants.swift
index 2a4a9ae5f..038036d45 100644
--- a/MailCore/UI/UIConstants.swift
+++ b/MailCore/UI/UIConstants.swift
@@ -98,6 +98,8 @@ public enum UIConstants {
public static let componentsMaxWidth: CGFloat = 496
+ public static let chipInsets = UIEdgeInsets(top: 4, left: 8, bottom: 4, right: 8)
+
public static func isCompact(horizontalSizeClass: UserInterfaceSizeClass?, verticalSizeClass: UserInterfaceSizeClass?) -> Bool {
return horizontalSizeClass == .compact || verticalSizeClass == .compact
}
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
diff --git a/Project.swift b/Project.swift
index 7bdabeadc..bd2b85d0e 100644
--- a/Project.swift
+++ b/Project.swift
@@ -48,6 +48,8 @@ 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")),
.package(url: "https://github.com/shaps80/SwiftUIBackports", .upToNextMajor(from: "1.15.1"))
],
targets: [
@@ -79,6 +81,8 @@ let project = Project(name: "Mail",
.package(product: "WrappingHStack"),
.package(product: "FloatingPanel"),
.package(product: "Lottie"),
+ .package(product: "NavigationBackport"),
+ .package(product: "Popovers"),
.package(product: "SwiftUIBackports")
],
settings: .settings(base: baseSettings),