Favoriting
This commit is contained in:
parent
8ca6f43610
commit
f340569295
|
@ -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 */,
|
||||||
|
|
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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) }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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) {
|
||||||
|
|
Loading…
Reference in New Issue