Skip to content

Commit

Permalink
Promise-free client (#1171)
Browse files Browse the repository at this point in the history
* Bump minimum SDKs to iOS 15 and macOS 12

* Bump Nuke to 11.6.4

* Bump Nuke to 12.1.6

* Bump Stencil to 0.15.1

* Port ForumsClient to async/await

* Drop PromiseKit dependency from AwfulCore and take apart ForumsURLSession

The Awful app target was implicitly relying on AwfulCore's depending on PromiseKit. Now the app dependency is explicit.

* Bump Xcode version for CI
  • Loading branch information
nolanw authored Dec 10, 2023
1 parent 5979303 commit bb05837
Show file tree
Hide file tree
Showing 66 changed files with 1,829 additions and 2,031 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ jobs:
test:
runs-on: macos-13
env:
DEVELOPER_DIR: /Applications/Xcode_14.3.1.app/Contents/Developer
DEVELOPER_DIR: /Applications/Xcode_15.0.1.app/Contents/Developer
steps:
- uses: actions/checkout@v2
- name: xcodebuild test
Expand Down
8 changes: 4 additions & 4 deletions App/Composition/CompositionInputAccessoryView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -216,12 +216,12 @@ private final class KeyboardButton: SmilieButton {
override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) {
super.traitCollectionDidChange(previousTraitCollection)

guard previousTraitCollection?.userInterfaceIdiom != traitCollection.userInterfaceIdiom else { return }
if previousTraitCollection?.userInterfaceIdiom == traitCollection.userInterfaceIdiom { return }

if traitCollection.userInterfaceIdiom == .phone {
contentEdgeInsets = UIEdgeInsets(top: 0, left: 0, bottom: 5, right: 0)
configuration?.contentInsets = .init(top: 0, leading: 0, bottom: 5, trailing: 0)
} else {
contentEdgeInsets = UIEdgeInsets()
configuration?.contentInsets = .zero
}
}
}
4 changes: 2 additions & 2 deletions App/Composition/CompositionMenuTree.swift
Original file line number Diff line number Diff line change
Expand Up @@ -74,8 +74,8 @@ final class CompositionMenuTree: NSObject {
func showImagePicker(_ sourceType: UIImagePickerController.SourceType) {
let picker = UIImagePickerController()
picker.sourceType = sourceType
let mediaType : NSString = kUTTypeImage as NSString
picker.mediaTypes = [mediaType as String]
let mediaType = UTType.image
picker.mediaTypes = [mediaType.identifier]
picker.allowsEditing = false
picker.delegate = self
if UIDevice.current.userInterfaceIdiom == .pad && sourceType == .photoLibrary {
Expand Down
4 changes: 2 additions & 2 deletions App/Extensions/FLAnimatedImageView+Nuke.swift
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,11 @@
// Copyright 2019 Awful Contributors. CC BY-NC-SA 3.0 US https://github.com/Awful/Awful.app

import FLAnimatedImage
import Nuke
import NukeExtensions

extension FLAnimatedImageView {
open override func nuke_display(
image: PlatformImage?,
image: UIImage?,
data: Data?
) {
self.image = image
Expand Down
34 changes: 0 additions & 34 deletions App/Extensions/Photos+.swift
Original file line number Diff line number Diff line change
Expand Up @@ -9,37 +9,3 @@ extension PHAsset {
return fetchAssets(withLocalIdentifiers: [identifier], options: nil).firstObject
}
}

extension PHPhotoLibrary {

enum AccessLevel {
case addOnly, readWrite

@available(iOS 14, *)
fileprivate var phAccessLevel: PHAccessLevel {
switch self {
case .addOnly: return .addOnly
case .readWrite: return .readWrite
}
}
}

class func authorizationStatus(for accessLevel: AccessLevel) -> PHAuthorizationStatus {
if #available(iOS 14, *) {
return authorizationStatus(for: accessLevel.phAccessLevel)
} else {
return authorizationStatus()
}
}

class func requestAuthorization(
for accessLevel: AccessLevel,
handler: @escaping (PHAuthorizationStatus) -> Void
) {
if #available(iOS 14, *) {
requestAuthorization(for: accessLevel.phAccessLevel, handler: handler)
} else {
requestAuthorization(handler)
}
}
}
17 changes: 17 additions & 0 deletions App/Extensions/Task+sleepUnits.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
// Task+sleepUnits.swift
//
// Copyright 2023 Awful Contributors. CC BY-NC-SA 3.0 US https://github.com/Awful/Awful.app

import Foundation

extension Task where Success == Never, Failure == Never {
/// Suspends the current task for the given duration.
static func sleep(for duration: Measurement<UnitDuration>) async throws {
try await sleep(nanoseconds: UInt64(duration.converted(to: .nanoseconds).value))
}

/// Suspends the current task for the given duration.
static func sleep(timeInterval: TimeInterval) async throws {
try await sleep(nanoseconds: UInt64(timeInterval * TimeInterval(NSEC_PER_SEC)))
}
}
21 changes: 0 additions & 21 deletions App/Extensions/UIFont+MonospacedDigits.swift

This file was deleted.

4 changes: 2 additions & 2 deletions App/Extensions/UIKit.swift
Original file line number Diff line number Diff line change
Expand Up @@ -262,7 +262,7 @@ extension UISplitViewController {
/// Animates the primary view controller into view if it is not already visible.
func showPrimaryViewController() {
// The docs say that displayMode is "ignored" when we're collapsed. I'm not really sure what that means so let's bail early.
guard !isCollapsed, displayMode == .primaryHidden else { return }
guard !isCollapsed, displayMode == .secondaryOnly else { return }
let button = displayModeButtonItem
guard let target = button.target as? NSObject else { return }
target.perform(button.action, with: nil)
Expand All @@ -271,7 +271,7 @@ extension UISplitViewController {
/// Animates the primary view controller out of view if it is currently visible in an overlay.
func hidePrimaryViewController() {
// The docs say that displayMode is "ignored" when we're collapsed. I'm not really sure what that means so let's bail early.
guard !isCollapsed, displayMode == .primaryOverlay else { return }
guard !isCollapsed, displayMode == .oneOverSecondary else { return }
let button = displayModeButtonItem
guard let target = button.target as? NSObject else { return }
target.perform(button.action, with: nil)
Expand Down
18 changes: 18 additions & 0 deletions App/Extensions/UIViewController+async.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
// UIViewController+async.swift
//
// Copyright 2023 Awful Contributors. CC BY-NC-SA 3.0 US https://github.com/Awful/Awful.app

import UIKit

extension UIViewController {
/// Dismisses the view controller that was presented modally by the view controller.
func dismiss(
animated: Bool
) async {
await withCheckedContinuation { continuation in
dismiss(animated: animated) {
continuation.resume()
}
}
}
}
18 changes: 9 additions & 9 deletions App/Main/RootViewControllerStack.swift
Original file line number Diff line number Diff line change
Expand Up @@ -122,9 +122,9 @@ final class RootViewControllerStack: NSObject, AwfulSplitViewControllerDelegate
if UserDefaults.standard.hideSidebarInLandscape {
switch splitViewController.displayMode {
case .primaryOverlay, .allVisible:
splitViewController.preferredDisplayMode = .primaryOverlay
splitViewController.preferredDisplayMode = .oneOverSecondary
case .primaryHidden:
splitViewController.preferredDisplayMode = .primaryHidden
splitViewController.preferredDisplayMode = .secondaryOnly
default:
fatalError("unexpected display mode \(splitViewController.displayMode)")
}
Expand Down Expand Up @@ -164,13 +164,13 @@ final class RootViewControllerStack: NSObject, AwfulSplitViewControllerDelegate
let isPortrait = splitViewController.view.frame.width < splitViewController.view.frame.height
if !splitViewController.isCollapsed {
// One possibility is restoring in portrait orientation with the sidebar always visible.
if isPortrait && splitViewController.displayMode == .allVisible {
splitViewController.preferredDisplayMode = .primaryHidden
if isPortrait && splitViewController.displayMode == .oneBesideSecondary {
splitViewController.preferredDisplayMode = .secondaryOnly
}

// Another possibility is restoring in landscape orientation with the sidebar always hidden, and no button to show it.
if !isPortrait && splitViewController.displayMode == .primaryHidden && splitViewController.preferredDisplayMode == .automatic {
splitViewController.preferredDisplayMode = .allVisible
if !isPortrait && splitViewController.displayMode == .secondaryOnly && splitViewController.preferredDisplayMode == .automatic {
splitViewController.preferredDisplayMode = .oneBesideSecondary
splitViewController.preferredDisplayMode = .automatic
}
}
Expand All @@ -179,7 +179,7 @@ final class RootViewControllerStack: NSObject, AwfulSplitViewControllerDelegate
guard let self = self else { return }
if let detail = self.detailNavigationController?.viewControllers.first {
// Our UISplitViewControllerDelegate methods get called *before* we're done restoring state, so the "show sidebar" button item doesn't get put in place properly. Fix that here.
if self.splitViewController.displayMode != .allVisible {
if self.splitViewController.displayMode != .oneBesideSecondary {
detail.navigationItem.leftBarButtonItem = self.backBarButtonItem
}
}
Expand Down Expand Up @@ -287,7 +287,7 @@ extension RootViewControllerStack {
if splitViewController.isCollapsed {
primaryNavigationController.pushViewController(viewController, animated: true)
} else {
if splitViewController.displayMode != .allVisible {
if splitViewController.displayMode != .oneBesideSecondary {
viewController.navigationItem.leftBarButtonItem = backBarButtonItem
}

Expand Down Expand Up @@ -315,7 +315,7 @@ extension RootViewControllerStack {
let root = detailNav.viewControllers.first
{
let displayMode = self.splitViewController.displayMode
root.navigationItem.leftBarButtonItem = displayMode == .allVisible ? nil : self.backBarButtonItem
root.navigationItem.leftBarButtonItem = displayMode == .oneBesideSecondary ? nil : self.backBarButtonItem
}
})
}
Expand Down
103 changes: 49 additions & 54 deletions App/Posts/ReplyWorkspace.swift
Original file line number Diff line number Diff line change
Expand Up @@ -37,27 +37,10 @@ final class ReplyWorkspace: NSObject {
}

/// Constructs a workspace for editing a reply.
convenience init(post: Post) {
convenience init(post: Post, bbcode: String) {
let draft = EditReplyDraft(post: post)
self.init(draft: draft, didRestoreWithRestorationIdentifier: nil)

let progressView = MRProgressOverlayView.showOverlayAdded(to: viewController.view, animated: false)
progressView?.titleLabelText = "Reading post…"

ForumsClient.shared.findBBcodeContents(of: post)
.done { [weak self] bbcode in
self?.compositionViewController.textView.text = bbcode
}
.catch { [weak self] error in
guard let self = self else { return }
if self.compositionViewController.visible {
let alert = UIAlertController(title: "Couldn't Find BBcode", error: error)
self.viewController.present(alert, animated: true)
}
}
.finally {
progressView?.dismiss(true)
}
bbcodeForNewlyCreatedCompositionViewController = bbcode
}

/// A nil restorationIdentifier implies that we were not created by UIKit state restoration.
Expand Down Expand Up @@ -96,6 +79,9 @@ final class ReplyWorkspace: NSObject {

private var draftTitleObserver: NSKeyValueObservation?

// compositionViewController isn't available at init time, but sometimes we already know the bbcode.
private var bbcodeForNewlyCreatedCompositionViewController: String?

/*
Dealing with compositionViewController is annoyingly complicated. Ideally it'd be a constant ivar, so we could either restore state by passing it in via init() or make a new one if we're not restoring state.
Unfortunately, any compositionViewController that we preserve in encodeRestorableStateWithCoder() is not yet available in objectWithRestorationIdentifierPath(_:coder:); it only becomes available in decodeRestorableStateWithCoder().
Expand Down Expand Up @@ -284,42 +270,41 @@ final class ReplyWorkspace: NSObject {
if compositionViewController == nil {
compositionViewController = CompositionViewController()
compositionViewController.restorationIdentifier = "\(self.restorationIdentifier) Reply composition"

if let bbcodeForNewlyCreatedCompositionViewController {
compositionViewController.textView.text = bbcodeForNewlyCreatedCompositionViewController
self.bbcodeForNewlyCreatedCompositionViewController = nil
}
}
}

/// Append a quoted post to the reply.
func quotePost(_ post: Post, completion: @escaping (Error?) -> Void) {
@MainActor
func quotePost(_ post: Post) async throws {
createCompositionViewController()

ForumsClient.shared.quoteBBcodeContents(of: post)
.done { [weak self] bbcode in
guard let self = self else { return }

let textView = self.compositionViewController.textView
var replacement = bbcode
let selectedRange = textView.selectedTextRange ?? textView.textRange(from: textView.endOfDocument, to: textView.endOfDocument)!

// Yep. This is just a delight.
let precedingOffset = max(-2, textView.offset(from: selectedRange.start, to: textView.beginningOfDocument))
if
precedingOffset < 0,
let precedingStart = textView.position(from: selectedRange.start, offset: precedingOffset),
let precedingRange = textView.textRange(from: precedingStart, to: selectedRange.start),
let preceding = textView.text(in: precedingRange),
preceding != "\n\n"
{
if preceding.hasSuffix("\n") {
replacement = "\n" + replacement
} else {
replacement = "\n\n" + replacement
}
}

textView.replaceSelection(with: replacement)

completion(nil)
let bbcode = try await ForumsClient.shared.quoteBBcodeContents(of: post)
let textView = compositionViewController.textView
var replacement = bbcode
let selectedRange = textView.selectedTextRange ?? textView.textRange(from: textView.endOfDocument, to: textView.endOfDocument)!

// Yep. This is just a delight.
let precedingOffset = max(-2, textView.offset(from: selectedRange.start, to: textView.beginningOfDocument))
if
precedingOffset < 0,
let precedingStart = textView.position(from: selectedRange.start, offset: precedingOffset),
let precedingRange = textView.textRange(from: precedingStart, to: selectedRange.start),
let preceding = textView.text(in: precedingRange),
preceding != "\n\n"
{
if preceding.hasSuffix("\n") {
replacement = "\n" + replacement
} else {
replacement = "\n\n" + replacement
}
.catch(completion)
}

textView.replaceSelection(with: replacement)
}
}

Expand Down Expand Up @@ -455,9 +440,14 @@ extension NewReplyDraft: SubmittableDraft {
if let error = error {
completion(error)
} else {
ForumsClient.shared.reply(to: self.thread, bbcode: plainText ?? "")
.done { _ in completion(nil) }
.catch { completion($0) }
Task { @MainActor in
do {
_ = try await ForumsClient.shared.reply(to: thread, bbcode: plainText ?? "")
completion(nil)
} catch {
completion(error)
}
}
}
}
}
Expand All @@ -469,9 +459,14 @@ extension EditReplyDraft: SubmittableDraft {
if let error = error {
completion(error)
} else {
ForumsClient.shared.edit(self.post, bbcode: plainText ?? "")
.done { completion(nil) }
.catch { completion($0) }
Task { @MainActor in
do {
try await ForumsClient.shared.edit(post, bbcode: plainText ?? "")
completion(nil)
} catch {
completion(error)
}
}
}
}
}
Expand Down
Loading

0 comments on commit bb05837

Please sign in to comment.