diff --git a/Pachyderm/Model/Account.swift b/Pachyderm/Model/Account.swift index 63202d75..66b2d9cb 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") } diff --git a/Pachyderm/Model/Hashtag.swift b/Pachyderm/Model/Hashtag.swift index ff990359..af868580 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 diff --git a/Tusker/Preferences/Preferences.swift b/Tusker/Preferences/Preferences.swift index 75fdaa97..df3ad2e9 100644 --- a/Tusker/Preferences/Preferences.swift +++ b/Tusker/Preferences/Preferences.swift @@ -44,8 +44,9 @@ 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.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) @@ -68,8 +69,9 @@ 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(mentionReblogger, forKey: .mentionReblogger) try container.encode(blurAllMedia, forKey: .blurAllMedia) try container.encode(openLinksInApps, forKey: .openLinksInApps) try container.encode(useInAppSafari, forKey: .useInAppSafari) @@ -91,8 +93,9 @@ 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 mentionReblogger = false @Published var blurAllMedia = false @Published var openLinksInApps = true @Published var useInAppSafari = true @@ -114,8 +117,9 @@ class Preferences: Codable, ObservableObject { case defaultPostVisibility case automaticallySaveDrafts - case contentWarningCopyMode 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 e36e5ff3..d0abd874 100644 --- a/Tusker/Screens/Compose/ComposeViewController.swift +++ b/Tusker/Screens/Compose/ComposeViewController.swift @@ -15,7 +15,7 @@ class ComposeViewController: UIViewController { let mastodonController: MastodonController var inReplyToID: String? - var accountsToMention: [String] + var accountsToMention = [String]() var initialText: String? var contentWarningEnabled = false { didSet { @@ -78,11 +78,12 @@ class ComposeViewController: UIViewController { self.inReplyToID = inReplyToID if let inReplyToID = inReplyToID, let inReplyTo = mastodonController.cache.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 bb07a322..ae221e0a 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,26 @@ 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) + } + Toggle(isOn: $preferences.mentionReblogger) { + Text("Mention Reblogger") + } + } + } + + var readingSection: some View { Section(header: Text("READING")) { Toggle(isOn: $preferences.blurAllMedia) { Text("Blur All Media") @@ -54,7 +63,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") diff --git a/Tusker/TuskerNavigationDelegate.swift b/Tusker/TuskerNavigationDelegate.swift index 41f39603..fe3671ff 100644 --- a/Tusker/TuskerNavigationDelegate.swift +++ b/Tusker/TuskerNavigationDelegate.swift @@ -34,6 +34,8 @@ protocol TuskerNavigationDelegate: class { 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 @@ -127,7 +129,7 @@ extension TuskerNavigationDelegate where Self: UIViewController { compose(mentioning: nil) } - func compose(mentioning: String? = nil) { + func compose(mentioning: String?) { let compose = ComposeViewController(mentioningAcct: mentioning, mastodonController: apiController) let vc = UINavigationController(rootViewController: compose) vc.presentationController?.delegate = compose @@ -135,7 +137,11 @@ extension TuskerNavigationDelegate where Self: UIViewController { } func reply(to statusID: String) { - let compose = ComposeViewController(inReplyTo: statusID, mastodonController: apiController) + reply(to: statusID, mentioningAcct: nil) + } + + func reply(to statusID: String, mentioningAcct: String?) { + let compose = ComposeViewController(inReplyTo: statusID, mentioningAcct: mentioningAcct, mastodonController: apiController) 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 6ffa7b64..863b37bb 100644 --- a/Tusker/Views/Status/TimelineStatusTableViewCell.swift +++ b/Tusker/Views/Status/TimelineStatusTableViewCell.swift @@ -120,6 +120,16 @@ class TimelineStatusTableViewCell: BaseStatusTableViewCell { } } + func reply() { + if Preferences.shared.mentionReblogger, + let rebloggerID = rebloggerID, + let rebloggerAccount = mastodonController.cache.account(for: rebloggerID) { + delegate?.reply(to: statusID, mentioningAcct: rebloggerAccount.acct) + } else { + delegate?.reply(to: statusID) + } + } + override func prepareForReuse() { super.prepareForReuse() updateTimestampWorkItem?.cancel() @@ -132,6 +142,10 @@ class TimelineStatusTableViewCell: BaseStatusTableViewCell { delegate?.selected(account: rebloggerID) } + override func replyPressed() { + reply() + } + override func getStatusCellPreviewProviders(for location: CGPoint, sourceViewController: UIViewController) -> BaseStatusTableViewCell.PreviewProviders? { guard let mastodonController = mastodonController else { return nil } return ( @@ -215,7 +229,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