From 6d692c273069113f2419d0aa211fdf4bdd6d19a5 Mon Sep 17 00:00:00 2001 From: Shadowfacts Date: Wed, 9 Nov 2022 18:18:31 -0500 Subject: [PATCH] Rewrite Drafts screen with SwiftUI --- Tusker.xcodeproj/project.pbxproj | 40 +---- Tusker/Models/Draft.swift | 2 + Tusker/Models/DraftsManager.swift | 9 +- .../Compose/ComposeHostingController.swift | 50 ++---- Tusker/Screens/Compose/ComposeUIState.swift | 2 + Tusker/Screens/Compose/ComposeView.swift | 3 + Tusker/Screens/Compose/DraftsView.swift | 119 +++++++++++++++ .../Drafts/DraftsTableViewController.swift | 142 ------------------ .../Drafts/DraftsTableViewController.xib | 22 --- Tusker/Views/AlertWithData.swift | 45 ++++++ .../Views/Draft Cell/DraftTableViewCell.swift | 73 --------- .../Views/Draft Cell/DraftTableViewCell.xib | 98 ------------ 12 files changed, 197 insertions(+), 408 deletions(-) create mode 100644 Tusker/Screens/Compose/DraftsView.swift delete mode 100644 Tusker/Screens/Drafts/DraftsTableViewController.swift delete mode 100644 Tusker/Screens/Drafts/DraftsTableViewController.xib create mode 100644 Tusker/Views/AlertWithData.swift delete mode 100644 Tusker/Views/Draft Cell/DraftTableViewCell.swift delete mode 100644 Tusker/Views/Draft Cell/DraftTableViewCell.xib diff --git a/Tusker.xcodeproj/project.pbxproj b/Tusker.xcodeproj/project.pbxproj index 95b79dfb..ce4c83fc 100644 --- a/Tusker.xcodeproj/project.pbxproj +++ b/Tusker.xcodeproj/project.pbxproj @@ -76,10 +76,6 @@ D627944D23A9A03D00D38C68 /* ListTimelineViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D627944C23A9A03D00D38C68 /* ListTimelineViewController.swift */; }; D627944F23A9C99800D38C68 /* EditListAccountsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D627944E23A9C99800D38C68 /* EditListAccountsViewController.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 */; }; D6285B5321EA708700FE4B39 /* StatusFormat.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6285B5221EA708700FE4B39 /* StatusFormat.swift */; }; D6289E84217B795D0003D1D7 /* LargeImageViewController.xib in Resources */ = {isa = PBXBuildFile; fileRef = D6289E83217B795D0003D1D7 /* LargeImageViewController.xib */; }; D62D2422217AA7E1005076CC /* UserActivityManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = D62D2421217AA7E1005076CC /* UserActivityManager.swift */; }; @@ -257,6 +253,8 @@ D6BC9DDA232D8BE5002CA326 /* SearchResultsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6BC9DD9232D8BE5002CA326 /* SearchResultsViewController.swift */; }; D6BEA245291A0EDE002F4D01 /* Duckable in Frameworks */ = {isa = PBXBuildFile; productRef = D6BEA244291A0EDE002F4D01 /* Duckable */; }; D6BEA247291A0F2D002F4D01 /* Duckable+Root.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6BEA246291A0F2D002F4D01 /* Duckable+Root.swift */; }; + D6BEA249291C6118002F4D01 /* DraftsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6BEA248291C6118002F4D01 /* DraftsView.swift */; }; + D6BEA24B291C6A2B002F4D01 /* AlertWithData.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6BEA24A291C6A2B002F4D01 /* AlertWithData.swift */; }; D6BED174212667E900F02DA0 /* TimelineStatusTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6BED173212667E900F02DA0 /* TimelineStatusTableViewCell.swift */; }; D6C143DA253510F4007DC240 /* ComposeEmojiTextField.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6C143D9253510F4007DC240 /* ComposeEmojiTextField.swift */; }; D6C1B2082545D1EC00DAAA66 /* StatusCardView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6C1B2072545D1EC00DAAA66 /* StatusCardView.swift */; }; @@ -432,10 +430,6 @@ D627944C23A9A03D00D38C68 /* ListTimelineViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ListTimelineViewController.swift; sourceTree = ""; }; D627944E23A9C99800D38C68 /* EditListAccountsViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EditListAccountsViewController.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 = ""; }; D6285B5221EA708700FE4B39 /* StatusFormat.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatusFormat.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 = ""; }; @@ -615,6 +609,8 @@ D6BC9DD9232D8BE5002CA326 /* SearchResultsViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchResultsViewController.swift; sourceTree = ""; }; D6BEA243291A0C83002F4D01 /* Duckable */ = {isa = PBXFileReference; lastKnownFileType = wrapper; name = Duckable; path = Packages/Duckable; sourceTree = ""; }; D6BEA246291A0F2D002F4D01 /* Duckable+Root.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Duckable+Root.swift"; sourceTree = ""; }; + D6BEA248291C6118002F4D01 /* DraftsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DraftsView.swift; sourceTree = ""; }; + D6BEA24A291C6A2B002F4D01 /* AlertWithData.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AlertWithData.swift; sourceTree = ""; }; D6BED173212667E900F02DA0 /* TimelineStatusTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelineStatusTableViewCell.swift; sourceTree = ""; }; D6C143D9253510F4007DC240 /* ComposeEmojiTextField.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComposeEmojiTextField.swift; sourceTree = ""; }; D6C1B2072545D1EC00DAAA66 /* StatusCardView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatusCardView.swift; sourceTree = ""; }; @@ -743,15 +739,6 @@ path = "Hashtag Cell"; sourceTree = ""; }; - D61959D0241E842400A37B8E /* Draft Cell */ = { - isa = PBXGroup; - children = ( - D627FF7C217E958900CC0648 /* DraftTableViewCell.xib */, - D627FF7E217E95E000CC0648 /* DraftTableViewCell.swift */, - ); - path = "Draft Cell"; - sourceTree = ""; - }; D61959D2241E846D00A37B8E /* Models */ = { isa = PBXGroup; children = ( @@ -850,15 +837,6 @@ path = Lists; sourceTree = ""; }; - D627FF77217E94F200CC0648 /* Drafts */ = { - isa = PBXGroup; - children = ( - D627FF78217E950100CC0648 /* DraftsTableViewController.xib */, - D627FF7A217E951500CC0648 /* DraftsTableViewController.swift */, - ); - path = Drafts; - sourceTree = ""; - }; D62D241E217AA46B005076CC /* Shortcuts */ = { isa = PBXGroup; children = ( @@ -906,7 +884,6 @@ D641C787213DD862004B4513 /* Compose */, D641C785213DD83B004B4513 /* Conversation */, D6F2E960249E772F005846BB /* Crash Reporter */, - D627FF77217E94F200CC0648 /* Drafts */, D627943C23A5635D00D38C68 /* Explore */, D6A4DCC92553666600D9DE31 /* Fast Account Switcher */, D641C788213DD86D004B4513 /* Large Image */, @@ -1011,6 +988,7 @@ D62275A724F1CA2800B82A16 /* ComposeReplyContentView.swift */, D6E4267625327FB400C02E1C /* ComposeAutocompleteView.swift */, D6C143D9253510F4007DC240 /* ComposeEmojiTextField.swift */, + D6BEA248291C6118002F4D01 /* DraftsView.swift */, ); path = Compose; sourceTree = ""; @@ -1284,6 +1262,7 @@ isa = PBXGroup; children = ( D6B4A4FE2506B81A000C81C1 /* AccountDisplayNameLabel.swift */, + D6BEA24A291C6A2B002F4D01 /* AlertWithData.swift */, D68E6F5E253C9B2D001A1B4C /* BaseEmojiLabel.swift */, D6ADB6EF28ED1F25009924AB /* CachedImageView.swift */, D6895DC328D65342006341DA /* ConfirmReblogStatusPreviewView.swift */, @@ -1312,7 +1291,6 @@ D626494023C122C800612E6E /* Asset Picker */, D6C7D27B22B6EBE200071952 /* Attachments */, D6DEA0DB268400AF00FE896A /* Confirm Load More Cell */, - D61959D0241E842400A37B8E /* Draft Cell */, D611C2CC232DC5FC00C86A49 /* Hashtag Cell */, D61AC1DA232EA43100C54D2D /* Instance Cell */, D641C78C213DD937004B4513 /* Notifications */, @@ -1670,7 +1648,6 @@ D61AC1D9232EA42D00C54D2D /* InstanceTableViewCell.xib in Resources */, D626493923C0FD0000612E6E /* AllPhotosTableViewCell.xib in Resources */, D6A3BC8B2321F79B00FD64D5 /* AccountTableViewCell.xib in Resources */, - D627FF7D217E958900CC0648 /* DraftTableViewCell.xib in Resources */, D6A3BC7D232195C600FD64D5 /* ActionNotificationGroupTableViewCell.xib in Resources */, D6412B0B24B0D4C600F5412E /* ProfileHeaderView.xib in Resources */, D6C82B5725C5F3F20017F1E6 /* ExpandThreadTableViewCell.xib in Resources */, @@ -1682,7 +1659,6 @@ D663625D2135C74800C9CBA2 /* ConversationMainStatusTableViewCell.xib in Resources */, D640D76922BAF5E6004FBE69 /* DomainBlocks.plist in Resources */, D6289E84217B795D0003D1D7 /* LargeImageViewController.xib in Resources */, - D627FF79217E950100CC0648 /* DraftsTableViewController.xib in Resources */, D626493D23C1000300612E6E /* AlbumTableViewCell.xib in Resources */, D627944723A6AC9300D38C68 /* BasicTableViewCell.xib in Resources */, D64BC19023C18B9D000D0238 /* FollowRequestNotificationTableViewCell.xib in Resources */, @@ -1861,7 +1837,7 @@ D67B506D250B291200FAECFB /* BlurHashDecode.swift in Sources */, D62275A024F1677200B82A16 /* ComposeHostingController.swift in Sources */, 04DACE8E212CC7CC009840C4 /* ImageCache.swift in Sources */, - D627FF7B217E951500CC0648 /* DraftsTableViewController.swift in Sources */, + D6BEA249291C6118002F4D01 /* DraftsView.swift in Sources */, D6403CC224A6B72D00E81C55 /* VisualEffectImageButton.swift in Sources */, D6531DEE246B81C9000F9538 /* GifvAttachmentView.swift in Sources */, D673ACCE2919E74200D6F8B0 /* MenuPicker.swift in Sources */, @@ -1875,7 +1851,6 @@ D6BC9DDA232D8BE5002CA326 /* SearchResultsViewController.swift in Sources */, D61ABEF628EE74D400B29151 /* StatusCollectionViewCell.swift in Sources */, D61DC84628F498F200B82C6E /* Logging.swift in Sources */, - D627FF7F217E95E000CC0648 /* DraftTableViewCell.swift in Sources */, D6B17255254F88B800128392 /* OppositeCollapseKeywordsView.swift in Sources */, D6A00B1D26379FC900316AD4 /* PollOptionsView.swift in Sources */, D6DF95C12533F5DE0027A9B6 /* RelationshipMO.swift in Sources */, @@ -1933,6 +1908,7 @@ D681E4D3246E2AFF0053414F /* MuteConversationActivity.swift in Sources */, D6969EA0240C8384002843CE /* EmojiLabel.swift in Sources */, D64BC18623C1253A000D0238 /* AssetPreviewViewController.swift in Sources */, + D6BEA24B291C6A2B002F4D01 /* AlertWithData.swift in Sources */, D61ABEFE28F1C92600B29151 /* FavoriteService.swift in Sources */, D663626221360B1900C9CBA2 /* Preferences.swift in Sources */, D626493823C0FD0000612E6E /* AllPhotosTableViewCell.swift in Sources */, diff --git a/Tusker/Models/Draft.swift b/Tusker/Models/Draft.swift index f8af8df5..6c2ef939 100644 --- a/Tusker/Models/Draft.swift +++ b/Tusker/Models/Draft.swift @@ -107,6 +107,8 @@ extension Draft: Equatable { } } +extension Draft: Identifiable {} + extension Draft { enum CodingKeys: String, CodingKey { case id diff --git a/Tusker/Models/DraftsManager.swift b/Tusker/Models/DraftsManager.swift index 46940ba3..8f7d9c1e 100644 --- a/Tusker/Models/DraftsManager.swift +++ b/Tusker/Models/DraftsManager.swift @@ -8,7 +8,7 @@ import Foundation -class DraftsManager: Codable { +class DraftsManager: Codable, ObservableObject { private(set) static var shared: DraftsManager = load() @@ -48,7 +48,12 @@ class DraftsManager: Codable { } } - private var drafts: [UUID: Draft] = [:] + func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + try container.encode(drafts, forKey: .drafts) + } + + @Published private var drafts: [UUID: Draft] = [:] var sorted: [Draft] { return drafts.values.sorted(by: { $0.lastModified > $1.lastModified }) } diff --git a/Tusker/Screens/Compose/ComposeHostingController.swift b/Tusker/Screens/Compose/ComposeHostingController.swift index fa9aa989..d4423d6d 100644 --- a/Tusker/Screens/Compose/ComposeHostingController.swift +++ b/Tusker/Screens/Compose/ComposeHostingController.swift @@ -335,9 +335,7 @@ class ComposeHostingController: UIHostingController, Ducka } @objc func draftsButtonPresed() { - let draftsVC = DraftsTableViewController(account: mastodonController.accountInfo!, exclude: draft) - draftsVC.delegate = self - present(UINavigationController(rootViewController: draftsVC), animated: true) + uiState.isShowingDraftsList = true } } @@ -377,6 +375,16 @@ extension ComposeHostingController: ComposeUIStateDelegate { present(ComposeDrawingNavigationController(editing: drawing, delegate: self), animated: true) } + + func selectDraft(_ draft: Draft) { + if self.draft.hasContent { + DraftsManager.save() + } else { + DraftsManager.shared.remove(self.draft) + } + uiState.draft = draft + uiState.isShowingDraftsList = false + } } extension ComposeHostingController: AssetPickerViewControllerDelegate { @@ -403,42 +411,6 @@ extension ComposeHostingController: AssetPickerViewControllerDelegate { } } -extension ComposeHostingController: DraftsTableViewControllerDelegate { - func draftSelectionCanceled() { - } - - func shouldSelectDraft(_ draft: Draft, completion: @escaping (Bool) -> Void) { - if draft.inReplyToID != self.draft.inReplyToID, - self.draft.hasContent { - let alertController = UIAlertController(title: "Different Reply", message: "The selected draft is a reply to a different status, do you wish to use it?", preferredStyle: .alert) - alertController.addAction(UIAlertAction(title: "Cancel", style: .cancel, handler: { (_) in - completion(false) - })) - alertController.addAction(UIAlertAction(title: "Restore Draft", style: .default, handler: { (_) in - completion(true) - })) - // we can't present the laert ourselves since the compose VC is already presenting the draft selector - // but presenting on the presented view controller seems hacky, is there a better way to do this? - presentedViewController!.present(alertController, animated: true) - } else { - completion(true) - } - } - - func draftSelected(_ draft: Draft) { - if self.draft.hasContent { - DraftsManager.save() - } else { - DraftsManager.shared.remove(self.draft) - } - - uiState.draft = draft - } - - func draftSelectionCompleted() { - } -} - // superseded by duckable stuff @available(iOS, obsoleted: 16.0) extension ComposeHostingController: UIAdaptivePresentationControllerDelegate { diff --git a/Tusker/Screens/Compose/ComposeUIState.swift b/Tusker/Screens/Compose/ComposeUIState.swift index ac07d190..95c0d0ca 100644 --- a/Tusker/Screens/Compose/ComposeUIState.swift +++ b/Tusker/Screens/Compose/ComposeUIState.swift @@ -15,6 +15,7 @@ protocol ComposeUIStateDelegate: AnyObject { // @available(iOS, obsoleted: 16.0) func presentAssetPickerSheet() func presentComposeDrawing() + func selectDraft(_ draft: Draft) func keyboardWillShow(accessoryView: UIView, notification: Notification) func keyboardWillHide(accessoryView: UIView, notification: Notification) @@ -27,6 +28,7 @@ class ComposeUIState: ObservableObject { @Published var draft: Draft @Published var isShowingSaveDraftSheet = false + @Published var isShowingDraftsList = false @Published var attachmentsMissingDescriptions = Set() @Published var autocompleteState: AutocompleteState? = nil diff --git a/Tusker/Screens/Compose/ComposeView.swift b/Tusker/Screens/Compose/ComposeView.swift index 5b13310b..c26c8bdc 100644 --- a/Tusker/Screens/Compose/ComposeView.swift +++ b/Tusker/Screens/Compose/ComposeView.swift @@ -98,6 +98,9 @@ struct ComposeView: View { } }) .navigationTitle(navTitle) + .sheet(isPresented: $uiState.isShowingDraftsList) { + DraftsView(currentDraft: draft) + } .actionSheet(isPresented: $uiState.isShowingSaveDraftSheet, content: self.saveAndCloseSheet) .alert(isPresented: $isShowingPostErrorAlert) { Alert( diff --git a/Tusker/Screens/Compose/DraftsView.swift b/Tusker/Screens/Compose/DraftsView.swift new file mode 100644 index 00000000..f2348b7f --- /dev/null +++ b/Tusker/Screens/Compose/DraftsView.swift @@ -0,0 +1,119 @@ +// +// DraftsView.swift +// Tusker +// +// Created by Shadowfacts on 11/9/22. +// Copyright © 2022 Shadowfacts. All rights reserved. +// + +import SwiftUI + +struct DraftsView: View { + let currentDraft: Draft + @EnvironmentObject var uiState: ComposeUIState + @EnvironmentObject var mastodonController: MastodonController + @StateObject private var draftsManager = DraftsManager.shared + @State private var draftForDifferentReply: Draft? + + private var visibleDrafts: [Draft] { + draftsManager.sorted.filter { + $0.accountID == mastodonController.accountInfo!.id && $0.id != currentDraft.id + } + } + + var body: some View { + NavigationView { + List { + ForEach(visibleDrafts) { draft in + Button { + maybeSelectDraft(draft) + } label: { + DraftView(draft: draft) + } + .onDrag { + let activity = UserActivityManager.editDraftActivity(id: draft.id, accountID: mastodonController.accountInfo!.id) + activity.displaysAuxiliaryScene = true + return NSItemProvider(object: activity) + } + } + .onDelete { indices in + indices + .map { visibleDrafts[$0] } + .forEach { draftsManager.remove($0) } + } + } + .listStyle(.plain) + .navigationTitle(Text("Drafts")) + .navigationBarTitleDisplayMode(.inline) + .toolbar { + ToolbarItem(placement: .cancellationAction) { + Button("Cancel") { + uiState.isShowingDraftsList = false + } + } + } + } + .alertWithData("Different Reply", data: $draftForDifferentReply) { draft in + Button("Cancel", role: .cancel) { + draftForDifferentReply = nil + } + Button("Restore Draft") { + uiState.delegate?.selectDraft(draft) + } + } message: { draft in + Text("The selected draft is a reply to a different post, do you wish to use it?") + } + } + + private func maybeSelectDraft(_ draft: Draft) { + if draft.inReplyToID != currentDraft.inReplyToID, + currentDraft.hasContent { + draftForDifferentReply = draft + } else { + uiState.delegate?.selectDraft(draft) + } + } +} + +struct DraftView: View { + @ObservedObject private var draft: Draft + + init(draft: Draft) { + self._draft = ObservedObject(wrappedValue: draft) + } + + var body: some View { + HStack { + VStack(alignment: .leading) { + if draft.contentWarningEnabled { + Text(draft.contentWarning) + .font(.body.bold()) + .foregroundColor(.secondary) + } + + Text(draft.text) + .font(.body) + + HStack(spacing: 8) { + ForEach(draft.attachments) { attachment in + ComposeAttachmentImage(attachment: attachment, fullSize: false) + .frame(width: 50, height: 50) + .cornerRadius(5) + } + } + } + + Spacer() + + Text(draft.lastModified.timeAgoString()) + .font(.body) + .foregroundColor(.secondary) + } + } +} + +struct DraftsView_Previews: PreviewProvider { + static var previews: some View { + DraftsView(currentDraft: Draft(accountID: "")) + } +} diff --git a/Tusker/Screens/Drafts/DraftsTableViewController.swift b/Tusker/Screens/Drafts/DraftsTableViewController.swift deleted file mode 100644 index 81a0a829..00000000 --- a/Tusker/Screens/Drafts/DraftsTableViewController.swift +++ /dev/null @@ -1,142 +0,0 @@ -// -// DraftsTableViewController.swift -// Tusker -// -// Created by Shadowfacts on 10/22/18. -// Copyright © 2018 Shadowfacts. All rights reserved. -// - -import UIKit - -protocol DraftsTableViewControllerDelegate: AnyObject { - func draftSelectionCanceled() - func shouldSelectDraft(_ draft: Draft, completion: @escaping (Bool) -> Void) - func draftSelected(_ draft: Draft) - func draftSelectionCompleted() -} - -class DraftsTableViewController: UITableViewController { - - let account: LocalData.UserAccountInfo - let excludedDraft: Draft? - weak var delegate: DraftsTableViewControllerDelegate? - - var drafts = [Draft]() - - init(account: LocalData.UserAccountInfo, exclude: Draft? = nil) { - self.account = account - self.excludedDraft = exclude - - 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.rowHeight = UITableView.automaticDimension - tableView.estimatedRowHeight = 140 - - tableView.register(UINib(nibName: "DraftTableViewCell", bundle: nil), forCellReuseIdentifier: "draftCell") - - tableView.dragDelegate = self - - drafts = DraftsManager.shared.sorted.filter { (draft) in - draft.accountID == account.id && draft != excludedDraft - } - } - - func draft(for indexPath: IndexPath) -> Draft { - return drafts[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 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) { - let draft = self.draft(for: indexPath) - func select() { - delegate?.draftSelected(draft) - dismiss(animated: true) { - self.delegate?.draftSelectionCompleted() - } - } - if let delegate = delegate { - delegate.shouldSelectDraft(draft) { (shouldSelect) in - if shouldSelect { - select() - } else { - tableView.selectRow(at: nil, animated: true, scrollPosition: .none) - } - } - } else { - select() - } - } - - override func tableView(_ tableView: UITableView, editingStyleForRowAt indexPath: IndexPath) -> UITableViewCell.EditingStyle { - return .delete - } - - override func tableView(_ tableView: UITableView, canEditRowAt indexPath: IndexPath) -> Bool { - return true - } - - override func tableView(_ tableView: UITableView, commit editingStyle: UITableViewCell.EditingStyle, forRowAt indexPath: IndexPath) { - guard editingStyle == .delete else { return } - DraftsManager.shared.remove(draft(for: indexPath)) - drafts.remove(at: indexPath.row) - tableView.deleteRows(at: [indexPath], with: .automatic) - } - - override func tableView(_ tableView: UITableView, contextMenuConfigurationForRowAt indexPath: IndexPath, point: CGPoint) -> UIContextMenuConfiguration? { - return UIContextMenuConfiguration(actionProvider: { _ in - return UIMenu(children: [ - UIAction(title: "Delete Draft", image: UIImage(systemName: "trash"), attributes: .destructive, handler: { [unowned self] _ in - DraftsManager.shared.remove(self.draft(for: indexPath)) - drafts.remove(at: indexPath.row) - tableView.deleteRows(at: [indexPath], with: .automatic) - }) - ]) - }) - } - - // MARK: - Interaction - - @objc func cancelPressed() { - delegate?.draftSelectionCanceled() - dismiss(animated: true) - } - -} - -extension DraftsTableViewController: UITableViewDragDelegate { - func tableView(_ tableView: UITableView, itemsForBeginning session: UIDragSession, at indexPath: IndexPath) -> [UIDragItem] { - let draft = self.draft(for: indexPath) - let activity = UserActivityManager.editDraftActivity(id: draft.id, accountID: account.id) - activity.displaysAuxiliaryScene = true - let provider = NSItemProvider(object: activity) - return [UIDragItem(itemProvider: provider)] - } -} diff --git a/Tusker/Screens/Drafts/DraftsTableViewController.xib b/Tusker/Screens/Drafts/DraftsTableViewController.xib deleted file mode 100644 index 612670fe..00000000 --- a/Tusker/Screens/Drafts/DraftsTableViewController.xib +++ /dev/null @@ -1,22 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - diff --git a/Tusker/Views/AlertWithData.swift b/Tusker/Views/AlertWithData.swift new file mode 100644 index 00000000..226ab9f2 --- /dev/null +++ b/Tusker/Views/AlertWithData.swift @@ -0,0 +1,45 @@ +// +// AlertWithData.swift +// Tusker +// +// Created by Shadowfacts on 11/9/22. +// Copyright © 2022 Shadowfacts. All rights reserved. +// + +import SwiftUI + +struct AlertWithData: ViewModifier { + let title: LocalizedStringKey + @Binding var data: Data? + let actions: (Data) -> A + let message: (Data) -> M + + private var isPresented: Binding { + Binding(get: { + data != nil + }, set: { newValue in + guard !newValue else { + fatalError("Cannot set isPresented to true without data") + } + data = nil + }) + } + + init(title: LocalizedStringKey, data: Binding, @ViewBuilder actions: @escaping (Data) -> A, @ViewBuilder message: @escaping (Data) -> M) { + self.title = title + self._data = data + self.actions = actions + self.message = message + } + + func body(content: Content) -> some View { + content + .alert(title, isPresented: isPresented, presenting: data, actions: actions, message: message) + } +} + +extension View { + func alertWithData(_ title: LocalizedStringKey, data: Binding, @ViewBuilder actions: @escaping (Data) -> A, @ViewBuilder message: @escaping (Data) -> M) -> some View { + modifier(AlertWithData(title: title, data: data, actions: actions, message: message)) + } +} diff --git a/Tusker/Views/Draft Cell/DraftTableViewCell.swift b/Tusker/Views/Draft Cell/DraftTableViewCell.swift deleted file mode 100644 index 3407ffcc..00000000 --- a/Tusker/Views/Draft Cell/DraftTableViewCell.swift +++ /dev/null @@ -1,73 +0,0 @@ -// -// DraftsTableViewCell.swift -// Tusker -// -// Created by Shadowfacts on 10/22/18. -// Copyright © 2018 Shadowfacts. All rights reserved. -// - -import UIKit -import Photos - -class DraftTableViewCell: UITableViewCell { - - @IBOutlet weak var contentWarningLabel: UILabel! - @IBOutlet weak var contentLabel: UILabel! - @IBOutlet weak var lastModifiedLabel: UILabel! - @IBOutlet weak var attachmentsStackViewContainer: UIView! - @IBOutlet weak var attachmentsStackView: UIStackView! - - override func awakeFromNib() { - super.awakeFromNib() - - contentView.heightAnchor.constraint(greaterThanOrEqualToConstant: 44).isActive = true - - contentWarningLabel.font = UIFontMetrics(forTextStyle: .body).scaledFont(for: .systemFont(ofSize: 15, weight: .bold)) - contentWarningLabel.adjustsFontForContentSizeCategory = true - } - - func updateUI(for draft: Draft) { - contentWarningLabel.text = draft.contentWarning - contentWarningLabel.isHidden = !draft.contentWarningEnabled - contentLabel.text = draft.text - lastModifiedLabel.text = draft.lastModified.timeAgoString() - - attachmentsStackViewContainer.isHidden = draft.attachments.count == 0 - - for attachment in draft.attachments { - let size = CGSize(width: 50, height: 50) - let imageView = UIImageView(frame: CGRect(origin: .zero, size: size)) - imageView.contentMode = .scaleAspectFill - imageView.layer.masksToBounds = true - imageView.layer.cornerRadius = 5 - attachmentsStackView.addArrangedSubview(imageView) - imageView.widthAnchor.constraint(equalTo: imageView.heightAnchor).isActive = true - - imageView.backgroundColor = .secondarySystemBackground - imageView.contentMode = .scaleAspectFill - - switch attachment.data { - case let .asset(asset): - PHImageManager.default().requestImage(for: asset, targetSize: size, contentMode: .aspectFill, options: nil) { (image, _) in - imageView.image = image - } - case let .image(image): - imageView.image = image - case .video(_): - // videos aren't saved to drafts, so this is unreachable - return - case let .drawing(drawing): - imageView.image = drawing.imageInLightMode(from: drawing.bounds) - imageView.backgroundColor = .white - imageView.contentMode = .scaleAspectFit - } - } - } - - override func prepareForReuse() { - super.prepareForReuse() - - attachmentsStackView.arrangedSubviews.forEach { $0.removeFromSuperview() } - } - -} diff --git a/Tusker/Views/Draft Cell/DraftTableViewCell.xib b/Tusker/Views/Draft Cell/DraftTableViewCell.xib deleted file mode 100644 index 8441c956..00000000 --- a/Tusker/Views/Draft Cell/DraftTableViewCell.xib +++ /dev/null @@ -1,98 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -