Compare commits

...

4 Commits

Author SHA1 Message Date
Shadowfacts 5b03e0cf12
Fix follow notifications not showing names for users without explicit
display names
2020-09-09 18:45:38 -04:00
Shadowfacts 7c4bbfd730
Improve compose posting error messages 2020-09-09 18:33:59 -04:00
Shadowfacts e19a6528ad
Improve gallery expand animation
Use spring timing, slide in top/bottom controls
2020-09-08 23:41:15 -04:00
Shadowfacts f5110c773a
Tweak default font sizes 2020-09-07 18:49:25 -04:00
13 changed files with 99 additions and 49 deletions

View File

@ -50,22 +50,22 @@ public class Client {
let task = session.dataTask(with: request) { data, response, error in
if let error = error {
completion(.failure(error))
completion(.failure(.networkError(error)))
return
}
guard let data = data,
let response = response as? HTTPURLResponse else {
completion(.failure(Error.invalidResponse))
completion(.failure(.invalidResponse))
return
}
guard response.statusCode == 200 else {
let mastodonError = try? self.decoder.decode(MastodonError.self, from: data)
let error = mastodonError.flatMap { Error.mastodonError($0.description) } ?? Error.unknownError
let error: Error = mastodonError.flatMap { .mastodonError($0.description) } ?? .unexpectedStatus(response.statusCode)
completion(.failure(error))
return
}
guard let result = try? self.decoder.decode(Result.self, from: data) else {
completion(.failure(Error.invalidModel))
completion(.failure(.invalidModel))
return
}
let pagination = response.allHeaderFields["Link"].flatMap { $0 as? String }.flatMap(Pagination.init)
@ -315,7 +315,8 @@ public class Client {
extension Client {
public enum Error: LocalizedError {
case unknownError
case networkError(Swift.Error)
case unexpectedStatus(Int)
case invalidRequest
case invalidResponse
case invalidModel
@ -323,8 +324,13 @@ extension Client {
public var localizedDescription: String {
switch self {
case .unknownError:
return "Unknown Error"
case .networkError(let error):
return "Network Error: \(error.localizedDescription)"
// todo: support more status codes
case .unexpectedStatus(413):
return "HTTP 413: Payload Too Large"
case .unexpectedStatus(let code):
return "HTTP Code \(code)"
case .invalidRequest:
return "Invalid Request"
case .invalidResponse:

View File

@ -10,5 +10,5 @@ import Foundation
public enum Response<Result: Decodable> {
case success(Result, Pagination?)
case failure(Error)
case failure(Client.Error)
}

View File

@ -22,16 +22,16 @@ public class InstanceSelector {
let request = URLRequest(url: url)
let task = URLSession.shared.dataTask(with: request) { (data, response, error) in
if let error = error {
completion(.failure(error))
completion(.failure(.networkError(error)))
return
}
guard let data = data,
let response = response as? HTTPURLResponse else {
completion(.failure(Client.Error.invalidResponse))
completion(.failure(.invalidResponse))
return
}
guard response.statusCode == 200 else {
completion(.failure(Client.Error.unknownError))
completion(.failure(.unexpectedStatus(response.statusCode)))
return
}
guard let result = try? decoder.decode([Instance].self, from: data) else {

View File

@ -27,6 +27,10 @@ class GalleryViewController: UIPageViewController, UIPageViewControllerDataSourc
}
var animationSourceView: UIImageView? { sourceViews[currentIndex] }
var largeImageController: LargeImageViewController? {
// use protocol because page controllers may be loading or non-loading VCs
(pages[currentIndex] as? LargeImageAnimatableViewController)?.largeImageController
}
var animationImage: UIImage? {
if let page = pages[currentIndex] as? LargeImageAnimatableViewController,
let image = page.animationImage {
@ -48,6 +52,9 @@ class GalleryViewController: UIPageViewController, UIPageViewControllerDataSourc
override var prefersStatusBarHidden: Bool {
return true
}
override var preferredStatusBarUpdateAnimation: UIStatusBarAnimation {
return .none
}
override var childForHomeIndicatorAutoHidden: UIViewController? {
return viewControllers?.first
}

View File

@ -12,14 +12,14 @@ import Combine
struct ComposeView: View {
@ObservedObject var draft: Draft
@EnvironmentObject var mastodonController: MastodonController
@EnvironmentObject var uiState: ComposeUIState
@State var isPosting = false
@State var postProgress: Double = 0
@State var postTotalProgress: Double = 0
@State var isShowingPostErrorAlert = false
@State var postError: Error?
@State private var isPosting = false
@State private var postProgress: Double = 0
@State private var postTotalProgress: Double = 0
@State private var isShowingPostErrorAlert = false
@State private var postError: PostError?
private let stackPadding: CGFloat = 8
@ -191,6 +191,9 @@ struct ComposeView: View {
case let .failure(error):
self.isShowingPostErrorAlert = true
self.postError = error
self.postProgress = 0
self.postTotalProgress = 0
self.isPosting = false
case let .success(uploadedAttachments):
let request = Client.createStatus(text: draft.text,
@ -222,7 +225,7 @@ struct ComposeView: View {
}
}
private func uploadAttachments(_ completion: @escaping (Result<[Attachment], Error>) -> Void) {
private func uploadAttachments(_ completion: @escaping (Result<[Attachment], AttachmentUploadError>) -> Void) {
let group = DispatchGroup()
var anyFailed = false
@ -239,13 +242,13 @@ struct ComposeView: View {
let formAttachment = FormAttachment(mimeType: mimeType, data: data, fileName: "file")
let request = Client.upload(attachment: formAttachment, description: compAttachment.attachmentDescription)
self.mastodonController.run(request) { (response) in
postProgress += 1
switch response {
case let .failure(error):
uploadedAttachments[index] = .failure(error)
anyFailed = true
case let .success(attachment, _):
postProgress += 1
uploadedAttachments[index] = .success(attachment)
}
@ -274,14 +277,37 @@ struct ComposeView: View {
}
}
fileprivate struct AttachmentUploadError: LocalizedError {
fileprivate protocol PostError: LocalizedError {}
extension PostError {
var localizedDescription: String {
if let self = self as? Client.Error {
return self.localizedDescription
} else if let self = self as? AttachmentUploadError {
return self.localizedDescription
} else {
return "Unknown Error"
}
}
}
extension Client.Error: PostError {}
fileprivate struct AttachmentUploadError: PostError {
let errors: [Error?]
var localizedDescription: String {
return errors.enumerated().compactMap { (index, error) -> String? in
guard let error = error else { return nil }
return "Attachment \(index + 1): \(error.localizedDescription)"
}.joined(separator: ", ")
let description: String
// need to downcast to use more specific localizedDescription impl from Pachyderm
if let error = error as? Client.Error {
description = error.localizedDescription
} else {
description = error.localizedDescription
}
return "Attachment \(index + 1): \(description)"
}.joined(separator: ",\n")
}
}

View File

@ -13,6 +13,7 @@ class LargeImageViewController: UIViewController, UIScrollViewDelegate, LargeIma
typealias ContentView = UIView & LargeImageContentView
weak var animationSourceView: UIImageView?
var largeImageController: LargeImageViewController? { self }
var animationImage: UIImage? { contentView.animationImage }
var animationGifData: Data? { contentView.animationGifData }
var dismissInteractionController: LargeImageInteractionController?
@ -49,6 +50,9 @@ class LargeImageViewController: UIViewController, UIScrollViewDelegate, LargeIma
override var prefersStatusBarHidden: Bool {
return true
}
override var preferredStatusBarUpdateAnimation: UIStatusBarAnimation {
return .none
}
override var prefersHomeIndicatorAutoHidden: Bool {
return !controlsVisible

View File

@ -36,6 +36,7 @@ class LoadingLargeImageViewController: UIViewController, LargeImageAnimatableVie
var shrinkGestureEnabled = true
weak var animationSourceView: UIImageView?
var largeImageController: LargeImageViewController? { largeImageVC }
var animationImage: UIImage? { largeImageVC?.animationImage ?? animationSourceView?.image }
var animationGifData: Data? { largeImageVC?.animationGifData }
var dismissInteractionController: LargeImageInteractionController?
@ -43,6 +44,9 @@ class LoadingLargeImageViewController: UIViewController, LargeImageAnimatableVie
override var prefersStatusBarHidden: Bool {
return true
}
override var preferredStatusBarUpdateAnimation: UIStatusBarAnimation {
return .none
}
override var childForHomeIndicatorAutoHidden: UIViewController? {
return largeImageVC
}

View File

@ -11,6 +11,7 @@ import Gifu
protocol LargeImageAnimatableViewController: UIViewController {
var animationSourceView: UIImageView? { get }
var largeImageController: LargeImageViewController? { get }
var animationImage: UIImage? { get }
var animationGifData: Data? { get }
var dismissInteractionController: LargeImageInteractionController? { get }
@ -37,7 +38,7 @@ extension LargeImageAnimatableViewController {
class LargeImageExpandAnimationController: NSObject, UIViewControllerAnimatedTransitioning {
func transitionDuration(using transitionContext: UIViewControllerContextTransitioning?) -> TimeInterval {
return 0.2
return 0.5
}
func animateTransition(using transitionContext: UIViewControllerContextTransitioning) {
@ -47,7 +48,7 @@ class LargeImageExpandAnimationController: NSObject, UIViewControllerAnimatedTra
}
let containerView = transitionContext.containerView
containerView.addSubview(toVC.view)
let finalVCFrame = transitionContext.finalFrame(for: toVC)
guard let sourceView = toVC.animationSourceView,
@ -58,8 +59,11 @@ class LargeImageExpandAnimationController: NSObject, UIViewControllerAnimatedTra
return
}
// use alpha, becaus isHidden makes stack views re-layout
// use alpha, because isHidden makes stack views re-layout
sourceView.alpha = 0
toVC.view.alpha = 0
toVC.largeImageController?.contentView.isHidden = true
toVC.largeImageController?.setControlsVisible(false, animated: false)
var finalFrameSize = finalVCFrame.inset(by: fromVC.view.safeAreaInsets).size
let newWidth = finalFrameSize.width / image.size.width
@ -81,21 +85,17 @@ class LargeImageExpandAnimationController: NSObject, UIViewControllerAnimatedTra
imageView.layer.maskedCorners = sourceView.layer.maskedCorners
imageView.layer.masksToBounds = true
let blackView = UIView(frame: finalVCFrame)
blackView.backgroundColor = .black
blackView.alpha = 0
containerView.addSubview(blackView)
containerView.addSubview(toVC.view)
containerView.addSubview(imageView)
toVC.view.isHidden = true
let duration = transitionDuration(using: transitionContext)
UIView.animate(withDuration: duration, animations: {
let velocity = 1 / CGFloat(duration)
UIView.animate(withDuration: duration, delay: 0, usingSpringWithDamping: 0.65, initialSpringVelocity: velocity, options: []) {
imageView.frame = finalFrame
imageView.layer.cornerRadius = 0
blackView.alpha = 1
}, completion: { _ in
toVC.view.alpha = 1
toVC.largeImageController?.setControlsVisible(true, animated: false)
} completion: { (_) in
// This shouldn't be necessary. I believe it's a workaround for using a XIB
// for the large image VC. Without this, the final frame of the large image VC
// is not set to the propper rect (it uses the frame of the preview device
@ -103,15 +103,14 @@ class LargeImageExpandAnimationController: NSObject, UIViewControllerAnimatedTra
// (or UIKit does layout differently when loading the view) and this is not necessary.
toVC.view.frame = finalVCFrame
toVC.view.isHidden = false
toVC.largeImageController?.contentView.isHidden = false
fromVC.view.isHidden = false
blackView.removeFromSuperview()
imageView.removeFromSuperview()
sourceView.alpha = 1
transitionContext.completeTransition(!transitionContext.transitionWasCancelled)
})
}
}
}

View File

@ -77,11 +77,11 @@ class FollowNotificationGroupTableViewCell: UITableViewCell {
let peopleStr: String
switch people.count {
case 1:
peopleStr = people.first!.displayName
peopleStr = people.first!.displayOrUserName
case 2:
peopleStr = people.first!.displayName + " and " + people.last!.displayName
peopleStr = people.first!.displayOrUserName + " and " + people.last!.displayOrUserName
default:
peopleStr = people.dropLast().map { $0.displayName }.joined(separator: ", ") + ", and " + people.last!.displayName
peopleStr = people.dropLast().map { $0.displayOrUserName }.joined(separator: ", ") + ", and " + people.last!.displayOrUserName
}
actionLabel.text = "Followed by \(peopleStr)"

View File

@ -35,7 +35,7 @@ class ConversationMainStatusTableViewCell: BaseStatusTableViewCell {
profileAccessibilityElement.accessibilityFrameInContainerSpace = profileDetailContainerView.convert(profileDetailContainerView.frame, to: self)
accessibilityElements = [profileAccessibilityElement!, contentWarningLabel!, collapseButton!, contentTextView!, totalFavoritesButton!, totalReblogsButton!, timestampAndClientLabel!, replyButton!, favoriteButton!, reblogButton!, moreButton!]
contentTextView.defaultFont = .systemFont(ofSize: 20)
contentTextView.defaultFont = .systemFont(ofSize: 18)
}
override func updateUI(statusID: String, state: StatusState) {

View File

@ -48,6 +48,8 @@ class TimelineStatusTableViewCell: BaseStatusTableViewCell {
// todo: double check this on RTL layouts
replyButton.imageView!.leadingAnchor.constraint(equalTo: contentTextView.leadingAnchor).isActive = true
contentTextView.defaultFont = .systemFont(ofSize: 16)
}
override func createObserversIfNecessary() {

View File

@ -1,9 +1,9 @@
<?xml version="1.0" encoding="UTF-8"?>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.XIB" version="3.0" toolsVersion="17147" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES">
<document type="com.apple.InterfaceBuilder3.CocoaTouch.XIB" version="3.0" toolsVersion="17154" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES">
<device id="retina4_7" orientation="portrait" appearance="light"/>
<dependencies>
<deployment identifier="iOS"/>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="17120"/>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="17124"/>
<capability name="Safe area layout guides" minToolsVersion="9.0"/>
<capability name="System colors in document resources" minToolsVersion="11.0"/>
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
@ -111,7 +111,7 @@
<rect key="frame" x="0.0" y="83" width="277" height="86.5"/>
<string key="text">Lorem ipsum dolor sit er elit lamet, consectetaur cillium adipisicing pecu, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. Nam liber te conscient to factor tum poen legum odioque civiuda.</string>
<color key="textColor" systemColor="labelColor"/>
<fontDescription key="fontDescription" type="system" pointSize="17"/>
<fontDescription key="fontDescription" type="system" pointSize="16"/>
<textInputTraits key="textInputTraits" autocapitalizationType="sentences"/>
</textView>
<view hidden="YES" contentMode="scaleToFill" verticalCompressionResistancePriority="1" translatesAutoresizingMaskIntoConstraints="NO" id="nbq-yr-2mA" customClass="AttachmentsContainerView" customModule="Tusker" customModuleProvider="target">
@ -240,7 +240,7 @@
</view>
</objects>
<resources>
<image name="arrowshape.turn.up.left.fill" catalog="system" width="128" height="104"/>
<image name="arrowshape.turn.up.left.fill" catalog="system" width="128" height="106"/>
<image name="bubble.left.and.bubble.right" catalog="system" width="128" height="96"/>
<image name="chevron.down" catalog="system" width="128" height="72"/>
<image name="ellipsis" catalog="system" width="128" height="37"/>

View File

@ -21,6 +21,8 @@ struct WrappedProgressView: UIViewRepresentable {
func updateUIView(_ uiView: UIProgressView, context: Context) {
if total > 0 {
uiView.setProgress(Float(value / total), animated: true)
} else {
uiView.setProgress(0, animated: true)
}
}
}