From 7934bc15ac53d4401f6de2f02d4f97dcc469d667 Mon Sep 17 00:00:00 2001 From: Shadowfacts Date: Sun, 19 Jan 2020 23:23:00 -0500 Subject: [PATCH 1/5] Split Composing prefs into Composing and Replying --- Tusker/Preferences/Preferences.swift | 8 +++--- .../Preferences/BehaviorPrefsView.swift | 28 +++++++++++-------- 2 files changed, 21 insertions(+), 15 deletions(-) diff --git a/Tusker/Preferences/Preferences.swift b/Tusker/Preferences/Preferences.swift index 75fdaa9706..12fdd88f5a 100644 --- a/Tusker/Preferences/Preferences.swift +++ b/Tusker/Preferences/Preferences.swift @@ -44,8 +44,8 @@ class Preferences: Codable, ObservableObject { self.defaultPostVisibility = try container.decode(Status.Visibility.self, forKey: .defaultPostVisibility) self.automaticallySaveDrafts = try container.decode(Bool.self, forKey: .automaticallySaveDrafts) - self.contentWarningCopyMode = try container.decode(ContentWarningCopyMode.self, forKey: .contentWarningCopyMode) self.requireAttachmentDescriptions = try container.decode(Bool.self, forKey: .requireAttachmentDescriptions) + self.contentWarningCopyMode = try container.decode(ContentWarningCopyMode.self, forKey: .contentWarningCopyMode) self.blurAllMedia = try container.decode(Bool.self, forKey: .blurAllMedia) self.openLinksInApps = try container.decode(Bool.self, forKey: .openLinksInApps) self.useInAppSafari = try container.decode(Bool.self, forKey: .useInAppSafari) @@ -68,8 +68,8 @@ class Preferences: Codable, ObservableObject { try container.encode(defaultPostVisibility, forKey: .defaultPostVisibility) try container.encode(automaticallySaveDrafts, forKey: .automaticallySaveDrafts) - try container.encode(contentWarningCopyMode, forKey: .contentWarningCopyMode) try container.encode(requireAttachmentDescriptions, forKey: .requireAttachmentDescriptions) + try container.encode(contentWarningCopyMode, forKey: .contentWarningCopyMode) try container.encode(blurAllMedia, forKey: .blurAllMedia) try container.encode(openLinksInApps, forKey: .openLinksInApps) try container.encode(useInAppSafari, forKey: .useInAppSafari) @@ -91,8 +91,8 @@ class Preferences: Codable, ObservableObject { // MARK: - Behavior @Published var defaultPostVisibility = Status.Visibility.public @Published var automaticallySaveDrafts = true - @Published var contentWarningCopyMode = ContentWarningCopyMode.asIs @Published var requireAttachmentDescriptions = false + @Published var contentWarningCopyMode = ContentWarningCopyMode.asIs @Published var blurAllMedia = false @Published var openLinksInApps = true @Published var useInAppSafari = true @@ -114,8 +114,8 @@ class Preferences: Codable, ObservableObject { case defaultPostVisibility case automaticallySaveDrafts - case contentWarningCopyMode case requireAttachmentDescriptions + case contentWarningCopyMode case blurAllMedia case openLinksInApps case useInAppSafari diff --git a/Tusker/Screens/Preferences/BehaviorPrefsView.swift b/Tusker/Screens/Preferences/BehaviorPrefsView.swift index bb07a3226b..540f466e44 100644 --- a/Tusker/Screens/Preferences/BehaviorPrefsView.swift +++ b/Tusker/Screens/Preferences/BehaviorPrefsView.swift @@ -13,14 +13,15 @@ struct BehaviorPrefsView: View { var body: some View { List { - section1 - section2 - section3 + composingSection + replyingSection + readingSection + linksSection }.listStyle(GroupedListStyle()) .navigationBarTitle(Text("Behavior")) } - var section1: some View { + var composingSection: some View { Section(header: Text("COMPOSING")) { Picker(selection: $preferences.defaultPostVisibility, label: Text("Default Post Visibility")) { ForEach(Status.Visibility.allCases, id: \.self) { visibility in @@ -35,18 +36,23 @@ struct BehaviorPrefsView: View { Toggle(isOn: $preferences.automaticallySaveDrafts) { Text("Automatically Save Drafts") } - Picker(selection: $preferences.contentWarningCopyMode, label: Text("Content Warning Copy Style")) { - Text("As-is").tag(ContentWarningCopyMode.asIs) - Text("Prepend 're: '").tag(ContentWarningCopyMode.prependRe) - Text("Don't copy").tag(ContentWarningCopyMode.doNotCopy) - } Toggle(isOn: $preferences.requireAttachmentDescriptions) { Text("Require Attachment Descriptions") } } } - var section2: some View { + var replyingSection: some View { + Section(header: Text("REPLYING")) { + Picker(selection: $preferences.contentWarningCopyMode, label: Text("Content Warning Copy Style")) { + Text("As-is").tag(ContentWarningCopyMode.asIs) + Text("Prepend 're: '").tag(ContentWarningCopyMode.prependRe) + Text("Don't copy").tag(ContentWarningCopyMode.doNotCopy) + } + } + } + + var readingSection: some View { Section(header: Text("READING")) { Toggle(isOn: $preferences.blurAllMedia) { Text("Blur All Media") @@ -54,7 +60,7 @@ struct BehaviorPrefsView: View { } } - var section3: some View { + var linksSection: some View { Section(header: Text("LINKS")) { Toggle(isOn: $preferences.openLinksInApps) { Text("Open Links in Apps") From f7421d83efaa41f6d173160b5a3f4fd9fd106ae0 Mon Sep 17 00:00:00 2001 From: Shadowfacts Date: Sun, 19 Jan 2020 23:48:36 -0500 Subject: [PATCH 2/5] Add preference to mention reblogger when replying to a reblogged status --- Tusker/Preferences/Preferences.swift | 4 ++++ .../Screens/Compose/ComposeViewController.swift | 7 ++++--- .../Screens/Preferences/BehaviorPrefsView.swift | 3 +++ Tusker/TuskerNavigationDelegate.swift | 12 +++++++++--- .../Status/TimelineStatusTableViewCell.swift | 16 +++++++++++++++- 5 files changed, 35 insertions(+), 7 deletions(-) diff --git a/Tusker/Preferences/Preferences.swift b/Tusker/Preferences/Preferences.swift index 12fdd88f5a..df3ad2e96e 100644 --- a/Tusker/Preferences/Preferences.swift +++ b/Tusker/Preferences/Preferences.swift @@ -46,6 +46,7 @@ class Preferences: Codable, ObservableObject { self.automaticallySaveDrafts = try container.decode(Bool.self, forKey: .automaticallySaveDrafts) self.requireAttachmentDescriptions = try container.decode(Bool.self, forKey: .requireAttachmentDescriptions) self.contentWarningCopyMode = try container.decode(ContentWarningCopyMode.self, forKey: .contentWarningCopyMode) + self.mentionReblogger = try container.decode(Bool.self, forKey: .mentionReblogger) self.blurAllMedia = try container.decode(Bool.self, forKey: .blurAllMedia) self.openLinksInApps = try container.decode(Bool.self, forKey: .openLinksInApps) self.useInAppSafari = try container.decode(Bool.self, forKey: .useInAppSafari) @@ -70,6 +71,7 @@ class Preferences: Codable, ObservableObject { try container.encode(automaticallySaveDrafts, forKey: .automaticallySaveDrafts) try container.encode(requireAttachmentDescriptions, forKey: .requireAttachmentDescriptions) try container.encode(contentWarningCopyMode, forKey: .contentWarningCopyMode) + try container.encode(mentionReblogger, forKey: .mentionReblogger) try container.encode(blurAllMedia, forKey: .blurAllMedia) try container.encode(openLinksInApps, forKey: .openLinksInApps) try container.encode(useInAppSafari, forKey: .useInAppSafari) @@ -93,6 +95,7 @@ class Preferences: Codable, ObservableObject { @Published var automaticallySaveDrafts = true @Published var requireAttachmentDescriptions = false @Published var contentWarningCopyMode = ContentWarningCopyMode.asIs + @Published var mentionReblogger = false @Published var blurAllMedia = false @Published var openLinksInApps = true @Published var useInAppSafari = true @@ -116,6 +119,7 @@ class Preferences: Codable, ObservableObject { case automaticallySaveDrafts case requireAttachmentDescriptions case contentWarningCopyMode + case mentionReblogger case blurAllMedia case openLinksInApps case useInAppSafari diff --git a/Tusker/Screens/Compose/ComposeViewController.swift b/Tusker/Screens/Compose/ComposeViewController.swift index 165fa0bbd3..977be20fd4 100644 --- a/Tusker/Screens/Compose/ComposeViewController.swift +++ b/Tusker/Screens/Compose/ComposeViewController.swift @@ -13,7 +13,7 @@ import Intents class ComposeViewController: UIViewController { var inReplyToID: String? - var accountsToMention: [String] + var accountsToMention = [String]() var initialText: String? var contentWarningEnabled = false { didSet { @@ -74,11 +74,12 @@ class ComposeViewController: UIViewController { self.inReplyToID = inReplyToID if let inReplyToID = inReplyToID, let inReplyTo = MastodonCache.status(for: inReplyToID) { accountsToMention = [inReplyTo.account.acct] + inReplyTo.mentions.map { $0.acct } - } else if let mentioningAcct = mentioningAcct { - accountsToMention = [mentioningAcct] } else { accountsToMention = [] } + if let mentioningAcct = mentioningAcct { + accountsToMention.append(mentioningAcct) + } if let ownAccount = MastodonController.account { accountsToMention.removeAll(where: { acct in ownAccount.acct == acct }) } diff --git a/Tusker/Screens/Preferences/BehaviorPrefsView.swift b/Tusker/Screens/Preferences/BehaviorPrefsView.swift index 540f466e44..ae221e0a2a 100644 --- a/Tusker/Screens/Preferences/BehaviorPrefsView.swift +++ b/Tusker/Screens/Preferences/BehaviorPrefsView.swift @@ -49,6 +49,9 @@ struct BehaviorPrefsView: View { Text("Prepend 're: '").tag(ContentWarningCopyMode.prependRe) Text("Don't copy").tag(ContentWarningCopyMode.doNotCopy) } + Toggle(isOn: $preferences.mentionReblogger) { + Text("Mention Reblogger") + } } } diff --git a/Tusker/TuskerNavigationDelegate.swift b/Tusker/TuskerNavigationDelegate.swift index f22b126ddb..43cf731fd4 100644 --- a/Tusker/TuskerNavigationDelegate.swift +++ b/Tusker/TuskerNavigationDelegate.swift @@ -32,6 +32,8 @@ protocol TuskerNavigationDelegate { func reply(to statusID: String) + func reply(to statusID: String, mentioningAcct: String?) + func largeImage(_ image: UIImage, description: String?, sourceView: UIImageView) -> LargeImageViewController func largeImage(gifData: Data, description: String?, sourceView: UIImageView) -> LargeImageViewController @@ -125,15 +127,19 @@ extension TuskerNavigationDelegate where Self: UIViewController { compose(mentioning: nil) } - func compose(mentioning: String? = nil) { - let compose = ComposeViewController( mentioningAcct: mentioning) + func compose(mentioning: String?) { + let compose = ComposeViewController(mentioningAcct: mentioning) let vc = UINavigationController(rootViewController: compose) vc.presentationController?.delegate = compose present(vc, animated: true) } func reply(to statusID: String) { - let compose = ComposeViewController(inReplyTo: statusID) + reply(to: statusID, mentioningAcct: nil) + } + + func reply(to statusID: String, mentioningAcct: String?) { + let compose = ComposeViewController(inReplyTo: statusID, mentioningAcct: mentioningAcct) let vc = UINavigationController(rootViewController: compose) vc.presentationController?.delegate = compose present(vc, animated: true) diff --git a/Tusker/Views/Status/TimelineStatusTableViewCell.swift b/Tusker/Views/Status/TimelineStatusTableViewCell.swift index 0ce1aab8a1..5393529d43 100644 --- a/Tusker/Views/Status/TimelineStatusTableViewCell.swift +++ b/Tusker/Views/Status/TimelineStatusTableViewCell.swift @@ -112,6 +112,16 @@ class TimelineStatusTableViewCell: BaseStatusTableViewCell { } } + func reply() { + if Preferences.shared.mentionReblogger, + let rebloggerID = rebloggerID, + let rebloggerAccount = MastodonCache.account(for: rebloggerID) { + delegate?.reply(to: statusID, mentioningAcct: rebloggerAccount.acct) + } else { + delegate?.reply(to: statusID) + } + } + override func prepareForReuse() { super.prepareForReuse() updateTimestampWorkItem?.cancel() @@ -124,6 +134,10 @@ class TimelineStatusTableViewCell: BaseStatusTableViewCell { delegate?.selected(account: rebloggerID) } + override func replyPressed() { + reply() + } + override func getStatusCellPreviewProviders(for location: CGPoint, sourceViewController: UIViewController) -> BaseStatusTableViewCell.PreviewProviders? { return ( content: { ConversationTableViewController(for: self.statusID, state: self.statusState.copy()) }, @@ -205,7 +219,7 @@ extension TimelineStatusTableViewCell: TableViewSwipeActionProvider { func trailingSwipeActionsConfiguration() -> UISwipeActionsConfiguration? { let reply = UIContextualAction(style: .normal, title: "Reply") { (action, view, completion) in completion(true) - self.delegate?.reply(to: self.statusID) + self.reply() } reply.image = UIImage(systemName: "arrowshape.turn.up.left.fill") reply.backgroundColor = tintColor From 79f44c9b5899cc618d7e285cd9bd128264a5bfbf Mon Sep 17 00:00:00 2001 From: Shadowfacts Date: Sat, 18 Jan 2020 19:49:10 -0500 Subject: [PATCH 3/5] Change recommended instance selector to store categories as strings instead of enum Additional categories can be added, which would cause a crash when decoding. As the category isn't used for anything, storing it as an enum value is not necessary. --- Pachyderm/Utilities/InstanceSelector.swift | 19 +------------------ .../Instance Cell/InstanceTableViewCell.swift | 2 +- 2 files changed, 2 insertions(+), 19 deletions(-) diff --git a/Pachyderm/Utilities/InstanceSelector.swift b/Pachyderm/Utilities/InstanceSelector.swift index e0341f450f..dd2ce0645a 100644 --- a/Pachyderm/Utilities/InstanceSelector.swift +++ b/Pachyderm/Utilities/InstanceSelector.swift @@ -51,7 +51,7 @@ public extension InstanceSelector { public let description: String public let proxiedThumbnailURL: URL public let language: String - public let category: Category + public let category: String enum CodingKeys: String, CodingKey { case domain @@ -62,20 +62,3 @@ public extension InstanceSelector { } } } - -public extension InstanceSelector { - enum Category: String, Codable { - // source: https://source.joinmastodon.org/mastodon/joinmastodon/blob/master/src/Wizard.js#L108 - case general - case regional - case art - case journalism - case activism - case lgbt - case games - case tech - case adult - case furry - case food - } -} diff --git a/Tusker/Views/Instance Cell/InstanceTableViewCell.swift b/Tusker/Views/Instance Cell/InstanceTableViewCell.swift index 920e068f41..161015b3f7 100644 --- a/Tusker/Views/Instance Cell/InstanceTableViewCell.swift +++ b/Tusker/Views/Instance Cell/InstanceTableViewCell.swift @@ -36,7 +36,7 @@ class InstanceTableViewCell: UITableViewCell { self.instance = nil domainLabel.text = instance.domain - adultLabel.isHidden = instance.category != .adult + adultLabel.isHidden = instance.category != "adult" descriptionTextView.setTextFromHtml(instance.description) updateThumbnail(url: instance.proxiedThumbnailURL) } From f9ffb240efbb6d55d2bfc6ef812945f408a59025 Mon Sep 17 00:00:00 2001 From: Shadowfacts Date: Mon, 20 Jan 2020 12:03:10 -0500 Subject: [PATCH 4/5] Fix decoding Hashtag.History on Mastodon --- Pachyderm/Model/Hashtag.swift | 33 +++++++++++++++++++++++++++++++++ 1 file changed, 33 insertions(+) diff --git a/Pachyderm/Model/Hashtag.swift b/Pachyderm/Model/Hashtag.swift index ff990359e0..af86858062 100644 --- a/Pachyderm/Model/Hashtag.swift +++ b/Pachyderm/Model/Hashtag.swift @@ -32,6 +32,39 @@ extension Hashtag { public let uses: Int public let accounts: Int + public required init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + + if let day = try? container.decode(Date.self, forKey: .day) { + self.day = day + } else if let unixTimestamp = try? container.decode(Double.self, forKey: .day) { + self.day = Date(timeIntervalSince1970: unixTimestamp) + } else if let str = try? container.decode(String.self, forKey: .day), + let unixTimestamp = Double(str) { + self.day = Date(timeIntervalSince1970: unixTimestamp) + } else { + throw DecodingError.dataCorruptedError(forKey: .day, in: container, debugDescription: "day must be either date or UNIX timestamp") + } + + if let uses = try? container.decode(Int.self, forKey: .uses) { + self.uses = uses + } else if let str = try? container.decode(String.self, forKey: .uses), + let uses = Int(str) { + self.uses = uses + } else { + throw DecodingError.dataCorruptedError(forKey: .uses, in: container, debugDescription: "uses must either be int or string containing int") + } + + if let accounts = try? container.decode(Int.self, forKey: .accounts) { + self.accounts = accounts + } else if let str = try? container.decode(String.self, forKey: .accounts), + let accounts = Int(str) { + self.accounts = accounts + } else { + throw DecodingError.dataCorruptedError(forKey: .accounts, in: container, debugDescription: "accounts must either be int or string containing int") + } + } + private enum CodingKeys: String, CodingKey { case day case uses From cece8825adb58ee807e5e7640bdd3890a33ef452 Mon Sep 17 00:00:00 2001 From: Shadowfacts Date: Mon, 20 Jan 2020 12:10:10 -0500 Subject: [PATCH 5/5] Fix decoding Account.moved on Mastodon --- Pachyderm/Model/Account.swift | 35 +++++++++++++++++++++++++++++++++++ 1 file changed, 35 insertions(+) diff --git a/Pachyderm/Model/Account.swift b/Pachyderm/Model/Account.swift index 63202d7586..66b2d9cb8b 100644 --- a/Pachyderm/Model/Account.swift +++ b/Pachyderm/Model/Account.swift @@ -26,9 +26,44 @@ public class Account: Decodable { public let headerStatic: URL public private(set) var emojis: [Emoji] public let moved: Bool? + public let movedTo: Account? public let fields: [Field]? public let bot: Bool? + public required init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + + self.id = try container.decode(String.self, forKey: .id) + self.username = try container.decode(String.self, forKey: .username) + self.acct = try container.decode(String.self, forKey: .acct) + self.displayName = try container.decode(String.self, forKey: .displayName) + self.locked = try container.decode(Bool.self, forKey: .locked) + self.createdAt = try container.decode(Date.self, forKey: .createdAt) + self.followersCount = try container.decode(Int.self, forKey: .followersCount) + self.followingCount = try container.decode(Int.self, forKey: .followingCount) + self.statusesCount = try container.decode(Int.self, forKey: .statusesCount) + self.note = try container.decode(String.self, forKey: .note) + self.url = try container.decode(URL.self, forKey: .url) + self.avatar = try container.decode(URL.self, forKey: .avatar) + self.avatarStatic = try container.decode(URL.self, forKey: .avatarStatic) + self.header = try container.decode(URL.self, forKey: .header) + self.headerStatic = try container.decode(URL.self, forKey: .url) + self.emojis = try container.decode([Emoji].self, forKey: .emojis) + self.fields = try? container.decode([Field].self, forKey: .fields) + self.bot = try? container.decode(Bool.self, forKey: .bot) + + if let moved = try? container.decode(Bool.self, forKey: .moved) { + self.moved = moved + self.movedTo = nil + } else if let account = try? container.decode(Account.self, forKey: .moved) { + self.moved = true + self.movedTo = account + } else { + self.moved = false + self.movedTo = nil + } + } + public static func authorizeFollowRequest(_ account: Account) -> Request { return Request(method: .post, path: "/api/v1/follow_requests/\(account.id)/authorize") }