diff --git a/Pachyderm/Model/Status.swift b/Pachyderm/Model/Status.swift index 28b5bc3af7..ca65c03fc4 100644 --- a/Pachyderm/Model/Status.swift +++ b/Pachyderm/Model/Status.swift @@ -111,8 +111,10 @@ public class Status: Decodable, ClientModel { client.run(request) { response in if case .success = response { self.favourited = true + self.reblog?.favourited = true } else { self.favourited = oldValue + self.reblog?.favourited = oldValue } completion(response) } @@ -124,8 +126,10 @@ public class Status: Decodable, ClientModel { client.run(request) { response in if case .success = response { self.favourited = false + self.reblog?.favourited = false } else { self.favourited = oldValue + self.reblog?.favourited = oldValue } completion(response) } diff --git a/Pachyderm/Model/Timeline.swift b/Pachyderm/Model/Timeline.swift index e1eb77940e..ba4eb0472e 100644 --- a/Pachyderm/Model/Timeline.swift +++ b/Pachyderm/Model/Timeline.swift @@ -17,22 +17,25 @@ public enum Timeline { } extension Timeline { - func request(range: RequestRange) -> Request<[Status]> { - var request: Request<[Status]> + var endpoint: String { switch self { case .home: - request = Request(method: .get, path: "/api/v1/timelines/home") - case let .public(local): - request = Request(method: .get, path: "/api/v1/timelines/public") - if local { - request.queryParameters.append("local" => true) - } + return "/api/v1/timelines/home" + case .public: + return "/api/v1/timelines/public" case let .tag(hashtag): - request = Request(method: .get, path: "/api/v1/timeliens/tag/\(hashtag)") + return "/api/v1/timelines/tag/\(hashtag)" case let .list(id): - request = Request(method: .get, path: "/api/v1/timelines/list/\(id)") + return "/api/v1/timelines/list/\(id)" case .direct: - request = Request(method: .get, path: "/api/v1/timelines/direct") + return "/api/v1/timelines/direct" + } + } + + func request(range: RequestRange) -> Request<[Status]> { + var request = Request<[Status]>(method: .get, path: endpoint) + if case .public(true) = self { + request.queryParameters.append("local" => true) } request.range = range return request diff --git a/Pachyderm/Request/Parameter.swift b/Pachyderm/Request/Parameter.swift index c6362765ec..8c03b9a534 100644 --- a/Pachyderm/Request/Parameter.swift +++ b/Pachyderm/Request/Parameter.swift @@ -74,7 +74,8 @@ extension Array where Element == Parameter { var queryItems: [URLQueryItem] { return compactMap { - URLQueryItem(name: $0.name, value: $0.value) + guard let value = $0.value else { return nil } + return URLQueryItem(name: $0.name, value: value) } } } diff --git a/Pachyderm/Request/Request.swift b/Pachyderm/Request/Request.swift index c9b3a1b01b..522369d3d7 100644 --- a/Pachyderm/Request/Request.swift +++ b/Pachyderm/Request/Request.swift @@ -40,17 +40,26 @@ extension Request { } set { let rangeParams = newValue.queryParameters - let max = rangeParams.first { $0.name == "max_id" } - let since = rangeParams.first { $0.name == "since_id" } - let count = rangeParams.first { $0.name == "count" } - if let max = max, let i = queryParameters.firstIndex(where: { $0.name == "max_id" }) { - queryParameters[i] = max + if let max = rangeParams.first(where: { $0.name == "max_id" }) { + if let i = queryParameters.firstIndex(where: { $0.name == "max_id" }) { + queryParameters[i] = max + } else { + queryParameters.append(max) + } } - if let since = since, let i = queryParameters.firstIndex(where: { $0.name == "since_id" }) { - queryParameters[i] = since + if let since = rangeParams.first(where: { $0.name == "since_id" }) { + if let i = queryParameters.firstIndex(where: { $0.name == "since_id" }) { + queryParameters[i] = since + } else { + queryParameters.append(since) + } } - if let count = count, let i = queryParameters.firstIndex(where: { $0.name == "count" }) { - queryParameters[i] = count + if let count = rangeParams.first(where: { $0.name == "count" }) { + if let i = queryParameters.firstIndex(where: { $0.name == "count" }) { + queryParameters[i] = count + } else { + queryParameters.append(count) + } } } } diff --git a/Pachyderm/Response/Pagination.swift b/Pachyderm/Response/Pagination.swift index c0ec4cec9c..07d86a5461 100644 --- a/Pachyderm/Response/Pagination.swift +++ b/Pachyderm/Response/Pagination.swift @@ -30,9 +30,9 @@ extension Pagination { var range: RequestRange { switch kind { case .next: - return .after(id: id, count: limit) - case .prev: return .before(id: id, count: limit) + case .prev: + return .after(id: id, count: limit) } } diff --git a/Tusker.xcodeproj/project.pbxproj b/Tusker.xcodeproj/project.pbxproj index 876dfa933f..b04889390c 100644 --- a/Tusker.xcodeproj/project.pbxproj +++ b/Tusker.xcodeproj/project.pbxproj @@ -67,6 +67,7 @@ D64D0AAD2128D88B005A6F37 /* LocalData.swift in Sources */ = {isa = PBXBuildFile; fileRef = D64D0AAC2128D88B005A6F37 /* LocalData.swift */; }; D64D0AAF2128D954005A6F37 /* Onboarding.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = D64D0AAE2128D954005A6F37 /* Onboarding.storyboard */; }; D64D0AB12128D9AE005A6F37 /* OnboardingViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D64D0AB02128D9AE005A6F37 /* OnboardingViewController.swift */; }; + D6538945214D6D7500E3CEFC /* TableViewSwipeActionProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6538944214D6D7500E3CEFC /* TableViewSwipeActionProvider.swift */; }; D65A37F321472F300087646E /* SwiftSoup.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D6BED16E212663DA00F02DA0 /* SwiftSoup.framework */; }; D663625D2135C74800C9CBA2 /* ConversationMainStatusTableViewCell.xib in Resources */ = {isa = PBXBuildFile; fileRef = D663625C2135C74800C9CBA2 /* ConversationMainStatusTableViewCell.xib */; }; D663625F2135C75500C9CBA2 /* ConversationMainStatusTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = D663625E2135C75500C9CBA2 /* ConversationMainStatusTableViewCell.swift */; }; @@ -221,6 +222,7 @@ D64D0AAC2128D88B005A6F37 /* LocalData.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocalData.swift; sourceTree = ""; }; D64D0AAE2128D954005A6F37 /* Onboarding.storyboard */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; path = Onboarding.storyboard; sourceTree = ""; }; D64D0AB02128D9AE005A6F37 /* OnboardingViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OnboardingViewController.swift; sourceTree = ""; }; + D6538944214D6D7500E3CEFC /* TableViewSwipeActionProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TableViewSwipeActionProvider.swift; sourceTree = ""; }; D663625C2135C74800C9CBA2 /* ConversationMainStatusTableViewCell.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = ConversationMainStatusTableViewCell.xib; sourceTree = ""; }; D663625E2135C75500C9CBA2 /* ConversationMainStatusTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConversationMainStatusTableViewCell.swift; sourceTree = ""; }; D663626121360B1900C9CBA2 /* Preferences.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Preferences.swift; sourceTree = ""; }; @@ -571,6 +573,7 @@ D6333B762138D94E00CE884A /* ComposeMediaView.swift */, D641C77E213DC78A004B4513 /* InlineTextAttachment.swift */, 04ED00B021481ED800567C53 /* SteppedProgressView.swift */, + D6538944214D6D7500E3CEFC /* TableViewSwipeActionProvider.swift */, D641C78A213DD926004B4513 /* Status */, D641C78B213DD92F004B4513 /* Profile Header */, D641C78C213DD937004B4513 /* Notifications */, @@ -926,6 +929,7 @@ D6333B772138D94E00CE884A /* ComposeMediaView.swift in Sources */, 04ED00B121481ED800567C53 /* SteppedProgressView.swift in Sources */, D663626421360D2300C9CBA2 /* AvatarStyle.swift in Sources */, + D6538945214D6D7500E3CEFC /* TableViewSwipeActionProvider.swift in Sources */, D6BED174212667E900F02DA0 /* StatusTableViewCell.swift in Sources */, D64D0AAD2128D88B005A6F37 /* LocalData.swift in Sources */, D6C94D892139E6EC00CB5196 /* AttachmentView.swift in Sources */, diff --git a/Tusker/Screens/Conversation/ConversationViewController.swift b/Tusker/Screens/Conversation/ConversationViewController.swift index 99c4f6cd07..53b3983596 100644 --- a/Tusker/Screens/Conversation/ConversationViewController.swift +++ b/Tusker/Screens/Conversation/ConversationViewController.swift @@ -117,6 +117,18 @@ class ConversationViewController: UIViewController, UITableViewDataSource, UITab let status = statuses[indexPath.row] return status == mainStatus ? nil : indexPath } + + func tableView(_ tableView: UITableView, canEditRowAt indexPath: IndexPath) -> Bool { + return tableView.cellForRow(at: indexPath) is TableViewSwipeActionProvider + } + + func tableView(_ tableView: UITableView, leadingSwipeActionsConfigurationForRowAt indexPath: IndexPath) -> UISwipeActionsConfiguration? { + return (tableView.cellForRow(at: indexPath) as? TableViewSwipeActionProvider)?.leadingSwipeActionsConfiguration() + } + + func tableView(_ tableView: UITableView, trailingSwipeActionsConfigurationForRowAt indexPath: IndexPath) -> UISwipeActionsConfiguration? { + return (tableView.cellForRow(at: indexPath) as? TableViewSwipeActionProvider)?.trailingSwipeActionsConfiguration() + } } diff --git a/Tusker/Screens/Notifications/NotificationsTableViewController.swift b/Tusker/Screens/Notifications/NotificationsTableViewController.swift index c1e785d1d5..ef6b3d4dda 100644 --- a/Tusker/Screens/Notifications/NotificationsTableViewController.swift +++ b/Tusker/Screens/Notifications/NotificationsTableViewController.swift @@ -118,6 +118,18 @@ class NotificationsTableViewController: UITableViewController { } } + override func tableView(_ tableView: UITableView, canEditRowAt indexPath: IndexPath) -> Bool { + return tableView.cellForRow(at: indexPath) is TableViewSwipeActionProvider + } + + override func tableView(_ tableView: UITableView, leadingSwipeActionsConfigurationForRowAt indexPath: IndexPath) -> UISwipeActionsConfiguration? { + return (tableView.cellForRow(at: indexPath) as? TableViewSwipeActionProvider)?.leadingSwipeActionsConfiguration() + } + + override func tableView(_ tableView: UITableView, trailingSwipeActionsConfigurationForRowAt indexPath: IndexPath) -> UISwipeActionsConfiguration? { + return (tableView.cellForRow(at: indexPath) as? TableViewSwipeActionProvider)?.trailingSwipeActionsConfiguration() + } + @IBAction func refreshNotifications(_ sender: Any) { guard let newer = newer else { return } diff --git a/Tusker/Screens/Profile/ProfileTableViewController.swift b/Tusker/Screens/Profile/ProfileTableViewController.swift index 4927442e0e..9f67d77586 100644 --- a/Tusker/Screens/Profile/ProfileTableViewController.swift +++ b/Tusker/Screens/Profile/ProfileTableViewController.swift @@ -132,6 +132,18 @@ class ProfileTableViewController: UITableViewController, PreferencesAdaptive { } } + override func tableView(_ tableView: UITableView, canEditRowAt indexPath: IndexPath) -> Bool { + return tableView.cellForRow(at: indexPath) is TableViewSwipeActionProvider + } + + override func tableView(_ tableView: UITableView, leadingSwipeActionsConfigurationForRowAt indexPath: IndexPath) -> UISwipeActionsConfiguration? { + return (tableView.cellForRow(at: indexPath) as? TableViewSwipeActionProvider)?.leadingSwipeActionsConfiguration() + } + + override func tableView(_ tableView: UITableView, trailingSwipeActionsConfigurationForRowAt indexPath: IndexPath) -> UISwipeActionsConfiguration? { + return (tableView.cellForRow(at: indexPath) as? TableViewSwipeActionProvider)?.trailingSwipeActionsConfiguration() + } + @IBAction func refreshStatuses(_ sender: Any) { guard let newer = newer else { return } diff --git a/Tusker/Screens/Timeline/TimelineTableViewController.swift b/Tusker/Screens/Timeline/TimelineTableViewController.swift index 2a6a7aa551..792e844eec 100644 --- a/Tusker/Screens/Timeline/TimelineTableViewController.swift +++ b/Tusker/Screens/Timeline/TimelineTableViewController.swift @@ -35,6 +35,19 @@ class TimelineTableViewController: UITableViewController { return navigationController } + lazy var favoriteActionImage: UIImage = UIGraphicsImageRenderer(size: CGSize(width: 30 * 137/131, height: 30)).image { _ in + UIImage(named: "Favorite")!.draw(in: CGRect(x: 0, y: 0, width: 30 * 137/131, height: 30)) + } + lazy var reblogActionImage: UIImage = UIGraphicsImageRenderer(size: CGSize(width: 30 * 927/558, height: 30)).image { _ in + UIImage(named: "Reblog")!.draw(in: CGRect(x: 0, y: 0, width: 30 * 927/558, height: 30)) + } + lazy var replyActionImage: UIImage = UIGraphicsImageRenderer(size: CGSize(width: 30 * 205/151, height: 30)).image { _ in + UIImage(named: "Reply")!.draw(in: CGRect(x: 0, y: 0, width: 30 * 205/151, height: 30)) + } + lazy var moreActionImage: UIImage = UIGraphicsImageRenderer(size: CGSize(width: 30 * 2/1, height: 30)).image { _ in + UIImage(named: "More")!.draw(in: CGRect(x: 0, y: 0, width: 30 * 2/1, height: 30)) + } + var timeline: Timeline! var statuses: [Status] = [] { @@ -120,6 +133,18 @@ class TimelineTableViewController: UITableViewController { } } + override func tableView(_ tableView: UITableView, canEditRowAt indexPath: IndexPath) -> Bool { + return tableView.cellForRow(at: indexPath) is TableViewSwipeActionProvider + } + + override func tableView(_ tableView: UITableView, leadingSwipeActionsConfigurationForRowAt indexPath: IndexPath) -> UISwipeActionsConfiguration? { + return (tableView.cellForRow(at: indexPath) as? TableViewSwipeActionProvider)?.leadingSwipeActionsConfiguration() + } + + override func tableView(_ tableView: UITableView, trailingSwipeActionsConfigurationForRowAt indexPath: IndexPath) -> UISwipeActionsConfiguration? { + return (tableView.cellForRow(at: indexPath) as? TableViewSwipeActionProvider)?.trailingSwipeActionsConfiguration() + } + @IBAction func refreshStatuses(_ sender: Any) { guard let newer = newer else { return } diff --git a/Tusker/Views/Status/StatusTableViewCell.swift b/Tusker/Views/Status/StatusTableViewCell.swift index 72fdd75896..54154fb584 100644 --- a/Tusker/Views/Status/StatusTableViewCell.swift +++ b/Tusker/Views/Status/StatusTableViewCell.swift @@ -215,8 +215,8 @@ class StatusTableViewCell: UITableViewCell, PreferencesAdaptive { let realStatus: Status = status.reblog ?? status (favorited ? realStatus.favourite : realStatus.unfavourite)() { response in - self.favorited = realStatus.favourited ?? false DispatchQueue.main.async { + self.favorited = realStatus.favourited ?? false if case .success = response { UIImpactFeedbackGenerator(style: .light).impactOccurred() } else { @@ -235,8 +235,8 @@ class StatusTableViewCell: UITableViewCell, PreferencesAdaptive { let realStatus: Status = status.reblog ?? status (reblogged ? realStatus.reblog : realStatus.unreblog)() { response in - self.reblogged = realStatus.reblogged ?? false DispatchQueue.main.async { + self.reblogged = realStatus.reblogged ?? false if case .success = response { UIImpactFeedbackGenerator(style: .light).impactOccurred() } else { @@ -254,6 +254,99 @@ class StatusTableViewCell: UITableViewCell, PreferencesAdaptive { } +extension StatusTableViewCell: TableViewSwipeActionProvider { + + static var favoriteActionImage: UIImage = UIGraphicsImageRenderer(size: CGSize(width: 30 * 137/131, height: 30)).image { _ in + UIImage(named: "Favorite")!.draw(in: CGRect(x: 0, y: 0, width: 30 * 137/131, height: 30)) + } + static var reblogActionImage: UIImage = UIGraphicsImageRenderer(size: CGSize(width: 30 * 927/558, height: 30)).image { _ in + UIImage(named: "Reblog")!.draw(in: CGRect(x: 0, y: 0, width: 30 * 927/558, height: 30)) + } + static var replyActionImage: UIImage = UIGraphicsImageRenderer(size: CGSize(width: 30 * 205/151, height: 30)).image { _ in + UIImage(named: "Reply")!.draw(in: CGRect(x: 0, y: 0, width: 30 * 205/151, height: 30)) + } + static var moreActionImage: UIImage = UIGraphicsImageRenderer(size: CGSize(width: 30 * 2/1, height: 30)).image { _ in + UIImage(named: "More")!.draw(in: CGRect(x: 0, y: 0, width: 30 * 2/1, height: 30)) + } + + func leadingSwipeActionsConfiguration() -> UISwipeActionsConfiguration? { + let favoriteTitle: String + let favoriteAction: (@escaping Client.Callback) -> Void + let favoriteColor: UIColor + if status.favourited ?? false { + favoriteTitle = "Unfavorite" + favoriteAction = status.unfavourite + favoriteColor = UIColor(displayP3Red: 235/255, green: 77/255, blue: 62/255, alpha: 1) + + } else { + favoriteTitle = "Favorite" + favoriteAction = status.favourite + favoriteColor = UIColor(displayP3Red: 1, green: 204/255, blue: 0, alpha: 1) + } + let favorite = UIContextualAction(style: .normal, title: favoriteTitle) { (action, view, completion) in + favoriteAction { response in + DispatchQueue.main.async { + if case .success = response { + completion(true) + self.updateUI(for: self.status) + } else { + completion(false) + } + } + } + } + favorite.image = StatusTableViewCell.favoriteActionImage + favorite.backgroundColor = favoriteColor + + let reblogTitle: String + let reblogAction: (@escaping Client.Callback) -> Void + let reblogColor: UIColor + if status.reblogged ?? false { + reblogTitle = "Unreblog" + reblogAction = status.unreblog + reblogColor = UIColor(displayP3Red: 235/255, green: 77/255, blue: 62/255, alpha: 1) + } else { + reblogTitle = "Reblog" + reblogAction = status.reblog + reblogColor = tintColor + } + let reblog = UIContextualAction(style: .normal, title: reblogTitle) { (action, view, completion) in + reblogAction { response in + DispatchQueue.main.async { + if case .success = response { + completion(true) + self.updateUI(for: self.status) + } else { + completion(false) + } + } + } + } + reblog.image = StatusTableViewCell.reblogActionImage + reblog.backgroundColor = reblogColor + + return UISwipeActionsConfiguration(actions: [favorite, reblog]) + } + + func trailingSwipeActionsConfiguration() -> UISwipeActionsConfiguration? { + let reply = UIContextualAction(style: .normal, title: "Reply") { (action, view, completion) in + completion(true) + self.delegate?.reply(to: self.status) + } + reply.image = StatusTableViewCell.replyActionImage + reply.backgroundColor = tintColor + let more = UIContextualAction(style: .normal, title: "More") { (action, view, completion) in + completion(true) + self.delegate?.showMoreOptions(status: self.status) + } + more.image = StatusTableViewCell.moreActionImage + more.backgroundColor = .gray + return UISwipeActionsConfiguration(actions: [reply, more]) + } + + +} + extension StatusTableViewCell: HTMLContentLabelDelegate { func selected(mention: Mention) { diff --git a/Tusker/Views/TableViewSwipeActionProvider.swift b/Tusker/Views/TableViewSwipeActionProvider.swift new file mode 100644 index 0000000000..c2554cd9b1 --- /dev/null +++ b/Tusker/Views/TableViewSwipeActionProvider.swift @@ -0,0 +1,17 @@ +// +// TableViewSwipeActionProvider.swift +// Tusker +// +// Created by Shadowfacts on 9/15/18. +// Copyright © 2018 Shadowfacts. All rights reserved. +// + +import UIKit + +protocol TableViewSwipeActionProvider { + + func leadingSwipeActionsConfiguration() -> UISwipeActionsConfiguration? + + func trailingSwipeActionsConfiguration() -> UISwipeActionsConfiguration? + +}