mirror of
https://github.com/ProtonMail/ios-mail.git
synced 2026-06-14 09:54:45 +00:00
refactor(swift-format): Full config
This commit is contained in:
+66
-2
@@ -1,15 +1,79 @@
|
||||
{
|
||||
"fileScopedDeclarationPrivacy": {
|
||||
"accessLevel": "private"
|
||||
},
|
||||
"indentBlankLines": false,
|
||||
"indentConditionalCompilationBlocks": true,
|
||||
"indentSwitchCaseLabels": false,
|
||||
"indentation": {
|
||||
"spaces": 4
|
||||
},
|
||||
"lineBreakAroundMultilineExpressionChainComponents": true,
|
||||
"lineBreakBeforeControlFlowKeywords": false,
|
||||
"lineBreakBeforeEachArgument": false,
|
||||
"lineBreakBeforeEachGenericRequirement": false,
|
||||
"lineBreakBetweenDeclarationAttributes": false,
|
||||
"lineLength": 200,
|
||||
"maximumBlankLines": 1,
|
||||
"multiElementCollectionTrailingCommas": true,
|
||||
"multilineTrailingCommaBehavior": "alwaysUsed",
|
||||
"noAssignmentInExpressions": {
|
||||
"allowedFunctions": [
|
||||
"XCTAssertNoThrow"
|
||||
]
|
||||
},
|
||||
"prioritizeKeepingFunctionOutputTogether": false,
|
||||
"reflowMultilineStringLiterals": {
|
||||
"never": {}
|
||||
},
|
||||
"respectsExistingLineBreaks": true,
|
||||
"rules": {
|
||||
"AllPublicDeclarationsHaveDocumentation": false,
|
||||
"AlwaysUseLiteralForEmptyCollectionInit": true,
|
||||
"AlwaysUseLowerCamelCase": true,
|
||||
"AmbiguousTrailingClosureOverload": true,
|
||||
"AvoidRetroactiveConformances": false,
|
||||
"BeginDocumentationCommentWithOneLineSummary": true,
|
||||
"DoNotUseSemicolons": true,
|
||||
"DontRepeatTypeInStaticProperties": true,
|
||||
"FileScopedDeclarationPrivacy": true,
|
||||
"FullyIndirectEnum": true,
|
||||
"GroupNumericLiterals": true,
|
||||
"IdentifiersMustBeASCII": true,
|
||||
"NeverForceUnwrap": false,
|
||||
"NeverUseForceTry": false,
|
||||
"NeverUseImplicitlyUnwrappedOptionals": false,
|
||||
"NoAccessLevelOnExtensionDeclaration": false,
|
||||
"NoAssignmentInExpressions": true,
|
||||
"NoBlockComments": false,
|
||||
"NoCasesWithOnlyFallthrough": true,
|
||||
"NoEmptyLinesOpeningClosingBraces": true,
|
||||
"NoEmptyTrailingClosureParentheses": true,
|
||||
"NoLabelsInCasePatterns": true,
|
||||
"NoLeadingUnderscores": false,
|
||||
"NoParensAroundConditions": true,
|
||||
"NoPlaygroundLiterals": true,
|
||||
"NoVoidReturnOnFunctionSignature": true,
|
||||
"OmitExplicitReturns": true,
|
||||
"OneCasePerLine": true,
|
||||
"OneVariableDeclarationPerLine": true,
|
||||
"OnlyOneTrailingClosureArgument": true,
|
||||
"OrderedImports": true,
|
||||
"UseTripleSlashForDocumentationComments": true
|
||||
}
|
||||
"ReplaceForEachWithForLoop": true,
|
||||
"ReturnVoidInsteadOfEmptyTuple": true,
|
||||
"TypeNamesShouldBeCapitalized": true,
|
||||
"UseEarlyExits": false,
|
||||
"UseExplicitNilCheckInConditions": true,
|
||||
"UseLetInEveryBoundCaseVariable": true,
|
||||
"UseShorthandTypeNames": true,
|
||||
"UseSingleLinePropertyGetter": true,
|
||||
"UseSynthesizedInitializer": true,
|
||||
"UseTripleSlashForDocumentationComments": true,
|
||||
"UseWhereClausesInForLoops": true,
|
||||
"ValidateDocumentationComments": true
|
||||
},
|
||||
"spacesAroundRangeFormationOperators": false,
|
||||
"spacesBeforeEndOfLineComments": 2,
|
||||
"tabWidth": 8,
|
||||
"version": 1
|
||||
}
|
||||
|
||||
@@ -153,10 +153,8 @@ final class UserNotificationCenterDelegate: NSObject, UNUserNotificationCenterDe
|
||||
}
|
||||
|
||||
private func waitUntilSessionBecomesActive(sessionId: String) async {
|
||||
for await sessionState in sessionStatePublisher.values {
|
||||
if (try? sessionState.userSession?.sessionId().get()) == sessionId {
|
||||
break
|
||||
}
|
||||
for await sessionState in sessionStatePublisher.values where (try? sessionState.userSession?.sessionId().get()) == sessionId {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -23,12 +23,13 @@ enum PreviewData {
|
||||
|
||||
extension LabelUIModel {
|
||||
static func random(num: Int) -> [LabelUIModel] {
|
||||
(0..<num).map { _ in
|
||||
LabelUIModel(
|
||||
labelId: .random(),
|
||||
text: ["a", "b", "c"].randomElement()!,
|
||||
color: [Color.blue, .red, .green].randomElement()!
|
||||
)
|
||||
}
|
||||
(0..<num)
|
||||
.map { _ in
|
||||
LabelUIModel(
|
||||
labelId: .random(),
|
||||
text: ["a", "b", "c"].randomElement()!,
|
||||
color: [Color.blue, .red, .green].randomElement()!
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -89,7 +89,8 @@ final class AppContext: Sendable, ObservableObject {
|
||||
hvNotifier: accountChallengeCoordinator,
|
||||
deviceInfoProvider: ChallengePayloadProvider(),
|
||||
issueReporter: SentryIssueReporter()
|
||||
).get()
|
||||
)
|
||||
.get()
|
||||
|
||||
excludeDirectoriesFromBackup(params: params)
|
||||
|
||||
|
||||
@@ -128,39 +128,40 @@ final class MailboxModel: ObservableObject {
|
||||
|
||||
extension MailboxModel {
|
||||
private func setUpBindings() {
|
||||
appRoute.$route.sink { [weak self] route in
|
||||
guard let self else { return }
|
||||
appRoute.$route
|
||||
.sink { [weak self] route in
|
||||
guard let self else { return }
|
||||
|
||||
switch route {
|
||||
case .mailbox(selectedMailbox: let newSelectedMailbox):
|
||||
guard newSelectedMailbox != selectedMailbox else {
|
||||
return
|
||||
}
|
||||
switch route {
|
||||
case .mailbox(selectedMailbox: let newSelectedMailbox):
|
||||
guard newSelectedMailbox != selectedMailbox else {
|
||||
return
|
||||
}
|
||||
|
||||
Task {
|
||||
self.selectionMode.selectionModifier.exitSelectionMode()
|
||||
self.selectedMailbox = newSelectedMailbox
|
||||
await self.updateMailboxAndScroller()
|
||||
await self.prepareSwipeActions()
|
||||
}
|
||||
case .mailboxOpenMessage(seed: let openedItem):
|
||||
state.isSearchPresented = false
|
||||
replaceCurrentNavigationPath(with: openedItem)
|
||||
case .composer(let fromShareExtension):
|
||||
state.isSearchPresented = false
|
||||
Task {
|
||||
self.selectionMode.selectionModifier.exitSelectionMode()
|
||||
self.selectedMailbox = newSelectedMailbox
|
||||
await self.updateMailboxAndScroller()
|
||||
await self.prepareSwipeActions()
|
||||
}
|
||||
case .mailboxOpenMessage(seed: let openedItem):
|
||||
state.isSearchPresented = false
|
||||
replaceCurrentNavigationPath(with: openedItem)
|
||||
case .composer(let fromShareExtension):
|
||||
state.isSearchPresented = false
|
||||
|
||||
if fromShareExtension {
|
||||
openDraftForShareExtension()
|
||||
} else {
|
||||
createDraft()
|
||||
if fromShareExtension {
|
||||
openDraftForShareExtension()
|
||||
} else {
|
||||
createDraft()
|
||||
}
|
||||
case .mailto(let mailtoURL):
|
||||
createDraft(with: mailtoURL)
|
||||
case .search:
|
||||
state.isSearchPresented = true
|
||||
}
|
||||
case .mailto(let mailtoURL):
|
||||
createDraft(with: mailtoURL)
|
||||
case .search:
|
||||
state.isSearchPresented = true
|
||||
}
|
||||
}
|
||||
.store(in: &cancellables)
|
||||
.store(in: &cancellables)
|
||||
|
||||
Publishers.Merge(
|
||||
mailSettingsLiveQuery.settingHasChanged(keyPath: \.swipeLeft),
|
||||
@@ -312,14 +313,16 @@ extension MailboxModel {
|
||||
callback: MessageScrollerLiveQueryCallbackWrapper { [weak self] update in
|
||||
self?.scrollerUpdates.enqueueUpdate(update)
|
||||
}
|
||||
).get()
|
||||
)
|
||||
.get()
|
||||
} else {
|
||||
conversationScroller = try await scrollConversationsForLabel(
|
||||
mailbox: mailbox,
|
||||
callback: ConversationScrollerLiveQueryCallbackWrapper { [weak self] update in
|
||||
self?.scrollerUpdates.enqueueUpdate(update)
|
||||
}
|
||||
).get()
|
||||
)
|
||||
.get()
|
||||
}
|
||||
paginatedDataSource.fetchInitialPage()
|
||||
|
||||
@@ -406,7 +409,7 @@ extension MailboxModel {
|
||||
case .append(let conversations):
|
||||
let items = await mailboxItems(conversations: conversations)
|
||||
updateType = .append(items: items)
|
||||
case let .replaceRange(from, to, conversations):
|
||||
case .replaceRange(let from, let to, let conversations):
|
||||
let items = await mailboxItems(conversations: conversations)
|
||||
updateType = .replaceRange(from: Int(from), to: Int(to), items: items)
|
||||
completion = { [weak self] in self?.updateSelectedItemsAfterDestructiveUpdate() }
|
||||
@@ -451,7 +454,7 @@ extension MailboxModel {
|
||||
case .append(let messages):
|
||||
let items = await mailboxItems(messages: messages)
|
||||
updateType = .append(items: items)
|
||||
case let .replaceRange(from, to, messages):
|
||||
case .replaceRange(let from, let to, let messages):
|
||||
let items = await mailboxItems(messages: messages)
|
||||
updateType = .replaceRange(from: Int(from), to: Int(to), items: items)
|
||||
completion = { [weak self] in self?.updateSelectedItemsAfterDestructiveUpdate() }
|
||||
@@ -547,7 +550,8 @@ extension MailboxModel {
|
||||
showLocation: showLocation
|
||||
)
|
||||
}
|
||||
}.value
|
||||
}
|
||||
.value
|
||||
}
|
||||
|
||||
private func mailboxItems(conversations: [Conversation]) async -> [MailboxItemCellUIModel] {
|
||||
@@ -558,7 +562,8 @@ extension MailboxModel {
|
||||
conversations.map { conversation in
|
||||
conversation.toMailboxItemCellUIModel(selectedIds: selectedIds, showLocation: showLocation)
|
||||
}
|
||||
}.value
|
||||
}
|
||||
.value
|
||||
}
|
||||
}
|
||||
|
||||
@@ -690,11 +695,12 @@ extension MailboxModel {
|
||||
|
||||
func onMailboxItemAction(_ context: SwipeActionContext, toastStateStore: ToastStateStore) {
|
||||
guard let mailbox,
|
||||
let output = swipeActionsHandler?.handle(
|
||||
context,
|
||||
toastStateStore: toastStateStore,
|
||||
viewMode: mailbox.viewMode()
|
||||
)
|
||||
let output = swipeActionsHandler?
|
||||
.handle(
|
||||
context,
|
||||
toastStateStore: toastStateStore,
|
||||
viewMode: mailbox.viewMode()
|
||||
)
|
||||
else { return }
|
||||
switch output.sheetType {
|
||||
case .labelAs:
|
||||
|
||||
@@ -92,7 +92,8 @@ struct ConversationActionsMenu<OpenMenuButtonContent: View>: View {
|
||||
actions = try await allAvailableConversationActionsForActionSheet(
|
||||
mailbox: mailbox,
|
||||
conversationId: conversationID
|
||||
).get()
|
||||
)
|
||||
.get()
|
||||
} catch {
|
||||
AppLogger.log(error: error, category: .conversationDetail)
|
||||
}
|
||||
|
||||
@@ -28,6 +28,6 @@ extension DraftProvider {
|
||||
}
|
||||
|
||||
static var dummy: Self {
|
||||
.init(makeDraft: { _, _ in return NewDraftResult.ok(.init(noPointer: .init())) })
|
||||
.init(makeDraft: { _, _ in NewDraftResult.ok(.init(noPointer: .init())) })
|
||||
}
|
||||
}
|
||||
|
||||
@@ -43,7 +43,8 @@ struct LabelAsActionPerformer {
|
||||
input.selectedLabelsIDs,
|
||||
input.partiallySelectedLabelsIDs,
|
||||
input.archive
|
||||
).get()
|
||||
)
|
||||
.get()
|
||||
|
||||
return output
|
||||
}
|
||||
|
||||
@@ -29,7 +29,7 @@ final class SendResultPresenter {
|
||||
private typealias MessageID = ID
|
||||
private let regularDuration: Toast.Duration = .short
|
||||
private let extendedDuration: TimeInterval = 3.0
|
||||
private var toasts = [MessageID: Toast]()
|
||||
private var toasts: [MessageID: Toast] = [:]
|
||||
private let subject = PassthroughSubject<SendResultToastAction, Never>()
|
||||
private let draftPresenter: DraftPresenter
|
||||
|
||||
|
||||
@@ -84,10 +84,12 @@ class SnoozeStore: StateStore {
|
||||
|
||||
private func loadSnoozeData() async {
|
||||
do {
|
||||
let snoozeActions = try await snoozeService.availableSnoozeActions(
|
||||
for: state.conversationIDs,
|
||||
systemCalendarWeekStart: DateEnvironment.calendar.nonDefaultWeekStart
|
||||
).get()
|
||||
let snoozeActions =
|
||||
try await snoozeService.availableSnoozeActions(
|
||||
for: state.conversationIDs,
|
||||
systemCalendarWeekStart: DateEnvironment.calendar.nonDefaultWeekStart
|
||||
)
|
||||
.get()
|
||||
|
||||
state = state.copy(\.snoozeActions, to: snoozeActions)
|
||||
} catch {
|
||||
@@ -98,11 +100,13 @@ class SnoozeStore: StateStore {
|
||||
|
||||
private func snoozeConversations(snoozeTime: UnixTimestamp) async {
|
||||
do {
|
||||
_ = try await snoozeService.snooze(
|
||||
conversation: state.conversationIDs,
|
||||
labelId: state.labelId,
|
||||
timestamp: snoozeTime
|
||||
).get()
|
||||
_ =
|
||||
try await snoozeService.snooze(
|
||||
conversation: state.conversationIDs,
|
||||
labelId: state.labelId,
|
||||
timestamp: snoozeTime
|
||||
)
|
||||
.get()
|
||||
toastStateStore.present(toast: .snooze(snoozeDate: snoozeTime.date))
|
||||
dismiss()
|
||||
} catch {
|
||||
@@ -113,10 +117,12 @@ class SnoozeStore: StateStore {
|
||||
|
||||
private func unsnoozeConversations() async {
|
||||
do {
|
||||
_ = try await snoozeService.unsnooze(
|
||||
conversation: state.conversationIDs,
|
||||
labelId: state.labelId
|
||||
).get()
|
||||
_ =
|
||||
try await snoozeService.unsnooze(
|
||||
conversation: state.conversationIDs,
|
||||
labelId: state.labelId
|
||||
)
|
||||
.get()
|
||||
toastStateStore.present(toast: .unsnooze)
|
||||
dismiss()
|
||||
} catch {
|
||||
|
||||
@@ -37,7 +37,7 @@ struct UndoScheduleSendProvider {
|
||||
}
|
||||
|
||||
static func mockInstance(
|
||||
stubbedResult: DraftCancelScheduleSendResult = .ok(.init(lastScheduledTime: 1747728129))
|
||||
stubbedResult: DraftCancelScheduleSendResult = .ok(.init(lastScheduledTime: 1_747_728_129))
|
||||
) -> UndoScheduleSendProvider {
|
||||
.init(undoScheduleSend: { _ in stubbedResult })
|
||||
}
|
||||
|
||||
+2
-1
@@ -81,7 +81,8 @@ struct AppProtectionSelectionScreen: View {
|
||||
) {
|
||||
store.handle(action: .autoLockTapped)
|
||||
}
|
||||
}.animation(.easeInOut, value: state.shouldShowAutoLockButton)
|
||||
}
|
||||
.animation(.easeInOut, value: state.shouldShowAutoLockButton)
|
||||
}
|
||||
Spacer()
|
||||
}
|
||||
|
||||
+2
-1
@@ -52,7 +52,8 @@ struct CustomizeToolbarsScreen: View {
|
||||
}
|
||||
.padding(.horizontal, DS.Spacing.large)
|
||||
.padding(.bottom, DS.Spacing.extraLarge)
|
||||
}.onAppear {
|
||||
}
|
||||
.onAppear {
|
||||
store.handle(action: .onAppear)
|
||||
}
|
||||
.onChange(
|
||||
|
||||
+4
-3
@@ -67,9 +67,10 @@ class EditToolbarStore: StateStore {
|
||||
to: .init(selected: selectedList, unselected: unselectedList))
|
||||
case .onLoad:
|
||||
do {
|
||||
let actions = try await customizeToolbarRepository.fetchActions()[
|
||||
keyPath: state.toolbarType.actionsKeyPath
|
||||
]
|
||||
let actions =
|
||||
try await customizeToolbarRepository.fetchActions()[
|
||||
keyPath: state.toolbarType.actionsKeyPath
|
||||
]
|
||||
state = state.copy(\.toolbarActions, to: actions)
|
||||
} catch {
|
||||
AppLogger.log(error: error, category: .customizeToolbar)
|
||||
|
||||
@@ -35,7 +35,8 @@ struct PINRouterView: View {
|
||||
view(route: route)
|
||||
.navigationBarBackButtonHidden()
|
||||
}
|
||||
}.environmentObject(router)
|
||||
}
|
||||
.environmentObject(router)
|
||||
}
|
||||
|
||||
private var navigationPath: Binding<[PINRoute]> {
|
||||
|
||||
@@ -95,25 +95,25 @@ final class MessageBodyStateStore: StateStore {
|
||||
case .onLoad:
|
||||
await loadMessageBody(with: .init())
|
||||
case .refreshBanners:
|
||||
if case let .loaded(body, _) = state.body {
|
||||
if case .loaded(let body, _) = state.body {
|
||||
await loadMessageBody(with: body.html.options)
|
||||
}
|
||||
case .displayEmbeddedImages:
|
||||
if case let .loaded(body, _) = state.body {
|
||||
if case .loaded(let body, _) = state.body {
|
||||
let updatedOptions = body.html.options
|
||||
.copy(\.hideEmbeddedImages, to: false)
|
||||
|
||||
await loadMessageBody(with: updatedOptions)
|
||||
}
|
||||
case .downloadRemoteContent:
|
||||
if case let .loaded(body, _) = state.body {
|
||||
if case .loaded(let body, _) = state.body {
|
||||
let updatedOptions = body.html.options
|
||||
.copy(\.hideRemoteImages, to: false)
|
||||
|
||||
await loadMessageBody(with: updatedOptions)
|
||||
}
|
||||
case .reloadFailedProxyImages:
|
||||
if case let .loaded(body, _) = state.body {
|
||||
if case .loaded(let body, _) = state.body {
|
||||
var newBanners = state.eventBanners
|
||||
newBanners.remove(.proxyImageLoadFail)
|
||||
state =
|
||||
@@ -131,11 +131,11 @@ final class MessageBodyStateStore: StateStore {
|
||||
case .markAsLegitimateConfirmed(let action):
|
||||
state = state.copy(\.alert, to: nil)
|
||||
|
||||
if case let .loaded(body, _) = state.body, case .markAsLegitimate = action {
|
||||
if case .loaded(let body, _) = state.body, case .markAsLegitimate = action {
|
||||
await markAsLegitimate(with: body.html.options)
|
||||
}
|
||||
case .unblockSender(let emailAddress):
|
||||
if case let .loaded(body, _) = state.body {
|
||||
if case .loaded(let body, _) = state.body {
|
||||
await unblockSender(emailAddress: emailAddress, with: body.html.options)
|
||||
}
|
||||
case .unsubscribeNewsletter:
|
||||
@@ -146,7 +146,7 @@ final class MessageBodyStateStore: StateStore {
|
||||
case .unsubscribeNewsletterConfirmed(let action):
|
||||
state = state.copy(\.alert, to: nil)
|
||||
|
||||
if case let .loaded(body, _) = state.body, case .unsubscribe = action {
|
||||
if case .loaded(let body, _) = state.body, case .unsubscribe = action {
|
||||
await unsubscribeNewsletter(with: body.newsletterService, options: body.html.options)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -173,5 +173,6 @@ enum ExpandedMessageCellEvent {
|
||||
onEvent: { _ in },
|
||||
htmlDisplayed: {}
|
||||
)
|
||||
}.environmentObject(ToastStateStore(initialState: .initial))
|
||||
}
|
||||
.environmentObject(ToastStateStore(initialState: .initial))
|
||||
}
|
||||
|
||||
+1
-1
@@ -139,7 +139,7 @@ struct MessageBodyAttachmentsView: View {
|
||||
private extension Array where Element == AttachmentDisplayModel {
|
||||
var totalSize: Int64 {
|
||||
reduce(0) { result, next in
|
||||
return result + Int64(next.size)
|
||||
result + Int64(next.size)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+1
-1
@@ -542,7 +542,7 @@ enum MessageDetailsPreviewProvider {
|
||||
recipientsTo: recipientsTo,
|
||||
recipientsCc: recipientsCc,
|
||||
recipientsBcc: recipientsBcc,
|
||||
date: Date(timeIntervalSince1970: 1724347300),
|
||||
date: Date(timeIntervalSince1970: 1_724_347_300),
|
||||
location: location?.model,
|
||||
labels: labels,
|
||||
isStarred: false,
|
||||
|
||||
@@ -420,12 +420,13 @@ final class ConversationDetailModel: Sendable, ObservableObject {
|
||||
let alert: AlertModel = .deleteConfirmation(
|
||||
itemsCount: 1,
|
||||
action: { [weak self] action in
|
||||
await self?.handle(
|
||||
id: conversationID,
|
||||
mailboxItem: .conversation,
|
||||
action: action,
|
||||
toastStateStore: toastStateStore, goBack: goBack
|
||||
)
|
||||
await self?
|
||||
.handle(
|
||||
id: conversationID,
|
||||
mailboxItem: .conversation,
|
||||
action: action,
|
||||
toastStateStore: toastStateStore, goBack: goBack
|
||||
)
|
||||
}
|
||||
)
|
||||
actionAlert = alert
|
||||
@@ -758,7 +759,8 @@ extension ConversationDetailModel {
|
||||
mailbox: mailbox,
|
||||
id: conversationID,
|
||||
showAll: showAllMessages
|
||||
).get()
|
||||
)
|
||||
.get()
|
||||
let hiddenMessagesBanner = conversationAndMessages?.conversation.hiddenMessagesBanner
|
||||
let isStarred = conversationAndMessages?.conversation.isStarred ?? false
|
||||
let messages = conversationAndMessages?.messages ?? []
|
||||
@@ -846,7 +848,8 @@ extension ConversationDetailModel {
|
||||
let actions = try await allAvailableConversationActionsForConversation(
|
||||
mailbox: mailbox,
|
||||
conversationId: conversationItem.id
|
||||
).get()
|
||||
)
|
||||
.get()
|
||||
self.conversationToolbarActions = .conversation(actions: actions, conversationID: conversationItem.id)
|
||||
} catch {
|
||||
AppLogger.log(error: error, category: .conversationDetail)
|
||||
@@ -861,7 +864,8 @@ extension ConversationDetailModel {
|
||||
mailbox: mailbox,
|
||||
theme: theme,
|
||||
messageId: conversationItem.id
|
||||
).get()
|
||||
)
|
||||
.get()
|
||||
self.conversationToolbarActions = .message(actions: actions, messageID: conversationItem.id)
|
||||
} catch {
|
||||
AppLogger.log(error: error, category: .conversationDetail)
|
||||
|
||||
+2
-1
@@ -44,7 +44,8 @@ private extension MailboxItemCellUIModel {
|
||||
AttachmentCapsuleUIModel(id: .init(value: 1), icon: DS.Icon.icFileTypeIconPdf, name: "#34JE3KLP.pdf"),
|
||||
AttachmentCapsuleUIModel(id: .init(value: 2), icon: DS.Icon.icFileTypeIconWord, name: "meeting_minutes.doc"),
|
||||
AttachmentCapsuleUIModel(id: .init(value: 1), icon: DS.Icon.icFileTypeIconExcel, name: "ARR_Q2.xls"),
|
||||
].randomElement()!
|
||||
]
|
||||
.randomElement()!
|
||||
]
|
||||
} else {
|
||||
[]
|
||||
|
||||
@@ -244,7 +244,7 @@ final class SearchModel: ObservableObject {
|
||||
case .append(let messages):
|
||||
let items = await mailboxItems(messages: messages)
|
||||
updateType = .append(items: items)
|
||||
case let .replaceRange(from, to, messages):
|
||||
case .replaceRange(let from, let to, let messages):
|
||||
let items = await mailboxItems(messages: messages)
|
||||
updateType = .replaceRange(from: Int(from), to: Int(to), items: items)
|
||||
completion = { [weak self] in self?.updateSelectedItemsAfterDestructiveUpdate() }
|
||||
@@ -274,7 +274,8 @@ final class SearchModel: ObservableObject {
|
||||
showLocation: true
|
||||
)
|
||||
}
|
||||
}.value
|
||||
}
|
||||
.value
|
||||
}
|
||||
|
||||
func prepareSwipeActions() async {
|
||||
|
||||
@@ -116,7 +116,8 @@ struct SidebarScreen: View {
|
||||
.padding(.vertical, DS.Spacing.small)
|
||||
.onTapGesture(count: 5) { screenModel.handle(action: .logoTappedFiveTimes) }
|
||||
separator
|
||||
}.background(
|
||||
}
|
||||
.background(
|
||||
GeometryReader { geometry in
|
||||
BlurredBackground(fallbackBackgroundColor: DS.Color.Sidebar.background)
|
||||
.edgesIgnoringSafeArea(.all)
|
||||
@@ -225,11 +226,13 @@ struct SidebarScreen: View {
|
||||
.padding(.vertical, DS.Spacing.medium)
|
||||
separator
|
||||
appVersionNote
|
||||
}.onChange(of: appUIStateStore.sidebarState.isOpen) { _, isSidebarOpen in
|
||||
}
|
||||
.onChange(of: appUIStateStore.sidebarState.isOpen) { _, isSidebarOpen in
|
||||
if isSidebarOpen, let first = screenModel.state.items.first {
|
||||
proxy.scrollTo(first.id, anchor: .zero)
|
||||
}
|
||||
}.accessibilityElement(children: .contain)
|
||||
}
|
||||
.accessibilityElement(children: .contain)
|
||||
}
|
||||
.scrollDisabled(gestureState.lockedAxis == .horizontal || lastCommittedAxis == .horizontal)
|
||||
.frame(maxWidth: .infinity)
|
||||
|
||||
@@ -160,7 +160,7 @@ private struct AttachmentCapsuleStyle: ButtonStyle {
|
||||
}
|
||||
}
|
||||
|
||||
fileprivate enum Layout {
|
||||
private enum Layout {
|
||||
static let spacingBetweenCapsules = DS.Spacing.tiny
|
||||
static let extraAttachmentsViewWidth = 42.0
|
||||
static let capsuleHPadding = DS.Spacing.standard
|
||||
|
||||
@@ -39,7 +39,8 @@ struct AvatarCheckboxView: View {
|
||||
.padding(10)
|
||||
.accessibilityIdentifier(AvatarCheckboxViewIdentifiers.avatarChecked)
|
||||
}
|
||||
}.accessibilityElement(children: .contain)
|
||||
}
|
||||
.accessibilityElement(children: .contain)
|
||||
} else {
|
||||
AvatarView(avatar: avatar)
|
||||
}
|
||||
|
||||
@@ -279,12 +279,12 @@ private extension AssignedSwipeAction {
|
||||
|
||||
private extension AssignedSwipeAction {
|
||||
func isDestructive(locationSystemLabel: SystemLabel?, itemSystemLabel: SystemLabel?) -> Bool {
|
||||
guard case let .moveTo(location) = self else {
|
||||
guard case .moveTo(let location) = self else {
|
||||
return false
|
||||
}
|
||||
|
||||
switch location {
|
||||
case let .moveToSystemLabel(targetSystemLabel, _):
|
||||
case .moveToSystemLabel(let targetSystemLabel, _):
|
||||
switch locationSystemLabel {
|
||||
case .allMail, .allSent, .allDrafts:
|
||||
return false
|
||||
|
||||
@@ -51,7 +51,8 @@ struct OneLineLabelsListView: View {
|
||||
}
|
||||
}
|
||||
}
|
||||
}.frame(height: height)
|
||||
}
|
||||
.frame(height: height)
|
||||
}
|
||||
|
||||
// MARK: - Private
|
||||
@@ -99,5 +100,6 @@ struct OneLineLabelsListView: View {
|
||||
OneLineLabelsListView(labels: labels)
|
||||
}
|
||||
Spacer()
|
||||
}.padding()
|
||||
}
|
||||
.padding()
|
||||
}
|
||||
|
||||
+2
-1
@@ -25,7 +25,8 @@ enum OneLineLabelsListViewPreviewDataProvider {
|
||||
["😈"],
|
||||
["Long long long long long long long long long long long long long long"],
|
||||
["Aaaaaaaa", "Long long label long long long long long", "aaaaaaaaaaaaa"],
|
||||
].map { $0.map(LabelUIModel.testData) }
|
||||
]
|
||||
.map { $0.map(LabelUIModel.testData) }
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -57,13 +57,13 @@ final class PaginatedListDataSource<Item: Equatable>: ObservableObject {
|
||||
switch update.value {
|
||||
case .append(let items):
|
||||
newState.items.append(contentsOf: items)
|
||||
case let .replaceRange(from, to, items):
|
||||
case .replaceRange(let from, let to, let items):
|
||||
guard isSafeIndex(from), isSafeIndex(to) else { break }
|
||||
newState.items.replaceSubrange(from..<to, with: items)
|
||||
case let .replaceFrom(index, items):
|
||||
case .replaceFrom(let index, let items):
|
||||
guard isSafeIndex(index) else { break }
|
||||
newState.items.replaceSubrange(index..<newState.items.endIndex, with: items)
|
||||
case let .replaceBefore(index, items):
|
||||
case .replaceBefore(let index, let items):
|
||||
guard isSafeIndex(index) else { break }
|
||||
newState.items.replaceSubrange(newState.items.startIndex..<index, with: items)
|
||||
case .none, .error:
|
||||
|
||||
+5
-3
@@ -94,9 +94,11 @@ class BackgroundTransitionActionsExecutor: ApplicationServiceDidEnterBackground,
|
||||
Self.log("Internet connection on start: \(hasAccessToInternetOnStart == true ? "Online" : "Offline")")
|
||||
|
||||
do {
|
||||
backgroundExecutionHandle = try backgroundTaskExecutorProvider().startBackgroundExecution(
|
||||
callback: callback
|
||||
).get()
|
||||
backgroundExecutionHandle = try backgroundTaskExecutorProvider()
|
||||
.startBackgroundExecution(
|
||||
callback: callback
|
||||
)
|
||||
.get()
|
||||
Self.log("Handle is returned, background actions in progress")
|
||||
Self.log("Handle present: \(self.backgroundExecutionHandle != nil)?")
|
||||
} catch {
|
||||
|
||||
@@ -21,8 +21,8 @@ import SwiftUI
|
||||
/// Keeps `maxElements` in memory. When the limit is reached evicts from the cache the oldest element
|
||||
final class MemoryCache<Key: Hashable & Sendable, Value: Sendable>: @unchecked Sendable {
|
||||
private let maxElements: Int
|
||||
private var dictionary = [Key: Value]()
|
||||
private var fifoQueue = [Key]()
|
||||
private var dictionary: [Key: Value] = [:]
|
||||
private var fifoQueue: [Key] = []
|
||||
|
||||
private let queue = DispatchQueue(label: "\(Bundle.defaultIdentifier).MemoryCache", attributes: .concurrent)
|
||||
|
||||
|
||||
@@ -26,7 +26,8 @@ extension UIApplication {
|
||||
// Keep only the first `UIWindowScene`
|
||||
.first(where: { $0 is UIWindowScene })
|
||||
// Get its associated windows
|
||||
.flatMap({ $0 as? UIWindowScene })?.windows
|
||||
.flatMap({ $0 as? UIWindowScene })?
|
||||
.windows
|
||||
// Finally, keep only the key window
|
||||
.first(where: \.isKeyWindow)
|
||||
}
|
||||
|
||||
@@ -18,6 +18,6 @@
|
||||
extension UInt64 {
|
||||
// Timestamp 1753883097 = 2025-05-31 01:24:57 UTC
|
||||
static var timestamp: UInt64 {
|
||||
1753883097
|
||||
1_753_883_097
|
||||
}
|
||||
}
|
||||
|
||||
@@ -34,7 +34,8 @@ final class MoveToSheetSnapshotTests {
|
||||
moveToActions: .dummy,
|
||||
navigation: { _ in },
|
||||
mailUserSession: .dummy
|
||||
).environmentObject(ToastStateStore(initialState: .initial))
|
||||
)
|
||||
.environmentObject(ToastStateStore(initialState: .initial))
|
||||
assertSnapshotsOnIPhoneX(of: sut, named: "move_to_sheet")
|
||||
}
|
||||
}
|
||||
|
||||
+5
-5
@@ -59,7 +59,7 @@ private extension MailboxItemCellUIModel {
|
||||
}
|
||||
|
||||
static func makeSimpleMessage(type: SimpleMessageType) -> MailboxItemCellUIModel {
|
||||
let snoozeTime = Date(timeIntervalSince1970: 1878451200)
|
||||
let snoozeTime = Date(timeIntervalSince1970: 1_878_451_200)
|
||||
let expirationTime = Date(timeIntervalSince1970: Date.now.timeIntervalSince1970 + 3600 * 24 * 365 * 2)
|
||||
return MailboxItemCellUIModel(
|
||||
id: .random(),
|
||||
@@ -71,7 +71,7 @@ private extension MailboxItemCellUIModel {
|
||||
),
|
||||
emails: "arya.lindt@example.com",
|
||||
subject: "Making the most of Safari",
|
||||
date: Date(timeIntervalSince1970: 1717485341),
|
||||
date: Date(timeIntervalSince1970: 1_717_485_341),
|
||||
location: nil,
|
||||
locationIcon: type == .locationIcon ? DS.Icon.icInbox.image : nil,
|
||||
isRead: false,
|
||||
@@ -103,7 +103,7 @@ private extension MailboxItemCellUIModel {
|
||||
),
|
||||
emails: "Travel",
|
||||
subject: "Your booking confirmation KL877N",
|
||||
date: Date(timeIntervalSince1970: 1717483827),
|
||||
date: Date(timeIntervalSince1970: 1_717_483_827),
|
||||
location: nil,
|
||||
locationIcon: nil,
|
||||
isRead: true,
|
||||
@@ -141,7 +141,7 @@ private extension MailboxItemCellUIModel {
|
||||
),
|
||||
emails: "Flights to Palo Alto - 20th of September, 2025",
|
||||
subject: "You're invited to flight KCY877N",
|
||||
date: Date(timeIntervalSince1970: 1717484927),
|
||||
date: Date(timeIntervalSince1970: 1_717_484_927),
|
||||
location: nil,
|
||||
locationIcon: nil,
|
||||
isRead: true,
|
||||
@@ -173,7 +173,7 @@ private extension MailboxItemCellUIModel {
|
||||
),
|
||||
emails: "Jane Doe, Mike, Laureen Smith",
|
||||
subject: "Photos from Portugal",
|
||||
date: Date(timeIntervalSince1970: 1717484830),
|
||||
date: Date(timeIntervalSince1970: 1_717_484_830),
|
||||
location: nil,
|
||||
locationIcon: nil,
|
||||
isRead: true,
|
||||
|
||||
@@ -92,7 +92,7 @@ private extension SnoozeActions {
|
||||
}
|
||||
|
||||
private extension SnoozeTime {
|
||||
private static let timestamp: UInt64 = 1752697012
|
||||
private static let timestamp: UInt64 = 1_752_697_012
|
||||
|
||||
static var tomorrow: Self {
|
||||
.tomorrow(timestamp)
|
||||
|
||||
@@ -24,11 +24,12 @@ import UIKit
|
||||
final class AppDelegateTests {
|
||||
@Test
|
||||
func testSceneConfiguration_WhenConnectingSceneSession_HasCustomSceneDelegateConfigured() throws {
|
||||
let sceneConfiguration = AppDelegate().application(
|
||||
.shared,
|
||||
configurationForConnecting: try scene(),
|
||||
options: try connectionOptions()
|
||||
)
|
||||
let sceneConfiguration = AppDelegate()
|
||||
.application(
|
||||
.shared,
|
||||
configurationForConnecting: try scene(),
|
||||
options: try connectionOptions()
|
||||
)
|
||||
|
||||
#expect(sceneConfiguration.delegateClass === SceneDelegate.self)
|
||||
}
|
||||
|
||||
@@ -67,7 +67,7 @@ final class SceneDelegateTests: BaseTestCase {
|
||||
let overlayWindow = try XCTUnwrap(sut.overlayWindow)
|
||||
|
||||
XCTAssert(overlayWindow is PassThroughWindow)
|
||||
XCTAssert(overlayWindow.rootViewController is UIHostingController<ModifiedContent<ToastSceneView, _EnvironmentKeyWritingModifier<Optional<ToastStateStore>>>>)
|
||||
XCTAssert(overlayWindow.rootViewController is UIHostingController<ModifiedContent<ToastSceneView, _EnvironmentKeyWritingModifier<ToastStateStore?>>>)
|
||||
XCTAssertEqual(overlayWindow.rootViewController?.view.backgroundColor, .clear)
|
||||
XCTAssertFalse(overlayWindow.isHidden)
|
||||
}
|
||||
|
||||
+4
-2
File diff suppressed because one or more lines are too long
@@ -74,7 +74,8 @@ private extension MainKeyUnlockerTests {
|
||||
.init(
|
||||
base64Encoded:
|
||||
"dAcGOBeHqCMJvQPyOOy303bveHdY+QmCt8RpD6xX8u6+7PLF3pnUXhn91fIb2UND5P7Se8wKkKboY9a9ayFOJMm9uviXe6jCnT9C9Mh8rT3Bn04ctKPIg1YwZXCQwz80kQ/y/tW8wWACS4xRJ70v2MG5nh9jCGsi2nZ3PfFuX4545dfK0H6K0IpdwYYaZqWT6WJrGr6x+QGkJZZc6qfzLvZ7O7lmenuzc2u/fS7+fRouUROQW/2O7bo="
|
||||
).unsafelyUnwrapped
|
||||
)
|
||||
.unsafelyUnwrapped
|
||||
}
|
||||
|
||||
var pinProtectionSalt: Data {
|
||||
|
||||
@@ -280,7 +280,7 @@ extension DraftPresenterTests {
|
||||
func makeSUT(
|
||||
stubbedNewDraftResult: NewDraftResult = .ok(.dummyDraft),
|
||||
stubbedUndoSendError: DraftUndoSendError? = nil,
|
||||
stubbedCancelScheduleResult: DraftCancelScheduleSendResult = .ok(.init(lastScheduledTime: 1747728129))
|
||||
stubbedCancelScheduleResult: DraftCancelScheduleSendResult = .ok(.init(lastScheduledTime: 1_747_728_129))
|
||||
) -> DraftPresenter {
|
||||
DraftPresenter(
|
||||
userSession: MailUserSessionSpy(id: ""),
|
||||
|
||||
@@ -344,10 +344,8 @@ private extension Array where Element == SendResultToastAction {
|
||||
func isSame(as other: [SendResultToastAction]) -> Bool {
|
||||
guard self.count == other.count else { return false }
|
||||
|
||||
for (action1, action2) in zip(self, other) {
|
||||
if !action1.isSame(as: action2) {
|
||||
return false
|
||||
}
|
||||
for (action1, action2) in zip(self, other) where !action1.isSame(as: action2) {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
@@ -105,7 +105,8 @@ final class SidebarModelTests {
|
||||
func test_WhenCustomFolderIsExpandedAndCollapsed_ItExpandsAndCollapsesTheFolder() throws {
|
||||
let parentFolder = try XCTUnwrap(
|
||||
sidebarSpy.stubbedCustomFolders.first(where: { !$0.children.isEmpty })
|
||||
).sidebarFolder
|
||||
)
|
||||
.sidebarFolder
|
||||
|
||||
XCTAssertEqual(sidebarSpy.expandFolderInvoked, [])
|
||||
XCTAssertEqual(sidebarSpy.collapseFolderInvoked, [])
|
||||
@@ -144,6 +145,7 @@ extension SidebarState {
|
||||
func find(folderWithName name: String, in folders: [SidebarFolder]) -> SidebarFolder? {
|
||||
folders.compactMap { folder in
|
||||
folder.name == name ? folder : find(folderWithName: name, in: folder.childFolders)
|
||||
}.first
|
||||
}
|
||||
.first
|
||||
}
|
||||
}
|
||||
|
||||
+1
-1
@@ -21,7 +21,7 @@ import Testing
|
||||
|
||||
struct MessageExpiryTimeFormatterTests {
|
||||
enum Timestamp: Int {
|
||||
case _2025_02_22_15_30_00 = 1740238200
|
||||
case _2025_02_22_15_30_00 = 1_740_238_200
|
||||
}
|
||||
|
||||
@Test(
|
||||
|
||||
@@ -35,7 +35,7 @@ final class ConversationDetailHeaderMainTests: PMUIMockedNetworkTestCase {
|
||||
index: 0,
|
||||
senderName: "Not Proton",
|
||||
senderAddress: "no-reply@not.proton.black",
|
||||
timestamp: 1716199297,
|
||||
timestamp: 1_716_199_297,
|
||||
toRecipients: [UITestHeaderRecipientEntry(index: 0, name: "youngbee@proton.black", address: "youngbee@proton.black")]
|
||||
)
|
||||
|
||||
|
||||
+3
-3
@@ -58,7 +58,7 @@ final class ConversationDetailHeaderMultipleFieldsTests: PMUIMockedNetworkTestCa
|
||||
index: 0,
|
||||
senderName: "Test Free Account",
|
||||
senderAddress: "notsofree@proton.black",
|
||||
timestamp: 1718884640,
|
||||
timestamp: 1_718_884_640,
|
||||
toRecipients: [
|
||||
UITestHeaderRecipientEntry(index: 0, name: "plus@proton.black", address: "plus@proton.black")
|
||||
],
|
||||
@@ -71,7 +71,7 @@ final class ConversationDetailHeaderMultipleFieldsTests: PMUIMockedNetworkTestCa
|
||||
index: 1,
|
||||
senderName: "Young Bee",
|
||||
senderAddress: "youngbee@proton.black",
|
||||
timestamp: 1718885017,
|
||||
timestamp: 1_718_885_017,
|
||||
toRecipients: [
|
||||
UITestHeaderRecipientEntry(index: 0, name: "Test Free Account", address: "notsofree@proton.black")
|
||||
],
|
||||
@@ -84,7 +84,7 @@ final class ConversationDetailHeaderMultipleFieldsTests: PMUIMockedNetworkTestCa
|
||||
index: 2,
|
||||
senderName: "Young Bee",
|
||||
senderAddress: "youngbee@proton.black",
|
||||
timestamp: 1718976443,
|
||||
timestamp: 1_718_976_443,
|
||||
toRecipients: [
|
||||
UITestHeaderRecipientEntry(index: 0, name: "Test Free Account", address: "notsofree@proton.black"),
|
||||
UITestHeaderRecipientEntry(index: 1, name: "notsofree+1@proton.black", address: "notsofree+1@proton.black"),
|
||||
|
||||
@@ -33,14 +33,17 @@ final class MockServer: Sendable {
|
||||
value: 1
|
||||
)
|
||||
.childChannelInitializer { channel in
|
||||
channel.pipeline.configureHTTPServerPipeline(
|
||||
withPipeliningAssistance: false,
|
||||
withErrorHandling: false
|
||||
).flatMap { _ in
|
||||
channel.pipeline.addHandler(BackPressureHandler()).flatMap { item in
|
||||
channel.pipeline.addHandler(self.requestsHandler)
|
||||
channel.pipeline
|
||||
.configureHTTPServerPipeline(
|
||||
withPipeliningAssistance: false,
|
||||
withErrorHandling: false
|
||||
)
|
||||
.flatMap { _ in
|
||||
channel.pipeline.addHandler(BackPressureHandler())
|
||||
.flatMap { item in
|
||||
channel.pipeline.addHandler(self.requestsHandler)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.childChannelOption(ChannelOptions.socket(IPPROTO_TCP, TCP_NODELAY), value: 1)
|
||||
.childChannelOption(
|
||||
|
||||
@@ -121,11 +121,13 @@ extension RequestsHandler {
|
||||
promise: nil
|
||||
)
|
||||
|
||||
context.channel.writeAndFlush(
|
||||
self.wrapOutboundOut(HTTPServerResponsePart.end(nil))
|
||||
).whenComplete { _ in
|
||||
/* no op */
|
||||
}
|
||||
context.channel
|
||||
.writeAndFlush(
|
||||
self.wrapOutboundOut(HTTPServerResponsePart.end(nil))
|
||||
)
|
||||
.whenComplete { _ in
|
||||
/* no op */
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -52,9 +52,10 @@ extension RequestsHandlerActor {
|
||||
.first { mockRequest in
|
||||
switch true {
|
||||
case mockRequest.ignoreQueryParams && mockRequest.wildcardMatch:
|
||||
return clientRequest.withStrippedQueryParams().wildcardMatches(
|
||||
mockRequest.remoteRequest
|
||||
)
|
||||
return clientRequest.withStrippedQueryParams()
|
||||
.wildcardMatches(
|
||||
mockRequest.remoteRequest
|
||||
)
|
||||
case mockRequest.ignoreQueryParams:
|
||||
return clientRequest.stripPathQueryParams() == mockRequest.remoteRequest.path
|
||||
case mockRequest.wildcardMatch:
|
||||
|
||||
@@ -44,7 +44,7 @@ public final class ComposerContactProvider {
|
||||
task?.cancel()
|
||||
task = Task {
|
||||
let textForMatching = text.toContactMatchFormat()
|
||||
var matchingContacts = [ComposerContact]()
|
||||
var matchingContacts: [ComposerContact] = []
|
||||
|
||||
if let contactsResult {
|
||||
matchingContacts = contactsResult.filter(textForMatching)
|
||||
|
||||
@@ -244,30 +244,32 @@ final class MockComposerRecipientList: ComposerRecipientListProtocol, @unchecked
|
||||
}
|
||||
|
||||
final class MockAttachmentList: AttachmentListProtocol, @unchecked Sendable {
|
||||
var mockAttachments = [DraftAttachment]()
|
||||
var mockAttachments: [DraftAttachment] = []
|
||||
var attachmentUploadDirectoryURL: URL = URL(fileURLWithPath: .empty)
|
||||
var capturedAddCalls: [(path: String, filenameOverride: String?)] = []
|
||||
var capturedAddInlineCalls: [(path: String, filenameOverride: String?)] = []
|
||||
var capturedSwapInlineCalls: [String] = []
|
||||
var capturedRemoveIdCalls: [ID] = []
|
||||
var capturedRemoveContentIdCalls: [String] = []
|
||||
var mockAttachmentListAddResult = [(lastPathComponent: String, result: AttachmentListAddResult)]()
|
||||
var mockAttachmentListAddInlineResult = [(lastPathComponent: String, result: AttachmentListAddInlineResult)]()
|
||||
var mockAttachmentListAddResult: [(lastPathComponent: String, result: AttachmentListAddResult)] = []
|
||||
var mockAttachmentListAddInlineResult: [(lastPathComponent: String, result: AttachmentListAddInlineResult)] = []
|
||||
var mockAttachmentSwapWithCidResult: VoidDraftAttachmentDispositionSwapResult = .ok
|
||||
var mockAttachmentListRemoveWithCidResult = [(cid: String, result: AttachmentListRemoveWithCidResult)]()
|
||||
var mockAttachmentListRemoveWithCidResult: [(cid: String, result: AttachmentListRemoveWithCidResult)] = []
|
||||
|
||||
func add(path: String, filenameOverride: String?) async -> AttachmentListAddResult {
|
||||
capturedAddCalls.append((path, filenameOverride))
|
||||
return mockAttachmentListAddResult.first(where: {
|
||||
$0.lastPathComponent == path.suffix($0.lastPathComponent.count)
|
||||
})?.result ?? AttachmentListAddResult.ok
|
||||
})?
|
||||
.result ?? AttachmentListAddResult.ok
|
||||
}
|
||||
|
||||
func addInline(path: String, filenameOverride: String?) async -> AttachmentListAddInlineResult {
|
||||
capturedAddInlineCalls.append((path, filenameOverride))
|
||||
return mockAttachmentListAddInlineResult.first(where: {
|
||||
$0.lastPathComponent == path.suffix($0.lastPathComponent.count)
|
||||
})?.result ?? AttachmentListAddInlineResult.ok("12345")
|
||||
})?
|
||||
.result ?? AttachmentListAddInlineResult.ok("12345")
|
||||
}
|
||||
|
||||
func attachmentUploadDirectory() -> String {
|
||||
@@ -287,7 +289,8 @@ final class MockAttachmentList: AttachmentListProtocol, @unchecked Sendable {
|
||||
capturedRemoveContentIdCalls.append(contentId)
|
||||
return mockAttachmentListRemoveWithCidResult.first(where: {
|
||||
$0.cid == contentId
|
||||
})?.result ?? AttachmentListRemoveWithCidResult.ok
|
||||
})?
|
||||
.result ?? AttachmentListRemoveWithCidResult.ok
|
||||
}
|
||||
|
||||
func retry(attachmentId: Id) async -> AttachmentListRetryResult {
|
||||
|
||||
+1
-1
@@ -95,7 +95,7 @@ extension Array where Element == RecipientUIModel {
|
||||
func extractAddressesThatDoNotExist(from array: [RecipientUIModel]) -> Set<String> {
|
||||
Set(
|
||||
array.compactMap { model in
|
||||
if case let .single(singleRecipient) = model.composerRecipient,
|
||||
if case .single(let singleRecipient) = model.composerRecipient,
|
||||
case .invalid(.doesNotExist) = singleRecipient.validState
|
||||
{
|
||||
return singleRecipient.address
|
||||
|
||||
+3
-3
@@ -140,10 +140,10 @@ extension DraftAttachmentsSectionViewController {
|
||||
}
|
||||
|
||||
static let uiModels: [DraftAttachmentUIModel] = [
|
||||
Model.makeUIModel(id: 1, name: "meeting_minutes_for_last_friday.pdf", cat: .pdf, size: 36123512, state: .uploading),
|
||||
Model.makeUIModel(id: 1, name: "meeting_minutes_for_last_friday.pdf", cat: .pdf, size: 36_123_512, state: .uploading),
|
||||
Model.makeUIModel(id: 2, name: "budget.xls", cat: .excel, size: 263478, state: .uploaded),
|
||||
Model.makeUIModel(id: 3, name: "photo_1.jpg", cat: .image, size: 7824333, state: .offline),
|
||||
Model.makeUIModel(id: 4, name: "photo_2_this_one_a_bit_closer.jpg", cat: .image, size: 6123512, state: .uploaded),
|
||||
Model.makeUIModel(id: 3, name: "photo_1.jpg", cat: .image, size: 7_824_333, state: .offline),
|
||||
Model.makeUIModel(id: 4, name: "photo_2_this_one_a_bit_closer.jpg", cat: .image, size: 6_123_512, state: .uploaded),
|
||||
]
|
||||
}
|
||||
|
||||
|
||||
+2
-1
@@ -64,7 +64,8 @@ final class SpinnerButton: UIButton {
|
||||
startAngle: 0,
|
||||
endAngle: CGFloat.pi * 3 / 2,
|
||||
clockwise: true
|
||||
).cgPath
|
||||
)
|
||||
.cgPath
|
||||
|
||||
progressLayer.position = CGPoint(x: bounds.midX, y: bounds.midY)
|
||||
progressLayer.path = circlePath
|
||||
|
||||
+7
-6
@@ -58,12 +58,13 @@ final class ContactPickerCell: UITableViewCell {
|
||||
checked.heightAnchor.constraint(equalToConstant: 24),
|
||||
])
|
||||
|
||||
[initials, groupIcon].forEach {
|
||||
NSLayoutConstraint.activate([
|
||||
$0.centerXAnchor.constraint(equalTo: avatarView.centerXAnchor),
|
||||
$0.centerYAnchor.constraint(equalTo: avatarView.centerYAnchor),
|
||||
])
|
||||
}
|
||||
[initials, groupIcon]
|
||||
.forEach {
|
||||
NSLayoutConstraint.activate([
|
||||
$0.centerXAnchor.constraint(equalTo: avatarView.centerXAnchor),
|
||||
$0.centerYAnchor.constraint(equalTo: avatarView.centerYAnchor),
|
||||
])
|
||||
}
|
||||
}
|
||||
|
||||
func configure(uiModel: ComposerContactUIModel) {
|
||||
|
||||
+7
-6
@@ -105,12 +105,13 @@ final class DraftActionBarViewController: UIViewController {
|
||||
topBorder.trailingAnchor.constraint(equalTo: view.trailingAnchor),
|
||||
topBorder.topAnchor.constraint(equalTo: view.topAnchor),
|
||||
])
|
||||
[attachmentButton, passwordButton, discardButton].forEach { button in
|
||||
NSLayoutConstraint.activate([
|
||||
button.widthAnchor.constraint(equalToConstant: buttonSize),
|
||||
button.heightAnchor.constraint(equalTo: button.widthAnchor),
|
||||
])
|
||||
}
|
||||
[attachmentButton, passwordButton, discardButton]
|
||||
.forEach { button in
|
||||
NSLayoutConstraint.activate([
|
||||
button.widthAnchor.constraint(equalToConstant: buttonSize),
|
||||
button.heightAnchor.constraint(equalTo: button.widthAnchor),
|
||||
])
|
||||
}
|
||||
}
|
||||
|
||||
private func applyState() {
|
||||
|
||||
+6
-6
@@ -92,7 +92,7 @@ extension AttachmentErrorAlertState {
|
||||
}
|
||||
}
|
||||
|
||||
var result = [AttachmentErrorAlertModel]()
|
||||
var result: [AttachmentErrorAlertModel] = []
|
||||
if overSizeLimitCount > 0 {
|
||||
result.append(.overSizeLimit(origin: .adding(.defaultAddAttachmentError(count: overSizeLimitCount))))
|
||||
}
|
||||
@@ -110,10 +110,10 @@ extension AttachmentErrorAlertState {
|
||||
|
||||
/// Groups together `DraftAttachment` by error type to reduce the total number of alerts.
|
||||
private func aggregateUploadingAttachmentErrors(_ attachments: [DraftAttachment]) -> [AttachmentErrorAlertModel] {
|
||||
var overSizeFailures = [DraftAttachment]()
|
||||
var tooManyAttachmentsFailures = [DraftAttachment]()
|
||||
var storageQuotaExceededFailures = [DraftAttachment]()
|
||||
var otherFailures = [DraftAttachment]()
|
||||
var overSizeFailures: [DraftAttachment] = []
|
||||
var tooManyAttachmentsFailures: [DraftAttachment] = []
|
||||
var storageQuotaExceededFailures: [DraftAttachment] = []
|
||||
var otherFailures: [DraftAttachment] = []
|
||||
|
||||
for attachment in attachments {
|
||||
guard let error = attachment.state.attachmentUploadError else { continue }
|
||||
@@ -136,7 +136,7 @@ extension AttachmentErrorAlertState {
|
||||
}
|
||||
}
|
||||
|
||||
var result = [AttachmentErrorAlertModel]()
|
||||
var result: [AttachmentErrorAlertModel] = []
|
||||
if !overSizeFailures.isEmpty, let uploadingError = mapUnseenToUploadingErrorOrigin(overSizeFailures) {
|
||||
result.append(.overSizeLimit(origin: uploadingError))
|
||||
}
|
||||
|
||||
+1
-1
@@ -31,7 +31,7 @@ struct FilePickerItemHandler {
|
||||
let uploadFolder: URL = URL(fileURLWithPath: draft.attachmentList().attachmentUploadDirectory())
|
||||
switch selectionResult {
|
||||
case .success(let urls):
|
||||
var allErrors = [DraftAttachmentUploadError]()
|
||||
var allErrors: [DraftAttachmentUploadError] = []
|
||||
for await result in copyFilePickerItems(files: urls, destinationFolder: uploadFolder) {
|
||||
switch result {
|
||||
case .success(let file):
|
||||
|
||||
+2
-2
@@ -30,8 +30,8 @@ struct PhotosPickerItemHandler {
|
||||
to draft: AppDraftProtocol,
|
||||
photos: [PhotosPickerItemTransferable]
|
||||
) async -> PhotosPickerItemHandlerResult {
|
||||
var successfulContentIds = [String]()
|
||||
var allErrors = [DraftAttachmentUploadError]()
|
||||
var successfulContentIds: [String] = []
|
||||
var allErrors: [DraftAttachmentUploadError] = []
|
||||
let uploadFolder: URL = URL(fileURLWithPath: draft.attachmentList().attachmentUploadDirectory())
|
||||
for await result in saveToFile(items: photos, destinationFolder: uploadFolder) {
|
||||
switch result {
|
||||
|
||||
@@ -58,8 +58,8 @@ final class ComposerModel: ObservableObject {
|
||||
)
|
||||
lazy var invalidAddressAlertStore = InvalidAddressAlertStateStore(
|
||||
validator: .init(
|
||||
readState: { [weak self] in return self?.state },
|
||||
composerWillDismiss: { [weak self] in return self?.composerWillDismiss ?? false }
|
||||
readState: { [weak self] in self?.state },
|
||||
composerWillDismiss: { [weak self] in self?.composerWillDismiss ?? false }
|
||||
),
|
||||
alertBinding: alertBinding
|
||||
)
|
||||
@@ -160,11 +160,12 @@ final class ComposerModel: ObservableObject {
|
||||
recipientFieldState.copy(\.controllerState, to: .editing)
|
||||
}
|
||||
newState = newState.copy(\.editingRecipientsGroup, to: group)
|
||||
RecipientGroupType.allCases(excluding: group).forEach { group in
|
||||
newState.overrideRecipientState(for: group) { recipientFieldState in
|
||||
recipientFieldState.copy(\.controllerState, to: .expanded)
|
||||
RecipientGroupType.allCases(excluding: group)
|
||||
.forEach { group in
|
||||
newState.overrideRecipientState(for: group) { recipientFieldState in
|
||||
recipientFieldState.copy(\.controllerState, to: .expanded)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
state = newState
|
||||
}
|
||||
@@ -521,9 +522,10 @@ extension ComposerModel {
|
||||
selecting selectedIndexes: Set<Int> = []
|
||||
) -> [RecipientUIModel] {
|
||||
let recipientList = recipientList(from: draft, group: group)
|
||||
return recipientList.recipients().enumerated().map { index, recipient in
|
||||
RecipientUIModel(composerRecipient: recipient, isSelected: selectedIndexes.contains(index))
|
||||
}
|
||||
return recipientList.recipients().enumerated()
|
||||
.map { index, recipient in
|
||||
RecipientUIModel(composerRecipient: recipient, isSelected: selectedIndexes.contains(index))
|
||||
}
|
||||
}
|
||||
|
||||
private func setUpCallbacks() {
|
||||
|
||||
@@ -88,7 +88,7 @@ struct ComposerView: View {
|
||||
case .viewDidDisappear:
|
||||
Task { await model.viewDidDisappear() }
|
||||
|
||||
case let .recipientFieldEvent(recipientFieldEvent, group):
|
||||
case .recipientFieldEvent(let recipientFieldEvent, let group):
|
||||
switch recipientFieldEvent {
|
||||
case .onFieldTap:
|
||||
model.startEditingRecipients(for: group)
|
||||
@@ -104,7 +104,7 @@ struct ComposerView: View {
|
||||
model.removeRecipientsThatAreSelected(group: group)
|
||||
}
|
||||
|
||||
case let .contactPickerEvent(event, group):
|
||||
case .contactPickerEvent(let event, let group):
|
||||
switch event {
|
||||
case .onInputChange(let text):
|
||||
model.matchContact(group: group, text: text)
|
||||
|
||||
+1
-1
@@ -192,7 +192,7 @@ private extension View {
|
||||
)
|
||||
|
||||
ScheduleSendTimeOptionsView(
|
||||
predefinedTimeOptions: options(true, 1904565584),
|
||||
predefinedTimeOptions: options(true, 1_904_565_584),
|
||||
isCustomOptionAvailable: true,
|
||||
dateFormatter: .init(),
|
||||
onTimeSelected: { _ in },
|
||||
|
||||
@@ -20,8 +20,10 @@ struct InlineImageHTML {
|
||||
let content: String
|
||||
|
||||
init(cids: [String]) {
|
||||
content = cids.map { cid in
|
||||
#"<img src="cid:\#(cid)" style="max-width: 100%;"><br>"#
|
||||
}.joined()
|
||||
content =
|
||||
cids.map { cid in
|
||||
#"<img src="cid:\#(cid)" style="max-width: 100%;"><br>"#
|
||||
}
|
||||
.joined()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -29,11 +29,13 @@ final class ScheduleSendSheetSnapshotTests {
|
||||
|
||||
@Test
|
||||
func testScheduleSend_whenFreeUser_itLayoutsCorrectOnIphoneX() throws {
|
||||
let options: DraftScheduleSendOptions = try ScheduleSendOptionsProvider.dummy(
|
||||
isCustomAvailable: false,
|
||||
stubTomorrowTime: 1810210838,
|
||||
stubMondayTime: 1810729238
|
||||
).scheduleSendOptions().get()
|
||||
let options: DraftScheduleSendOptions =
|
||||
try ScheduleSendOptionsProvider.dummy(
|
||||
isCustomAvailable: false,
|
||||
stubTomorrowTime: 1_810_210_838,
|
||||
stubMondayTime: 1_810_729_238
|
||||
)
|
||||
.scheduleSendOptions().get()
|
||||
let scheduleSend = ScheduleSendPickerSheet(
|
||||
predefinedTimeOptions: options.toScheduleSendTimeOptions(lastScheduleSendTime: nil),
|
||||
isCustomOptionAvailable: options.isCustomOptionAvailable,
|
||||
@@ -45,11 +47,13 @@ final class ScheduleSendSheetSnapshotTests {
|
||||
|
||||
@Test
|
||||
func testScheduleSend_whenPaidUser_itLayoutsCorrectOnIphoneX() throws {
|
||||
let options: DraftScheduleSendOptions = try ScheduleSendOptionsProvider.dummy(
|
||||
isCustomAvailable: true,
|
||||
stubTomorrowTime: 1810210838,
|
||||
stubMondayTime: 1810729238
|
||||
).scheduleSendOptions().get()
|
||||
let options: DraftScheduleSendOptions =
|
||||
try ScheduleSendOptionsProvider.dummy(
|
||||
isCustomAvailable: true,
|
||||
stubTomorrowTime: 1_810_210_838,
|
||||
stubMondayTime: 1_810_729_238
|
||||
)
|
||||
.scheduleSendOptions().get()
|
||||
let scheduleSend = ScheduleSendPickerSheet(
|
||||
predefinedTimeOptions: options.toScheduleSendTimeOptions(lastScheduleSendTime: nil),
|
||||
isCustomOptionAvailable: options.isCustomOptionAvailable,
|
||||
@@ -61,13 +65,15 @@ final class ScheduleSendSheetSnapshotTests {
|
||||
|
||||
@Test
|
||||
func testScheduleSend_whenPaidUser_andPreviouslySetTime_itLayoutsCorrectOnIphoneX() throws {
|
||||
let options: DraftScheduleSendOptions = try ScheduleSendOptionsProvider.dummy(
|
||||
isCustomAvailable: true,
|
||||
stubTomorrowTime: 1810210838,
|
||||
stubMondayTime: 1810729238
|
||||
).scheduleSendOptions().get()
|
||||
let options: DraftScheduleSendOptions =
|
||||
try ScheduleSendOptionsProvider.dummy(
|
||||
isCustomAvailable: true,
|
||||
stubTomorrowTime: 1_810_210_838,
|
||||
stubMondayTime: 1_810_729_238
|
||||
)
|
||||
.scheduleSendOptions().get()
|
||||
let scheduleSend = ScheduleSendPickerSheet(
|
||||
predefinedTimeOptions: options.toScheduleSendTimeOptions(lastScheduleSendTime: 1810483200),
|
||||
predefinedTimeOptions: options.toScheduleSendTimeOptions(lastScheduleSendTime: 1_810_483_200),
|
||||
isCustomOptionAvailable: options.isCustomOptionAvailable,
|
||||
dateFormatter: dateFormatter,
|
||||
onTimeSelected: { _ in }
|
||||
|
||||
@@ -23,9 +23,9 @@ import proton_app_uniffi
|
||||
@MainActor
|
||||
final class AttachmentErrorAlertStateTests {
|
||||
private var sut: AttachmentErrorAlertState!
|
||||
private static let timeStamp1: Int64 = 1743000520
|
||||
private static let timeStamp2: Int64 = 1743009702
|
||||
private static let timeStamp3: Int64 = 1743032002
|
||||
private static let timeStamp1: Int64 = 1_743_000_520
|
||||
private static let timeStamp2: Int64 = 1_743_009_702
|
||||
private static let timeStamp3: Int64 = 1_743_032_002
|
||||
private let tooManyError1 = DraftAttachment.makeMock(id: 1, state: .error(.upload(.reason(.tooManyAttachments))), timestamp: timeStamp1)
|
||||
private let tooManyError2 = DraftAttachment.makeMock(id: 2, state: .error(.upload(.reason(.tooManyAttachments))), timestamp: timeStamp2)
|
||||
private let tooLargeError1 = DraftAttachment.makeMock(id: 3, state: .error(.upload(.reason(.attachmentTooLarge))), timestamp: timeStamp3)
|
||||
|
||||
@@ -797,7 +797,7 @@ final class ComposerModelTests: BaseTestCase {
|
||||
let sut = makeSut(draft: mockDraft, draftOrigin: .new, contactProvider: .mockInstance)
|
||||
let dismissSpy = DismissSpy()
|
||||
|
||||
let scheduleTime: UInt64 = 1905427712
|
||||
let scheduleTime: UInt64 = 1_905_427_712
|
||||
await sut.sendMessage(at: scheduleTime.date, dismissAction: dismissSpy)
|
||||
|
||||
XCTAssertTrue(mockDraft.scheduleSendWasCalled)
|
||||
@@ -813,7 +813,7 @@ final class ComposerModelTests: BaseTestCase {
|
||||
let sut = makeSut(draft: mockDraft, draftOrigin: .new, contactProvider: .mockInstance)
|
||||
let dismissSpy = DismissSpy()
|
||||
|
||||
let scheduleTime: UInt64 = 1905427712
|
||||
let scheduleTime: UInt64 = 1_905_427_712
|
||||
await sut.sendMessage(at: scheduleTime.date, dismissAction: dismissSpy)
|
||||
|
||||
XCTAssertTrue(mockDraft.scheduleSendWasCalled)
|
||||
@@ -943,11 +943,12 @@ private extension ComposerModelTests {
|
||||
}
|
||||
|
||||
func fulfill(_ expectation: XCTestExpectation, in sut: ComposerModel, when condition: @escaping (ComposerState) -> Bool) {
|
||||
sut.$state.sink { state in
|
||||
guard condition(state) else { return }
|
||||
expectation.fulfill()
|
||||
}
|
||||
.store(in: &cancellables)
|
||||
sut.$state
|
||||
.sink { state in
|
||||
guard condition(state) else { return }
|
||||
expectation.fulfill()
|
||||
}
|
||||
.store(in: &cancellables)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1010,7 +1011,7 @@ extension DraftAttachment {
|
||||
size: 123456,
|
||||
isListable: false
|
||||
)
|
||||
return DraftAttachment(state: state, attachment: mockAttachment, stateModifiedTimestamp: 1742829536)
|
||||
return DraftAttachment(state: state, attachment: mockAttachment, stateModifiedTimestamp: 1_742_829_536)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1039,7 +1040,7 @@ private extension MockDraft {
|
||||
size: 123456,
|
||||
isListable: false
|
||||
)
|
||||
return [DraftAttachment(state: .uploaded, attachment: mockAttachment, stateModifiedTimestamp: 1742829536)]
|
||||
return [DraftAttachment(state: .uploaded, attachment: mockAttachment, stateModifiedTimestamp: 1_742_829_536)]
|
||||
}
|
||||
|
||||
static var defaultMockDraft: MockDraft {
|
||||
|
||||
@@ -43,7 +43,7 @@ final class DebouncedTaskTests: XCTestCase {
|
||||
// MARK: Execute Immediately
|
||||
|
||||
func testExecuteImmediately_itShouldExecuteTheTask() async {
|
||||
var result = [Int]()
|
||||
var result: [Int] = []
|
||||
sut = .init(duration: .seconds(10), block: { result.append(1) }, onBlockCompletion: {})
|
||||
|
||||
await sut.executeImmediately()
|
||||
@@ -51,7 +51,7 @@ final class DebouncedTaskTests: XCTestCase {
|
||||
}
|
||||
|
||||
func testExecuteImmediately_itShouldCancelTheDebouncedOpertionToAvoidRunningItTwice() async {
|
||||
var result = [Int]()
|
||||
var result: [Int] = []
|
||||
let duration = Duration.milliseconds(10)
|
||||
sut = .init(duration: duration, block: { result.append(1) }, onBlockCompletion: {})
|
||||
|
||||
@@ -63,7 +63,7 @@ final class DebouncedTaskTests: XCTestCase {
|
||||
}
|
||||
|
||||
func testExecuteImmediately_itShouldCallOnBlockCompletion() async {
|
||||
var result = [Int]()
|
||||
var result: [Int] = []
|
||||
var completionCalled = false
|
||||
let duration = Duration.milliseconds(10)
|
||||
sut = .init(
|
||||
|
||||
@@ -37,7 +37,7 @@ final class ScheduleSendDateFormatterTests {
|
||||
.tomorrowTime
|
||||
.date
|
||||
}
|
||||
private var distantFuture: Date { Date(timeIntervalSince1970: 1889427600) }
|
||||
private var distantFuture: Date { Date(timeIntervalSince1970: 1_889_427_600) }
|
||||
|
||||
// MARK: Format.short
|
||||
|
||||
|
||||
@@ -22,7 +22,7 @@ public final class AppLogger: @unchecked Sendable {
|
||||
private static let shared = AppLogger()
|
||||
private let serialQueue = DispatchQueue(label: "\(Bundle.defaultIdentifier).AppLogger")
|
||||
|
||||
private var loggers = [String: Any]()
|
||||
private var loggers: [String: Any] = [:]
|
||||
private var bundleId: String {
|
||||
Bundle.main.bundleIdentifier ?? Bundle.defaultIdentifier
|
||||
}
|
||||
|
||||
@@ -35,6 +35,7 @@ public struct BlurredCoverView: View {
|
||||
}
|
||||
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .top)
|
||||
.background(.ultraThinMaterial)
|
||||
}.ignoresSafeArea()
|
||||
}
|
||||
.ignoresSafeArea()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -46,7 +46,8 @@ public struct LockScreen: View {
|
||||
error: $store.state.pinAuthenticationError
|
||||
) { output in
|
||||
store.handle(action: .pin(output))
|
||||
}.onLoad {
|
||||
}
|
||||
.onLoad {
|
||||
store.handle(action: .pinScreenLoaded)
|
||||
}
|
||||
case .biometric:
|
||||
|
||||
@@ -121,7 +121,8 @@ public struct PINLockScreen: View {
|
||||
.foregroundStyle(DS.Color.Text.weak)
|
||||
.transition(.identity)
|
||||
}
|
||||
}.animation(.easeInOut(duration: 0.2), value: store.state.error)
|
||||
}
|
||||
.animation(.easeInOut(duration: 0.2), value: store.state.error)
|
||||
}
|
||||
|
||||
private var pinBinding: Binding<String> {
|
||||
|
||||
@@ -63,8 +63,9 @@ private struct TransparentBlur: UIViewRepresentable {
|
||||
|
||||
private class TransparentBlurView: UIVisualEffectView {
|
||||
override func layoutSublayers(of layer: CALayer) {
|
||||
layer.sublayers?.first?.filters?.removeAll(where: { filter in
|
||||
String(describing: filter) != "gaussianBlur"
|
||||
})
|
||||
layer.sublayers?.first?.filters?
|
||||
.removeAll(where: { filter in
|
||||
String(describing: filter) != "gaussianBlur"
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -37,13 +37,13 @@ open class Keychain {
|
||||
|
||||
public var errorDescription: String? {
|
||||
switch self {
|
||||
case let .readFailed(key, code):
|
||||
case .readFailed(let key, let code):
|
||||
return "Keychain.AccessError.readFailed(\(key), \(code))"
|
||||
case let .writeFailed(key, code):
|
||||
case .writeFailed(let key, let code):
|
||||
return "Keychain.AccessError.writeFailed(\(key), \(code))"
|
||||
case let .updateFailed(key, code):
|
||||
case .updateFailed(let key, let code):
|
||||
return "Keychain.AccessError.updateFailed(\(key), \(code))"
|
||||
case let .deleteFailed(key, code):
|
||||
case .deleteFailed(let key, let code):
|
||||
return "Keychain.AccessError.deleteFailed(\(key), \(code))"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -60,7 +60,10 @@ final class KeychainTests: XCTestCase {
|
||||
// when
|
||||
XCTAssertThrowsError(try out.dataOrError(forKey: "any.key")) { error in
|
||||
// then
|
||||
guard case let Keychain.AccessError.readFailed(key, errorCode) = error else { XCTFail(); return }
|
||||
guard case Keychain.AccessError.readFailed(let key, let errorCode) = error else {
|
||||
XCTFail()
|
||||
return
|
||||
}
|
||||
XCTAssertEqual(key, "any.key")
|
||||
XCTAssertEqual(errorCode, errSecInteractionNotAllowed)
|
||||
}
|
||||
@@ -73,7 +76,10 @@ final class KeychainTests: XCTestCase {
|
||||
// when
|
||||
XCTAssertThrowsError(try out.stringOrError(forKey: "any.key")) { error in
|
||||
// then
|
||||
guard case let Keychain.AccessError.readFailed(key, errorCode) = error else { XCTFail(); return }
|
||||
guard case Keychain.AccessError.readFailed(let key, let errorCode) = error else {
|
||||
XCTFail()
|
||||
return
|
||||
}
|
||||
XCTAssertEqual(key, "any.key")
|
||||
XCTAssertEqual(errorCode, errSecInteractionNotAllowed)
|
||||
}
|
||||
@@ -96,7 +102,10 @@ final class KeychainTests: XCTestCase {
|
||||
// when
|
||||
XCTAssertThrowsError(try out.setOrError(Data(), forKey: "any.key")) { error in
|
||||
// then
|
||||
guard case let Keychain.AccessError.writeFailed(key, errorCode) = error else { XCTFail(); return }
|
||||
guard case Keychain.AccessError.writeFailed(let key, let errorCode) = error else {
|
||||
XCTFail()
|
||||
return
|
||||
}
|
||||
XCTAssertEqual(key, "any.key")
|
||||
XCTAssertEqual(errorCode, errSecInteractionNotAllowed)
|
||||
}
|
||||
@@ -109,7 +118,10 @@ final class KeychainTests: XCTestCase {
|
||||
// when
|
||||
XCTAssertThrowsError(try out.setOrError(String(), forKey: "any.key")) { error in
|
||||
// then
|
||||
guard case let Keychain.AccessError.writeFailed(key, errorCode) = error else { XCTFail(); return }
|
||||
guard case Keychain.AccessError.writeFailed(let key, let errorCode) = error else {
|
||||
XCTFail()
|
||||
return
|
||||
}
|
||||
XCTAssertEqual(key, "any.key")
|
||||
XCTAssertEqual(errorCode, errSecInteractionNotAllowed)
|
||||
}
|
||||
@@ -132,7 +144,10 @@ final class KeychainTests: XCTestCase {
|
||||
// when
|
||||
XCTAssertThrowsError(try out.setOrError(Data(), forKey: "any.key")) { error in
|
||||
// then
|
||||
guard case let Keychain.AccessError.updateFailed(key, errorCode) = error else { XCTFail(); return }
|
||||
guard case Keychain.AccessError.updateFailed(let key, let errorCode) = error else {
|
||||
XCTFail()
|
||||
return
|
||||
}
|
||||
XCTAssertEqual(key, "any.key")
|
||||
XCTAssertEqual(errorCode, errSecInteractionNotAllowed)
|
||||
}
|
||||
@@ -145,7 +160,10 @@ final class KeychainTests: XCTestCase {
|
||||
// when
|
||||
XCTAssertThrowsError(try out.setOrError(String(), forKey: "any.key")) { error in
|
||||
// then
|
||||
guard case let Keychain.AccessError.updateFailed(key, errorCode) = error else { XCTFail(); return }
|
||||
guard case Keychain.AccessError.updateFailed(let key, let errorCode) = error else {
|
||||
XCTFail()
|
||||
return
|
||||
}
|
||||
XCTAssertEqual(key, "any.key")
|
||||
XCTAssertEqual(errorCode, errSecInteractionNotAllowed)
|
||||
}
|
||||
@@ -168,7 +186,10 @@ final class KeychainTests: XCTestCase {
|
||||
// when
|
||||
XCTAssertThrowsError(try out.removeOrError(forKey: "any.key")) { error in
|
||||
// then
|
||||
guard case let Keychain.AccessError.deleteFailed(key, errorCode) = error else { XCTFail(); return }
|
||||
guard case Keychain.AccessError.deleteFailed(let key, let errorCode) = error else {
|
||||
XCTFail()
|
||||
return
|
||||
}
|
||||
XCTAssertEqual(key, "any.key")
|
||||
XCTAssertEqual(errorCode, errSecInteractionNotAllowed)
|
||||
}
|
||||
|
||||
@@ -56,14 +56,15 @@ enum EventDateFormatter {
|
||||
return relativeFormatter.string(from: relativeDate)
|
||||
}
|
||||
|
||||
var allDayStyle = Date.FormatStyle(
|
||||
date: .abbreviated,
|
||||
time: .omitted,
|
||||
locale: calendar.locale!,
|
||||
calendar: calendar,
|
||||
timeZone: calendar.timeZone
|
||||
)
|
||||
.weekday()
|
||||
var allDayStyle =
|
||||
Date.FormatStyle(
|
||||
date: .abbreviated,
|
||||
time: .omitted,
|
||||
locale: calendar.locale!,
|
||||
calendar: calendar,
|
||||
timeZone: calendar.timeZone
|
||||
)
|
||||
.weekday()
|
||||
|
||||
if calendar.isDate(fromDate, equalTo: now, toGranularity: .year) {
|
||||
if #available(iOS 18, *) {
|
||||
@@ -74,25 +75,27 @@ enum EventDateFormatter {
|
||||
return fromDate.formatted(allDayStyle)
|
||||
}
|
||||
|
||||
let allDayIntervalStyle = Date.IntervalFormatStyle(
|
||||
date: .abbreviated,
|
||||
time: .omitted,
|
||||
locale: calendar.locale!,
|
||||
calendar: calendar,
|
||||
timeZone: calendar.timeZone
|
||||
)
|
||||
.weekday()
|
||||
let allDayIntervalStyle =
|
||||
Date.IntervalFormatStyle(
|
||||
date: .abbreviated,
|
||||
time: .omitted,
|
||||
locale: calendar.locale!,
|
||||
calendar: calendar,
|
||||
timeZone: calendar.timeZone
|
||||
)
|
||||
.weekday()
|
||||
|
||||
return allDayIntervalStyle.format(fromDate..<adjustedToDate)
|
||||
case .dateTime:
|
||||
let calendar = DateEnvironment.calendar
|
||||
let dateTimeStyle = Date.IntervalFormatStyle(
|
||||
date: .abbreviated,
|
||||
time: .shortened,
|
||||
locale: calendar.locale!,
|
||||
timeZone: calendar.timeZone
|
||||
)
|
||||
.weekday()
|
||||
let dateTimeStyle =
|
||||
Date.IntervalFormatStyle(
|
||||
date: .abbreviated,
|
||||
time: .shortened,
|
||||
locale: calendar.locale!,
|
||||
timeZone: calendar.timeZone
|
||||
)
|
||||
.weekday()
|
||||
|
||||
return dateTimeStyle.format(fromDate..<toDate)
|
||||
}
|
||||
|
||||
@@ -46,7 +46,7 @@ enum EventMapper {
|
||||
private static func answerButtonsState(from state: RsvpState, attendeeIndex: UInt32?) -> Event.AnswerButtonsState {
|
||||
let buttonsState: Event.AnswerButtonsState
|
||||
|
||||
if case let .answerableInvite(_, attendance) = state, let attendeeIndex {
|
||||
if case .answerableInvite(_, let attendance) = state, let attendeeIndex {
|
||||
buttonsState = .visible(attendance: attendance, attendeeIndex: Int(attendeeIndex))
|
||||
} else {
|
||||
buttonsState = .hidden
|
||||
@@ -57,7 +57,7 @@ enum EventMapper {
|
||||
|
||||
private static func banner(from state: RsvpState) -> Event.Banner? {
|
||||
switch state {
|
||||
case let .answerableInvite(progress, _), let .reminder(progress):
|
||||
case .answerableInvite(let progress, _), .reminder(let progress):
|
||||
switch progress {
|
||||
case .pending:
|
||||
return nil
|
||||
@@ -102,12 +102,13 @@ enum EventMapper {
|
||||
}
|
||||
|
||||
private static func participants(attendees: [RsvpAttendee], userIndex: UInt32?) -> [Event.Participant] {
|
||||
attendees.enumerated().map { index, attendee in
|
||||
let isCurrentUser = isCurrentUser(attendeeIndex: index, userAttendeeIndex: userIndex)
|
||||
let displayName = isCurrentUser ? userDisplayName(from: attendee) : otherAttendeeDisplayName(from: attendee)
|
||||
attendees.enumerated()
|
||||
.map { index, attendee in
|
||||
let isCurrentUser = isCurrentUser(attendeeIndex: index, userAttendeeIndex: userIndex)
|
||||
let displayName = isCurrentUser ? userDisplayName(from: attendee) : otherAttendeeDisplayName(from: attendee)
|
||||
|
||||
return .init(email: attendee.email, displayName: displayName, status: attendee.status)
|
||||
}
|
||||
return .init(email: attendee.email, displayName: displayName, status: attendee.status)
|
||||
}
|
||||
}
|
||||
|
||||
private static func isCurrentUser(attendeeIndex: Int, userAttendeeIndex: UInt32?) -> Bool {
|
||||
|
||||
@@ -69,11 +69,11 @@ final class RSVPStateStore: StateStore {
|
||||
case .onLoad, .retry:
|
||||
await loadEventDetails()
|
||||
case .answer(let status):
|
||||
if case let .loaded(service, event) = internalState {
|
||||
if case .loaded(let service, let event) = internalState {
|
||||
await answer(with: status, event: event, service: service)
|
||||
}
|
||||
case .calendarIconTapped:
|
||||
if case let .loaded(_, event) = internalState {
|
||||
if case .loaded(_, let event) = internalState {
|
||||
tryToOpenEventInCalendarApp(with: event)
|
||||
}
|
||||
case .copyAddress(let email):
|
||||
|
||||
@@ -36,7 +36,7 @@ struct EventHeader: View {
|
||||
.fontWeight(.medium)
|
||||
.foregroundStyle(DS.Color.Text.norm)
|
||||
.minimumScaleFactor(0.75)
|
||||
if case let .visible(attendance, _) = answerButtons, attendance == .optional {
|
||||
if case .visible(let attendance, _) = answerButtons, attendance == .optional {
|
||||
Text(L10n.attendanceOptional)
|
||||
.font(.footnote)
|
||||
.fontWeight(.regular)
|
||||
|
||||
@@ -50,7 +50,7 @@ struct RSVPEventView: View {
|
||||
VStack(alignment: .leading, spacing: DS.Spacing.large) {
|
||||
eventHeader
|
||||
.padding(.horizontal, DS.Spacing.extraLarge)
|
||||
if case let .visible(_, userParticipantIndex) = event.answerButtons {
|
||||
if case .visible(_, let userParticipantIndex) = event.answerButtons {
|
||||
answerSection(userParticipantIndex: userParticipantIndex)
|
||||
.padding(.bottom, DS.Spacing.small)
|
||||
.padding(.horizontal, DS.Spacing.extraLarge)
|
||||
@@ -165,8 +165,8 @@ private extension RsvpAttendeeStatus {
|
||||
location: "Huddle Room",
|
||||
description: "A brief check-in.",
|
||||
recurrence: nil,
|
||||
startsAt: 1754042400, // Aug 1, 2025 10:00 AM UTC
|
||||
endsAt: 1754044200, // Aug 1, 2025 10:30 AM UTC
|
||||
startsAt: 1_754_042_400, // Aug 1, 2025 10:00 AM UTC
|
||||
endsAt: 1_754_044_200, // Aug 1, 2025 10:30 AM UTC
|
||||
occurrence: .dateTime,
|
||||
organizer: RsvpOrganizer(name: .none, email: "organizer1@example.com"),
|
||||
attendees: [
|
||||
|
||||
@@ -26,8 +26,8 @@ import proton_app_uniffi
|
||||
final class EventMapperTests {
|
||||
@Test(
|
||||
arguments: [
|
||||
(summary: Optional<String>("Amazing Apple event!"), expected: "Amazing Apple event!"),
|
||||
(summary: Optional<String>(nil), expected: L10n.noEventTitlePlacholder.string),
|
||||
(summary: String?("Amazing Apple event!"), expected: "Amazing Apple event!"),
|
||||
(summary: String?(nil), expected: L10n.noEventTitlePlacholder.string),
|
||||
]
|
||||
)
|
||||
func testTitleMapping(summary: String?, expectedTitle: String) {
|
||||
@@ -39,7 +39,7 @@ final class EventMapperTests {
|
||||
|
||||
@Test(
|
||||
arguments: zip(
|
||||
Array<RsvpEvent>([
|
||||
[RsvpEvent]([
|
||||
RsvpEvent.testData(
|
||||
userAttendeeIdx: 1,
|
||||
state: .answerableInvite(progress: .pending, attendance: .optional)
|
||||
@@ -89,7 +89,7 @@ final class EventMapperTests {
|
||||
RsvpState.cancelledReminder,
|
||||
],
|
||||
[
|
||||
Optional<Event.Banner>(nil),
|
||||
Event.Banner?(nil),
|
||||
Event.Banner(style: .now, regularText: L10n.Header.happening, boldText: L10n.Header.now),
|
||||
.init(style: .ended, regularText: L10n.Header.event, boldText: L10n.Header.ended),
|
||||
nil,
|
||||
@@ -134,8 +134,8 @@ final class EventMapperTests {
|
||||
|
||||
@Test(arguments: [
|
||||
(
|
||||
userAttendeeIndex: Optional<UInt32>(1),
|
||||
expected: Array<Event.Participant>([
|
||||
userAttendeeIndex: UInt32?(1),
|
||||
expected: [Event.Participant]([
|
||||
.init(email: "alice@proton.me", displayName: "Alice Sherington • alice@proton.me", status: .yes),
|
||||
.init(email: "bob@outlook.com", displayName: "You • bob@outlook.com", status: .no, ),
|
||||
.init(email: "cyril@gmail.com", displayName: "cyril@gmail.com", status: .maybe, ),
|
||||
@@ -144,7 +144,7 @@ final class EventMapperTests {
|
||||
),
|
||||
(
|
||||
userAttendeeIndex: Optional<UInt32>.none,
|
||||
expected: Array<Event.Participant>([
|
||||
expected: [Event.Participant]([
|
||||
.init(email: "alice@proton.me", displayName: "Alice Sherington • alice@proton.me", status: .yes),
|
||||
.init(email: "bob@outlook.com", displayName: "Bob Charlton • bob@outlook.com", status: .no),
|
||||
.init(email: "cyril@gmail.com", displayName: "cyril@gmail.com", status: .maybe, ),
|
||||
|
||||
@@ -228,7 +228,7 @@ final class RSVPStateStoreTests {
|
||||
func calendarIconTappedAction_WhenHasEventInformation_ItOpensCalendarAppWithEventDetails() async {
|
||||
let expectedEvent: RsvpEvent = .bestEvent(
|
||||
id: "event_id_9",
|
||||
startsAt: 1672531200,
|
||||
startsAt: 1_672_531_200,
|
||||
calendar: .init(id: "calendar_id_42", name: "Work", color: .empty)
|
||||
)
|
||||
|
||||
@@ -252,7 +252,7 @@ final class RSVPStateStoreTests {
|
||||
func calendarIconTappedAction_WhenHasEventInformationButAppVersionIsToOld_ItTriesToOpenCalendarAppAndFallbacksToAppStore() async {
|
||||
let expectedEvent: RsvpEvent = .bestEvent(
|
||||
id: "event_id_3",
|
||||
startsAt: 1609459200,
|
||||
startsAt: 1_609_459_200,
|
||||
calendar: .init(id: "calendar_id_19", name: "Work", color: .empty)
|
||||
)
|
||||
|
||||
|
||||
@@ -31,7 +31,7 @@ extension Conversation {
|
||||
displaySnoozeReminder: false,
|
||||
snoozedUntil: nil,
|
||||
locations: [.system(name: .inbox, id: .init(value: 41))],
|
||||
expirationTime: 1625140800,
|
||||
expirationTime: 1_625_140_800,
|
||||
isStarred: true,
|
||||
numAttachments: 0,
|
||||
numMessages: 1,
|
||||
@@ -43,7 +43,7 @@ extension Conversation {
|
||||
senders: senders,
|
||||
size: 1_024,
|
||||
subject: .notUsed,
|
||||
time: 1622548800,
|
||||
time: 1_622_548_800,
|
||||
avatar: .init(text: .notUsed, color: .notUsed),
|
||||
hiddenMessagesBanner: nil
|
||||
)
|
||||
|
||||
@@ -34,7 +34,7 @@ extension Message {
|
||||
bccList: bcc,
|
||||
ccList: cc,
|
||||
location: .system(name: .inbox, id: .init(value: 33)),
|
||||
expirationTime: 1625140800,
|
||||
expirationTime: 1_625_140_800,
|
||||
flags: .init(value: 2),
|
||||
isForwarded: true,
|
||||
isReplied: true,
|
||||
@@ -46,7 +46,7 @@ extension Message {
|
||||
snoozedUntil: .none,
|
||||
displaySnoozeReminder: false,
|
||||
subject: .notUsed,
|
||||
time: 1622548800,
|
||||
time: 1_622_548_800,
|
||||
toList: to,
|
||||
unread: true,
|
||||
customLabels: [],
|
||||
|
||||
@@ -46,18 +46,19 @@ final class ShareViewController: UINavigationController {
|
||||
}
|
||||
|
||||
private func setUpBindings(observing model: ShareScreenModel) {
|
||||
model.$alert.sink { [weak self] message in
|
||||
guard let self else { return }
|
||||
model.$alert
|
||||
.sink { [weak self] message in
|
||||
guard let self else { return }
|
||||
|
||||
if presentedViewController != nil {
|
||||
dismiss(animated: true)
|
||||
}
|
||||
if presentedViewController != nil {
|
||||
dismiss(animated: true)
|
||||
}
|
||||
|
||||
if let message {
|
||||
let alert = UIAlertController(title: message, message: nil, preferredStyle: .alert)
|
||||
present(alert, animated: true)
|
||||
if let message {
|
||||
let alert = UIAlertController(title: message, message: nil, preferredStyle: .alert)
|
||||
present(alert, animated: true)
|
||||
}
|
||||
}
|
||||
}
|
||||
.store(in: &cancellables)
|
||||
.store(in: &cancellables)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -74,9 +74,10 @@ final class DraftStubWriterTests {
|
||||
|
||||
@Test
|
||||
func movesNonInlineAttachmentsToTemporaryDirectoryBeforeAdding() async throws {
|
||||
let sourceURLs = (0..<3).map { index in
|
||||
attachmentSourceDir.appending(path: "data-\(index).txt")
|
||||
}
|
||||
let sourceURLs = (0..<3)
|
||||
.map { index in
|
||||
attachmentSourceDir.appending(path: "data-\(index).txt")
|
||||
}
|
||||
|
||||
let sharedContent = SharedContent(
|
||||
subject: nil,
|
||||
@@ -100,9 +101,10 @@ final class DraftStubWriterTests {
|
||||
|
||||
@Test
|
||||
func movesInlineAttachmentsToTemporaryDirectoryBeforeAdding() async throws {
|
||||
let sourceURLs = (0..<3).map { index in
|
||||
attachmentSourceDir.appending(path: "image-\(index).png")
|
||||
}
|
||||
let sourceURLs = (0..<3)
|
||||
.map { index in
|
||||
attachmentSourceDir.appending(path: "image-\(index).png")
|
||||
}
|
||||
|
||||
let sharedContent = SharedContent(
|
||||
subject: nil,
|
||||
@@ -126,9 +128,10 @@ final class DraftStubWriterTests {
|
||||
|
||||
@Test
|
||||
func extractsImagesFromScreenshotPlists() async throws {
|
||||
let sourceURLs = (0..<3).map { index in
|
||||
attachmentSourceDir.appending(path: "image-\(index).png")
|
||||
}
|
||||
let sourceURLs = (0..<3)
|
||||
.map { index in
|
||||
attachmentSourceDir.appending(path: "image-\(index).png")
|
||||
}
|
||||
|
||||
let sharedContent = SharedContent(
|
||||
subject: nil,
|
||||
|
||||
@@ -111,10 +111,11 @@ final class SharedItemsParserTests {
|
||||
]
|
||||
|
||||
if fromWithinPage {
|
||||
extensionItem.attachments!.insert(
|
||||
.init(item: "irrelevant" as NSSecureCoding, typeIdentifier: UTType.plainText.identifier),
|
||||
at: 0
|
||||
)
|
||||
extensionItem.attachments!
|
||||
.insert(
|
||||
.init(item: "irrelevant" as NSSecureCoding, typeIdentifier: UTType.plainText.identifier),
|
||||
at: 0
|
||||
)
|
||||
}
|
||||
|
||||
return [extensionItem]
|
||||
|
||||
@@ -21,20 +21,21 @@ import UniformTypeIdentifiers
|
||||
|
||||
enum TestDataFactory {
|
||||
static func makeItemProviders(types: [UTType], count: UInt) -> [NSItemProvider] {
|
||||
(0..<count).map { index in
|
||||
let itemProvider = NSItemProvider()
|
||||
(0..<count)
|
||||
.map { index in
|
||||
let itemProvider = NSItemProvider()
|
||||
|
||||
for type in types {
|
||||
let url = URL(fileURLWithPath: "attachments/\(index)-\(type.identifier)")
|
||||
for type in types {
|
||||
let url = URL(fileURLWithPath: "attachments/\(index)-\(type.identifier)")
|
||||
|
||||
itemProvider.registerFileRepresentation(for: type) { completion in
|
||||
completion(url, true, nil)
|
||||
return nil
|
||||
itemProvider.registerFileRepresentation(for: type) { completion in
|
||||
completion(url, true, nil)
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return itemProvider
|
||||
}
|
||||
return itemProvider
|
||||
}
|
||||
}
|
||||
|
||||
static func stubShortLivedData(in urls: [URL]) throws -> [NSItemProvider] {
|
||||
|
||||
Reference in New Issue
Block a user