Compare commits
7 Commits
06442b5629
...
382decd7da
Author | SHA1 | Date |
---|---|---|
Shadowfacts | 382decd7da | |
Shadowfacts | 05d79d5d03 | |
Shadowfacts | 4c0607af79 | |
Shadowfacts | eb6cfba9aa | |
Shadowfacts | c26657bafa | |
Shadowfacts | 0c78af7d4f | |
Shadowfacts | 681cdb8bb5 |
|
@ -35,6 +35,7 @@ public class Status: Decodable {
|
|||
public let application: Application?
|
||||
public let language: String?
|
||||
public let pinned: Bool?
|
||||
public let bookmarked: Bool?
|
||||
|
||||
public static func getContext(_ status: Status) -> Request<ConversationContext> {
|
||||
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")
|
||||
}
|
||||
|
||||
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> {
|
||||
return Request<Status>(method: .post, path: "/api/v1/statuses/\(status.id)/mute")
|
||||
}
|
||||
|
@ -118,6 +127,7 @@ public class Status: Decodable {
|
|||
case application
|
||||
case language
|
||||
case pinned
|
||||
case bookmarked
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -76,6 +76,11 @@
|
|||
D61AC1D5232E9FA600C54D2D /* InstanceSelectorTableViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D61AC1D4232E9FA600C54D2D /* InstanceSelectorTableViewController.swift */; };
|
||||
D61AC1D8232EA42D00C54D2D /* InstanceTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = D61AC1D6232EA42D00C54D2D /* InstanceTableViewCell.swift */; };
|
||||
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 */; };
|
||||
D627FF79217E950100CC0648 /* DraftsTableViewController.xib in Resources */ = {isa = PBXBuildFile; fileRef = D627FF78217E950100CC0648 /* DraftsTableViewController.xib */; };
|
||||
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>"; };
|
||||
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>"; };
|
||||
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>"; };
|
||||
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>"; };
|
||||
|
@ -697,6 +707,16 @@
|
|||
path = "Instance Cell";
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
D627943323A5523800D38C68 /* Status Activities */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
D627943423A5525100D38C68 /* StatusActivity.swift */,
|
||||
D627943623A552C200D38C68 /* BookmarkStatusActivity.swift */,
|
||||
D627943823A553B600D38C68 /* UnbookmarkStatusActivity.swift */,
|
||||
);
|
||||
path = "Status Activities";
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
D627FF77217E94F200CC0648 /* Drafts */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
|
@ -1041,8 +1061,9 @@
|
|||
isa = PBXGroup;
|
||||
children = (
|
||||
D6AEBB3D2321638100E5038B /* UIActivity+Types.swift */,
|
||||
D6AEBB4623216B0C00E5038B /* Account Activities */,
|
||||
D6AEBB422321685E00E5038B /* OpenInSafariActivity.swift */,
|
||||
D6AEBB4623216B0C00E5038B /* Account Activities */,
|
||||
D627943323A5523800D38C68 /* Status Activities */,
|
||||
);
|
||||
path = Activities;
|
||||
sourceTree = "<group>";
|
||||
|
@ -1074,6 +1095,8 @@
|
|||
D6C693F82162E4DB007D6A6D /* StatusContentLabel.swift */,
|
||||
D641C77E213DC78A004B4513 /* InlineTextAttachment.swift */,
|
||||
04ED00B021481ED800567C53 /* SteppedProgressView.swift */,
|
||||
D627943A23A55BA600D38C68 /* NavigableTableViewCell.swift */,
|
||||
D627943123A5466600D38C68 /* SelectableTableViewCell.swift */,
|
||||
D67C57A721E2649B00C3118B /* Account Detail */,
|
||||
D67C57B021E28F9400C3118B /* Compose Status Reply */,
|
||||
D60C07E221E817560057FAA8 /* Compose Media */,
|
||||
|
@ -1577,6 +1600,7 @@
|
|||
D6F953F021251A2900CF0F2B /* MastodonController.swift in Sources */,
|
||||
0411610022B442870030A9B7 /* AttachmentViewController.swift in Sources */,
|
||||
D611C2CF232DC61100C86A49 /* HashtagTableViewCell.swift in Sources */,
|
||||
D627943723A552C200D38C68 /* BookmarkStatusActivity.swift in Sources */,
|
||||
D62D2426217ABF63005076CC /* UserActivityType.swift in Sources */,
|
||||
D66362712136338600C9CBA2 /* ComposeViewController.swift in Sources */,
|
||||
D6BC9DD7232D7811002CA326 /* TimelinesPageViewController.swift in Sources */,
|
||||
|
@ -1603,6 +1627,7 @@
|
|||
D6AEBB4A23216F0400E5038B /* UnfollowAccountActivity.swift in Sources */,
|
||||
D663626421360D2300C9CBA2 /* AvatarStyle.swift in Sources */,
|
||||
D679C09F215850EF00DA27FE /* XCBActions.swift in Sources */,
|
||||
D627943223A5466600D38C68 /* SelectableTableViewCell.swift in Sources */,
|
||||
D6DD353F22F502EC00A9563A /* Preferences+Notification.swift in Sources */,
|
||||
D63661C02381C144004B9E16 /* PreferencesNavigationController.swift in Sources */,
|
||||
D6AEBB4523216AF800E5038B /* FollowAccountActivity.swift in Sources */,
|
||||
|
@ -1610,14 +1635,17 @@
|
|||
D6E0DC8E216EDF1E00369478 /* Previewing.swift in Sources */,
|
||||
D6BED174212667E900F02DA0 /* TimelineStatusTableViewCell.swift in Sources */,
|
||||
0427033822B30F5F000D31B6 /* BehaviorPrefsView.swift in Sources */,
|
||||
D627943923A553B600D38C68 /* UnbookmarkStatusActivity.swift in Sources */,
|
||||
D64D0AAD2128D88B005A6F37 /* LocalData.swift in Sources */,
|
||||
D6C94D892139E6EC00CB5196 /* AttachmentView.swift in Sources */,
|
||||
D6C693EF216192C2007D6A6D /* TuskerNavigationDelegate.swift in Sources */,
|
||||
D6C94D872139E62700CB5196 /* LargeImageViewController.swift in Sources */,
|
||||
D6434EB3215B1856001A919A /* XCBRequest.swift in Sources */,
|
||||
D663626221360B1900C9CBA2 /* Preferences.swift in Sources */,
|
||||
D627943B23A55BA600D38C68 /* NavigableTableViewCell.swift in Sources */,
|
||||
D6333B792139AEFD00CE884A /* Date+TimeAgo.swift in Sources */,
|
||||
D641C77F213DC78A004B4513 /* InlineTextAttachment.swift in Sources */,
|
||||
D627943523A5525100D38C68 /* StatusActivity.swift in Sources */,
|
||||
04496BD721625361001F1B23 /* ContentLabel.swift in Sources */,
|
||||
D663626C21361C6700C9CBA2 /* Account+Preferences.swift in Sources */,
|
||||
D6333B372137838300CE884A /* AttributedString+Helpers.swift in Sources */,
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -18,5 +18,7 @@ extension UIActivity.ActivityType {
|
|||
static let unfollowAccount = UIActivity.ActivityType("\(Bundle.main.bundleIdentifier!).unfollow_account")
|
||||
|
||||
// Status
|
||||
static let bookmarkStatus = UIActivity.ActivityType("\(Bundle.main.bundleIdentifier!).bookmark_status")
|
||||
static let unbookmarkStatus = UIActivity.ActivityType("\(Bundle.main.bundleIdentifier!).unbookmark_status")
|
||||
|
||||
}
|
||||
|
|
|
@ -16,9 +16,11 @@ class DraftsManager: Codable {
|
|||
private static var archiveURL = DraftsManager.documentsDirectory.appendingPathComponent("drafts").appendingPathExtension("plist")
|
||||
|
||||
static func save() {
|
||||
let encoder = PropertyListEncoder()
|
||||
let data = try? encoder.encode(shared)
|
||||
try? data?.write(to: archiveURL, options: .noFileProtection)
|
||||
DispatchQueue.global(qos: .userInitiated).async {
|
||||
let encoder = PropertyListEncoder()
|
||||
let data = try? encoder.encode(shared)
|
||||
try? data?.write(to: archiveURL, options: .noFileProtection)
|
||||
}
|
||||
}
|
||||
|
||||
static func load() -> DraftsManager {
|
||||
|
@ -37,8 +39,10 @@ class DraftsManager: Codable {
|
|||
return drafts.sorted(by: { $0.lastModified > $1.lastModified })
|
||||
}
|
||||
|
||||
func create(text: String, contentWarning: String?, attachments: [DraftAttachment]) {
|
||||
drafts.append(Draft(text: text, contentWarning: contentWarning, lastModified: Date(), attachments: attachments))
|
||||
func create(text: String, contentWarning: String?, inReplyToID: String?, attachments: [DraftAttachment]) -> Draft {
|
||||
let draft = Draft(text: text, contentWarning: contentWarning, inReplyToID: inReplyToID, attachments: attachments)
|
||||
drafts.append(draft)
|
||||
return draft
|
||||
}
|
||||
|
||||
func remove(_ draft: Draft) {
|
||||
|
@ -53,15 +57,17 @@ extension DraftsManager {
|
|||
let id: UUID
|
||||
private(set) var text: String
|
||||
private(set) var contentWarning: String?
|
||||
private(set) var lastModified: Date
|
||||
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.text = text
|
||||
self.contentWarning = contentWarning
|
||||
self.lastModified = lastModified
|
||||
self.inReplyToID = inReplyToID
|
||||
self.attachments = attachments
|
||||
self.lastModified = lastModified
|
||||
}
|
||||
|
||||
func update(text: String, contentWarning: String?, attachments: [DraftAttachment]) {
|
||||
|
|
|
@ -12,10 +12,10 @@ import Pachyderm
|
|||
|
||||
class MastodonCache {
|
||||
|
||||
private static var statuses = [String: Status]()
|
||||
private static var accounts = [String: Account]()
|
||||
private static var relationships = [String: Relationship]()
|
||||
private static var notifications = [String: Pachyderm.Notification]()
|
||||
private static var statuses = CachedDictionary<Status>(name: "Statuses")
|
||||
private static var accounts = CachedDictionary<Account>(name: "Accounts")
|
||||
private static var relationships = CachedDictionary<Relationship>(name: "Relationships")
|
||||
private static var notifications = CachedDictionary<Pachyderm.Notification>(name: "Notifications")
|
||||
|
||||
static let statusSubject = PassthroughSubject<Status, 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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -57,6 +57,8 @@ class ComposeViewController: UIViewController {
|
|||
@IBOutlet weak var statusTextView: UITextView!
|
||||
@IBOutlet weak var placeholderLabel: UILabel!
|
||||
|
||||
@IBOutlet weak var inReplyToContainer: UIView!
|
||||
@IBOutlet weak var inReplyToLabel: UILabel!
|
||||
@IBOutlet weak var contentWarningContainerView: UIView!
|
||||
@IBOutlet weak var contentWarningTextField: UITextField!
|
||||
|
||||
|
@ -127,49 +129,7 @@ class ComposeViewController: UIViewController {
|
|||
}
|
||||
}
|
||||
|
||||
if let inReplyToID = inReplyToID, let inReplyTo = MastodonCache.status(for: inReplyToID) {
|
||||
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)
|
||||
}
|
||||
updateInReplyTo()
|
||||
|
||||
// we have to set the font here, because the monospaced digit font is not available in IB
|
||||
charactersRemainingLabel.font = .monospacedDigitSystemFont(ofSize: 17, weight: .regular)
|
||||
|
@ -177,13 +137,65 @@ class ComposeViewController: UIViewController {
|
|||
updatePlaceholder()
|
||||
|
||||
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
|
||||
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) {
|
||||
NotificationCenter.default.addObserver(self, selector: #selector(adjustForKeyboard), name: UIResponder.keyboardWillHideNotification, 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 {
|
||||
currentDraft.update(text: self.statusTextView.text, contentWarning: cw, attachments: attachments)
|
||||
} 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() {
|
||||
|
@ -597,9 +610,30 @@ extension ComposeViewController: DraftsTableViewControllerDelegate {
|
|||
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) {
|
||||
self.currentDraft = draft
|
||||
|
||||
inReplyToID = draft.inReplyToID
|
||||
updateInReplyTo()
|
||||
|
||||
statusTextView.text = draft.text
|
||||
contentWarningEnabled = draft.contentWarning != nil
|
||||
contentWarningTextField.text = draft.contentWarning
|
||||
|
|
|
@ -1,8 +1,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"/>
|
||||
<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="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
|
||||
</dependencies>
|
||||
|
@ -15,6 +15,8 @@
|
|||
<outlet property="contentView" destination="pcX-rB-RxJ" id="o95-Qa-6N7"/>
|
||||
<outlet property="contentWarningContainerView" destination="kU2-7l-MSy" id="Gnq-Jb-kCA"/>
|
||||
<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="postProgressView" destination="Tq7-6P-hMT" id="amT-F1-JI0"/>
|
||||
<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"/>
|
||||
<subviews>
|
||||
<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>
|
||||
<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>
|
||||
<view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="6V0-mH-Mhu">
|
||||
<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"/>
|
||||
</constraints>
|
||||
</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">
|
||||
<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>
|
||||
<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"/>
|
||||
<constraints>
|
||||
<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="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="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>
|
||||
</view>
|
||||
<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>
|
||||
<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"/>
|
||||
|
@ -112,7 +135,7 @@
|
|||
</constraints>
|
||||
</view>
|
||||
<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>
|
||||
<view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="752-dD-eAO">
|
||||
<rect key="frame" x="0.0" y="0.0" width="375" height="80"/>
|
||||
|
|
|
@ -10,6 +10,7 @@ import UIKit
|
|||
|
||||
protocol DraftsTableViewControllerDelegate {
|
||||
func draftSelectionCanceled()
|
||||
func shouldSelectDraft(_ draft: DraftsManager.Draft, completion: @escaping (Bool) -> Void)
|
||||
func draftSelected(_ draft: DraftsManager.Draft)
|
||||
func draftSelectionCompleted()
|
||||
}
|
||||
|
@ -61,9 +62,23 @@ class DraftsTableViewController: UITableViewController {
|
|||
}
|
||||
|
||||
override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
|
||||
delegate?.draftSelected(draft(for: indexPath))
|
||||
dismiss(animated: true) {
|
||||
self.delegate?.draftSelectionCompleted()
|
||||
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()
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -113,10 +113,7 @@ class ConversationTableViewController: EnhancedTableViewController {
|
|||
}
|
||||
}
|
||||
|
||||
override func tableView(_ tableView: UITableView, willSelectRowAt indexPath: IndexPath) -> IndexPath? {
|
||||
let statusID = statuses[indexPath.row].id
|
||||
return statusID == mainStatusID ? nil : indexPath
|
||||
}
|
||||
// MARK: - Table view delegate
|
||||
|
||||
override func tableView(_ tableView: UITableView, canEditRowAt indexPath: IndexPath) -> Bool {
|
||||
return true
|
||||
|
|
|
@ -109,6 +109,7 @@ class NotificationsTableViewController: EnhancedTableViewController {
|
|||
}
|
||||
}
|
||||
|
||||
// MARK: - Table view delegate
|
||||
|
||||
override func tableView(_ tableView: UITableView, willDisplay cell: UITableViewCell, forRowAt indexPath: IndexPath) {
|
||||
if indexPath.row == groups.count - 1 {
|
||||
|
|
|
@ -51,12 +51,10 @@ class InstanceSelectorTableViewController: UITableViewController {
|
|||
case let .selected(instance):
|
||||
let cell = tableView.dequeueReusableCell(withIdentifier: instanceCell, for: indexPath) as! InstanceTableViewCell
|
||||
cell.updateUI(instance: instance)
|
||||
cell.delegate = self
|
||||
return cell
|
||||
case let .recommended(instance):
|
||||
let cell = tableView.dequeueReusableCell(withIdentifier: instanceCell, for: indexPath) as! InstanceTableViewCell
|
||||
cell.updateUI(instance: instance)
|
||||
cell.delegate = self
|
||||
return cell
|
||||
}
|
||||
})
|
||||
|
@ -135,6 +133,23 @@ class InstanceSelectorTableViewController: UITableViewController {
|
|||
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 {
|
||||
|
@ -179,16 +194,3 @@ extension InstanceSelectorTableViewController: UISearchResultsUpdating {
|
|||
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!)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -180,6 +180,8 @@ class ProfileTableViewController: EnhancedTableViewController {
|
|||
}
|
||||
}
|
||||
|
||||
// MARK: - Table view delegate
|
||||
|
||||
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 {
|
||||
guard let older = older else { return }
|
||||
|
@ -245,17 +247,10 @@ extension ProfileTableViewController: ProfileHeaderTableViewCellDelegate {
|
|||
MastodonCache.relationship(for: account.id) { [weak self] (relationship) in
|
||||
guard let self = self else { return }
|
||||
|
||||
let customActivities: [UIActivity]
|
||||
var customActivities: [UIActivity] = [OpenInSafariActivity()]
|
||||
if let relationship = relationship {
|
||||
let toggleFollowActivity = relationship.following ? UnfollowAccountActivity() : FollowAccountActivity()
|
||||
customActivities = [
|
||||
toggleFollowActivity,
|
||||
OpenInSafariActivity()
|
||||
]
|
||||
} else {
|
||||
customActivities = [
|
||||
OpenInSafariActivity()
|
||||
]
|
||||
customActivities.insert(toggleFollowActivity, at: 0)
|
||||
}
|
||||
|
||||
DispatchQueue.main.async {
|
||||
|
|
|
@ -156,8 +156,12 @@ extension SearchTableViewController {
|
|||
}
|
||||
|
||||
class DataSource: UITableViewDiffableDataSource<Section, Item> {
|
||||
override func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String? {
|
||||
return Section.allCases[section].displayName
|
||||
override func tableView(_ tableView: UITableView, titleForHeaderInSection sectionIndex: Int) -> String? {
|
||||
let currentSnapshot = snapshot()
|
||||
for section in Section.allCases where currentSnapshot.indexOfSection(section) == sectionIndex {
|
||||
return section.displayName
|
||||
}
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -35,6 +35,13 @@ class EnhancedTableViewController: UITableViewController {
|
|||
override func scrollViewWillBeginDragging(_ scrollView: UIScrollView) {
|
||||
prevScrollToTopOffset = nil
|
||||
}
|
||||
|
||||
|
||||
override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
|
||||
if let cell = tableView.cellForRow(at: indexPath) as? SelectableTableViewCell {
|
||||
cell.didSelectCell()
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
|
|
@ -14,27 +14,25 @@ protocol MenuPreviewProvider {
|
|||
|
||||
typealias PreviewProviders = (content: UIContextMenuContentPreviewProvider, actions: () -> [UIAction])
|
||||
|
||||
var navigationDelegate: TuskerNavigationDelegate? { get }
|
||||
|
||||
func getPreviewProviders(for location: CGPoint, sourceViewController: UIViewController) -> PreviewProviders?
|
||||
|
||||
}
|
||||
|
||||
extension MenuPreviewProvider {
|
||||
|
||||
fileprivate func present(_ vc: UIViewController) {
|
||||
UIApplication.shared.keyWindow!.rootViewController!.present(vc, animated: true)
|
||||
}
|
||||
|
||||
func actionsForProfile(accountID: String) -> [UIAction] {
|
||||
guard let account = MastodonCache.account(for: accountID) else { return [] }
|
||||
return [
|
||||
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
|
||||
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
|
||||
self.present(UIActivityViewController(activityItems: [account.url], applicationActivities: nil))
|
||||
self.navigationDelegate?.showMoreOptions(forAccount: accountID)
|
||||
})
|
||||
]
|
||||
}
|
||||
|
@ -42,10 +40,10 @@ extension MenuPreviewProvider {
|
|||
func actionsForURL(_ url: URL) -> [UIAction] {
|
||||
return [
|
||||
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
|
||||
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 [] }
|
||||
return [
|
||||
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
|
||||
self.present(SFSafariViewController(url: status.url!))
|
||||
self.navigationDelegate?.selected(url: status.url!)
|
||||
}),
|
||||
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)
|
||||
})
|
||||
]
|
||||
}
|
||||
|
|
|
@ -28,6 +28,8 @@ protocol TuskerNavigationDelegate {
|
|||
|
||||
func compose()
|
||||
|
||||
func compose(mentioning: String?)
|
||||
|
||||
func reply(to statusID: String)
|
||||
|
||||
func largeImage(_ image: UIImage, description: String?, sourceView: UIImageView) -> LargeImageViewController
|
||||
|
@ -44,6 +46,8 @@ protocol TuskerNavigationDelegate {
|
|||
|
||||
func showMoreOptions(forStatus statusID: String)
|
||||
|
||||
func showMoreOptions(forAccount accountID: String)
|
||||
|
||||
func showMoreOptions(forURL url: URL)
|
||||
|
||||
func showFollowedByList(accountIDs: [String])
|
||||
|
@ -112,8 +116,13 @@ extension TuskerNavigationDelegate where Self: UIViewController {
|
|||
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() {
|
||||
let compose = ComposeViewController()
|
||||
compose(mentioning: nil)
|
||||
}
|
||||
|
||||
func compose(mentioning: String? = nil) {
|
||||
let compose = ComposeViewController( mentioningAcct: mentioning)
|
||||
let vc = UINavigationController(rootViewController: compose)
|
||||
vc.presentationController?.delegate = compose
|
||||
present(vc, animated: true)
|
||||
|
@ -185,7 +194,20 @@ extension TuskerNavigationDelegate where Self: UIViewController {
|
|||
private func moreOptions(forStatus statusID: String) -> UIViewController {
|
||||
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)") }
|
||||
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) {
|
||||
|
@ -196,6 +218,10 @@ extension TuskerNavigationDelegate where Self: UIViewController {
|
|||
present(moreOptions(forURL: url), animated: true)
|
||||
}
|
||||
|
||||
func showMoreOptions(forAccount accountID: String) {
|
||||
present(moreOptions(forAccount: accountID), animated: true)
|
||||
}
|
||||
|
||||
func showFollowedByList(accountIDs: [String]) {
|
||||
let vc = AccountListTableViewController(accountIDs: accountIDs)
|
||||
vc.title = NSLocalizedString("Followed By", comment: "followed by accounts list title")
|
||||
|
|
|
@ -56,20 +56,18 @@ class AccountTableViewCell: UITableViewCell {
|
|||
updateUIForPrefrences()
|
||||
}
|
||||
|
||||
override func setSelected(_ selected: Bool, animated: Bool) {
|
||||
super.setSelected(selected, animated: animated)
|
||||
}
|
||||
|
||||
if selected {
|
||||
delegate?.selected(account: accountID)
|
||||
}
|
||||
extension AccountTableViewCell: SelectableTableViewCell {
|
||||
func didSelectCell() {
|
||||
delegate?.selected(account: accountID)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
extension AccountTableViewCell: MenuPreviewProvider {
|
||||
var navigationDelegate: TuskerNavigationDelegate? { return delegate }
|
||||
|
||||
func getPreviewProviders(for location: CGPoint, sourceViewController: UIViewController) -> PreviewProviders? {
|
||||
return (content: { ProfileTableViewController(accountID: self.accountID) }, actions: { self.actionsForProfile(accountID: self.accountID) })
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -23,12 +23,10 @@ class HashtagTableViewCell: UITableViewCell {
|
|||
hashtagLabel.text = "#\(hashtag.name)"
|
||||
}
|
||||
|
||||
override func setSelected(_ selected: Bool, animated: Bool) {
|
||||
super.setSelected(selected, animated: animated)
|
||||
|
||||
if selected {
|
||||
delegate?.selected(tag: hashtag)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
extension HashtagTableViewCell: SelectableTableViewCell {
|
||||
func didSelectCell() {
|
||||
delegate?.selected(tag: hashtag)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -9,15 +9,8 @@
|
|||
import UIKit
|
||||
import Pachyderm
|
||||
|
||||
protocol InstanceTableViewCellDelegate {
|
||||
func didSelectInstance(_ instance: Instance)
|
||||
func didSelectInstance(_ instance: InstanceSelector.Instance)
|
||||
}
|
||||
|
||||
class InstanceTableViewCell: UITableViewCell {
|
||||
|
||||
var delegate: InstanceTableViewCellDelegate?
|
||||
|
||||
@IBOutlet weak var thumbnailImageView: UIImageView!
|
||||
@IBOutlet weak var domainLabel: 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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -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 }
|
||||
}
|
|
@ -149,30 +149,30 @@ class ActionNotificationGroupTableViewCell: UITableViewCell {
|
|||
updateTimestampWorkItem?.cancel()
|
||||
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 {
|
||||
var navigationDelegate: TuskerNavigationDelegate? { return delegate }
|
||||
|
||||
func getPreviewProviders(for location: CGPoint, sourceViewController: UIViewController) -> PreviewProviders? {
|
||||
return (content: {
|
||||
|
@ -192,5 +192,4 @@ extension ActionNotificationGroupTableViewCell: MenuPreviewProvider {
|
|||
return []
|
||||
})
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -112,26 +112,25 @@ class FollowNotificationGroupTableViewCell: UITableViewCell {
|
|||
updateTimestampWorkItem?.cancel()
|
||||
updateTimestampWorkItem = nil
|
||||
}
|
||||
|
||||
override func setSelected(_ selected: Bool, animated: Bool) {
|
||||
super.setSelected(selected, animated: animated)
|
||||
|
||||
if selected {
|
||||
let people = group.notificationIDs.compactMap(MastodonCache.notification(for:)).map { $0.account.id }
|
||||
switch people.count {
|
||||
case 0:
|
||||
return
|
||||
case 1:
|
||||
delegate?.selected(account: people.first!)
|
||||
default:
|
||||
delegate?.showFollowedByList(accountIDs: people)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
extension FollowNotificationGroupTableViewCell: SelectableTableViewCell {
|
||||
func didSelectCell() {
|
||||
let people = group.notificationIDs.compactMap(MastodonCache.notification(for:)).map { $0.account.id }
|
||||
switch people.count {
|
||||
case 0:
|
||||
return
|
||||
case 1:
|
||||
delegate?.selected(account: people.first!)
|
||||
default:
|
||||
delegate?.showFollowedByList(accountIDs: people)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
extension FollowNotificationGroupTableViewCell: MenuPreviewProvider {
|
||||
var navigationDelegate: TuskerNavigationDelegate? { return delegate }
|
||||
|
||||
func getPreviewProviders(for location: CGPoint, sourceViewController: UIViewController) -> PreviewProviders? {
|
||||
return (content: {
|
||||
|
@ -145,5 +144,4 @@ extension FollowNotificationGroupTableViewCell: MenuPreviewProvider {
|
|||
return []
|
||||
})
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -151,6 +151,7 @@ class ProfileHeaderTableViewCell: UITableViewCell {
|
|||
}
|
||||
|
||||
extension ProfileHeaderTableViewCell: MenuPreviewProvider {
|
||||
var navigationDelegate: TuskerNavigationDelegate? { return delegate }
|
||||
func getPreviewProviders(for location: CGPoint, sourceViewController: UIViewController) -> PreviewProviders? {
|
||||
let noteLabelPoint = noteLabel.convert(location, from: self)
|
||||
if noteLabel.bounds.contains(noteLabelPoint),
|
||||
|
|
|
@ -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()
|
||||
}
|
|
@ -15,13 +15,12 @@ protocol StatusTableViewCellDelegate: TuskerNavigationDelegate {
|
|||
}
|
||||
|
||||
class BaseStatusTableViewCell: UITableViewCell {
|
||||
|
||||
var delegate: StatusTableViewCellDelegate? {
|
||||
didSet {
|
||||
contentLabel.navigationDelegate = delegate
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@IBOutlet weak var avatarImageView: UIImageView!
|
||||
@IBOutlet weak var displayNameLabel: UILabel!
|
||||
@IBOutlet weak var usernameLabel: UILabel!
|
||||
|
@ -311,6 +310,8 @@ extension BaseStatusTableViewCell: AttachmentViewDelegate {
|
|||
}
|
||||
|
||||
extension BaseStatusTableViewCell: MenuPreviewProvider {
|
||||
var navigationDelegate: TuskerNavigationDelegate? { return delegate }
|
||||
|
||||
func getPreviewProviders(for location: CGPoint, sourceViewController: UIViewController) -> PreviewProviders? {
|
||||
if avatarImageView.frame.contains(location) {
|
||||
return (content: { ProfileTableViewController(accountID: self.accountID)}, actions: { self.actionsForProfile(accountID: self.accountID) })
|
||||
|
|
|
@ -119,14 +119,6 @@ class TimelineStatusTableViewCell: BaseStatusTableViewCell {
|
|||
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() {
|
||||
guard let rebloggerID = rebloggerID else { return }
|
||||
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 {
|
||||
|
||||
func leadingSwipeActionsConfiguration() -> UISwipeActionsConfiguration? {
|
||||
|
|
Loading…
Reference in New Issue