diff --git a/Mail/Components/BottomBarView.swift b/Mail/Components/BottomBarView.swift new file mode 100644 index 000000000..cedfe3958 --- /dev/null +++ b/Mail/Components/BottomBarView.swift @@ -0,0 +1,84 @@ +/* + 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 BottomBar: ViewModifier { + let isVisible: Bool + @ViewBuilder var items: () -> Items + + func body(content: Content) -> some View { + content + .safeAreaInset(edge: .bottom) { + if isVisible { + BottomBarView(items: items) + .transition(.opacity) + } + } + } +} + +extension View { + func bottomBar(isVisible: Bool = true, @ViewBuilder items: @escaping () -> Items) -> some View { + modifier(BottomBar(isVisible: isVisible, items: items)) + } +} + +struct BottomBarView: View { + @State private var hasBottomSafeArea = true + + @ViewBuilder var items: () -> Items + + var body: some View { + HStack { + Spacer(minLength: UIConstants.bottomBarHorizontalMinimumSpace) + items() + Spacer(minLength: UIConstants.bottomBarHorizontalMinimumSpace) + } + .padding(.top, UIConstants.bottomBarVerticalPadding) + .padding(.bottom, hasBottomSafeArea ? UIConstants.bottomBarSmallVerticalPadding : UIConstants.bottomBarVerticalPadding) + .background(MailResourcesAsset.backgroundTabBarColor.swiftUIColor) + .overlay(alignment: .top) { + Divider() + .frame(height: 1) + .overlay(Color(uiColor: .systemGray3)) + } + .overlay { + ViewGeometry(key: BottomSafeAreaKey.self, property: \.safeAreaInsets.bottom) + } + .onPreferenceChange(BottomSafeAreaKey.self) { value in + hasBottomSafeArea = value > 0 + } + } +} + +struct BottomBarView_Previews: PreviewProvider { + static var previews: some View { + NavigationView { + List { + Text("View #1") + } + .navigationTitle("Title") + .bottomBar { + Text("Coucou") + } + } + } +} diff --git a/Mail/Components/MailButton.swift b/Mail/Components/MailButton.swift index 31d9ed3ec..96db5370b 100644 --- a/Mail/Components/MailButton.swift +++ b/Mail/Components/MailButton.swift @@ -20,7 +20,7 @@ import MailCore import MailResources import SwiftUI -// MARK: - Modifiers +// MARK: - Environment struct MailButtonStyleKey: EnvironmentKey { static var defaultValue = MailButton.Style.large diff --git a/Mail/Components/ToolbarButton.swift b/Mail/Components/ToolbarButton.swift index ac4810be2..dd8ecfba9 100644 --- a/Mail/Components/ToolbarButton.swift +++ b/Mail/Components/ToolbarButton.swift @@ -30,8 +30,12 @@ struct ToolbarButtonLabel: View { Label { Text(text) .textStyle(MailTextStyle.labelMediumAccent) + .minimumScaleFactor(0.8) + .lineLimit(1) } icon: { icon + .resizable() + .frame(width: 24, height: 24) } .dynamicLabelStyle(sizeClass: sizeClass ?? .regular) } diff --git a/Mail/Utils/GeometryReaderHelpers.swift b/Mail/Utils/GeometryReaderHelpers.swift new file mode 100644 index 000000000..c8f677d3f --- /dev/null +++ b/Mail/Utils/GeometryReaderHelpers.swift @@ -0,0 +1,47 @@ +/* + 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 + +struct ViewWidthKey: PreferenceKey { + static var defaultValue: CGFloat = .zero + + static func reduce(value: inout CGFloat, nextValue: () -> CGFloat) { + value = max(value, nextValue()) + } +} + +struct BottomSafeAreaKey: PreferenceKey { + static var defaultValue: CGFloat = .zero + + static func reduce(value: inout CGFloat, nextValue: () -> CGFloat) { + value = nextValue() + } +} + +struct ViewGeometry: View where K: PreferenceKey { + let key: K.Type + let property: KeyPath + + var body: some View { + GeometryReader { proxy in + Color.clear + .preference(key: key, value: proxy[keyPath: property]) + } + } +} diff --git a/Mail/Views/Thread List/ThreadListModifiers.swift b/Mail/Views/Thread List/ThreadListModifiers.swift index 6445d3b5a..d5463219e 100644 --- a/Mail/Views/Thread List/ThreadListModifiers.swift +++ b/Mail/Views/Thread List/ThreadListModifiers.swift @@ -130,37 +130,34 @@ struct ThreadListToolbar: ViewModifier { .accessibilityLabel(MailResourcesStrings.Localizable.contentDescriptionUserAvatar) } } - - ToolbarItemGroup(placement: .bottomBar) { - if multipleSelectionViewModel.isEnabled { - HStack(spacing: 0) { - ForEach(multipleSelectionViewModel.toolbarActions) { action in - ToolbarButton( - text: action.shortTitle ?? action.title, - icon: action.icon - ) { - Task { - await tryOrDisplayError { - try await multipleSelectionViewModel.didTap( - action: action, - flushAlert: $flushAlert - ) - } - } + } + .bottomBar(isVisible: multipleSelectionViewModel.isEnabled) { + HStack(spacing: 0) { + ForEach(multipleSelectionViewModel.toolbarActions) { action in + ToolbarButton( + text: action.shortTitle ?? action.title, + icon: action.icon + ) { + Task { + await tryOrDisplayError { + try await multipleSelectionViewModel.didTap( + action: action, + flushAlert: $flushAlert + ) } - .disabled(action == .archive && splitViewManager.selectedFolder?.role == .archive) - } - - ToolbarButton( - text: MailResourcesStrings.Localizable.buttonMore, - icon: MailResourcesAsset.plusActions.swiftUIImage - ) { - multipleSelectionActionsTarget = .threads(Array(multipleSelectionViewModel.selectedItems), true) } } - .disabled(multipleSelectionViewModel.selectedItems.isEmpty) + .disabled(action == .archive && splitViewManager.selectedFolder?.role == .archive) + } + + ToolbarButton( + text: MailResourcesStrings.Localizable.buttonMore, + icon: MailResourcesAsset.plusActions.swiftUIImage + ) { + multipleSelectionActionsTarget = .threads(Array(multipleSelectionViewModel.selectedItems), true) } } + .disabled(multipleSelectionViewModel.selectedItems.isEmpty) } .actionsPanel(actionsTarget: $multipleSelectionActionsTarget) { withAnimation { diff --git a/Mail/Views/Thread/MessageHeaderDetailView.swift b/Mail/Views/Thread/MessageHeaderDetailView.swift index 58a26abdd..f8d774d21 100644 --- a/Mail/Views/Thread/MessageHeaderDetailView.swift +++ b/Mail/Views/Thread/MessageHeaderDetailView.swift @@ -25,23 +25,6 @@ import RealmSwift import SwiftUI import WrappingHStack -struct ViewWidthKey: PreferenceKey { - static var defaultValue: CGFloat = .zero - - static func reduce(value: inout CGFloat, nextValue: () -> CGFloat) { - value = max(value, nextValue()) - } -} - -struct ViewGeometry: View { - var body: some View { - GeometryReader { geometry in - Color.clear - .preference(key: ViewWidthKey.self, value: geometry.size.width) - } - } -} - struct MessageHeaderDetailView: View { @ObservedRealmObject var message: Message @@ -109,7 +92,7 @@ struct RecipientLabel: View { HStack(alignment: .top) { Text(title) .textStyle(.bodySmallSecondary) - .background(ViewGeometry()) + .background(ViewGeometry(key: ViewWidthKey.self, property: \.size.width)) .frame(width: labelWidth, alignment: .leading) VStack(alignment: .leading, spacing: 4) { ForEach(recipients, id: \.self) { recipient in diff --git a/Mail/Views/Thread/ThreadView.swift b/Mail/Views/Thread/ThreadView.swift index 6cf0c2a5d..b4a490046 100644 --- a/Mail/Views/Thread/ThreadView.swift +++ b/Mail/Views/Thread/ThreadView.swift @@ -53,10 +53,10 @@ struct ThreadView: View { var body: some View { ScrollView { VStack(spacing: 0) { - GeometryReader { geometry in + GeometryReader { proxy in Color.clear.preference( key: ScrollOffsetPreferenceKey.self, - value: geometry.frame(in: .named("scrollView")).origin + value: proxy.frame(in: .named("scrollView")).origin ) } .frame(width: 0, height: 0) @@ -100,32 +100,33 @@ struct ThreadView: View { .foregroundColor(thread.flagged ? MailResourcesAsset.yellowColor.swiftUIColor : .accentColor) } } - ToolbarItemGroup(placement: .bottomBar) { - ForEach(toolbarActions) { action in - if action == .reply { - ToolbarButton(text: action.title, icon: action.icon) { - didTap(action: action) - } - .adaptivePanel(item: $replyOrReplyAllMessage) { message in - ReplyActionsView( - mailboxManager: mailboxManager, - message: message, - messageReply: $navigationStore.messageReply - ) - } - } else { - ToolbarButton(text: action.title, icon: action.icon) { - didTap(action: action) - } - .disabled(action == .archive && thread.folder?.role == .archive) + } + .bottomBar { + ForEach(toolbarActions) { action in + if action == .reply { + ToolbarButton(text: action.title, icon: action.icon) { + didTap(action: action) } - Spacer() - } - ActionsPanelButton(threads: [thread]) { - ToolbarButtonLabel(text: MailResourcesStrings.Localizable.buttonMore, - icon: MailResourcesAsset.plusActions.swiftUIImage) + .adaptivePanel(item: $replyOrReplyAllMessage) { message in + ReplyActionsView( + mailboxManager: mailboxManager, + message: message, + messageReply: $navigationStore.messageReply + ) + } + } else { + ToolbarButton(text: action.title, icon: action.icon) { + didTap(action: action) + } + .disabled(action == .archive && thread.folder?.role == .archive) } + Spacer() + } + ActionsPanelButton(threads: [thread]) { + ToolbarButtonLabel(text: MailResourcesStrings.Localizable.buttonMore, + icon: MailResourcesAsset.plusActions.swiftUIImage) } + .frame(maxWidth: .infinity) } .onChange(of: thread.messages) { newMessagesList in if newMessagesList.isEmpty || thread.messageInFolderCount == 0 { diff --git a/MailCore/UI/UIConstants.swift b/MailCore/UI/UIConstants.swift index 3a7b9540d..3c5fb0a56 100644 --- a/MailCore/UI/UIConstants.swift +++ b/MailCore/UI/UIConstants.swift @@ -101,6 +101,10 @@ public enum UIConstants { public static let buttonsRadius: CGFloat = 16 public static let buttonsIconSize: CGFloat = 16 + public static let bottomBarVerticalPadding: CGFloat = 8 + public static let bottomBarSmallVerticalPadding: CGFloat = 4 + public static let bottomBarHorizontalMinimumSpace: CGFloat = 8 + public static let bottomSheetHorizontalPadding: CGFloat = 24 public static let componentsMaxWidth: CGFloat = 496