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 textURL: URL?
public let meta: Metadata? public let meta: Metadata?
public let description: String? public let description: String?
public let blurHash: String?
public static func update(_ attachment: Attachment, focus: (Float, Float)?, description: String?) -> Request<Attachment> { 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([ 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.textURL = try? container.decode(URL?.self, forKey: .textURL)
self.meta = try? container.decode(Metadata?.self, forKey: .meta) self.meta = try? container.decode(Metadata?.self, forKey: .meta)
self.description = try? container.decode(String?.self, forKey: .description) self.description = try? container.decode(String?.self, forKey: .description)
self.blurHash = try? container.decode(String?.self, forKey: .blurHash)
} }
private enum CodingKeys: String, CodingKey { private enum CodingKeys: String, CodingKey {
@ -46,6 +48,7 @@ public class Attachment: Codable {
case textURL = "text_url" case textURL = "text_url"
case meta case meta
case description case description
case blurHash = "blurhash"
} }
} }
@ -60,7 +63,7 @@ extension Attachment {
} }
extension Attachment { extension Attachment {
public class Metadata: Codable { public struct Metadata: Codable {
public let length: String? public let length: String?
public let duration: Float? public let duration: Float?
public let audioEncoding: String? public let audioEncoding: String?
@ -91,7 +94,7 @@ extension Attachment {
} }
} }
public class ImageMetadata: Codable { public struct ImageMetadata: Codable {
public let width: Int? public let width: Int?
public let height: Int? public let height: Int?
public let size: String? public let size: String?

View File

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

View File

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

View File

@ -47,4 +47,15 @@ enum Cache<T> {
try hybrid.setObject(object, forKey: key, expiry: expiry) 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 attachments = ImageCache(name: "Attachments", memoryExpiry: .seconds(60 * 2))
static let emojis = ImageCache(name: "Emojis", memoryExpiry: .seconds(60 * 5), diskExpiry: .seconds(60 * 60)) 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]() private var groups = [URL: RequestGroup]()
@ -68,6 +68,10 @@ class ImageCache {
groups[url]?.cancelWithoutCallback() groups[url]?.cancelWithoutCallback()
} }
func reset() throws {
try cache.removeAll()
}
private class RequestGroup { private class RequestGroup {
let url: URL let url: URL
private let onFinished: (Data?) -> Void private let onFinished: (Data?) -> Void

View File

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

View File

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

View File

@ -10,14 +10,16 @@ import Pachyderm
class LoadingLargeImageViewController: UIViewController, LargeImageAnimatableViewController { class LoadingLargeImageViewController: UIViewController, LargeImageAnimatableViewController {
private var attachment: Attachment?
let url: URL let url: URL
let cache: ImageCache let cache: ImageCache
let imageDescription: String? let imageDescription: String?
var largeImageVC: LargeImageViewController? private(set) var loaded = false
var loadingVC: LoadingViewController? private(set) var largeImageVC: LargeImageViewController?
private var loadingVC: LoadingViewController?
var imageRequest: ImageCache.Request? private var imageRequest: ImageCache.Request?
private var initialControlsVisible: Bool = true private var initialControlsVisible: Bool = true
var controlsVisible: Bool { var controlsVisible: Bool {
@ -70,6 +72,7 @@ class LoadingLargeImageViewController: UIViewController, LargeImageAnimatableVie
convenience init(attachment: Attachment) { convenience init(attachment: Attachment) {
self.init(url: attachment.url, cache: .attachments, imageDescription: attachment.description) self.init(url: attachment.url, cache: .attachments, imageDescription: attachment.description)
self.attachment = attachment
} }
required init?(coder: NSCoder) { required init?(coder: NSCoder) {
@ -85,6 +88,8 @@ class LoadingLargeImageViewController: UIViewController, LargeImageAnimatableVie
if let data = cache.get(url) { if let data = cache.get(url) {
createLargeImage(data: data) createLargeImage(data: data)
} else { } else {
createPreview()
loadingVC = LoadingViewController() loadingVC = LoadingViewController()
embedChild(loadingVC!) embedChild(loadingVC!)
imageRequest = cache.get(url) { [weak self] (data) in 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 } guard let image = UIImage(data: data) else { return }
let gifData = url.pathExtension == "gif" ? data : nil 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) let imageView = LargeImageImageContentView(image: image, gifData: gifData)
largeImageVC = LargeImageViewController(contentView: imageView, description: imageDescription, sourceView: animationSourceView) if let existing = largeImageVC {
largeImageVC!.initialControlsVisible = initialControlsVisible existing.contentView = imageView
largeImageVC!.shrinkGestureEnabled = false } else {
embedChild(largeImageVC!) 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 { class LargeImageExpandAnimationController: NSObject, UIViewControllerAnimatedTransitioning {
func transitionDuration(using transitionContext: UIViewControllerContextTransitioning?) -> TimeInterval { func transitionDuration(using transitionContext: UIViewControllerContextTransitioning?) -> TimeInterval {
return 0.5 return 0.4
} }
func animateTransition(using transitionContext: UIViewControllerContextTransitioning) { func animateTransition(using transitionContext: UIViewControllerContextTransitioning) {
@ -48,7 +48,7 @@ class LargeImageExpandAnimationController: NSObject, UIViewControllerAnimatedTra
} }
let containerView = transitionContext.containerView let containerView = transitionContext.containerView
containerView.addSubview(toVC.view)
let finalVCFrame = transitionContext.finalFrame(for: toVC) let finalVCFrame = transitionContext.finalFrame(for: toVC)
guard let sourceView = toVC.animationSourceView, guard let sourceView = toVC.animationSourceView,
@ -85,12 +85,11 @@ class LargeImageExpandAnimationController: NSObject, UIViewControllerAnimatedTra
imageView.layer.maskedCorners = sourceView.layer.maskedCorners imageView.layer.maskedCorners = sourceView.layer.maskedCorners
imageView.layer.masksToBounds = true imageView.layer.masksToBounds = true
containerView.addSubview(toVC.view)
containerView.addSubview(imageView) containerView.addSubview(imageView)
let duration = transitionDuration(using: transitionContext) let duration = transitionDuration(using: transitionContext)
let velocity = 1 / CGFloat(duration) 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.frame = finalFrame
imageView.layer.cornerRadius = 0 imageView.layer.cornerRadius = 0
toVC.view.alpha = 1 toVC.view.alpha = 1

View File

@ -46,12 +46,15 @@ struct AdvancedPrefsView : View {
var cachingSection: some View { var cachingSection: some View {
Section(header: Text("Caching")) { Section(header: Text("Caching")) {
Button(action: clearCache) { Button(action: clearCache) {
Text("Clear Cache") Text("Clear Mastodon Cache")
}.foregroundColor(.red)
Button(action: clearImageCaches) {
Text("Clear Image Caches")
}.foregroundColor(.red) }.foregroundColor(.red)
} }
} }
func clearCache() { private func clearCache() {
for account in LocalData.shared.accounts { for account in LocalData.shared.accounts {
let controller = MastodonController.getForAccount(account) let controller = MastodonController.getForAccount(account)
let coordinator = controller.persistentContainer.persistentStoreCoordinator let coordinator = controller.persistentContainer.persistentStoreCoordinator
@ -59,7 +62,22 @@ struct AdvancedPrefsView : View {
try! coordinator.destroyPersistentStore(at: store.url!, ofType: store.type, options: store.options) 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()! let mostRecent = LocalData.shared.getMostRecentAccount()!
NotificationCenter.default.post(name: .activateAccount, object: nil, userInfo: ["account": mostRecent]) 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 attachment: Attachment!
var index: Int! var index: Int!
var expectedSize: CGSize!
private var attachmentRequest: ImageCache.Request? private var attachmentRequest: ImageCache.Request?
@ -35,12 +36,13 @@ class AttachmentView: UIImageView, GIFAnimatable {
public lazy var animator: Animator? = Animator(withDelegate: self) public lazy var animator: Animator? = Animator(withDelegate: self)
init(attachment: Attachment, index: Int) { init(attachment: Attachment, index: Int, expectedSize: CGSize) {
super.init(image: nil) super.init(image: nil)
commonInit() commonInit()
self.attachment = attachment self.attachment = attachment
self.index = index self.index = index
self.expectedSize = expectedSize
loadAttachment() loadAttachment()
} }
@ -90,6 +92,31 @@ class AttachmentView: UIImageView, GIFAnimatable {
guard AttachmentsContainerView.supportedAttachmentTypes.contains(attachment.kind) else { guard AttachmentsContainerView.supportedAttachmentTypes.contains(attachment.kind) else {
preconditionFailure("invalid attachment type") 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 { switch attachment.kind {
case .image: case .image:
loadImage() loadImage()

View File

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

View File

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

View File

@ -1,9 +1,11 @@
<?xml version="1.0" encoding="UTF-8"?> <?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"/> <device id="retina6_1" orientation="portrait" appearance="light"/>
<dependencies> <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="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"/> <capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
</dependencies> </dependencies>
<objects> <objects>
@ -30,12 +32,12 @@
</constraints> </constraints>
</imageView> </imageView>
<view contentMode="scaleToFill" horizontalHuggingPriority="249" translatesAutoresizingMaskIntoConstraints="NO" id="9WN-Ql-DDL"> <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> </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"> <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"/> <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"/> <nil key="highlightedColor"/>
</label> </label>
</subviews> </subviews>
@ -49,19 +51,20 @@
<stackView opaque="NO" contentMode="scaleToFill" distribution="fillEqually" translatesAutoresizingMaskIntoConstraints="NO" id="20F-2n-eQx"> <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"/> <rect key="frame" x="0.0" y="58.5" width="230" height="26.5"/>
<subviews> <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"/> <rect key="frame" x="0.0" y="0.0" width="115" height="26.5"/>
<state key="normal" image="checkmark.circle.fill" catalog="system"> <accessibility key="accessibilityConfiguration" label="Accept Request"/>
<preferredSymbolConfiguration key="preferredSymbolConfiguration" configurationType="pointSize" pointSize="22"/> <state key="normal" title=" Accept" image="checkmark.circle.fill" catalog="system">
<color key="titleColor" systemColor="systemBlueColor"/>
</state> </state>
<connections> <connections>
<action selector="acceptButtonPressed" destination="Pcu-ap-Xqf" eventType="touchUpInside" id="hGw-3d-RNi"/> <action selector="acceptButtonPressed" destination="Pcu-ap-Xqf" eventType="touchUpInside" id="hGw-3d-RNi"/>
</connections> </connections>
</button> </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"/> <rect key="frame" x="115" y="0.0" width="115" height="26.5"/>
<state key="normal" image="xmark.circle.fill" catalog="system"> <state key="normal" title=" Reject" image="xmark.circle.fill" catalog="system">
<preferredSymbolConfiguration key="preferredSymbolConfiguration" configurationType="pointSize" pointSize="22"/> <color key="titleColor" systemColor="systemBlueColor"/>
</state> </state>
<connections> <connections>
<action selector="rejectButtonPressed" destination="Pcu-ap-Xqf" eventType="touchUpInside" id="EP6-Bg-3nC"/> <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="checkmark.circle.fill" catalog="system" width="128" height="121"/>
<image name="person.fill" catalog="system" width="128" height="120"/> <image name="person.fill" catalog="system" width="128" height="120"/>
<image name="xmark.circle.fill" catalog="system" width="128" height="121"/> <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> </resources>
</document> </document>