Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Promise-free client #1171

Merged
merged 7 commits into from
Dec 10, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading