Skip to content

Commit

Permalink
Merge pull request #831 from Infomaniak/new-recipients-behavior
Browse files Browse the repository at this point in the history
feat: ComposeView collapse fields
  • Loading branch information
PhilippeWeidmann committed Jun 27, 2023
2 parents db09fd6 + 6a0257e commit f9cb09c
Show file tree
Hide file tree
Showing 15 changed files with 275 additions and 50 deletions.
48 changes: 48 additions & 0 deletions Mail/Components/MoreRecipientsChip.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
/*
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 <http://www.gnu.org/licenses/>.
*/

import SwiftUI
import MailCore

extension EdgeInsets {
init(uiEdgeInsets insets: UIEdgeInsets) {
self.init(top: insets.top, leading: insets.left, bottom: insets.bottom, trailing: insets.right)
}
}

struct MoreRecipientsChip: View {
@AppStorage(UserDefaults.shared.key(.accentColor)) private var accentColor = DefaultPreferences.accentColor

let count: Int

var body: some View {
Text("+\(count)")
.textStyle(.bodyAccent)
.padding(EdgeInsets(uiEdgeInsets: UIConstants.chipInsets))
.background(
RoundedRectangle(cornerRadius: 50)
.fill(accentColor.secondary.swiftUIColor)
)
}
}

struct MoreRecipientsChip_Previews: PreviewProvider {
static var previews: some View {
MoreRecipientsChip(count: 42)
}
}
10 changes: 7 additions & 3 deletions Mail/Components/RecipientChipLabel.swift
Original file line number Diff line number Diff line change
Expand Up @@ -22,21 +22,25 @@ import SwiftUI
import UIKit

struct RecipientChipLabelView: UIViewRepresentable {
@Environment(\.isEnabled) private var isEnabled: Bool

let recipient: Recipient
let removeHandler: () -> Void
let switchFocusHandler: () -> Void
var removeHandler: (() -> Void)?
var switchFocusHandler: (() -> Void)?

func makeUIView(context: Context) -> RecipientChipLabel {
let label = RecipientChipLabel(recipient: recipient)
label.removeHandler = removeHandler
label.switchFocusHandler = switchFocusHandler
label.setContentHuggingPriority(.defaultHigh, for: .horizontal)
label.setContentHuggingPriority(.defaultHigh, for: .vertical)
label.setContentCompressionResistancePriority(.defaultLow, for: .horizontal)
return label
}

func updateUIView(_ uiLabel: RecipientChipLabel, context: Context) {
uiLabel.text = recipient.name.isEmpty ? recipient.email : recipient.name
uiLabel.isUserInteractionEnabled = isEnabled
}
}

Expand All @@ -51,7 +55,7 @@ class RecipientChipLabel: UILabel, UIKeyInput {
return contentSize
}

override var canBecomeFirstResponder: Bool { return true }
override var canBecomeFirstResponder: Bool { return isUserInteractionEnabled }

var hasText = false

Expand Down
2 changes: 2 additions & 0 deletions Mail/Helpers/PreviewHelper.swift
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,8 @@ enum PreviewHelper {

static let sampleRecipient3 = Recipient(email: "test@example.com", name: "")

static let sampleRecipientsList = [sampleRecipient1, sampleRecipient2, sampleRecipient3].toRealmList()

static let sampleAttachment = Attachment(
uuid: "",
partId: "",
Expand Down
2 changes: 1 addition & 1 deletion Mail/Views/New Message/AutocompletionView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,7 @@ struct AutocompletionView_Previews: PreviewProvider {
AutocompletionView(
textDebounce: TextDebounce(),
autocompletion: .constant([]),
addedRecipients: .constant([PreviewHelper.sampleRecipient1].toRealmList())
addedRecipients: .constant(PreviewHelper.sampleRecipientsList)
) { _ in /* Preview */ }
}
}
5 changes: 3 additions & 2 deletions Mail/Views/New Message/ComposeMessageHeaderView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ struct ComposeMessageHeaderView: View {
@Binding var autocompletionType: ComposeViewFieldType?

var body: some View {
VStack(spacing: UIConstants.composeViewVerticalSpacing) {
VStack(spacing: 0) {
ComposeMessageCellStaticText(
autocompletionType: $autocompletionType,
type: .from,
Expand All @@ -44,7 +44,8 @@ struct ComposeMessageHeaderView: View {
showRecipientsFields: $showRecipientsFields,
autocompletionType: $autocompletionType,
focusedField: _focusedField,
type: .to
type: .to,
areCCAndBCCEmpty: draft.cc.isEmpty && draft.bcc.isEmpty
)

if showRecipientsFields {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,12 @@ struct ComposeMessageCellRecipients: View {
@FocusState var focusedField: ComposeViewFieldType?

let type: ComposeViewFieldType
var areCCAndBCCEmpty = false

/// It should be displayed only for the field to if cc and bcc are empty and when autocompletion is not displayed
private var shouldDisplayChevron: Bool {
return type == .to && autocompletionType == nil && areCCAndBCCEmpty
}

var body: some View {
VStack(spacing: 0) {
Expand All @@ -58,24 +64,25 @@ struct ComposeMessageCellRecipients: View {
.textStyle(.bodySecondary)

RecipientField(
focusedField: _focusedField,
currentText: $textDebounce.text,
recipients: $recipients,
focusedField: _focusedField,
type: type
) {
if let bestMatch = autocompletion.first {
addNewRecipient(bestMatch)
}
}

if type == .to && autocompletionType == nil {
if shouldDisplayChevron {
Spacer()
ChevronButton(isExpanded: $showRecipientsFields)
}
}
.frame(maxWidth: .infinity, alignment: .leading)
.padding(.vertical, UIConstants.composeViewHeaderCellVerticalSpacing)

IKDivider()
.padding(.top, UIConstants.composeViewVerticalSpacing)
}

if autocompletionType == type {
Expand All @@ -88,6 +95,7 @@ struct ComposeMessageCellRecipients: View {
.padding(.top, 8)
}
}
.contentShape(Rectangle())
.onTapGesture {
focusedField = type
}
Expand Down Expand Up @@ -125,8 +133,11 @@ struct ComposeMessageCellRecipients: View {

struct ComposeMessageCellRecipients_Previews: PreviewProvider {
static var previews: some View {
ComposeMessageCellRecipients(recipients: .constant([
PreviewHelper.sampleRecipient1, PreviewHelper.sampleRecipient2, PreviewHelper.sampleRecipient3
].toRealmList()), showRecipientsFields: .constant(false), autocompletionType: .constant(nil), type: .bcc)
ComposeMessageCellRecipients(
recipients: .constant(PreviewHelper.sampleRecipientsList),
showRecipientsFields: .constant(false),
autocompletionType: .constant(nil),
type: .bcc
)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ struct ComposeMessageCellStaticText: View {

var body: some View {
if autocompletionType == nil {
VStack(spacing: UIConstants.composeViewVerticalSpacing) {
VStack(spacing: 0) {
HStack {
Text(type.title)
.textStyle(.bodySecondary)
Expand All @@ -36,6 +36,8 @@ struct ComposeMessageCellStaticText: View {
.textStyle(.body)
}
.frame(maxWidth: .infinity, alignment: .leading)
.padding(.vertical, UIConstants.composeViewHeaderCellLargeVerticalSpacing)

IKDivider()
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,15 +29,18 @@ struct ComposeMessageCellTextField: View {

var body: some View {
if autocompletionType == nil {
VStack(spacing: UIConstants.composeViewVerticalSpacing) {
VStack(spacing: 0) {
HStack {
Text(type.title)
.textStyle(.bodySecondary)

TextField("", text: $text)
.focused($focusedField, equals: .subject)
.textStyle(.body)
}
.frame(maxWidth: .infinity, alignment: .leading)
.padding(.vertical, UIConstants.composeViewHeaderCellLargeVerticalSpacing)

IKDivider()
}
.onTapGesture {
Expand Down
56 changes: 25 additions & 31 deletions Mail/Views/New Message/RecipientField.swift
Original file line number Diff line number Diff line change
Expand Up @@ -28,11 +28,11 @@ import WrappingHStack
struct RecipientField: View {
@State private var keyboardHeight: CGFloat = 0

@FocusState var focusedField: ComposeViewFieldType?

@Binding var currentText: String
@Binding var recipients: RealmSwift.List<Recipient>

@FocusState var focusedField: ComposeViewFieldType?

let type: ComposeViewFieldType
var onSubmit: (() -> Void)?

Expand All @@ -41,22 +41,34 @@ struct RecipientField: View {
currentText.trimmingCharacters(in: .whitespacesAndNewlines)
}

private var isCurrentFieldFocused: Bool {
if case let .chip(hash, _) = focusedField {
return type.hashValue == hash
}
return type == focusedField
}

private var isExpanded: Bool {
return isCurrentFieldFocused || recipients.isEmpty
}

var body: some View {
VStack {
VStack(spacing: 0) {
if !recipients.isEmpty {
WrappingHStack(recipients.indices, spacing: .constant(8), lineSpacing: 8) { i in
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 }
RecipientsList(
focusedField: _focusedField,
recipients: $recipients,
isCurrentFieldFocused: isCurrentFieldFocused,
type: type
)
}

RecipientsTextField(text: $currentText, onSubmit: onSubmit, onBackspace: handleBackspaceTextField)
.focused($focusedField, equals: type)
.padding(.top, isCurrentFieldFocused && !recipients.isEmpty ? 4 : 0)
.padding(.top, UIConstants.chipInsets.top)
.padding(.bottom, UIConstants.chipInsets.bottom)
.frame(width: isExpanded ? nil : 0, height: isExpanded ? nil : 0)
}
.onReceive(NotificationCenter.default.publisher(for: UIResponder.keyboardDidShowNotification)) { output in
if let userInfo = output.userInfo,
Expand All @@ -69,33 +81,15 @@ struct RecipientField: View {
}
}

@MainActor private func remove(recipientAt: Int) {
withAnimation {
$recipients.remove(at: recipientAt)
}
}

private func handleBackspaceTextField(isTextEmpty: Bool) {
if let recipient = recipients.last, isTextEmpty {
focusedField = .chip(type.hashValue, recipient)
}
}

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 {
static var previews: some View {
RecipientField(currentText: .constant(""), recipients: .constant([
PreviewHelper.sampleRecipient1, PreviewHelper.sampleRecipient2, PreviewHelper.sampleRecipient3
].toRealmList()), type: .to)
RecipientField(currentText: .constant(""), recipients: .constant(PreviewHelper.sampleRecipientsList), type: .to)
}
}
63 changes: 63 additions & 0 deletions Mail/Views/New Message/Recipients/FullRecipientsList.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
/*
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 <http://www.gnu.org/licenses/>.
*/

import MailCore
import RealmSwift
import SwiftUI
import WrappingHStack

struct FullRecipientsList: View {
@Binding var recipients: RealmSwift.List<Recipient>

@FocusState var focusedField: ComposeViewFieldType?

let type: ComposeViewFieldType

var body: some View {
WrappingHStack(recipients.indices, spacing: .constant(8), lineSpacing: 8) { i in
RecipientChip(recipient: recipients[i], fieldType: type, focusedField: _focusedField) {
remove(recipientAt: i)
} switchFocusHandler: {
switchFocus()
}
.focused($focusedField, equals: .chip(type.hashValue, recipients[i]))
}
}

@MainActor private func remove(recipientAt: Int) {
withAnimation {
$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 FullRecipientsList_Previews: PreviewProvider {
static var previews: some View {
FullRecipientsList(recipients: .constant(PreviewHelper.sampleRecipientsList), type: .to)
}
}
Loading

0 comments on commit f9cb09c

Please sign in to comment.