Favoriting

This commit is contained in:
Justin Mazzocchi 2020-08-23 19:50:54 -07:00
parent 8ca6f43610
commit f340569295
No known key found for this signature in database
GPG key ID: E223E6937AAFB01C
6 changed files with 81 additions and 10 deletions

View file

@ -7,6 +7,8 @@
objects = { objects = {
/* Begin PBXBuildFile section */ /* Begin PBXBuildFile section */
D002A0FB24F3362100E8AEBB /* StatusEndpoint.swift in Sources */ = {isa = PBXBuildFile; fileRef = D002A0FA24F3362100E8AEBB /* StatusEndpoint.swift */; };
D002A0FC24F3362100E8AEBB /* StatusEndpoint.swift in Sources */ = {isa = PBXBuildFile; fileRef = D002A0FA24F3362100E8AEBB /* StatusEndpoint.swift */; };
D0091B6824DC10B30040E8D2 /* PostingReadingPreferencesView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0091B6724DC10B30040E8D2 /* PostingReadingPreferencesView.swift */; }; D0091B6824DC10B30040E8D2 /* PostingReadingPreferencesView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0091B6724DC10B30040E8D2 /* PostingReadingPreferencesView.swift */; };
D0091B6924DC10B30040E8D2 /* PostingReadingPreferencesView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0091B6724DC10B30040E8D2 /* PostingReadingPreferencesView.swift */; }; D0091B6924DC10B30040E8D2 /* PostingReadingPreferencesView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0091B6724DC10B30040E8D2 /* PostingReadingPreferencesView.swift */; };
D0091B6B24DC10CE0040E8D2 /* PostingReadingPreferencesViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0091B6A24DC10CE0040E8D2 /* PostingReadingPreferencesViewModel.swift */; }; D0091B6B24DC10CE0040E8D2 /* PostingReadingPreferencesViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0091B6A24DC10CE0040E8D2 /* PostingReadingPreferencesViewModel.swift */; };
@ -277,6 +279,7 @@
/* End PBXCopyFilesBuildPhase section */ /* End PBXCopyFilesBuildPhase section */
/* Begin PBXFileReference section */ /* Begin PBXFileReference section */
D002A0FA24F3362100E8AEBB /* StatusEndpoint.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = StatusEndpoint.swift; sourceTree = "<group>"; };
D0091B6724DC10B30040E8D2 /* PostingReadingPreferencesView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PostingReadingPreferencesView.swift; sourceTree = "<group>"; }; D0091B6724DC10B30040E8D2 /* PostingReadingPreferencesView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PostingReadingPreferencesView.swift; sourceTree = "<group>"; };
D0091B6A24DC10CE0040E8D2 /* PostingReadingPreferencesViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PostingReadingPreferencesViewModel.swift; sourceTree = "<group>"; }; D0091B6A24DC10CE0040E8D2 /* PostingReadingPreferencesViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PostingReadingPreferencesViewModel.swift; sourceTree = "<group>"; };
D0091B6D24DD68090040E8D2 /* PreferencesView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PreferencesView.swift; sourceTree = "<group>"; }; D0091B6D24DD68090040E8D2 /* PreferencesView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PreferencesView.swift; sourceTree = "<group>"; };
@ -510,6 +513,7 @@
D019E6E024DF72E700697C7D /* InstanceEndpoint.swift */, D019E6E024DF72E700697C7D /* InstanceEndpoint.swift */,
D019E6DD24DF72E700697C7D /* PreferencesEndpoint.swift */, D019E6DD24DF72E700697C7D /* PreferencesEndpoint.swift */,
D0EC8DED24E2704D00A08489 /* PushSubscriptionEndpoint.swift */, D0EC8DED24E2704D00A08489 /* PushSubscriptionEndpoint.swift */,
D002A0FA24F3362100E8AEBB /* StatusEndpoint.swift */,
D05494F624EA49F7008B00A5 /* TimelinesEndpoint.swift */, D05494F624EA49F7008B00A5 /* TimelinesEndpoint.swift */,
); );
path = Endpoints; path = Endpoints;
@ -1082,6 +1086,7 @@
D019E6ED24DF7BF300697C7D /* IdentityDatabase.swift in Sources */, D019E6ED24DF7BF300697C7D /* IdentityDatabase.swift in Sources */,
D081A40524D0F1A8001B016E /* String+Extensions.swift in Sources */, D081A40524D0F1A8001B016E /* String+Extensions.swift in Sources */,
D019E6E724DF72E700697C7D /* AccessTokenEndpoint.swift in Sources */, D019E6E724DF72E700697C7D /* AccessTokenEndpoint.swift in Sources */,
D002A0FB24F3362100E8AEBB /* StatusEndpoint.swift in Sources */,
D02D870524EFBB79004583CC /* String+UIKitExtensions.swift in Sources */, D02D870524EFBB79004583CC /* String+UIKitExtensions.swift in Sources */,
D0BEC93824C9632800E864C4 /* RootViewModel.swift in Sources */, D0BEC93824C9632800E864C4 /* RootViewModel.swift in Sources */,
D02D86EC24EF9CA3004583CC /* CodingUserInfoKey+Extensions.swift in Sources */, D02D86EC24EF9CA3004583CC /* CodingUserInfoKey+Extensions.swift in Sources */,
@ -1247,6 +1252,7 @@
D04FD73D24D4A83A007D572D /* InstanceEndpoint+Stubbing.swift in Sources */, D04FD73D24D4A83A007D572D /* InstanceEndpoint+Stubbing.swift in Sources */,
D0DC175C24D0154F00A75C65 /* MastodonAPI.swift in Sources */, D0DC175C24D0154F00A75C65 /* MastodonAPI.swift in Sources */,
D020F51524ECBA60005AB084 /* LazyView.swift in Sources */, D020F51524ECBA60005AB084 /* LazyView.swift in Sources */,
D002A0FC24F3362100E8AEBB /* StatusEndpoint.swift in Sources */,
D0ED1BD224CF779B00B4899C /* MastodonTarget.swift in Sources */, D0ED1BD224CF779B00B4899C /* MastodonTarget.swift in Sources */,
D0EC8DC624DF842700A08489 /* KeychainService.swift in Sources */, D0EC8DC624DF842700A08489 /* KeychainService.swift in Sources */,
D020F50C24EC9F1D005AB084 /* ContextService.swift in Sources */, D020F50C24EC9F1D005AB084 /* ContextService.swift in Sources */,

View file

@ -0,0 +1,32 @@
// Copyright © 2020 Metabolist. All rights reserved.
import Foundation
enum StatusEndpoint {
case favourite(id: String)
case unfavourite(id: String)
}
extension StatusEndpoint: MastodonEndpoint {
typealias ResultType = Status
var context: [String] {
defaultContext + ["statuses"]
}
var pathComponentsInContext: [String] {
switch self {
case let .favourite(id):
return [id, "favourite"]
case let .unfavourite(id):
return [id, "unfavourite"]
}
}
var method: HTTPMethod {
switch self {
case .favourite, .unfavourite:
return .post
}
}
}

View file

@ -14,3 +14,14 @@ struct StatusService {
self.contentDatabase = contentDatabase self.contentDatabase = contentDatabase
} }
} }
extension StatusService {
func toggleFavorited() -> AnyPublisher<Void, Error> {
networkClient.request(status.favourited
? StatusEndpoint.unfavourite(id: status.id)
: StatusEndpoint.favourite(id: status.id))
.map { ([$0], nil) }
.flatMap(contentDatabase.insert(statuses:collection:))
.eraseToAnyPublisher()
}
}

View file

@ -19,8 +19,10 @@ struct StatusViewModel {
var isReplyInContext = false var isReplyInContext = false
var hasReplyFollowing = false var hasReplyFollowing = false
var sensitiveContentToggled = false var sensitiveContentToggled = false
let events: AnyPublisher<AnyPublisher<Void, Error>, Never>
private let statusService: StatusService private let statusService: StatusService
private let eventsInput = PassthroughSubject<AnyPublisher<Void, Error>, Never>()
init(statusService: StatusService) { init(statusService: StatusService) {
self.statusService = statusService self.statusService = statusService
@ -38,6 +40,7 @@ struct StatusViewModel {
rebloggedByDisplayNameEmoji = statusService.status.account.emojis rebloggedByDisplayNameEmoji = statusService.status.account.emojis
pollOptionTitles = statusService.status.displayStatus.poll?.options.map { $0.title } ?? [] pollOptionTitles = statusService.status.displayStatus.poll?.options.map { $0.title } ?? []
pollEmoji = statusService.status.displayStatus.poll?.emojis ?? [] pollEmoji = statusService.status.displayStatus.poll?.emojis ?? []
events = eventsInput.eraseToAnyPublisher()
} }
} }
@ -74,9 +77,9 @@ extension StatusViewModel {
var favoritesCount: Int { statusService.status.displayStatus.favouritesCount } var favoritesCount: Int { statusService.status.displayStatus.favouritesCount }
var reblogged: Bool { statusService.status.displayStatus.reblogged ?? false } var reblogged: Bool { statusService.status.displayStatus.reblogged }
var favorited: Bool { statusService.status.displayStatus.favourited ?? false } var favorited: Bool { statusService.status.displayStatus.favourited }
var sensitive: Bool { statusService.status.displayStatus.sensitive } var sensitive: Bool { statusService.status.displayStatus.sensitive }
@ -98,6 +101,10 @@ extension StatusViewModel {
return true return true
} }
} }
func toggleFavorited() {
eventsInput.send(statusService.toggleFavorited())
}
} }
private extension StatusViewModel { private extension StatusViewModel {

View file

@ -9,13 +9,17 @@ class StatusesViewModel: ObservableObject {
@Published private(set) var loading = false @Published private(set) var loading = false
private(set) var maintainScrollPositionOfStatusID: String? private(set) var maintainScrollPositionOfStatusID: String?
private let statusListService: StatusListService private let statusListService: StatusListService
private var statusViewModelCache = [Status: (StatusViewModel, AnyCancellable)]()
private var cancellables = Set<AnyCancellable>() private var cancellables = Set<AnyCancellable>()
init(statusListService: StatusListService) { init(statusListService: StatusListService) {
self.statusListService = statusListService self.statusListService = statusListService
statusListService.statusSections statusListService.statusSections
.handleEvents(receiveOutput: determineIfScrollPositionShouldBeMaintained(newStatusSections:)) .handleEvents(receiveOutput: { [weak self] in
self?.determineIfScrollPositionShouldBeMaintained(newStatusSections: $0)
self?.cleanViewModelCache(newStatusSections: $0)
})
.assignErrorsToAlertItem(to: \.alertItem, on: self) .assignErrorsToAlertItem(to: \.alertItem, on: self)
.assign(to: &$statusSections) .assign(to: &$statusSections)
} }
@ -35,16 +39,23 @@ extension StatusesViewModel {
} }
func statusViewModel(status: Status) -> StatusViewModel { func statusViewModel(status: Status) -> StatusViewModel {
var statusViewModel = Self.viewModelCache[status] var statusViewModel: StatusViewModel
?? StatusViewModel(statusService: statusListService.statusService(status: status))
if let cachedViewModel = statusViewModelCache[status]?.0 {
statusViewModel = cachedViewModel
} else {
statusViewModel = StatusViewModel(statusService: statusListService.statusService(status: status))
statusViewModelCache[status] = (statusViewModel, statusViewModel.events
.flatMap { $0 }
.assignErrorsToAlertItem(to: \.alertItem, on: self)
.sink {})
}
statusViewModel.isContextParent = status == contextParent statusViewModel.isContextParent = status == contextParent
statusViewModel.isPinned = statusListService.isPinned(status: status) statusViewModel.isPinned = statusListService.isPinned(status: status)
statusViewModel.isReplyInContext = statusListService.isReplyInContext(status: status) statusViewModel.isReplyInContext = statusListService.isReplyInContext(status: status)
statusViewModel.hasReplyFollowing = statusListService.hasReplyFollowing(status: status) statusViewModel.hasReplyFollowing = statusListService.hasReplyFollowing(status: status)
Self.viewModelCache[status] = statusViewModel
return statusViewModel return statusViewModel
} }
@ -54,8 +65,6 @@ extension StatusesViewModel {
} }
private extension StatusesViewModel { private extension StatusesViewModel {
static var viewModelCache = [Status: StatusViewModel]()
func determineIfScrollPositionShouldBeMaintained(newStatusSections: [[Status]]) { func determineIfScrollPositionShouldBeMaintained(newStatusSections: [[Status]]) {
maintainScrollPositionOfStatusID = nil // clear old value maintainScrollPositionOfStatusID = nil // clear old value
@ -64,4 +73,10 @@ private extension StatusesViewModel {
maintainScrollPositionOfStatusID = contextParent.id maintainScrollPositionOfStatusID = contextParent.id
} }
} }
func cleanViewModelCache(newStatusSections: [[Status]]) {
let newStatuses = Set(newStatusSections.reduce([], +))
statusViewModelCache = statusViewModelCache.filter { newStatuses.contains($0.key) }
}
} }

View file

@ -274,7 +274,7 @@ extension StatusTableViewCell {
} }
@IBAction func favoriteButtonTapped(_ sender: UIButton) { @IBAction func favoriteButtonTapped(_ sender: UIButton) {
viewModel.toggleFavorited()
} }
@IBAction func actionsButtonTapped(_ sender: Any) { @IBAction func actionsButtonTapped(_ sender: Any) {