Rewrite Drafts screen with SwiftUI

This commit is contained in:
Shadowfacts 2022-11-09 18:18:31 -05:00
parent d0f8691560
commit 6d692c2730
12 changed files with 197 additions and 408 deletions

View File

@ -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 = "<group>"; };
D627944E23A9C99800D38C68 /* EditListAccountsViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EditListAccountsViewController.swift; sourceTree = "<group>"; };
D627FF75217E923E00CC0648 /* DraftsManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DraftsManager.swift; sourceTree = "<group>"; };
D627FF78217E950100CC0648 /* DraftsTableViewController.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = DraftsTableViewController.xib; sourceTree = "<group>"; };
D627FF7A217E951500CC0648 /* DraftsTableViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DraftsTableViewController.swift; sourceTree = "<group>"; };
D627FF7C217E958900CC0648 /* DraftTableViewCell.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = DraftTableViewCell.xib; sourceTree = "<group>"; };
D627FF7E217E95E000CC0648 /* DraftTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DraftTableViewCell.swift; sourceTree = "<group>"; };
D6285B5221EA708700FE4B39 /* StatusFormat.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatusFormat.swift; sourceTree = "<group>"; };
D6289E83217B795D0003D1D7 /* LargeImageViewController.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = LargeImageViewController.xib; sourceTree = "<group>"; };
D62D2421217AA7E1005076CC /* UserActivityManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserActivityManager.swift; sourceTree = "<group>"; };
@ -615,6 +609,8 @@
D6BC9DD9232D8BE5002CA326 /* SearchResultsViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchResultsViewController.swift; sourceTree = "<group>"; };
D6BEA243291A0C83002F4D01 /* Duckable */ = {isa = PBXFileReference; lastKnownFileType = wrapper; name = Duckable; path = Packages/Duckable; sourceTree = "<group>"; };
D6BEA246291A0F2D002F4D01 /* Duckable+Root.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Duckable+Root.swift"; sourceTree = "<group>"; };
D6BEA248291C6118002F4D01 /* DraftsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DraftsView.swift; sourceTree = "<group>"; };
D6BEA24A291C6A2B002F4D01 /* AlertWithData.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AlertWithData.swift; sourceTree = "<group>"; };
D6BED173212667E900F02DA0 /* TimelineStatusTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelineStatusTableViewCell.swift; sourceTree = "<group>"; };
D6C143D9253510F4007DC240 /* ComposeEmojiTextField.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComposeEmojiTextField.swift; sourceTree = "<group>"; };
D6C1B2072545D1EC00DAAA66 /* StatusCardView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatusCardView.swift; sourceTree = "<group>"; };
@ -743,15 +739,6 @@
path = "Hashtag Cell";
sourceTree = "<group>";
};
D61959D0241E842400A37B8E /* Draft Cell */ = {
isa = PBXGroup;
children = (
D627FF7C217E958900CC0648 /* DraftTableViewCell.xib */,
D627FF7E217E95E000CC0648 /* DraftTableViewCell.swift */,
);
path = "Draft Cell";
sourceTree = "<group>";
};
D61959D2241E846D00A37B8E /* Models */ = {
isa = PBXGroup;
children = (
@ -850,15 +837,6 @@
path = Lists;
sourceTree = "<group>";
};
D627FF77217E94F200CC0648 /* Drafts */ = {
isa = PBXGroup;
children = (
D627FF78217E950100CC0648 /* DraftsTableViewController.xib */,
D627FF7A217E951500CC0648 /* DraftsTableViewController.swift */,
);
path = Drafts;
sourceTree = "<group>";
};
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 = "<group>";
@ -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 */,

View File

@ -107,6 +107,8 @@ extension Draft: Equatable {
}
}
extension Draft: Identifiable {}
extension Draft {
enum CodingKeys: String, CodingKey {
case id

View File

@ -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 })
}

View File

@ -335,9 +335,7 @@ class ComposeHostingController: UIHostingController<ComposeContainerView>, 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 {

View File

@ -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<UUID>()
@Published var autocompleteState: AutocompleteState? = nil

View File

@ -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(

View File

@ -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: ""))
}
}

View File

@ -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)]
}
}

View File

@ -1,22 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.XIB" version="3.0" toolsVersion="14810.11" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES">
<device id="retina4_7" orientation="portrait" appearance="light"/>
<dependencies>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="14766.13"/>
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
</dependencies>
<objects>
<placeholder placeholderIdentifier="IBFilesOwner" id="-1" userLabel="File's Owner" customClass="DraftsTableViewController" customModule="Tusker" customModuleProvider="target">
<connections>
<outlet property="view" destination="O5v-ea-iTS" id="sft-3K-LZf"/>
</connections>
</placeholder>
<placeholder placeholderIdentifier="IBFirstResponder" id="-2" customClass="UIResponder"/>
<tableView clipsSubviews="YES" contentMode="scaleToFill" alwaysBounceVertical="YES" style="plain" separatorStyle="default" rowHeight="-1" estimatedRowHeight="-1" sectionHeaderHeight="28" sectionFooterHeight="28" id="O5v-ea-iTS">
<rect key="frame" x="0.0" y="0.0" width="375" height="667"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
<color key="backgroundColor" xcode11CocoaTouchSystemColor="systemBackgroundColor" cocoaTouchSystemColor="whiteColor"/>
<point key="canvasLocation" x="-302" y="87"/>
</tableView>
</objects>
</document>

View File

@ -0,0 +1,45 @@
//
// AlertWithData.swift
// Tusker
//
// Created by Shadowfacts on 11/9/22.
// Copyright © 2022 Shadowfacts. All rights reserved.
//
import SwiftUI
struct AlertWithData<Data, A: View, M: View>: ViewModifier {
let title: LocalizedStringKey
@Binding var data: Data?
let actions: (Data) -> A
let message: (Data) -> M
private var isPresented: Binding<Bool> {
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<Data?>, @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<Data, A: View, M: View>(_ title: LocalizedStringKey, data: Binding<Data?>, @ViewBuilder actions: @escaping (Data) -> A, @ViewBuilder message: @escaping (Data) -> M) -> some View {
modifier(AlertWithData(title: title, data: data, actions: actions, message: message))
}
}

View File

@ -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() }
}
}

View File

@ -1,98 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.XIB" version="3.0" toolsVersion="21507" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES">
<device id="retina4_7" orientation="portrait" appearance="light"/>
<dependencies>
<deployment identifier="iOS"/>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="21505"/>
<capability name="System colors in document resources" minToolsVersion="11.0"/>
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
</dependencies>
<objects>
<placeholder placeholderIdentifier="IBFilesOwner" id="-1" userLabel="File's Owner"/>
<placeholder placeholderIdentifier="IBFirstResponder" id="-2" customClass="UIResponder"/>
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" selectionStyle="default" indentationWidth="10" rowHeight="143" id="Q7N-Mt-RPF" customClass="DraftTableViewCell" customModule="Tusker" customModuleProvider="target">
<rect key="frame" x="0.0" y="0.0" width="375" height="143"/>
<autoresizingMask key="autoresizingMask"/>
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" preservesSuperviewLayoutMargins="YES" insetsLayoutMarginsFromSafeArea="NO" tableViewCell="Q7N-Mt-RPF" id="KVi-jA-AET">
<rect key="frame" x="0.0" y="0.0" width="375" height="143"/>
<autoresizingMask key="autoresizingMask"/>
<subviews>
<stackView opaque="NO" contentMode="scaleToFill" axis="vertical" spacing="8" translatesAutoresizingMaskIntoConstraints="NO" id="gaD-3B-qO1">
<rect key="frame" x="16" y="11" width="351" height="124"/>
<subviews>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" ambiguous="YES" text="Content Warning" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="VhS-ig-6Fu">
<rect key="frame" x="0.0" y="0.0" width="351" height="18"/>
<fontDescription key="fontDescription" type="boldSystem" pointSize="15"/>
<color key="textColor" systemColor="secondaryLabelColor"/>
<nil key="highlightedColor"/>
</label>
<view contentMode="scaleToFill" ambiguous="YES" translatesAutoresizingMaskIntoConstraints="NO" id="zMS-88-DcM">
<rect key="frame" x="0.0" y="26" width="351" height="40"/>
<subviews>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" verticalHuggingPriority="251" ambiguous="YES" text="Content" textAlignment="natural" lineBreakMode="tailTruncation" numberOfLines="3" baselineAdjustment="alignBaselines" adjustsFontForContentSizeCategory="YES" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="8eA-yd-rBp">
<rect key="frame" x="0.0" y="0.0" width="310.5" height="32"/>
<fontDescription key="fontDescription" style="UICTFontTextStyleBody"/>
<nil key="textColor"/>
<nil key="highlightedColor"/>
</label>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" horizontalCompressionResistancePriority="751" text="2m" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontForContentSizeCategory="YES" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="D2X-9O-iQw">
<rect key="frame" x="326.5" y="0.0" width="24.5" height="20.5"/>
<fontDescription key="fontDescription" style="UICTFontTextStyleBody"/>
<color key="textColor" systemColor="secondaryLabelColor"/>
<nil key="highlightedColor"/>
</label>
</subviews>
<constraints>
<constraint firstItem="D2X-9O-iQw" firstAttribute="leading" secondItem="8eA-yd-rBp" secondAttribute="trailing" constant="16" id="6Ux-ee-J5h"/>
<constraint firstAttribute="trailing" secondItem="D2X-9O-iQw" secondAttribute="trailing" id="IRH-mM-HSs"/>
<constraint firstItem="8eA-yd-rBp" firstAttribute="leading" secondItem="zMS-88-DcM" secondAttribute="leading" id="StS-F9-9B3"/>
<constraint firstItem="8eA-yd-rBp" firstAttribute="top" secondItem="zMS-88-DcM" secondAttribute="top" id="Uuq-g5-n0A"/>
<constraint firstItem="D2X-9O-iQw" firstAttribute="top" secondItem="zMS-88-DcM" secondAttribute="top" id="lWB-6Z-nbG"/>
<constraint firstAttribute="bottom" secondItem="8eA-yd-rBp" secondAttribute="bottom" id="zCK-s5-4Zo"/>
</constraints>
</view>
<view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="csc-gx-KVg">
<rect key="frame" x="0.0" y="74" width="351" height="50"/>
<subviews>
<stackView opaque="NO" contentMode="scaleToFill" ambiguous="YES" spacing="8" translatesAutoresizingMaskIntoConstraints="NO" id="htC-hf-vJ4">
<rect key="frame" x="0.0" y="0.0" width="352" height="50"/>
<constraints>
<constraint firstAttribute="height" constant="50" id="lxT-O2-afE"/>
</constraints>
</stackView>
</subviews>
<constraints>
<constraint firstItem="htC-hf-vJ4" firstAttribute="leading" secondItem="csc-gx-KVg" secondAttribute="leading" id="c0s-O9-XKa"/>
<constraint firstItem="htC-hf-vJ4" firstAttribute="top" secondItem="csc-gx-KVg" secondAttribute="top" id="lcl-RN-qHw"/>
<constraint firstAttribute="bottom" secondItem="htC-hf-vJ4" secondAttribute="bottom" id="oHX-Qh-bmI"/>
</constraints>
</view>
</subviews>
<constraints>
<constraint firstAttribute="trailing" secondItem="csc-gx-KVg" secondAttribute="trailing" id="AcZ-yc-8Zh"/>
</constraints>
</stackView>
</subviews>
<constraints>
<constraint firstAttribute="bottom" secondItem="gaD-3B-qO1" secondAttribute="bottomMargin" constant="8" id="4Hz-ax-JI6"/>
<constraint firstItem="gaD-3B-qO1" firstAttribute="leading" secondItem="KVi-jA-AET" secondAttribute="leadingMargin" id="KRA-Q8-klX"/>
<constraint firstAttribute="trailing" secondItem="gaD-3B-qO1" secondAttribute="trailingMargin" constant="8" id="iGc-c4-n9y"/>
<constraint firstItem="gaD-3B-qO1" firstAttribute="top" secondItem="KVi-jA-AET" secondAttribute="topMargin" id="rVE-Jo-6zG"/>
</constraints>
</tableViewCellContentView>
<connections>
<outlet property="attachmentsStackView" destination="htC-hf-vJ4" id="kEX-m7-LuE"/>
<outlet property="attachmentsStackViewContainer" destination="csc-gx-KVg" id="rIM-pj-TFX"/>
<outlet property="contentLabel" destination="8eA-yd-rBp" id="Uy0-8G-WbU"/>
<outlet property="contentWarningLabel" destination="VhS-ig-6Fu" id="jIU-vr-OsY"/>
<outlet property="lastModifiedLabel" destination="D2X-9O-iQw" id="dx7-0E-RuM"/>
</connections>
<point key="canvasLocation" x="-388" y="184.85757121439281"/>
</tableViewCell>
</objects>
<resources>
<systemColor name="secondaryLabelColor">
<color red="0.23529411764705882" green="0.23529411764705882" blue="0.2627450980392157" alpha="0.59999999999999998" colorSpace="custom" customColorSpace="sRGB"/>
</systemColor>
</resources>
</document>