From 9f02197873bf487e97fc1ccdd7a677f6bc8a7f15 Mon Sep 17 00:00:00 2001 From: CMK Date: Mon, 15 Mar 2021 14:40:10 +0800 Subject: [PATCH] feat: add custom emojis API endpoint --- Mastodon.xcodeproj/project.pbxproj | 24 ++++++ .../Scene/Compose/ComposeViewController.swift | 2 +- .../APIService/APIService+CustomEmoji.swift | 22 +++++ Mastodon/Service/AuthenticationService.swift | 2 +- .../EmojiService+CustomEmoji+LoadState.swift | 86 +++++++++++++++++++ .../EmojiService+CustomEmoji.swift | 45 ++++++++++ .../Service/EmojiService/EmojiService.swift | 26 ++++++ Mastodon/State/AppContext.swift | 8 +- .../API/Mastodon+API+CustomEmojis.swift | 48 +++++++++++ .../MastodonSDK/API/Mastodon+API.swift | 5 +- 10 files changed, 262 insertions(+), 6 deletions(-) create mode 100644 Mastodon/Service/APIService/APIService+CustomEmoji.swift create mode 100644 Mastodon/Service/EmojiService/EmojiService+CustomEmoji+LoadState.swift create mode 100644 Mastodon/Service/EmojiService/EmojiService+CustomEmoji.swift create mode 100644 Mastodon/Service/EmojiService/EmojiService.swift create mode 100644 MastodonSDK/Sources/MastodonSDK/API/Mastodon+API+CustomEmojis.swift diff --git a/Mastodon.xcodeproj/project.pbxproj b/Mastodon.xcodeproj/project.pbxproj index 0d7b441de..e75ae17ce 100644 --- a/Mastodon.xcodeproj/project.pbxproj +++ b/Mastodon.xcodeproj/project.pbxproj @@ -141,6 +141,10 @@ DB45FAF925CA80A2005A8AC7 /* APIService+CoreData+MastodonAuthentication.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB45FAF825CA80A2005A8AC7 /* APIService+CoreData+MastodonAuthentication.swift */; }; DB45FB0F25CA87D0005A8AC7 /* AuthenticationService.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB45FB0E25CA87D0005A8AC7 /* AuthenticationService.swift */; }; DB45FB1D25CA9D23005A8AC7 /* APIService+HomeTimeline.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB45FB1C25CA9D23005A8AC7 /* APIService+HomeTimeline.swift */; }; + DB49A61425FF2C5600B98345 /* EmojiService.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB49A61325FF2C5600B98345 /* EmojiService.swift */; }; + DB49A61F25FF32AA00B98345 /* EmojiService+CustomEmoji.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB49A61E25FF32AA00B98345 /* EmojiService+CustomEmoji.swift */; }; + DB49A62525FF334C00B98345 /* EmojiService+CustomEmoji+LoadState.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB49A62425FF334C00B98345 /* EmojiService+CustomEmoji+LoadState.swift */; }; + DB49A62B25FF36C700B98345 /* APIService+CustomEmoji.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB49A62A25FF36C700B98345 /* APIService+CustomEmoji.swift */; }; DB5086A525CC0B7000C2C187 /* AvatarBarButtonItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB5086A425CC0B7000C2C187 /* AvatarBarButtonItem.swift */; }; DB5086AB25CC0BBB00C2C187 /* AvatarConfigurableView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB5086AA25CC0BBB00C2C187 /* AvatarConfigurableView.swift */; }; DB5086B825CC0D6400C2C187 /* Kingfisher in Frameworks */ = {isa = PBXBuildFile; productRef = DB5086B725CC0D6400C2C187 /* Kingfisher */; }; @@ -407,6 +411,10 @@ DB45FAF825CA80A2005A8AC7 /* APIService+CoreData+MastodonAuthentication.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "APIService+CoreData+MastodonAuthentication.swift"; sourceTree = ""; }; DB45FB0E25CA87D0005A8AC7 /* AuthenticationService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AuthenticationService.swift; sourceTree = ""; }; DB45FB1C25CA9D23005A8AC7 /* APIService+HomeTimeline.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "APIService+HomeTimeline.swift"; sourceTree = ""; }; + DB49A61325FF2C5600B98345 /* EmojiService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EmojiService.swift; sourceTree = ""; }; + DB49A61E25FF32AA00B98345 /* EmojiService+CustomEmoji.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "EmojiService+CustomEmoji.swift"; sourceTree = ""; }; + DB49A62425FF334C00B98345 /* EmojiService+CustomEmoji+LoadState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "EmojiService+CustomEmoji+LoadState.swift"; sourceTree = ""; }; + DB49A62A25FF36C700B98345 /* APIService+CustomEmoji.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "APIService+CustomEmoji.swift"; sourceTree = ""; }; DB5086A425CC0B7000C2C187 /* AvatarBarButtonItem.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AvatarBarButtonItem.swift; sourceTree = ""; }; DB5086AA25CC0BBB00C2C187 /* AvatarConfigurableView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AvatarConfigurableView.swift; sourceTree = ""; }; DB5086BD25CC0D9900C2C187 /* SplashPreference.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SplashPreference.swift; sourceTree = ""; }; @@ -688,6 +696,7 @@ 2D206B8B25F6015000143C56 /* AudioPlayer.swift */, 2DA6054625F716A2006356F9 /* PlaybackState.swift */, 5DF1054025F886D400D6C0D4 /* ViedeoPlaybackService.swift */, + DB49A61925FF327D00B98345 /* EmojiService */, ); path = Service; sourceTree = ""; @@ -967,6 +976,7 @@ DB45FB1C25CA9D23005A8AC7 /* APIService+HomeTimeline.swift */, DB0AC6FB25CD02E600D75117 /* APIService+Instance.swift */, DB59F10D25EF724F001F1DAB /* APIService+Poll.swift */, + DB49A62A25FF36C700B98345 /* APIService+CustomEmoji.swift */, ); path = APIService; sourceTree = ""; @@ -981,6 +991,16 @@ path = CoreData; sourceTree = ""; }; + DB49A61925FF327D00B98345 /* EmojiService */ = { + isa = PBXGroup; + children = ( + DB49A61325FF2C5600B98345 /* EmojiService.swift */, + DB49A61E25FF32AA00B98345 /* EmojiService+CustomEmoji.swift */, + DB49A62425FF334C00B98345 /* EmojiService+CustomEmoji+LoadState.swift */, + ); + path = EmojiService; + sourceTree = ""; + }; DB5086CB25CC0DB400C2C187 /* Preference */ = { isa = PBXGroup; children = ( @@ -1632,6 +1652,7 @@ 2D206B8C25F6015000143C56 /* AudioPlayer.swift in Sources */, 2D59819B25E4A581000FB903 /* MastodonConfirmEmailViewController.swift in Sources */, DB45FB1D25CA9D23005A8AC7 /* APIService+HomeTimeline.swift in Sources */, + DB49A61425FF2C5600B98345 /* EmojiService.swift in Sources */, 2D7631B325C159F700929FB9 /* Item.swift in Sources */, 5DF1054125F886D400D6C0D4 /* ViedeoPlaybackService.swift in Sources */, 0FB3D2F725E4C24D00AAD544 /* MastodonPickServerViewModel.swift in Sources */, @@ -1664,6 +1685,7 @@ DB0AC6FC25CD02E600D75117 /* APIService+Instance.swift in Sources */, 2D69D00A25CAA00300C3A1B2 /* APIService+CoreData+Toot.swift in Sources */, DB4481C625EE2ADA00BEFB67 /* PollSection.swift in Sources */, + DB49A62B25FF36C700B98345 /* APIService+CustomEmoji.swift in Sources */, 2D939AB525EDD8A90076FA61 /* String.swift in Sources */, DB4481B925EE289600BEFB67 /* UITableView.swift in Sources */, 0FAA101C25E10E760017CCDE /* UIFont.swift in Sources */, @@ -1688,6 +1710,7 @@ 2D82B9FF25E7863200E36F0F /* OnboardingViewControllerAppearance.swift in Sources */, 5DF1054725F8870E00D6C0D4 /* VideoPlayerViewModel.swift in Sources */, 2D38F1E525CD46C100561493 /* HomeTimelineViewModel.swift in Sources */, + DB49A61F25FF32AA00B98345 /* EmojiService+CustomEmoji.swift in Sources */, 2D76316B25C14D4C00929FB9 /* PublicTimelineViewModel.swift in Sources */, 2DA6055125F74407006356F9 /* AudioContainerViewModel.swift in Sources */, 0FB3D2FE25E4CB6400AAD544 /* PickServerTitleCell.swift in Sources */, @@ -1770,6 +1793,7 @@ DB0140AE25C40C7300F9F3CF /* MastodonPinBasedAuthenticationViewModel.swift in Sources */, 2D32EAAC25CB96DC00C9ED86 /* TimelineMiddleLoaderTableViewCell.swift in Sources */, 2D5A3D6225CFD9CB002347D6 /* HomeTimelineViewController+DebugAction.swift in Sources */, + DB49A62525FF334C00B98345 /* EmojiService+CustomEmoji+LoadState.swift in Sources */, DB9D6BFF25E4F5940051B173 /* ProfileViewController.swift in Sources */, 0FB3D30825E524C600AAD544 /* PickServerCategoriesCell.swift in Sources */, DB789A1225F9F2CC0071ACA0 /* ComposeViewModel.swift in Sources */, diff --git a/Mastodon/Scene/Compose/ComposeViewController.swift b/Mastodon/Scene/Compose/ComposeViewController.swift index 8504ffc5b..7ad5f7174 100644 --- a/Mastodon/Scene/Compose/ComposeViewController.swift +++ b/Mastodon/Scene/Compose/ComposeViewController.swift @@ -375,7 +375,7 @@ extension ComposeViewController: UITableViewDelegate { } } -// MARK: - ComposeViewController +// MARK: - UIAdaptivePresentationControllerDelegate extension ComposeViewController: UIAdaptivePresentationControllerDelegate { func presentationControllerShouldDismiss(_ presentationController: UIPresentationController) -> Bool { diff --git a/Mastodon/Service/APIService/APIService+CustomEmoji.swift b/Mastodon/Service/APIService/APIService+CustomEmoji.swift new file mode 100644 index 000000000..96dbcb96b --- /dev/null +++ b/Mastodon/Service/APIService/APIService+CustomEmoji.swift @@ -0,0 +1,22 @@ +// +// APIService+CustomEmoji.swift +// Mastodon +// +// Created by MainasuK Cirno on 2021-3-15. +// + +import Foundation +import Combine +import CoreData +import CoreDataStack +import CommonOSLog +import DateToolsSwift +import MastodonSDK + +extension APIService { + + func customEmoji(domain: String) -> AnyPublisher, Error> { + return Mastodon.API.CustomEmojis.customEmojis(session: session, domain: domain) + } + +} diff --git a/Mastodon/Service/AuthenticationService.swift b/Mastodon/Service/AuthenticationService.swift index 9fa411f22..89ce7a182 100644 --- a/Mastodon/Service/AuthenticationService.swift +++ b/Mastodon/Service/AuthenticationService.swift @@ -12,7 +12,7 @@ import CoreData import CoreDataStack import MastodonSDK -class AuthenticationService: NSObject { +final class AuthenticationService: NSObject { var disposeBag = Set() // input diff --git a/Mastodon/Service/EmojiService/EmojiService+CustomEmoji+LoadState.swift b/Mastodon/Service/EmojiService/EmojiService+CustomEmoji+LoadState.swift new file mode 100644 index 000000000..3ac6a49a2 --- /dev/null +++ b/Mastodon/Service/EmojiService/EmojiService+CustomEmoji+LoadState.swift @@ -0,0 +1,86 @@ +// +// EmojiService+CustomEmoji+LoadState.swift +// Mastodon +// +// Created by MainasuK Cirno on 2021-3-15. +// + +import os.log +import Foundation +import GameplayKit + +extension EmojiService.CustomEmoji { + class LoadState: GKState { + weak var viewModel: EmojiService.CustomEmoji? + + init(viewModel: EmojiService.CustomEmoji) { + self.viewModel = viewModel + } + + override func didEnter(from previousState: GKState?) { + os_log("%{public}s[%{public}ld], %{public}s: enter %s, previous: %s", ((#file as NSString).lastPathComponent), #line, #function, self.debugDescription, previousState.debugDescription) + } + } +} + +extension EmojiService.CustomEmoji.LoadState { + + class Initial: EmojiService.CustomEmoji.LoadState { + override func isValidNextState(_ stateClass: AnyClass) -> Bool { + return stateClass == Loading.self + } + } + + class Loading: EmojiService.CustomEmoji.LoadState { + override func isValidNextState(_ stateClass: AnyClass) -> Bool { + return stateClass == Fail.self || stateClass == Finish.self + } + + override func didEnter(from previousState: GKState?) { + super.didEnter(from: previousState) + guard let viewModel = viewModel, let stateMachine = stateMachine else { return } + + viewModel.context.apiService.customEmoji(domain: viewModel.domain) + .receive(on: DispatchQueue.main) + .sink { completion in + switch completion { + case .failure(let error): + os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s: failed to load custom emojis for %s: %s. Retry 10s later", ((#file as NSString).lastPathComponent), #line, #function, viewModel.domain, error.localizedDescription) + stateMachine.enter(Fail.self) + case .finished: + break + } + } receiveValue: { response in + os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s: load %ld custom emojis for %s", ((#file as NSString).lastPathComponent), #line, #function, response.value.count, viewModel.domain) + stateMachine.enter(Finish.self) + viewModel.emojis.value = response.value + } + .store(in: &viewModel.disposeBag) + } + } + + class Fail: EmojiService.CustomEmoji.LoadState { + override func isValidNextState(_ stateClass: AnyClass) -> Bool { + return stateClass == Loading.self || stateClass == Finish.self + } + + override func didEnter(from previousState: GKState?) { + super.didEnter(from: previousState) + + guard let stateMachine = stateMachine else { return } + + // retry 10s later + DispatchQueue.main.asyncAfter(deadline: .now() + 10) { + stateMachine.enter(Loading.self) + } + } + } + + class Finish: EmojiService.CustomEmoji.LoadState { + override func isValidNextState(_ stateClass: AnyClass) -> Bool { + // one time task + return false + } + } + +} diff --git a/Mastodon/Service/EmojiService/EmojiService+CustomEmoji.swift b/Mastodon/Service/EmojiService/EmojiService+CustomEmoji.swift new file mode 100644 index 000000000..aa253cf9a --- /dev/null +++ b/Mastodon/Service/EmojiService/EmojiService+CustomEmoji.swift @@ -0,0 +1,45 @@ +// +// EmojiService+CustomEmoji.swift +// Mastodon +// +// Created by MainasuK Cirno on 2021-3-15. +// + +import Foundation +import Combine +import GameplayKit +import MastodonSDK + +extension EmojiService { + final class CustomEmoji { + + var disposeBag = Set() + + // input + let domain: String + let context: AppContext + + // output + private(set) lazy var stateMachine: GKStateMachine = { + // exclude timeline middle fetcher state + let stateMachine = GKStateMachine(states: [ + LoadState.Initial(viewModel: self), + LoadState.Loading(viewModel: self), + LoadState.Fail(viewModel: self), + LoadState.Finish(viewModel: self), + ]) + stateMachine.enter(LoadState.Initial.self) + return stateMachine + }() + let emojis = CurrentValueSubject<[Mastodon.Entity.Emoji], Never>([]) + + init(domain: String, context: AppContext) { + self.domain = domain + self.context = context + + // trigger loading + stateMachine.enter(LoadState.Loading.self) + } + + } +} diff --git a/Mastodon/Service/EmojiService/EmojiService.swift b/Mastodon/Service/EmojiService/EmojiService.swift new file mode 100644 index 000000000..468c2e6dd --- /dev/null +++ b/Mastodon/Service/EmojiService/EmojiService.swift @@ -0,0 +1,26 @@ +// +// EmojiService.swift +// Mastodon +// +// Created by MainasuK Cirno on 2021-3-15. +// + +import os.log +import Foundation +import Combine +import MastodonSDK + +final class EmojiService { + + let workingQueue = DispatchQueue(label: "com.twidere.twiderex.video-playback-service.working-queue") + + weak var apiService: APIService? + + // ouput + + + init(apiService: APIService) { + self.apiService = apiService + } +} + diff --git a/Mastodon/State/AppContext.swift b/Mastodon/State/AppContext.swift index 30069ec30..bbb1c7952 100644 --- a/Mastodon/State/AppContext.swift +++ b/Mastodon/State/AppContext.swift @@ -23,12 +23,12 @@ class AppContext: ObservableObject { let apiService: APIService let authenticationService: AuthenticationService + let emojiService: EmojiService + let videoPlaybackService = VideoPlaybackService() let documentStore: DocumentStore private var documentStoreSubscription: AnyCancellable! - let videoPlaybackService = VideoPlaybackService() - let overrideTraitCollection = CurrentValueSubject(nil) init() { @@ -48,6 +48,10 @@ class AppContext: ObservableObject { apiService: _apiService ) + emojiService = EmojiService( + apiService: apiService + ) + documentStore = DocumentStore() documentStoreSubscription = documentStore.objectWillChange .receive(on: DispatchQueue.main) diff --git a/MastodonSDK/Sources/MastodonSDK/API/Mastodon+API+CustomEmojis.swift b/MastodonSDK/Sources/MastodonSDK/API/Mastodon+API+CustomEmojis.swift new file mode 100644 index 000000000..091e12d11 --- /dev/null +++ b/MastodonSDK/Sources/MastodonSDK/API/Mastodon+API+CustomEmojis.swift @@ -0,0 +1,48 @@ +// +// Mastodon+API+CustomEmojis.swift +// +// +// Created by MainasuK Cirno on 2021-3-15. +// + +import Foundation +import Combine + +extension Mastodon.API.CustomEmojis { + + static func customEmojisEndpointURL(domain: String) -> URL { + return Mastodon.API.endpointURL(domain: domain).appendingPathComponent("custom_emojis") + } + + /// Custom emoji + /// + /// Returns custom emojis that are available on the server. + /// + /// - Since: 2.0.0 + /// - Version: 3.3.0 + /// # Last Update + /// 2021/3/15 + /// # Reference + /// [Document](https://docs.joinmastodon.org/methods/instance/custom_emojis/) + /// - Parameters: + /// - session: `URLSession` + /// - domain: Mastodon instance domain. e.g. "example.com" + /// - Returns: `AnyPublisher` contains [`Emoji`] nested in the response + public static func customEmojis( + session: URLSession, + domain: String + ) -> AnyPublisher, Error> { + let request = Mastodon.API.get( + url: customEmojisEndpointURL(domain: domain), + query: nil, + authorization: nil + ) + return session.dataTaskPublisher(for: request) + .tryMap { data, response in + let value = try Mastodon.API.decode(type: [Mastodon.Entity.Emoji].self, from: data, response: response) + return Mastodon.Response.Content(value: value, response: response) + } + .eraseToAnyPublisher() + } + +} diff --git a/MastodonSDK/Sources/MastodonSDK/API/Mastodon+API.swift b/MastodonSDK/Sources/MastodonSDK/API/Mastodon+API.swift index 5443fa22d..8b952fdb0 100644 --- a/MastodonSDK/Sources/MastodonSDK/API/Mastodon+API.swift +++ b/MastodonSDK/Sources/MastodonSDK/API/Mastodon+API.swift @@ -91,13 +91,14 @@ extension Mastodon.API { extension Mastodon.API { public enum Account { } public enum App { } + public enum CustomEmojis { } + public enum Favorites { } public enum Instance { } public enum OAuth { } public enum Onboarding { } public enum Polls { } - public enum Timeline { } public enum Statuses { } - public enum Favorites { } + public enum Timeline { } } extension Mastodon.API {