From f340569295a582f713d55688734ceb5d59d74cf5 Mon Sep 17 00:00:00 2001 From: Justin Mazzocchi <2831158+jzzocc@users.noreply.github.com> Date: Sun, 23 Aug 2020 19:50:54 -0700 Subject: [PATCH] Favoriting --- Metatext.xcodeproj/project.pbxproj | 6 ++++ .../Endpoints/StatusEndpoint.swift | 32 +++++++++++++++++++ Shared/Services/StatusService.swift | 11 +++++++ Shared/View Models/StatusViewModel.swift | 11 +++++-- Shared/View Models/StatusesViewModel.swift | 29 +++++++++++++---- iOS/Views/StatusTableViewCell.swift | 2 +- 6 files changed, 81 insertions(+), 10 deletions(-) create mode 100644 Shared/Networking/Mastodon API/Endpoints/StatusEndpoint.swift diff --git a/Metatext.xcodeproj/project.pbxproj b/Metatext.xcodeproj/project.pbxproj index c12673c..61a935b 100644 --- a/Metatext.xcodeproj/project.pbxproj +++ b/Metatext.xcodeproj/project.pbxproj @@ -7,6 +7,8 @@ objects = { /* 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 */; }; D0091B6924DC10B30040E8D2 /* PostingReadingPreferencesView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0091B6724DC10B30040E8D2 /* PostingReadingPreferencesView.swift */; }; D0091B6B24DC10CE0040E8D2 /* PostingReadingPreferencesViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0091B6A24DC10CE0040E8D2 /* PostingReadingPreferencesViewModel.swift */; }; @@ -277,6 +279,7 @@ /* End PBXCopyFilesBuildPhase section */ /* Begin PBXFileReference section */ + D002A0FA24F3362100E8AEBB /* StatusEndpoint.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = StatusEndpoint.swift; sourceTree = ""; }; D0091B6724DC10B30040E8D2 /* PostingReadingPreferencesView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PostingReadingPreferencesView.swift; sourceTree = ""; }; D0091B6A24DC10CE0040E8D2 /* PostingReadingPreferencesViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PostingReadingPreferencesViewModel.swift; sourceTree = ""; }; D0091B6D24DD68090040E8D2 /* PreferencesView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PreferencesView.swift; sourceTree = ""; }; @@ -510,6 +513,7 @@ D019E6E024DF72E700697C7D /* InstanceEndpoint.swift */, D019E6DD24DF72E700697C7D /* PreferencesEndpoint.swift */, D0EC8DED24E2704D00A08489 /* PushSubscriptionEndpoint.swift */, + D002A0FA24F3362100E8AEBB /* StatusEndpoint.swift */, D05494F624EA49F7008B00A5 /* TimelinesEndpoint.swift */, ); path = Endpoints; @@ -1082,6 +1086,7 @@ D019E6ED24DF7BF300697C7D /* IdentityDatabase.swift in Sources */, D081A40524D0F1A8001B016E /* String+Extensions.swift in Sources */, D019E6E724DF72E700697C7D /* AccessTokenEndpoint.swift in Sources */, + D002A0FB24F3362100E8AEBB /* StatusEndpoint.swift in Sources */, D02D870524EFBB79004583CC /* String+UIKitExtensions.swift in Sources */, D0BEC93824C9632800E864C4 /* RootViewModel.swift in Sources */, D02D86EC24EF9CA3004583CC /* CodingUserInfoKey+Extensions.swift in Sources */, @@ -1247,6 +1252,7 @@ D04FD73D24D4A83A007D572D /* InstanceEndpoint+Stubbing.swift in Sources */, D0DC175C24D0154F00A75C65 /* MastodonAPI.swift in Sources */, D020F51524ECBA60005AB084 /* LazyView.swift in Sources */, + D002A0FC24F3362100E8AEBB /* StatusEndpoint.swift in Sources */, D0ED1BD224CF779B00B4899C /* MastodonTarget.swift in Sources */, D0EC8DC624DF842700A08489 /* KeychainService.swift in Sources */, D020F50C24EC9F1D005AB084 /* ContextService.swift in Sources */, diff --git a/Shared/Networking/Mastodon API/Endpoints/StatusEndpoint.swift b/Shared/Networking/Mastodon API/Endpoints/StatusEndpoint.swift new file mode 100644 index 0000000..1c24887 --- /dev/null +++ b/Shared/Networking/Mastodon API/Endpoints/StatusEndpoint.swift @@ -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 + } + } +} diff --git a/Shared/Services/StatusService.swift b/Shared/Services/StatusService.swift index d7f37f1..e9a256a 100644 --- a/Shared/Services/StatusService.swift +++ b/Shared/Services/StatusService.swift @@ -14,3 +14,14 @@ struct StatusService { self.contentDatabase = contentDatabase } } + +extension StatusService { + func toggleFavorited() -> AnyPublisher { + networkClient.request(status.favourited + ? StatusEndpoint.unfavourite(id: status.id) + : StatusEndpoint.favourite(id: status.id)) + .map { ([$0], nil) } + .flatMap(contentDatabase.insert(statuses:collection:)) + .eraseToAnyPublisher() + } +} diff --git a/Shared/View Models/StatusViewModel.swift b/Shared/View Models/StatusViewModel.swift index 2547df7..da34133 100644 --- a/Shared/View Models/StatusViewModel.swift +++ b/Shared/View Models/StatusViewModel.swift @@ -19,8 +19,10 @@ struct StatusViewModel { var isReplyInContext = false var hasReplyFollowing = false var sensitiveContentToggled = false + let events: AnyPublisher, Never> private let statusService: StatusService + private let eventsInput = PassthroughSubject, Never>() init(statusService: StatusService) { self.statusService = statusService @@ -38,6 +40,7 @@ struct StatusViewModel { rebloggedByDisplayNameEmoji = statusService.status.account.emojis pollOptionTitles = statusService.status.displayStatus.poll?.options.map { $0.title } ?? [] pollEmoji = statusService.status.displayStatus.poll?.emojis ?? [] + events = eventsInput.eraseToAnyPublisher() } } @@ -74,9 +77,9 @@ extension StatusViewModel { 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 } @@ -98,6 +101,10 @@ extension StatusViewModel { return true } } + + func toggleFavorited() { + eventsInput.send(statusService.toggleFavorited()) + } } private extension StatusViewModel { diff --git a/Shared/View Models/StatusesViewModel.swift b/Shared/View Models/StatusesViewModel.swift index 0d0790a..111acb0 100644 --- a/Shared/View Models/StatusesViewModel.swift +++ b/Shared/View Models/StatusesViewModel.swift @@ -9,13 +9,17 @@ class StatusesViewModel: ObservableObject { @Published private(set) var loading = false private(set) var maintainScrollPositionOfStatusID: String? private let statusListService: StatusListService + private var statusViewModelCache = [Status: (StatusViewModel, AnyCancellable)]() private var cancellables = Set() init(statusListService: StatusListService) { self.statusListService = statusListService statusListService.statusSections - .handleEvents(receiveOutput: determineIfScrollPositionShouldBeMaintained(newStatusSections:)) + .handleEvents(receiveOutput: { [weak self] in + self?.determineIfScrollPositionShouldBeMaintained(newStatusSections: $0) + self?.cleanViewModelCache(newStatusSections: $0) + }) .assignErrorsToAlertItem(to: \.alertItem, on: self) .assign(to: &$statusSections) } @@ -35,16 +39,23 @@ extension StatusesViewModel { } func statusViewModel(status: Status) -> StatusViewModel { - var statusViewModel = Self.viewModelCache[status] - ?? StatusViewModel(statusService: statusListService.statusService(status: status)) + var statusViewModel: StatusViewModel + + 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.isPinned = statusListService.isPinned(status: status) statusViewModel.isReplyInContext = statusListService.isReplyInContext(status: status) statusViewModel.hasReplyFollowing = statusListService.hasReplyFollowing(status: status) - Self.viewModelCache[status] = statusViewModel - return statusViewModel } @@ -54,8 +65,6 @@ extension StatusesViewModel { } private extension StatusesViewModel { - static var viewModelCache = [Status: StatusViewModel]() - func determineIfScrollPositionShouldBeMaintained(newStatusSections: [[Status]]) { maintainScrollPositionOfStatusID = nil // clear old value @@ -64,4 +73,10 @@ private extension StatusesViewModel { maintainScrollPositionOfStatusID = contextParent.id } } + + func cleanViewModelCache(newStatusSections: [[Status]]) { + let newStatuses = Set(newStatusSections.reduce([], +)) + + statusViewModelCache = statusViewModelCache.filter { newStatuses.contains($0.key) } + } } diff --git a/iOS/Views/StatusTableViewCell.swift b/iOS/Views/StatusTableViewCell.swift index 942fede..f0e742c 100644 --- a/iOS/Views/StatusTableViewCell.swift +++ b/iOS/Views/StatusTableViewCell.swift @@ -274,7 +274,7 @@ extension StatusTableViewCell { } @IBAction func favoriteButtonTapped(_ sender: UIButton) { - + viewModel.toggleFavorited() } @IBAction func actionsButtonTapped(_ sender: Any) {