Compare commits

..

4 Commits

13 changed files with 235 additions and 131 deletions

View File

@ -168,6 +168,7 @@
D67C57B421E2910700C3118B /* ComposeStatusReplyView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D67C57B321E2910700C3118B /* ComposeStatusReplyView.swift */; }; D67C57B421E2910700C3118B /* ComposeStatusReplyView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D67C57B321E2910700C3118B /* ComposeStatusReplyView.swift */; };
D68015402401A6BA00D6103B /* ComposingPrefsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D680153F2401A6BA00D6103B /* ComposingPrefsView.swift */; }; D68015402401A6BA00D6103B /* ComposingPrefsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D680153F2401A6BA00D6103B /* ComposingPrefsView.swift */; };
D68015422401A74600D6103B /* MediaPrefsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D68015412401A74600D6103B /* MediaPrefsView.swift */; }; D68015422401A74600D6103B /* MediaPrefsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D68015412401A74600D6103B /* MediaPrefsView.swift */; };
D681A29A249AD62D0085E54E /* LargeImageContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D681A299249AD62D0085E54E /* LargeImageContentView.swift */; };
D681E4D3246E2AFF0053414F /* MuteConversationActivity.swift in Sources */ = {isa = PBXBuildFile; fileRef = D681E4D2246E2AFF0053414F /* MuteConversationActivity.swift */; }; D681E4D3246E2AFF0053414F /* MuteConversationActivity.swift in Sources */ = {isa = PBXBuildFile; fileRef = D681E4D2246E2AFF0053414F /* MuteConversationActivity.swift */; };
D681E4D5246E2BC30053414F /* UnmuteConversationActivity.swift in Sources */ = {isa = PBXBuildFile; fileRef = D681E4D4246E2BC30053414F /* UnmuteConversationActivity.swift */; }; D681E4D5246E2BC30053414F /* UnmuteConversationActivity.swift in Sources */ = {isa = PBXBuildFile; fileRef = D681E4D4246E2BC30053414F /* UnmuteConversationActivity.swift */; };
D681E4D7246E32290053414F /* StatusActivityItemSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = D681E4D6246E32290053414F /* StatusActivityItemSource.swift */; }; D681E4D7246E32290053414F /* StatusActivityItemSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = D681E4D6246E32290053414F /* StatusActivityItemSource.swift */; };
@ -471,6 +472,7 @@
D67C57B321E2910700C3118B /* ComposeStatusReplyView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComposeStatusReplyView.swift; sourceTree = "<group>"; }; D67C57B321E2910700C3118B /* ComposeStatusReplyView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComposeStatusReplyView.swift; sourceTree = "<group>"; };
D680153F2401A6BA00D6103B /* ComposingPrefsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComposingPrefsView.swift; sourceTree = "<group>"; }; D680153F2401A6BA00D6103B /* ComposingPrefsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComposingPrefsView.swift; sourceTree = "<group>"; };
D68015412401A74600D6103B /* MediaPrefsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MediaPrefsView.swift; sourceTree = "<group>"; }; D68015412401A74600D6103B /* MediaPrefsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MediaPrefsView.swift; sourceTree = "<group>"; };
D681A299249AD62D0085E54E /* LargeImageContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LargeImageContentView.swift; sourceTree = "<group>"; };
D681E4D2246E2AFF0053414F /* MuteConversationActivity.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MuteConversationActivity.swift; sourceTree = "<group>"; }; D681E4D2246E2AFF0053414F /* MuteConversationActivity.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MuteConversationActivity.swift; sourceTree = "<group>"; };
D681E4D4246E2BC30053414F /* UnmuteConversationActivity.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UnmuteConversationActivity.swift; sourceTree = "<group>"; }; D681E4D4246E2BC30053414F /* UnmuteConversationActivity.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UnmuteConversationActivity.swift; sourceTree = "<group>"; };
D681E4D6246E32290053414F /* StatusActivityItemSource.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatusActivityItemSource.swift; sourceTree = "<group>"; }; D681E4D6246E32290053414F /* StatusActivityItemSource.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatusActivityItemSource.swift; sourceTree = "<group>"; };
@ -942,6 +944,7 @@
D646C954213B364600269FB5 /* Transitions */, D646C954213B364600269FB5 /* Transitions */,
D6289E83217B795D0003D1D7 /* LargeImageViewController.xib */, D6289E83217B795D0003D1D7 /* LargeImageViewController.xib */,
D6C94D862139E62700CB5196 /* LargeImageViewController.swift */, D6C94D862139E62700CB5196 /* LargeImageViewController.swift */,
D681A299249AD62D0085E54E /* LargeImageContentView.swift */,
041160FE22B442870030A9B7 /* LoadingLargeImageViewController.swift */, 041160FE22B442870030A9B7 /* LoadingLargeImageViewController.swift */,
); );
path = "Large Image"; path = "Large Image";
@ -1770,6 +1773,7 @@
D663626C21361C6700C9CBA2 /* Account+Preferences.swift in Sources */, D663626C21361C6700C9CBA2 /* Account+Preferences.swift in Sources */,
D67895C0246870DE00D4CD9E /* LocalAccountAvatarView.swift in Sources */, D67895C0246870DE00D4CD9E /* LocalAccountAvatarView.swift in Sources */,
D6333B372137838300CE884A /* AttributedString+Helpers.swift in Sources */, D6333B372137838300CE884A /* AttributedString+Helpers.swift in Sources */,
D681A29A249AD62D0085E54E /* LargeImageContentView.swift in Sources */,
D6DFC69E242C490400ACC392 /* TrackpadScrollGestureRecognizer.swift in Sources */, D6DFC69E242C490400ACC392 /* TrackpadScrollGestureRecognizer.swift in Sources */,
D61AC1D8232EA42D00C54D2D /* InstanceTableViewCell.swift in Sources */, D61AC1D8232EA42D00C54D2D /* InstanceTableViewCell.swift in Sources */,
D63F9C68241C4F79004C03CF /* AddAttachmentTableViewCell.swift in Sources */, D63F9C68241C4F79004C03CF /* AddAttachmentTableViewCell.swift in Sources */,

View File

@ -41,14 +41,18 @@ class Preferences: Codable, ObservableObject {
self.showRepliesInProfiles = try container.decode(Bool.self, forKey: .showRepliesInProfiles) self.showRepliesInProfiles = try container.decode(Bool.self, forKey: .showRepliesInProfiles)
self.avatarStyle = try container.decode(AvatarStyle.self, forKey: .avatarStyle) self.avatarStyle = try container.decode(AvatarStyle.self, forKey: .avatarStyle)
self.hideCustomEmojiInUsernames = try container.decode(Bool.self, forKey: .hideCustomEmojiInUsernames) self.hideCustomEmojiInUsernames = try container.decode(Bool.self, forKey: .hideCustomEmojiInUsernames)
self.showIsStatusReplyIcon = try container.decode(Bool.self, forKey: .showIsStatusReplyIcon)
self.alwaysShowStatusVisibilityIcon = try container.decode(Bool.self, forKey: .alwaysShowStatusVisibilityIcon)
self.defaultPostVisibility = try container.decode(Status.Visibility.self, forKey: .defaultPostVisibility) self.defaultPostVisibility = try container.decode(Status.Visibility.self, forKey: .defaultPostVisibility)
self.automaticallySaveDrafts = try container.decode(Bool.self, forKey: .automaticallySaveDrafts) self.automaticallySaveDrafts = try container.decode(Bool.self, forKey: .automaticallySaveDrafts)
self.requireAttachmentDescriptions = try container.decode(Bool.self, forKey: .requireAttachmentDescriptions) self.requireAttachmentDescriptions = try container.decode(Bool.self, forKey: .requireAttachmentDescriptions)
self.contentWarningCopyMode = try container.decode(ContentWarningCopyMode.self, forKey: .contentWarningCopyMode) self.contentWarningCopyMode = try container.decode(ContentWarningCopyMode.self, forKey: .contentWarningCopyMode)
self.mentionReblogger = try container.decode(Bool.self, forKey: .mentionReblogger) self.mentionReblogger = try container.decode(Bool.self, forKey: .mentionReblogger)
self.blurAllMedia = try container.decode(Bool.self, forKey: .blurAllMedia) self.blurAllMedia = try container.decode(Bool.self, forKey: .blurAllMedia)
self.automaticallyPlayGifs = try container.decode(Bool.self, forKey: .automaticallyPlayGifs) self.automaticallyPlayGifs = try container.decode(Bool.self, forKey: .automaticallyPlayGifs)
self.openLinksInApps = try container.decode(Bool.self, forKey: .openLinksInApps) self.openLinksInApps = try container.decode(Bool.self, forKey: .openLinksInApps)
self.useInAppSafari = try container.decode(Bool.self, forKey: .useInAppSafari) self.useInAppSafari = try container.decode(Bool.self, forKey: .useInAppSafari)
self.inAppSafariAutomaticReaderMode = try container.decode(Bool.self, forKey: .inAppSafariAutomaticReaderMode) self.inAppSafariAutomaticReaderMode = try container.decode(Bool.self, forKey: .inAppSafariAutomaticReaderMode)
@ -67,14 +71,18 @@ class Preferences: Codable, ObservableObject {
try container.encode(showRepliesInProfiles, forKey: .showRepliesInProfiles) try container.encode(showRepliesInProfiles, forKey: .showRepliesInProfiles)
try container.encode(avatarStyle, forKey: .avatarStyle) try container.encode(avatarStyle, forKey: .avatarStyle)
try container.encode(hideCustomEmojiInUsernames, forKey: .hideCustomEmojiInUsernames) try container.encode(hideCustomEmojiInUsernames, forKey: .hideCustomEmojiInUsernames)
try container.encode(showIsStatusReplyIcon, forKey: .showIsStatusReplyIcon)
try container.encode(alwaysShowStatusVisibilityIcon, forKey: .alwaysShowStatusVisibilityIcon)
try container.encode(defaultPostVisibility, forKey: .defaultPostVisibility) try container.encode(defaultPostVisibility, forKey: .defaultPostVisibility)
try container.encode(automaticallySaveDrafts, forKey: .automaticallySaveDrafts) try container.encode(automaticallySaveDrafts, forKey: .automaticallySaveDrafts)
try container.encode(requireAttachmentDescriptions, forKey: .requireAttachmentDescriptions) try container.encode(requireAttachmentDescriptions, forKey: .requireAttachmentDescriptions)
try container.encode(contentWarningCopyMode, forKey: .contentWarningCopyMode) try container.encode(contentWarningCopyMode, forKey: .contentWarningCopyMode)
try container.encode(mentionReblogger, forKey: .mentionReblogger) try container.encode(mentionReblogger, forKey: .mentionReblogger)
try container.encode(blurAllMedia, forKey: .blurAllMedia) try container.encode(blurAllMedia, forKey: .blurAllMedia)
try container.encode(automaticallyPlayGifs, forKey: .automaticallyPlayGifs) try container.encode(automaticallyPlayGifs, forKey: .automaticallyPlayGifs)
try container.encode(openLinksInApps, forKey: .openLinksInApps) try container.encode(openLinksInApps, forKey: .openLinksInApps)
try container.encode(useInAppSafari, forKey: .useInAppSafari) try container.encode(useInAppSafari, forKey: .useInAppSafari)
try container.encode(inAppSafariAutomaticReaderMode, forKey: .inAppSafariAutomaticReaderMode) try container.encode(inAppSafariAutomaticReaderMode, forKey: .inAppSafariAutomaticReaderMode)
@ -86,29 +94,35 @@ class Preferences: Codable, ObservableObject {
try container.encode(statusContentType, forKey: .statusContentType) try container.encode(statusContentType, forKey: .statusContentType)
} }
// MARK: - Appearance // MARK: Appearance
@Published var theme = UIUserInterfaceStyle.unspecified @Published var theme = UIUserInterfaceStyle.unspecified
@Published var showRepliesInProfiles = false @Published var showRepliesInProfiles = false
@Published var avatarStyle = AvatarStyle.roundRect @Published var avatarStyle = AvatarStyle.roundRect
@Published var hideCustomEmojiInUsernames = false @Published var hideCustomEmojiInUsernames = false
@Published var showIsStatusReplyIcon = false
@Published var alwaysShowStatusVisibilityIcon = false
// MARK: - Behavior // MARK: Composing
@Published var defaultPostVisibility = Status.Visibility.public @Published var defaultPostVisibility = Status.Visibility.public
@Published var automaticallySaveDrafts = true @Published var automaticallySaveDrafts = true
@Published var requireAttachmentDescriptions = false @Published var requireAttachmentDescriptions = false
@Published var contentWarningCopyMode = ContentWarningCopyMode.asIs @Published var contentWarningCopyMode = ContentWarningCopyMode.asIs
@Published var mentionReblogger = false @Published var mentionReblogger = false
// MARK: Media
@Published var blurAllMedia = false @Published var blurAllMedia = false
@Published var automaticallyPlayGifs = true @Published var automaticallyPlayGifs = true
// MARK: Behavior
@Published var openLinksInApps = true @Published var openLinksInApps = true
@Published var useInAppSafari = true @Published var useInAppSafari = true
@Published var inAppSafariAutomaticReaderMode = false @Published var inAppSafariAutomaticReaderMode = false
// MARK: - Digital Wellness // MARK: Digital Wellness
@Published var showFavoriteAndReblogCounts = true @Published var showFavoriteAndReblogCounts = true
@Published var defaultNotificationsMode = NotificationsMode.allNotifications @Published var defaultNotificationsMode = NotificationsMode.allNotifications
// MARK: - Advanced // MARK: Advanced
@Published var silentActions: [String: Permission] = [:] @Published var silentActions: [String: Permission] = [:]
@Published var statusContentType: StatusContentType = .plain @Published var statusContentType: StatusContentType = .plain
@ -117,14 +131,18 @@ class Preferences: Codable, ObservableObject {
case showRepliesInProfiles case showRepliesInProfiles
case avatarStyle case avatarStyle
case hideCustomEmojiInUsernames case hideCustomEmojiInUsernames
case showIsStatusReplyIcon
case alwaysShowStatusVisibilityIcon
case defaultPostVisibility case defaultPostVisibility
case automaticallySaveDrafts case automaticallySaveDrafts
case requireAttachmentDescriptions case requireAttachmentDescriptions
case contentWarningCopyMode case contentWarningCopyMode
case mentionReblogger case mentionReblogger
case blurAllMedia case blurAllMedia
case automaticallyPlayGifs case automaticallyPlayGifs
case openLinksInApps case openLinksInApps
case useInAppSafari case useInAppSafari
case inAppSafariAutomaticReaderMode case inAppSafariAutomaticReaderMode

View File

@ -28,8 +28,8 @@ class GalleryViewController: UIPageViewController, UIPageViewControllerDataSourc
var animationSourceView: UIImageView? { sourceViews[currentIndex] } var animationSourceView: UIImageView? { sourceViews[currentIndex] }
var animationImage: UIImage? { var animationImage: UIImage? {
if let page = pages[currentIndex] as? LoadingLargeImageViewController, if let page = pages[currentIndex] as? LargeImageAnimatableViewController,
let image = page.largeImageVC?.image { let image = page.animationImage {
return image return image
} else { } else {
return animationSourceView?.image return animationSourceView?.image
@ -65,18 +65,29 @@ class GalleryViewController: UIPageViewController, UIPageViewControllerDataSourc
self.sourceViews = WeakArray(sourceViews) self.sourceViews = WeakArray(sourceViews)
self.startIndex = startIndex self.startIndex = startIndex
self.pages = attachments.map { self.pages = attachments.enumerated().map { (index, attachment) in
switch $0.kind { switch attachment.kind {
case .image: case .image:
let vc = LoadingLargeImageViewController(attachment: $0) let vc = LoadingLargeImageViewController(attachment: attachment)
vc.shrinkGestureEnabled = false vc.shrinkGestureEnabled = false
return vc return vc
case .video, .audio: case .video, .audio:
let vc = AVPlayerViewController() let vc = AVPlayerViewController()
vc.player = AVPlayer(url: $0.url) vc.player = AVPlayer(url: attachment.url)
return vc return vc
case .gifv: case .gifv:
return GifvAttachmentViewController(attachment: $0) // Passing the source view to the LargeImageGifvContentView is a crappy workaround for not
// having the video size directly inside the content view. This will break when there
// are more than 4 attachments and there is a gifv at index >= 3 (the More... button will show
// in place of the fourth attachment, so there aren't source views for the attachments at index >= 3).
// Really, what should happen is the LargeImageGifvContentView should get the size of the video from
// the AVFoundation instead of the source view.
// This isn't a priority as only Mastodon converts gifs to gifvs, and Mastodon (in its default configuration,
// I don't know about forks) doesn't allow more than four attachments, meaning there will always be a source view.
let gifvContentView = LargeImageGifvContentView(attachment: attachment, source: sourceViews[index]!)
let vc = LargeImageViewController(contentView: gifvContentView, description: attachment.description, sourceView: nil)
vc.shrinkGestureEnabled = false
return vc
default: default:
fatalError() fatalError()
} }

View File

@ -0,0 +1,84 @@
//
// LargeImageContentView.swift
// Tusker
//
// Created by Shadowfacts on 6/17/20.
// Copyright © 2020 Shadowfacts. All rights reserved.
//
import UIKit
import Gifu
import Pachyderm
import AVFoundation
protocol LargeImageContentView {
var animationImage: UIImage? { get }
var animationGifData: Data? { get }
var activityItemsForSharing: [Any] { get }
}
class LargeImageImageContentView: UIImageView, GIFAnimatable, LargeImageContentView {
lazy var animator: Animator? = {
return Animator(withDelegate: self)
}()
var animationImage: UIImage? { image! }
let animationGifData: Data?
var activityItemsForSharing: [Any] {
[image!]
}
init(image: UIImage, gifData: Data?) {
self.animationGifData = gifData
super.init(image: image)
contentMode = .scaleAspectFit
if let data = gifData {
self.animate(withGIFData: data)
}
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override public func display(_ layer: CALayer) {
updateImageIfNeeded()
}
}
class LargeImageGifvContentView: GifvAttachmentView, LargeImageContentView {
private(set) var animationImage: UIImage?
var animationGifData: Data? { nil }
var activityItemsForSharing: [Any] {
// todo: what should we share for gifvs?
// some SO posts indicate that just sharing a URL to the video should work, but that may need to be a local URL?
[]
}
private let asset: AVURLAsset
// The content view needs to supply an intrinsicContentSize for the LargeImageViewController to handle layout/scrolling/zooming correctly
override var intrinsicContentSize: CGSize {
// This is a really sucky workaround for the fact that in the content view, we don't have access to the size of the underlying video.
// There's probably some way of getting this from the AVPlayer/AVAsset directly
animationImage?.size ?? CGSize(width: UIView.noIntrinsicMetric, height: UIView.noIntrinsicMetric)
}
init(attachment: Attachment, source: UIImageView) {
precondition(attachment.kind == .gifv)
self.asset = AVURLAsset(url: attachment.url)
super.init(asset: asset, gravity: .resizeAspect)
self.animationImage = source.image
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}

View File

@ -7,22 +7,17 @@
// //
import UIKit import UIKit
import Gifu
class LargeImageViewController: UIViewController, UIScrollViewDelegate, LargeImageAnimatableViewController { class LargeImageViewController: UIViewController, UIScrollViewDelegate, LargeImageAnimatableViewController {
typealias ContentView = UIView & LargeImageContentView
weak var animationSourceView: UIImageView? weak var animationSourceView: UIImageView?
var animationImage: UIImage? { image ?? animationSourceView?.image } var animationImage: UIImage? { contentView.animationImage }
var animationGifData: Data? { gifData } var animationGifData: Data? { contentView.animationGifData }
var dismissInteractionController: LargeImageInteractionController? var dismissInteractionController: LargeImageInteractionController?
@IBOutlet weak var scrollView: UIScrollView! @IBOutlet weak var scrollView: UIScrollView!
@IBOutlet weak var imageView: GIFImageView!
@IBOutlet weak var imageViewLeadingConstraint: NSLayoutConstraint!
@IBOutlet weak var imageViewTrailingConstraint: NSLayoutConstraint!
@IBOutlet weak var imageViewTopConstraint: NSLayoutConstraint!
@IBOutlet weak var imageViewBottomConstraint: NSLayoutConstraint!
@IBOutlet weak var topControlsView: UIView! @IBOutlet weak var topControlsView: UIView!
@IBOutlet weak var topControlsHeightConstraint: NSLayoutConstraint! @IBOutlet weak var topControlsHeightConstraint: NSLayoutConstraint!
@IBOutlet weak var shareButton: UIButton! @IBOutlet weak var shareButton: UIButton!
@ -35,8 +30,10 @@ 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 image: UIImage? var contentView: ContentView
var gifData: Data? var contentViewLeadingConstraint: NSLayoutConstraint!
var contentViewTopConstraint: NSLayoutConstraint!
var imageDescription: String? var imageDescription: String?
var initialControlsVisible = true var initialControlsVisible = true
@ -57,10 +54,11 @@ class LargeImageViewController: UIViewController, UIScrollViewDelegate, LargeIma
return !controlsVisible return !controlsVisible
} }
init(image: UIImage, description: String?, sourceView: UIImageView?) { init(contentView: ContentView, description: String?, sourceView: UIImageView?) {
self.image = image
self.imageDescription = description self.imageDescription = description
self.animationSourceView = sourceView self.animationSourceView = sourceView
self.contentView = contentView
super.init(nibName: "LargeImageViewController", bundle: nil) super.init(nibName: "LargeImageViewController", bundle: nil)
@ -74,16 +72,20 @@ class LargeImageViewController: UIViewController, UIScrollViewDelegate, LargeIma
override func viewDidLoad() { override func viewDidLoad() {
super.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,
])
setControlsVisible(initialControlsVisible, animated: false) setControlsVisible(initialControlsVisible, animated: false)
shareButton.isEnabled = !contentView.activityItemsForSharing.isEmpty
imageView.image = image
if let gifData = gifData {
imageView.animate(withGIFData: gifData)
}
scrollView.delegate = self scrollView.delegate = self
imageView.bounds = CGRect(origin: .zero, size: imageView.image!.size)
if let imageDescription = imageDescription { if let imageDescription = imageDescription {
descriptionLabel.text = imageDescription descriptionLabel.text = imageDescription
} else { } else {
@ -99,23 +101,24 @@ class LargeImageViewController: UIViewController, UIScrollViewDelegate, LargeIma
doubleTap.numberOfTapsRequired = 2 doubleTap.numberOfTapsRequired = 2
view.addGestureRecognizer(doubleTap) view.addGestureRecognizer(doubleTap)
} }
override func viewDidLayoutSubviews() { override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews() super.viewDidLayoutSubviews()
// todo: does this need to be in viewDidLayoutSubviews?
// limit the image height to the safe area height, so the image doesn't overlap the top controls // limit the image height to the safe area height, so the image doesn't overlap the top controls
// while zoomed all the way out // while zoomed all the way out
let maxHeight = view.bounds.height - view.safeAreaInsets.top - view.safeAreaInsets.bottom let maxHeight = view.bounds.height - view.safeAreaInsets.top - view.safeAreaInsets.bottom
let heightScale = maxHeight / imageView.bounds.height let heightScale = maxHeight / contentView.intrinsicContentSize.height
let widthScale = view.bounds.width / imageView.bounds.width let widthScale = view.bounds.width / contentView.intrinsicContentSize.width
let minScale = min(widthScale, heightScale) let minScale = min(widthScale, heightScale)
scrollView.minimumZoomScale = minScale scrollView.minimumZoomScale = minScale
scrollView.zoomScale = minScale scrollView.zoomScale = minScale
scrollView.maximumZoomScale = minScale >= 1 ? minScale + 2 : 2 scrollView.maximumZoomScale = minScale >= 1 ? minScale + 2 : 2
centerImage() centerImage()
// todo: does this need to be in viewDidLayoutSubviews?
if view.safeAreaInsets.top == 44 { if view.safeAreaInsets.top == 44 {
// running on iPhone X style notched device // running on iPhone X style notched device
let notchWidth: CGFloat = 209 let notchWidth: CGFloat = 209
@ -147,7 +150,7 @@ class LargeImageViewController: UIViewController, UIScrollViewDelegate, LargeIma
} }
func viewForZooming(in scrollView: UIScrollView) -> UIView? { func viewForZooming(in scrollView: UIScrollView) -> UIView? {
return imageView return contentView
} }
func scrollViewDidZoom(_ scrollView: UIScrollView) { func scrollViewDidZoom(_ scrollView: UIScrollView) {
@ -163,18 +166,18 @@ class LargeImageViewController: UIViewController, UIScrollViewDelegate, LargeIma
} }
func centerImage() { func centerImage() {
let yOffset = max(0, (view.bounds.size.height - imageView.frame.height) / 2) let yOffset = max(0, (view.bounds.size.height - contentView.frame.height) / 2)
imageViewTopConstraint.constant = yOffset contentViewTopConstraint.constant = yOffset
let xOffset = max(0, (view.bounds.size.width - imageView.frame.width) / 2) let xOffset = max(0, (view.bounds.size.width - contentView.frame.width) / 2)
imageViewLeadingConstraint.constant = xOffset contentViewLeadingConstraint.constant = xOffset
} }
func zoomRectFor(scale: CGFloat, center: CGPoint) -> CGRect { func zoomRectFor(scale: CGFloat, center: CGPoint) -> CGRect {
var zoomRect = CGRect.zero var zoomRect = CGRect.zero
zoomRect.size.width = imageView.frame.width / scale zoomRect.size.width = contentView.frame.width / scale
zoomRect.size.height = imageView.frame.height / scale zoomRect.size.height = contentView.frame.height / scale
let newCenter = scrollView.convert(center, to: imageView) let newCenter = scrollView.convert(center, to: contentView)
zoomRect.origin.x = newCenter.x - (zoomRect.width / 2) zoomRect.origin.x = newCenter.x - (zoomRect.width / 2)
zoomRect.origin.y = newCenter.y - (zoomRect.height / 2) zoomRect.origin.y = newCenter.y - (zoomRect.height / 2)
return zoomRect return zoomRect
@ -225,11 +228,8 @@ class LargeImageViewController: UIViewController, UIScrollViewDelegate, LargeIma
} }
@IBAction func sharePressed(_ sender: Any) { @IBAction func sharePressed(_ sender: Any) {
guard let image = image else { return } let activityVC = UIActivityViewController(activityItems: contentView.activityItemsForSharing, applicationActivities: nil)
let activityVC = UIActivityViewController(activityItems: [image], applicationActivities: nil) activityVC.popoverPresentationController?.sourceView = shareButton
if let presentationController = activityVC.presentationController as? UIPopoverPresentationController {
presentationController.sourceView = shareButton
}
present(activityVC, animated: true) present(activityVC, animated: true)
} }

View File

@ -14,9 +14,6 @@
<outlet property="closeButtonTopConstraint" destination="ImD-2H-0XK" id="DUe-b1-a2N"/> <outlet property="closeButtonTopConstraint" destination="ImD-2H-0XK" id="DUe-b1-a2N"/>
<outlet property="closeButtonTrailingConstraint" destination="JFe-ig-3Ic" id="cWO-Rr-y3F"/> <outlet property="closeButtonTrailingConstraint" destination="JFe-ig-3Ic" id="cWO-Rr-y3F"/>
<outlet property="descriptionLabel" destination="eo5-fc-RV8" id="vrW-RJ-y5k"/> <outlet property="descriptionLabel" destination="eo5-fc-RV8" id="vrW-RJ-y5k"/>
<outlet property="imageView" destination="qcn-1t-3sS" id="Q01-G2-y1c"/>
<outlet property="imageViewLeadingConstraint" destination="bI3-V8-M70" id="nIe-xI-E9u"/>
<outlet property="imageViewTopConstraint" destination="tfL-hp-2I2" id="EDV-RO-pTe"/>
<outlet property="scrollView" destination="Skj-xq-AgQ" id="TFb-zF-m1b"/> <outlet property="scrollView" destination="Skj-xq-AgQ" id="TFb-zF-m1b"/>
<outlet property="shareButton" destination="vhp-0u-Q0S" id="JZS-K9-4w9"/> <outlet property="shareButton" destination="vhp-0u-Q0S" id="JZS-K9-4w9"/>
<outlet property="shareButtonLeadingConstraint" destination="MJx-2r-p0k" id="Dn5-Eg-Pid"/> <outlet property="shareButtonLeadingConstraint" destination="MJx-2r-p0k" id="Dn5-Eg-Pid"/>
@ -31,19 +28,9 @@
<rect key="frame" x="0.0" y="0.0" width="375" height="667"/> <rect key="frame" x="0.0" y="0.0" width="375" height="667"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/> <autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<subviews> <subviews>
<scrollView clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="scaleToFill" ambiguous="YES" minimumZoomScale="0.25" maximumZoomScale="2" translatesAutoresizingMaskIntoConstraints="NO" id="Skj-xq-AgQ"> <scrollView clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="scaleToFill" minimumZoomScale="0.25" maximumZoomScale="2" translatesAutoresizingMaskIntoConstraints="NO" id="Skj-xq-AgQ">
<rect key="frame" x="0.0" y="0.0" width="375" height="667"/> <rect key="frame" x="0.0" y="0.0" width="375" height="667"/>
<subviews>
<imageView contentMode="scaleAspectFit" horizontalHuggingPriority="251" verticalHuggingPriority="251" ambiguous="YES" translatesAutoresizingMaskIntoConstraints="NO" id="qcn-1t-3sS" customClass="GIFImageView" customModule="Gifu">
<rect key="frame" x="0.0" y="-10" width="375" height="647"/>
<gestureRecognizers/>
</imageView>
</subviews>
<gestureRecognizers/> <gestureRecognizers/>
<constraints>
<constraint firstItem="qcn-1t-3sS" firstAttribute="leading" secondItem="Skj-xq-AgQ" secondAttribute="leading" id="bI3-V8-M70"/>
<constraint firstItem="qcn-1t-3sS" firstAttribute="top" secondItem="Skj-xq-AgQ" secondAttribute="top" id="tfL-hp-2I2"/>
</constraints>
</scrollView> </scrollView>
<view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="kHo-B9-R7a"> <view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="kHo-B9-R7a">
<rect key="frame" x="0.0" y="0.0" width="375" height="36"/> <rect key="frame" x="0.0" y="0.0" width="375" height="36"/>

View File

@ -36,8 +36,8 @@ class LoadingLargeImageViewController: UIViewController, LargeImageAnimatableVie
var shrinkGestureEnabled = true var shrinkGestureEnabled = true
weak var animationSourceView: UIImageView? weak var animationSourceView: UIImageView?
var animationImage: UIImage? { largeImageVC?.image ?? animationSourceView?.image } var animationImage: UIImage? { largeImageVC?.animationImage ?? animationSourceView?.image }
var animationGifData: Data? { largeImageVC?.gifData } var animationGifData: Data? { largeImageVC?.animationGifData }
var dismissInteractionController: LargeImageInteractionController? var dismissInteractionController: LargeImageInteractionController?
override var prefersStatusBarHidden: Bool { override var prefersStatusBarHidden: Bool {
@ -108,12 +108,12 @@ class LoadingLargeImageViewController: UIViewController, LargeImageAnimatableVie
func createLargeImage(data: Data) { func createLargeImage(data: Data) {
guard let image = UIImage(data: data) else { return } guard let image = UIImage(data: data) else { return }
largeImageVC = LargeImageViewController(image: image, description: imageDescription, sourceView: animationSourceView) let gifData = url.pathExtension == "gif" ? data : nil
let imageView = LargeImageImageContentView(image: image, gifData: gifData)
largeImageVC = LargeImageViewController(contentView: imageView, description: imageDescription, sourceView: animationSourceView)
largeImageVC!.initialControlsVisible = initialControlsVisible largeImageVC!.initialControlsVisible = initialControlsVisible
largeImageVC!.shrinkGestureEnabled = false largeImageVC!.shrinkGestureEnabled = false
if url.pathExtension == "gif" {
largeImageVC!.gifData = data
}
embedChild(largeImageVC!) embedChild(largeImageVC!)
} }

View File

@ -39,6 +39,12 @@ struct AppearancePrefsView : View {
Toggle(isOn: $preferences.hideCustomEmojiInUsernames) { Toggle(isOn: $preferences.hideCustomEmojiInUsernames) {
Text("Hide Custom Emoji in Usernames") Text("Hide Custom Emoji in Usernames")
} }
Toggle(isOn: $preferences.showIsStatusReplyIcon) {
Text("Show Status Reply Icons")
}
Toggle(isOn: $preferences.alwaysShowStatusVisibilityIcon) {
Text("Always Show Status Visibility Icons")
}
} }
.listStyle(GroupedListStyle()) .listStyle(GroupedListStyle())
.navigationBarTitle(Text("Appearance")) .navigationBarTitle(Text("Appearance"))

View File

@ -36,14 +36,6 @@ protocol TuskerNavigationDelegate: class {
func reply(to statusID: String, mentioningAcct: String?) func reply(to statusID: String, mentioningAcct: String?)
func largeImage(_ image: UIImage, description: String?, sourceView: UIImageView) -> LargeImageViewController
func largeImage(gifData: Data, description: String?, sourceView: UIImageView) -> LargeImageViewController
func showLargeImage(_ image: UIImage, description: String?, animatingFrom sourceView: UIImageView)
func showLargeImage(gifData: Data, description: String?, animatingFrom sourceView: UIImageView)
func loadingLargeImage(url: URL, cache: ImageCache, description: String?, animatingFrom sourceView: UIImageView) -> LoadingLargeImageViewController func loadingLargeImage(url: URL, cache: ImageCache, description: String?, animatingFrom sourceView: UIImageView) -> LoadingLargeImageViewController
func showLoadingLargeImage(url: URL, cache: ImageCache, description: String?, animatingFrom sourceView: UIImageView) func showLoadingLargeImage(url: URL, cache: ImageCache, description: String?, animatingFrom sourceView: UIImageView)
@ -150,27 +142,6 @@ extension TuskerNavigationDelegate where Self: UIViewController {
vc.presentationController?.delegate = compose vc.presentationController?.delegate = compose
present(vc, animated: true) present(vc, animated: true)
} }
func largeImage(_ image: UIImage, description: String?, sourceView: UIImageView) -> LargeImageViewController {
let vc = LargeImageViewController(image: image, description: description, sourceView: sourceView)
vc.transitioningDelegate = self
return vc
}
func largeImage(gifData: Data, description: String?, sourceView: UIImageView) -> LargeImageViewController {
let vc = LargeImageViewController(image: UIImage(data: gifData)!, description: description, sourceView: sourceView)
vc.transitioningDelegate = self
vc.gifData = gifData
return vc
}
func showLargeImage(_ image: UIImage, description: String?, animatingFrom sourceView: UIImageView) {
present(largeImage(image, description: description, sourceView: sourceView), animated: true)
}
func showLargeImage(gifData: Data, description: String?, animatingFrom sourceView: UIImageView) {
present(largeImage(gifData: gifData, description: description, sourceView: sourceView), animated: true)
}
func loadingLargeImage(url: URL, cache: ImageCache, description: String?, animatingFrom sourceView: UIImageView) -> LoadingLargeImageViewController { func loadingLargeImage(url: URL, cache: ImageCache, description: String?, animatingFrom sourceView: UIImageView) -> LoadingLargeImageViewController {
let vc = LoadingLargeImageViewController(url: url, cache: cache, imageDescription: description) let vc = LoadingLargeImageViewController(url: url, cache: cache, imageDescription: description)

View File

@ -19,8 +19,8 @@ class GifvAttachmentView: UIView {
layer as! AVPlayerLayer layer as! AVPlayerLayer
} }
private let item: AVPlayerItem let item: AVPlayerItem
private let player: AVPlayer let player: AVPlayer
init(asset: AVAsset, gravity: AVLayerVideoGravity) { init(asset: AVAsset, gravity: AVLayerVideoGravity) {
item = AVPlayerItem(asset: asset) item = AVPlayerItem(asset: asset)

View File

@ -128,9 +128,6 @@ class BaseStatusTableViewCell: UITableViewCell {
updateUI(account: account) updateUI(account: account)
updateUIForPreferences(account: account) updateUIForPreferences(account: account)
visibilityImageView.image = UIImage(systemName: status.visibility.unfilledImageName)
visibilityImageView.accessibilityLabel = String(format: NSLocalizedString("Visibility: %@", comment: "status visibility indicator accessibility label"), status.visibility.displayName)
attachmentsView.updateUI(status: status) attachmentsView.updateUI(status: status)
attachmentsView.isAccessibilityElement = status.attachments.count > 0 attachmentsView.isAccessibilityElement = status.attachments.count > 0
attachmentsView.accessibilityLabel = String(format: NSLocalizedString("%d attachments", comment: "status attachments count accessibility label"), status.attachments.count) attachmentsView.accessibilityLabel = String(format: NSLocalizedString("%d attachments", comment: "status attachments count accessibility label"), status.attachments.count)
@ -154,8 +151,8 @@ class BaseStatusTableViewCell: UITableViewCell {
reblogDisabled = status.visibility == .direct || (status.visibility == .private && status.account.id != mastodonController.account.id) reblogDisabled = status.visibility == .direct || (status.visibility == .private && status.account.id != mastodonController.account.id)
} }
reblogButton.isEnabled = !reblogDisabled reblogButton.isEnabled = !reblogDisabled
let reblogButtonImage = reblogDisabled ? UIImage(systemName: status.visibility.imageName) : UIImage(systemName: "repeat")
reblogButton.setImage(reblogButtonImage, for: .normal) updateStatusIconsForPreferences(status)
if state.unknown { if state.unknown {
collapsible = !status.spoilerText.isEmpty collapsible = !status.spoilerText.isEmpty
@ -207,8 +204,11 @@ class BaseStatusTableViewCell: UITableViewCell {
} }
@objc func preferencesChanged() { @objc func preferencesChanged() {
guard let mastodonController = mastodonController, let account = mastodonController.persistentContainer.account(for: accountID) else { return } guard let mastodonController = mastodonController,
let account = mastodonController.persistentContainer.account(for: accountID),
let status = mastodonController.persistentContainer.status(for: statusID) else { return }
updateUIForPreferences(account: account) updateUIForPreferences(account: account)
updateStatusIconsForPreferences(status)
} }
func updateUIForPreferences(account: AccountMO) { func updateUIForPreferences(account: AccountMO) {
@ -217,6 +217,21 @@ class BaseStatusTableViewCell: UITableViewCell {
attachmentsView.contentHidden = Preferences.shared.blurAllMedia || (mastodonController.persistentContainer.status(for: statusID)?.sensitive ?? false) attachmentsView.contentHidden = Preferences.shared.blurAllMedia || (mastodonController.persistentContainer.status(for: statusID)?.sensitive ?? false)
} }
func updateStatusIconsForPreferences(_ status: StatusMO) {
visibilityImageView.isHidden = !Preferences.shared.alwaysShowStatusVisibilityIcon
if Preferences.shared.alwaysShowStatusVisibilityIcon {
visibilityImageView.image = UIImage(systemName: status.visibility.unfilledImageName)
visibilityImageView.accessibilityLabel = String(format: NSLocalizedString("Visibility: %@", comment: "status visibility indicator accessibility label"), status.visibility.displayName)
}
let reblogButtonImage: UIImage
if Preferences.shared.alwaysShowStatusVisibilityIcon || reblogButton.isEnabled {
reblogButtonImage = UIImage(systemName: "repeat")!
} else {
reblogButtonImage = UIImage(systemName: status.visibility.imageName)!
}
reblogButton.setImage(reblogButtonImage, for: .normal)
}
override func prepareForReuse() { override func prepareForReuse() {
super.prepareForReuse() super.prepareForReuse()

View File

@ -89,12 +89,11 @@ class TimelineStatusTableViewCell: BaseStatusTableViewCell {
timestampLabel.isHidden = pinned timestampLabel.isHidden = pinned
pinImageView.isHidden = !pinned pinImageView.isHidden = !pinned
} }
replyImageView.isHidden = !showReplyIndicator || status.inReplyToID == nil
} }
@objc override func preferencesChanged() { @objc override func preferencesChanged() {
super.preferencesChanged() super.preferencesChanged()
if let rebloggerID = rebloggerID, if let rebloggerID = rebloggerID,
let reblogger = mastodonController.persistentContainer.account(for: rebloggerID) { let reblogger = mastodonController.persistentContainer.account(for: rebloggerID) {
updateRebloggerLabel(reblogger: reblogger) updateRebloggerLabel(reblogger: reblogger)
@ -111,6 +110,12 @@ class TimelineStatusTableViewCell: BaseStatusTableViewCell {
} }
} }
override func updateStatusIconsForPreferences(_ status: StatusMO) {
super.updateStatusIconsForPreferences(status)
replyImageView.isHidden = !Preferences.shared.showIsStatusReplyIcon || !showReplyIndicator || status.inReplyToID == nil
}
func updateTimestamp() { func updateTimestamp() {
// if the mastodonController is nil (i.e. the delegate is nil), then the screen this cell was a part of has been deallocated // if the mastodonController is nil (i.e. the delegate is nil), then the screen this cell was a part of has been deallocated
// so we bail out immediately, since there's nothing to update // so we bail out immediately, since there's nothing to update

View File

@ -151,36 +151,39 @@
</stackView> </stackView>
</subviews> </subviews>
</stackView> </stackView>
<imageView clipsSubviews="YES" userInteractionEnabled="NO" contentMode="scaleAspectFit" horizontalHuggingPriority="251" verticalHuggingPriority="251" image="globe" catalog="system" translatesAutoresizingMaskIntoConstraints="NO" id="LRh-Cc-1br"> <stackView opaque="NO" contentMode="scaleToFill" distribution="equalSpacing" spacing="5" translatesAutoresizingMaskIntoConstraints="NO" id="oie-wK-IpU">
<rect key="frame" x="31" y="55" width="19" height="20"/> <rect key="frame" x="0.5" y="54" width="49.5" height="22"/>
<color key="tintColor" systemColor="secondaryLabelColor" red="0.23529411759999999" green="0.23529411759999999" blue="0.26274509800000001" alpha="0.59999999999999998" colorSpace="custom" customColorSpace="sRGB"/> <subviews>
<constraints> <imageView clipsSubviews="YES" userInteractionEnabled="NO" contentMode="scaleAspectFit" horizontalHuggingPriority="251" verticalHuggingPriority="251" image="bubble.left.and.bubble.right" catalog="system" translatesAutoresizingMaskIntoConstraints="NO" id="KdQ-Zn-IhD">
<constraint firstAttribute="height" constant="22" id="3Mk-NN-6fY"/> <rect key="frame" x="0.0" y="1" width="25.5" height="21.5"/>
</constraints> <color key="tintColor" systemColor="secondaryLabelColor" red="0.23529411759999999" green="0.23529411759999999" blue="0.26274509800000001" alpha="0.59999999999999998" colorSpace="custom" customColorSpace="sRGB"/>
<preferredSymbolConfiguration key="preferredSymbolConfiguration" weight="thin"/> <accessibility key="accessibilityConfiguration" label="Is a reply"/>
</imageView> <constraints>
<imageView clipsSubviews="YES" userInteractionEnabled="NO" contentMode="scaleAspectFit" horizontalHuggingPriority="251" verticalHuggingPriority="251" image="bubble.left.and.bubble.right" catalog="system" translatesAutoresizingMaskIntoConstraints="NO" id="KdQ-Zn-IhD"> <constraint firstAttribute="height" constant="22" id="x0C-Qo-YVA"/>
<rect key="frame" x="0.0" y="55" width="25.5" height="21.5"/> </constraints>
<color key="tintColor" systemColor="secondaryLabelColor" red="0.23529411759999999" green="0.23529411759999999" blue="0.26274509800000001" alpha="0.59999999999999998" colorSpace="custom" customColorSpace="sRGB"/> <preferredSymbolConfiguration key="preferredSymbolConfiguration" weight="thin"/>
<accessibility key="accessibilityConfiguration" label="Is a reply"/> </imageView>
<constraints> <imageView clipsSubviews="YES" userInteractionEnabled="NO" contentMode="scaleAspectFit" horizontalHuggingPriority="251" verticalHuggingPriority="251" image="globe" catalog="system" translatesAutoresizingMaskIntoConstraints="NO" id="LRh-Cc-1br">
<constraint firstAttribute="height" constant="22" id="x0C-Qo-YVA"/> <rect key="frame" x="30.5" y="1" width="19" height="20"/>
</constraints> <color key="tintColor" systemColor="secondaryLabelColor" red="0.23529411759999999" green="0.23529411759999999" blue="0.26274509800000001" alpha="0.59999999999999998" colorSpace="custom" customColorSpace="sRGB"/>
<preferredSymbolConfiguration key="preferredSymbolConfiguration" weight="thin"/> <constraints>
</imageView> <constraint firstAttribute="height" constant="22" id="3Mk-NN-6fY"/>
</constraints>
<preferredSymbolConfiguration key="preferredSymbolConfiguration" weight="thin"/>
</imageView>
</subviews>
</stackView>
</subviews> </subviews>
<constraints> <constraints>
<constraint firstItem="gIY-Wp-RSk" firstAttribute="leading" secondItem="QMP-j2-HLn" secondAttribute="trailing" constant="8" id="0Tm-v7-Ts4"/> <constraint firstItem="gIY-Wp-RSk" firstAttribute="leading" secondItem="QMP-j2-HLn" secondAttribute="trailing" constant="8" id="0Tm-v7-Ts4"/>
<constraint firstAttribute="bottom" relation="greaterThanOrEqual" secondItem="QMP-j2-HLn" secondAttribute="bottom" constant="8" id="2Ao-Gj-fY3"/> <constraint firstAttribute="bottom" relation="greaterThanOrEqual" secondItem="QMP-j2-HLn" secondAttribute="bottom" constant="8" id="2Ao-Gj-fY3"/>
<constraint firstItem="oie-wK-IpU" firstAttribute="top" secondItem="QMP-j2-HLn" secondAttribute="bottom" constant="4" id="7Mp-WS-FhY"/>
<constraint firstItem="QMP-j2-HLn" firstAttribute="top" secondItem="ve3-Y1-NQH" secondAttribute="top" id="PC4-Bi-QXm"/> <constraint firstItem="QMP-j2-HLn" firstAttribute="top" secondItem="ve3-Y1-NQH" secondAttribute="top" id="PC4-Bi-QXm"/>
<constraint firstItem="LRh-Cc-1br" firstAttribute="top" secondItem="QMP-j2-HLn" secondAttribute="bottom" constant="4" id="QE5-0f-q1a"/> <constraint firstItem="oie-wK-IpU" firstAttribute="leading" relation="greaterThanOrEqual" secondItem="ve3-Y1-NQH" secondAttribute="leading" id="QKi-ny-jOJ"/>
<constraint firstItem="KdQ-Zn-IhD" firstAttribute="top" secondItem="QMP-j2-HLn" secondAttribute="bottom" constant="4" id="R2V-fr-WCN"/>
<constraint firstItem="gIY-Wp-RSk" firstAttribute="top" secondItem="QMP-j2-HLn" secondAttribute="top" id="fEd-wN-kuQ"/> <constraint firstItem="gIY-Wp-RSk" firstAttribute="top" secondItem="QMP-j2-HLn" secondAttribute="top" id="fEd-wN-kuQ"/>
<constraint firstItem="gIY-Wp-RSk" firstAttribute="leading" secondItem="oie-wK-IpU" secondAttribute="trailing" constant="8" id="fqd-p6-oGe"/>
<constraint firstAttribute="trailingMargin" secondItem="gIY-Wp-RSk" secondAttribute="trailing" id="hKk-kO-wFT"/> <constraint firstAttribute="trailingMargin" secondItem="gIY-Wp-RSk" secondAttribute="trailing" id="hKk-kO-wFT"/>
<constraint firstAttribute="bottom" secondItem="gIY-Wp-RSk" secondAttribute="bottom" id="kRU-Ct-CIg"/> <constraint firstAttribute="bottom" secondItem="gIY-Wp-RSk" secondAttribute="bottom" id="kRU-Ct-CIg"/>
<constraint firstItem="KdQ-Zn-IhD" firstAttribute="bottom" relation="lessThanOrEqual" secondItem="ve3-Y1-NQH" secondAttribute="bottom" id="rp8-N9-Iid"/>
<constraint firstItem="KdQ-Zn-IhD" firstAttribute="leading" secondItem="QMP-j2-HLn" secondAttribute="leading" id="uJd-Cz-AG3"/>
<constraint firstItem="gIY-Wp-RSk" firstAttribute="leading" secondItem="LRh-Cc-1br" secondAttribute="trailing" constant="8" id="zFc-5l-916"/>
<constraint firstItem="QMP-j2-HLn" firstAttribute="leading" secondItem="ve3-Y1-NQH" secondAttribute="leading" id="zeW-tQ-uJl"/> <constraint firstItem="QMP-j2-HLn" firstAttribute="leading" secondItem="ve3-Y1-NQH" secondAttribute="leading" id="zeW-tQ-uJl"/>
</constraints> </constraints>
</view> </view>