From fc69b9684bbd4359a9bbaaf5ffb2c6761de8d059 Mon Sep 17 00:00:00 2001 From: Shadowfacts Date: Thu, 30 Aug 2018 22:30:19 -0400 Subject: [PATCH] Initial implementation Compose UI --- Tusker.xcodeproj/project.pbxproj | 24 ++ Tusker/AppDelegate.swift | 1 + .../Remove.imageset/Contents.json | 21 ++ .../Remove.imageset/Remove.pdf | 68 +++++ Tusker/Controllers/MastodonController.swift | 10 + .../Extensions/UITextView+Placeholder.swift | 66 +++++ ...ntroller+StatusTableViewCellDelegate.swift | 5 + Tusker/Extensions/Visibility+Helpers.swift | 30 +++ Tusker/Info.plist | 4 + Tusker/Preferences/Preferences.swift | 3 + Tusker/Storyboards/Compose.storyboard | 208 +++++++++++++++ Tusker/Storyboards/Profile.storyboard | 9 +- Tusker/Storyboards/Timeline.storyboard | 16 +- .../ComposeViewController.swift | 245 ++++++++++++++++++ .../MainTabBarViewController.swift | 3 + .../ProfileTableViewController.swift | 14 +- Tusker/Views/ComposeMediaView.swift | 44 ++++ .../ConversationMainStatusTableViewCell.swift | 4 + .../ConversationMainStatusTableViewCell.xib | 103 +++++--- Tusker/Views/StatusTableViewCell.swift | 6 + Tusker/Views/StatusTableViewCell.xib | 32 ++- 21 files changed, 866 insertions(+), 50 deletions(-) create mode 100644 Tusker/Assets.xcassets/Remove.imageset/Contents.json create mode 100644 Tusker/Assets.xcassets/Remove.imageset/Remove.pdf create mode 100644 Tusker/Extensions/UITextView+Placeholder.swift create mode 100644 Tusker/Extensions/Visibility+Helpers.swift create mode 100644 Tusker/Storyboards/Compose.storyboard create mode 100644 Tusker/View Controllers/ComposeViewController.swift create mode 100644 Tusker/Views/ComposeMediaView.swift diff --git a/Tusker.xcodeproj/project.pbxproj b/Tusker.xcodeproj/project.pbxproj index ee39bbfa..68b3027b 100644 --- a/Tusker.xcodeproj/project.pbxproj +++ b/Tusker.xcodeproj/project.pbxproj @@ -10,6 +10,8 @@ 04DACE8A212CA6B7009840C4 /* Timeline.swift in Sources */ = {isa = PBXBuildFile; fileRef = 04DACE89212CA6B7009840C4 /* Timeline.swift */; }; 04DACE8C212CB14B009840C4 /* MainTabBarViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 04DACE8B212CB14B009840C4 /* MainTabBarViewController.swift */; }; 04DACE8E212CC7CC009840C4 /* AvatarCache.swift in Sources */ = {isa = PBXBuildFile; fileRef = 04DACE8D212CC7CC009840C4 /* AvatarCache.swift */; }; + D6333B372137838300CE884A /* AttributedString+Trim.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6333B362137838300CE884A /* AttributedString+Trim.swift */; }; + D6333B772138D94E00CE884A /* ComposeMediaView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6333B762138D94E00CE884A /* ComposeMediaView.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 */; }; @@ -22,6 +24,10 @@ D663626821360E2C00C9CBA2 /* PreferencesTableViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D663626721360E2C00C9CBA2 /* PreferencesTableViewController.swift */; }; D663626A2136163000C9CBA2 /* PreferencesAdaptive.swift in Sources */ = {isa = PBXBuildFile; fileRef = D66362692136163000C9CBA2 /* PreferencesAdaptive.swift */; }; D663626C21361C6700C9CBA2 /* Account+Preferences.swift in Sources */ = {isa = PBXBuildFile; fileRef = D663626B21361C6700C9CBA2 /* Account+Preferences.swift */; }; + D663626F213632A000C9CBA2 /* Compose.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = D663626E213632A000C9CBA2 /* Compose.storyboard */; }; + D66362712136338600C9CBA2 /* ComposeViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D66362702136338600C9CBA2 /* ComposeViewController.swift */; }; + D66362732136FFC600C9CBA2 /* UITextView+Placeholder.swift in Sources */ = {isa = PBXBuildFile; fileRef = D66362722136FFC600C9CBA2 /* UITextView+Placeholder.swift */; }; + D66362752137068A00C9CBA2 /* Visibility+Helpers.swift in Sources */ = {isa = PBXBuildFile; fileRef = D66362742137068A00C9CBA2 /* Visibility+Helpers.swift */; }; D667E5E12134937B0057A976 /* StatusTableViewCell.xib in Resources */ = {isa = PBXBuildFile; fileRef = D667E5E02134937B0057A976 /* StatusTableViewCell.xib */; }; D667E5E3213499F70057A976 /* Profile.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = D667E5E2213499F70057A976 /* Profile.storyboard */; }; D667E5E721349D4C0057A976 /* ProfileTableViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D667E5E621349D4C0057A976 /* ProfileTableViewController.swift */; }; @@ -84,6 +90,8 @@ 04DACE89212CA6B7009840C4 /* Timeline.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Timeline.swift; sourceTree = ""; }; 04DACE8B212CB14B009840C4 /* MainTabBarViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainTabBarViewController.swift; sourceTree = ""; }; 04DACE8D212CC7CC009840C4 /* AvatarCache.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AvatarCache.swift; sourceTree = ""; }; + 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 = ""; }; 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 = ""; }; @@ -97,6 +105,10 @@ D66362692136163000C9CBA2 /* PreferencesAdaptive.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PreferencesAdaptive.swift; sourceTree = ""; }; D663626B21361C6700C9CBA2 /* Account+Preferences.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Account+Preferences.swift"; sourceTree = ""; }; D663626D213629B300C9CBA2 /* MyPlayground.playground */ = {isa = PBXFileReference; lastKnownFileType = file.playground; path = MyPlayground.playground; sourceTree = ""; xcLanguageSpecificationIdentifier = xcode.lang.swift; }; + D663626E213632A000C9CBA2 /* Compose.storyboard */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; path = Compose.storyboard; sourceTree = ""; }; + D66362702136338600C9CBA2 /* ComposeViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComposeViewController.swift; sourceTree = ""; }; + D66362722136FFC600C9CBA2 /* UITextView+Placeholder.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UITextView+Placeholder.swift"; sourceTree = ""; }; + D66362742137068A00C9CBA2 /* Visibility+Helpers.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Visibility+Helpers.swift"; sourceTree = ""; }; D667E5E02134937B0057A976 /* StatusTableViewCell.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = StatusTableViewCell.xib; sourceTree = ""; }; D667E5E2213499F70057A976 /* Profile.storyboard */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; path = Profile.storyboard; sourceTree = ""; }; D667E5E621349D4C0057A976 /* ProfileTableViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfileTableViewController.swift; sourceTree = ""; }; @@ -170,6 +182,9 @@ D667E5F02134D5050057A976 /* UIViewController+StatusTableViewCellDelegate.swift */, D667E5F72135C3040057A976 /* Mastodon+Equatable.swift */, D663626B21361C6700C9CBA2 /* Account+Preferences.swift */, + D66362722136FFC600C9CBA2 /* UITextView+Placeholder.swift */, + D66362742137068A00C9CBA2 /* Visibility+Helpers.swift */, + D6333B362137838300CE884A /* AttributedString+Trim.swift */, ); path = Extensions; sourceTree = ""; @@ -185,6 +200,7 @@ D667E5EA21349EF80057A976 /* ProfileHeaderTableViewCell.swift */, D663625C2135C74800C9CBA2 /* ConversationMainStatusTableViewCell.xib */, D663625E2135C75500C9CBA2 /* ConversationMainStatusTableViewCell.swift */, + D6333B762138D94E00CE884A /* ComposeMediaView.swift */, ); path = Views; sourceTree = ""; @@ -259,6 +275,7 @@ D667E5E621349D4C0057A976 /* ProfileTableViewController.swift */, D667E5F42135BCD50057A976 /* ConversationViewController.swift */, D663626721360E2C00C9CBA2 /* PreferencesTableViewController.swift */, + D66362702136338600C9CBA2 /* ComposeViewController.swift */, ); path = "View Controllers"; sourceTree = ""; @@ -272,6 +289,7 @@ D667E5E2213499F70057A976 /* Profile.storyboard */, D667E5F22135BC260057A976 /* Conversation.storyboard */, D663626521360DD700C9CBA2 /* Preferences.storyboard */, + D663626E213632A000C9CBA2 /* Compose.storyboard */, ); path = Storyboards; sourceTree = ""; @@ -396,6 +414,7 @@ D6D4DDD7212518A200E1C4BB /* Assets.xcassets in Resources */, D663625D2135C74800C9CBA2 /* ConversationMainStatusTableViewCell.xib in Resources */, D6F953EE21251A0700CF0F2B /* Timeline.storyboard in Resources */, + D663626F213632A000C9CBA2 /* Compose.storyboard in Resources */, D6D4DDD5212518A000E1C4BB /* Main.storyboard in Resources */, D667E5E3213499F70057A976 /* Profile.storyboard in Resources */, D663626621360DD700C9CBA2 /* Preferences.storyboard in Resources */, @@ -428,16 +447,21 @@ 04DACE8C212CB14B009840C4 /* MainTabBarViewController.swift in Sources */, D667E5F52135BCD50057A976 /* ConversationViewController.swift in Sources */, D6F953F021251A2900CF0F2B /* MastodonController.swift in Sources */, + D66362712136338600C9CBA2 /* ComposeViewController.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 */, D663626221360B1900C9CBA2 /* Preferences.swift in Sources */, D663626C21361C6700C9CBA2 /* Account+Preferences.swift in Sources */, + D6333B372137838300CE884A /* AttributedString+Trim.swift in Sources */, D667E5EF2134C39F0057A976 /* StatusContentLabel.swift in Sources */, D64D0AB12128D9AE005A6F37 /* OnboardingViewController.swift in Sources */, D663626821360E2C00C9CBA2 /* PreferencesTableViewController.swift in Sources */, + D66362732136FFC600C9CBA2 /* UITextView+Placeholder.swift in Sources */, + D66362752137068A00C9CBA2 /* Visibility+Helpers.swift in Sources */, D6F953EC212519E700CF0F2B /* TimelineTableViewController.swift in Sources */, D663626A2136163000C9CBA2 /* PreferencesAdaptive.swift in Sources */, D667E5EB21349EF80057A976 /* ProfileHeaderTableViewCell.swift in Sources */, diff --git a/Tusker/AppDelegate.swift b/Tusker/AppDelegate.swift index 54e534c1..51e3a3e9 100644 --- a/Tusker/AppDelegate.swift +++ b/Tusker/AppDelegate.swift @@ -18,6 +18,7 @@ class AppDelegate: UIResponder, UIApplicationDelegate { if LocalData.shared.onboardingComplete { MastodonController.shared.createClient() + MastodonController.shared.getOwnAccount() } else { showOnboarding() } diff --git a/Tusker/Assets.xcassets/Remove.imageset/Contents.json b/Tusker/Assets.xcassets/Remove.imageset/Contents.json new file mode 100644 index 00000000..d2cdfd45 --- /dev/null +++ b/Tusker/Assets.xcassets/Remove.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "Remove.pdf", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Tusker/Assets.xcassets/Remove.imageset/Remove.pdf b/Tusker/Assets.xcassets/Remove.imageset/Remove.pdf new file mode 100644 index 00000000..57c67336 --- /dev/null +++ b/Tusker/Assets.xcassets/Remove.imageset/Remove.pdf @@ -0,0 +1,68 @@ +%PDF-1.5 +%µí®û +3 0 obj +<< /Length 4 0 R + /Filter /FlateDecode +>> +stream +xœePËN1 ¼û+æ0Žóò^{©„Ä¡\´¨­=ý}œlV° +rÆž;¹’ Åç ÷¯‚Ó´xÚ£rÆÍÛ~Þéù‚7JxÄÕMu×’ —J1̪…Í"’°—pÁÀE8]Ó–4Ð &D®yUôûàÏ ì‘~ú}^¥Ôá²ñcxþ·÷‡ñþ3Á¼ªÈ•UÔ›¹”r–Š”ÈqòU”«NŽ³±XA¨œ:>Ó_‡c«$K]cæ×h¶îYÂâ™Ë¢èØgV›º§ï9<7G:Ð7~s[O +endstream +endobj +4 0 obj + 226 +endobj +2 0 obj +<< + /ExtGState << + /a0 << /CA 1 /ca 1 >> + >> +>> +endobj +5 0 obj +<< /Type /Page + /Parent 1 0 R + /MediaBox [ 0 0 80.631668 80.631668 ] + /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 +0000000638 00000 n +0000000340 00000 n +0000000015 00000 n +0000000318 00000 n +0000000412 00000 n +0000000703 00000 n +0000000830 00000 n +trailer +<< /Size 8 + /Root 7 0 R + /Info 6 0 R +>> +startxref +882 +%%EOF diff --git a/Tusker/Controllers/MastodonController.swift b/Tusker/Controllers/MastodonController.swift index fa23084f..19c1bd86 100644 --- a/Tusker/Controllers/MastodonController.swift +++ b/Tusker/Controllers/MastodonController.swift @@ -15,6 +15,8 @@ class MastodonController { var client: Client! + var account: Account! + private init() { } @@ -55,4 +57,12 @@ class MastodonController { } } + func getOwnAccount() { + let req = Accounts.currentUser() + client.run(req) { result in + guard case let .success(account, _) = result else { fatalError() } + self.account = account + } + } + } diff --git a/Tusker/Extensions/UITextView+Placeholder.swift b/Tusker/Extensions/UITextView+Placeholder.swift new file mode 100644 index 00000000..110403a2 --- /dev/null +++ b/Tusker/Extensions/UITextView+Placeholder.swift @@ -0,0 +1,66 @@ +// +// PlaceholderTextView.swift +// Tusker +// +// Created by Shadowfacts on 8/29/18. +// Copyright © 2018 Shadowfacts. All rights reserved. +// + +import UIKit + +// Source: https://finnwea.com/blog/adding-placeholders-to-uitextviews-in-swift/ +extension UITextView: UITextViewDelegate { + + override open var bounds: CGRect { + didSet { + resizePlaceholder() + } + } + + var placeholder: String? { + get { + return (viewWithTag(100) as? UILabel)?.text + } + set { + if let placeholderLabel = viewWithTag(100) as? UILabel { + placeholderLabel.text = newValue + placeholderLabel.sizeToFit() + } else { + guard let newValue = newValue else { return } + addPlaceholder(newValue) + } + } + } + + public func textViewDidChange(_ textView: UITextView) { + if let placeholderLabel = viewWithTag(100) as? UILabel { + placeholderLabel.isHidden = !text.isEmpty + } + } + + private func resizePlaceholder() { + guard let placeholderLabel = viewWithTag(100) as? UILabel else { fatalError() } + let labelX = textContainer.lineFragmentPadding + let labelY = textContainerInset.top + let labelWidth = frame.width - (labelX * 2) + let labelHeight = placeholderLabel.frame.height + placeholderLabel.frame = CGRect(x: labelX, y: labelY, width: labelWidth, height: labelHeight) + } + + private func addPlaceholder(_ placeholderText: String) { + let placeholderLabel = UILabel() + + placeholderLabel.text = placeholderText + placeholderLabel.sizeToFit() + placeholderLabel.font = font + placeholderLabel.textColor = .lightGray + placeholderLabel.tag = 100 + + placeholderLabel.isHighlighted = !text.isEmpty + + addSubview(placeholderLabel) + resizePlaceholder() + delegate = self + + } +} diff --git a/Tusker/Extensions/UIViewController+StatusTableViewCellDelegate.swift b/Tusker/Extensions/UIViewController+StatusTableViewCellDelegate.swift index 1dc506d5..0282c90b 100644 --- a/Tusker/Extensions/UIViewController+StatusTableViewCellDelegate.swift +++ b/Tusker/Extensions/UIViewController+StatusTableViewCellDelegate.swift @@ -53,4 +53,9 @@ extension UIViewController: StatusTableViewCellDelegate { navigationController.pushViewController(vc, animated: true) } + func reply(to status: Status) { + let vc = ComposeViewController.create(inReplyTo: status) + present(vc, animated: true) + } + } diff --git a/Tusker/Extensions/Visibility+Helpers.swift b/Tusker/Extensions/Visibility+Helpers.swift new file mode 100644 index 00000000..188f4e21 --- /dev/null +++ b/Tusker/Extensions/Visibility+Helpers.swift @@ -0,0 +1,30 @@ +// +// Visibility+String.swift +// Tusker +// +// Created by Shadowfacts on 8/29/18. +// Copyright © 2018 Shadowfacts. All rights reserved. +// + +import MastodonKit + +extension Visibility { + + static var allCases: [Visibility] { + return [.public, .unlisted, .private, .direct] + } + + var displayName: String { + switch self { + case .public: + return "Public" + case .unlisted: + return "Unlisted" + case .private: + return "Private" + case .direct: + return "Direct" + } + } + +} diff --git a/Tusker/Info.plist b/Tusker/Info.plist index d629a297..4fd5c0d6 100644 --- a/Tusker/Info.plist +++ b/Tusker/Info.plist @@ -2,6 +2,10 @@ + NSCameraUsageDescription + To post photos from the camera. + NSPhotoLibraryUsageDescription + To post photos from the photo library. CFBundleDevelopmentRegion $(DEVELOPMENT_LANGUAGE) CFBundleExecutable diff --git a/Tusker/Preferences/Preferences.swift b/Tusker/Preferences/Preferences.swift index 61fb1077..67ebec1e 100644 --- a/Tusker/Preferences/Preferences.swift +++ b/Tusker/Preferences/Preferences.swift @@ -7,6 +7,7 @@ // import Foundation +import MastodonKit class Preferences: Codable { @@ -36,4 +37,6 @@ class Preferences: Codable { var hideCustomEmojiInUsernames = false + var defaultPostVisibility = Visibility.public + } diff --git a/Tusker/Storyboards/Compose.storyboard b/Tusker/Storyboards/Compose.storyboard new file mode 100644 index 00000000..f6dc9cf9 --- /dev/null +++ b/Tusker/Storyboards/Compose.storyboard @@ -0,0 +1,208 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Tusker/Storyboards/Profile.storyboard b/Tusker/Storyboards/Profile.storyboard index 559e2f0b..17d944c7 100644 --- a/Tusker/Storyboards/Profile.storyboard +++ b/Tusker/Storyboards/Profile.storyboard @@ -21,8 +21,15 @@ - + + + + + + + + diff --git a/Tusker/Storyboards/Timeline.storyboard b/Tusker/Storyboards/Timeline.storyboard index c0490d95..830d3ad9 100644 --- a/Tusker/Storyboards/Timeline.storyboard +++ b/Tusker/Storyboards/Timeline.storyboard @@ -21,7 +21,13 @@ - + + + + + + + @@ -35,6 +41,14 @@ + + + + + + + + diff --git a/Tusker/View Controllers/ComposeViewController.swift b/Tusker/View Controllers/ComposeViewController.swift new file mode 100644 index 00000000..ef4b1b7b --- /dev/null +++ b/Tusker/View Controllers/ComposeViewController.swift @@ -0,0 +1,245 @@ +// +// ComposeViewController.swift +// Tusker +// +// Created by Shadowfacts on 8/28/18. +// Copyright © 2018 Shadowfacts. All rights reserved. +// + +import UIKit +import MastodonKit + +class ComposeViewController: UIViewController { + + static func create(inReplyTo: Status? = nil, mentioning: Account? = nil) -> UIViewController { + guard let navigationController = UIStoryboard(name: "Compose", bundle: nil).instantiateInitialViewController() as? UINavigationController, + let composeVC = navigationController.topViewController as? ComposeViewController else { fatalError() } + composeVC.inReplyTo = inReplyTo + composeVC.mentioning = mentioning + return navigationController + } + + @IBOutlet weak var scrollView: UIScrollView! + @IBOutlet weak var inReplyToContainerView: UIView! + @IBOutlet weak var inReplyToAvatarImageView: UIImageView! + @IBOutlet weak var inReplyToDisplayNameLabel: UILabel! + @IBOutlet weak var inReplyToUsernameLabel: UILabel! + @IBOutlet weak var inReplyToContentLabel: StatusContentLabel! + @IBOutlet weak var inReplyToLabel: UILabel! + @IBOutlet weak var statusTextView: UITextView! + @IBOutlet weak var visibilityButton: UIButton! + @IBOutlet weak var contentWarningTextField: UITextField! + @IBOutlet weak var mediaStackView: UIStackView! + @IBOutlet weak var paddingView: UIView! + + var scrolled = false + + var inReplyTo: Status? + var mentioning: Account? + + var contentWarning = false + var visibility = Preferences.shared.defaultPostVisibility + + var status: Status? + + override func viewDidLoad() { + super.viewDidLoad() + + statusTextView.placeholder = "What is on your mind?" + statusTextView.layer.cornerRadius = 5 + statusTextView.layer.masksToBounds = true + visibilityButton.setTitle(visibility.displayName, for: .normal) + contentWarningTextField.delegate = self + + let toolbar = UIToolbar(frame: CGRect(x: 0, y: 0, width: view.frame.width, height: 30)) + let flexSpace = UIBarButtonItem(barButtonSystemItem: .flexibleSpace, target: nil, action: nil) + let done = UIBarButtonItem(barButtonSystemItem: .done, target: self, action: #selector(keyboardDoneButtonPressed)) + toolbar.setItems([flexSpace, done], animated: false) + toolbar.sizeToFit() + statusTextView.inputAccessoryView = toolbar + + if let inReplyTo = inReplyTo { + inReplyToDisplayNameLabel.text = inReplyTo.account.realDisplayName + inReplyToUsernameLabel.text = "@\(inReplyTo.account.username)" + inReplyToContentLabel.status = inReplyTo + inReplyToAvatarImageView.layer.cornerRadius = Preferences.shared.avatarStyle.cornerRadius(for: inReplyToAvatarImageView) + inReplyToAvatarImageView.layer.masksToBounds = true + inReplyToAvatarImageView.image = nil + if let url = URL(string: inReplyTo.account.avatar) { + AvatarCache.shared.get(url) { image in + DispatchQueue.main.async { + self.inReplyToAvatarImageView.image = image + } + } + } + inReplyToLabel.text = "In reply to \(inReplyTo.account.realDisplayName)" + if inReplyTo.account != MastodonController.shared.account { + statusTextView.text = "@\(inReplyTo.account.acct) " + } + statusTextView.text += inReplyTo.mentions.filter({ $0.id != MastodonController.shared.account.id }).map({ "@\($0.acct) " }).joined() + statusTextView.textViewDidChange(statusTextView) + } else { + inReplyToLabel.isHidden = true + inReplyToContainerView.isHidden = true + } + + if let mentioning = mentioning { + statusTextView.text += "@\(mentioning.acct) " + statusTextView.textViewDidChange(statusTextView) + } + } + + override func viewDidLayoutSubviews() { + if inReplyTo != nil && !scrolled { + scrollView.contentOffset = CGPoint(x: 0, y: inReplyToContainerView.bounds.height - 44) + scrolled = true + } + } + + func addMedia(for image: UIImage) { + let mediaView = ComposeMediaView(image: image) + mediaStackView.addArrangedSubview(mediaView) + } + + // MARK: - Navigation + + override func prepare(for segue: UIStoryboardSegue, sender: Any?) { + statusTextView.resignFirstResponder() + contentWarningTextField.resignFirstResponder() + + if segue.identifier == "postComplete" { + guard let status = status else { fatalError("postComplete segue can't occur without Status") } + guard let dest = segue.destination as? MainTabBarViewController, + let navController = dest.selectedViewController as? UINavigationController, + let topVC = navController.topViewController else { return } + topVC.selected(status: status) + } + } + + // MARK: - Interaction + + @IBAction func visibilityPressed(_ sender: Any) { + let alertController = UIAlertController(title: "Post Visibility", message: nil, preferredStyle: .actionSheet) + for visibility in Visibility.allCases { + let action = UIAlertAction(title: visibility.displayName, style: .default, handler: { _ in + self.visibility = visibility + UIView.performWithoutAnimation { + self.visibilityButton.setTitle(visibility.displayName, for: .normal) + self.visibilityButton.layoutIfNeeded() + } + }) + if visibility == self.visibility { + action.setValue(true, forKey: "checked") + } + alertController.addAction(action) + } + alertController.addAction(UIAlertAction(title: "Cancel", style: .cancel, handler: nil)) + present(alertController, animated: true) + } + + @IBAction func contentWarningPressed(_ sender: Any) { + contentWarning = !contentWarning + contentWarningTextField.isHidden = !contentWarning + } + + @IBAction func mediaPressed(_ sender: Any) { + let imagePicker = UIImagePickerController() + imagePicker.delegate = self + + let alertController = UIAlertController(title: "Choose Image Source", message: nil, preferredStyle: .actionSheet) + if UIImagePickerController.isSourceTypeAvailable(.camera) { + alertController.addAction(UIAlertAction(title: "Camera", style: .default, handler: { _ in + imagePicker.sourceType = .camera + self.present(imagePicker, animated: true) + })) + } + if UIImagePickerController.isSourceTypeAvailable(.photoLibrary) { + alertController.addAction(UIAlertAction(title: "Photo Library", style: .default, handler: { __ in + imagePicker.sourceType = .photoLibrary + self.present(imagePicker, animated: true) + })) + } + alertController.addAction(UIAlertAction(title: "Cancel", style: .cancel, handler: nil)) + present(alertController, animated: true) + } + + @IBAction func postPressed(_ sender: Any) { + guard let text = statusTextView.text, + !text.isEmpty else { return } + + let inReplyToID = inReplyTo?.id + + let contentWarning: String? + if self.contentWarning, + let text = contentWarningTextField.text, + !text.isEmpty { + contentWarning = text + } else { + contentWarning = nil + } + let sensitive = contentWarning != nil + + let visibility = self.visibility + + var attachments: [Attachment?] = [] + let group = DispatchGroup() + for view in mediaStackView.arrangedSubviews { + guard let imageView = view as? UIImageView, + let image = imageView.image, + let data = image.pngData() else { continue } + let index = attachments.count + attachments.append(nil) + group.enter() + let req = Media.upload(media: .png(data)) + MastodonController.shared.client.run(req) { result in + guard case let .success(attachment, _) = result else { fatalError() } + attachments[index] = attachment + group.leave() + } + } + + group.notify(queue: .main) { + let mediaIDs = attachments.map { $0!.id } + + let req = Statuses.create(status: text, + replyToID: inReplyToID, + mediaIDs: mediaIDs, + sensitive: sensitive, + spoilerText: contentWarning, + visibility: visibility) + + MastodonController.shared.client.run(req) { result in + guard case let .success(status, _) = result else { fatalError() } + self.status = status + DispatchQueue.main.async { + self.performSegue(withIdentifier: "postComplete", sender: self) + } + } + } + } + + @objc func imagePressed(_ gesture: UITapGestureRecognizer) { + gesture.view!.superview!.removeFromSuperview() + } + + @objc func keyboardDoneButtonPressed() { + statusTextView.endEditing(false) + } + +} + +extension ComposeViewController: UITextFieldDelegate { + func textFieldShouldReturn(_ textField: UITextField) -> Bool { + textField.endEditing(false) + return true + } +} + +extension ComposeViewController: UIImagePickerControllerDelegate, UINavigationControllerDelegate { + func imagePickerController(_ picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [UIImagePickerController.InfoKey : Any]) { + if let selectedImage = info[.originalImage] as? UIImage { + addMedia(for: selectedImage) + dismiss(animated: true) + } + } +} diff --git a/Tusker/View Controllers/MainTabBarViewController.swift b/Tusker/View Controllers/MainTabBarViewController.swift index 04c5900b..95afd1fc 100644 --- a/Tusker/View Controllers/MainTabBarViewController.swift +++ b/Tusker/View Controllers/MainTabBarViewController.swift @@ -32,4 +32,7 @@ class MainTabBarViewController: UITabBarController { } */ + @IBAction func unwindToTabBarController(segue: UIStoryboardSegue) { + } + } diff --git a/Tusker/View Controllers/ProfileTableViewController.swift b/Tusker/View Controllers/ProfileTableViewController.swift index 735169a9..48e5a52b 100644 --- a/Tusker/View Controllers/ProfileTableViewController.swift +++ b/Tusker/View Controllers/ProfileTableViewController.swift @@ -47,6 +47,8 @@ class ProfileTableViewController: UITableViewController, PreferencesAdaptive { updateUIForPreferences() + + MastodonController.shared.client.run(request()) { result in guard case let .success(statuses, pagination) = result else { fatalError() } self.statuses = statuses @@ -70,6 +72,11 @@ class ProfileTableViewController: UITableViewController, PreferencesAdaptive { navigationItem.title = account.realDisplayName } + func sendMessageMentioning() { + let vc = ComposeViewController.create(mentioning: account) + present(vc, animated: true) + } + /* // MARK: - Navigation @@ -146,6 +153,11 @@ class ProfileTableViewController: UITableViewController, PreferencesAdaptive { } } } + + @IBAction func composePressed(_ sender: Any) { + sendMessageMentioning() + } + } extension ProfileTableViewController: ProfileHeaderTableViewCellDelegate { @@ -161,7 +173,7 @@ extension ProfileTableViewController: ProfileHeaderTableViewCellDelegate { self.present(vc, animated: true) })) alert.addAction(UIAlertAction(title: "Send Message...", style: .default, handler: { _ in - print("send message to @\(self.account.acct)") + self.sendMessageMentioning() })) alert.addAction(UIAlertAction(title: "Cancel", style: .cancel, handler: nil)) present(alert, animated: true) diff --git a/Tusker/Views/ComposeMediaView.swift b/Tusker/Views/ComposeMediaView.swift new file mode 100644 index 00000000..3f6e96a1 --- /dev/null +++ b/Tusker/Views/ComposeMediaView.swift @@ -0,0 +1,44 @@ +// +// ComposeAttachmentView.swift +// Tusker +// +// Created by Shadowfacts on 8/30/18. +// Copyright © 2018 Shadowfacts. All rights reserved. +// + +import UIKit + +class ComposeMediaView: UIImageView { + + var remove: UIImageView + + required init?(coder aDecoder: NSCoder) { + return nil + } + + init(image: UIImage) { + remove = UIImageView(image: UIImage(named: "Remove")) + + super.init(image: image) + + contentMode = .scaleAspectFill + layer.cornerRadius = 5 + layer.masksToBounds = true + isUserInteractionEnabled = true + + remove.isUserInteractionEnabled = true + remove.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(removePressed))) + remove.translatesAutoresizingMaskIntoConstraints = false + addSubview(remove) + + remove.widthAnchor.constraint(equalToConstant: 20).isActive = true + remove.heightAnchor.constraint(equalToConstant: 20).isActive = true + remove.topAnchor.constraint(equalTo: topAnchor, constant: 5).isActive = true + remove.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -5).isActive = true + } + + @objc func removePressed() { + removeFromSuperview() + } + +} diff --git a/Tusker/Views/ConversationMainStatusTableViewCell.swift b/Tusker/Views/ConversationMainStatusTableViewCell.swift index f8d43985..043f11b9 100644 --- a/Tusker/Views/ConversationMainStatusTableViewCell.swift +++ b/Tusker/Views/ConversationMainStatusTableViewCell.swift @@ -77,6 +77,10 @@ class ConversationMainStatusTableViewCell: UITableViewCell, PreferencesAdaptive delegate?.selected(account: account) } + @IBAction func replyPressed(_ sender: Any) { + delegate?.reply(to: status) + } + } extension ConversationMainStatusTableViewCell: HTMLContentLabelDelegate { diff --git a/Tusker/Views/ConversationMainStatusTableViewCell.xib b/Tusker/Views/ConversationMainStatusTableViewCell.xib index 22ecf244..5da1b90b 100644 --- a/Tusker/Views/ConversationMainStatusTableViewCell.xib +++ b/Tusker/Views/ConversationMainStatusTableViewCell.xib @@ -15,46 +15,75 @@ - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - - - - - - - - - - + + + + diff --git a/Tusker/Views/StatusTableViewCell.swift b/Tusker/Views/StatusTableViewCell.swift index 48db7275..53111e21 100644 --- a/Tusker/Views/StatusTableViewCell.swift +++ b/Tusker/Views/StatusTableViewCell.swift @@ -21,6 +21,8 @@ protocol StatusTableViewCellDelegate { func selected(status: Status) + func reply(to status: Status) + } class StatusTableViewCell: UITableViewCell, PreferencesAdaptive { @@ -99,6 +101,10 @@ class StatusTableViewCell: UITableViewCell, PreferencesAdaptive { } } + @IBAction func replyPressed(_ sender: Any) { + delegate?.reply(to: status) + } + @objc func accountPressed() { delegate?.selected(account: account) } diff --git a/Tusker/Views/StatusTableViewCell.xib b/Tusker/Views/StatusTableViewCell.xib index ca995e79..fdb087ff 100644 --- a/Tusker/Views/StatusTableViewCell.xib +++ b/Tusker/Views/StatusTableViewCell.xib @@ -12,20 +12,20 @@ - + - - + + - - + + @@ -43,7 +43,7 @@ + + + + + + - + @@ -89,7 +101,7 @@ - +