Compare commits
3 Commits
Author | SHA1 | Date |
---|---|---|
Shadowfacts | 28332ef448 | |
Shadowfacts | b9edf13b92 | |
Shadowfacts | c36a239f46 |
|
@ -101,6 +101,19 @@ public class Client {
|
||||||
return task
|
return task
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public func run<Result>(_ request: Request<Result>) async throws -> (Result, Pagination?) {
|
||||||
|
return try await withCheckedThrowingContinuation { continuation in
|
||||||
|
run(request) { response in
|
||||||
|
switch response {
|
||||||
|
case let .failure(error):
|
||||||
|
continuation.resume(throwing: error)
|
||||||
|
case let .success(result, pagination):
|
||||||
|
continuation.resume(returning: (result, pagination))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func createURLRequest<Result>(request: Request<Result>) -> URLRequest? {
|
func createURLRequest<Result>(request: Request<Result>) -> URLRequest? {
|
||||||
guard var components = URLComponents(url: baseURL, resolvingAgainstBaseURL: true) else { return nil }
|
guard var components = URLComponents(url: baseURL, resolvingAgainstBaseURL: true) else { return nil }
|
||||||
components.path = request.path
|
components.path = request.path
|
||||||
|
@ -117,24 +130,21 @@ public class Client {
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - Authorization
|
// MARK: - Authorization
|
||||||
public func registerApp(name: String, redirectURI: String, scopes: [Scope], website: URL? = nil, completion: @escaping Callback<RegisteredApplication>) {
|
public func registerApp(name: String, redirectURI: String, scopes: [Scope], website: URL? = nil) async throws -> RegisteredApplication {
|
||||||
let request = Request<RegisteredApplication>(method: .post, path: "/api/v1/apps", body: ParametersBody([
|
let request = Request<RegisteredApplication>(method: .post, path: "/api/v1/apps", body: ParametersBody([
|
||||||
"client_name" => name,
|
"client_name" => name,
|
||||||
"redirect_uris" => redirectURI,
|
"redirect_uris" => redirectURI,
|
||||||
"scopes" => scopes.scopeString,
|
"scopes" => scopes.scopeString,
|
||||||
"website" => website?.absoluteString
|
"website" => website?.absoluteString
|
||||||
]))
|
]))
|
||||||
run(request) { result in
|
let (application, _) = try await run(request)
|
||||||
defer { completion(result) }
|
self.appID = application.id
|
||||||
guard case let .success(application, _) = result else { return }
|
self.clientID = application.clientID
|
||||||
|
self.clientSecret = application.clientSecret
|
||||||
self.appID = application.id
|
return application
|
||||||
self.clientID = application.clientID
|
|
||||||
self.clientSecret = application.clientSecret
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public func getAccessToken(authorizationCode: String, redirectURI: String, completion: @escaping Callback<LoginSettings>) {
|
public func getAccessToken(authorizationCode: String, redirectURI: String) async throws -> LoginSettings {
|
||||||
let request = Request<LoginSettings>(method: .post, path: "/oauth/token", body: ParametersBody([
|
let request = Request<LoginSettings>(method: .post, path: "/oauth/token", body: ParametersBody([
|
||||||
"client_id" => clientID,
|
"client_id" => clientID,
|
||||||
"client_secret" => clientSecret,
|
"client_secret" => clientSecret,
|
||||||
|
@ -142,12 +152,9 @@ public class Client {
|
||||||
"code" => authorizationCode,
|
"code" => authorizationCode,
|
||||||
"redirect_uri" => redirectURI
|
"redirect_uri" => redirectURI
|
||||||
]))
|
]))
|
||||||
run(request) { result in
|
let (settings, _) = try await run(request)
|
||||||
defer { completion(result) }
|
self.accessToken = settings.accessToken
|
||||||
guard case let .success(loginSettings, _) = result else { return }
|
return settings
|
||||||
|
|
||||||
self.accessToken = loginSettings.accessToken
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - Self
|
// MARK: - Self
|
||||||
|
|
|
@ -2342,6 +2342,7 @@
|
||||||
DYLIB_INSTALL_NAME_BASE = "@rpath";
|
DYLIB_INSTALL_NAME_BASE = "@rpath";
|
||||||
INFOPLIST_FILE = Pachyderm/Info.plist;
|
INFOPLIST_FILE = Pachyderm/Info.plist;
|
||||||
INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks";
|
INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks";
|
||||||
|
IPHONEOS_DEPLOYMENT_TARGET = 15.0;
|
||||||
"IPHONEOS_DEPLOYMENT_TARGET[sdk=macosx*]" = 14.3;
|
"IPHONEOS_DEPLOYMENT_TARGET[sdk=macosx*]" = 14.3;
|
||||||
LD_RUNPATH_SEARCH_PATHS = (
|
LD_RUNPATH_SEARCH_PATHS = (
|
||||||
"$(inherited)",
|
"$(inherited)",
|
||||||
|
@ -2373,6 +2374,7 @@
|
||||||
DYLIB_INSTALL_NAME_BASE = "@rpath";
|
DYLIB_INSTALL_NAME_BASE = "@rpath";
|
||||||
INFOPLIST_FILE = Pachyderm/Info.plist;
|
INFOPLIST_FILE = Pachyderm/Info.plist;
|
||||||
INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks";
|
INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks";
|
||||||
|
IPHONEOS_DEPLOYMENT_TARGET = 15.0;
|
||||||
"IPHONEOS_DEPLOYMENT_TARGET[sdk=macosx*]" = 14.3;
|
"IPHONEOS_DEPLOYMENT_TARGET[sdk=macosx*]" = 14.3;
|
||||||
LD_RUNPATH_SEARCH_PATHS = (
|
LD_RUNPATH_SEARCH_PATHS = (
|
||||||
"$(inherited)",
|
"$(inherited)",
|
||||||
|
@ -2560,7 +2562,7 @@
|
||||||
CURRENT_PROJECT_VERSION = 19;
|
CURRENT_PROJECT_VERSION = 19;
|
||||||
DEVELOPMENT_TEAM = V4WK9KR9U2;
|
DEVELOPMENT_TEAM = V4WK9KR9U2;
|
||||||
INFOPLIST_FILE = Tusker/Info.plist;
|
INFOPLIST_FILE = Tusker/Info.plist;
|
||||||
IPHONEOS_DEPLOYMENT_TARGET = 14.1;
|
IPHONEOS_DEPLOYMENT_TARGET = 15.0;
|
||||||
"IPHONEOS_DEPLOYMENT_TARGET[sdk=macosx*]" = 14.3;
|
"IPHONEOS_DEPLOYMENT_TARGET[sdk=macosx*]" = 14.3;
|
||||||
LD_RUNPATH_SEARCH_PATHS = (
|
LD_RUNPATH_SEARCH_PATHS = (
|
||||||
"$(inherited)",
|
"$(inherited)",
|
||||||
|
@ -2593,7 +2595,7 @@
|
||||||
CURRENT_PROJECT_VERSION = 19;
|
CURRENT_PROJECT_VERSION = 19;
|
||||||
DEVELOPMENT_TEAM = V4WK9KR9U2;
|
DEVELOPMENT_TEAM = V4WK9KR9U2;
|
||||||
INFOPLIST_FILE = Tusker/Info.plist;
|
INFOPLIST_FILE = Tusker/Info.plist;
|
||||||
IPHONEOS_DEPLOYMENT_TARGET = 14.1;
|
IPHONEOS_DEPLOYMENT_TARGET = 15.0;
|
||||||
"IPHONEOS_DEPLOYMENT_TARGET[sdk=macosx*]" = 14.3;
|
"IPHONEOS_DEPLOYMENT_TARGET[sdk=macosx*]" = 14.3;
|
||||||
LD_RUNPATH_SEARCH_PATHS = (
|
LD_RUNPATH_SEARCH_PATHS = (
|
||||||
"$(inherited)",
|
"$(inherited)",
|
||||||
|
|
|
@ -99,6 +99,11 @@
|
||||||
value = "1"
|
value = "1"
|
||||||
isEnabled = "NO">
|
isEnabled = "NO">
|
||||||
</EnvironmentVariable>
|
</EnvironmentVariable>
|
||||||
|
<EnvironmentVariable
|
||||||
|
key = "LIBDISPATCH_COOPERATIVE_POOL_STRICT"
|
||||||
|
value = "1"
|
||||||
|
isEnabled = "YES">
|
||||||
|
</EnvironmentVariable>
|
||||||
</EnvironmentVariables>
|
</EnvironmentVariables>
|
||||||
</LaunchAction>
|
</LaunchAction>
|
||||||
<ProfileAction
|
<ProfileAction
|
||||||
|
|
|
@ -2,7 +2,7 @@
|
||||||
"object": {
|
"object": {
|
||||||
"pins": [
|
"pins": [
|
||||||
{
|
{
|
||||||
"package": "PLCrashReporter",
|
"package": "plcrashreporter",
|
||||||
"repositoryURL": "https://github.com/microsoft/plcrashreporter",
|
"repositoryURL": "https://github.com/microsoft/plcrashreporter",
|
||||||
"state": {
|
"state": {
|
||||||
"branch": null,
|
"branch": null,
|
||||||
|
|
|
@ -46,8 +46,8 @@ class MastodonController: ObservableObject {
|
||||||
@Published private(set) var instance: Instance!
|
@Published private(set) var instance: Instance!
|
||||||
private(set) var customEmojis: [Emoji]?
|
private(set) var customEmojis: [Emoji]?
|
||||||
|
|
||||||
private var pendingOwnInstanceRequestCallbacks = [(Instance) -> Void]()
|
@MainActor private var pendingOwnInstanceRequestCallbacks = [(Instance) -> Void]()
|
||||||
private var ownInstanceRequest: URLSessionTask?
|
@MainActor private var ownInstanceRequest: URLSessionTask?
|
||||||
|
|
||||||
var loggedIn: Bool {
|
var loggedIn: Bool {
|
||||||
accountInfo != nil
|
accountInfo != nil
|
||||||
|
@ -65,28 +65,22 @@ class MastodonController: ObservableObject {
|
||||||
return client.run(request, completion: completion)
|
return client.run(request, completion: completion)
|
||||||
}
|
}
|
||||||
|
|
||||||
func registerApp(completion: @escaping (_ clientID: String, _ clientSecret: String) -> Void) {
|
@discardableResult
|
||||||
guard client.clientID == nil,
|
func run<Result>(_ request: Request<Result>) async throws -> (Result, Pagination?) {
|
||||||
client.clientSecret == nil else {
|
return try await client.run(request)
|
||||||
|
|
||||||
completion(client.clientID!, client.clientSecret!)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
client.registerApp(name: "Tusker", redirectURI: "tusker://oauth", scopes: [.read, .write, .follow]) { response in
|
|
||||||
guard case let .success(app, _) = response else { fatalError() }
|
|
||||||
self.client.clientID = app.clientID
|
|
||||||
self.client.clientSecret = app.clientSecret
|
|
||||||
completion(app.clientID, app.clientSecret)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func authorize(authorizationCode: String, completion: @escaping (_ accessToken: String) -> Void) {
|
func registerApp() async -> (String, String) {
|
||||||
client.getAccessToken(authorizationCode: authorizationCode, redirectURI: "tusker://oauth") { response in
|
guard client.clientID == nil,
|
||||||
guard case let .success(settings, _) = response else { fatalError() }
|
client.clientSecret == nil else {
|
||||||
self.client.accessToken = settings.accessToken
|
return (client.clientID!, client.clientSecret!)
|
||||||
completion(settings.accessToken)
|
}
|
||||||
}
|
let app = try! await client.registerApp(name: "Tusker", redirectURI: "tusker://oauth", scopes: [.read, .write, .follow])
|
||||||
|
return (app.clientID, app.clientSecret)
|
||||||
|
}
|
||||||
|
|
||||||
|
func authorize(authorizationCode: String) async -> String {
|
||||||
|
return try! await client.getAccessToken(authorizationCode: authorizationCode, redirectURI: "tusker://oauth").accessToken
|
||||||
}
|
}
|
||||||
|
|
||||||
func getOwnAccount(completion: ((Result<Account, Client.Error>) -> Void)? = nil) {
|
func getOwnAccount(completion: ((Result<Account, Client.Error>) -> Void)? = nil) {
|
||||||
|
@ -118,14 +112,19 @@ class MastodonController: ObservableObject {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func getOwnAccount() async throws -> Account {
|
||||||
|
return try await withCheckedThrowingContinuation { continuation in
|
||||||
|
getOwnAccount(completion: continuation.resume)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@MainActor
|
||||||
func getOwnInstance(completion: ((Instance) -> Void)? = nil) {
|
func getOwnInstance(completion: ((Instance) -> Void)? = nil) {
|
||||||
getOwnInstanceInternal(retryAttempt: 0, completion: completion)
|
getOwnInstanceInternal(retryAttempt: 0, completion: completion)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@MainActor
|
||||||
private func getOwnInstanceInternal(retryAttempt: Int, completion: ((Instance) -> Void)?) {
|
private func getOwnInstanceInternal(retryAttempt: Int, completion: ((Instance) -> Void)?) {
|
||||||
// this is main thread only to prevent concurrent access to ownInstanceRequest and pendingOwnInstanceRequestCallbacks
|
|
||||||
assert(Thread.isMainThread)
|
|
||||||
|
|
||||||
if let instance = self.instance {
|
if let instance = self.instance {
|
||||||
completion?(instance)
|
completion?(instance)
|
||||||
} else {
|
} else {
|
||||||
|
|
|
@ -104,6 +104,14 @@ enum CompositionAttachmentData {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func getData() async -> (Data, String) {
|
||||||
|
return await withCheckedContinuation { continuation in
|
||||||
|
getData { data, mimeType in
|
||||||
|
continuation.resume(returning: (data, mimeType))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private static func exportVideoData(session: AVAssetExportSession, completion: @escaping (Data, String) -> Void) {
|
private static func exportVideoData(session: AVAssetExportSession, completion: @escaping (Data, String) -> Void) {
|
||||||
session.outputFileType = .mp4
|
session.outputFileType = .mp4
|
||||||
session.outputURL = FileManager.default.temporaryDirectory.appendingPathComponent("exported_video_\(UUID())").appendingPathExtension("mp4")
|
session.outputURL = FileManager.default.temporaryDirectory.appendingPathComponent("exported_video_\(UUID())").appendingPathExtension("mp4")
|
||||||
|
|
|
@ -19,7 +19,7 @@ struct ComposeView: View {
|
||||||
@State private var postProgress: Double = 0
|
@State private var postProgress: Double = 0
|
||||||
@State private var postTotalProgress: Double = 0
|
@State private var postTotalProgress: Double = 0
|
||||||
@State private var isShowingPostErrorAlert = false
|
@State private var isShowingPostErrorAlert = false
|
||||||
@State private var postError: PostError?
|
@State private var postError: Error?
|
||||||
|
|
||||||
private let stackPadding: CGFloat = 8
|
private let stackPadding: CGFloat = 8
|
||||||
|
|
||||||
|
@ -148,7 +148,11 @@ struct ComposeView: View {
|
||||||
}
|
}
|
||||||
|
|
||||||
private var postButton: some View {
|
private var postButton: some View {
|
||||||
Button(action: self.postStatus) {
|
Button {
|
||||||
|
async {
|
||||||
|
await self.postStatus()
|
||||||
|
}
|
||||||
|
} label: {
|
||||||
Text("Post")
|
Text("Post")
|
||||||
}
|
}
|
||||||
.disabled(!postButtonEnabled)
|
.disabled(!postButtonEnabled)
|
||||||
|
@ -190,7 +194,7 @@ struct ComposeView: View {
|
||||||
])
|
])
|
||||||
}
|
}
|
||||||
|
|
||||||
private func postStatus() {
|
private func postStatus() async {
|
||||||
guard draft.hasContent else { return }
|
guard draft.hasContent else { return }
|
||||||
|
|
||||||
isPosting = true
|
isPosting = true
|
||||||
|
@ -205,71 +209,58 @@ struct ComposeView: View {
|
||||||
postTotalProgress = Double(2 + (draft.attachments.count * 2))
|
postTotalProgress = Double(2 + (draft.attachments.count * 2))
|
||||||
postProgress = 1
|
postProgress = 1
|
||||||
|
|
||||||
uploadAttachments { (result) in
|
let uploadedAttachments: [Attachment]
|
||||||
switch result {
|
do {
|
||||||
case let .failure(error):
|
uploadedAttachments = try await uploadAttachments()
|
||||||
self.isShowingPostErrorAlert = true
|
} catch {
|
||||||
self.postError = error
|
self.isShowingPostErrorAlert = true
|
||||||
self.postProgress = 0
|
self.postError = error
|
||||||
self.postTotalProgress = 0
|
self.postProgress = 0
|
||||||
self.isPosting = false
|
self.postTotalProgress = 0
|
||||||
|
self.isPosting = false
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
let request = Client.createStatus(text: draft.textForPosting,
|
||||||
|
contentType: Preferences.shared.statusContentType,
|
||||||
|
inReplyTo: draft.inReplyToID,
|
||||||
|
media: uploadedAttachments,
|
||||||
|
sensitive: sensitive,
|
||||||
|
spoilerText: contentWarning,
|
||||||
|
visibility: draft.visibility,
|
||||||
|
language: nil,
|
||||||
|
pollOptions: draft.poll?.options.map(\.text),
|
||||||
|
pollExpiresIn: draft.poll == nil ? nil : Int(draft.poll!.duration),
|
||||||
|
pollMultiple: draft.poll?.multiple)
|
||||||
|
do {
|
||||||
|
try await mastodonController.run(request)
|
||||||
|
self.postProgress += 1
|
||||||
|
|
||||||
case let .success(uploadedAttachments):
|
DraftsManager.shared.remove(self.draft)
|
||||||
let request = Client.createStatus(text: draft.textForPosting,
|
|
||||||
contentType: Preferences.shared.statusContentType,
|
// wait .25 seconds so the user can see the progress bar has completed
|
||||||
inReplyTo: draft.inReplyToID,
|
DispatchQueue.main.asyncAfter(deadline: .now() + .milliseconds(250)) {
|
||||||
media: uploadedAttachments,
|
self.uiState.delegate?.dismissCompose(mode: .post)
|
||||||
sensitive: sensitive,
|
|
||||||
spoilerText: contentWarning,
|
|
||||||
visibility: draft.visibility,
|
|
||||||
language: nil,
|
|
||||||
pollOptions: draft.poll?.options.map(\.text),
|
|
||||||
pollExpiresIn: draft.poll == nil ? nil : Int(draft.poll!.duration),
|
|
||||||
pollMultiple: draft.poll?.multiple)
|
|
||||||
self.mastodonController.run(request) { (response) in
|
|
||||||
switch response {
|
|
||||||
case let .failure(error):
|
|
||||||
self.isShowingPostErrorAlert = true
|
|
||||||
self.postError = error
|
|
||||||
|
|
||||||
case .success(_, _):
|
|
||||||
self.postProgress += 1
|
|
||||||
|
|
||||||
DraftsManager.shared.remove(self.draft)
|
|
||||||
|
|
||||||
// wait .25 seconds so the user can see the progress bar has completed
|
|
||||||
DispatchQueue.main.asyncAfter(deadline: .now() + .milliseconds(250)) {
|
|
||||||
self.uiState.delegate?.dismissCompose(mode: .post)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
} catch {
|
||||||
|
self.isShowingPostErrorAlert = true
|
||||||
|
self.postError = error
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private func uploadAttachments(_ completion: @escaping (Result<[Attachment], AttachmentUploadError>) -> Void) {
|
private func uploadAttachments() async throws -> [Attachment] {
|
||||||
let group = DispatchGroup()
|
guard !draft.attachments.isEmpty else {
|
||||||
|
return []
|
||||||
var attachmentDatas = [(Data, String)?]()
|
|
||||||
|
|
||||||
for (index, compAttachment) in draft.attachments.enumerated() {
|
|
||||||
group.enter()
|
|
||||||
|
|
||||||
attachmentDatas.append(nil)
|
|
||||||
|
|
||||||
compAttachment.data.getData { (data, mimeType) in
|
|
||||||
postProgress += 1
|
|
||||||
|
|
||||||
attachmentDatas[index] = (data, mimeType)
|
|
||||||
group.leave()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
group.notify(queue: .global(qos: .userInitiated)) {
|
return try await withThrowingTaskGroup(of: (CompositionAttachment, (Data, String)).self) { taskGroup in
|
||||||
|
for compAttachment in draft.attachments {
|
||||||
var anyFailed = false
|
postProgress += 1
|
||||||
var uploadedAttachments = [Result<Attachment, Error>?]()
|
taskGroup.async {
|
||||||
|
return (compAttachment, await compAttachment.data.getData())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Mastodon does not respect the order of the `media_ids` parameter in the create post request,
|
// Mastodon does not respect the order of the `media_ids` parameter in the create post request,
|
||||||
// it determines attachment order by which was uploaded first. Since the upload attachment request
|
// it determines attachment order by which was uploaded first. Since the upload attachment request
|
||||||
// does not include any timestamp data, and requests may arrive at the server out-of-order,
|
// does not include any timestamp data, and requests may arrive at the server out-of-order,
|
||||||
|
@ -277,67 +268,40 @@ struct ComposeView: View {
|
||||||
// posted status reflects order the user set.
|
// posted status reflects order the user set.
|
||||||
// Pleroma does respect the order of the `media_ids` parameter.
|
// Pleroma does respect the order of the `media_ids` parameter.
|
||||||
|
|
||||||
for (index, (data, mimeType)) in attachmentDatas.map(\.unsafelyUnwrapped).enumerated() {
|
var anyFailed = false
|
||||||
group.enter()
|
var uploaded = [Result<Attachment, Error>]()
|
||||||
|
|
||||||
let compAttachment = draft.attachments[index]
|
for try await (compAttachment, (data, mimeType)) in taskGroup {
|
||||||
let formAttachment = FormAttachment(mimeType: mimeType, data: data, fileName: "file")
|
let formAttachment = FormAttachment(mimeType: mimeType, data: data, fileName: "file")
|
||||||
let request = Client.upload(attachment: formAttachment, description: compAttachment.attachmentDescription)
|
let request = Client.upload(attachment: formAttachment, description: compAttachment.attachmentDescription)
|
||||||
self.mastodonController.run(request) { (response) in
|
|
||||||
switch response {
|
do {
|
||||||
case let .failure(error):
|
let (uploadedAttachment, _) = try await mastodonController.run(request)
|
||||||
uploadedAttachments.append(.failure(error))
|
uploaded.append(.success(uploadedAttachment))
|
||||||
anyFailed = true
|
postProgress += 1
|
||||||
|
} catch {
|
||||||
case let .success(attachment, _):
|
uploaded.append(.failure(error))
|
||||||
self.postProgress += 1
|
anyFailed = true
|
||||||
uploadedAttachments.append(.success(attachment))
|
|
||||||
}
|
|
||||||
|
|
||||||
group.leave()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
group.wait()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
if anyFailed {
|
if anyFailed {
|
||||||
let errors = uploadedAttachments.map { (result) -> Error? in
|
let errors = uploaded.map { result -> Error? in
|
||||||
if case let .failure(error) = result {
|
if case let .failure(error) = result {
|
||||||
return error
|
return error
|
||||||
} else {
|
} else {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
completion(.failure(AttachmentUploadError(errors: errors)))
|
throw AttachmentUploadError(errors: errors)
|
||||||
} else {
|
} else {
|
||||||
let uploadedAttachments = uploadedAttachments.map {
|
return uploaded.map { try! $0.get() }
|
||||||
try! $0!.get()
|
|
||||||
}
|
|
||||||
completion(.success(uploadedAttachments))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fileprivate protocol PostError: LocalizedError {}
|
fileprivate struct AttachmentUploadError: LocalizedError {
|
||||||
|
|
||||||
extension PostError {
|
|
||||||
var localizedDescription: String {
|
|
||||||
if let self = self as? Client.Error {
|
|
||||||
return self.localizedDescription
|
|
||||||
} else if let self = self as? AttachmentUploadError {
|
|
||||||
return self.localizedDescription
|
|
||||||
} else {
|
|
||||||
return "Unknown Error"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
extension Client.Error: PostError {}
|
|
||||||
|
|
||||||
fileprivate struct AttachmentUploadError: PostError {
|
|
||||||
let errors: [Error?]
|
let errors: [Error?]
|
||||||
|
|
||||||
var localizedDescription: String {
|
var localizedDescription: String {
|
||||||
|
|
|
@ -8,6 +8,7 @@
|
||||||
|
|
||||||
import UIKit
|
import UIKit
|
||||||
import AuthenticationServices
|
import AuthenticationServices
|
||||||
|
import Pachyderm
|
||||||
|
|
||||||
protocol OnboardingViewControllerDelegate {
|
protocol OnboardingViewControllerDelegate {
|
||||||
func didFinishOnboarding(account: LocalData.UserAccountInfo)
|
func didFinishOnboarding(account: LocalData.UserAccountInfo)
|
||||||
|
@ -40,54 +41,20 @@ class OnboardingViewController: UINavigationController {
|
||||||
|
|
||||||
instanceSelector.delegate = self
|
instanceSelector.delegate = self
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
private func doAuthenticationSession(url: URL) async throws -> URL {
|
||||||
|
return try await withCheckedThrowingContinuation { continuation in
|
||||||
extension OnboardingViewController: InstanceSelectorTableViewControllerDelegate {
|
self.authenticationSession = ASWebAuthenticationSession(url: url, callbackURLScheme: "tusker") { url, error in
|
||||||
func didSelectInstance(url instanceURL: URL) {
|
defer {
|
||||||
let mastodonController = MastodonController(instanceURL: instanceURL)
|
DispatchQueue.main.async {
|
||||||
mastodonController.registerApp { (clientID, clientSecret) in
|
self.authenticationSession = nil
|
||||||
|
|
||||||
var components = URLComponents(url: instanceURL, resolvingAgainstBaseURL: false)!
|
|
||||||
components.path = "/oauth/authorize"
|
|
||||||
components.queryItems = [
|
|
||||||
URLQueryItem(name: "client_id", value: clientID),
|
|
||||||
URLQueryItem(name: "response_type", value: "code"),
|
|
||||||
URLQueryItem(name: "scope", value: "read write follow"),
|
|
||||||
URLQueryItem(name: "redirect_uri", value: "tusker://oauth")
|
|
||||||
]
|
|
||||||
let authorizeURL = components.url!
|
|
||||||
|
|
||||||
self.authenticationSession = ASWebAuthenticationSession(url: authorizeURL, callbackURLScheme: "tusker") { url, error in
|
|
||||||
guard error == nil,
|
|
||||||
let url = url,
|
|
||||||
let components = URLComponents(url: url, resolvingAgainstBaseURL: true),
|
|
||||||
let item = components.queryItems?.first(where: { $0.name == "code" }),
|
|
||||||
let authCode = item.value else { return }
|
|
||||||
|
|
||||||
mastodonController.authorize(authorizationCode: authCode) { (accessToken) in
|
|
||||||
// construct a temporary UserAccountInfo instance for the MastodonController to use to fetch it's own account
|
|
||||||
let tempAccountInfo = LocalData.UserAccountInfo(id: "temp", instanceURL: instanceURL, clientID: clientID, clientSecret: clientSecret, username: nil, accessToken: accessToken)
|
|
||||||
mastodonController.accountInfo = tempAccountInfo
|
|
||||||
|
|
||||||
mastodonController.getOwnAccount { (result) in
|
|
||||||
DispatchQueue.main.async {
|
|
||||||
switch result {
|
|
||||||
case let .failure(error):
|
|
||||||
let alert = UIAlertController(title: "Unable to Verify Credentials", message: "Your account could not be fetched at this time: \(error.localizedDescription)", preferredStyle: .alert)
|
|
||||||
alert.addAction(UIAlertAction(title: "Ok", style: .default, handler: nil))
|
|
||||||
self.present(alert, animated: true)
|
|
||||||
|
|
||||||
case let .success(account):
|
|
||||||
// this needs to happen on the main thread because it publishes a new value for the ObservableObject
|
|
||||||
let accountInfo = LocalData.shared.addAccount(instanceURL: instanceURL, clientID: clientID, clientSecret: clientSecret, username: account.username, accessToken: accessToken)
|
|
||||||
mastodonController.accountInfo = accountInfo
|
|
||||||
|
|
||||||
self.onboardingDelegate?.didFinishOnboarding(account: accountInfo)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if let url = url {
|
||||||
|
continuation.resume(returning: url)
|
||||||
|
} else {
|
||||||
|
continuation.resume(throwing: error!)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
DispatchQueue.main.async {
|
DispatchQueue.main.async {
|
||||||
// Prefer ephemeral sessions to make it easier to sign into multiple accounts on the same instance.
|
// Prefer ephemeral sessions to make it easier to sign into multiple accounts on the same instance.
|
||||||
|
@ -97,6 +64,53 @@ extension OnboardingViewController: InstanceSelectorTableViewControllerDelegate
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
extension OnboardingViewController: InstanceSelectorTableViewControllerDelegate {
|
||||||
|
func didSelectInstance(url instanceURL: URL) {
|
||||||
|
async {
|
||||||
|
let mastodonController = MastodonController(instanceURL: instanceURL)
|
||||||
|
let (clientID, clientSecret) = await mastodonController.registerApp()
|
||||||
|
|
||||||
|
var components = URLComponents(url: instanceURL, resolvingAgainstBaseURL: false)!
|
||||||
|
components.path = "/oauth/authorize"
|
||||||
|
components.queryItems = [
|
||||||
|
URLQueryItem(name: "client_id", value: clientID),
|
||||||
|
URLQueryItem(name: "response_type", value: "code"),
|
||||||
|
URLQueryItem(name: "scope", value: "read write follow"),
|
||||||
|
URLQueryItem(name: "redirect_uri", value: "tusker://oauth")
|
||||||
|
]
|
||||||
|
|
||||||
|
guard let url = try? await self.doAuthenticationSession(url: components.url!),
|
||||||
|
let components = URLComponents(url: url, resolvingAgainstBaseURL: true),
|
||||||
|
let item = components.queryItems?.first(where: { $0.name == "code" }),
|
||||||
|
let authCode = item.value else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
let accessToken = await mastodonController.authorize(authorizationCode: authCode)
|
||||||
|
|
||||||
|
// construct a temporary UserAccountInfo instance for the MastodonController to use to fetch its own account
|
||||||
|
let tempAccountInfo = LocalData.UserAccountInfo(id: "temp", instanceURL: instanceURL, clientID: clientID, clientSecret: clientSecret, username: nil, accessToken: accessToken)
|
||||||
|
mastodonController.accountInfo = tempAccountInfo
|
||||||
|
|
||||||
|
let account: Account
|
||||||
|
do {
|
||||||
|
account = try await mastodonController.getOwnAccount()
|
||||||
|
} catch {
|
||||||
|
let alert = UIAlertController(title: "Unable to Verify Credentials", message: "Your account fcould not be fetcheda this time: \(error.localizedDescription)", preferredStyle: .alert)
|
||||||
|
alert.addAction(UIAlertAction(title: "Ok", style: .default, handler: nil))
|
||||||
|
self.present(alert, animated: true)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
let accountInfo = LocalData.shared.addAccount(instanceURL: instanceURL, clientID: clientID, clientSecret: clientSecret, username: account.username, accessToken: accessToken)
|
||||||
|
mastodonController.accountInfo = accountInfo
|
||||||
|
|
||||||
|
self.onboardingDelegate?.didFinishOnboarding(account: accountInfo)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
extension OnboardingViewController: ASWebAuthenticationPresentationContextProviding {
|
extension OnboardingViewController: ASWebAuthenticationPresentationContextProviding {
|
||||||
|
|
Loading…
Reference in New Issue