Skip to content

Commit

Permalink
Merge release/1.115.0 into main
Browse files Browse the repository at this point in the history
  • Loading branch information
daxmobile authored Nov 22, 2024
2 parents 1e8c6e1 + 55246c9 commit 6e79c08
Show file tree
Hide file tree
Showing 6 changed files with 192 additions and 14 deletions.
2 changes: 1 addition & 1 deletion Configuration/BuildNumber.xcconfig
Original file line number Diff line number Diff line change
@@ -1 +1 @@
CURRENT_PROJECT_VERSION = 312
CURRENT_PROJECT_VERSION = 313
3 changes: 3 additions & 0 deletions DuckDuckGo/Common/Localizables/UserText.swift
Original file line number Diff line number Diff line change
Expand Up @@ -825,6 +825,9 @@ struct UserText {
static let downloadFailed = NSLocalizedString("downloads.error.other", value: "Error", comment: "Short error description when Download failed")
static let downloadBytesLoadedFormat = NSLocalizedString("downloads.bytes.format", value: "%@ of %@", comment: "Number of bytes out of total bytes downloaded (1Mb of 2Mb)")
static let downloadSpeedFormat = NSLocalizedString("downloads.speed.format", value: "%@/s", comment: "Download speed format (1Mb/sec)")
static let downloadsErrorMessage = NSLocalizedString("downloads.error.message.for.specific.os", value: "The download failed because of a known issue on macOS 14.7.1 and 15.0.1. Update to macOS 15.1 and try downloading again.", comment: "This error message will appear in an error banner when users cannot download files on macOS 14.7.1 or 15.0.1")
static let downloadsErrorSandboxCallToAction = NSLocalizedString("downloads.error.cta.sandbox", value: "How To Update", comment: "Call to action for the OS specific downloads issue")
static let downloadsErrorNonSandboxCallToAction = NSLocalizedString("downloads.error.cta.non-sandbox", value: "Open Settings", comment: "Call to action for the OS specific downloads issue")

static let cancelDownloadToolTip = NSLocalizedString("downloads.tooltip.cancel", value: "Cancel Download", comment: "Mouse-over tooltip for Cancel Download button")
static let restartDownloadToolTip = NSLocalizedString("downloads.tooltip.restart", value: "Restart Download", comment: "Mouse-over tooltip for Restart Download button")
Expand Down
37 changes: 34 additions & 3 deletions DuckDuckGo/FileDownload/Model/DownloadListViewModel.swift
Original file line number Diff line number Diff line change
Expand Up @@ -27,9 +27,10 @@ final class DownloadListViewModel {
private let fireWindowSession: FireWindowSessionRef?
private let coordinator: DownloadListCoordinator
private var viewModels: [UUID: DownloadViewModel]
private var cancellable: AnyCancellable?
private var cancellables = Set<AnyCancellable>()

@Published private(set) var items: [DownloadViewModel]
@Published private(set) var shouldShowErrorBanner: Bool = false

init(fireWindowSession: FireWindowSessionRef?, coordinator: DownloadListCoordinator = DownloadListCoordinator.shared) {
self.fireWindowSession = fireWindowSession
Expand All @@ -40,9 +41,10 @@ final class DownloadListViewModel {
.map(DownloadViewModel.init)
self.items = items
self.viewModels = items.reduce(into: [:]) { $0[$1.id] = $1 }
cancellable = coordinator.updates.receive(on: DispatchQueue.main).sink { [weak self] update in
coordinator.updates.receive(on: DispatchQueue.main).sink { [weak self] update in
self?.handleDownloadsUpdate(of: update.kind, item: update.item)
}
}.store(in: &cancellables)
self.setupErrorBannerBinding()
}

private func handleDownloadsUpdate(of kind: DownloadListCoordinator.UpdateKind, item: DownloadListItem) {
Expand All @@ -65,6 +67,35 @@ final class DownloadListViewModel {
}
}

private func setupErrorBannerBinding() {
$items.flatMap { items in
Publishers.MergeMany(items.map { $0.$state })
}.map { state in
if case .failed(let error) = state {
if error.isNSFileReadUnknownError && self.isAffectedMacOSVersion() {
return true
}
}

return false
}
.removeDuplicates()
.receive(on: DispatchQueue.main)
.sink { [weak self] showError in
self?.shouldShowErrorBanner = showError
}
.store(in: &cancellables)
}

/// macOS 15.0.1 and 14.7.1 have a bug that affects downloads. Apple fixed the issue on macOS 15.1
/// For more information: https://app.asana.com/0/1204006570077678/1208522448255790/f
private func isAffectedMacOSVersion() -> Bool {
let currentVersion = AppVersion.shared.osVersion
let targetVersions = ["15.0.1", "14.7.1"]

return targetVersions.contains(currentVersion)
}

func cleanupInactiveDownloads() {
coordinator.cleanupInactiveDownloads(for: fireWindowSession)
}
Expand Down
12 changes: 12 additions & 0 deletions DuckDuckGo/FileDownload/Model/FileDownloadError.swift
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,18 @@ import Foundation
enum FileDownloadError: Error {
case failedToMoveFileToDownloads
case failedToCompleteDownloadTask(underlyingError: Error?, resumeData: Data?, isRetryable: Bool)

var isNSFileReadUnknownError: Bool {
switch self {
case .failedToMoveFileToDownloads:
return false
case .failedToCompleteDownloadTask(let underlyingError, _, _):
guard let underlyingError else { return false }

let nsError = underlyingError as NSError
return nsError.domain == NSCocoaErrorDomain && nsError.code == NSFileReadUnknownError
}
}
}

extension FileDownloadError: LocalizedError {
Expand Down
114 changes: 105 additions & 9 deletions DuckDuckGo/FileDownload/View/DownloadsViewController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@

import Cocoa
import Combine
import SwiftUI

protocol DownloadsViewControllerDelegate: AnyObject {
func clearDownloadsActionTriggered()
Expand All @@ -34,13 +35,18 @@ final class DownloadsViewController: NSViewController {

private lazy var scrollView = NSScrollView()
private lazy var tableView = NSTableView()
private var hostingViewConstraints: [NSLayoutConstraint] = []
private var tableViewHeightConstraint: NSLayoutConstraint!
private var errorBannerTopAnchorConstraint: NSLayoutConstraint!
private var cellIndexToUnselect: Int?

weak var delegate: DownloadsViewControllerDelegate?

private let separator = NSBox()
private let viewModel: DownloadListViewModel
private var downloadsCancellable: AnyCancellable?
private var errorBannerCancellable: AnyCancellable?
private var errorBannerHostingView: NSHostingView<DownloadsErrorBannerView>?

init(viewModel: DownloadListViewModel) {
self.viewModel = viewModel
Expand Down Expand Up @@ -127,16 +133,31 @@ final class DownloadsViewController: NSViewController {

scrollView.contentView = clipView

let separator = NSBox()
separator.boxType = .separator
separator.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(separator)

setupLayout(separator: separator)
let swiftUIView = DownloadsErrorBannerView(dismiss: { self.dismiss() },
errorType: NSApp.isSandboxed ? .openHelpURL : .openSystemSettings)
let hostingView = NSHostingView(rootView: swiftUIView)
hostingView.translatesAutoresizingMaskIntoConstraints = false
hostingView.isHidden = true
view.addSubview(hostingView)
errorBannerHostingView = hostingView

setupLayout(separator: separator, hostingView: hostingView)
}

private func setupLayout(separator: NSBox) {
private func setupLayout(separator: NSBox, hostingView: NSHostingView<DownloadsErrorBannerView>) {
tableViewHeightConstraint = scrollView.heightAnchor.constraint(equalToConstant: 440)
errorBannerTopAnchorConstraint = scrollView.topAnchor.constraint(equalTo: separator.bottomAnchor, constant: 12)

hostingViewConstraints = [
hostingView.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 12),
hostingView.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -12),
hostingView.topAnchor.constraint(equalTo: separator.bottomAnchor, constant: 12),
scrollView.topAnchor.constraint(equalTo: hostingView.bottomAnchor, constant: 12)
]

NSLayoutConstraint.activate([
titleLabel.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 12),
Expand All @@ -153,19 +174,33 @@ final class DownloadsViewController: NSViewController {
view.trailingAnchor.constraint(equalTo: clearDownloadsButton.trailingAnchor, constant: 11),
clearDownloadsButton.centerYAnchor.constraint(equalTo: openDownloadsFolderButton.centerYAnchor),

view.trailingAnchor.constraint(equalTo: scrollView.trailingAnchor),
view.bottomAnchor.constraint(equalTo: scrollView.bottomAnchor),
scrollView.topAnchor.constraint(equalTo: view.topAnchor, constant: 44),
scrollView.leadingAnchor.constraint(equalTo: view.leadingAnchor),

separator.centerXAnchor.constraint(equalTo: view.centerXAnchor),
separator.widthAnchor.constraint(equalTo: view.widthAnchor, constant: -2),
separator.topAnchor.constraint(equalTo: view.topAnchor, constant: 43),

scrollView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
scrollView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
view.bottomAnchor.constraint(equalTo: scrollView.bottomAnchor, constant: 12),

errorBannerTopAnchorConstraint,
tableViewHeightConstraint
])
}

private func showErrorBanner() {
errorBannerHostingView?.isHidden = false
NSLayoutConstraint.deactivate([errorBannerTopAnchorConstraint])
NSLayoutConstraint.activate(hostingViewConstraints)
view.layoutSubtreeIfNeeded()
}

private func hideErrorBanner() {
errorBannerHostingView?.isHidden = true
NSLayoutConstraint.deactivate(hostingViewConstraints)
NSLayoutConstraint.activate([errorBannerTopAnchorConstraint])
view.layoutSubtreeIfNeeded()
}

override func viewDidLoad() {
super.viewDidLoad()

Expand Down Expand Up @@ -204,6 +239,17 @@ final class DownloadsViewController: NSViewController {
}
}

errorBannerCancellable = viewModel.$shouldShowErrorBanner
.sink { [weak self] shouldShowErrorBanner in
guard let self = self else { return }

if shouldShowErrorBanner {
self.showErrorBanner()
} else {
self.hideErrorBanner()
}
}

for item in viewModel.items {
item.didAppear() // initial table appearance should have no progress animations
}
Expand All @@ -213,6 +259,7 @@ final class DownloadsViewController: NSViewController {

override func viewWillDisappear() {
downloadsCancellable = nil
errorBannerCancellable = nil
}

private func setUpContextMenu() -> NSMenu {
Expand Down Expand Up @@ -384,7 +431,7 @@ extension DownloadsViewController: NSMenuDelegate {
for menuItem in menu.items {
switch menuItem.action {
case #selector(openDownloadAction(_:)),
#selector(revealDownloadAction(_:)):
#selector(revealDownloadAction(_:)):
if case .complete(.some(let url)) = item.state,
FileManager.default.fileExists(atPath: url.path) {
menuItem.isHidden = false
Expand Down Expand Up @@ -479,6 +526,55 @@ extension DownloadsViewController: NSTableViewDataSource, NSTableViewDelegate {

}

enum DownloadsErrorViewType {
case openHelpURL
case openSystemSettings

var errorMessage: String {
return UserText.downloadsErrorMessage
}

var title: String {
switch self {
case .openHelpURL: return UserText.downloadsErrorSandboxCallToAction
case .openSystemSettings: return UserText.downloadsErrorNonSandboxCallToAction
}
}

@MainActor func onAction() {
switch self {
case .openHelpURL:
let updateHelpURL = URL(string: "https://support.apple.com/guide/mac-help/get-macos-updates-and-apps-mh35618/mac")!
WindowControllersManager.shared.show(url: updateHelpURL, source: .ui, newTab: true)
case .openSystemSettings:
let softwareUpdateURL = URL(string: "x-apple.systempreferences:com.apple.Software-Update-Settings.extension")!
NSWorkspace.shared.open(softwareUpdateURL)
}
}
}

struct DownloadsErrorBannerView: View {
var dismiss: () -> Void
let errorType: DownloadsErrorViewType

var body: some View {
HStack {
Image("Clear-Recolorable-16")
Text(errorType.errorMessage)
.font(.body)
Button(errorType.title) {
errorType.onAction()
dismiss()
}
}
.padding()
.background(Color.gray.opacity(0.1))
.cornerRadius(8)
.frame(width: 420)
.frame(minHeight: 84.0)
}
}

#if DEBUG
@available(macOS 14.0, *)
#Preview(traits: DownloadsViewController.preferredContentSize.fixedLayout) { {
Expand Down
38 changes: 37 additions & 1 deletion DuckDuckGo/Localizable.xcstrings
Original file line number Diff line number Diff line change
Expand Up @@ -19452,6 +19452,42 @@
}
}
},
"downloads.error.cta.non-sandbox" : {
"comment" : "Call to action for the OS specific downloads issue",
"extractionState" : "extracted_with_value",
"localizations" : {
"en" : {
"stringUnit" : {
"state" : "new",
"value" : "Open Settings"
}
}
}
},
"downloads.error.cta.sandbox" : {
"comment" : "Call to action for the OS specific downloads issue",
"extractionState" : "extracted_with_value",
"localizations" : {
"en" : {
"stringUnit" : {
"state" : "new",
"value" : "How To Update"
}
}
}
},
"downloads.error.message.for.specific.os" : {
"comment" : "This error message will appear in an error banner when users cannot download files on macOS 14.7.1 or 15.0.1",
"extractionState" : "extracted_with_value",
"localizations" : {
"en" : {
"stringUnit" : {
"state" : "new",
"value" : "The download failed because of a known issue on macOS 14.7.1 and 15.0.1. Update to macOS 15.1 and try downloading again."
}
}
}
},
"downloads.error.move.failed" : {
"comment" : "Short error description when could not move downloaded file to the Downloads folder",
"extractionState" : "extracted_with_value",
Expand Down Expand Up @@ -64570,4 +64606,4 @@
}
},
"version" : "1.0"
}
}

0 comments on commit 6e79c08

Please sign in to comment.