Compare commits

..

8 Commits

Author SHA1 Message Date
Shadowfacts 0b6ef6517b
Fix gallery action buttons not being centered in device "ears" on iPhone
XR and 11
2020-09-12 12:01:16 -04:00
Shadowfacts 34a01094f7
Fix gallery expand animation description not starting at correct
position

Safe are insets weren't being taken into account when hiding the
controls, because the toVC had not yet been added to the container view
and thus didn't have anything to receive insets from.
2020-09-12 12:01:16 -04:00
Shadowfacts 95b215c6b5
Add Clear Image Cache option to Advanced prefs 2020-09-12 12:01:16 -04:00
Shadowfacts e21dceb3b3
Tweak gallery spring animation parameters 2020-09-12 12:01:16 -04:00
Shadowfacts 9534f19262
Show BlurHash previews of attachments 2020-09-12 12:01:08 -04:00
Shadowfacts e44ae29775
Improve asset picker opening animation 2020-09-10 23:24:24 -04:00
Shadowfacts a5b30c4243
Update PLCrashReporter 2020-09-10 23:24:14 -04:00
Shadowfacts 479ca23e00
Tweak follow request notification cells 2020-09-10 22:54:01 -04:00
15 changed files with 368 additions and 82 deletions

View File

@ -17,6 +17,7 @@ public class Attachment: Codable {
public let textURL: URL?
public let meta: Metadata?
public let description: String?
public let blurHash: String?
public static func update(_ attachment: Attachment, focus: (Float, Float)?, description: String?) -> Request<Attachment> {
return Request<Attachment>(method: .put, path: "/api/v1/media/\(attachment.id)", body: .formData([
@ -35,6 +36,7 @@ public class Attachment: Codable {
self.textURL = try? container.decode(URL?.self, forKey: .textURL)
self.meta = try? container.decode(Metadata?.self, forKey: .meta)
self.description = try? container.decode(String?.self, forKey: .description)
self.blurHash = try? container.decode(String?.self, forKey: .blurHash)
}
private enum CodingKeys: String, CodingKey {
@ -46,6 +48,7 @@ public class Attachment: Codable {
case textURL = "text_url"
case meta
case description
case blurHash = "blurhash"
}
}
@ -60,7 +63,7 @@ extension Attachment {
}
extension Attachment {
public class Metadata: Codable {
public struct Metadata: Codable {
public let length: String?
public let duration: Float?
public let audioEncoding: String?
@ -91,7 +94,7 @@ extension Attachment {
}
}
public class ImageMetadata: Codable {
public struct ImageMetadata: Codable {
public let width: Int?
public let height: Int?
public let size: String?

View File

@ -169,6 +169,7 @@
D67895BC24671E6D00D4CD9E /* PKDrawing+Render.swift in Sources */ = {isa = PBXBuildFile; fileRef = D67895BB24671E6D00D4CD9E /* PKDrawing+Render.swift */; };
D67895C0246870DE00D4CD9E /* LocalAccountAvatarView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D67895BF246870DE00D4CD9E /* LocalAccountAvatarView.swift */; };
D679C09F215850EF00DA27FE /* XCBActions.swift in Sources */ = {isa = PBXBuildFile; fileRef = D679C09E215850EF00DA27FE /* XCBActions.swift */; };
D67B506D250B291200FAECFB /* BlurHashDecode.swift in Sources */ = {isa = PBXBuildFile; fileRef = D67B506C250B291200FAECFB /* BlurHashDecode.swift */; };
D67C57AD21E265FC00C3118B /* LargeAccountDetailView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D67C57AC21E265FC00C3118B /* LargeAccountDetailView.swift */; };
D67C57AF21E28EAD00C3118B /* Array+Uniques.swift in Sources */ = {isa = PBXBuildFile; fileRef = D67C57AE21E28EAD00C3118B /* Array+Uniques.swift */; };
D67C57B221E28FAD00C3118B /* ComposeStatusReplyView.xib in Resources */ = {isa = PBXBuildFile; fileRef = D67C57B121E28FAD00C3118B /* ComposeStatusReplyView.xib */; };
@ -494,6 +495,7 @@
D67895BB24671E6D00D4CD9E /* PKDrawing+Render.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "PKDrawing+Render.swift"; sourceTree = "<group>"; };
D67895BF246870DE00D4CD9E /* LocalAccountAvatarView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocalAccountAvatarView.swift; sourceTree = "<group>"; };
D679C09E215850EF00DA27FE /* XCBActions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = XCBActions.swift; sourceTree = "<group>"; };
D67B506C250B291200FAECFB /* BlurHashDecode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BlurHashDecode.swift; sourceTree = "<group>"; };
D67C57AC21E265FC00C3118B /* LargeAccountDetailView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LargeAccountDetailView.swift; sourceTree = "<group>"; };
D67C57AE21E28EAD00C3118B /* Array+Uniques.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Array+Uniques.swift"; sourceTree = "<group>"; };
D67C57B121E28FAD00C3118B /* ComposeStatusReplyView.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = ComposeStatusReplyView.xib; sourceTree = "<group>"; };
@ -1119,6 +1121,14 @@
path = XCallbackURL;
sourceTree = "<group>";
};
D67B506B250B28FF00FAECFB /* Vendor */ = {
isa = PBXGroup;
children = (
D67B506C250B291200FAECFB /* BlurHashDecode.swift */,
);
path = Vendor;
sourceTree = "<group>";
};
D67C57A721E2649B00C3118B /* Account Detail */ = {
isa = PBXGroup;
children = (
@ -1337,6 +1347,7 @@
D64D8CA82463B494006B0BAA /* CachedDictionary.swift */,
D60E2F2B24423EAD005F8713 /* LazilyDecoding.swift */,
D6CA6A91249FAD8900AD45C1 /* AudioSessionHelper.swift */,
D67B506B250B28FF00FAECFB /* Vendor */,
D6F1F84E2193B9BE00F5FE67 /* Caching */,
D6757A7A2157E00100721E32 /* XCallbackURL */,
D62D241E217AA46B005076CC /* Shortcuts */,
@ -1800,6 +1811,7 @@
D6B4A4FF2506B81A000C81C1 /* AccountDisplayNameLabel.swift in Sources */,
D67C57B421E2910700C3118B /* ComposeStatusReplyView.swift in Sources */,
D6B053A623BD2D0C00A066FA /* AssetCollectionViewController.swift in Sources */,
D67B506D250B291200FAECFB /* BlurHashDecode.swift in Sources */,
D62275A024F1677200B82A16 /* ComposeHostingController.swift in Sources */,
04DACE8E212CC7CC009840C4 /* ImageCache.swift in Sources */,
D627FF7B217E951500CC0648 /* DraftsTableViewController.swift in Sources */,

View File

@ -6,8 +6,8 @@
"repositoryURL": "https://github.com/microsoft/plcrashreporter",
"state": {
"branch": null,
"revision": "4637a7854de2cc5c354d46fb931d74bdbc2c043e",
"version": "1.7.0"
"revision": "6b7ca9a2faad6ea990ff60b0a3ee4fdf3db59150",
"version": "1.7.2"
}
},
{
@ -15,7 +15,7 @@
"repositoryURL": "https://git.shadowfacts.net/shadowfacts/SheetController.git",
"state": {
"branch": "master",
"revision": "6926446c4e15eb7f4513c4c00df9279553b330be",
"revision": "7ac34efeabb5b5eb08fcf3d1235dbc9ca0441662",
"version": null
}
},

View File

@ -47,4 +47,15 @@ enum Cache<T> {
try hybrid.setObject(object, forKey: key, expiry: expiry)
}
}
func removeAll() throws {
switch self {
case let .memory(memory):
memory.removeAll()
case let .disk(disk):
try disk.removeAll()
case let .hybrid(hybrid):
try hybrid.removeAll()
}
}
}

View File

@ -16,7 +16,7 @@ class ImageCache {
static let attachments = ImageCache(name: "Attachments", memoryExpiry: .seconds(60 * 2))
static let emojis = ImageCache(name: "Emojis", memoryExpiry: .seconds(60 * 5), diskExpiry: .seconds(60 * 60))
let cache: Cache<Data>
private let cache: Cache<Data>
private var groups = [URL: RequestGroup]()
@ -68,6 +68,10 @@ class ImageCache {
groups[url]?.cancelWithoutCallback()
}
func reset() throws {
try cache.removeAll()
}
private class RequestGroup {
let url: URL
private let onFinished: (Data?) -> Void

View File

@ -77,6 +77,7 @@ class GalleryViewController: UIPageViewController, UIPageViewControllerDataSourc
case .image:
let vc = LoadingLargeImageViewController(attachment: attachment)
vc.shrinkGestureEnabled = false
vc.animationSourceView = sourceViews[index]
return vc
case .video, .audio:
let vc = GalleryPlayerViewController()

View File

@ -31,7 +31,12 @@ class LargeImageViewController: UIViewController, UIScrollViewDelegate, LargeIma
@IBOutlet weak var bottomControlsView: UIView!
@IBOutlet weak var descriptionLabel: UILabel!
var contentView: ContentView
var contentView: ContentView {
didSet {
oldValue.removeFromSuperview()
setupContentView()
}
}
var contentViewLeadingConstraint: NSLayoutConstraint!
var contentViewTopConstraint: NSLayoutConstraint!
@ -76,14 +81,7 @@ class LargeImageViewController: UIViewController, UIScrollViewDelegate, LargeIma
override func viewDidLoad() {
super.viewDidLoad()
contentView.translatesAutoresizingMaskIntoConstraints = false
scrollView.addSubview(contentView)
contentViewLeadingConstraint = contentView.leadingAnchor.constraint(equalTo: scrollView.leadingAnchor)
contentViewTopConstraint = contentView.topAnchor.constraint(equalTo: scrollView.topAnchor)
NSLayoutConstraint.activate([
contentViewLeadingConstraint,
contentViewTopConstraint,
])
setupContentView()
setControlsVisible(initialControlsVisible, animated: false)
shareButton.isEnabled = !contentView.activityItemsForSharing.isEmpty
@ -107,6 +105,17 @@ class LargeImageViewController: UIViewController, UIScrollViewDelegate, LargeIma
view.addGestureRecognizer(doubleTap)
}
private func setupContentView() {
contentView.translatesAutoresizingMaskIntoConstraints = false
scrollView.addSubview(contentView)
contentViewLeadingConstraint = contentView.leadingAnchor.constraint(equalTo: scrollView.leadingAnchor)
contentViewTopConstraint = contentView.topAnchor.constraint(equalTo: scrollView.topAnchor)
NSLayoutConstraint.activate([
contentViewLeadingConstraint,
contentViewTopConstraint,
])
}
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
@ -124,8 +133,9 @@ class LargeImageViewController: UIViewController, UIScrollViewDelegate, LargeIma
centerImage()
// todo: does this need to be in viewDidLayoutSubviews?
if view.safeAreaInsets.top == 44 {
// running on iPhone X style notched device
// on iPhone X, Xs, Xs Max, 11 Pro, 11 Pro Max, the top safe area inset is 44pts
// on iPhone XR, 11, the top inset is 48pts
if view.safeAreaInsets.top == 44 || view.safeAreaInsets.top == 48 {
let notchWidth: CGFloat = 209
let earWidth = (view.bounds.width - notchWidth) / 2
let offset = (earWidth - shareButton.bounds.width) / 2

View File

@ -10,14 +10,16 @@ import Pachyderm
class LoadingLargeImageViewController: UIViewController, LargeImageAnimatableViewController {
private var attachment: Attachment?
let url: URL
let cache: ImageCache
let imageDescription: String?
var largeImageVC: LargeImageViewController?
var loadingVC: LoadingViewController?
private(set) var loaded = false
private(set) var largeImageVC: LargeImageViewController?
private var loadingVC: LoadingViewController?
var imageRequest: ImageCache.Request?
private var imageRequest: ImageCache.Request?
private var initialControlsVisible: Bool = true
var controlsVisible: Bool {
@ -70,6 +72,7 @@ class LoadingLargeImageViewController: UIViewController, LargeImageAnimatableVie
convenience init(attachment: Attachment) {
self.init(url: attachment.url, cache: .attachments, imageDescription: attachment.description)
self.attachment = attachment
}
required init?(coder: NSCoder) {
@ -85,6 +88,8 @@ class LoadingLargeImageViewController: UIViewController, LargeImageAnimatableVie
if let data = cache.get(url) {
createLargeImage(data: data)
} else {
createPreview()
loadingVC = LoadingViewController()
embedChild(loadingVC!)
imageRequest = cache.get(url) { [weak self] (data) in
@ -110,15 +115,32 @@ class LoadingLargeImageViewController: UIViewController, LargeImageAnimatableVie
}
}
func createLargeImage(data: Data) {
private func createLargeImage(data: Data) {
guard !loaded else { return }
loaded = true
guard let image = UIImage(data: data) else { return }
let gifData = url.pathExtension == "gif" ? data : nil
createLargeImage(image: image, gifData: gifData)
}
private func createLargeImage(image: UIImage, gifData: Data?) {
let imageView = LargeImageImageContentView(image: image, gifData: gifData)
if let existing = largeImageVC {
existing.contentView = imageView
} else {
largeImageVC = LargeImageViewController(contentView: imageView, description: imageDescription, sourceView: animationSourceView)
largeImageVC!.initialControlsVisible = initialControlsVisible
largeImageVC!.shrinkGestureEnabled = false
embedChild(largeImageVC!)
}
}
private func createPreview() {
guard !self.loaded,
let image = animationSourceView?.image else { return }
self.createLargeImage(image: image, gifData: nil)
}
}

View File

@ -38,7 +38,7 @@ extension LargeImageAnimatableViewController {
class LargeImageExpandAnimationController: NSObject, UIViewControllerAnimatedTransitioning {
func transitionDuration(using transitionContext: UIViewControllerContextTransitioning?) -> TimeInterval {
return 0.5
return 0.4
}
func animateTransition(using transitionContext: UIViewControllerContextTransitioning) {
@ -48,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,
@ -85,12 +85,11 @@ class LargeImageExpandAnimationController: NSObject, UIViewControllerAnimatedTra
imageView.layer.maskedCorners = sourceView.layer.maskedCorners
imageView.layer.masksToBounds = true
containerView.addSubview(toVC.view)
containerView.addSubview(imageView)
let duration = transitionDuration(using: transitionContext)
let velocity = 1 / CGFloat(duration)
UIView.animate(withDuration: duration, delay: 0, usingSpringWithDamping: 0.65, initialSpringVelocity: velocity, options: []) {
UIView.animate(withDuration: duration, delay: 0, usingSpringWithDamping: 0.75, initialSpringVelocity: velocity, options: []) {
imageView.frame = finalFrame
imageView.layer.cornerRadius = 0
toVC.view.alpha = 1

View File

@ -46,12 +46,15 @@ struct AdvancedPrefsView : View {
var cachingSection: some View {
Section(header: Text("Caching")) {
Button(action: clearCache) {
Text("Clear Cache")
Text("Clear Mastodon Cache")
}.foregroundColor(.red)
Button(action: clearImageCaches) {
Text("Clear Image Caches")
}.foregroundColor(.red)
}
}
func clearCache() {
private func clearCache() {
for account in LocalData.shared.accounts {
let controller = MastodonController.getForAccount(account)
let coordinator = controller.persistentContainer.persistentStoreCoordinator
@ -59,7 +62,22 @@ struct AdvancedPrefsView : View {
try! coordinator.destroyPersistentStore(at: store.url!, ofType: store.type, options: store.options)
}
}
MastodonController.resetAll()
resetUI()
}
private func clearImageCaches() {
[
ImageCache.avatars,
ImageCache.headers,
ImageCache.attachments,
ImageCache.emojis,
].forEach {
try! $0.reset()
}
resetUI()
}
private func resetUI() {
let mostRecent = LocalData.shared.getMostRecentAccount()!
NotificationCenter.default.post(name: .activateAccount, object: nil, userInfo: ["account": mostRecent])
}

148
Tusker/Vendor/BlurHashDecode.swift vendored Normal file
View File

@ -0,0 +1,148 @@
/// BlurHash reference decoder implementation.
/// From https://github.com/woltapp/blurhash/blob/b23214ddcab803fe1ec9a3e6b20558caf33a23a5/Swift/BlurHashDecode.swift
import UIKit
extension UIImage {
public convenience init?(blurHash: String, size: CGSize, punch: Float = 1) {
guard blurHash.count >= 6 else { return nil }
let sizeFlag = String(blurHash[0]).decode83()
let numY = (sizeFlag / 9) + 1
let numX = (sizeFlag % 9) + 1
let quantisedMaximumValue = String(blurHash[1]).decode83()
let maximumValue = Float(quantisedMaximumValue + 1) / 166
guard blurHash.count == 4 + 2 * numX * numY else { return nil }
let colours: [(Float, Float, Float)] = (0 ..< numX * numY).map { i in
if i == 0 {
let value = String(blurHash[2 ..< 6]).decode83()
return decodeDC(value)
} else {
let value = String(blurHash[4 + i * 2 ..< 4 + i * 2 + 2]).decode83()
return decodeAC(value, maximumValue: maximumValue * punch)
}
}
let width = Int(size.width)
let height = Int(size.height)
let bytesPerRow = width * 3
guard let data = CFDataCreateMutable(kCFAllocatorDefault, bytesPerRow * height) else { return nil }
CFDataSetLength(data, bytesPerRow * height)
guard let pixels = CFDataGetMutableBytePtr(data) else { return nil }
for y in 0 ..< height {
for x in 0 ..< width {
var r: Float = 0
var g: Float = 0
var b: Float = 0
for j in 0 ..< numY {
for i in 0 ..< numX {
let basis = cos(Float.pi * Float(x) * Float(i) / Float(width)) * cos(Float.pi * Float(y) * Float(j) / Float(height))
let colour = colours[i + j * numX]
r += colour.0 * basis
g += colour.1 * basis
b += colour.2 * basis
}
}
let intR = UInt8(linearTosRGB(r))
let intG = UInt8(linearTosRGB(g))
let intB = UInt8(linearTosRGB(b))
pixels[3 * x + 0 + y * bytesPerRow] = intR
pixels[3 * x + 1 + y * bytesPerRow] = intG
pixels[3 * x + 2 + y * bytesPerRow] = intB
}
}
let bitmapInfo = CGBitmapInfo(rawValue: CGImageAlphaInfo.none.rawValue)
guard let provider = CGDataProvider(data: data) else { return nil }
guard let cgImage = CGImage(width: width, height: height, bitsPerComponent: 8, bitsPerPixel: 24, bytesPerRow: bytesPerRow,
space: CGColorSpaceCreateDeviceRGB(), bitmapInfo: bitmapInfo, provider: provider, decode: nil, shouldInterpolate: true, intent: .defaultIntent) else { return nil }
self.init(cgImage: cgImage)
}
}
private func decodeDC(_ value: Int) -> (Float, Float, Float) {
let intR = value >> 16
let intG = (value >> 8) & 255
let intB = value & 255
return (sRGBToLinear(intR), sRGBToLinear(intG), sRGBToLinear(intB))
}
private func decodeAC(_ value: Int, maximumValue: Float) -> (Float, Float, Float) {
let quantR = value / (19 * 19)
let quantG = (value / 19) % 19
let quantB = value % 19
let rgb = (
signPow((Float(quantR) - 9) / 9, 2) * maximumValue,
signPow((Float(quantG) - 9) / 9, 2) * maximumValue,
signPow((Float(quantB) - 9) / 9, 2) * maximumValue
)
return rgb
}
private func signPow(_ value: Float, _ exp: Float) -> Float {
return copysign(pow(abs(value), exp), value)
}
private func linearTosRGB(_ value: Float) -> Int {
let v = max(0, min(1, value))
if v <= 0.0031308 { return Int(v * 12.92 * 255 + 0.5) }
else { return Int((1.055 * pow(v, 1 / 2.4) - 0.055) * 255 + 0.5) }
}
private func sRGBToLinear<Type: BinaryInteger>(_ value: Type) -> Float {
let v = Float(Int64(value)) / 255
if v <= 0.04045 { return v / 12.92 }
else { return pow((v + 0.055) / 1.055, 2.4) }
}
private let encodeCharacters: [String] = {
return "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz#$%*+,-.:;=?@[]^_{|}~".map { String($0) }
}()
private let decodeCharacters: [String: Int] = {
var dict: [String: Int] = [:]
for (index, character) in encodeCharacters.enumerated() {
dict[character] = index
}
return dict
}()
private extension String {
func decode83() -> Int {
var value: Int = 0
for character in self {
if let digit = decodeCharacters[String(character)] {
value = value * 83 + digit
}
}
return value
}
}
private extension String {
subscript (offset: Int) -> Character {
return self[index(startIndex, offsetBy: offset)]
}
subscript (bounds: CountableClosedRange<Int>) -> Substring {
let start = index(startIndex, offsetBy: bounds.lowerBound)
let end = index(startIndex, offsetBy: bounds.upperBound)
return self[start...end]
}
subscript (bounds: CountableRange<Int>) -> Substring {
let start = index(startIndex, offsetBy: bounds.lowerBound)
let end = index(startIndex, offsetBy: bounds.upperBound)
return self[start..<end]
}
}

View File

@ -25,6 +25,7 @@ class AttachmentView: UIImageView, GIFAnimatable {
var attachment: Attachment!
var index: Int!
var expectedSize: CGSize!
private var attachmentRequest: ImageCache.Request?
@ -35,12 +36,13 @@ class AttachmentView: UIImageView, GIFAnimatable {
public lazy var animator: Animator? = Animator(withDelegate: self)
init(attachment: Attachment, index: Int) {
init(attachment: Attachment, index: Int, expectedSize: CGSize) {
super.init(image: nil)
commonInit()
self.attachment = attachment
self.index = index
self.expectedSize = expectedSize
loadAttachment()
}
@ -90,6 +92,31 @@ class AttachmentView: UIImageView, GIFAnimatable {
guard AttachmentsContainerView.supportedAttachmentTypes.contains(attachment.kind) else {
preconditionFailure("invalid attachment type")
}
if let hash = attachment.blurHash {
DispatchQueue.global(qos: .default).async { [weak self] in
guard let self = self else { return }
let size: CGSize
if let meta = self.attachment.meta,
let width = meta.width, let height = meta.height {
size = CGSize(width: width, height: height)
} else if let orig = self.attachment.meta?.original,
let width = orig.width, let height = orig.height {
size = CGSize(width: width, height: height)
} else {
size = self.expectedSize
}
if let preview = UIImage(blurHash: hash, size: size) {
DispatchQueue.main.async { [weak self] in
guard let self = self else { return }
if self.image == nil {
self.image = preview
}
}
}
}
}
switch attachment.kind {
case .image:
loadImage()

View File

@ -65,18 +65,18 @@ class AttachmentsContainerView: UIView {
switch attachments.count {
case 1:
let attachmentView = createAttachmentView(index: 0)
let attachmentView = createAttachmentView(index: 0, hSize: .full, vSize: .full)
attachmentView.layer.cornerRadius = 5
attachmentView.layer.masksToBounds = true
fillView(attachmentView)
sendSubviewToBack(attachmentView)
accessibilityElements.append(attachmentView)
case 2:
let left = createAttachmentView(index: 0)
let left = createAttachmentView(index: 0, hSize: .half, vSize: .full)
left.layer.cornerRadius = 5
left.layer.maskedCorners = [.layerMinXMinYCorner, .layerMinXMaxYCorner]
left.layer.masksToBounds = true
let right = createAttachmentView(index: 1)
let right = createAttachmentView(index: 1, hSize: .half, vSize: .full)
right.layer.cornerRadius = 5
right.layer.maskedCorners = [.layerMaxXMinYCorner, .layerMaxXMaxYCorner]
right.layer.masksToBounds = true
@ -92,15 +92,15 @@ class AttachmentsContainerView: UIView {
accessibilityElements.append(left)
accessibilityElements.append(right)
case 3:
let left = createAttachmentView(index: 0)
let left = createAttachmentView(index: 0, hSize: .half, vSize: .full)
left.layer.cornerRadius = 5
left.layer.maskedCorners = [.layerMinXMinYCorner, .layerMinXMaxYCorner]
left.layer.masksToBounds = true
let topRight = createAttachmentView(index: 1)
let topRight = createAttachmentView(index: 1, hSize: .half, vSize: .half)
topRight.layer.cornerRadius = 5
topRight.layer.maskedCorners = .layerMaxXMinYCorner
topRight.layer.masksToBounds = true
let bottomRight = createAttachmentView(index: 2)
let bottomRight = createAttachmentView(index: 2, hSize: .half, vSize: .half)
bottomRight.layer.cornerRadius = 5
bottomRight.layer.maskedCorners = .layerMaxXMaxYCorner
bottomRight.layer.masksToBounds = true
@ -121,11 +121,11 @@ class AttachmentsContainerView: UIView {
accessibilityElements.append(topRight)
accessibilityElements.append(bottomRight)
case 4:
let topLeft = createAttachmentView(index: 0)
let topLeft = createAttachmentView(index: 0, hSize: .half, vSize: .half)
topLeft.layer.cornerRadius = 5
topLeft.layer.maskedCorners = .layerMinXMinYCorner
topLeft.layer.masksToBounds = true
let bottomLeft = createAttachmentView(index: 2)
let bottomLeft = createAttachmentView(index: 2, hSize: .half, vSize: .half)
bottomLeft.layer.cornerRadius = 5
bottomLeft.layer.maskedCorners = .layerMinXMaxYCorner
bottomLeft.layer.masksToBounds = true
@ -133,11 +133,11 @@ class AttachmentsContainerView: UIView {
topLeft,
bottomLeft
])
let topRight = createAttachmentView(index: 1)
let topRight = createAttachmentView(index: 1, hSize: .half, vSize: .half)
topRight.layer.cornerRadius = 5
topRight.layer.maskedCorners = .layerMaxXMinYCorner
topRight.layer.masksToBounds = true
let bottomRight = createAttachmentView(index: 3)
let bottomRight = createAttachmentView(index: 3, hSize: .half, vSize: .half)
bottomRight.layer.cornerRadius = 5
bottomRight.layer.maskedCorners = .layerMaxXMaxYCorner
bottomRight.layer.masksToBounds = true
@ -177,11 +177,11 @@ class AttachmentsContainerView: UIView {
moreView.addSubview(moreLabel)
moreView.accessibilityLabel = moreLabel.text
let topLeft = createAttachmentView(index: 0)
let topLeft = createAttachmentView(index: 0, hSize: .half, vSize: .half)
topLeft.layer.cornerRadius = 5
topLeft.layer.maskedCorners = .layerMinXMinYCorner
topLeft.layer.masksToBounds = true
let bottomLeft = createAttachmentView(index: 2)
let bottomLeft = createAttachmentView(index: 2, hSize: .half, vSize: .half)
bottomLeft.layer.cornerRadius = 5
bottomLeft.layer.maskedCorners = .layerMinXMaxYCorner
bottomLeft.layer.masksToBounds = true
@ -189,7 +189,7 @@ class AttachmentsContainerView: UIView {
topLeft,
bottomLeft
])
let topRight = createAttachmentView(index: 1)
let topRight = createAttachmentView(index: 1, hSize: .half, vSize: .half)
topRight.layer.cornerRadius = 5
topRight.layer.maskedCorners = .layerMaxXMinYCorner
topRight.layer.masksToBounds = true
@ -225,8 +225,24 @@ class AttachmentsContainerView: UIView {
contentHidden = Preferences.shared.blurAllMedia || status.sensitive
}
private func createAttachmentView(index: Int) -> AttachmentView {
let attachmentView = AttachmentView(attachment: attachments[index], index: index)
private func createAttachmentView(index: Int, hSize: RelativeSize, vSize: RelativeSize) -> AttachmentView {
let width: CGFloat
switch hSize {
case .full:
width = bounds.width
case .half:
width = (bounds.width - 4) / 2
}
let height: CGFloat
switch vSize {
case .full:
height = bounds.height
case .half:
height = (bounds.height - 4) / 2
}
let size = CGSize(width: width, height: height)
let attachmentView = AttachmentView(attachment: attachments[index], index: index, expectedSize: size)
attachmentView.delegate = delegate
attachmentView.translatesAutoresizingMaskIntoConstraints = false
attachmentView.isAccessibilityElement = true
@ -376,8 +392,7 @@ class AttachmentsContainerView: UIView {
}
fileprivate extension UIView {
enum RelativeSize {
fileprivate enum RelativeSize {
case full, half
var multiplier: CGFloat {
@ -390,6 +405,7 @@ fileprivate extension UIView {
}
}
fileprivate extension UIView {
func halfWidth(spacing: CGFloat = 4) -> NSLayoutConstraint {
return widthAnchor.constraint(equalTo: superview!.widthAnchor, multiplier: 0.5, constant: -spacing / 2)
}

View File

@ -104,36 +104,42 @@ class FollowRequestNotificationTableViewCell: UITableViewCell {
updateTimestampWorkItem = nil
}
private func addLabel(_ text: String) {
let label = UILabel()
label.textAlignment = .center
label.font = .boldSystemFont(ofSize: 17)
label.text = text
self.stackView.addArrangedSubview(label)
}
// MARK: - Interaction
@IBAction func rejectButtonPressed() {
acceptButton.isEnabled = false
rejectButton.isEnabled = false
let request = Account.rejectFollowRequest(account)
mastodonController.run(request) { (response) in
guard case .success(_, _) = response else { fatalError() }
DispatchQueue.main.async {
UINotificationFeedbackGenerator().notificationOccurred(.success)
self.actionButtonsStackView.isHidden = true
let label = UILabel()
label.textAlignment = .center
label.font = .boldSystemFont(ofSize: 17)
label.text = NSLocalizedString("Rejected", comment: "rejected follow request label")
self.stackView.addArrangedSubview(label)
self.addLabel(NSLocalizedString("Rejected", comment: "rejected follow request label"))
}
}
}
@IBAction func acceptButtonPressed() {
acceptButton.isEnabled = false
rejectButton.isEnabled = false
let request = Account.authorizeFollowRequest(account)
mastodonController.run(request) { (response) in
guard case .success(_, _) = response else { fatalError() }
DispatchQueue.main.async {
UINotificationFeedbackGenerator().notificationOccurred(.success)
self.actionButtonsStackView.isHidden = true
let label = UILabel()
label.textAlignment = .center
label.font = .boldSystemFont(ofSize: 17)
label.text = NSLocalizedString("Accepted", comment: "accepted follow request label")
self.stackView.addArrangedSubview(label)
self.addLabel(NSLocalizedString("Accepted", comment: "accepted follow request label"))
}
}
}

View File

@ -1,9 +1,11 @@
<?xml version="1.0" encoding="UTF-8"?>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.XIB" version="3.0" toolsVersion="16092.1" 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="retina6_1" orientation="portrait" appearance="light"/>
<dependencies>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="16082.1"/>
<deployment identifier="iOS"/>
<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"/>
</dependencies>
<objects>
@ -30,12 +32,12 @@
</constraints>
</imageView>
<view contentMode="scaleToFill" horizontalHuggingPriority="249" translatesAutoresizingMaskIntoConstraints="NO" id="9WN-Ql-DDL">
<rect key="frame" x="30" y="0.0" width="175.5" height="30"/>
<rect key="frame" x="30" y="0.0" width="176" height="30"/>
</view>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" horizontalCompressionResistancePriority="751" text="2m" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="Saq-P5-oVH">
<rect key="frame" x="205.5" y="0.0" width="24.5" height="30"/>
<rect key="frame" x="206" y="0.0" width="24" height="30"/>
<fontDescription key="fontDescription" type="system" weight="light" pointSize="17"/>
<color key="textColor" systemColor="secondaryLabelColor" red="0.23529411759999999" green="0.23529411759999999" blue="0.26274509800000001" alpha="0.59999999999999998" colorSpace="custom" customColorSpace="sRGB"/>
<color key="textColor" systemColor="secondaryLabelColor"/>
<nil key="highlightedColor"/>
</label>
</subviews>
@ -49,19 +51,20 @@
<stackView opaque="NO" contentMode="scaleToFill" distribution="fillEqually" translatesAutoresizingMaskIntoConstraints="NO" id="20F-2n-eQx">
<rect key="frame" x="0.0" y="58.5" width="230" height="26.5"/>
<subviews>
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="CMQ-TI-X9k">
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" lineBreakMode="middleTruncation" pointerInteraction="YES" translatesAutoresizingMaskIntoConstraints="NO" id="CMQ-TI-X9k">
<rect key="frame" x="0.0" y="0.0" width="115" height="26.5"/>
<state key="normal" image="checkmark.circle.fill" catalog="system">
<preferredSymbolConfiguration key="preferredSymbolConfiguration" configurationType="pointSize" pointSize="22"/>
<accessibility key="accessibilityConfiguration" label="Accept Request"/>
<state key="normal" title=" Accept" image="checkmark.circle.fill" catalog="system">
<color key="titleColor" systemColor="systemBlueColor"/>
</state>
<connections>
<action selector="acceptButtonPressed" destination="Pcu-ap-Xqf" eventType="touchUpInside" id="hGw-3d-RNi"/>
</connections>
</button>
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="7MW-rY-m5l">
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" lineBreakMode="middleTruncation" pointerInteraction="YES" translatesAutoresizingMaskIntoConstraints="NO" id="7MW-rY-m5l">
<rect key="frame" x="115" y="0.0" width="115" height="26.5"/>
<state key="normal" image="xmark.circle.fill" catalog="system">
<preferredSymbolConfiguration key="preferredSymbolConfiguration" configurationType="pointSize" pointSize="22"/>
<state key="normal" title=" Reject" image="xmark.circle.fill" catalog="system">
<color key="titleColor" systemColor="systemBlueColor"/>
</state>
<connections>
<action selector="rejectButtonPressed" destination="Pcu-ap-Xqf" eventType="touchUpInside" id="EP6-Bg-3nC"/>
@ -105,5 +108,11 @@
<image name="checkmark.circle.fill" catalog="system" width="128" height="121"/>
<image name="person.fill" catalog="system" width="128" height="120"/>
<image name="xmark.circle.fill" catalog="system" width="128" height="121"/>
<systemColor name="secondaryLabelColor">
<color red="0.23529411764705882" green="0.23529411764705882" blue="0.2627450980392157" alpha="0.59999999999999998" colorSpace="custom" customColorSpace="sRGB"/>
</systemColor>
<systemColor name="systemBlueColor">
<color red="0.0" green="0.47843137254901963" blue="1" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
</systemColor>
</resources>
</document>