feat: add custom emojis API endpoint

This commit is contained in:
CMK 2021-03-15 14:40:10 +08:00
parent 8eb24871c5
commit 9f02197873
10 changed files with 262 additions and 6 deletions

View File

@ -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 = "<group>"; };
DB45FB0E25CA87D0005A8AC7 /* AuthenticationService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AuthenticationService.swift; sourceTree = "<group>"; };
DB45FB1C25CA9D23005A8AC7 /* APIService+HomeTimeline.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "APIService+HomeTimeline.swift"; sourceTree = "<group>"; };
DB49A61325FF2C5600B98345 /* EmojiService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EmojiService.swift; sourceTree = "<group>"; };
DB49A61E25FF32AA00B98345 /* EmojiService+CustomEmoji.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "EmojiService+CustomEmoji.swift"; sourceTree = "<group>"; };
DB49A62425FF334C00B98345 /* EmojiService+CustomEmoji+LoadState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "EmojiService+CustomEmoji+LoadState.swift"; sourceTree = "<group>"; };
DB49A62A25FF36C700B98345 /* APIService+CustomEmoji.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "APIService+CustomEmoji.swift"; sourceTree = "<group>"; };
DB5086A425CC0B7000C2C187 /* AvatarBarButtonItem.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AvatarBarButtonItem.swift; sourceTree = "<group>"; };
DB5086AA25CC0BBB00C2C187 /* AvatarConfigurableView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AvatarConfigurableView.swift; sourceTree = "<group>"; };
DB5086BD25CC0D9900C2C187 /* SplashPreference.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SplashPreference.swift; sourceTree = "<group>"; };
@ -688,6 +696,7 @@
2D206B8B25F6015000143C56 /* AudioPlayer.swift */,
2DA6054625F716A2006356F9 /* PlaybackState.swift */,
5DF1054025F886D400D6C0D4 /* ViedeoPlaybackService.swift */,
DB49A61925FF327D00B98345 /* EmojiService */,
);
path = Service;
sourceTree = "<group>";
@ -967,6 +976,7 @@
DB45FB1C25CA9D23005A8AC7 /* APIService+HomeTimeline.swift */,
DB0AC6FB25CD02E600D75117 /* APIService+Instance.swift */,
DB59F10D25EF724F001F1DAB /* APIService+Poll.swift */,
DB49A62A25FF36C700B98345 /* APIService+CustomEmoji.swift */,
);
path = APIService;
sourceTree = "<group>";
@ -981,6 +991,16 @@
path = CoreData;
sourceTree = "<group>";
};
DB49A61925FF327D00B98345 /* EmojiService */ = {
isa = PBXGroup;
children = (
DB49A61325FF2C5600B98345 /* EmojiService.swift */,
DB49A61E25FF32AA00B98345 /* EmojiService+CustomEmoji.swift */,
DB49A62425FF334C00B98345 /* EmojiService+CustomEmoji+LoadState.swift */,
);
path = EmojiService;
sourceTree = "<group>";
};
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 */,

View File

@ -375,7 +375,7 @@ extension ComposeViewController: UITableViewDelegate {
}
}
// MARK: - ComposeViewController
// MARK: - UIAdaptivePresentationControllerDelegate
extension ComposeViewController: UIAdaptivePresentationControllerDelegate {
func presentationControllerShouldDismiss(_ presentationController: UIPresentationController) -> Bool {

View File

@ -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<Mastodon.Response.Content<[Mastodon.Entity.Emoji]>, Error> {
return Mastodon.API.CustomEmojis.customEmojis(session: session, domain: domain)
}
}

View File

@ -12,7 +12,7 @@ import CoreData
import CoreDataStack
import MastodonSDK
class AuthenticationService: NSObject {
final class AuthenticationService: NSObject {
var disposeBag = Set<AnyCancellable>()
// input

View File

@ -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
}
}
}

View File

@ -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<AnyCancellable>()
// 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)
}
}
}

View File

@ -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
}
}

View File

@ -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<UITraitCollection?, Never>(nil)
init() {
@ -48,6 +48,10 @@ class AppContext: ObservableObject {
apiService: _apiService
)
emojiService = EmojiService(
apiService: apiService
)
documentStore = DocumentStore()
documentStoreSubscription = documentStore.objectWillChange
.receive(on: DispatchQueue.main)

View File

@ -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<Mastodon.Response.Content<[Mastodon.Entity.Emoji]>, 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()
}
}

View File

@ -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 {