fix: media attachment needs wait process issue. resolve #275

This commit is contained in:
CMK 2021-08-09 17:02:32 +08:00
parent 9ac69d6d8b
commit 3570c7108c
7 changed files with 138 additions and 6 deletions

View File

@ -7,12 +7,12 @@
<key>AppShared.xcscheme_^#shared#^_</key>
<dict>
<key>orderHint</key>
<integer>30</integer>
<integer>32</integer>
</dict>
<key>CoreDataStack.xcscheme_^#shared#^_</key>
<dict>
<key>orderHint</key>
<integer>34</integer>
<integer>31</integer>
</dict>
<key>Mastodon - ASDK.xcscheme_^#shared#^_</key>
<dict>
@ -77,7 +77,7 @@
<key>MastodonIntent.xcscheme_^#shared#^_</key>
<dict>
<key>orderHint</key>
<integer>32</integer>
<integer>33</integer>
</dict>
<key>MastodonIntents.xcscheme_^#shared#^_</key>
<dict>
@ -97,7 +97,7 @@
<key>ShareActionExtension.xcscheme_^#shared#^_</key>
<dict>
<key>orderHint</key>
<integer>31</integer>
<integer>30</integer>
</dict>
</dict>
<key>SuppressBuildableAutocreation</key>

View File

@ -338,6 +338,9 @@ final class ComposeViewModel: NSObject {
if currentState is MastodonAttachmentService.UploadState.Finish {
continue
}
if currentState is MastodonAttachmentService.UploadState.Processing {
continue
}
if currentState is MastodonAttachmentService.UploadState.Uploading {
break
}

View File

@ -57,6 +57,25 @@ extension APIService {
}
extension APIService {
func getMedia(
attachmentID: Mastodon.Entity.Attachment.ID,
mastodonAuthenticationBox: MastodonAuthenticationBox
) -> AnyPublisher<Mastodon.Response.Content<Mastodon.Entity.Attachment>, Error> {
let authorization = mastodonAuthenticationBox.userAuthorization
return Mastodon.API.Media.getMedia(
session: session,
domain: mastodonAuthenticationBox.domain,
attachmentID: attachmentID,
authorization: authorization
)
.eraseToAnyPublisher()
}
}
extension APIService {
func updateMedia(

View File

@ -47,7 +47,10 @@ extension MastodonAttachmentService.UploadState {
var needsFallback = false
override func isValidNextState(_ stateClass: AnyClass) -> Bool {
return stateClass == Fail.self || stateClass == Finish.self || stateClass == Uploading.self
return stateClass == Fail.self
|| stateClass == Finish.self
|| stateClass == Uploading.self
|| stateClass == Processing.self
}
override func didEnter(from previousState: GKState?) {
@ -96,11 +99,70 @@ extension MastodonAttachmentService.UploadState {
} receiveValue: { response in
os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s: upload attachment %s success: %s", ((#file as NSString).lastPathComponent), #line, #function, response.value.id, response.value.url ?? "<nil>")
service.attachment.value = response.value
if response.statusCode == 202 {
// check if still processing
stateMachine.enter(Processing.self)
} else {
stateMachine.enter(Finish.self)
}
}
.store(in: &service.disposeBag)
}
}
class Processing: MastodonAttachmentService.UploadState {
static let retryLimit = 10
var retryCount = 0
override func isValidNextState(_ stateClass: AnyClass) -> Bool {
return stateClass == Fail.self || stateClass == Finish.self || stateClass == Processing.self
}
override func didEnter(from previousState: GKState?) {
super.didEnter(from: previousState)
guard let service = service, let stateMachine = stateMachine else { return }
guard let authenticationBox = service.authenticationBox else { return }
guard let attachment = service.attachment.value else { return }
retryCount += 1
guard retryCount < Processing.retryLimit else {
stateMachine.enter(Fail.self)
return
}
service.context.apiService.getMedia(
attachmentID: attachment.id,
mastodonAuthenticationBox: authenticationBox
)
.retry(3)
.receive(on: DispatchQueue.main)
.sink { [weak self] completion in
guard let _ = self else { return }
switch completion {
case .failure(let error):
os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s: get attachment fail: %s", ((#file as NSString).lastPathComponent), #line, #function, error.localizedDescription)
service.error.send(error)
stateMachine.enter(Fail.self)
case .finished:
os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s: get attachment success", ((#file as NSString).lastPathComponent), #line, #function)
break
}
} receiveValue: { [weak self] response in
guard let self = self else { return }
guard let _ = response.value.url else {
DispatchQueue.main.asyncAfter(deadline: .now() + 2) { [weak self] in
os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s: processing, retry in 2s", ((#file as NSString).lastPathComponent), #line, #function)
self?.stateMachine?.enter(Processing.self)
}
return
}
stateMachine.enter(Finish.self)
}
.store(in: &service.disposeBag)
}
}
class Fail: MastodonAttachmentService.UploadState {

View File

@ -41,6 +41,7 @@ final class MastodonAttachmentService {
let stateMachine = GKStateMachine(states: [
UploadState.Initial(service: self),
UploadState.Uploading(service: self),
UploadState.Processing(service: self),
UploadState.Fail(service: self),
UploadState.Finish(service: self),
])

View File

@ -113,6 +113,49 @@ extension Mastodon.API.Media {
}
extension Mastodon.API.Media {
static func getMediaEndpointURL(domain: String, attachmentID: Mastodon.Entity.Attachment.ID) -> URL {
Mastodon.API.endpointURL(domain: domain).appendingPathComponent("media").appendingPathComponent(attachmentID)
}
/// Get media attachment
///
/// Get an Attachment, before it is attached to a status and posted, but after it is accepted for processing.
///
/// - Since: 0.0.0
/// - Version: 3.4.1
/// # Last Update
/// 2021/8/9
/// # Reference
/// [Document](https://docs.joinmastodon.org/methods/statuses/media/)
/// - Parameters:
/// - session: `URLSession`
/// - domain: Mastodon instance domain. e.g. "example.com"
/// - mediaID: The ID of attachment
/// - authorization: User token
/// - Returns: `AnyPublisher` contains `Attachment` nested in the response
public static func getMedia(
session: URLSession,
domain: String,
attachmentID: Mastodon.Entity.Attachment.ID,
authorization: Mastodon.API.OAuth.Authorization?
) -> AnyPublisher<Mastodon.Response.Content<Mastodon.Entity.Attachment>, Error> {
var request = Mastodon.API.get(
url: getMediaEndpointURL(domain: domain, attachmentID: attachmentID),
query: nil,
authorization: authorization
)
request.timeoutInterval = 10 // short timeout for quick retry
return session.dataTaskPublisher(for: request)
.tryMap { data, response in
let value = try Mastodon.API.decode(type: Mastodon.Entity.Attachment.self, from: data, response: response)
return Mastodon.Response.Content(value: value, response: response)
}
.eraseToAnyPublisher()
}
}
extension Mastodon.API.Media {
static func updateMediaEndpointURL(domain: String, attachmentID: Mastodon.Entity.Attachment.ID) -> URL {

View File

@ -14,6 +14,7 @@ extension Mastodon.Response {
public let value: T
// standard fields
public let statusCode: Int? ///< HTTP Code
public let date: Date?
// application fields
@ -28,6 +29,8 @@ extension Mastodon.Response {
public init(value: T, response: URLResponse) {
self.value = value
self.statusCode = (response as? HTTPURLResponse)?.statusCode
self.date = {
guard let string = (response as? HTTPURLResponse)?.value(forHTTPHeaderField: "date") else { return nil }
return Mastodon.API.httpHeaderDateFormatter.date(from: string)
@ -47,6 +50,7 @@ extension Mastodon.Response {
init<O>(value: T, old: Mastodon.Response.Content<O>) {
self.value = value
self.statusCode = old.statusCode
self.date = old.date
self.rateLimit = old.rateLimit
self.link = old.link