Merge branch 'main' into task/ET-4278

# Conflicts:
#	Modules/App/Sources/Resources/Localizable.xcstrings
This commit is contained in:
Maciej Gomółka
2025-08-18 14:40:50 +02:00
34 changed files with 548 additions and 237 deletions
+1 -1
View File
@@ -1,4 +1,4 @@
{
"project": "apple-mail-new",
"locale": "c6e4f8631d24ac1b10b5a6c3f3e71f8f1cbe68e8"
"locale": "5de0ac2b963308bb91a25e069a2ce18a605a6f40"
}
@@ -964,6 +964,24 @@
"state" : "translated",
"value" : "Required to save files in your Photos app."
}
},
"es" : {
"stringUnit" : {
"state" : "translated",
"value" : "Necesario para guardar archivos en tu aplicación Fotos."
}
},
"fr" : {
"stringUnit" : {
"state" : "translated",
"value" : "Nécessaire pour enregistrer des fichiers dans votre application Photos."
}
},
"ja" : {
"stringUnit" : {
"state" : "translated",
"value" : "お客様のフォトアプリにファイルを保存するために必要です。"
}
}
}
}
@@ -203,6 +203,42 @@
"state" : "new",
"value" : "%1$@ of %2$@"
}
},
"es" : {
"stringUnit" : {
"state" : "translated",
"value" : "%1$@ de %2$@"
}
},
"es-419" : {
"stringUnit" : {
"state" : "translated",
"value" : "%1$@ de %2$@"
}
},
"fr" : {
"stringUnit" : {
"state" : "translated",
"value" : "%1$@ De %2$@"
}
},
"ja" : {
"stringUnit" : {
"state" : "translated",
"value" : "%2$@ の中の%1$@"
}
},
"nl" : {
"stringUnit" : {
"state" : "translated",
"value" : "%1$@ van %2$@"
}
},
"sv" : {
"stringUnit" : {
"state" : "translated",
"value" : "%1$@ av %2$@"
}
}
}
},
@@ -5110,6 +5146,112 @@
}
}
}
},
"es" : {
"stringUnit" : {
"state" : "translated",
"value" : "%#@arg1@"
},
"substitutions" : {
"arg1" : {
"argNum" : 1,
"formatSpecifier" : "lld",
"variations" : {
"plural" : {
"one" : {
"stringUnit" : {
"state" : "translated",
"value" : "Conversación movida al archivo."
}
},
"other" : {
"stringUnit" : {
"state" : "translated",
"value" : "Conversaciones movidas al archivo."
}
}
}
}
}
}
},
"fr" : {
"stringUnit" : {
"state" : "translated",
"value" : "%#@arg1@."
},
"substitutions" : {
"arg1" : {
"argNum" : 1,
"formatSpecifier" : "lld",
"variations" : {
"plural" : {
"one" : {
"stringUnit" : {
"state" : "translated",
"value" : "La conversation a été déplacée dans les Archives"
}
},
"other" : {
"stringUnit" : {
"state" : "translated",
"value" : "Les conversations ont été déplacées dans les Archives."
}
}
}
}
}
}
},
"ja" : {
"stringUnit" : {
"state" : "translated",
"value" : "%#@arg1@"
},
"substitutions" : {
"arg1" : {
"argNum" : 1,
"formatSpecifier" : "lld",
"variations" : {
"plural" : {
"other" : {
"stringUnit" : {
"state" : "translated",
"value" : "会話をアーカイブに移動しました。"
}
}
}
}
}
}
},
"sv" : {
"stringUnit" : {
"state" : "translated",
"value" : "%#@arg1@"
},
"substitutions" : {
"arg1" : {
"argNum" : 1,
"formatSpecifier" : "lld",
"variations" : {
"plural" : {
"one" : {
"stringUnit" : {
"state" : "translated",
"value" : "Konversation flyttad till arkiv."
}
},
"other" : {
"stringUnit" : {
"state" : "translated",
"value" : "Konversationer flyttade till arkiv."
}
}
}
}
}
}
}
}
},
@@ -10565,6 +10707,112 @@
}
}
}
},
"es" : {
"stringUnit" : {
"state" : "translated",
"value" : "%#@arg1@"
},
"substitutions" : {
"arg1" : {
"argNum" : 1,
"formatSpecifier" : "lld",
"variations" : {
"plural" : {
"one" : {
"stringUnit" : {
"state" : "translated",
"value" : "Mensaje movido al archivo."
}
},
"other" : {
"stringUnit" : {
"state" : "translated",
"value" : "Mensajes movidos al Archivo."
}
}
}
}
}
}
},
"fr" : {
"stringUnit" : {
"state" : "translated",
"value" : "%#@arg1@."
},
"substitutions" : {
"arg1" : {
"argNum" : 1,
"formatSpecifier" : "lld",
"variations" : {
"plural" : {
"one" : {
"stringUnit" : {
"state" : "translated",
"value" : "Le message a été déplacé dans les Archives."
}
},
"other" : {
"stringUnit" : {
"state" : "translated",
"value" : "Les messages ont été déplacés dans les Archives."
}
}
}
}
}
}
},
"ja" : {
"stringUnit" : {
"state" : "translated",
"value" : "%#@arg1@"
},
"substitutions" : {
"arg1" : {
"argNum" : 1,
"formatSpecifier" : "lld",
"variations" : {
"plural" : {
"other" : {
"stringUnit" : {
"state" : "translated",
"value" : "メッセージをアーカイブに移動しました。"
}
}
}
}
}
}
},
"sv" : {
"stringUnit" : {
"state" : "translated",
"value" : "%#@arg1@"
},
"substitutions" : {
"arg1" : {
"argNum" : 1,
"formatSpecifier" : "lld",
"variations" : {
"plural" : {
"one" : {
"stringUnit" : {
"state" : "translated",
"value" : "Meddelande flyttat till arkiv."
}
},
"other" : {
"stringUnit" : {
"state" : "translated",
"value" : "Meddelanden flyttade till arkiv."
}
}
}
}
}
}
}
}
},
@@ -16,6 +16,7 @@
// along with Proton Mail. If not, see https://www.gnu.org/licenses/.
import Combine
import Foundation
import InboxCore
import proton_app_uniffi
@@ -59,6 +60,7 @@ final class MailSettingsLiveQuery: MailSettingLiveQuerying {
.map(keyPath)
.removeDuplicates()
.dropFirst()
.debounce(for: .milliseconds(100), scheduler: DispatchQueue.main)
.eraseToAnyPublisher()
}
@@ -21,15 +21,15 @@ import proton_app_uniffi
import SwiftUI
extension View {
func mailboxActionBar(
state: MailboxActionBarState,
availableActions: AvailableMailboxActionBarActions,
func listActionsToolbar(
state: ListActionsToolbarState,
availableActions: AvailableListToolbarActions,
itemTypeForActionBar: MailboxItemType,
mailUserSession: MailUserSession,
selectedItems: Binding<Set<MailboxSelectedItem>>
) -> some View {
modifier(
MailboxActionBarViewModifier(
ListActionBarViewModifier(
state: state,
availableActions: availableActions,
itemTypeForActionBar: itemTypeForActionBar,
@@ -39,13 +39,13 @@ extension View {
}
}
private struct MailboxActionBarViewModifier: ViewModifier {
private struct ListActionBarViewModifier: ViewModifier {
@Binding var selectedItems: Set<MailboxSelectedItem>
@EnvironmentObject var mailbox: Mailbox
@EnvironmentObject var toastStateStore: ToastStateStore
private let state: MailboxActionBarState
private let state: ListActionsToolbarState
private let itemTypeForActionBar: MailboxItemType
private let availableActions: AvailableMailboxActionBarActions
private let availableActions: AvailableListToolbarActions
private let deleteActions: DeleteActions
private let moveToActions: MoveToActions
private let mailUserSession: MailUserSession
@@ -53,8 +53,8 @@ private struct MailboxActionBarViewModifier: ViewModifier {
private let readActionPerformerActions: ReadActionPerformerActions
init(
state: MailboxActionBarState,
availableActions: AvailableMailboxActionBarActions,
state: ListActionsToolbarState,
availableActions: AvailableListToolbarActions,
starActionPerformerActions: StarActionPerformerActions = .productionInstance,
readActionPerformerActions: ReadActionPerformerActions = .productionInstance,
deleteActions: DeleteActions = .productionInstance,
@@ -76,7 +76,7 @@ private struct MailboxActionBarViewModifier: ViewModifier {
func body(content: Content) -> some View {
StoreView(
store: MailboxActionBarStateStore(
store: ListActionsToolbarStore(
state: state,
availableActions: availableActions,
starActionPerformerActions: starActionPerformerActions,
@@ -96,11 +96,11 @@ private struct MailboxActionBarViewModifier: ViewModifier {
.bottomToolbarStyle()
.onChange(of: selectedItems) { oldValue, newValue in
if oldValue != newValue {
store.handle(action: .mailboxItemsSelectionUpdated(ids: selectedItemsIDs))
store.handle(action: .listItemsSelectionUpdated(ids: selectedItemsIDs))
}
}
.onLoad {
store.handle(action: .mailboxItemsSelectionUpdated(ids: selectedItemsIDs))
store.handle(action: .listItemsSelectionUpdated(ids: selectedItemsIDs))
}
.labelAsSheet(
mailbox: { mailbox },
@@ -114,7 +114,7 @@ private struct MailboxActionBarViewModifier: ViewModifier {
navigation: { _ in }
)
.sheet(item: store.binding(\.moreActionSheetPresented)) { state in
MailboxActionBarMoreSheet(state: state) { action in
ListActionsToolbarMoreSheet(state: state) { action in
store.handle(action: .moreSheetAction(action, ids: selectedItemsIDs))
} editToolbarTapped: {
// FIXME: - Edit toolbar action
@@ -141,8 +141,8 @@ private struct MailboxActionBarViewModifier: ViewModifier {
}
private func toolbarContent(
state: MailboxActionBarState,
store: MailboxActionBarStateStore
state: ListActionsToolbarState,
store: ListActionsToolbarStore
) -> some ToolbarContent {
ToolbarItemGroup(placement: .bottomBar) {
HStack {
@@ -151,7 +151,7 @@ private struct MailboxActionBarViewModifier: ViewModifier {
Spacer()
}
Button(action: { store.handle(action: .actionSelected(action, ids: selectedItemsIDs)) }) {
action.displayData.icon
action.displayData.image
.foregroundStyle(DS.Color.Icon.weak)
}
.accessibilityIdentifier(MailboxActionBarViewIdentifiers.button(index: index))
@@ -17,12 +17,12 @@
import proton_app_uniffi
struct MailboxActionBarActionsProvider {
let availableActions: AvailableMailboxActionBarActions
struct ListActionsToolbarActionsProvider {
let availableActions: AvailableListToolbarActions
let itemTypeForActionBar: MailboxItemType
let mailbox: Mailbox
func actions(forItemsWith ids: [ID]) async -> AllBottomBarMessageActions {
func actions(forItemsWith ids: [ID]) async -> AllListActions {
switch itemTypeForActionBar {
case .message:
try! await availableActions.message(mailbox, ids).get()
@@ -32,21 +32,21 @@ struct MailboxActionBarActionsProvider {
}
}
struct AvailableMailboxActionBarActions {
let message: MessageBottomBarActionsProvider
let conversation: ConversationBottomBarActionsProvider
struct AvailableListToolbarActions {
let message: MessageListActionsToolbarActionsProvider
let conversation: ConversationListActionsToolbarActionsProvider
}
extension AvailableMailboxActionBarActions {
extension AvailableListToolbarActions {
static var productionInstance: Self {
.init(
message: allAvailableBottomBarActionsForMessages,
conversation: allAvailableBottomBarActionsForConversations
message: allAvailableListActionsForMessages,
conversation: allAvailableListActionsForConversations
)
}
}
typealias MessageBottomBarActionsProvider = (Mailbox, [Id]) async -> AllAvailableBottomBarActionsForMessagesResult
typealias ConversationBottomBarActionsProvider = (Mailbox, [Id]) async -> AllAvailableBottomBarActionsForConversationsResult
typealias MessageListActionsToolbarActionsProvider = (Mailbox, [Id]) async -> AllAvailableListActionsForMessagesResult
typealias ConversationListActionsToolbarActionsProvider = (Mailbox, [Id]) async -> AllAvailableListActionsForConversationsResult
@@ -15,18 +15,19 @@
// You should have received a copy of the GNU General Public License
// along with Proton Mail. If not, see https://www.gnu.org/licenses/.
import proton_app_uniffi
import InboxCoreUI
import InboxDesignSystem
import SwiftUI
struct MailboxActionBarMoreSheet: View {
let state: MailboxActionBarMoreSheetState
let actionTapped: (BottomBarAction) -> Void
struct ListActionsToolbarMoreSheet: View {
let state: ListActionsToolbarMoreSheetState
let actionTapped: (ListActions) -> Void
let editToolbarTapped: () -> Void
init(
state: MailboxActionBarMoreSheetState,
actionTapped: @escaping (BottomBarAction) -> Void,
state: ListActionsToolbarMoreSheetState,
actionTapped: @escaping (ListActions) -> Void,
editToolbarTapped: @escaping () -> Void
) {
self.state = state
@@ -54,11 +55,11 @@ struct MailboxActionBarMoreSheet: View {
// MARK: - Private
private func section(content: [BottomBarAction]) -> some View {
private func section(content: [ListActions]) -> some View {
ActionSheetSection {
ForEachLast(collection: content) { action, isLast in
ActionSheetImageButton(
displayData: action.actionDisplayData,
displayData: action.displayData,
displayBottomSeparator: !isLast,
action: { actionTapped(action) }
)
@@ -79,15 +80,9 @@ struct MailboxActionBarMoreSheet: View {
}
private extension BottomBarAction {
var actionDisplayData: ActionDisplayData {
.init(title: displayData.name.unsafelyUnwrapped, image: displayData.icon)
}
}
#Preview {
MailboxActionBarMoreSheet(
state: MailboxActionBarMoreSheetPreviewProvider.state(),
ListActionsToolbarMoreSheet(
state: ListActionsToolbarMoreSheetPreviewProvider.state(),
actionTapped: { _ in },
editToolbarTapped: {}
)
@@ -17,9 +17,9 @@
import Foundation
enum MailboxActionBarMoreSheetPreviewProvider {
enum ListActionsToolbarMoreSheetPreviewProvider {
static func state() -> MailboxActionBarMoreSheetState {
static func state() -> ListActionsToolbarMoreSheetState {
.init(
selectedItemsIDs: [.init(value: 1), .init(value: 2), .init(value: 3)],
bottomBarActions: [
@@ -27,12 +27,12 @@ enum MailboxActionBarMoreSheetPreviewProvider {
.moveToSystemFolder(.init(localId: .init(value: 4), name: .archive)),
.moveToSystemFolder(.init(localId: .init(value: 5), name: .inbox)),
.moveToSystemFolder(.init(localId: .init(value: 6), name: .trash)),
.star
.star,
],
moreSheetOnlyActions: [
.labelAs,
.moveTo,
.moveToSystemFolder(.init(localId: .init(value: 7), name: .spam))
.moveToSystemFolder(.init(localId: .init(value: 7), name: .spam)),
]
)
}
@@ -20,10 +20,10 @@ import InboxCoreUI
import proton_app_uniffi
import SwiftUI
final class MailboxActionBarStateStore: StateStore {
@Published var state: MailboxActionBarState
final class ListActionsToolbarStore: StateStore {
@Published var state: ListActionsToolbarState
private let actionsProvider: MailboxActionBarActionsProvider
private let actionsProvider: ListActionsToolbarActionsProvider
private let starActionPerformer: StarActionPerformer
private let readActionPerformer: ReadActionPerformer
private let deleteActionsPerformer: DeleteActionPerformer
@@ -33,8 +33,8 @@ final class MailboxActionBarStateStore: StateStore {
private let mailUserSession: MailUserSession
init(
state: MailboxActionBarState,
availableActions: AvailableMailboxActionBarActions,
state: ListActionsToolbarState,
availableActions: AvailableListToolbarActions,
starActionPerformerActions: StarActionPerformerActions,
readActionPerformerActions: ReadActionPerformerActions,
deleteActions: DeleteActions,
@@ -65,9 +65,9 @@ final class MailboxActionBarStateStore: StateStore {
self.toastStateStore = toastStateStore
}
func handle(action: MailboxActionBarAction) {
func handle(action: ListActionsToolbarAction) {
switch action {
case .mailboxItemsSelectionUpdated(let ids):
case .listItemsSelectionUpdated(let ids):
fetchAvailableBottomBarActions(for: ids)
case .actionSelected(let action, let ids):
handle(action: action, ids: ids)
@@ -84,10 +84,10 @@ final class MailboxActionBarStateStore: StateStore {
// MARK: - Private
private func handle(action: BottomBarActions, ids: [ID]) {
private func handle(action: ListActions, ids: [ID]) {
switch action {
case .more:
let moreActionSheetState = MailboxActionBarMoreSheetState(
let moreActionSheetState = ListActionsToolbarMoreSheetState(
selectedItemsIDs: ids,
bottomBarActions: state.bottomBarActions.moreActionFiltered,
moreSheetOnlyActions: state.moreSheetOnlyActions
@@ -118,7 +118,7 @@ final class MailboxActionBarStateStore: StateStore {
dismissMoreActionSheet()
readActionPerformer.markAsUnread(itemsWithIDs: ids, itemType: itemTypeForActionBar)
case .permanentDelete:
let keyPath: WritableKeyPath<MailboxActionBarState, AlertModel?> =
let keyPath: WritableKeyPath<ListActionsToolbarState, AlertModel?> =
state.moreActionSheetPresented != nil ? \.moreDeleteConfirmationAlert : \.deleteConfirmationAlert
let alert: AlertModel = .deleteConfirmation(
itemsCount: ids.count,
@@ -190,11 +190,11 @@ final class MailboxActionBarStateStore: StateStore {
}
}
private func updateActions(actions: AllBottomBarMessageActions) {
private func updateActions(actions: AllListActions) {
state =
state
.copy(\.bottomBarActions, to: actions.visibleBottomBarActions)
.copy(\.moreSheetOnlyActions, to: actions.hiddenBottomBarActions)
.copy(\.bottomBarActions, to: actions.visibleListActions)
.copy(\.moreSheetOnlyActions, to: actions.hiddenListActions)
}
private func dismissMoreActionSheet() {
@@ -230,7 +230,7 @@ final class MailboxActionBarStateStore: StateStore {
}
}
private extension Array where Element == BottomBarActions {
private extension Array where Element == ListActions {
var moreActionFiltered: Self {
filter { $0 != .more }
@@ -17,16 +17,16 @@
import proton_app_uniffi
extension AllBottomBarMessageActions {
extension AllListActions {
static var testData: Self {
.init(
hiddenBottomBarActions: [
hiddenListActions: [
.notSpam(.testInbox),
.permanentDelete,
.moveToSystemFolder(.init(localId: .init(value: 7), name: .archive))
],
visibleBottomBarActions: [.markRead, .star, .moveTo, .labelAs, .more]
visibleListActions: [.markRead, .star, .moveTo, .labelAs, .more]
)
}
@@ -0,0 +1,66 @@
// Copyright (c) 2024 Proton Technologies AG
//
// This file is part of Proton Mail.
//
// Proton Mail 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.
//
// Proton Mail 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 Proton Mail. If not, see https://www.gnu.org/licenses/.
import proton_app_uniffi
import InboxDesignSystem
import SwiftUI
extension ListActions {
var displayData: ActionDisplayData {
switch self {
case .labelAs:
.init(title: L10n.Action.labelAs, imageResource: DS.Icon.icTag)
case .markRead:
.init(title: L10n.Action.markAsRead, imageResource: DS.Icon.icEnvelopeOpen)
case .markUnread:
.init(title: L10n.Action.markAsUnread, imageResource: DS.Icon.icEnvelopeDot)
case .more:
.init(title: .empty, imageResource: DS.Icon.icThreeDotsHorizontal)
case .moveTo:
.init(title: L10n.Action.moveTo, imageResource: DS.Icon.icFolderArrowIn)
case .moveToSystemFolder(let systemFolder):
switch systemFolder.name {
case .archive:
.init(title: L10n.Action.moveToArchive, imageResource: DS.Icon.icArchiveBox)
case .inbox:
.init(title: L10n.Action.moveToInbox, imageResource: DS.Icon.icInbox)
case .spam:
.init(title: L10n.Action.moveToSpam, imageResource: DS.Icon.icSpam)
case .trash:
.init(title: L10n.Action.moveToTrash, imageResource: DS.Icon.icTrash)
}
case .notSpam:
.init(title: L10n.Action.notSpam, imageResource: DS.Icon.icNotSpam)
case .permanentDelete:
.init(title: L10n.Action.deletePermanently, imageResource: DS.Icon.icTrashCross)
case .star:
.init(title: L10n.Action.star, image: Image(symbol: .star))
case .unstar:
.init(title: L10n.Action.unstar, image: Image(symbol: .starSlash))
case .snooze:
.init(title: L10n.Action.snooze, imageResource: DS.Icon.icClock)
}
}
}
extension LocalizedStringResource {
static var empty: Self {
"".notLocalized.stringResource
}
}
@@ -15,10 +15,12 @@
// You should have received a copy of the GNU General Public License
// along with Proton Mail. If not, see https://www.gnu.org/licenses/.
enum MailboxActionBarAction {
case mailboxItemsSelectionUpdated(ids: [ID])
case actionSelected(BottomBarAction, ids: [ID])
case moreSheetAction(BottomBarAction, ids: [ID])
import proton_app_uniffi
enum ListActionsToolbarAction {
case listItemsSelectionUpdated(ids: [ID])
case actionSelected(ListActions, ids: [ID])
case moreSheetAction(ListActions, ids: [ID])
case dismissLabelAsSheet
case dismissMoveToSheet
case alertActionTapped(DeleteConfirmationAlertAction, ids: [ID])
@@ -15,12 +15,13 @@
// You should have received a copy of the GNU General Public License
// along with Proton Mail. If not, see https://www.gnu.org/licenses/.
import proton_app_uniffi
import Foundation
struct MailboxActionBarMoreSheetState: Identifiable, Equatable {
struct ListActionsToolbarMoreSheetState: Identifiable, Equatable {
let selectedItemsIDs: [ID]
let bottomBarActions: [BottomBarAction]
let moreSheetOnlyActions: [BottomBarAction]
let bottomBarActions: [ListActions]
let moreSheetOnlyActions: [ListActions]
// MARK: - Identifiable
@@ -15,13 +15,14 @@
// You should have received a copy of the GNU General Public License
// along with Proton Mail. If not, see https://www.gnu.org/licenses/.
import proton_app_uniffi
import InboxCore
import InboxCoreUI
struct MailboxActionBarState: Copying, Equatable {
var bottomBarActions: [BottomBarAction]
var moreSheetOnlyActions: [BottomBarAction]
var moreActionSheetPresented: MailboxActionBarMoreSheetState?
struct ListActionsToolbarState: Copying, Equatable {
var bottomBarActions: [ListActions]
var moreSheetOnlyActions: [ListActions]
var moreActionSheetPresented: ListActionsToolbarMoreSheetState?
var labelAsSheetPresented: ActionSheetInput?
var moveToSheetPresented: ActionSheetInput?
var deleteConfirmationAlert: AlertModel?
@@ -29,7 +30,7 @@ struct MailboxActionBarState: Copying, Equatable {
var isSnoozeSheetPresented: Bool
}
extension MailboxActionBarState {
extension ListActionsToolbarState {
static var initial: Self {
.init(
bottomBarActions: [],
@@ -1,67 +0,0 @@
// Copyright (c) 2024 Proton Technologies AG
//
// This file is part of Proton Mail.
//
// Proton Mail 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.
//
// Proton Mail 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 Proton Mail. If not, see https://www.gnu.org/licenses/.
import InboxDesignSystem
import SwiftUI
extension BottomBarAction {
var displayData: MailboxActionBarActionDisplayData {
switch self {
case .labelAs:
.init(imageResource: DS.Icon.icTag, name: L10n.Action.labelAs)
case .markRead:
.init(imageResource: DS.Icon.icEnvelopeOpen, name: L10n.Action.markAsRead)
case .markUnread:
.init(imageResource: DS.Icon.icEnvelopeDot, name: L10n.Action.markAsUnread)
case .more:
.init(imageResource: DS.Icon.icThreeDotsHorizontal, name: nil)
case .moveTo:
.init(imageResource: DS.Icon.icFolderArrowIn, name: L10n.Action.moveTo)
case .moveToSystemFolder(let systemFolder):
switch systemFolder.name {
case .archive:
.init(imageResource: DS.Icon.icArchiveBox, name: L10n.Action.moveToArchive)
case .inbox:
.init(imageResource: DS.Icon.icInbox, name: L10n.Action.moveToInbox)
case .spam:
.init(imageResource: DS.Icon.icSpam, name: L10n.Action.moveToSpam)
case .trash:
.init(imageResource: DS.Icon.icTrash, name: L10n.Action.moveToTrash)
}
case .notSpam:
.init(imageResource: DS.Icon.icNotSpam, name: L10n.Action.notSpam)
case .permanentDelete:
.init(imageResource: DS.Icon.icTrashCross, name: L10n.Action.deletePermanently)
case .star:
.init(icon: Image(symbol: .star), name: L10n.Action.star)
case .unstar:
.init(icon: Image(symbol: .starSlash), name: L10n.Action.unstar)
case .snooze:
.init(imageResource: DS.Icon.icClock, name: L10n.Action.snooze)
}
}
}
private extension MailboxActionBarActionDisplayData {
init(imageResource: ImageResource, name: LocalizedStringResource?) {
self.init(icon: Image(imageResource), name: name)
}
}
@@ -1,21 +0,0 @@
// Copyright (c) 2024 Proton Technologies AG
//
// This file is part of Proton Mail.
//
// Proton Mail 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.
//
// Proton Mail 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 Proton Mail. If not, see https://www.gnu.org/licenses/.
import proton_app_uniffi
@available(*, deprecated, message: "Use BottomBarActions directly or rename the uniffi enum")
typealias BottomBarAction = BottomBarActions
@@ -1,28 +0,0 @@
// Copyright (c) 2024 Proton Technologies AG
//
// This file is part of Proton Mail.
//
// Proton Mail 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.
//
// Proton Mail 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 Proton Mail. If not, see https://www.gnu.org/licenses/.
import SwiftUI
struct MailboxActionBarActionDisplayData {
let icon: Image
let name: LocalizedStringResource?
init(icon: Image, name: LocalizedStringResource?) {
self.icon = icon
self.name = name
}
}
@@ -28,7 +28,7 @@ final class ConversationDetailModel: Sendable, ObservableObject {
@Published private(set) var mailbox: Mailbox?
@Published private(set) var conversationID: ID?
@Published private(set) var isStarred: Bool
@Published private(set) var bottomBarActions: [BottomBarActions] = []
@Published private(set) var bottomBarActions: [ListActions] = []
@Published var actionSheets: MailboxActionSheetsState = .initial()
@Published var editScheduledMessageConfirmationAlert: AlertModel?
@Published var deleteConfirmationAlert: AlertModel?
@@ -171,7 +171,7 @@ final class ConversationDetailModel: Sendable, ObservableObject {
isStarred ? unstarConversation() : starConversation()
}
func handleConversation(action: BottomBarActions, toastStateStore: ToastStateStore, goBack: @escaping () -> Void) {
func handleConversation(action: ListActions, toastStateStore: ToastStateStore, goBack: @escaping () -> Void) {
let conversationID = conversationID.unsafelyUnwrapped
switch action {
case .labelAs:
@@ -527,9 +527,9 @@ extension ConversationDetailModel {
bottomBarActions =
try! await dependencies
.bottomBarConversationActionsProvider(mailbox, [conversationID])
.conversationListActionsToolbarActionsProvider(mailbox, [conversationID])
.get()
.visibleBottomBarActions
.visibleListActions
}
}
@@ -553,14 +553,14 @@ extension ConversationDetailModel {
struct Dependencies {
let appContext: AppContext
let bottomBarConversationActionsProvider: ConversationBottomBarActionsProvider
let conversationListActionsToolbarActionsProvider: ConversationListActionsToolbarActionsProvider
init(
appContext: AppContext = .shared,
bottomBarConversationActionsProvider: @escaping ConversationBottomBarActionsProvider = allAvailableBottomBarActionsForConversations
conversationListActionsToolbarActionsProvider: @escaping ConversationListActionsToolbarActionsProvider = allAvailableListActionsForConversations
) {
self.appContext = appContext
self.bottomBarConversationActionsProvider = bottomBarConversationActionsProvider
self.conversationListActionsToolbarActionsProvider = conversationListActionsToolbarActionsProvider
}
}
}
@@ -132,7 +132,7 @@ struct ConversationDetailScreen: View {
goBack: { navigationPath.removeLast() }
)
}) {
action.displayData.icon
action.displayData.image
.foregroundStyle(DS.Color.Icon.weak)
}
.accessibilityIdentifier(MailboxActionBarViewIdentifiers.button(index: index))
@@ -56,7 +56,7 @@ struct MailboxItemsListView<EmptyView: View>: View {
.animation(.none, value: selectionState.hasItems)
.toolbar(selectionState.hasItems ? .visible : .hidden, for: .bottomBar)
.animation(.default, value: selectionState.hasItems)
.mailboxActionBar(
.listActionsToolbar(
state: .initial,
availableActions: .productionInstance,
itemTypeForActionBar: config.itemTypeForActionBar,
@@ -20,16 +20,16 @@ import InboxSnapshotTesting
import InboxTesting
@MainActor
class MailboxActionBarMoreSheetSnapshotTests: BaseTestCase {
class ListActionsToolbarMoreSheetSnapshotTests: BaseTestCase {
func testMailboxActionBarMoreSheetLayoutsCorrectly() {
CustomizeToolbarsFlag.$isVisible.withValue(true) {
let sut = MailboxActionBarMoreSheet(
state: MailboxActionBarMoreSheetPreviewProvider.state(),
let sut = ListActionsToolbarMoreSheet(
state: ListActionsToolbarMoreSheetPreviewProvider.state(),
actionTapped: { _ in },
editToolbarTapped: {}
)
assertSnapshotsOnIPhoneX(of: sut, named: "mailbox_action_bar_more_sheet")
assertSnapshotsOnIPhoneX(of: sut, named: "list_actions_toolbar_more_sheet")
}
}
@@ -21,13 +21,13 @@ import InboxTesting
import proton_app_uniffi
import XCTest
class MailboxActionBarStateStoreTests: BaseTestCase {
class ListActionsToolbarStoreTests: BaseTestCase {
var sut: MailboxActionBarStateStore!
var sut: ListActionsToolbarStore!
var invokedAvailableMessageActionsWithIDs: [[ID]]!
var stubbedAvailableMessageActions: AllBottomBarMessageActions!
var stubbedAvailableMessageActions: AllListActions!
var invokedAvailableConversationActionsWithIDs: [[ID]]!
var stubbedAvailableConversationActions: AllBottomBarMessageActions!
var stubbedAvailableConversationActions: AllListActions!
var starActionPerformerActionsSpy: StarActionPerformerActionsSpy!
var readActionPerformerActionsSpy: ReadActionPerformerActionsSpy!
var deleteActionsSpy: DeleteActionsSpy!
@@ -60,16 +60,16 @@ class MailboxActionBarStateStoreTests: BaseTestCase {
super.tearDown()
}
func testState_WhenMailboxItemsSelectionIsUpdatedInMessageMode_ItReturnsCorrectState() {
func testState_WhenListItemsSelectionIsUpdatedInMessageMode_ItReturnsCorrectState() {
sut = makeSUT(viewMode: .messages)
stubbedAvailableMessageActions = .init(
hiddenBottomBarActions: [.labelAs, .markRead],
visibleBottomBarActions: [.notSpam(.testInbox)]
hiddenListActions: [.labelAs, .markRead],
visibleListActions: [.notSpam(.testInbox)]
)
let ids: [ID] = [.init(value: 11)]
sut.handle(action: .mailboxItemsSelectionUpdated(ids: ids))
sut.handle(action: .listItemsSelectionUpdated(ids: ids))
XCTAssertEqual(invokedAvailableMessageActionsWithIDs.count, 1)
XCTAssertEqual(invokedAvailableConversationActionsWithIDs.count, 0)
@@ -83,15 +83,15 @@ class MailboxActionBarStateStoreTests: BaseTestCase {
))
}
func testState_WhenMailboxItemsSelectionIsUpdatedInConversationModel_ItReturnsCorrectState() {
func testState_WhenListItemsSelectionIsUpdatedInConversationModel_ItReturnsCorrectState() {
sut = makeSUT(viewMode: .conversations)
stubbedAvailableConversationActions = .init(
hiddenBottomBarActions: [.notSpam(.testInbox), .permanentDelete],
visibleBottomBarActions: [.more]
hiddenListActions: [.notSpam(.testInbox), .permanentDelete],
visibleListActions: [.more]
)
let ids: [ID] = [.init(value: 22)]
sut.handle(action: .mailboxItemsSelectionUpdated(ids: ids))
sut.handle(action: .listItemsSelectionUpdated(ids: ids))
XCTAssertEqual(invokedAvailableMessageActionsWithIDs.count, 0)
XCTAssertEqual(invokedAvailableConversationActionsWithIDs.count, 1)
@@ -105,10 +105,10 @@ class MailboxActionBarStateStoreTests: BaseTestCase {
))
}
func testState_WhenMailboxItemsSelectionIsUpdatedWithNoSelection_ItReturnsCorrectState() {
func testState_WhenListItemsSelectionIsUpdatedWithNoSelection_ItReturnsCorrectState() {
sut = makeSUT(viewMode: .messages)
sut.handle(action: .mailboxItemsSelectionUpdated(ids: []))
sut.handle(action: .listItemsSelectionUpdated(ids: []))
XCTAssertEqual(invokedAvailableMessageActionsWithIDs.count, 0)
}
@@ -151,7 +151,7 @@ class MailboxActionBarStateStoreTests: BaseTestCase {
XCTAssertNil(sut.state.moreActionSheetPresented)
sut.handle(action: .mailboxItemsSelectionUpdated(ids: ids))
sut.handle(action: .listItemsSelectionUpdated(ids: ids))
sut.handle(action: .actionSelected(.more, ids: ids))
XCTAssertEqual(
@@ -285,8 +285,8 @@ class MailboxActionBarStateStoreTests: BaseTestCase {
// MARK: - Private
private func makeSUT(viewMode: ViewMode) -> MailboxActionBarStateStore {
MailboxActionBarStateStore(
private func makeSUT(viewMode: ViewMode) -> ListActionsToolbarStore {
ListActionsToolbarStore(
state: .initial,
availableActions: .init(
message: { _, ids in
@@ -4,8 +4,8 @@
import Foundation
import proton_app_uniffi
public extension AllAvailableBottomBarActionsForConversationsResult {
func get() throws(ActionError) -> AllBottomBarMessageActions {
public extension AllAvailableListActionsForConversationsResult {
func get() throws(ActionError) -> AllListActions {
switch self {
case .ok(let value):
value
@@ -14,8 +14,8 @@ public extension AllAvailableBottomBarActionsForConversationsResult {
}
}
}
public extension AllAvailableBottomBarActionsForMessagesResult {
func get() throws(ActionError) -> AllBottomBarMessageActions {
public extension AllAvailableListActionsForMessagesResult {
func get() throws(ActionError) -> AllListActions {
switch self {
case .ok(let value):
value
@@ -161,7 +161,9 @@ struct ContactDetailsScreen: View {
case .urls(let urls):
FormList(collection: urls) { item in
button(item: ContactFormatter.URL.formatted(from: item)) {
store.handle(action: .openURL(urlString: item.url))
if let urlString = item.url.urlString {
store.handle(action: .openURL(urlString: urlString))
}
}
}
case .logos, .photos:
@@ -207,6 +209,19 @@ private extension FormList {
}
private extension VCardUrlValue {
var urlString: String? {
switch self {
case .http(let string):
string
case .notHttp, .text:
nil
}
}
}
#Preview {
ContactDetailsScreen(
contact: ContactItem(
@@ -97,7 +97,7 @@ enum ContactFormatter {
static func formatted(from vcardURL: VCardUrl) -> ContactDetailsItem {
let label = vcardURL.urlType.humanReadable(fallback: L10n.ContactDetails.Label.url.string)
return .init(label: label, value: vcardURL.url, isInteractive: true)
return .init(label: label, value: vcardURL.url.value, isInteractive: true)
}
}
@@ -177,3 +177,14 @@ private extension String {
}
}
private extension VCardUrlValue {
var value: String {
switch self {
case .http(let string), .notHttp(let string), .text(let string):
string
}
}
}
@@ -99,7 +99,7 @@ final class ContactDetailsScreenSnapshotTests {
.languages(["french"]),
.organizations(["CERN", "NASA"]),
.members(["N/A"]),
.urls([.init(url: "https://www.nasa.gov", urlType: [.work])]),
.urls([.init(url: .http("https://www.nasa.gov"), urlType: [.work])]),
]
assertSnapshotsOnIPhoneX(of: makeSUT(items: items))
@@ -171,11 +171,11 @@ struct ContactFormatterTests {
arguments:
zip(
[
VCardUrl(url: "https://swissbank.ch", urlType: [.work]),
VCardUrl(url: "https://voice.example", urlType: [.voice]),
VCardUrl(url: "https://personal.blog", urlType: [.home]),
VCardUrl(url: "https://example.org", urlType: []),
VCardUrl(url: "https://custom.link", urlType: [.string("GitHub")]),
VCardUrl(url: .http("https://swissbank.ch"), urlType: [.work]),
VCardUrl(url: .http("https://voice.example"), urlType: [.voice]),
VCardUrl(url: .http("https://personal.blog"), urlType: [.home]),
VCardUrl(url: .http("https://example.org"), urlType: []),
VCardUrl(url: .http("https://custom.link"), urlType: [.string("GitHub")]),
],
[
ContactDetailsItem(label: "Work", value: "https://swissbank.ch", isInteractive: true),
@@ -44,6 +44,26 @@ public extension ChallengeLoaderGetResult {
}
}
}
public extension ChallengeLoaderPostResult {
func get() throws(ProtonError) -> ChallengeLoaderResponse {
switch self {
case .ok(let value):
value
case .error(let error):
throw error
}
}
}
public extension ChallengeLoaderPutResult {
func get() throws(ProtonError) -> ChallengeLoaderResponse {
switch self {
case .ok(let value):
value
case .error(let error):
throw error
}
}
}
public extension CreateMailSessionResult {
func get() throws(UserSessionError) -> MailSession {
switch self {
@@ -54,6 +74,36 @@ public extension CreateMailSessionResult {
}
}
}
public extension CustomSettingsMobileSignatureResult {
func get() throws(ProtonError) -> MobileSignature {
switch self {
case .ok(let value):
value
case .error(let error):
throw error
}
}
}
public extension CustomSettingsSetMobileSignatureEnabledResult {
func get() throws(ProtonError) {
switch self {
case .ok:
break
case .error(let error):
throw error
}
}
}
public extension CustomSettingsSetMobileSignatureResult {
func get() throws(ProtonError) {
switch self {
case .ok:
break
case .error(let error):
throw error
}
}
}
public extension DraftExpirationTimeResult {
func get() throws(ProtonError) -> DraftExpirationTime {
switch self {
@@ -214,13 +214,13 @@
"one" : {
"stringUnit" : {
"state" : "translated",
"value" : "Il vous reste %lld tentative avant la déconnexion."
"value" : "%lld Tentative restante avant la déconnexion"
}
},
"other" : {
"stringUnit" : {
"state" : "translated",
"value" : "Il vous reste %lld tentatives avant la déconnexion."
"value" : "%lld tentatives restantes avant la déconnexion"
}
}
}
@@ -39,6 +39,24 @@
"state" : "new",
"value" : "Billed at %1$@ every %2$@"
}
},
"es" : {
"stringUnit" : {
"state" : "translated",
"value" : "Facturado a %1$@ cada %2$@"
}
},
"fr" : {
"stringUnit" : {
"state" : "translated",
"value" : "Facturé %1$@ tous les %2$@"
}
},
"ja" : {
"stringUnit" : {
"state" : "translated",
"value" : "毎%2$@に%1$@の支払い"
}
}
}
},
+1 -1
View File
@@ -1 +1 @@
0.125.1
0.130.0