Compare commits

..

7 Commits

Author SHA1 Message Date
Shadowfacts 382decd7da
Fix search section titles 2019-12-16 22:23:12 -05:00
Shadowfacts 05d79d5d03
Use same nav delegate more options for context menu share sheet 2019-12-14 13:36:05 -05:00
Shadowfacts 4c0607af79
Add (un)bookmarking to status more options 2019-12-14 12:40:50 -05:00
Shadowfacts eb6cfba9aa
Fix tablel view cells being re-selected on aborted nav swipe back 2019-12-14 11:59:31 -05:00
Shadowfacts c26657bafa
Use synchronized MastodonCache to prevent race condition crashes 2019-12-14 11:31:14 -05:00
Shadowfacts 0c78af7d4f
Store in reply to status in drafts 2019-12-14 11:30:35 -05:00
Shadowfacts 681cdb8bb5
Fix automatically created drafts not being deleted after successful post
The newly created draft needs to be set to the compose VC's currentDraft
so that it gets removed after the status is successfully created.

Also, save the drafts to disk after saving a draft so that crashes don't
cause draft loss.
2019-11-28 22:26:37 -05:00
29 changed files with 485 additions and 195 deletions

View File

@ -35,6 +35,7 @@ public class Status: Decodable {
public let application: Application? public let application: Application?
public let language: String? public let language: String?
public let pinned: Bool? public let pinned: Bool?
public let bookmarked: Bool?
public static func getContext(_ status: Status) -> Request<ConversationContext> { public static func getContext(_ status: Status) -> Request<ConversationContext> {
return Request<ConversationContext>(method: .get, path: "/api/v1/statuses/\(status.id)/context") return Request<ConversationContext>(method: .get, path: "/api/v1/statuses/\(status.id)/context")
@ -84,6 +85,14 @@ public class Status: Decodable {
return Request<Status>(method: .post, path: "/api/v1/statuses/\(status.id)/unpin") return Request<Status>(method: .post, path: "/api/v1/statuses/\(status.id)/unpin")
} }
public static func bookmark(_ status: Status) -> Request<Status> {
return Request<Status>(method: .post, path: "/api/v1/statuses/\(status.id)/bookmark")
}
public static func unbookmark(_ status: Status) -> Request<Status> {
return Request<Status>(method: .post, path: "/api/v1/statuses/\(status.id)/unbookmark")
}
public static func muteConversation(_ status: Status) -> Request<Status> { public static func muteConversation(_ status: Status) -> Request<Status> {
return Request<Status>(method: .post, path: "/api/v1/statuses/\(status.id)/mute") return Request<Status>(method: .post, path: "/api/v1/statuses/\(status.id)/mute")
} }
@ -118,6 +127,7 @@ public class Status: Decodable {
case application case application
case language case language
case pinned case pinned
case bookmarked
} }
} }

View File

@ -76,6 +76,11 @@
D61AC1D5232E9FA600C54D2D /* InstanceSelectorTableViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D61AC1D4232E9FA600C54D2D /* InstanceSelectorTableViewController.swift */; }; D61AC1D5232E9FA600C54D2D /* InstanceSelectorTableViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D61AC1D4232E9FA600C54D2D /* InstanceSelectorTableViewController.swift */; };
D61AC1D8232EA42D00C54D2D /* InstanceTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = D61AC1D6232EA42D00C54D2D /* InstanceTableViewCell.swift */; }; D61AC1D8232EA42D00C54D2D /* InstanceTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = D61AC1D6232EA42D00C54D2D /* InstanceTableViewCell.swift */; };
D61AC1D9232EA42D00C54D2D /* InstanceTableViewCell.xib in Resources */ = {isa = PBXBuildFile; fileRef = D61AC1D7232EA42D00C54D2D /* InstanceTableViewCell.xib */; }; D61AC1D9232EA42D00C54D2D /* InstanceTableViewCell.xib in Resources */ = {isa = PBXBuildFile; fileRef = D61AC1D7232EA42D00C54D2D /* InstanceTableViewCell.xib */; };
D627943223A5466600D38C68 /* SelectableTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = D627943123A5466600D38C68 /* SelectableTableViewCell.swift */; };
D627943523A5525100D38C68 /* StatusActivity.swift in Sources */ = {isa = PBXBuildFile; fileRef = D627943423A5525100D38C68 /* StatusActivity.swift */; };
D627943723A552C200D38C68 /* BookmarkStatusActivity.swift in Sources */ = {isa = PBXBuildFile; fileRef = D627943623A552C200D38C68 /* BookmarkStatusActivity.swift */; };
D627943923A553B600D38C68 /* UnbookmarkStatusActivity.swift in Sources */ = {isa = PBXBuildFile; fileRef = D627943823A553B600D38C68 /* UnbookmarkStatusActivity.swift */; };
D627943B23A55BA600D38C68 /* NavigableTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = D627943A23A55BA600D38C68 /* NavigableTableViewCell.swift */; };
D627FF76217E923E00CC0648 /* DraftsManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = D627FF75217E923E00CC0648 /* DraftsManager.swift */; }; D627FF76217E923E00CC0648 /* DraftsManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = D627FF75217E923E00CC0648 /* DraftsManager.swift */; };
D627FF79217E950100CC0648 /* DraftsTableViewController.xib in Resources */ = {isa = PBXBuildFile; fileRef = D627FF78217E950100CC0648 /* DraftsTableViewController.xib */; }; D627FF79217E950100CC0648 /* DraftsTableViewController.xib in Resources */ = {isa = PBXBuildFile; fileRef = D627FF78217E950100CC0648 /* DraftsTableViewController.xib */; };
D627FF7B217E951500CC0648 /* DraftsTableViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D627FF7A217E951500CC0648 /* DraftsTableViewController.swift */; }; D627FF7B217E951500CC0648 /* DraftsTableViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D627FF7A217E951500CC0648 /* DraftsTableViewController.swift */; };
@ -339,6 +344,11 @@
D61AC1D4232E9FA600C54D2D /* InstanceSelectorTableViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InstanceSelectorTableViewController.swift; sourceTree = "<group>"; }; D61AC1D4232E9FA600C54D2D /* InstanceSelectorTableViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InstanceSelectorTableViewController.swift; sourceTree = "<group>"; };
D61AC1D6232EA42D00C54D2D /* InstanceTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InstanceTableViewCell.swift; sourceTree = "<group>"; }; D61AC1D6232EA42D00C54D2D /* InstanceTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InstanceTableViewCell.swift; sourceTree = "<group>"; };
D61AC1D7232EA42D00C54D2D /* InstanceTableViewCell.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = InstanceTableViewCell.xib; sourceTree = "<group>"; }; D61AC1D7232EA42D00C54D2D /* InstanceTableViewCell.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = InstanceTableViewCell.xib; sourceTree = "<group>"; };
D627943123A5466600D38C68 /* SelectableTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SelectableTableViewCell.swift; sourceTree = "<group>"; };
D627943423A5525100D38C68 /* StatusActivity.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatusActivity.swift; sourceTree = "<group>"; };
D627943623A552C200D38C68 /* BookmarkStatusActivity.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BookmarkStatusActivity.swift; sourceTree = "<group>"; };
D627943823A553B600D38C68 /* UnbookmarkStatusActivity.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UnbookmarkStatusActivity.swift; sourceTree = "<group>"; };
D627943A23A55BA600D38C68 /* NavigableTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NavigableTableViewCell.swift; sourceTree = "<group>"; };
D627FF75217E923E00CC0648 /* DraftsManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DraftsManager.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>"; }; 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>"; }; D627FF7A217E951500CC0648 /* DraftsTableViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DraftsTableViewController.swift; sourceTree = "<group>"; };
@ -697,6 +707,16 @@
path = "Instance Cell"; path = "Instance Cell";
sourceTree = "<group>"; sourceTree = "<group>";
}; };
D627943323A5523800D38C68 /* Status Activities */ = {
isa = PBXGroup;
children = (
D627943423A5525100D38C68 /* StatusActivity.swift */,
D627943623A552C200D38C68 /* BookmarkStatusActivity.swift */,
D627943823A553B600D38C68 /* UnbookmarkStatusActivity.swift */,
);
path = "Status Activities";
sourceTree = "<group>";
};
D627FF77217E94F200CC0648 /* Drafts */ = { D627FF77217E94F200CC0648 /* Drafts */ = {
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
@ -1041,8 +1061,9 @@
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
D6AEBB3D2321638100E5038B /* UIActivity+Types.swift */, D6AEBB3D2321638100E5038B /* UIActivity+Types.swift */,
D6AEBB4623216B0C00E5038B /* Account Activities */,
D6AEBB422321685E00E5038B /* OpenInSafariActivity.swift */, D6AEBB422321685E00E5038B /* OpenInSafariActivity.swift */,
D6AEBB4623216B0C00E5038B /* Account Activities */,
D627943323A5523800D38C68 /* Status Activities */,
); );
path = Activities; path = Activities;
sourceTree = "<group>"; sourceTree = "<group>";
@ -1074,6 +1095,8 @@
D6C693F82162E4DB007D6A6D /* StatusContentLabel.swift */, D6C693F82162E4DB007D6A6D /* StatusContentLabel.swift */,
D641C77E213DC78A004B4513 /* InlineTextAttachment.swift */, D641C77E213DC78A004B4513 /* InlineTextAttachment.swift */,
04ED00B021481ED800567C53 /* SteppedProgressView.swift */, 04ED00B021481ED800567C53 /* SteppedProgressView.swift */,
D627943A23A55BA600D38C68 /* NavigableTableViewCell.swift */,
D627943123A5466600D38C68 /* SelectableTableViewCell.swift */,
D67C57A721E2649B00C3118B /* Account Detail */, D67C57A721E2649B00C3118B /* Account Detail */,
D67C57B021E28F9400C3118B /* Compose Status Reply */, D67C57B021E28F9400C3118B /* Compose Status Reply */,
D60C07E221E817560057FAA8 /* Compose Media */, D60C07E221E817560057FAA8 /* Compose Media */,
@ -1577,6 +1600,7 @@
D6F953F021251A2900CF0F2B /* MastodonController.swift in Sources */, D6F953F021251A2900CF0F2B /* MastodonController.swift in Sources */,
0411610022B442870030A9B7 /* AttachmentViewController.swift in Sources */, 0411610022B442870030A9B7 /* AttachmentViewController.swift in Sources */,
D611C2CF232DC61100C86A49 /* HashtagTableViewCell.swift in Sources */, D611C2CF232DC61100C86A49 /* HashtagTableViewCell.swift in Sources */,
D627943723A552C200D38C68 /* BookmarkStatusActivity.swift in Sources */,
D62D2426217ABF63005076CC /* UserActivityType.swift in Sources */, D62D2426217ABF63005076CC /* UserActivityType.swift in Sources */,
D66362712136338600C9CBA2 /* ComposeViewController.swift in Sources */, D66362712136338600C9CBA2 /* ComposeViewController.swift in Sources */,
D6BC9DD7232D7811002CA326 /* TimelinesPageViewController.swift in Sources */, D6BC9DD7232D7811002CA326 /* TimelinesPageViewController.swift in Sources */,
@ -1603,6 +1627,7 @@
D6AEBB4A23216F0400E5038B /* UnfollowAccountActivity.swift in Sources */, D6AEBB4A23216F0400E5038B /* UnfollowAccountActivity.swift in Sources */,
D663626421360D2300C9CBA2 /* AvatarStyle.swift in Sources */, D663626421360D2300C9CBA2 /* AvatarStyle.swift in Sources */,
D679C09F215850EF00DA27FE /* XCBActions.swift in Sources */, D679C09F215850EF00DA27FE /* XCBActions.swift in Sources */,
D627943223A5466600D38C68 /* SelectableTableViewCell.swift in Sources */,
D6DD353F22F502EC00A9563A /* Preferences+Notification.swift in Sources */, D6DD353F22F502EC00A9563A /* Preferences+Notification.swift in Sources */,
D63661C02381C144004B9E16 /* PreferencesNavigationController.swift in Sources */, D63661C02381C144004B9E16 /* PreferencesNavigationController.swift in Sources */,
D6AEBB4523216AF800E5038B /* FollowAccountActivity.swift in Sources */, D6AEBB4523216AF800E5038B /* FollowAccountActivity.swift in Sources */,
@ -1610,14 +1635,17 @@
D6E0DC8E216EDF1E00369478 /* Previewing.swift in Sources */, D6E0DC8E216EDF1E00369478 /* Previewing.swift in Sources */,
D6BED174212667E900F02DA0 /* TimelineStatusTableViewCell.swift in Sources */, D6BED174212667E900F02DA0 /* TimelineStatusTableViewCell.swift in Sources */,
0427033822B30F5F000D31B6 /* BehaviorPrefsView.swift in Sources */, 0427033822B30F5F000D31B6 /* BehaviorPrefsView.swift in Sources */,
D627943923A553B600D38C68 /* UnbookmarkStatusActivity.swift in Sources */,
D64D0AAD2128D88B005A6F37 /* LocalData.swift in Sources */, D64D0AAD2128D88B005A6F37 /* LocalData.swift in Sources */,
D6C94D892139E6EC00CB5196 /* AttachmentView.swift in Sources */, D6C94D892139E6EC00CB5196 /* AttachmentView.swift in Sources */,
D6C693EF216192C2007D6A6D /* TuskerNavigationDelegate.swift in Sources */, D6C693EF216192C2007D6A6D /* TuskerNavigationDelegate.swift in Sources */,
D6C94D872139E62700CB5196 /* LargeImageViewController.swift in Sources */, D6C94D872139E62700CB5196 /* LargeImageViewController.swift in Sources */,
D6434EB3215B1856001A919A /* XCBRequest.swift in Sources */, D6434EB3215B1856001A919A /* XCBRequest.swift in Sources */,
D663626221360B1900C9CBA2 /* Preferences.swift in Sources */, D663626221360B1900C9CBA2 /* Preferences.swift in Sources */,
D627943B23A55BA600D38C68 /* NavigableTableViewCell.swift in Sources */,
D6333B792139AEFD00CE884A /* Date+TimeAgo.swift in Sources */, D6333B792139AEFD00CE884A /* Date+TimeAgo.swift in Sources */,
D641C77F213DC78A004B4513 /* InlineTextAttachment.swift in Sources */, D641C77F213DC78A004B4513 /* InlineTextAttachment.swift in Sources */,
D627943523A5525100D38C68 /* StatusActivity.swift in Sources */,
04496BD721625361001F1B23 /* ContentLabel.swift in Sources */, 04496BD721625361001F1B23 /* ContentLabel.swift in Sources */,
D663626C21361C6700C9CBA2 /* Account+Preferences.swift in Sources */, D663626C21361C6700C9CBA2 /* Account+Preferences.swift in Sources */,
D6333B372137838300CE884A /* AttributedString+Helpers.swift in Sources */, D6333B372137838300CE884A /* AttributedString+Helpers.swift in Sources */,

View File

@ -0,0 +1,41 @@
//
// BookmarkStatusActivity.swift
// Tusker
//
// Created by Shadowfacts on 12/14/19.
// Copyright © 2019 Shadowfacts. All rights reserved.
//
import UIKit
import Pachyderm
class BookmarkStatusActivity: StatusActivity {
override var activityType: UIActivity.ActivityType? {
return .bookmarkStatus
}
override var activityTitle: String? {
return NSLocalizedString("Bookmark", comment: "bookmark status activity title")
}
override var activityImage: UIImage? {
return UIImage(systemName: "bookmark")
}
override func perform() {
guard let status = status else { return }
let request = Status.bookmark(status)
MastodonController.client.run(request) { (response) in
if case let .success(status, _) = response {
MastodonCache.add(status: status)
} else {
// todo: display error message
UINotificationFeedbackGenerator().notificationOccurred(.error)
fatalError()
}
}
}
}

View File

@ -0,0 +1,34 @@
//
// StatusActivity.swift
// Tusker
//
// Created by Shadowfacts on 12/14/19.
// Copyright © 2019 Shadowfacts. All rights reserved.
//
import UIKit
import Pachyderm
class StatusActivity: UIActivity {
override class var activityCategory: UIActivity.Category {
return .action
}
var status: Status?
override func canPerform(withActivityItems activityItems: [Any]) -> Bool {
for case is Status in activityItems {
return true
}
return false
}
override func prepare(withActivityItems activityItems: [Any]) {
for case let status as Status in activityItems {
self.status = status
return
}
}
}

View File

@ -0,0 +1,41 @@
//
// UnbookmarkStatusActivity.swift
// Tusker
//
// Created by Shadowfacts on 12/14/19.
// Copyright © 2019 Shadowfacts. All rights reserved.
//
import UIKit
import Pachyderm
class UnbookmarkStatusActivity: StatusActivity {
override var activityType: UIActivity.ActivityType? {
return .unbookmarkStatus
}
override var activityTitle: String? {
return NSLocalizedString("Unbookmark", comment: "unbookmark status activity title")
}
override var activityImage: UIImage? {
return UIImage(systemName: "bookmark.fill")
}
override func perform() {
guard let status = status else { return }
let request = Status.unbookmark(status)
MastodonController.client.run(request) { (response) in
if case let .success(status, _) = response {
MastodonCache.add(status: status)
} else {
// todo: display error message
UINotificationFeedbackGenerator().notificationOccurred(.error)
fatalError()
}
}
}
}

View File

@ -18,5 +18,7 @@ extension UIActivity.ActivityType {
static let unfollowAccount = UIActivity.ActivityType("\(Bundle.main.bundleIdentifier!).unfollow_account") static let unfollowAccount = UIActivity.ActivityType("\(Bundle.main.bundleIdentifier!).unfollow_account")
// Status // Status
static let bookmarkStatus = UIActivity.ActivityType("\(Bundle.main.bundleIdentifier!).bookmark_status")
static let unbookmarkStatus = UIActivity.ActivityType("\(Bundle.main.bundleIdentifier!).unbookmark_status")
} }

View File

@ -16,9 +16,11 @@ class DraftsManager: Codable {
private static var archiveURL = DraftsManager.documentsDirectory.appendingPathComponent("drafts").appendingPathExtension("plist") private static var archiveURL = DraftsManager.documentsDirectory.appendingPathComponent("drafts").appendingPathExtension("plist")
static func save() { static func save() {
let encoder = PropertyListEncoder() DispatchQueue.global(qos: .userInitiated).async {
let data = try? encoder.encode(shared) let encoder = PropertyListEncoder()
try? data?.write(to: archiveURL, options: .noFileProtection) let data = try? encoder.encode(shared)
try? data?.write(to: archiveURL, options: .noFileProtection)
}
} }
static func load() -> DraftsManager { static func load() -> DraftsManager {
@ -37,8 +39,10 @@ class DraftsManager: Codable {
return drafts.sorted(by: { $0.lastModified > $1.lastModified }) return drafts.sorted(by: { $0.lastModified > $1.lastModified })
} }
func create(text: String, contentWarning: String?, attachments: [DraftAttachment]) { func create(text: String, contentWarning: String?, inReplyToID: String?, attachments: [DraftAttachment]) -> Draft {
drafts.append(Draft(text: text, contentWarning: contentWarning, lastModified: Date(), attachments: attachments)) let draft = Draft(text: text, contentWarning: contentWarning, inReplyToID: inReplyToID, attachments: attachments)
drafts.append(draft)
return draft
} }
func remove(_ draft: Draft) { func remove(_ draft: Draft) {
@ -53,15 +57,17 @@ extension DraftsManager {
let id: UUID let id: UUID
private(set) var text: String private(set) var text: String
private(set) var contentWarning: String? private(set) var contentWarning: String?
private(set) var lastModified: Date
private(set) var attachments: [DraftAttachment] private(set) var attachments: [DraftAttachment]
private(set) var inReplyToID: String?
private(set) var lastModified: Date
init(text: String, contentWarning: String?, lastModified: Date, attachments: [DraftAttachment]) { init(text: String, contentWarning: String?, inReplyToID: String?, attachments: [DraftAttachment], lastModified: Date = Date()) {
self.id = UUID() self.id = UUID()
self.text = text self.text = text
self.contentWarning = contentWarning self.contentWarning = contentWarning
self.lastModified = lastModified self.inReplyToID = inReplyToID
self.attachments = attachments self.attachments = attachments
self.lastModified = lastModified
} }
func update(text: String, contentWarning: String?, attachments: [DraftAttachment]) { func update(text: String, contentWarning: String?, attachments: [DraftAttachment]) {

View File

@ -12,10 +12,10 @@ import Pachyderm
class MastodonCache { class MastodonCache {
private static var statuses = [String: Status]() private static var statuses = CachedDictionary<Status>(name: "Statuses")
private static var accounts = [String: Account]() private static var accounts = CachedDictionary<Account>(name: "Accounts")
private static var relationships = [String: Relationship]() private static var relationships = CachedDictionary<Relationship>(name: "Relationships")
private static var notifications = [String: Pachyderm.Notification]() private static var notifications = CachedDictionary<Pachyderm.Notification>(name: "Notifications")
static let statusSubject = PassthroughSubject<Status, Never>() static let statusSubject = PassthroughSubject<Status, Never>()
static let accountSubject = PassthroughSubject<Account, Never>() static let accountSubject = PassthroughSubject<Account, Never>()
@ -134,3 +134,29 @@ class MastodonCache {
} }
} }
class CachedDictionary<Value> {
private let name: String
private var dict = [String: Value]()
private let queue: DispatchQueue
init(name: String) {
self.name = name
self.queue = DispatchQueue(label: "CachedDictionary (\(name)) Coordinator", attributes: .concurrent)
}
subscript(key: String) -> Value? {
get {
var result: Value? = nil
queue.sync {
result = dict[key]
}
return result
}
set(value) {
queue.async(flags: .barrier) {
self.dict[key] = value
}
}
}
}

View File

@ -57,6 +57,8 @@ class ComposeViewController: UIViewController {
@IBOutlet weak var statusTextView: UITextView! @IBOutlet weak var statusTextView: UITextView!
@IBOutlet weak var placeholderLabel: UILabel! @IBOutlet weak var placeholderLabel: UILabel!
@IBOutlet weak var inReplyToContainer: UIView!
@IBOutlet weak var inReplyToLabel: UILabel!
@IBOutlet weak var contentWarningContainerView: UIView! @IBOutlet weak var contentWarningContainerView: UIView!
@IBOutlet weak var contentWarningTextField: UITextField! @IBOutlet weak var contentWarningTextField: UITextField!
@ -127,49 +129,7 @@ class ComposeViewController: UIViewController {
} }
} }
if let inReplyToID = inReplyToID, let inReplyTo = MastodonCache.status(for: inReplyToID) { updateInReplyTo()
visibility = inReplyTo.visibility
if Preferences.shared.contentWarningCopyMode == .doNotCopy {
contentWarningEnabled = false
contentWarningContainerView.isHidden = true
} else {
contentWarningEnabled = !inReplyTo.spoilerText.isEmpty
contentWarningContainerView.isHidden = !contentWarningEnabled
if Preferences.shared.contentWarningCopyMode == .prependRe,
!inReplyTo.spoilerText.lowercased().starts(with: "re:") {
contentWarningTextField.text = "re: \(inReplyTo.spoilerText)"
} else {
contentWarningTextField.text = inReplyTo.spoilerText
}
}
let replyView = ComposeStatusReplyView.create()
replyView.updateUI(for: inReplyTo)
stackView.insertArrangedSubview(replyView, at: 0)
self.replyView = replyView
replyAvatarImageViewTopConstraint = replyView.avatarImageView.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor, constant: 8)
replyAvatarImageViewTopConstraint!.isActive = true
let replyLabelContainer = UIView()
replyLabelContainer.backgroundColor = .clear
replyLabelContainer.translatesAutoresizingMaskIntoConstraints = false
let replyLabel = UILabel()
replyLabel.translatesAutoresizingMaskIntoConstraints = false
replyLabel.text = "In reply to \(inReplyTo.account.realDisplayName)"
replyLabel.textColor = .secondaryLabel
replyLabelContainer.addSubview(replyLabel)
NSLayoutConstraint.activate([
replyLabel.leadingAnchor.constraint(equalTo: replyLabelContainer.leadingAnchor, constant: 8),
replyLabel.trailingAnchor.constraint(equalTo: replyLabelContainer.trailingAnchor, constant: -8),
replyLabel.topAnchor.constraint(equalTo: replyLabelContainer.topAnchor),
replyLabel.bottomAnchor.constraint(equalTo: replyLabelContainer.bottomAnchor)
])
stackView.insertArrangedSubview(replyLabelContainer, at: 1)
}
// we have to set the font here, because the monospaced digit font is not available in IB // we have to set the font here, because the monospaced digit font is not available in IB
charactersRemainingLabel.font = .monospacedDigitSystemFont(ofSize: 17, weight: .regular) charactersRemainingLabel.font = .monospacedDigitSystemFont(ofSize: 17, weight: .regular)
@ -177,13 +137,65 @@ class ComposeViewController: UIViewController {
updatePlaceholder() updatePlaceholder()
NotificationCenter.default.addObserver(self, selector: #selector(contentWarningTextFieldDidChange), name: UITextField.textDidChangeNotification, object: contentWarningTextField) NotificationCenter.default.addObserver(self, selector: #selector(contentWarningTextFieldDidChange), name: UITextField.textDidChangeNotification, object: contentWarningTextField)
}
func updateInReplyTo() {
if let replyView = replyView {
replyView.removeFromSuperview()
}
if inReplyToID == nil { if let inReplyToID = inReplyToID {
if let status = MastodonCache.status(for: inReplyToID) {
updateInReplyTo(inReplyTo: status)
} else {
let loadingVC = LoadingViewController()
embedChild(loadingVC)
MastodonCache.status(for: inReplyToID) { (status) in
guard let status = status else { return }
DispatchQueue.main.async {
self.updateInReplyTo(inReplyTo: status)
loadingVC.removeViewAndController()
}
}
}
} else {
visibility = Preferences.shared.defaultPostVisibility visibility = Preferences.shared.defaultPostVisibility
contentWarningEnabled = false contentWarningEnabled = false
inReplyToContainer.isHidden = true
} }
} }
func updateInReplyTo(inReplyTo: Status) {
visibility = inReplyTo.visibility
if Preferences.shared.contentWarningCopyMode == .doNotCopy {
contentWarningEnabled = false
contentWarningContainerView.isHidden = true
} else {
contentWarningEnabled = !inReplyTo.spoilerText.isEmpty
contentWarningContainerView.isHidden = !contentWarningEnabled
if Preferences.shared.contentWarningCopyMode == .prependRe,
!inReplyTo.spoilerText.lowercased().starts(with: "re:") {
contentWarningTextField.text = "re: \(inReplyTo.spoilerText)"
} else {
contentWarningTextField.text = inReplyTo.spoilerText
}
}
let replyView = ComposeStatusReplyView.create()
replyView.updateUI(for: inReplyTo)
stackView.insertArrangedSubview(replyView, at: 0)
self.replyView = replyView
replyAvatarImageViewTopConstraint = replyView.avatarImageView.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor, constant: 8)
replyAvatarImageViewTopConstraint!.isActive = true
inReplyToContainer.isHidden = false
inReplyToLabel.text = "In reply to \(inReplyTo.account.realDisplayName)"
}
override func viewWillAppear(_ animated: Bool) { override func viewWillAppear(_ animated: Bool) {
NotificationCenter.default.addObserver(self, selector: #selector(adjustForKeyboard), name: UIResponder.keyboardWillHideNotification, object: nil) NotificationCenter.default.addObserver(self, selector: #selector(adjustForKeyboard), name: UIResponder.keyboardWillHideNotification, object: nil)
NotificationCenter.default.addObserver(self, selector: #selector(adjustForKeyboard), name: UIResponder.keyboardWillChangeFrameNotification, object: nil) NotificationCenter.default.addObserver(self, selector: #selector(adjustForKeyboard), name: UIResponder.keyboardWillChangeFrameNotification, object: nil)
@ -338,8 +350,9 @@ class ComposeViewController: UIViewController {
if let currentDraft = self.currentDraft { if let currentDraft = self.currentDraft {
currentDraft.update(text: self.statusTextView.text, contentWarning: cw, attachments: attachments) currentDraft.update(text: self.statusTextView.text, contentWarning: cw, attachments: attachments)
} else { } else {
DraftsManager.shared.create(text: self.statusTextView.text, contentWarning: cw, attachments: attachments) self.currentDraft = DraftsManager.shared.create(text: self.statusTextView.text, contentWarning: cw, inReplyToID: inReplyToID, attachments: attachments)
} }
DraftsManager.save()
} }
@objc func close() { @objc func close() {
@ -597,9 +610,30 @@ extension ComposeViewController: DraftsTableViewControllerDelegate {
func draftSelectionCanceled() { func draftSelectionCanceled() {
} }
func shouldSelectDraft(_ draft: DraftsManager.Draft, completion: @escaping (Bool) -> Void) {
if draft.inReplyToID != self.inReplyToID {
// todo: better text for this
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 alert 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: DraftsManager.Draft) { func draftSelected(_ draft: DraftsManager.Draft) {
self.currentDraft = draft self.currentDraft = draft
inReplyToID = draft.inReplyToID
updateInReplyTo()
statusTextView.text = draft.text statusTextView.text = draft.text
contentWarningEnabled = draft.contentWarning != nil contentWarningEnabled = draft.contentWarning != nil
contentWarningTextField.text = draft.contentWarning contentWarningTextField.text = draft.contentWarning

View File

@ -1,8 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.XIB" version="3.0" toolsVersion="15400" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES"> <document type="com.apple.InterfaceBuilder3.CocoaTouch.XIB" version="3.0" toolsVersion="15505" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES">
<device id="retina4_7" orientation="portrait" appearance="light"/> <device id="retina4_7" orientation="portrait" appearance="light"/>
<dependencies> <dependencies>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="15404"/> <plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="15509"/>
<capability name="Safe area layout guides" minToolsVersion="9.0"/> <capability name="Safe area layout guides" minToolsVersion="9.0"/>
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/> <capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
</dependencies> </dependencies>
@ -15,6 +15,8 @@
<outlet property="contentView" destination="pcX-rB-RxJ" id="o95-Qa-6N7"/> <outlet property="contentView" destination="pcX-rB-RxJ" id="o95-Qa-6N7"/>
<outlet property="contentWarningContainerView" destination="kU2-7l-MSy" id="Gnq-Jb-kCA"/> <outlet property="contentWarningContainerView" destination="kU2-7l-MSy" id="Gnq-Jb-kCA"/>
<outlet property="contentWarningTextField" destination="T05-p6-vTz" id="Ivu-Ll-ByO"/> <outlet property="contentWarningTextField" destination="T05-p6-vTz" id="Ivu-Ll-ByO"/>
<outlet property="inReplyToContainer" destination="2Dv-Q7-UEA" id="hfG-5j-G5R"/>
<outlet property="inReplyToLabel" destination="Y25-eP-tDE" id="9Ei-3s-dAx"/>
<outlet property="placeholderLabel" destination="EW3-YK-vPC" id="Rsw-Nv-TNz"/> <outlet property="placeholderLabel" destination="EW3-YK-vPC" id="Rsw-Nv-TNz"/>
<outlet property="postProgressView" destination="Tq7-6P-hMT" id="amT-F1-JI0"/> <outlet property="postProgressView" destination="Tq7-6P-hMT" id="amT-F1-JI0"/>
<outlet property="scrollView" destination="6Z0-Vy-hMX" id="ya0-2T-QaV"/> <outlet property="scrollView" destination="6Z0-Vy-hMX" id="ya0-2T-QaV"/>
@ -33,10 +35,10 @@
<rect key="frame" x="0.0" y="0.0" width="375" height="667"/> <rect key="frame" x="0.0" y="0.0" width="375" height="667"/>
<subviews> <subviews>
<view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="pcX-rB-RxJ"> <view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="pcX-rB-RxJ">
<rect key="frame" x="0.0" y="0.0" width="375" height="342"/> <rect key="frame" x="0.0" y="0.0" width="375" height="371.5"/>
<subviews> <subviews>
<stackView opaque="NO" contentMode="scaleToFill" axis="vertical" translatesAutoresizingMaskIntoConstraints="NO" id="bOB-hF-O9w"> <stackView opaque="NO" contentMode="scaleToFill" axis="vertical" translatesAutoresizingMaskIntoConstraints="NO" id="bOB-hF-O9w">
<rect key="frame" x="0.0" y="0.0" width="375" height="342"/> <rect key="frame" x="0.0" y="0.0" width="375" height="371.5"/>
<subviews> <subviews>
<view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="6V0-mH-Mhu"> <view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="6V0-mH-Mhu">
<rect key="frame" x="0.0" y="0.0" width="375" height="66"/> <rect key="frame" x="0.0" y="0.0" width="375" height="66"/>
@ -63,11 +65,32 @@
<constraint firstItem="PMB-Wa-Ht0" firstAttribute="leading" secondItem="zZ3-Gv-4P5" secondAttribute="trailing" id="sVv-tH-7eB"/> <constraint firstItem="PMB-Wa-Ht0" firstAttribute="leading" secondItem="zZ3-Gv-4P5" secondAttribute="trailing" id="sVv-tH-7eB"/>
</constraints> </constraints>
</view> </view>
<view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="2Dv-Q7-UEA">
<rect key="frame" x="0.0" y="66" width="375" height="33.5"/>
<subviews>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="In reply to Display Name" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="Y25-eP-tDE">
<rect key="frame" x="4" y="8" width="367" height="21.5"/>
<constraints>
<constraint firstAttribute="height" constant="21.5" id="man-Xn-eVt"/>
</constraints>
<fontDescription key="fontDescription" type="system" pointSize="17"/>
<color key="textColor" systemColor="secondaryLabelColor" red="0.23529411759999999" green="0.23529411759999999" blue="0.26274509800000001" alpha="0.59999999999999998" colorSpace="custom" customColorSpace="sRGB"/>
<nil key="highlightedColor"/>
</label>
</subviews>
<color key="backgroundColor" white="0.0" alpha="0.0" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<constraints>
<constraint firstAttribute="bottom" secondItem="Y25-eP-tDE" secondAttribute="bottom" constant="4" id="1sZ-CX-GDU"/>
<constraint firstAttribute="trailing" secondItem="Y25-eP-tDE" secondAttribute="trailing" constant="4" id="I31-Rs-QwW"/>
<constraint firstItem="Y25-eP-tDE" firstAttribute="leading" secondItem="2Dv-Q7-UEA" secondAttribute="leading" constant="4" id="kdQ-zs-u7N"/>
<constraint firstItem="Y25-eP-tDE" firstAttribute="top" secondItem="2Dv-Q7-UEA" secondAttribute="top" constant="8" id="qdC-S5-CgV"/>
</constraints>
</view>
<view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="kU2-7l-MSy"> <view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="kU2-7l-MSy">
<rect key="frame" x="0.0" y="66" width="375" height="46"/> <rect key="frame" x="0.0" y="99.5" width="375" height="42"/>
<subviews> <subviews>
<textField opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="left" contentVerticalAlignment="center" borderStyle="roundedRect" placeholder="Write your warning here" textAlignment="natural" minimumFontSize="17" translatesAutoresizingMaskIntoConstraints="NO" id="T05-p6-vTz"> <textField opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="left" contentVerticalAlignment="center" borderStyle="roundedRect" placeholder="Write your warning here" textAlignment="natural" minimumFontSize="17" translatesAutoresizingMaskIntoConstraints="NO" id="T05-p6-vTz">
<rect key="frame" x="4" y="8" width="367" height="30"/> <rect key="frame" x="4" y="4" width="367" height="30"/>
<color key="backgroundColor" systemColor="secondarySystemBackgroundColor" red="0.94901960780000005" green="0.94901960780000005" blue="0.96862745100000003" alpha="1" colorSpace="custom" customColorSpace="sRGB"/> <color key="backgroundColor" systemColor="secondarySystemBackgroundColor" red="0.94901960780000005" green="0.94901960780000005" blue="0.96862745100000003" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
<constraints> <constraints>
<constraint firstAttribute="height" constant="30" id="yzY-MF-Ukx"/> <constraint firstAttribute="height" constant="30" id="yzY-MF-Ukx"/>
@ -80,11 +103,11 @@
<constraint firstAttribute="trailing" secondItem="T05-p6-vTz" secondAttribute="trailing" constant="4" id="8tG-eW-TG4"/> <constraint firstAttribute="trailing" secondItem="T05-p6-vTz" secondAttribute="trailing" constant="4" id="8tG-eW-TG4"/>
<constraint firstAttribute="bottom" secondItem="T05-p6-vTz" secondAttribute="bottom" constant="8" id="SUL-Hk-uvM"/> <constraint firstAttribute="bottom" secondItem="T05-p6-vTz" secondAttribute="bottom" constant="8" id="SUL-Hk-uvM"/>
<constraint firstItem="T05-p6-vTz" firstAttribute="leading" secondItem="kU2-7l-MSy" secondAttribute="leading" constant="4" id="WGG-B2-lPC"/> <constraint firstItem="T05-p6-vTz" firstAttribute="leading" secondItem="kU2-7l-MSy" secondAttribute="leading" constant="4" id="WGG-B2-lPC"/>
<constraint firstItem="T05-p6-vTz" firstAttribute="top" secondItem="kU2-7l-MSy" secondAttribute="top" constant="8" id="lvW-S0-4k4"/> <constraint firstItem="T05-p6-vTz" firstAttribute="top" secondItem="kU2-7l-MSy" secondAttribute="top" constant="4" id="dN2-Pf-qFQ"/>
</constraints> </constraints>
</view> </view>
<view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="lhQ-ae-pe9"> <view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="lhQ-ae-pe9">
<rect key="frame" x="0.0" y="112" width="375" height="150"/> <rect key="frame" x="0.0" y="141.5" width="375" height="150"/>
<subviews> <subviews>
<textView clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="scaleToFill" scrollEnabled="NO" textAlignment="natural" translatesAutoresizingMaskIntoConstraints="NO" id="9pn-0T-IHb"> <textView clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="scaleToFill" scrollEnabled="NO" textAlignment="natural" translatesAutoresizingMaskIntoConstraints="NO" id="9pn-0T-IHb">
<rect key="frame" x="4" y="0.0" width="367" height="150"/> <rect key="frame" x="4" y="0.0" width="367" height="150"/>
@ -112,7 +135,7 @@
</constraints> </constraints>
</view> </view>
<stackView opaque="NO" contentMode="scaleToFill" axis="vertical" translatesAutoresizingMaskIntoConstraints="NO" id="P0F-3w-gI1"> <stackView opaque="NO" contentMode="scaleToFill" axis="vertical" translatesAutoresizingMaskIntoConstraints="NO" id="P0F-3w-gI1">
<rect key="frame" x="0.0" y="262" width="375" height="80"/> <rect key="frame" x="0.0" y="291.5" width="375" height="80"/>
<subviews> <subviews>
<view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="752-dD-eAO"> <view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="752-dD-eAO">
<rect key="frame" x="0.0" y="0.0" width="375" height="80"/> <rect key="frame" x="0.0" y="0.0" width="375" height="80"/>

View File

@ -10,6 +10,7 @@ import UIKit
protocol DraftsTableViewControllerDelegate { protocol DraftsTableViewControllerDelegate {
func draftSelectionCanceled() func draftSelectionCanceled()
func shouldSelectDraft(_ draft: DraftsManager.Draft, completion: @escaping (Bool) -> Void)
func draftSelected(_ draft: DraftsManager.Draft) func draftSelected(_ draft: DraftsManager.Draft)
func draftSelectionCompleted() func draftSelectionCompleted()
} }
@ -61,9 +62,23 @@ class DraftsTableViewController: UITableViewController {
} }
override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
delegate?.draftSelected(draft(for: indexPath)) let draft = self.draft(for: indexPath)
dismiss(animated: true) { func select() {
self.delegate?.draftSelectionCompleted() 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()
} }
} }

View File

@ -113,10 +113,7 @@ class ConversationTableViewController: EnhancedTableViewController {
} }
} }
override func tableView(_ tableView: UITableView, willSelectRowAt indexPath: IndexPath) -> IndexPath? { // MARK: - Table view delegate
let statusID = statuses[indexPath.row].id
return statusID == mainStatusID ? nil : indexPath
}
override func tableView(_ tableView: UITableView, canEditRowAt indexPath: IndexPath) -> Bool { override func tableView(_ tableView: UITableView, canEditRowAt indexPath: IndexPath) -> Bool {
return true return true

View File

@ -109,6 +109,7 @@ class NotificationsTableViewController: EnhancedTableViewController {
} }
} }
// MARK: - Table view delegate
override func tableView(_ tableView: UITableView, willDisplay cell: UITableViewCell, forRowAt indexPath: IndexPath) { override func tableView(_ tableView: UITableView, willDisplay cell: UITableViewCell, forRowAt indexPath: IndexPath) {
if indexPath.row == groups.count - 1 { if indexPath.row == groups.count - 1 {

View File

@ -51,12 +51,10 @@ class InstanceSelectorTableViewController: UITableViewController {
case let .selected(instance): case let .selected(instance):
let cell = tableView.dequeueReusableCell(withIdentifier: instanceCell, for: indexPath) as! InstanceTableViewCell let cell = tableView.dequeueReusableCell(withIdentifier: instanceCell, for: indexPath) as! InstanceTableViewCell
cell.updateUI(instance: instance) cell.updateUI(instance: instance)
cell.delegate = self
return cell return cell
case let .recommended(instance): case let .recommended(instance):
let cell = tableView.dequeueReusableCell(withIdentifier: instanceCell, for: indexPath) as! InstanceTableViewCell let cell = tableView.dequeueReusableCell(withIdentifier: instanceCell, for: indexPath) as! InstanceTableViewCell
cell.updateUI(instance: instance) cell.updateUI(instance: instance)
cell.delegate = self
return cell return cell
} }
}) })
@ -135,6 +133,23 @@ class InstanceSelectorTableViewController: UITableViewController {
self.dataSource.apply(snapshot) self.dataSource.apply(snapshot)
} }
} }
// MARK: - Table view delegate
override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
guard let delegate = delegate, let item = dataSource.itemIdentifier(for: indexPath) else {
return
}
switch item {
case let .selected(instance):
delegate.didSelectInstance(url: URL(string: instance.uri)!)
case let .recommended(instance):
var components = URLComponents()
components.scheme = "https"
components.host = instance.domain
components.path = "/"
delegate.didSelectInstance(url: components.url!)
}
}
} }
extension InstanceSelectorTableViewController { extension InstanceSelectorTableViewController {
@ -179,16 +194,3 @@ extension InstanceSelectorTableViewController: UISearchResultsUpdating {
urlCheckerSubject.send(currentQuery) urlCheckerSubject.send(currentQuery)
} }
} }
extension InstanceSelectorTableViewController: InstanceTableViewCellDelegate {
func didSelectInstance(_ instance: Instance) {
delegate?.didSelectInstance(url: URL(string: instance.uri)!)
}
func didSelectInstance(_ instance: InstanceSelector.Instance) {
var components = URLComponents()
components.scheme = "https"
components.host = instance.domain
components.path = "/"
delegate?.didSelectInstance(url: components.url!)
}
}

View File

@ -180,6 +180,8 @@ class ProfileTableViewController: EnhancedTableViewController {
} }
} }
// MARK: - Table view delegate
override func tableView(_ tableView: UITableView, willDisplay cell: UITableViewCell, forRowAt indexPath: IndexPath) { override func tableView(_ tableView: UITableView, willDisplay cell: UITableViewCell, forRowAt indexPath: IndexPath) {
if timelineSegments.count > 0 && indexPath.section - 1 == timelineSegments.count && indexPath.row == timelineSegments[indexPath.section - 2].count - 1 { if timelineSegments.count > 0 && indexPath.section - 1 == timelineSegments.count && indexPath.row == timelineSegments[indexPath.section - 2].count - 1 {
guard let older = older else { return } guard let older = older else { return }
@ -245,17 +247,10 @@ extension ProfileTableViewController: ProfileHeaderTableViewCellDelegate {
MastodonCache.relationship(for: account.id) { [weak self] (relationship) in MastodonCache.relationship(for: account.id) { [weak self] (relationship) in
guard let self = self else { return } guard let self = self else { return }
let customActivities: [UIActivity] var customActivities: [UIActivity] = [OpenInSafariActivity()]
if let relationship = relationship { if let relationship = relationship {
let toggleFollowActivity = relationship.following ? UnfollowAccountActivity() : FollowAccountActivity() let toggleFollowActivity = relationship.following ? UnfollowAccountActivity() : FollowAccountActivity()
customActivities = [ customActivities.insert(toggleFollowActivity, at: 0)
toggleFollowActivity,
OpenInSafariActivity()
]
} else {
customActivities = [
OpenInSafariActivity()
]
} }
DispatchQueue.main.async { DispatchQueue.main.async {

View File

@ -156,8 +156,12 @@ extension SearchTableViewController {
} }
class DataSource: UITableViewDiffableDataSource<Section, Item> { class DataSource: UITableViewDiffableDataSource<Section, Item> {
override func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String? { override func tableView(_ tableView: UITableView, titleForHeaderInSection sectionIndex: Int) -> String? {
return Section.allCases[section].displayName let currentSnapshot = snapshot()
for section in Section.allCases where currentSnapshot.indexOfSection(section) == sectionIndex {
return section.displayName
}
return nil
} }
} }
} }

View File

@ -35,6 +35,13 @@ class EnhancedTableViewController: UITableViewController {
override func scrollViewWillBeginDragging(_ scrollView: UIScrollView) { override func scrollViewWillBeginDragging(_ scrollView: UIScrollView) {
prevScrollToTopOffset = nil prevScrollToTopOffset = nil
} }
override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
if let cell = tableView.cellForRow(at: indexPath) as? SelectableTableViewCell {
cell.didSelectCell()
}
}
} }

View File

@ -14,27 +14,25 @@ protocol MenuPreviewProvider {
typealias PreviewProviders = (content: UIContextMenuContentPreviewProvider, actions: () -> [UIAction]) typealias PreviewProviders = (content: UIContextMenuContentPreviewProvider, actions: () -> [UIAction])
var navigationDelegate: TuskerNavigationDelegate? { get }
func getPreviewProviders(for location: CGPoint, sourceViewController: UIViewController) -> PreviewProviders? func getPreviewProviders(for location: CGPoint, sourceViewController: UIViewController) -> PreviewProviders?
} }
extension MenuPreviewProvider { extension MenuPreviewProvider {
fileprivate func present(_ vc: UIViewController) {
UIApplication.shared.keyWindow!.rootViewController!.present(vc, animated: true)
}
func actionsForProfile(accountID: String) -> [UIAction] { func actionsForProfile(accountID: String) -> [UIAction] {
guard let account = MastodonCache.account(for: accountID) else { return [] } guard let account = MastodonCache.account(for: accountID) else { return [] }
return [ return [
createAction(identifier: "openinsafari", title: "Open in Safari", systemImageName: "safari", handler: { (_) in createAction(identifier: "openinsafari", title: "Open in Safari", systemImageName: "safari", handler: { (_) in
self.present(SFSafariViewController(url: account.url)) self.navigationDelegate?.selected(url: account.url)
}), }),
createAction(identifier: "sendmessage", title: "Send Message", systemImageName: "envelope", handler: { (_) in createAction(identifier: "sendmessage", title: "Send Message", systemImageName: "envelope", handler: { (_) in
self.present(UINavigationController(rootViewController: ComposeViewController(mentioningAcct: account.acct))) self.navigationDelegate?.compose(mentioning: account.acct)
}), }),
createAction(identifier: "share", title: "Share...", systemImageName: "square.and.arrow.up", handler: { (_) in createAction(identifier: "share", title: "Share...", systemImageName: "square.and.arrow.up", handler: { (_) in
self.present(UIActivityViewController(activityItems: [account.url], applicationActivities: nil)) self.navigationDelegate?.showMoreOptions(forAccount: accountID)
}) })
] ]
} }
@ -42,10 +40,10 @@ extension MenuPreviewProvider {
func actionsForURL(_ url: URL) -> [UIAction] { func actionsForURL(_ url: URL) -> [UIAction] {
return [ return [
createAction(identifier: "openinsafari", title: "Open in Safari", systemImageName: "safari", handler: { (_) in createAction(identifier: "openinsafari", title: "Open in Safari", systemImageName: "safari", handler: { (_) in
self.present(SFSafariViewController(url: url)) self.navigationDelegate?.selected(url: url)
}), }),
createAction(identifier: "share", title: "Share...", systemImageName: "square.and.arrow.up", handler: { (_) in createAction(identifier: "share", title: "Share...", systemImageName: "square.and.arrow.up", handler: { (_) in
self.present(UIActivityViewController(activityItems: [url], applicationActivities: nil)) self.navigationDelegate?.showMoreOptions(forURL: url)
}) })
] ]
} }
@ -58,13 +56,13 @@ extension MenuPreviewProvider {
guard let status = MastodonCache.status(for: statusID) else { return [] } guard let status = MastodonCache.status(for: statusID) else { return [] }
return [ return [
createAction(identifier: "reply", title: "Reply", systemImageName: "arrowshape.turn.up.left", handler: { (_) in createAction(identifier: "reply", title: "Reply", systemImageName: "arrowshape.turn.up.left", handler: { (_) in
self.present(UINavigationController(rootViewController: ComposeViewController(inReplyTo: statusID))) self.navigationDelegate?.reply(to: statusID)
}), }),
createAction(identifier: "openinsafari", title: "Open in Safari", systemImageName: "safari", handler: { (_) in createAction(identifier: "openinsafari", title: "Open in Safari", systemImageName: "safari", handler: { (_) in
self.present(SFSafariViewController(url: status.url!)) self.navigationDelegate?.selected(url: status.url!)
}), }),
createAction(identifier: "share", title: "Share...", systemImageName: "square.and.arrow.up", handler: { (_) in createAction(identifier: "share", title: "Share...", systemImageName: "square.and.arrow.up", handler: { (_) in
self.present(UIActivityViewController(activityItems: [status.url!], applicationActivities: nil)) self.navigationDelegate?.showMoreOptions(forStatus: statusID)
}) })
] ]
} }

View File

@ -28,6 +28,8 @@ protocol TuskerNavigationDelegate {
func compose() func compose()
func compose(mentioning: String?)
func reply(to statusID: String) func reply(to statusID: String)
func largeImage(_ image: UIImage, description: String?, sourceView: UIImageView) -> LargeImageViewController func largeImage(_ image: UIImage, description: String?, sourceView: UIImageView) -> LargeImageViewController
@ -44,6 +46,8 @@ protocol TuskerNavigationDelegate {
func showMoreOptions(forStatus statusID: String) func showMoreOptions(forStatus statusID: String)
func showMoreOptions(forAccount accountID: String)
func showMoreOptions(forURL url: URL) func showMoreOptions(forURL url: URL)
func showFollowedByList(accountIDs: [String]) func showFollowedByList(accountIDs: [String])
@ -112,8 +116,13 @@ extension TuskerNavigationDelegate where Self: UIViewController {
show(ConversationTableViewController(for: statusID, state: state), sender: self) show(ConversationTableViewController(for: statusID, state: state), sender: self)
} }
// protocols can't have parameter defaults, so this stub is necessary to fulfill the protocol req
func compose() { func compose() {
let compose = ComposeViewController() compose(mentioning: nil)
}
func compose(mentioning: String? = nil) {
let compose = ComposeViewController( mentioningAcct: mentioning)
let vc = UINavigationController(rootViewController: compose) let vc = UINavigationController(rootViewController: compose)
vc.presentationController?.delegate = compose vc.presentationController?.delegate = compose
present(vc, animated: true) present(vc, animated: true)
@ -185,7 +194,20 @@ extension TuskerNavigationDelegate where Self: UIViewController {
private func moreOptions(forStatus statusID: String) -> UIViewController { private func moreOptions(forStatus statusID: String) -> UIViewController {
guard let status = MastodonCache.status(for: statusID) else { fatalError("Missing cached status \(statusID)") } guard let status = MastodonCache.status(for: statusID) else { fatalError("Missing cached status \(statusID)") }
guard let url = status.url else { fatalError("Missing url for status \(statusID)") } guard let url = status.url else { fatalError("Missing url for status \(statusID)") }
return moreOptions(forURL: url) var customActivites: [UIActivity] = [OpenInSafariActivity()]
if let bookmarked = status.bookmarked {
customActivites.insert(bookmarked ? UnbookmarkStatusActivity() : BookmarkStatusActivity(), at: 0)
}
let activityController = UIActivityViewController(activityItems: [url, status], applicationActivities: customActivites)
activityController.completionWithItemsHandler = OpenInSafariActivity.completionHandler(viewController: self, url: url)
return activityController
}
private func moreOptions(forAccount accountID: String) -> UIViewController {
guard let account = MastodonCache.account(for: accountID) else { fatalError("Missing cached account \(accountID)") }
return moreOptions(forURL: account.url)
} }
func showMoreOptions(forStatus statusID: String) { func showMoreOptions(forStatus statusID: String) {
@ -196,6 +218,10 @@ extension TuskerNavigationDelegate where Self: UIViewController {
present(moreOptions(forURL: url), animated: true) present(moreOptions(forURL: url), animated: true)
} }
func showMoreOptions(forAccount accountID: String) {
present(moreOptions(forAccount: accountID), animated: true)
}
func showFollowedByList(accountIDs: [String]) { func showFollowedByList(accountIDs: [String]) {
let vc = AccountListTableViewController(accountIDs: accountIDs) let vc = AccountListTableViewController(accountIDs: accountIDs)
vc.title = NSLocalizedString("Followed By", comment: "followed by accounts list title") vc.title = NSLocalizedString("Followed By", comment: "followed by accounts list title")

View File

@ -56,20 +56,18 @@ class AccountTableViewCell: UITableViewCell {
updateUIForPrefrences() updateUIForPrefrences()
} }
override func setSelected(_ selected: Bool, animated: Bool) { }
super.setSelected(selected, animated: animated)
if selected { extension AccountTableViewCell: SelectableTableViewCell {
delegate?.selected(account: accountID) func didSelectCell() {
} delegate?.selected(account: accountID)
} }
} }
extension AccountTableViewCell: MenuPreviewProvider { extension AccountTableViewCell: MenuPreviewProvider {
var navigationDelegate: TuskerNavigationDelegate? { return delegate }
func getPreviewProviders(for location: CGPoint, sourceViewController: UIViewController) -> PreviewProviders? { func getPreviewProviders(for location: CGPoint, sourceViewController: UIViewController) -> PreviewProviders? {
return (content: { ProfileTableViewController(accountID: self.accountID) }, actions: { self.actionsForProfile(accountID: self.accountID) }) return (content: { ProfileTableViewController(accountID: self.accountID) }, actions: { self.actionsForProfile(accountID: self.accountID) })
} }
} }

View File

@ -23,12 +23,10 @@ class HashtagTableViewCell: UITableViewCell {
hashtagLabel.text = "#\(hashtag.name)" hashtagLabel.text = "#\(hashtag.name)"
} }
override func setSelected(_ selected: Bool, animated: Bool) { }
super.setSelected(selected, animated: animated)
extension HashtagTableViewCell: SelectableTableViewCell {
if selected { func didSelectCell() {
delegate?.selected(tag: hashtag) delegate?.selected(tag: hashtag)
} }
}
} }

View File

@ -9,15 +9,8 @@
import UIKit import UIKit
import Pachyderm import Pachyderm
protocol InstanceTableViewCellDelegate {
func didSelectInstance(_ instance: Instance)
func didSelectInstance(_ instance: InstanceSelector.Instance)
}
class InstanceTableViewCell: UITableViewCell { class InstanceTableViewCell: UITableViewCell {
var delegate: InstanceTableViewCellDelegate?
@IBOutlet weak var thumbnailImageView: UIImageView! @IBOutlet weak var thumbnailImageView: UIImageView!
@IBOutlet weak var domainLabel: UILabel! @IBOutlet weak var domainLabel: UILabel!
@IBOutlet weak var adultLabel: UILabel! @IBOutlet weak var adultLabel: UILabel!
@ -74,16 +67,4 @@ class InstanceTableViewCell: UITableViewCell {
} }
} }
override func setSelected(_ selected: Bool, animated: Bool) {
super.setSelected(selected, animated: animated)
if selected, let delegate = delegate {
if let instance = instance {
delegate.didSelectInstance(instance)
} else if let instance = selectorInstance {
delegate.didSelectInstance(instance)
}
}
}
} }

View File

@ -0,0 +1,13 @@
//
// NavigableTableViewCell.swift
// Tusker
//
// Created by Shadowfacts on 12/14/19.
// Copyright © 2019 Shadowfacts. All rights reserved.
//
import Foundation
protocol NavigableTableViewCell {
var navigationDelegate: TuskerNavigationDelegate? { get }
}

View File

@ -149,30 +149,30 @@ class ActionNotificationGroupTableViewCell: UITableViewCell {
updateTimestampWorkItem?.cancel() updateTimestampWorkItem?.cancel()
updateTimestampWorkItem = nil updateTimestampWorkItem = nil
} }
override func setSelected(_ selected: Bool, animated: Bool) {
super.setSelected(selected, animated: animated)
if selected, let delegate = delegate {
let notifications = group.notificationIDs.compactMap(MastodonCache.notification(for:))
let accountIDs = notifications.map { $0.account.id }
let action: StatusActionAccountListTableViewController.ActionType
switch notifications.first!.kind {
case .favourite:
action = .favorite
case .reblog:
action = .reblog
default:
fatalError()
}
let vc = delegate.statusActionAccountList(action: action, statusID: statusID, statusState: .unknown, accountIDs: accountIDs)
delegate.show(vc)
}
}
} }
extension ActionNotificationGroupTableViewCell: SelectableTableViewCell {
func didSelectCell() {
guard let delegate = delegate else { return }
let notifications = group.notificationIDs.compactMap(MastodonCache.notification(for:))
let accountIDs = notifications.map { $0.account.id }
let action: StatusActionAccountListTableViewController.ActionType
switch notifications.first!.kind {
case .favourite:
action = .favorite
case .reblog:
action = .reblog
default:
fatalError()
}
let vc = delegate.statusActionAccountList(action: action, statusID: statusID, statusState: .unknown, accountIDs: accountIDs)
delegate.show(vc)
}
}
extension ActionNotificationGroupTableViewCell: MenuPreviewProvider { extension ActionNotificationGroupTableViewCell: MenuPreviewProvider {
var navigationDelegate: TuskerNavigationDelegate? { return delegate }
func getPreviewProviders(for location: CGPoint, sourceViewController: UIViewController) -> PreviewProviders? { func getPreviewProviders(for location: CGPoint, sourceViewController: UIViewController) -> PreviewProviders? {
return (content: { return (content: {
@ -192,5 +192,4 @@ extension ActionNotificationGroupTableViewCell: MenuPreviewProvider {
return [] return []
}) })
} }
} }

View File

@ -112,26 +112,25 @@ class FollowNotificationGroupTableViewCell: UITableViewCell {
updateTimestampWorkItem?.cancel() updateTimestampWorkItem?.cancel()
updateTimestampWorkItem = nil updateTimestampWorkItem = nil
} }
override func setSelected(_ selected: Bool, animated: Bool) { }
super.setSelected(selected, animated: animated)
extension FollowNotificationGroupTableViewCell: SelectableTableViewCell {
if selected { func didSelectCell() {
let people = group.notificationIDs.compactMap(MastodonCache.notification(for:)).map { $0.account.id } let people = group.notificationIDs.compactMap(MastodonCache.notification(for:)).map { $0.account.id }
switch people.count { switch people.count {
case 0: case 0:
return return
case 1: case 1:
delegate?.selected(account: people.first!) delegate?.selected(account: people.first!)
default: default:
delegate?.showFollowedByList(accountIDs: people) delegate?.showFollowedByList(accountIDs: people)
}
} }
} }
} }
extension FollowNotificationGroupTableViewCell: MenuPreviewProvider { extension FollowNotificationGroupTableViewCell: MenuPreviewProvider {
var navigationDelegate: TuskerNavigationDelegate? { return delegate }
func getPreviewProviders(for location: CGPoint, sourceViewController: UIViewController) -> PreviewProviders? { func getPreviewProviders(for location: CGPoint, sourceViewController: UIViewController) -> PreviewProviders? {
return (content: { return (content: {
@ -145,5 +144,4 @@ extension FollowNotificationGroupTableViewCell: MenuPreviewProvider {
return [] return []
}) })
} }
} }

View File

@ -151,6 +151,7 @@ class ProfileHeaderTableViewCell: UITableViewCell {
} }
extension ProfileHeaderTableViewCell: MenuPreviewProvider { extension ProfileHeaderTableViewCell: MenuPreviewProvider {
var navigationDelegate: TuskerNavigationDelegate? { return delegate }
func getPreviewProviders(for location: CGPoint, sourceViewController: UIViewController) -> PreviewProviders? { func getPreviewProviders(for location: CGPoint, sourceViewController: UIViewController) -> PreviewProviders? {
let noteLabelPoint = noteLabel.convert(location, from: self) let noteLabelPoint = noteLabel.convert(location, from: self)
if noteLabel.bounds.contains(noteLabelPoint), if noteLabel.bounds.contains(noteLabelPoint),

View File

@ -0,0 +1,13 @@
//
// SelectableTableViewCell.swift
// Tusker
//
// Created by Shadowfacts on 12/14/19.
// Copyright © 2019 Shadowfacts. All rights reserved.
//
import UIKit
protocol SelectableTableViewCell {
func didSelectCell()
}

View File

@ -15,13 +15,12 @@ protocol StatusTableViewCellDelegate: TuskerNavigationDelegate {
} }
class BaseStatusTableViewCell: UITableViewCell { class BaseStatusTableViewCell: UITableViewCell {
var delegate: StatusTableViewCellDelegate? { var delegate: StatusTableViewCellDelegate? {
didSet { didSet {
contentLabel.navigationDelegate = delegate contentLabel.navigationDelegate = delegate
} }
} }
@IBOutlet weak var avatarImageView: UIImageView! @IBOutlet weak var avatarImageView: UIImageView!
@IBOutlet weak var displayNameLabel: UILabel! @IBOutlet weak var displayNameLabel: UILabel!
@IBOutlet weak var usernameLabel: UILabel! @IBOutlet weak var usernameLabel: UILabel!
@ -311,6 +310,8 @@ extension BaseStatusTableViewCell: AttachmentViewDelegate {
} }
extension BaseStatusTableViewCell: MenuPreviewProvider { extension BaseStatusTableViewCell: MenuPreviewProvider {
var navigationDelegate: TuskerNavigationDelegate? { return delegate }
func getPreviewProviders(for location: CGPoint, sourceViewController: UIViewController) -> PreviewProviders? { func getPreviewProviders(for location: CGPoint, sourceViewController: UIViewController) -> PreviewProviders? {
if avatarImageView.frame.contains(location) { if avatarImageView.frame.contains(location) {
return (content: { ProfileTableViewController(accountID: self.accountID)}, actions: { self.actionsForProfile(accountID: self.accountID) }) return (content: { ProfileTableViewController(accountID: self.accountID)}, actions: { self.actionsForProfile(accountID: self.accountID) })

View File

@ -119,14 +119,6 @@ class TimelineStatusTableViewCell: BaseStatusTableViewCell {
showPinned = false showPinned = false
} }
override func setSelected(_ selected: Bool, animated: Bool) {
super.setSelected(selected, animated: animated)
if selected {
delegate?.selected(status: statusID, state: statusState.copy())
}
}
@objc func reblogLabelPressed() { @objc func reblogLabelPressed() {
guard let rebloggerID = rebloggerID else { return } guard let rebloggerID = rebloggerID else { return }
delegate?.selected(account: rebloggerID) delegate?.selected(account: rebloggerID)
@ -141,6 +133,12 @@ class TimelineStatusTableViewCell: BaseStatusTableViewCell {
} }
extension TimelineStatusTableViewCell: SelectableTableViewCell {
func didSelectCell() {
delegate?.selected(status: statusID, state: statusState.copy())
}
}
extension TimelineStatusTableViewCell: TableViewSwipeActionProvider { extension TimelineStatusTableViewCell: TableViewSwipeActionProvider {
func leadingSwipeActionsConfiguration() -> UISwipeActionsConfiguration? { func leadingSwipeActionsConfiguration() -> UISwipeActionsConfiguration? {