From df6976df2fd30956266e4a857d3abfa036598894 Mon Sep 17 00:00:00 2001 From: Shadowfacts Date: Mon, 22 Oct 2018 22:09:11 -0400 Subject: [PATCH] Add post drafts --- Tusker.xcodeproj/project.pbxproj | 28 ++++++++ Tusker/AppDelegate.swift | 1 + Tusker/AppRouter.swift | 4 ++ Tusker/DraftsManager.swift | 72 +++++++++++++++++++ Tusker/Preferences/Preferences.swift | 6 +- .../Compose/ComposeViewController.swift | 68 +++++++++++++++++- .../Compose/Drafts/DraftTableViewCell.swift | 21 ++++++ .../Compose/Drafts/DraftTableViewCell.xib | 48 +++++++++++++ .../Drafts/DraftsTableViewController.swift | 69 ++++++++++++++++++ .../Drafts/DraftsTableViewController.xib | 24 +++++++ .../Preferences/Preferences.storyboard | 35 ++++++++- .../PreferencesTableViewController.swift | 17 ++--- 12 files changed, 379 insertions(+), 14 deletions(-) create mode 100644 Tusker/DraftsManager.swift create mode 100644 Tusker/Screens/Compose/Drafts/DraftTableViewCell.swift create mode 100644 Tusker/Screens/Compose/Drafts/DraftTableViewCell.xib create mode 100644 Tusker/Screens/Compose/Drafts/DraftsTableViewController.swift create mode 100644 Tusker/Screens/Compose/Drafts/DraftsTableViewController.xib diff --git a/Tusker.xcodeproj/project.pbxproj b/Tusker.xcodeproj/project.pbxproj index ab390cd511..808d72ee36 100644 --- a/Tusker.xcodeproj/project.pbxproj +++ b/Tusker.xcodeproj/project.pbxproj @@ -61,6 +61,11 @@ D621544B21682AD30003D87D /* TabTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = D621544A21682AD30003D87D /* TabTableViewCell.swift */; }; D621544D21682AD90003D87D /* TabTableViewCell.xib in Resources */ = {isa = PBXBuildFile; fileRef = D621544C21682AD90003D87D /* TabTableViewCell.xib */; }; D627FF74217BBC9700CC0648 /* AppRouter.swift in Sources */ = {isa = PBXBuildFile; fileRef = D627FF73217BBC9700CC0648 /* AppRouter.swift */; }; + D627FF76217E923E00CC0648 /* DraftsManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = D627FF75217E923E00CC0648 /* DraftsManager.swift */; }; + D627FF79217E950100CC0648 /* DraftsTableViewController.xib in Resources */ = {isa = PBXBuildFile; fileRef = D627FF78217E950100CC0648 /* DraftsTableViewController.xib */; }; + D627FF7B217E951500CC0648 /* DraftsTableViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D627FF7A217E951500CC0648 /* DraftsTableViewController.swift */; }; + D627FF7D217E958900CC0648 /* DraftTableViewCell.xib in Resources */ = {isa = PBXBuildFile; fileRef = D627FF7C217E958900CC0648 /* DraftTableViewCell.xib */; }; + D627FF7F217E95E000CC0648 /* DraftTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = D627FF7E217E95E000CC0648 /* DraftTableViewCell.swift */; }; D6289E84217B795D0003D1D7 /* LargeImageViewController.xib in Resources */ = {isa = PBXBuildFile; fileRef = D6289E83217B795D0003D1D7 /* LargeImageViewController.xib */; }; D62D2422217AA7E1005076CC /* UserActivityManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = D62D2421217AA7E1005076CC /* UserActivityManager.swift */; }; D62D2424217ABF3F005076CC /* NSUserActivity+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = D62D2423217ABF3F005076CC /* NSUserActivity+Extensions.swift */; }; @@ -249,6 +254,11 @@ D621544A21682AD30003D87D /* TabTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TabTableViewCell.swift; sourceTree = ""; }; D621544C21682AD90003D87D /* TabTableViewCell.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = TabTableViewCell.xib; sourceTree = ""; }; D627FF73217BBC9700CC0648 /* AppRouter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppRouter.swift; sourceTree = ""; }; + D627FF75217E923E00CC0648 /* DraftsManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DraftsManager.swift; sourceTree = ""; }; + D627FF78217E950100CC0648 /* DraftsTableViewController.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = DraftsTableViewController.xib; sourceTree = ""; }; + D627FF7A217E951500CC0648 /* DraftsTableViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DraftsTableViewController.swift; sourceTree = ""; }; + D627FF7C217E958900CC0648 /* DraftTableViewCell.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = DraftTableViewCell.xib; sourceTree = ""; }; + D627FF7E217E95E000CC0648 /* DraftTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DraftTableViewCell.swift; sourceTree = ""; }; D6289E83217B795D0003D1D7 /* LargeImageViewController.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = LargeImageViewController.xib; sourceTree = ""; }; D62D2421217AA7E1005076CC /* UserActivityManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserActivityManager.swift; sourceTree = ""; }; D62D2423217ABF3F005076CC /* NSUserActivity+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NSUserActivity+Extensions.swift"; sourceTree = ""; }; @@ -481,6 +491,17 @@ path = Tab; sourceTree = ""; }; + D627FF77217E94F200CC0648 /* Drafts */ = { + isa = PBXGroup; + children = ( + D627FF78217E950100CC0648 /* DraftsTableViewController.xib */, + D627FF7A217E951500CC0648 /* DraftsTableViewController.swift */, + D627FF7C217E958900CC0648 /* DraftTableViewCell.xib */, + D627FF7E217E95E000CC0648 /* DraftTableViewCell.swift */, + ); + path = Drafts; + sourceTree = ""; + }; D62D241E217AA46B005076CC /* Shortcuts */ = { isa = PBXGroup; children = ( @@ -560,6 +581,7 @@ D641C787213DD862004B4513 /* Compose */ = { isa = PBXGroup; children = ( + D627FF77217E94F200CC0648 /* Drafts */, D6A5FAF0217B7E05003DB2D9 /* ComposeViewController.xib */, D66362702136338600C9CBA2 /* ComposeViewController.swift */, ); @@ -747,6 +769,7 @@ D6D4DDCF212518A000E1C4BB /* AppDelegate.swift */, D627FF73217BBC9700CC0648 /* AppRouter.swift */, D64D0AAC2128D88B005A6F37 /* LocalData.swift */, + D627FF75217E923E00CC0648 /* DraftsManager.swift */, 04DACE8D212CC7CC009840C4 /* ImageCache.swift */, D6028B9A2150811100F223B9 /* MastodonCache.swift */, D6C693EE216192C2007D6A6D /* TuskerNavigationDelegate.swift */, @@ -1007,6 +1030,7 @@ buildActionMask = 2147483647; files = ( D641C779213CAC56004B4513 /* ActionNotificationTableViewCell.xib in Resources */, + D627FF7D217E958900CC0648 /* DraftTableViewCell.xib in Resources */, D667E5E921349EE50057A976 /* ProfileHeaderTableViewCell.xib in Resources */, D6C693CD2161257B007D6A6D /* SilentActionPermissionCell.xib in Resources */, D6D4DDDA212518A200E1C4BB /* LaunchScreen.storyboard in Resources */, @@ -1016,6 +1040,7 @@ D641C77B213CB017004B4513 /* FollowNotificationTableViewCell.xib in Resources */, D6289E84217B795D0003D1D7 /* LargeImageViewController.xib in Resources */, D663626621360DD700C9CBA2 /* Preferences.storyboard in Resources */, + D627FF79217E950100CC0648 /* DraftsTableViewController.xib in Resources */, D667E5E12134937B0057A976 /* StatusTableViewCell.xib in Resources */, D6A5FAF1217B7E05003DB2D9 /* ComposeViewController.xib in Resources */, D621544D21682AD90003D87D /* TabTableViewCell.xib in Resources */, @@ -1118,8 +1143,10 @@ D667E5F82135C3040057A976 /* Mastodon+Equatable.swift in Sources */, D621544B21682AD30003D87D /* TabTableViewCell.swift in Sources */, 04DACE8E212CC7CC009840C4 /* ImageCache.swift in Sources */, + D627FF7B217E951500CC0648 /* DraftsTableViewController.swift in Sources */, D6333B772138D94E00CE884A /* ComposeMediaView.swift in Sources */, 04ED00B121481ED800567C53 /* SteppedProgressView.swift in Sources */, + D627FF7F217E95E000CC0648 /* DraftTableViewCell.swift in Sources */, D663626421360D2300C9CBA2 /* AvatarStyle.swift in Sources */, D679C09F215850EF00DA27FE /* XCBActions.swift in Sources */, D6538945214D6D7500E3CEFC /* TableViewSwipeActionProvider.swift in Sources */, @@ -1143,6 +1170,7 @@ D627FF74217BBC9700CC0648 /* AppRouter.swift in Sources */, D6C693FE2162FEEA007D6A6D /* UIViewController+Children.swift in Sources */, D64D0AB12128D9AE005A6F37 /* OnboardingViewController.swift in Sources */, + D627FF76217E923E00CC0648 /* DraftsManager.swift in Sources */, D663626821360E2C00C9CBA2 /* PreferencesTableViewController.swift in Sources */, D64F80E2215875CC00BEF393 /* XCBActionType.swift in Sources */, D66362752137068A00C9CBA2 /* Visibility+Helpers.swift in Sources */, diff --git a/Tusker/AppDelegate.swift b/Tusker/AppDelegate.swift index 7296cc271d..a24e263740 100644 --- a/Tusker/AppDelegate.swift +++ b/Tusker/AppDelegate.swift @@ -48,6 +48,7 @@ class AppDelegate: UIResponder, UIApplicationDelegate { // Use this method to release shared resources, save user data, invalidate timers, and store enough application state information to restore your application to its current state in case it is terminated later. // If your application supports background execution, this method is called instead of applicationWillTerminate: when the user quits. Preferences.save() + DraftsManager.save() } func applicationWillEnterForeground(_ application: UIApplication) { diff --git a/Tusker/AppRouter.swift b/Tusker/AppRouter.swift index 8d9d9cf27c..2de742ccdf 100644 --- a/Tusker/AppRouter.swift +++ b/Tusker/AppRouter.swift @@ -81,6 +81,10 @@ class AppRouter { return ComposeViewController(inReplyTo: inReplyToID, mentioningAcct: mentioningAcct, text: text, router: self) } + func drafts() -> DraftsTableViewController { + return DraftsTableViewController() + } + func largeImage(_ image: UIImage, description: String?, sourceFrame: CGRect, sourceCornerRadius: CGFloat, transitioningDelegate: UIViewControllerTransitioningDelegate?) -> LargeImageViewController { let vc = LargeImageViewController(image: image, description: description, sourceFrame: sourceFrame, sourceCornerRadius: sourceCornerRadius, router: self) vc.transitioningDelegate = transitioningDelegate diff --git a/Tusker/DraftsManager.swift b/Tusker/DraftsManager.swift new file mode 100644 index 0000000000..4c8f7050e5 --- /dev/null +++ b/Tusker/DraftsManager.swift @@ -0,0 +1,72 @@ +// +// DraftsManager.swift +// Tusker +// +// Created by Shadowfacts on 10/22/18. +// Copyright © 2018 Shadowfacts. All rights reserved. +// + +import Foundation + +class DraftsManager: Codable { + + private(set) static var shared: DraftsManager = load() + + private static var documentsDirectory = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first! + private static var archiveURL = DraftsManager.documentsDirectory.appendingPathComponent("drafts").appendingPathExtension("plist") + + static func save() { + let encoder = PropertyListEncoder() + let data = try? encoder.encode(shared) + try? data?.write(to: archiveURL, options: .noFileProtection) + } + + static func load() -> DraftsManager { + let decoder = PropertyListDecoder() + if let data = try? Data(contentsOf: archiveURL), + let draftsManager = try? decoder.decode(DraftsManager.self, from: data) { + return draftsManager + } + return DraftsManager() + } + + private init() {} + + var drafts: [Draft] = [] + var sorted: [Draft] { + return drafts.sorted(by: { $0.lastModified > $1.lastModified }) + } + + func create(text: String) { + drafts.append(Draft(text: text, lastModified: Date())) + } + + func remove(_ draft: Draft) { + let index = drafts.firstIndex(of: draft)! + drafts.remove(at: index) + } + +} + +extension DraftsManager { + class Draft: Codable, Equatable { + let id: UUID + private(set) var text: String + private(set) var lastModified: Date + + init(text: String, lastModified: Date) { + self.id = UUID() + self.text = text + self.lastModified = lastModified + } + + func update(text: String) { + self.text = text + self.lastModified = Date() + } + + static func ==(lhs: Draft, rhs: Draft) -> Bool { + return lhs.id == rhs.id + } + } +} diff --git a/Tusker/Preferences/Preferences.swift b/Tusker/Preferences/Preferences.swift index 59e4f3fc45..98219067a6 100644 --- a/Tusker/Preferences/Preferences.swift +++ b/Tusker/Preferences/Preferences.swift @@ -19,12 +19,12 @@ class Preferences: Codable { static func save() { let encoder = PropertyListEncoder() let data = try? encoder.encode(shared) - try? data?.write(to: Preferences.archiveURL, options: .noFileProtection) + try? data?.write(to: archiveURL, options: .noFileProtection) } static func load() -> Preferences { let decoder = PropertyListDecoder() - if let data = try? Data(contentsOf: Preferences.archiveURL), + if let data = try? Data(contentsOf: archiveURL), let preferences = try? decoder.decode(Preferences.self, from: data) { return preferences } @@ -41,6 +41,8 @@ class Preferences: Codable { var defaultPostVisibility = Status.Visibility.public + var automaticallySaveDrafts = true + // MARK: - Advanced var silentActions: [String: Permission] = [:] diff --git a/Tusker/Screens/Compose/ComposeViewController.swift b/Tusker/Screens/Compose/ComposeViewController.swift index 1fa8b2e6e2..af55d02ccb 100644 --- a/Tusker/Screens/Compose/ComposeViewController.swift +++ b/Tusker/Screens/Compose/ComposeViewController.swift @@ -34,8 +34,13 @@ class ComposeViewController: UIViewController { var scrolled = false var inReplyToID: String? + + // TODO: cleanup this var mentioningAcct: String? var text: String? + var initialText: String? + + var draft: DraftsManager.Draft? // Weak so that if a new session is initiated (i.e. XCBManager.currentSession is changed) while the current one is in progress, this one will be released weak var xcbSession: XCBSession? @@ -62,6 +67,7 @@ class ComposeViewController: UIViewController { super.init(nibName: "ComposeViewController", bundle: nil) navigationItem.leftBarButtonItem = UIBarButtonItem(barButtonSystemItem: .cancel, target: self, action: #selector(cancelPressed)) + navigationItem.rightBarButtonItem = UIBarButtonItem(title: "Drafts", style: .plain, target: self, action: #selector(draftsPressed)) } required init?(coder aDecoder: NSCoder) { @@ -117,6 +123,8 @@ class ComposeViewController: UIViewController { statusTextView.text += text } + initialText = statusTextView.text.trimmingCharacters(in: .whitespacesAndNewlines) + updateCharactersRemaining() updatePlaceholder() @@ -273,6 +281,11 @@ class ComposeViewController: UIViewController { guard case let .success(status, _) = response else { fatalError() } self.status = status MastodonCache.add(status: status) + + if let draft = self.draft { + DraftsManager.shared.remove(draft) + } + DispatchQueue.main.async { self.progressView.step() self.dismiss(animated: true) @@ -296,11 +309,53 @@ class ComposeViewController: UIViewController { statusTextView.endEditing(false) } - @objc func cancelPressed() { + func saveDraft() { + if let draft = draft { + draft.update(text: statusTextView.text) + } else { + DraftsManager.shared.create(text: statusTextView.text) + } + } + + func close() { dismiss(animated: true) xcbSession?.complete(with: .cancel) } + @objc func cancelPressed() { + guard statusTextView.text.trimmingCharacters(in: .whitespacesAndNewlines) != initialText else { + close() + return + } + + if Preferences.shared.automaticallySaveDrafts { + saveDraft() + close() + return + } + + let alert = UIAlertController(title: nil, message: nil, preferredStyle: .actionSheet) + alert.addAction(UIAlertAction(title: "Save draft", style: .default, handler: { (_) in + self.saveDraft() + self.close() + })) + alert.addAction(UIAlertAction(title: "Delete draft", style: .destructive, handler: { (_) in + if let draft = self.draft { + DraftsManager.shared.remove(draft) + } + self.close() + })) + alert.addAction(UIAlertAction(title: "Cancel", style: .cancel, handler: { (_) in + })) + router.present(alert, animated: true) + } + + @objc func draftsPressed() { + let drafts = router.drafts() + drafts.delegate = self + router.present(UINavigationController(rootViewController: drafts), animated: true) + } + } extension ComposeViewController: UITextFieldDelegate { @@ -340,3 +395,14 @@ extension ComposeViewController: ComposeMediaViewDelegate { present(alertController, animated: true) } } + +extension ComposeViewController: DraftsTableViewControllerDelegate { + func draftSelectionCanceled() { + } + + func draftSelected(_ draft: DraftsManager.Draft) { + self.draft = draft + statusTextView.text = draft.text + updatePlaceholder() + } +} diff --git a/Tusker/Screens/Compose/Drafts/DraftTableViewCell.swift b/Tusker/Screens/Compose/Drafts/DraftTableViewCell.swift new file mode 100644 index 0000000000..130fa386d3 --- /dev/null +++ b/Tusker/Screens/Compose/Drafts/DraftTableViewCell.swift @@ -0,0 +1,21 @@ +// +// DraftsTableViewCell.swift +// Tusker +// +// Created by Shadowfacts on 10/22/18. +// Copyright © 2018 Shadowfacts. All rights reserved. +// + +import UIKit + +class DraftTableViewCell: UITableViewCell { + + @IBOutlet weak var contentLabel: UILabel! + @IBOutlet weak var lastModifiedLabel: UILabel! + + func updateUI(for draft: DraftsManager.Draft) { + contentLabel.text = draft.text + lastModifiedLabel.text = draft.lastModified.timeAgoString() + } + +} diff --git a/Tusker/Screens/Compose/Drafts/DraftTableViewCell.xib b/Tusker/Screens/Compose/Drafts/DraftTableViewCell.xib new file mode 100644 index 0000000000..f83c9da506 --- /dev/null +++ b/Tusker/Screens/Compose/Drafts/DraftTableViewCell.xib @@ -0,0 +1,48 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Tusker/Screens/Compose/Drafts/DraftsTableViewController.swift b/Tusker/Screens/Compose/Drafts/DraftsTableViewController.swift new file mode 100644 index 0000000000..6e3c9c71cb --- /dev/null +++ b/Tusker/Screens/Compose/Drafts/DraftsTableViewController.swift @@ -0,0 +1,69 @@ +// +// DraftsTableViewController.swift +// Tusker +// +// Created by Shadowfacts on 10/22/18. +// Copyright © 2018 Shadowfacts. All rights reserved. +// + +import UIKit + +protocol DraftsTableViewControllerDelegate { + func draftSelectionCanceled() + func draftSelected(_ draft: DraftsManager.Draft) +} + +class DraftsTableViewController: UITableViewController { + + var delegate: DraftsTableViewControllerDelegate? + + init() { + super.init(nibName: "DraftsTableViewController", bundle: nil) + + title = "Drafts" + navigationItem.leftBarButtonItem = UIBarButtonItem(barButtonSystemItem: .cancel, target: self, action: #selector(cancelPressed)) + } + + required init?(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override func viewDidLoad() { + super.viewDidLoad() + + tableView.register(UINib(nibName: "DraftTableViewCell", bundle: nil), forCellReuseIdentifier: "draftCell") + } + + func draft(for indexPath: IndexPath) -> DraftsManager.Draft { + return DraftsManager.shared.sorted[indexPath.row] + } + + // MARK: - Table View Data Source + + override func numberOfSections(in tableView: UITableView) -> Int { + return 1 + } + + override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { + return DraftsManager.shared.drafts.count + } + + override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { + guard let cell = tableView.dequeueReusableCell(withIdentifier: "draftCell", for: indexPath) as? DraftTableViewCell else { fatalError() } + + cell.updateUI(for: draft(for: indexPath)) + + return cell + } + + override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { + delegate?.draftSelected(draft(for: indexPath)) + dismiss(animated: true) + } + + @objc func cancelPressed() { + delegate?.draftSelectionCanceled() + dismiss(animated: true) + } + +} diff --git a/Tusker/Screens/Compose/Drafts/DraftsTableViewController.xib b/Tusker/Screens/Compose/Drafts/DraftsTableViewController.xib new file mode 100644 index 0000000000..056a9daaca --- /dev/null +++ b/Tusker/Screens/Compose/Drafts/DraftsTableViewController.xib @@ -0,0 +1,24 @@ + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Tusker/Screens/Preferences/Preferences.storyboard b/Tusker/Screens/Preferences/Preferences.storyboard index 74510892f8..51a1082be2 100644 --- a/Tusker/Screens/Preferences/Preferences.storyboard +++ b/Tusker/Screens/Preferences/Preferences.storyboard @@ -44,10 +44,10 @@ - + - + @@ -77,12 +77,40 @@ + + + + + + + + + + + + + + + + + + + + + + + - + @@ -114,6 +142,7 @@ + diff --git a/Tusker/Screens/Preferences/PreferencesTableViewController.swift b/Tusker/Screens/Preferences/PreferencesTableViewController.swift index 95286bd39f..5b1f8d8507 100644 --- a/Tusker/Screens/Preferences/PreferencesTableViewController.swift +++ b/Tusker/Screens/Preferences/PreferencesTableViewController.swift @@ -17,6 +17,13 @@ class PreferencesTableViewController: UITableViewController { @IBOutlet weak var defaultPostVisibilityLabel: UILabel! + @IBOutlet weak var automaticallySaveDraftsSwitch: UISwitch! + + override func viewDidLoad() { + super.viewDidLoad() + + automaticallySaveDraftsSwitch.setOn(Preferences.shared.automaticallySaveDrafts, animated: false) + } override func viewWillAppear(_ animated: Bool) { super.viewWillAppear(animated) @@ -24,14 +31,8 @@ class PreferencesTableViewController: UITableViewController { defaultPostVisibilityLabel.text = Preferences.shared.defaultPostVisibility.displayName } - /* - // 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 automaticallySaveDraftsChanged(_ sender: Any) { + Preferences.shared.automaticallySaveDrafts = automaticallySaveDraftsSwitch.isOn } - */ }