diff --git a/Tusker.xcodeproj/project.pbxproj b/Tusker.xcodeproj/project.pbxproj index 4be137ad..bbf451a4 100644 --- a/Tusker.xcodeproj/project.pbxproj +++ b/Tusker.xcodeproj/project.pbxproj @@ -13,6 +13,9 @@ D6333B372137838300CE884A /* AttributedString+Trim.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6333B362137838300CE884A /* AttributedString+Trim.swift */; }; D6333B772138D94E00CE884A /* ComposeMediaView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6333B762138D94E00CE884A /* ComposeMediaView.swift */; }; D6333B792139AEFD00CE884A /* Date+TimeAgo.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6333B782139AEFD00CE884A /* Date+TimeAgo.swift */; }; + D646C956213B365700269FB5 /* LargeImageExpandAnimationController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D646C955213B365700269FB5 /* LargeImageExpandAnimationController.swift */; }; + D646C958213B367000269FB5 /* LargeImageShrinkAnimationController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D646C957213B367000269FB5 /* LargeImageShrinkAnimationController.swift */; }; + D646C95A213B5D0500269FB5 /* LargeImageInteractionController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D646C959213B5D0500269FB5 /* LargeImageInteractionController.swift */; }; D64A0CD32132153900640E3B /* HTMLContentLabel.swift in Sources */ = {isa = PBXBuildFile; fileRef = D64A0CD22132153900640E3B /* HTMLContentLabel.swift */; }; D64D0AAD2128D88B005A6F37 /* LocalData.swift in Sources */ = {isa = PBXBuildFile; fileRef = D64D0AAC2128D88B005A6F37 /* LocalData.swift */; }; D64D0AAF2128D954005A6F37 /* Onboarding.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = D64D0AAE2128D954005A6F37 /* Onboarding.storyboard */; }; @@ -35,13 +38,16 @@ D667E5E921349EE50057A976 /* ProfileHeaderTableViewCell.xib in Resources */ = {isa = PBXBuildFile; fileRef = D667E5E821349EE50057A976 /* ProfileHeaderTableViewCell.xib */; }; D667E5EB21349EF80057A976 /* ProfileHeaderTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = D667E5EA21349EF80057A976 /* ProfileHeaderTableViewCell.swift */; }; D667E5EF2134C39F0057A976 /* StatusContentLabel.swift in Sources */ = {isa = PBXBuildFile; fileRef = D667E5EE2134C39F0057A976 /* StatusContentLabel.swift */; }; - D667E5F12134D5050057A976 /* UIViewController+StatusTableViewCellDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = D667E5F02134D5050057A976 /* UIViewController+StatusTableViewCellDelegate.swift */; }; + D667E5F12134D5050057A976 /* UIViewController+Delegates.swift in Sources */ = {isa = PBXBuildFile; fileRef = D667E5F02134D5050057A976 /* UIViewController+Delegates.swift */; }; D667E5F32135BC260057A976 /* Conversation.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = D667E5F22135BC260057A976 /* Conversation.storyboard */; }; D667E5F52135BCD50057A976 /* ConversationViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D667E5F42135BCD50057A976 /* ConversationViewController.swift */; }; D667E5F82135C3040057A976 /* Mastodon+Equatable.swift in Sources */ = {isa = PBXBuildFile; fileRef = D667E5F72135C3040057A976 /* Mastodon+Equatable.swift */; }; D6BED16F212663DA00F02DA0 /* SwiftSoup.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D6BED16E212663DA00F02DA0 /* SwiftSoup.framework */; }; D6BED170212663DA00F02DA0 /* SwiftSoup.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = D6BED16E212663DA00F02DA0 /* SwiftSoup.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; D6BED174212667E900F02DA0 /* StatusTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6BED173212667E900F02DA0 /* StatusTableViewCell.swift */; }; + D6C94D852139DFD800CB5196 /* LargeImage.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = D6C94D842139DFD800CB5196 /* LargeImage.storyboard */; }; + D6C94D872139E62700CB5196 /* LargeImageViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6C94D862139E62700CB5196 /* LargeImageViewController.swift */; }; + D6C94D892139E6EC00CB5196 /* AttachmentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6C94D882139E6EC00CB5196 /* AttachmentView.swift */; }; D6D4DDD0212518A000E1C4BB /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6D4DDCF212518A000E1C4BB /* AppDelegate.swift */; }; D6D4DDD5212518A000E1C4BB /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = D6D4DDD3212518A000E1C4BB /* Main.storyboard */; }; D6D4DDD7212518A200E1C4BB /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = D6D4DDD6212518A200E1C4BB /* Assets.xcassets */; }; @@ -94,6 +100,9 @@ D6333B362137838300CE884A /* AttributedString+Trim.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "AttributedString+Trim.swift"; sourceTree = ""; }; D6333B762138D94E00CE884A /* ComposeMediaView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComposeMediaView.swift; sourceTree = ""; }; D6333B782139AEFD00CE884A /* Date+TimeAgo.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Date+TimeAgo.swift"; sourceTree = ""; }; + D646C955213B365700269FB5 /* LargeImageExpandAnimationController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LargeImageExpandAnimationController.swift; sourceTree = ""; }; + D646C957213B367000269FB5 /* LargeImageShrinkAnimationController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LargeImageShrinkAnimationController.swift; sourceTree = ""; }; + D646C959213B5D0500269FB5 /* LargeImageInteractionController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LargeImageInteractionController.swift; sourceTree = ""; }; D64A0CD22132153900640E3B /* HTMLContentLabel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HTMLContentLabel.swift; sourceTree = ""; }; D64D0AAC2128D88B005A6F37 /* LocalData.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocalData.swift; sourceTree = ""; }; D64D0AAE2128D954005A6F37 /* Onboarding.storyboard */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; path = Onboarding.storyboard; sourceTree = ""; }; @@ -117,12 +126,15 @@ D667E5E821349EE50057A976 /* ProfileHeaderTableViewCell.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = ProfileHeaderTableViewCell.xib; sourceTree = ""; }; D667E5EA21349EF80057A976 /* ProfileHeaderTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfileHeaderTableViewCell.swift; sourceTree = ""; }; D667E5EE2134C39F0057A976 /* StatusContentLabel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatusContentLabel.swift; sourceTree = ""; }; - D667E5F02134D5050057A976 /* UIViewController+StatusTableViewCellDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIViewController+StatusTableViewCellDelegate.swift"; sourceTree = ""; }; + D667E5F02134D5050057A976 /* UIViewController+Delegates.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIViewController+Delegates.swift"; sourceTree = ""; }; D667E5F22135BC260057A976 /* Conversation.storyboard */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; path = Conversation.storyboard; sourceTree = ""; }; D667E5F42135BCD50057A976 /* ConversationViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConversationViewController.swift; sourceTree = ""; }; D667E5F72135C3040057A976 /* Mastodon+Equatable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Mastodon+Equatable.swift"; sourceTree = ""; }; D6BED16E212663DA00F02DA0 /* SwiftSoup.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = SwiftSoup.framework; sourceTree = BUILT_PRODUCTS_DIR; }; D6BED173212667E900F02DA0 /* StatusTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatusTableViewCell.swift; sourceTree = ""; }; + D6C94D842139DFD800CB5196 /* LargeImage.storyboard */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; path = LargeImage.storyboard; sourceTree = ""; }; + D6C94D862139E62700CB5196 /* LargeImageViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LargeImageViewController.swift; sourceTree = ""; }; + D6C94D882139E6EC00CB5196 /* AttachmentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AttachmentView.swift; sourceTree = ""; }; D6D4DDCC212518A000E1C4BB /* Tusker.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Tusker.app; sourceTree = BUILT_PRODUCTS_DIR; }; D6D4DDCF212518A000E1C4BB /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; D6D4DDD4212518A000E1C4BB /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; @@ -168,6 +180,16 @@ /* End PBXFrameworksBuildPhase section */ /* Begin PBXGroup section */ + D646C954213B364600269FB5 /* Transitions */ = { + isa = PBXGroup; + children = ( + D646C955213B365700269FB5 /* LargeImageExpandAnimationController.swift */, + D646C957213B367000269FB5 /* LargeImageShrinkAnimationController.swift */, + D646C959213B5D0500269FB5 /* LargeImageInteractionController.swift */, + ); + path = Transitions; + sourceTree = ""; + }; D663626021360A9600C9CBA2 /* Preferences */ = { isa = PBXGroup; children = ( @@ -181,7 +203,7 @@ D667E5F62135C2ED0057A976 /* Extensions */ = { isa = PBXGroup; children = ( - D667E5F02134D5050057A976 /* UIViewController+StatusTableViewCellDelegate.swift */, + D667E5F02134D5050057A976 /* UIViewController+Delegates.swift */, D667E5F72135C3040057A976 /* Mastodon+Equatable.swift */, D663626B21361C6700C9CBA2 /* Account+Preferences.swift */, D66362722136FFC600C9CBA2 /* UITextView+Placeholder.swift */, @@ -204,6 +226,7 @@ D663625C2135C74800C9CBA2 /* ConversationMainStatusTableViewCell.xib */, D663625E2135C75500C9CBA2 /* ConversationMainStatusTableViewCell.swift */, D6333B762138D94E00CE884A /* ComposeMediaView.swift */, + D6C94D882139E6EC00CB5196 /* AttachmentView.swift */, ); path = Views; sourceTree = ""; @@ -272,6 +295,7 @@ D6F953E9212519B800CF0F2B /* View Controllers */ = { isa = PBXGroup; children = ( + D646C954213B364600269FB5 /* Transitions */, D6F953EB212519E700CF0F2B /* TimelineTableViewController.swift */, D64D0AB02128D9AE005A6F37 /* OnboardingViewController.swift */, 04DACE8B212CB14B009840C4 /* MainTabBarViewController.swift */, @@ -279,6 +303,7 @@ D667E5F42135BCD50057A976 /* ConversationViewController.swift */, D663626721360E2C00C9CBA2 /* PreferencesTableViewController.swift */, D66362702136338600C9CBA2 /* ComposeViewController.swift */, + D6C94D862139E62700CB5196 /* LargeImageViewController.swift */, ); path = "View Controllers"; sourceTree = ""; @@ -293,6 +318,7 @@ D667E5F22135BC260057A976 /* Conversation.storyboard */, D663626521360DD700C9CBA2 /* Preferences.storyboard */, D663626E213632A000C9CBA2 /* Compose.storyboard */, + D6C94D842139DFD800CB5196 /* LargeImage.storyboard */, ); path = Storyboards; sourceTree = ""; @@ -418,6 +444,7 @@ D663625D2135C74800C9CBA2 /* ConversationMainStatusTableViewCell.xib in Resources */, D6F953EE21251A0700CF0F2B /* Timeline.storyboard in Resources */, D663626F213632A000C9CBA2 /* Compose.storyboard in Resources */, + D6C94D852139DFD800CB5196 /* LargeImage.storyboard in Resources */, D6D4DDD5212518A000E1C4BB /* Main.storyboard in Resources */, D667E5E3213499F70057A976 /* Profile.storyboard in Resources */, D663626621360DD700C9CBA2 /* Preferences.storyboard in Resources */, @@ -451,12 +478,16 @@ D667E5F52135BCD50057A976 /* ConversationViewController.swift in Sources */, D6F953F021251A2900CF0F2B /* MastodonController.swift in Sources */, D66362712136338600C9CBA2 /* ComposeViewController.swift in Sources */, + D646C958213B367000269FB5 /* LargeImageShrinkAnimationController.swift in Sources */, + D646C956213B365700269FB5 /* LargeImageExpandAnimationController.swift in Sources */, D667E5F82135C3040057A976 /* Mastodon+Equatable.swift in Sources */, 04DACE8E212CC7CC009840C4 /* AvatarCache.swift in Sources */, D6333B772138D94E00CE884A /* ComposeMediaView.swift in Sources */, D663626421360D2300C9CBA2 /* AvatarStyle.swift in Sources */, D6BED174212667E900F02DA0 /* StatusTableViewCell.swift in Sources */, D64D0AAD2128D88B005A6F37 /* LocalData.swift in Sources */, + D6C94D892139E6EC00CB5196 /* AttachmentView.swift in Sources */, + D6C94D872139E62700CB5196 /* LargeImageViewController.swift in Sources */, D663626221360B1900C9CBA2 /* Preferences.swift in Sources */, D6333B792139AEFD00CE884A /* Date+TimeAgo.swift in Sources */, D663626C21361C6700C9CBA2 /* Account+Preferences.swift in Sources */, @@ -466,11 +497,12 @@ D663626821360E2C00C9CBA2 /* PreferencesTableViewController.swift in Sources */, D66362732136FFC600C9CBA2 /* UITextView+Placeholder.swift in Sources */, D66362752137068A00C9CBA2 /* Visibility+Helpers.swift in Sources */, + D646C95A213B5D0500269FB5 /* LargeImageInteractionController.swift in Sources */, D6F953EC212519E700CF0F2B /* TimelineTableViewController.swift in Sources */, D663626A2136163000C9CBA2 /* PreferencesAdaptive.swift in Sources */, D667E5EB21349EF80057A976 /* ProfileHeaderTableViewCell.swift in Sources */, D64A0CD32132153900640E3B /* HTMLContentLabel.swift in Sources */, - D667E5F12134D5050057A976 /* UIViewController+StatusTableViewCellDelegate.swift in Sources */, + D667E5F12134D5050057A976 /* UIViewController+Delegates.swift in Sources */, D663625F2135C75500C9CBA2 /* ConversationMainStatusTableViewCell.swift in Sources */, D667E5E721349D4C0057A976 /* ProfileTableViewController.swift in Sources */, D6D4DDD0212518A000E1C4BB /* AppDelegate.swift in Sources */, diff --git a/Tusker/Assets.xcassets/Close.imageset/Close.pdf b/Tusker/Assets.xcassets/Close.imageset/Close.pdf new file mode 100644 index 00000000..f0e7f1e1 --- /dev/null +++ b/Tusker/Assets.xcassets/Close.imageset/Close.pdf @@ -0,0 +1,69 @@ +%PDF-1.5 +% +3 0 obj +<< /Length 4 0 R + /Filter /FlateDecode +>> +stream +x]K0 D>/18Բ( I[uQM:s_p< w"q#y\#лHEܑ#怞;Ħř"5kDe1(` {jF +)|8{8,8Ih >YW=şsJ '̉!* TJ;ku]=j`NMLo+ e +endstream +endobj +4 0 obj + 206 +endobj +2 0 obj +<< + /ExtGState << + /a0 << /CA 1 /ca 1 >> + >> +>> +endobj +5 0 obj +<< /Type /Page + /Parent 1 0 R + /MediaBox [ 0 0 208.76799 208.76799 ] + /Contents 3 0 R + /Group << + /Type /Group + /S /Transparency + /I true + /CS /DeviceRGB + >> + /Resources 2 0 R +>> +endobj +1 0 obj +<< /Type /Pages + /Kids [ 5 0 R ] + /Count 1 +>> +endobj +6 0 obj +<< /Creator (cairo 1.14.8 (http://cairographics.org)) + /Producer (cairo 1.14.8 (http://cairographics.org)) +>> +endobj +7 0 obj +<< /Type /Catalog + /Pages 1 0 R +>> +endobj +xref +0 8 +0000000000 65535 f +0000000618 00000 n +0000000320 00000 n +0000000015 00000 n +0000000298 00000 n +0000000392 00000 n +0000000683 00000 n +0000000810 00000 n +trailer +<< /Size 8 + /Root 7 0 R + /Info 6 0 R +>> +startxref +862 +%%EOF diff --git a/Tusker/Assets.xcassets/Close.imageset/Contents.json b/Tusker/Assets.xcassets/Close.imageset/Contents.json new file mode 100644 index 00000000..7f809da4 --- /dev/null +++ b/Tusker/Assets.xcassets/Close.imageset/Contents.json @@ -0,0 +1,24 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "Close.pdf", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + }, + "properties" : { + "template-rendering-intent" : "template" + } +} \ No newline at end of file diff --git a/Tusker/Assets.xcassets/Download.imageset/Contents.json b/Tusker/Assets.xcassets/Download.imageset/Contents.json new file mode 100644 index 00000000..f2c088ba --- /dev/null +++ b/Tusker/Assets.xcassets/Download.imageset/Contents.json @@ -0,0 +1,24 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "Download.pdf", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + }, + "properties" : { + "template-rendering-intent" : "template" + } +} \ No newline at end of file diff --git a/Tusker/Assets.xcassets/Download.imageset/Download.pdf b/Tusker/Assets.xcassets/Download.imageset/Download.pdf new file mode 100644 index 00000000..e4f6836f --- /dev/null +++ b/Tusker/Assets.xcassets/Download.imageset/Download.pdf @@ -0,0 +1,69 @@ +%PDF-1.5 +% +3 0 obj +<< /Length 4 0 R + /Filter /FlateDecode +>> +stream +x}RKnC1@1zIIɢ³Dކ7R>Rd@ndtXqT+"e"Rg.pf&;Q4/yKRK~IA Ŝ0 ]btKWe:`kV*Ԯ[x  pvT[= +HJiՅ35\zp;@̷DAEduna(ҵ-$63PR[ZmCv3t`ߔggut|)W axK@Zx=պfS#Ds SV]*R3ӝ9Yxz1 [)ə+lGk~d +endstream +endobj +4 0 obj + 395 +endobj +2 0 obj +<< + /ExtGState << + /a0 << /CA 1 /ca 1 >> + >> +>> +endobj +5 0 obj +<< /Type /Page + /Parent 1 0 R + /MediaBox [ 0 0 283.464569 283.464569 ] + /Contents 3 0 R + /Group << + /Type /Group + /S /Transparency + /I true + /CS /DeviceRGB + >> + /Resources 2 0 R +>> +endobj +1 0 obj +<< /Type /Pages + /Kids [ 5 0 R ] + /Count 1 +>> +endobj +6 0 obj +<< /Creator (cairo 1.14.8 (http://cairographics.org)) + /Producer (cairo 1.14.8 (http://cairographics.org)) +>> +endobj +7 0 obj +<< /Type /Catalog + /Pages 1 0 R +>> +endobj +xref +0 8 +0000000000 65535 f +0000000809 00000 n +0000000509 00000 n +0000000015 00000 n +0000000487 00000 n +0000000581 00000 n +0000000874 00000 n +0000001001 00000 n +trailer +<< /Size 8 + /Root 7 0 R + /Info 6 0 R +>> +startxref +1053 +%%EOF diff --git a/Tusker/AvatarCache.swift b/Tusker/AvatarCache.swift index c9ce0c1d..bc7ca328 100644 --- a/Tusker/AvatarCache.swift +++ b/Tusker/AvatarCache.swift @@ -32,9 +32,7 @@ class AvatarCache { } else { requestCallbacks[url] = [completion] let task = URLSession.shared.dataTask(with: url) { data, response, error in - guard error == nil, - let data = data, - let image = UIImage(data: data) else { + guard error == nil, let data = data, let image = UIImage(data: data) else { let callbacks = self.requestCallbacks.removeValue(forKey: url) callbacks?.forEach({ callback in // todo: default avatar for failed requests diff --git a/Tusker/Extensions/UIViewController+Delegates.swift b/Tusker/Extensions/UIViewController+Delegates.swift new file mode 100644 index 00000000..eae9396d --- /dev/null +++ b/Tusker/Extensions/UIViewController+Delegates.swift @@ -0,0 +1,113 @@ +// +// UIViewController+StatusTableViewCellDelegate.swift +// Tusker +// +// Created by Shadowfacts on 8/27/18. +// Copyright © 2018 Shadowfacts. All rights reserved. +// + +import UIKit +import MastodonKit +import SafariServices + +extension UIViewController: StatusTableViewCellDelegate { + + func selected(account: Account) { + // don't open if the account is the same as the current one + if let profileController = self as? ProfileTableViewController, + profileController.account == account { + return + } + + guard let navigationController = navigationController else { + fatalError("Can't show profile VC when not in navigation controller") + } + let vc = ProfileTableViewController.create(for: account) + navigationController.pushViewController(vc, animated: true) + } + + func selected(mention: Mention) { + + } + + func selected(tag: Tag) { + + } + + func selected(url: URL) { + let vc = SFSafariViewController(url: url) + present(vc, animated: true) + } + + func selected(status: Status) { + // don't open if the conversation is the same as the current one + if let conversationController = self as? ConversationViewController, + conversationController.mainStatus == status { + return + } + + guard let navigationController = navigationController else { + fatalError("Can't show conversation VC when not in navigation controller") + } + let vc = ConversationViewController.create(for: status) + navigationController.pushViewController(vc, animated: true) + } + + func reply(to status: Status) { + let vc = ComposeViewController.create(inReplyTo: status) + present(vc, animated: true) + } + + func showLargeAttachment(for attachmentView: AttachmentView) { + let vc = LargeImageViewController.create(image: attachmentView.image!, description: attachmentView.attachment.description) + vc.delegate = self + var frame = attachmentView.convert(attachmentView.bounds, to: view) + if let scrollView = view as? UIScrollView { + let scale = scrollView.zoomScale + let width = frame.width * scale + let height = frame.height * scale + let x = frame.minX * scale - scrollView.contentOffset.x + scrollView.frame.minX + let y = frame.minY * scale - scrollView.contentOffset.y + scrollView.frame.minY + frame = CGRect(x: x, y: y, width: width, height: height) + } + vc.originFrame = frame + vc.transitioningDelegate = self + present(vc, animated: true) + } + +} + +extension UIViewController: LargeImageViewControllerDelegate { + func closeLargeImage() { + dismiss(animated: true) + } +} + +extension UIViewController: UIViewControllerTransitioningDelegate { + public func animationController(forPresented presented: UIViewController, presenting: UIViewController, source: UIViewController) -> UIViewControllerAnimatedTransitioning? { + if presented is LargeImageViewController { + return LargeImageExpandAnimationController() + } else { + return nil + } + } + + public func animationController(forDismissed dismissed: UIViewController) -> UIViewControllerAnimatedTransitioning? { + if let dismissed = dismissed as? LargeImageViewController { + return LargeImageShrinkAnimationController(interactionController: dismissed.dismissInteractionController) + } else { + return nil + } + } + + public func interactionControllerForDismissal(using animator: UIViewControllerAnimatedTransitioning) -> UIViewControllerInteractiveTransitioning? { + if let animator = animator as? LargeImageShrinkAnimationController, + let interactionController = animator.interactionController, + interactionController.inProgress { + + return interactionController + } else { + return nil + } + } +} diff --git a/Tusker/Extensions/UIViewController+StatusTableViewCellDelegate.swift b/Tusker/Extensions/UIViewController+StatusTableViewCellDelegate.swift deleted file mode 100644 index 0282c90b..00000000 --- a/Tusker/Extensions/UIViewController+StatusTableViewCellDelegate.swift +++ /dev/null @@ -1,61 +0,0 @@ -// -// UIViewController+StatusTableViewCellDelegate.swift -// Tusker -// -// Created by Shadowfacts on 8/27/18. -// Copyright © 2018 Shadowfacts. All rights reserved. -// - -import UIKit -import MastodonKit -import SafariServices - -extension UIViewController: StatusTableViewCellDelegate { - - func selected(account: Account) { - // don't open if the account is the same as the current one - if let profileController = self as? ProfileTableViewController, - profileController.account == account { - return - } - - guard let navigationController = navigationController else { - fatalError("Can't show profile VC when not in navigation controller") - } - let vc = ProfileTableViewController.create(for: account) - navigationController.pushViewController(vc, animated: true) - } - - func selected(mention: Mention) { - - } - - func selected(tag: Tag) { - - } - - func selected(url: URL) { - let vc = SFSafariViewController(url: url) - present(vc, animated: true) - } - - func selected(status: Status) { - // don't open if the conversation is the same as the current one - if let conversationController = self as? ConversationViewController, - conversationController.mainStatus == status { - return - } - - guard let navigationController = navigationController else { - fatalError("Can't show conversation VC when not in navigation controller") - } - let vc = ConversationViewController.create(for: status) - navigationController.pushViewController(vc, animated: true) - } - - func reply(to status: Status) { - let vc = ComposeViewController.create(inReplyTo: status) - present(vc, animated: true) - } - -} diff --git a/Tusker/Info.plist b/Tusker/Info.plist index 4fd5c0d6..d23f5ebc 100644 --- a/Tusker/Info.plist +++ b/Tusker/Info.plist @@ -2,10 +2,12 @@ + NSPhotoLibraryAddUsageDescription + Save photos directly from other people's posts. NSCameraUsageDescription - To post photos from the camera. + Post photos from the camera. NSPhotoLibraryUsageDescription - To post photos from the photo library. + Post photos from the photo library. CFBundleDevelopmentRegion $(DEVELOPMENT_LANGUAGE) CFBundleExecutable diff --git a/Tusker/Storyboards/LargeImage.storyboard b/Tusker/Storyboards/LargeImage.storyboard new file mode 100644 index 00000000..f49180c9 --- /dev/null +++ b/Tusker/Storyboards/LargeImage.storyboard @@ -0,0 +1,152 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Tusker/View Controllers/LargeImageViewController.swift b/Tusker/View Controllers/LargeImageViewController.swift new file mode 100644 index 00000000..a50d7f80 --- /dev/null +++ b/Tusker/View Controllers/LargeImageViewController.swift @@ -0,0 +1,206 @@ +// +// LargeImageViewController.swift +// Tusker +// +// Created by Shadowfacts on 8/31/18. +// Copyright © 2018 Shadowfacts. All rights reserved. +// + +import UIKit +import Photos + +protocol LargeImageViewControllerDelegate { + func closeLargeImage() +} + +class LargeImageViewController: UIViewController, UIScrollViewDelegate { + + static func create(image: UIImage, description: String?) -> LargeImageViewController { + guard let vc = UIStoryboard(name: "LargeImage", bundle: nil).instantiateInitialViewController() as? LargeImageViewController else { fatalError() } + vc.image = image + vc.imageDescription = description + return vc + } + + var delegate: LargeImageViewControllerDelegate? + + var originFrame: CGRect? + var dismissInteractionController: LargeImageInteractionController? + + @IBOutlet weak var scrollView: UIScrollView! + @IBOutlet weak var imageView: UIImageView! + @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 topControlsHeightConstraint: NSLayoutConstraint! + @IBOutlet weak var downloadButton: UIButton! + @IBOutlet weak var downloadButtonTopConstraint: NSLayoutConstraint! + @IBOutlet weak var downloadButtonLeadingConstraint: NSLayoutConstraint! + @IBOutlet weak var closeButton: UIButton! + @IBOutlet weak var closeButtonTopConstraint: NSLayoutConstraint! + @IBOutlet weak var closeButtonTrailingConstraint: NSLayoutConstraint! + + @IBOutlet weak var bottomControlsView: UIView! + @IBOutlet weak var descriptionLabel: UILabel! + + var image: UIImage? + var imageDescription: String? + + var controlsVisible = true { + didSet { + UIView.animate(withDuration: 0.2) { + let topOffset = self.controlsVisible ? 0 : -self.topControlsView.bounds.height + self.topControlsView.transform = CGAffineTransform(translationX: 0, y: topOffset) + if self.imageDescription != nil { + let bottomOffset = self.controlsVisible ? 0 : self.bottomControlsView.bounds.height + self.view.safeAreaInsets.bottom + self.bottomControlsView.transform = CGAffineTransform(translationX: 0, y: bottomOffset) + } + } + } + } + + var prevZoomScale: CGFloat? + + override var prefersStatusBarHidden: Bool { + return true + } + + override func viewDidLoad() { + super.viewDidLoad() + + imageView.image = image + scrollView.delegate = self + imageView.bounds = CGRect(origin: .zero, size: image!.size) + + if let imageDescription = imageDescription { + descriptionLabel.text = imageDescription + } else { + bottomControlsView.isHidden = true + } + + // if running on iPhone X + if UIScreen.main.nativeBounds.height == 2436 { + topControlsHeightConstraint.constant = 35 + downloadButtonLeadingConstraint.constant = 35 + closeButtonTrailingConstraint.constant = 35 + } + + dismissInteractionController = LargeImageInteractionController(viewController: self) + } + + override func viewDidLayoutSubviews() { + let widthScale = view.bounds.size.width / imageView.bounds.width + let heightScale = view.bounds.size.height / imageView.bounds.height + let minScale = min(widthScale, heightScale) + scrollView.minimumZoomScale = minScale + scrollView.zoomScale = minScale + scrollView.maximumZoomScale = minScale >= 1 ? minScale + 2 : 2 + } + + func viewForZooming(in scrollView: UIScrollView) -> UIView? { + return imageView + } + + func scrollViewDidZoom(_ scrollView: UIScrollView) { + centerImage() + + let prevZoomScale = self.prevZoomScale ?? scrollView.minimumZoomScale + if scrollView.zoomScale <= scrollView.minimumZoomScale { + controlsVisible = true + } else if scrollView.zoomScale > prevZoomScale { + controlsVisible = false + } + self.prevZoomScale = scrollView.zoomScale + } + + func centerImage() { + let yOffset = max(0, (view.bounds.size.height - imageView.frame.height) / 2) + imageViewTopConstraint.constant = yOffset + imageViewBottomConstraint.constant = yOffset + + let xOffset = max(0, (view.bounds.size.width - imageView.frame.width) / 2) + imageViewLeadingConstraint.constant = xOffset + imageViewTrailingConstraint.constant = xOffset + } + + func zoomRectFor(scale: CGFloat, center: CGPoint) -> CGRect { + var zoomRect = CGRect.zero + zoomRect.size.width = imageView.frame.width / scale + zoomRect.size.height = imageView.frame.height / scale + let newCenter = scrollView.convert(center, to: imageView) + zoomRect.origin.x = newCenter.x - (zoomRect.width / 2) + zoomRect.origin.y = newCenter.y - (zoomRect.height / 2) + return zoomRect + } + + func animateZoomOut() { + UIView.animate(withDuration: 0.3, animations: { + self.scrollView.zoomScale = self.scrollView.minimumZoomScale + self.view.layoutIfNeeded() + }) + } + + /* + // MARK: - Navigation + + // In a storyboard-based application, you will often want to do a little preparation before navigation + override func prepare(for segue: UIStoryboardSegue, sender: Any?) { + // Get the new view controller using segue.destination. + // Pass the selected object to the new view controller. + } + */ + + @IBAction func scrollViewPressed(_ sender: UITapGestureRecognizer) { + if scrollView.zoomScale > scrollView.minimumZoomScale { + animateZoomOut() + } else { + controlsVisible = !controlsVisible + } + } + + @IBAction func scrollViewDoubleTapped(_ recognizer: UITapGestureRecognizer) { + if scrollView.zoomScale <= scrollView.minimumZoomScale { + let point = recognizer.location(in: recognizer.view) + let scale: CGFloat + if scrollView.minimumZoomScale < 1 { + if 1 - scrollView.zoomScale <= 0.5 { + scale = scrollView.zoomScale + 1 + } else { + scale = 1 + } + } else { + scale = scrollView.maximumZoomScale + } + let rect = zoomRectFor(scale: scale, center: point) + UIView.animate(withDuration: 0.3) { + self.scrollView.zoom(to: rect, animated: false) + self.view.layoutIfNeeded() + } + } else { + animateZoomOut() + } + } + + @IBAction func closeButtonPressed(_ sender: Any) { + delegate?.closeLargeImage() + } + + @IBAction func downloadPressed(_ sender: Any) { + guard let image = image else { return } + PHPhotoLibrary.shared().performChanges({ + PHAssetChangeRequest.creationRequestForAsset(from: image) + }, completionHandler: { success, error in + if success { + return + } else if let error = error { + print("Couldn't save photo: \(error)") + } else { + return + } + }) + } + +} diff --git a/Tusker/View Controllers/Transitions/LargeImageExpandAnimationController.swift b/Tusker/View Controllers/Transitions/LargeImageExpandAnimationController.swift new file mode 100644 index 00000000..eed9b007 --- /dev/null +++ b/Tusker/View Controllers/Transitions/LargeImageExpandAnimationController.swift @@ -0,0 +1,61 @@ +// +// LargeImageExpandAnimationController.swift +// Tusker +// +// Created by Shadowfacts on 9/1/18. +// Copyright © 2018 Shadowfacts. All rights reserved. +// + +import UIKit + +class LargeImageExpandAnimationController: NSObject, UIViewControllerAnimatedTransitioning { + + func transitionDuration(using transitionContext: UIViewControllerContextTransitioning?) -> TimeInterval { + return 0.2 + } + + func animateTransition(using transitionContext: UIViewControllerContextTransitioning) { + guard let fromVC = transitionContext.viewController(forKey: .from), + let toVC = transitionContext.viewController(forKey: .to) as? LargeImageViewController, + let originFrame = toVC.originFrame else { + return + } + + let containerView = transitionContext.containerView + let finalVCFrame = transitionContext.finalFrame(for: toVC) + let image = toVC.image! + let ratio = image.size.width / image.size.height + let width = finalVCFrame.width + let height = width / ratio + let finalFrame = CGRect(x: finalVCFrame.midX - width / 2, y: finalVCFrame.midY - height / 2, width: width, height: height) + + let imageView = UIImageView(frame: originFrame) + imageView.image = toVC.image! + imageView.contentMode = .scaleAspectFill + imageView.layer.cornerRadius = 5 + imageView.layer.masksToBounds = true + + let blackView = UIView(frame: finalVCFrame) + blackView.backgroundColor = .black + blackView.alpha = 0 + + containerView.addSubview(toVC.view) + containerView.addSubview(blackView) + containerView.addSubview(imageView) + + toVC.view.isHidden = true + + let duration = transitionDuration(using: transitionContext) + UIView.animate(withDuration: duration, animations: { + imageView.frame = finalFrame + blackView.alpha = 1 + }, completion: { _ in + toVC.view.isHidden = false + fromVC.view.isHidden = false + blackView.removeFromSuperview() + imageView.removeFromSuperview() + transitionContext.completeTransition(!transitionContext.transitionWasCancelled) + }) + } + +} diff --git a/Tusker/View Controllers/Transitions/LargeImageInteractionController.swift b/Tusker/View Controllers/Transitions/LargeImageInteractionController.swift new file mode 100644 index 00000000..0cac25c1 --- /dev/null +++ b/Tusker/View Controllers/Transitions/LargeImageInteractionController.swift @@ -0,0 +1,60 @@ +// +// LargeImageInteractionController.swift +// Tusker +// +// Created by Shadowfacts on 9/1/18. +// Copyright © 2018 Shadowfacts. All rights reserved. +// + +import UIKit + +class LargeImageInteractionController: UIPercentDrivenInteractiveTransition { + + var inProgress = false + var direction: CGFloat? + + var shouldCompleteTransition = false + private weak var viewController: UIViewController! + + init(viewController: UIViewController) { + super.init() + self.viewController = viewController + viewController.view.addGestureRecognizer(UIPanGestureRecognizer(target: self, action: #selector(handleGesture(_:)))) + } + + @objc func handleGesture(_ recognizer: UIPanGestureRecognizer) { + let translation = recognizer.translation(in: recognizer.view!.superview!) + var progress = translation.y / 200 + if let direction = direction { + progress *= direction + } else { + direction = progress > 0 ? 1 : -1 + progress = abs(progress) + } + progress = min(max(progress, 0), 1) + let velocity = abs(recognizer.velocity(in: recognizer.view!.superview!).y) + + switch recognizer.state { + case .began: + inProgress = true + viewController.dismiss(animated: true) + case .changed: + shouldCompleteTransition = progress > 0.5 || velocity > 1000 + update(progress) + case .cancelled: + inProgress = false + cancel() + case .ended: + inProgress = false + direction = nil + if shouldCompleteTransition { + finish() + } else { + cancel() + } + default: + break + } + } + +} diff --git a/Tusker/View Controllers/Transitions/LargeImageShrinkAnimationController.swift b/Tusker/View Controllers/Transitions/LargeImageShrinkAnimationController.swift new file mode 100644 index 00000000..0638d65c --- /dev/null +++ b/Tusker/View Controllers/Transitions/LargeImageShrinkAnimationController.swift @@ -0,0 +1,67 @@ +// +// LargeImageShrinkAnimationController.swift +// Tusker +// +// Created by Shadowfacts on 9/1/18. +// Copyright © 2018 Shadowfacts. All rights reserved. +// + +import UIKit + +class LargeImageShrinkAnimationController: NSObject, UIViewControllerAnimatedTransitioning { + + let interactionController: LargeImageInteractionController? + + init(interactionController: LargeImageInteractionController?) { + self.interactionController = interactionController + } + + func transitionDuration(using transitionContext: UIViewControllerContextTransitioning?) -> TimeInterval { + return 0.2 + } + + func animateTransition(using transitionContext: UIViewControllerContextTransitioning) { + guard let fromVC = transitionContext.viewController(forKey: .from) as? LargeImageViewController, + let toVC = transitionContext.viewController(forKey: .to), + let finalFrame = fromVC.originFrame else { + return + } + + let originalVCFrame = fromVC.view.frame + + let containerView = transitionContext.containerView + let image = fromVC.image! + let ratio = image.size.width / image.size.height + let width = originalVCFrame.width + let height = width / ratio + let originalFrame = CGRect(x: originalVCFrame.midX - width / 2, y: originalVCFrame.midY - height / 2, width: width, height: height) + + let imageView = UIImageView(frame: originalFrame) + imageView.image = fromVC.image! + imageView.contentMode = .scaleAspectFill + imageView.layer.cornerRadius = 5 + imageView.layer.masksToBounds = true + + let blackView = UIView(frame: originalVCFrame) + blackView.backgroundColor = .black + blackView.alpha = 1 + + containerView.addSubview(toVC.view) + containerView.addSubview(blackView) + containerView.addSubview(imageView) + + let duration = transitionDuration(using: transitionContext) + UIView.animate(withDuration: duration, animations: { + imageView.frame = finalFrame + blackView.alpha = 0 + }, completion: { _ in + blackView.removeFromSuperview() + imageView.removeFromSuperview() + if transitionContext.transitionWasCancelled { + toVC.view.removeFromSuperview() + } + transitionContext.completeTransition(!transitionContext.transitionWasCancelled) + }) + } + +} diff --git a/Tusker/Views/AttachmentView.swift b/Tusker/Views/AttachmentView.swift new file mode 100644 index 00000000..cdf4d7ab --- /dev/null +++ b/Tusker/Views/AttachmentView.swift @@ -0,0 +1,64 @@ +// +// AttachmentView.swift +// Tusker +// +// Created by Shadowfacts on 8/31/18. +// Copyright © 2018 Shadowfacts. All rights reserved. +// + +import UIKit +import MastodonKit + +protocol AttachmentViewDelegate { + func showLargeAttachment(for attachmentView: AttachmentView) +} + +class AttachmentView: UIImageView { + + var delegate: AttachmentViewDelegate? + + var attachment: Attachment! + + var task: URLSessionDataTask? + + required init?(coder aDecoder: NSCoder) { + super.init(coder: aDecoder) + commonInit() + } + + override init(frame: CGRect) { + super.init(frame: frame) + commonInit() + } + + convenience init(frame: CGRect, attachment: Attachment) { + self.init(frame: frame) + self.attachment = attachment + loadImage() + } + + func commonInit() { + contentMode = .scaleAspectFill + layer.masksToBounds = true + isUserInteractionEnabled = true + addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(imagePressed))) + } + + func loadImage() { + guard let url = URL(string: attachment.url) else { fatalError("Invalid URL: \(attachment.url)") } + task = URLSession.shared.dataTask(with: url) { data, response, error in + guard error == nil, let data = data, let image = UIImage(data: data) else { return } + DispatchQueue.main.async { + self.image = image + } + } + task!.resume() + } + + @objc func imagePressed() { + if image != nil { + delegate?.showLargeAttachment(for: self) + } + } + +} diff --git a/Tusker/Views/StatusTableViewCell.swift b/Tusker/Views/StatusTableViewCell.swift index 8e064b7f..453fefa0 100644 --- a/Tusker/Views/StatusTableViewCell.swift +++ b/Tusker/Views/StatusTableViewCell.swift @@ -23,6 +23,8 @@ protocol StatusTableViewCellDelegate { func reply(to status: Status) + func showLargeAttachment(for attachmentView: AttachmentView) + } class StatusTableViewCell: UITableViewCell, PreferencesAdaptive { @@ -35,6 +37,7 @@ class StatusTableViewCell: UITableViewCell, PreferencesAdaptive { @IBOutlet weak var avatarImageView: UIImageView! @IBOutlet weak var reblogLabel: UILabel! @IBOutlet weak var timestampLabel: UILabel! + @IBOutlet weak var attachmentsView: UIView! var status: Status! var account: Account! @@ -44,6 +47,8 @@ class StatusTableViewCell: UITableViewCell, PreferencesAdaptive { var updateTimestampWorkItem: DispatchWorkItem? + var attachmentDataTasks: [URLSessionDataTask] = [] + override func awakeFromNib() { displayNameLabel.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(accountPressed))) displayNameLabel.isUserInteractionEnabled = true @@ -54,6 +59,8 @@ class StatusTableViewCell: UITableViewCell, PreferencesAdaptive { avatarImageView.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(accountPressed))) avatarImageView.isUserInteractionEnabled = true avatarImageView.layer.masksToBounds = true + attachmentsView.layer.cornerRadius = 5 + attachmentsView.layer.masksToBounds = true } func updateUIForPreferences() { @@ -95,6 +102,35 @@ class StatusTableViewCell: UITableViewCell, PreferencesAdaptive { } updateTimestamp() + attachmentsView.subviews.forEach { $0.removeFromSuperview() } + let attachments = status.mediaAttachments.filter({ $0.type == .image }) + if attachments.count > 0 { + attachmentsView.isHidden = false + let width = attachmentsView.bounds.width + let height: CGFloat = 200 + switch attachments.count { + case 1: + addAttachmentView(frame: CGRect(x: 0, y: 0, width: width, height: height), attachment: attachments[0]) + case 2: + addAttachmentView(frame: CGRect(x: 0, y: 0, width: width / 2 - 4, height: height), attachment: attachments[0]) + addAttachmentView(frame: CGRect(x: width / 2 + 4, y: 0, width: width / 2 - 4, height: height), attachment: attachments[1]) + case 3: + addAttachmentView(frame: CGRect(x: 0, y: 0, width: width / 2 - 4, height: height), attachment: attachments[0]) + addAttachmentView(frame: CGRect(x: width / 2 + 4, y: 0, width: width / 2 - 4, height: height / 2 - 4), attachment: attachments[1]) + addAttachmentView(frame: CGRect(x: width / 2 + 4, y: height / 2 + 4, width: width / 2 - 4, height: height / 2 - 4), attachment: attachments[2]) + case 4: + addAttachmentView(frame: CGRect(x: 0, y: 0, width: width / 2 - 4, height: height / 2 - 4), attachment: attachments[0]) + addAttachmentView(frame: CGRect(x: 0, y: height / 2 + 4, width: width / 2 - 4, height: height / 2 - 4), attachment: attachments[1]) + addAttachmentView(frame: CGRect(x: width / 2 + 4, y: 0, width: width / 2 - 4, height: height / 2 - 4), attachment: attachments[2]) + addAttachmentView(frame: CGRect(x: width / 2 + 4, y: height / 2 + 4, width: width / 2 - 4, height: height / 2 - 4), attachment: attachments[3]) + default: + fatalError("Too many attachments") + } + } else { + attachmentsView.isHidden = true + + } + contentLabel.status = status contentLabel.delegate = self } @@ -120,12 +156,21 @@ class StatusTableViewCell: UITableViewCell, PreferencesAdaptive { } } + func addAttachmentView(frame: CGRect, attachment: Attachment) { + let attachmentView = AttachmentView(frame: frame, attachment: attachment) + attachmentView.delegate = self + attachmentsView.addSubview(attachmentView) + } + override func prepareForReuse() { if let url = avatarURL { AvatarCache.shared.cancel(url) } updateTimestampWorkItem?.cancel() updateTimestampWorkItem = nil + attachmentsView.subviews.forEach { view in + (view as? AttachmentView)?.task?.cancel() + } } @IBAction func replyPressed(_ sender: Any) { @@ -162,3 +207,9 @@ extension StatusTableViewCell: HTMLContentLabelDelegate { } } + +extension StatusTableViewCell: AttachmentViewDelegate { + func showLargeAttachment(for attachmentView: AttachmentView) { + delegate?.showLargeAttachment(for: attachmentView) + } +} diff --git a/Tusker/Views/StatusTableViewCell.xib b/Tusker/Views/StatusTableViewCell.xib index f956bc5b..0f00d84b 100644 --- a/Tusker/Views/StatusTableViewCell.xib +++ b/Tusker/Views/StatusTableViewCell.xib @@ -15,11 +15,11 @@ - +