mirror of
https://github.com/metabolist/metatext.git
synced 2024-11-23 08:40:59 +00:00
Import and refactor stuff from old UIKit project
This commit is contained in:
parent
db27aaa206
commit
f384c659df
61 changed files with 2532 additions and 293 deletions
1
.gitignore
vendored
1
.gitignore
vendored
|
@ -1 +1,2 @@
|
||||||
xcuserdata/
|
xcuserdata/
|
||||||
|
*.xcodeproj/xcuserdata
|
||||||
|
|
2
.swiftlint.yml
Normal file
2
.swiftlint.yml
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
disabled_rules:
|
||||||
|
identifier_name
|
19
Development Assets/FakeKeychain.swift
Normal file
19
Development Assets/FakeKeychain.swift
Normal file
|
@ -0,0 +1,19 @@
|
||||||
|
// Copyright © 2020 Metabolist. All rights reserved.
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
|
||||||
|
typealias FakeKeychain = [String: Data]
|
||||||
|
|
||||||
|
extension FakeKeychain: KeychainType {
|
||||||
|
mutating func set(data: Data, forKey key: String) throws {
|
||||||
|
self[key] = data
|
||||||
|
}
|
||||||
|
|
||||||
|
mutating func deleteData(key: String) throws {
|
||||||
|
self[key] = nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func getData(key: String) throws -> Data? {
|
||||||
|
self[key]
|
||||||
|
}
|
||||||
|
}
|
16
Development Assets/HTTPStubs.swift
Normal file
16
Development Assets/HTTPStubs.swift
Normal file
|
@ -0,0 +1,16 @@
|
||||||
|
// Copyright © 2020 Metabolist. All rights reserved.
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
|
||||||
|
struct HTTPStubs {
|
||||||
|
static func stub(
|
||||||
|
request: URLRequest,
|
||||||
|
target: HTTPTarget? = nil,
|
||||||
|
userInfo: [String: Any] = [:]) -> HTTPStub? {
|
||||||
|
guard let url = request.url else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return (target as? Stubbing)?.stub(url: url)
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,19 @@
|
||||||
|
// Copyright © 2020 Metabolist. All rights reserved.
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
|
||||||
|
extension AccessTokenEndpoint: Stubbing {
|
||||||
|
func dataString(url: URL) -> String? {
|
||||||
|
switch self {
|
||||||
|
case let .oauthToken(_, _, _, _, scopes, _):
|
||||||
|
return """
|
||||||
|
{
|
||||||
|
"access_token": "ACCESS_TOKEN_STUB_VALUE",
|
||||||
|
"token_type": "Bearer",
|
||||||
|
"scope": "\(scopes)",
|
||||||
|
"created_at": "\(Int(Date().timeIntervalSince1970))"
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,22 @@
|
||||||
|
// Copyright © 2020 Metabolist. All rights reserved.
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
|
||||||
|
extension AppAuthorizationEndpoint: Stubbing {
|
||||||
|
func dataString(url: URL) -> String? {
|
||||||
|
switch self {
|
||||||
|
case let .apps(clientName, redirectURI, _, _):
|
||||||
|
return """
|
||||||
|
{
|
||||||
|
"id": "\(Int.random(in: 100000...999999))",
|
||||||
|
"name": "\(clientName)",
|
||||||
|
"website": null,
|
||||||
|
"redirect_uri": "\(redirectURI)",
|
||||||
|
"client_id": "AUTHORIZATION_CLIENT_ID_STUB_VALUE",
|
||||||
|
"client_secret": "AUTHORIZATION_CLIENT_SECRET_STUB_VALUE",
|
||||||
|
"vapid_key": "AUTHORIZATION_VAPID_KEY_STUB_VALUE"
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,21 @@
|
||||||
|
// Copyright © 2020 Metabolist. All rights reserved.
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
|
||||||
|
extension MastodonTarget: Stubbing {
|
||||||
|
func stub(url: URL) -> HTTPStub? {
|
||||||
|
(endpoint as? Stubbing)?.stub(url: url)
|
||||||
|
}
|
||||||
|
|
||||||
|
func data(url: URL) -> Data? {
|
||||||
|
(endpoint as? Stubbing)?.data(url: url)
|
||||||
|
}
|
||||||
|
|
||||||
|
func dataString(url: URL) -> String? {
|
||||||
|
(endpoint as? Stubbing)?.dataString(url: url)
|
||||||
|
}
|
||||||
|
|
||||||
|
func statusCode(url: URL) -> Int? {
|
||||||
|
(endpoint as? Stubbing)?.statusCode(url: url)
|
||||||
|
}
|
||||||
|
}
|
34
Development Assets/Stubbing.swift
Normal file
34
Development Assets/Stubbing.swift
Normal file
|
@ -0,0 +1,34 @@
|
||||||
|
// Copyright © 2020 Metabolist. All rights reserved.
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
|
||||||
|
typealias HTTPStub = Result<(URLResponse, Data), Error>
|
||||||
|
|
||||||
|
protocol Stubbing {
|
||||||
|
func stub(url: URL) -> HTTPStub?
|
||||||
|
func data(url: URL) -> Data?
|
||||||
|
func dataString(url: URL) -> String?
|
||||||
|
func statusCode(url: URL) -> Int?
|
||||||
|
}
|
||||||
|
|
||||||
|
extension Stubbing {
|
||||||
|
func stub(url: URL) -> HTTPStub? {
|
||||||
|
if let data = data(url: url),
|
||||||
|
let statusCode = statusCode(url: url),
|
||||||
|
let response = HTTPURLResponse(
|
||||||
|
url: url,
|
||||||
|
statusCode: statusCode,
|
||||||
|
httpVersion: nil,
|
||||||
|
headerFields: nil) {
|
||||||
|
return .success((response, data))
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func data(url: URL) -> Data? {
|
||||||
|
dataString(url: url)?.data(using: .utf8)
|
||||||
|
}
|
||||||
|
|
||||||
|
func statusCode(url: URL) -> Int? { 200 }
|
||||||
|
}
|
42
Development Assets/StubbingURLProtocol.swift
Normal file
42
Development Assets/StubbingURLProtocol.swift
Normal file
|
@ -0,0 +1,42 @@
|
||||||
|
// Copyright © 2020 Metabolist. All rights reserved.
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
|
||||||
|
class StubbingURLProtocol: URLProtocol {
|
||||||
|
private static var targetsForURLs = [URL: HTTPTarget]()
|
||||||
|
|
||||||
|
class func setTarget(_ target: HTTPTarget, forURL url: URL) {
|
||||||
|
targetsForURLs[url] = target
|
||||||
|
}
|
||||||
|
|
||||||
|
override class func canInit(with task: URLSessionTask) -> Bool {
|
||||||
|
true
|
||||||
|
}
|
||||||
|
|
||||||
|
override class func canInit(with request: URLRequest) -> Bool {
|
||||||
|
true
|
||||||
|
}
|
||||||
|
|
||||||
|
override class func canonicalRequest(for request: URLRequest) -> URLRequest {
|
||||||
|
request
|
||||||
|
}
|
||||||
|
|
||||||
|
override func startLoading() {
|
||||||
|
guard
|
||||||
|
let url = request.url,
|
||||||
|
let stub = HTTPStubs.stub(request: request, target: Self.targetsForURLs[url]) else {
|
||||||
|
preconditionFailure("Stub for request not found")
|
||||||
|
}
|
||||||
|
|
||||||
|
switch stub {
|
||||||
|
case let .success((response, data)):
|
||||||
|
client?.urlProtocol(self, didReceive: response, cacheStoragePolicy: .allowed)
|
||||||
|
client?.urlProtocol(self, didLoad: data)
|
||||||
|
client?.urlProtocolDidFinishLoading(self)
|
||||||
|
case let .failure(error):
|
||||||
|
client?.urlProtocol(self, didFailWithError: error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override func stopLoading() {}
|
||||||
|
}
|
71
Development Assets/StubbingWebAuthenticationSession.swift
Normal file
71
Development Assets/StubbingWebAuthenticationSession.swift
Normal file
|
@ -0,0 +1,71 @@
|
||||||
|
// Copyright © 2020 Metabolist. All rights reserved.
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
import AuthenticationServices
|
||||||
|
|
||||||
|
class StubbingWebAuthenticationSession: WebAuthenticationSessionType {
|
||||||
|
let completionHandler: ASWebAuthenticationSession.CompletionHandler
|
||||||
|
let url: URL
|
||||||
|
let callbackURLScheme: String?
|
||||||
|
var presentationContextProvider: ASWebAuthenticationPresentationContextProviding?
|
||||||
|
|
||||||
|
required init(
|
||||||
|
url URL: URL,
|
||||||
|
callbackURLScheme: String?,
|
||||||
|
completionHandler: @escaping ASWebAuthenticationSession.CompletionHandler) {
|
||||||
|
self.url = URL
|
||||||
|
self.callbackURLScheme = callbackURLScheme
|
||||||
|
self.completionHandler = completionHandler
|
||||||
|
}
|
||||||
|
|
||||||
|
func start() -> Bool {
|
||||||
|
completionHandler(completionHandlerURL, completionHandlerError)
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
var completionHandlerURL: URL? {
|
||||||
|
nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var completionHandlerError: Error? {
|
||||||
|
nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// swiftlint:disable type_name
|
||||||
|
class SuccessfulStubbingWebAuthenticationSession: StubbingWebAuthenticationSession {
|
||||||
|
// swiftlint:enable type_name
|
||||||
|
private let redirectURL: URL
|
||||||
|
|
||||||
|
required init(
|
||||||
|
url URL: URL,
|
||||||
|
callbackURLScheme: String?,
|
||||||
|
completionHandler: @escaping ASWebAuthenticationSession.CompletionHandler) {
|
||||||
|
redirectURL = Foundation.URL(
|
||||||
|
string: URLComponents(url: URL, resolvingAgainstBaseURL: true)!
|
||||||
|
.queryItems!.first(where: { $0.name == "redirect_uri" })!.value!)!
|
||||||
|
super.init(
|
||||||
|
url: URL,
|
||||||
|
callbackURLScheme: callbackURLScheme,
|
||||||
|
completionHandler: completionHandler)
|
||||||
|
}
|
||||||
|
|
||||||
|
override var completionHandlerURL: URL? {
|
||||||
|
var components = URLComponents(url: redirectURL, resolvingAgainstBaseURL: true)!
|
||||||
|
var queryItems = components.queryItems ?? []
|
||||||
|
|
||||||
|
queryItems.append(URLQueryItem(name: "code", value: UUID().uuidString))
|
||||||
|
components.queryItems = queryItems
|
||||||
|
|
||||||
|
return components.url
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// swiftlint:disable type_name
|
||||||
|
class CanceledLoginStubbingWebAuthenticationSession: StubbingWebAuthenticationSession {
|
||||||
|
// swiftlint:enable type_name
|
||||||
|
override var completionHandlerError: Error? {
|
||||||
|
ASWebAuthenticationSessionError(.canceledLogin)
|
||||||
|
}
|
||||||
|
}
|
13
Development Assets/URLSessionConfiguration+Extensions.swift
Normal file
13
Development Assets/URLSessionConfiguration+Extensions.swift
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
// Copyright © 2020 Metabolist. All rights reserved.
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
|
||||||
|
extension URLSessionConfiguration {
|
||||||
|
static var stubbing: URLSessionConfiguration {
|
||||||
|
let configuration = Self.default
|
||||||
|
|
||||||
|
configuration.protocolClasses = [StubbingURLProtocol.self]
|
||||||
|
|
||||||
|
return configuration
|
||||||
|
}
|
||||||
|
}
|
|
@ -3,52 +3,185 @@
|
||||||
archiveVersion = 1;
|
archiveVersion = 1;
|
||||||
classes = {
|
classes = {
|
||||||
};
|
};
|
||||||
objectVersion = 50;
|
objectVersion = 52;
|
||||||
objects = {
|
objects = {
|
||||||
|
|
||||||
/* Begin PBXBuildFile section */
|
/* Begin PBXBuildFile section */
|
||||||
D047FAA124C3E21200AF17C5 /* Tests_iOS.swift in Sources */ = {isa = PBXBuildFile; fileRef = D047FAA024C3E21200AF17C5 /* Tests_iOS.swift */; };
|
|
||||||
D047FAAC24C3E21200AF17C5 /* Tests_macOS.swift in Sources */ = {isa = PBXBuildFile; fileRef = D047FAAB24C3E21200AF17C5 /* Tests_macOS.swift */; };
|
|
||||||
D047FAAE24C3E21200AF17C5 /* MetatextApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = D047FA8524C3E21000AF17C5 /* MetatextApp.swift */; };
|
D047FAAE24C3E21200AF17C5 /* MetatextApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = D047FA8524C3E21000AF17C5 /* MetatextApp.swift */; };
|
||||||
D047FAAF24C3E21200AF17C5 /* MetatextApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = D047FA8524C3E21000AF17C5 /* MetatextApp.swift */; };
|
D047FAAF24C3E21200AF17C5 /* MetatextApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = D047FA8524C3E21000AF17C5 /* MetatextApp.swift */; };
|
||||||
D047FAB024C3E21200AF17C5 /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D047FA8624C3E21000AF17C5 /* ContentView.swift */; };
|
|
||||||
D047FAB124C3E21200AF17C5 /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D047FA8624C3E21000AF17C5 /* ContentView.swift */; };
|
|
||||||
D047FAB224C3E21200AF17C5 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = D047FA8724C3E21200AF17C5 /* Assets.xcassets */; };
|
D047FAB224C3E21200AF17C5 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = D047FA8724C3E21200AF17C5 /* Assets.xcassets */; };
|
||||||
D047FAB324C3E21200AF17C5 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = D047FA8724C3E21200AF17C5 /* Assets.xcassets */; };
|
D047FAB324C3E21200AF17C5 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = D047FA8724C3E21200AF17C5 /* Assets.xcassets */; };
|
||||||
|
D065F53924D37E5100741304 /* CombineExpectations in Frameworks */ = {isa = PBXBuildFile; productRef = D065F53824D37E5100741304 /* CombineExpectations */; };
|
||||||
|
D065F53B24D3B33A00741304 /* View+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = D065F53A24D3B33A00741304 /* View+Extensions.swift */; };
|
||||||
|
D065F53C24D3B33A00741304 /* View+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = D065F53A24D3B33A00741304 /* View+Extensions.swift */; };
|
||||||
|
D065F53E24D3D20300741304 /* InstanceEndpoint.swift in Sources */ = {isa = PBXBuildFile; fileRef = D065F53D24D3D20300741304 /* InstanceEndpoint.swift */; };
|
||||||
|
D065F53F24D3D20300741304 /* InstanceEndpoint.swift in Sources */ = {isa = PBXBuildFile; fileRef = D065F53D24D3D20300741304 /* InstanceEndpoint.swift */; };
|
||||||
|
D0666A4224C6BB7B00F3F04B /* IdentityDatabase.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0666A4124C6BB7B00F3F04B /* IdentityDatabase.swift */; };
|
||||||
|
D0666A4324C6BB7B00F3F04B /* IdentityDatabase.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0666A4124C6BB7B00F3F04B /* IdentityDatabase.swift */; };
|
||||||
|
D0666A4524C6BC0A00F3F04B /* DatabaseError.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0666A4424C6BC0A00F3F04B /* DatabaseError.swift */; };
|
||||||
|
D0666A4624C6BC0A00F3F04B /* DatabaseError.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0666A4424C6BC0A00F3F04B /* DatabaseError.swift */; };
|
||||||
|
D0666A4924C6C1A300F3F04B /* GRDB in Frameworks */ = {isa = PBXBuildFile; productRef = D0666A4824C6C1A300F3F04B /* GRDB */; };
|
||||||
|
D0666A4B24C6C37700F3F04B /* Identity.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0666A4A24C6C37700F3F04B /* Identity.swift */; };
|
||||||
|
D0666A4C24C6C37700F3F04B /* Identity.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0666A4A24C6C37700F3F04B /* Identity.swift */; };
|
||||||
|
D0666A4E24C6C39600F3F04B /* Instance.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0666A4D24C6C39600F3F04B /* Instance.swift */; };
|
||||||
|
D0666A4F24C6C39600F3F04B /* Instance.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0666A4D24C6C39600F3F04B /* Instance.swift */; };
|
||||||
|
D0666A5124C6C3BC00F3F04B /* Account.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0666A5024C6C3BC00F3F04B /* Account.swift */; };
|
||||||
|
D0666A5224C6C3BC00F3F04B /* Account.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0666A5024C6C3BC00F3F04B /* Account.swift */; };
|
||||||
|
D0666A5424C6C3E500F3F04B /* Emoji.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0666A5324C6C3E500F3F04B /* Emoji.swift */; };
|
||||||
|
D0666A5524C6C3E500F3F04B /* Emoji.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0666A5324C6C3E500F3F04B /* Emoji.swift */; };
|
||||||
|
D0666A5724C6C63400F3F04B /* MastodonDecoder.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0666A5624C6C63400F3F04B /* MastodonDecoder.swift */; };
|
||||||
|
D0666A5824C6C63400F3F04B /* MastodonDecoder.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0666A5624C6C63400F3F04B /* MastodonDecoder.swift */; };
|
||||||
|
D0666A5A24C6C64100F3F04B /* MastodonEncoder.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0666A5924C6C64100F3F04B /* MastodonEncoder.swift */; };
|
||||||
|
D0666A5B24C6C64100F3F04B /* MastodonEncoder.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0666A5924C6C64100F3F04B /* MastodonEncoder.swift */; };
|
||||||
|
D0666A6324C6DC6C00F3F04B /* AppAuthorization.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0666A6224C6DC6C00F3F04B /* AppAuthorization.swift */; };
|
||||||
|
D0666A6424C6DC6C00F3F04B /* AppAuthorization.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0666A6224C6DC6C00F3F04B /* AppAuthorization.swift */; };
|
||||||
|
D0666A6F24C6DFB300F3F04B /* AccessToken.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0666A6E24C6DFB300F3F04B /* AccessToken.swift */; };
|
||||||
|
D0666A7024C6DFB300F3F04B /* AccessToken.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0666A6E24C6DFB300F3F04B /* AccessToken.swift */; };
|
||||||
|
D0666A7224C6E0D300F3F04B /* Secrets.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0666A7124C6E0D300F3F04B /* Secrets.swift */; };
|
||||||
|
D0666A7324C6E0D300F3F04B /* Secrets.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0666A7124C6E0D300F3F04B /* Secrets.swift */; };
|
||||||
|
D0666A7D24C7745A00F3F04B /* GRDB in Frameworks */ = {isa = PBXBuildFile; productRef = D0666A7C24C7745A00F3F04B /* GRDB */; };
|
||||||
|
D06B491F24D3F7FE00642749 /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = D06B491E24D3F7FE00642749 /* Localizable.strings */; };
|
||||||
|
D06B492024D3FB8000642749 /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = D06B491E24D3F7FE00642749 /* Localizable.strings */; };
|
||||||
|
D074577724D29006004758DB /* StubbingWebAuthenticationSession.swift in Sources */ = {isa = PBXBuildFile; fileRef = D074577624D29006004758DB /* StubbingWebAuthenticationSession.swift */; };
|
||||||
|
D074577824D29006004758DB /* StubbingWebAuthenticationSession.swift in Sources */ = {isa = PBXBuildFile; fileRef = D074577624D29006004758DB /* StubbingWebAuthenticationSession.swift */; };
|
||||||
|
D074577A24D29366004758DB /* URLSessionConfiguration+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = D074577924D29366004758DB /* URLSessionConfiguration+Extensions.swift */; };
|
||||||
|
D074577B24D29366004758DB /* URLSessionConfiguration+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = D074577924D29366004758DB /* URLSessionConfiguration+Extensions.swift */; };
|
||||||
|
D081A40524D0F1A8001B016E /* String+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = D081A40424D0F1A8001B016E /* String+Extensions.swift */; };
|
||||||
|
D081A40624D0F1A8001B016E /* String+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = D081A40424D0F1A8001B016E /* String+Extensions.swift */; };
|
||||||
|
D0B23F0D24D210E90066F411 /* NSError+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0B23F0C24D210E90066F411 /* NSError+Extensions.swift */; };
|
||||||
|
D0B23F0E24D210E90066F411 /* NSError+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0B23F0C24D210E90066F411 /* NSError+Extensions.swift */; };
|
||||||
|
D0BEC93824C9632800E864C4 /* SceneViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0BEC93724C9632800E864C4 /* SceneViewModel.swift */; };
|
||||||
|
D0BEC93924C9632800E864C4 /* SceneViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0BEC93724C9632800E864C4 /* SceneViewModel.swift */; };
|
||||||
|
D0BEC93B24C96FD500E864C4 /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0BEC93A24C96FD500E864C4 /* ContentView.swift */; };
|
||||||
|
D0BEC93C24C96FD500E864C4 /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0BEC93A24C96FD500E864C4 /* ContentView.swift */; };
|
||||||
|
D0BEC94724CA22C400E864C4 /* TimelineViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0BEC94624CA22C400E864C4 /* TimelineViewModel.swift */; };
|
||||||
|
D0BEC94824CA22C400E864C4 /* TimelineViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0BEC94624CA22C400E864C4 /* TimelineViewModel.swift */; };
|
||||||
|
D0BEC94A24CA231200E864C4 /* TimelineView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0BEC94924CA231200E864C4 /* TimelineView.swift */; };
|
||||||
|
D0BEC94B24CA231200E864C4 /* TimelineView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0BEC94924CA231200E864C4 /* TimelineView.swift */; };
|
||||||
|
D0BEC94F24CA2B5300E864C4 /* SidebarNavigation.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0BEC94E24CA2B5300E864C4 /* SidebarNavigation.swift */; };
|
||||||
|
D0BEC95124CA2B7E00E864C4 /* TabNavigation.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0BEC95024CA2B7E00E864C4 /* TabNavigation.swift */; };
|
||||||
|
D0C963FB24CC359D003BD330 /* AlertItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0C963FA24CC359D003BD330 /* AlertItem.swift */; };
|
||||||
|
D0C963FC24CC359D003BD330 /* AlertItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0C963FA24CC359D003BD330 /* AlertItem.swift */; };
|
||||||
|
D0C963FE24CC3812003BD330 /* Publisher+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0C963FD24CC3812003BD330 /* Publisher+Extensions.swift */; };
|
||||||
|
D0C963FF24CC3812003BD330 /* Publisher+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0C963FD24CC3812003BD330 /* Publisher+Extensions.swift */; };
|
||||||
|
D0DB6EF424C5228A00D965FE /* AddIdentityView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0DB6EF324C5228A00D965FE /* AddIdentityView.swift */; };
|
||||||
|
D0DB6EF524C5233E00D965FE /* AddIdentityView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0DB6EF324C5228A00D965FE /* AddIdentityView.swift */; };
|
||||||
|
D0DB6F0924C65AC000D965FE /* AddIdentityViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0DB6F0824C65AC000D965FE /* AddIdentityViewModel.swift */; };
|
||||||
|
D0DB6F0A24C65AC000D965FE /* AddIdentityViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0DB6F0824C65AC000D965FE /* AddIdentityViewModel.swift */; };
|
||||||
|
D0DC174624CFEC2000A75C65 /* StubbingURLProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0DC174524CFEC2000A75C65 /* StubbingURLProtocol.swift */; };
|
||||||
|
D0DC174724CFEC2000A75C65 /* StubbingURLProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0DC174524CFEC2000A75C65 /* StubbingURLProtocol.swift */; };
|
||||||
|
D0DC174A24CFF15F00A75C65 /* AppAuthorizationEndpoint+Stubbing.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0DC174924CFF15F00A75C65 /* AppAuthorizationEndpoint+Stubbing.swift */; };
|
||||||
|
D0DC174B24CFF15F00A75C65 /* AppAuthorizationEndpoint+Stubbing.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0DC174924CFF15F00A75C65 /* AppAuthorizationEndpoint+Stubbing.swift */; };
|
||||||
|
D0DC174D24CFF1F100A75C65 /* Stubbing.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0DC174C24CFF1F100A75C65 /* Stubbing.swift */; };
|
||||||
|
D0DC174E24CFF1F100A75C65 /* Stubbing.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0DC174C24CFF1F100A75C65 /* Stubbing.swift */; };
|
||||||
|
D0DC175224D008E300A75C65 /* MastodonTarget+Stubbing.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0DC175124D008E300A75C65 /* MastodonTarget+Stubbing.swift */; };
|
||||||
|
D0DC175324D008E300A75C65 /* MastodonTarget+Stubbing.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0DC175124D008E300A75C65 /* MastodonTarget+Stubbing.swift */; };
|
||||||
|
D0DC175524D00F0A00A75C65 /* AccessTokenEndpoint+Stubbing.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0DC175424D00F0A00A75C65 /* AccessTokenEndpoint+Stubbing.swift */; };
|
||||||
|
D0DC175624D00F0A00A75C65 /* AccessTokenEndpoint+Stubbing.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0DC175424D00F0A00A75C65 /* AccessTokenEndpoint+Stubbing.swift */; };
|
||||||
|
D0DC175824D0130800A75C65 /* HTTPStubs.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0DC175724D0130800A75C65 /* HTTPStubs.swift */; };
|
||||||
|
D0DC175924D0130800A75C65 /* HTTPStubs.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0DC175724D0130800A75C65 /* HTTPStubs.swift */; };
|
||||||
|
D0DC175B24D0154F00A75C65 /* MastodonAPI.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0DC175A24D0154F00A75C65 /* MastodonAPI.swift */; };
|
||||||
|
D0DC175C24D0154F00A75C65 /* MastodonAPI.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0DC175A24D0154F00A75C65 /* MastodonAPI.swift */; };
|
||||||
|
D0DC175F24D016EA00A75C65 /* Alamofire in Frameworks */ = {isa = PBXBuildFile; productRef = D0DC175E24D016EA00A75C65 /* Alamofire */; };
|
||||||
|
D0DC176124D0171800A75C65 /* Alamofire in Frameworks */ = {isa = PBXBuildFile; productRef = D0DC176024D0171800A75C65 /* Alamofire */; };
|
||||||
|
D0DC177424D0B58800A75C65 /* Keychain.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0DC177324D0B58800A75C65 /* Keychain.swift */; };
|
||||||
|
D0DC177524D0B58800A75C65 /* Keychain.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0DC177324D0B58800A75C65 /* Keychain.swift */; };
|
||||||
|
D0DC177724D0CF2600A75C65 /* FakeKeychain.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0DC177624D0CF2600A75C65 /* FakeKeychain.swift */; };
|
||||||
|
D0DC177824D0CF2600A75C65 /* FakeKeychain.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0DC177624D0CF2600A75C65 /* FakeKeychain.swift */; };
|
||||||
|
D0ED1B6E24CE100C00B4899C /* AddIdentityViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0ED1B6D24CE100C00B4899C /* AddIdentityViewModelTests.swift */; };
|
||||||
|
D0ED1BB724CE47F400B4899C /* WebAuthenticationSession.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0ED1BB624CE47F400B4899C /* WebAuthenticationSession.swift */; };
|
||||||
|
D0ED1BB824CE47F400B4899C /* WebAuthenticationSession.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0ED1BB624CE47F400B4899C /* WebAuthenticationSession.swift */; };
|
||||||
|
D0ED1BC124CED48800B4899C /* HTTPClient.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0ED1BC024CED48800B4899C /* HTTPClient.swift */; };
|
||||||
|
D0ED1BC224CED48800B4899C /* HTTPClient.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0ED1BC024CED48800B4899C /* HTTPClient.swift */; };
|
||||||
|
D0ED1BC424CED54D00B4899C /* HTTPTarget.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0ED1BC324CED54D00B4899C /* HTTPTarget.swift */; };
|
||||||
|
D0ED1BC524CED54D00B4899C /* HTTPTarget.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0ED1BC324CED54D00B4899C /* HTTPTarget.swift */; };
|
||||||
|
D0ED1BCB24CF744200B4899C /* MastodonClient.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0ED1BCA24CF744200B4899C /* MastodonClient.swift */; };
|
||||||
|
D0ED1BCC24CF744200B4899C /* MastodonClient.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0ED1BCA24CF744200B4899C /* MastodonClient.swift */; };
|
||||||
|
D0ED1BCE24CF768200B4899C /* MastodonEndpoint.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0ED1BCD24CF768200B4899C /* MastodonEndpoint.swift */; };
|
||||||
|
D0ED1BCF24CF768200B4899C /* MastodonEndpoint.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0ED1BCD24CF768200B4899C /* MastodonEndpoint.swift */; };
|
||||||
|
D0ED1BD124CF779B00B4899C /* MastodonTarget.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0ED1BD024CF779B00B4899C /* MastodonTarget.swift */; };
|
||||||
|
D0ED1BD224CF779B00B4899C /* MastodonTarget.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0ED1BD024CF779B00B4899C /* MastodonTarget.swift */; };
|
||||||
|
D0ED1BD724CF94B200B4899C /* Application.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0ED1BD624CF94B200B4899C /* Application.swift */; };
|
||||||
|
D0ED1BD824CF94B200B4899C /* Application.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0ED1BD624CF94B200B4899C /* Application.swift */; };
|
||||||
|
D0ED1BDA24CF963E00B4899C /* AppAuthorizationEndpoint.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0ED1BD924CF963E00B4899C /* AppAuthorizationEndpoint.swift */; };
|
||||||
|
D0ED1BDB24CF963E00B4899C /* AppAuthorizationEndpoint.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0ED1BD924CF963E00B4899C /* AppAuthorizationEndpoint.swift */; };
|
||||||
|
D0ED1BDD24CF982600B4899C /* AccessTokenEndpoint.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0ED1BDC24CF982600B4899C /* AccessTokenEndpoint.swift */; };
|
||||||
|
D0ED1BDE24CF982600B4899C /* AccessTokenEndpoint.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0ED1BDC24CF982600B4899C /* AccessTokenEndpoint.swift */; };
|
||||||
|
D0ED1BE024CF98FB00B4899C /* AccountEndpoint.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0ED1BDF24CF98FB00B4899C /* AccountEndpoint.swift */; };
|
||||||
|
D0ED1BE124CF98FB00B4899C /* AccountEndpoint.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0ED1BDF24CF98FB00B4899C /* AccountEndpoint.swift */; };
|
||||||
|
D0ED1BE324CFA84400B4899C /* MastodonError.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0ED1BE224CFA84400B4899C /* MastodonError.swift */; };
|
||||||
|
D0ED1BE424CFA84400B4899C /* MastodonError.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0ED1BE224CFA84400B4899C /* MastodonError.swift */; };
|
||||||
/* End PBXBuildFile section */
|
/* End PBXBuildFile section */
|
||||||
|
|
||||||
/* Begin PBXContainerItemProxy section */
|
/* Begin PBXContainerItemProxy section */
|
||||||
D047FA9D24C3E21200AF17C5 /* PBXContainerItemProxy */ = {
|
D0666A2624C677B400F3F04B /* PBXContainerItemProxy */ = {
|
||||||
isa = PBXContainerItemProxy;
|
isa = PBXContainerItemProxy;
|
||||||
containerPortal = D047FA8024C3E21000AF17C5 /* Project object */;
|
containerPortal = D047FA8024C3E21000AF17C5 /* Project object */;
|
||||||
proxyType = 1;
|
proxyType = 1;
|
||||||
remoteGlobalIDString = D047FA8B24C3E21200AF17C5;
|
remoteGlobalIDString = D047FA8B24C3E21200AF17C5;
|
||||||
remoteInfo = "Metatext (iOS)";
|
remoteInfo = "Metatext (iOS)";
|
||||||
};
|
};
|
||||||
D047FAA824C3E21200AF17C5 /* PBXContainerItemProxy */ = {
|
|
||||||
isa = PBXContainerItemProxy;
|
|
||||||
containerPortal = D047FA8024C3E21000AF17C5 /* Project object */;
|
|
||||||
proxyType = 1;
|
|
||||||
remoteGlobalIDString = D047FA9324C3E21200AF17C5;
|
|
||||||
remoteInfo = "Metatext (macOS)";
|
|
||||||
};
|
|
||||||
/* End PBXContainerItemProxy section */
|
/* End PBXContainerItemProxy section */
|
||||||
|
|
||||||
/* Begin PBXFileReference section */
|
/* Begin PBXFileReference section */
|
||||||
D047FA8524C3E21000AF17C5 /* MetatextApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MetatextApp.swift; sourceTree = "<group>"; };
|
D047FA8524C3E21000AF17C5 /* MetatextApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MetatextApp.swift; sourceTree = "<group>"; };
|
||||||
D047FA8624C3E21000AF17C5 /* ContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = "<group>"; };
|
|
||||||
D047FA8724C3E21200AF17C5 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
|
D047FA8724C3E21200AF17C5 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
|
||||||
D047FA8C24C3E21200AF17C5 /* Metatext.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Metatext.app; sourceTree = BUILT_PRODUCTS_DIR; };
|
D047FA8C24C3E21200AF17C5 /* Metatext.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Metatext.app; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||||
D047FA8F24C3E21200AF17C5 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
|
D047FA8F24C3E21200AF17C5 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
|
||||||
D047FA9424C3E21200AF17C5 /* Metatext.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Metatext.app; sourceTree = BUILT_PRODUCTS_DIR; };
|
D047FA9424C3E21200AF17C5 /* Metatext.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Metatext.app; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||||
D047FA9624C3E21200AF17C5 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
|
D047FA9624C3E21200AF17C5 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
|
||||||
D047FA9724C3E21200AF17C5 /* macOS.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = macOS.entitlements; sourceTree = "<group>"; };
|
D047FA9724C3E21200AF17C5 /* macOS.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = macOS.entitlements; sourceTree = "<group>"; };
|
||||||
D047FA9C24C3E21200AF17C5 /* Tests iOS.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = "Tests iOS.xctest"; sourceTree = BUILT_PRODUCTS_DIR; };
|
D065F53A24D3B33A00741304 /* View+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "View+Extensions.swift"; sourceTree = "<group>"; };
|
||||||
D047FAA024C3E21200AF17C5 /* Tests_iOS.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Tests_iOS.swift; sourceTree = "<group>"; };
|
D065F53D24D3D20300741304 /* InstanceEndpoint.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InstanceEndpoint.swift; sourceTree = "<group>"; };
|
||||||
D047FAA224C3E21200AF17C5 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
|
D0666A2124C677B400F3F04B /* Tests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = Tests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||||
D047FAA724C3E21200AF17C5 /* Tests macOS.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = "Tests macOS.xctest"; sourceTree = BUILT_PRODUCTS_DIR; };
|
D0666A2524C677B400F3F04B /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
|
||||||
D047FAAB24C3E21200AF17C5 /* Tests_macOS.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Tests_macOS.swift; sourceTree = "<group>"; };
|
D0666A4124C6BB7B00F3F04B /* IdentityDatabase.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IdentityDatabase.swift; sourceTree = "<group>"; };
|
||||||
D047FAAD24C3E21200AF17C5 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
|
D0666A4424C6BC0A00F3F04B /* DatabaseError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DatabaseError.swift; sourceTree = "<group>"; };
|
||||||
|
D0666A4A24C6C37700F3F04B /* Identity.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Identity.swift; sourceTree = "<group>"; };
|
||||||
|
D0666A4D24C6C39600F3F04B /* Instance.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Instance.swift; sourceTree = "<group>"; };
|
||||||
|
D0666A5024C6C3BC00F3F04B /* Account.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Account.swift; sourceTree = "<group>"; };
|
||||||
|
D0666A5324C6C3E500F3F04B /* Emoji.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Emoji.swift; sourceTree = "<group>"; };
|
||||||
|
D0666A5624C6C63400F3F04B /* MastodonDecoder.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MastodonDecoder.swift; sourceTree = "<group>"; };
|
||||||
|
D0666A5924C6C64100F3F04B /* MastodonEncoder.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MastodonEncoder.swift; sourceTree = "<group>"; };
|
||||||
|
D0666A6224C6DC6C00F3F04B /* AppAuthorization.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppAuthorization.swift; sourceTree = "<group>"; };
|
||||||
|
D0666A6E24C6DFB300F3F04B /* AccessToken.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccessToken.swift; sourceTree = "<group>"; };
|
||||||
|
D0666A7124C6E0D300F3F04B /* Secrets.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Secrets.swift; sourceTree = "<group>"; };
|
||||||
|
D06B491E24D3F7FE00642749 /* Localizable.strings */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; path = Localizable.strings; sourceTree = "<group>"; };
|
||||||
|
D074577624D29006004758DB /* StubbingWebAuthenticationSession.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = StubbingWebAuthenticationSession.swift; sourceTree = "<group>"; };
|
||||||
|
D074577924D29366004758DB /* URLSessionConfiguration+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "URLSessionConfiguration+Extensions.swift"; sourceTree = "<group>"; };
|
||||||
|
D081A40424D0F1A8001B016E /* String+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "String+Extensions.swift"; sourceTree = "<group>"; };
|
||||||
|
D0B23F0C24D210E90066F411 /* NSError+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NSError+Extensions.swift"; sourceTree = "<group>"; };
|
||||||
|
D0BEC93724C9632800E864C4 /* SceneViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SceneViewModel.swift; sourceTree = "<group>"; };
|
||||||
|
D0BEC93A24C96FD500E864C4 /* ContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = "<group>"; };
|
||||||
|
D0BEC94624CA22C400E864C4 /* TimelineViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelineViewModel.swift; sourceTree = "<group>"; };
|
||||||
|
D0BEC94924CA231200E864C4 /* TimelineView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelineView.swift; sourceTree = "<group>"; };
|
||||||
|
D0BEC94E24CA2B5300E864C4 /* SidebarNavigation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SidebarNavigation.swift; sourceTree = "<group>"; };
|
||||||
|
D0BEC95024CA2B7E00E864C4 /* TabNavigation.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TabNavigation.swift; sourceTree = "<group>"; };
|
||||||
|
D0C963FA24CC359D003BD330 /* AlertItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AlertItem.swift; sourceTree = "<group>"; };
|
||||||
|
D0C963FD24CC3812003BD330 /* Publisher+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Publisher+Extensions.swift"; sourceTree = "<group>"; };
|
||||||
|
D0DB6EF324C5228A00D965FE /* AddIdentityView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddIdentityView.swift; sourceTree = "<group>"; };
|
||||||
|
D0DB6F0824C65AC000D965FE /* AddIdentityViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddIdentityViewModel.swift; sourceTree = "<group>"; };
|
||||||
|
D0DC174524CFEC2000A75C65 /* StubbingURLProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StubbingURLProtocol.swift; sourceTree = "<group>"; };
|
||||||
|
D0DC174924CFF15F00A75C65 /* AppAuthorizationEndpoint+Stubbing.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "AppAuthorizationEndpoint+Stubbing.swift"; sourceTree = "<group>"; };
|
||||||
|
D0DC174C24CFF1F100A75C65 /* Stubbing.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Stubbing.swift; sourceTree = "<group>"; };
|
||||||
|
D0DC175124D008E300A75C65 /* MastodonTarget+Stubbing.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "MastodonTarget+Stubbing.swift"; sourceTree = "<group>"; };
|
||||||
|
D0DC175424D00F0A00A75C65 /* AccessTokenEndpoint+Stubbing.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "AccessTokenEndpoint+Stubbing.swift"; sourceTree = "<group>"; };
|
||||||
|
D0DC175724D0130800A75C65 /* HTTPStubs.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HTTPStubs.swift; sourceTree = "<group>"; };
|
||||||
|
D0DC175A24D0154F00A75C65 /* MastodonAPI.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MastodonAPI.swift; sourceTree = "<group>"; };
|
||||||
|
D0DC177324D0B58800A75C65 /* Keychain.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Keychain.swift; sourceTree = "<group>"; };
|
||||||
|
D0DC177624D0CF2600A75C65 /* FakeKeychain.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FakeKeychain.swift; sourceTree = "<group>"; };
|
||||||
|
D0ED1B6D24CE100C00B4899C /* AddIdentityViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddIdentityViewModelTests.swift; sourceTree = "<group>"; };
|
||||||
|
D0ED1BB624CE47F400B4899C /* WebAuthenticationSession.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WebAuthenticationSession.swift; sourceTree = "<group>"; };
|
||||||
|
D0ED1BC024CED48800B4899C /* HTTPClient.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HTTPClient.swift; sourceTree = "<group>"; };
|
||||||
|
D0ED1BC324CED54D00B4899C /* HTTPTarget.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HTTPTarget.swift; sourceTree = "<group>"; };
|
||||||
|
D0ED1BCA24CF744200B4899C /* MastodonClient.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MastodonClient.swift; sourceTree = "<group>"; };
|
||||||
|
D0ED1BCD24CF768200B4899C /* MastodonEndpoint.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MastodonEndpoint.swift; sourceTree = "<group>"; };
|
||||||
|
D0ED1BD024CF779B00B4899C /* MastodonTarget.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MastodonTarget.swift; sourceTree = "<group>"; };
|
||||||
|
D0ED1BD624CF94B200B4899C /* Application.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Application.swift; sourceTree = "<group>"; };
|
||||||
|
D0ED1BD924CF963E00B4899C /* AppAuthorizationEndpoint.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppAuthorizationEndpoint.swift; sourceTree = "<group>"; };
|
||||||
|
D0ED1BDC24CF982600B4899C /* AccessTokenEndpoint.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccessTokenEndpoint.swift; sourceTree = "<group>"; };
|
||||||
|
D0ED1BDF24CF98FB00B4899C /* AccountEndpoint.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountEndpoint.swift; sourceTree = "<group>"; };
|
||||||
|
D0ED1BE224CFA84400B4899C /* MastodonError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MastodonError.swift; sourceTree = "<group>"; };
|
||||||
/* End PBXFileReference section */
|
/* End PBXFileReference section */
|
||||||
|
|
||||||
/* Begin PBXFrameworksBuildPhase section */
|
/* Begin PBXFrameworksBuildPhase section */
|
||||||
|
@ -56,6 +189,8 @@
|
||||||
isa = PBXFrameworksBuildPhase;
|
isa = PBXFrameworksBuildPhase;
|
||||||
buildActionMask = 2147483647;
|
buildActionMask = 2147483647;
|
||||||
files = (
|
files = (
|
||||||
|
D0666A4924C6C1A300F3F04B /* GRDB in Frameworks */,
|
||||||
|
D0DC175F24D016EA00A75C65 /* Alamofire in Frameworks */,
|
||||||
);
|
);
|
||||||
runOnlyForDeploymentPostprocessing = 0;
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
};
|
};
|
||||||
|
@ -63,20 +198,16 @@
|
||||||
isa = PBXFrameworksBuildPhase;
|
isa = PBXFrameworksBuildPhase;
|
||||||
buildActionMask = 2147483647;
|
buildActionMask = 2147483647;
|
||||||
files = (
|
files = (
|
||||||
|
D0DC176124D0171800A75C65 /* Alamofire in Frameworks */,
|
||||||
|
D0666A7D24C7745A00F3F04B /* GRDB in Frameworks */,
|
||||||
);
|
);
|
||||||
runOnlyForDeploymentPostprocessing = 0;
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
};
|
};
|
||||||
D047FA9924C3E21200AF17C5 /* Frameworks */ = {
|
D0666A1E24C677B400F3F04B /* Frameworks */ = {
|
||||||
isa = PBXFrameworksBuildPhase;
|
|
||||||
buildActionMask = 2147483647;
|
|
||||||
files = (
|
|
||||||
);
|
|
||||||
runOnlyForDeploymentPostprocessing = 0;
|
|
||||||
};
|
|
||||||
D047FAA424C3E21200AF17C5 /* Frameworks */ = {
|
|
||||||
isa = PBXFrameworksBuildPhase;
|
isa = PBXFrameworksBuildPhase;
|
||||||
buildActionMask = 2147483647;
|
buildActionMask = 2147483647;
|
||||||
files = (
|
files = (
|
||||||
|
D065F53924D37E5100741304 /* CombineExpectations in Frameworks */,
|
||||||
);
|
);
|
||||||
runOnlyForDeploymentPostprocessing = 0;
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
};
|
};
|
||||||
|
@ -86,21 +217,27 @@
|
||||||
D047FA7F24C3E21000AF17C5 = {
|
D047FA7F24C3E21000AF17C5 = {
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
D047FA8424C3E21000AF17C5 /* Shared */,
|
D0ED1BB224CE3A1600B4899C /* Development Assets */,
|
||||||
|
D0666A7924C7745A00F3F04B /* Frameworks */,
|
||||||
D047FA8E24C3E21200AF17C5 /* iOS */,
|
D047FA8E24C3E21200AF17C5 /* iOS */,
|
||||||
D047FA9524C3E21200AF17C5 /* macOS */,
|
D047FA9524C3E21200AF17C5 /* macOS */,
|
||||||
D047FA9F24C3E21200AF17C5 /* Tests iOS */,
|
|
||||||
D047FAAA24C3E21200AF17C5 /* Tests macOS */,
|
|
||||||
D047FA8D24C3E21200AF17C5 /* Products */,
|
D047FA8D24C3E21200AF17C5 /* Products */,
|
||||||
|
D047FA8424C3E21000AF17C5 /* Shared */,
|
||||||
|
D0666A2224C677B400F3F04B /* Tests */,
|
||||||
);
|
);
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
};
|
};
|
||||||
D047FA8424C3E21000AF17C5 /* Shared */ = {
|
D047FA8424C3E21000AF17C5 /* Shared */ = {
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
D047FA8524C3E21000AF17C5 /* MetatextApp.swift */,
|
|
||||||
D047FA8624C3E21000AF17C5 /* ContentView.swift */,
|
|
||||||
D047FA8724C3E21200AF17C5 /* Assets.xcassets */,
|
D047FA8724C3E21200AF17C5 /* Assets.xcassets */,
|
||||||
|
D0DB6F1624C665B400D965FE /* Extensions */,
|
||||||
|
D06B491D24D3F78A00642749 /* Localizations */,
|
||||||
|
D047FA8524C3E21000AF17C5 /* MetatextApp.swift */,
|
||||||
|
D0666A3A24C6B56200F3F04B /* Model */,
|
||||||
|
D0DB6EFA24C5730600D965FE /* Networking */,
|
||||||
|
D0DB6EFB24C658E400D965FE /* View Models */,
|
||||||
|
D0DB6EF024C5224F00D965FE /* Views */,
|
||||||
);
|
);
|
||||||
path = Shared;
|
path = Shared;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
|
@ -110,8 +247,7 @@
|
||||||
children = (
|
children = (
|
||||||
D047FA8C24C3E21200AF17C5 /* Metatext.app */,
|
D047FA8C24C3E21200AF17C5 /* Metatext.app */,
|
||||||
D047FA9424C3E21200AF17C5 /* Metatext.app */,
|
D047FA9424C3E21200AF17C5 /* Metatext.app */,
|
||||||
D047FA9C24C3E21200AF17C5 /* Tests iOS.xctest */,
|
D0666A2124C677B400F3F04B /* Tests.xctest */,
|
||||||
D047FAA724C3E21200AF17C5 /* Tests macOS.xctest */,
|
|
||||||
);
|
);
|
||||||
name = Products;
|
name = Products;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
|
@ -120,6 +256,7 @@
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
D047FA8F24C3E21200AF17C5 /* Info.plist */,
|
D047FA8F24C3E21200AF17C5 /* Info.plist */,
|
||||||
|
D0BEC95024CA2B7E00E864C4 /* TabNavigation.swift */,
|
||||||
);
|
);
|
||||||
path = iOS;
|
path = iOS;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
|
@ -129,26 +266,144 @@
|
||||||
children = (
|
children = (
|
||||||
D047FA9624C3E21200AF17C5 /* Info.plist */,
|
D047FA9624C3E21200AF17C5 /* Info.plist */,
|
||||||
D047FA9724C3E21200AF17C5 /* macOS.entitlements */,
|
D047FA9724C3E21200AF17C5 /* macOS.entitlements */,
|
||||||
|
D0BEC94E24CA2B5300E864C4 /* SidebarNavigation.swift */,
|
||||||
);
|
);
|
||||||
path = macOS;
|
path = macOS;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
};
|
};
|
||||||
D047FA9F24C3E21200AF17C5 /* Tests iOS */ = {
|
D0666A2224C677B400F3F04B /* Tests */ = {
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
D047FAA024C3E21200AF17C5 /* Tests_iOS.swift */,
|
D0ED1B6C24CE0EED00B4899C /* View Models */,
|
||||||
D047FAA224C3E21200AF17C5 /* Info.plist */,
|
D0666A2524C677B400F3F04B /* Info.plist */,
|
||||||
);
|
);
|
||||||
path = "Tests iOS";
|
path = Tests;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
};
|
};
|
||||||
D047FAAA24C3E21200AF17C5 /* Tests macOS */ = {
|
D0666A3A24C6B56200F3F04B /* Model */ = {
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
D047FAAB24C3E21200AF17C5 /* Tests_macOS.swift */,
|
D0666A6E24C6DFB300F3F04B /* AccessToken.swift */,
|
||||||
D047FAAD24C3E21200AF17C5 /* Info.plist */,
|
D0666A5024C6C3BC00F3F04B /* Account.swift */,
|
||||||
|
D0C963FA24CC359D003BD330 /* AlertItem.swift */,
|
||||||
|
D0666A6224C6DC6C00F3F04B /* AppAuthorization.swift */,
|
||||||
|
D0ED1BD624CF94B200B4899C /* Application.swift */,
|
||||||
|
D0666A4424C6BC0A00F3F04B /* DatabaseError.swift */,
|
||||||
|
D0666A5324C6C3E500F3F04B /* Emoji.swift */,
|
||||||
|
D0666A4A24C6C37700F3F04B /* Identity.swift */,
|
||||||
|
D0666A4124C6BB7B00F3F04B /* IdentityDatabase.swift */,
|
||||||
|
D0666A4D24C6C39600F3F04B /* Instance.swift */,
|
||||||
|
D0DC177324D0B58800A75C65 /* Keychain.swift */,
|
||||||
|
D0ED1BE224CFA84400B4899C /* MastodonError.swift */,
|
||||||
|
D0666A7124C6E0D300F3F04B /* Secrets.swift */,
|
||||||
);
|
);
|
||||||
path = "Tests macOS";
|
path = Model;
|
||||||
|
sourceTree = "<group>";
|
||||||
|
};
|
||||||
|
D0666A7924C7745A00F3F04B /* Frameworks */ = {
|
||||||
|
isa = PBXGroup;
|
||||||
|
children = (
|
||||||
|
);
|
||||||
|
name = Frameworks;
|
||||||
|
sourceTree = "<group>";
|
||||||
|
};
|
||||||
|
D06B491D24D3F78A00642749 /* Localizations */ = {
|
||||||
|
isa = PBXGroup;
|
||||||
|
children = (
|
||||||
|
D06B491E24D3F7FE00642749 /* Localizable.strings */,
|
||||||
|
);
|
||||||
|
path = Localizations;
|
||||||
|
sourceTree = "<group>";
|
||||||
|
};
|
||||||
|
D0DB6EF024C5224F00D965FE /* Views */ = {
|
||||||
|
isa = PBXGroup;
|
||||||
|
children = (
|
||||||
|
D0DB6EF324C5228A00D965FE /* AddIdentityView.swift */,
|
||||||
|
D0BEC93A24C96FD500E864C4 /* ContentView.swift */,
|
||||||
|
D0BEC94924CA231200E864C4 /* TimelineView.swift */,
|
||||||
|
);
|
||||||
|
path = Views;
|
||||||
|
sourceTree = "<group>";
|
||||||
|
};
|
||||||
|
D0DB6EFA24C5730600D965FE /* Networking */ = {
|
||||||
|
isa = PBXGroup;
|
||||||
|
children = (
|
||||||
|
D0ED1BC624CF6CE300B4899C /* Mastodon API */,
|
||||||
|
D0ED1BC324CED54D00B4899C /* HTTPTarget.swift */,
|
||||||
|
D0ED1BC024CED48800B4899C /* HTTPClient.swift */,
|
||||||
|
D0666A5624C6C63400F3F04B /* MastodonDecoder.swift */,
|
||||||
|
D0666A5924C6C64100F3F04B /* MastodonEncoder.swift */,
|
||||||
|
D0ED1BB624CE47F400B4899C /* WebAuthenticationSession.swift */,
|
||||||
|
);
|
||||||
|
path = Networking;
|
||||||
|
sourceTree = "<group>";
|
||||||
|
};
|
||||||
|
D0DB6EFB24C658E400D965FE /* View Models */ = {
|
||||||
|
isa = PBXGroup;
|
||||||
|
children = (
|
||||||
|
D0DB6F0824C65AC000D965FE /* AddIdentityViewModel.swift */,
|
||||||
|
D0BEC93724C9632800E864C4 /* SceneViewModel.swift */,
|
||||||
|
D0BEC94624CA22C400E864C4 /* TimelineViewModel.swift */,
|
||||||
|
);
|
||||||
|
path = "View Models";
|
||||||
|
sourceTree = "<group>";
|
||||||
|
};
|
||||||
|
D0DB6F1624C665B400D965FE /* Extensions */ = {
|
||||||
|
isa = PBXGroup;
|
||||||
|
children = (
|
||||||
|
D0B23F0C24D210E90066F411 /* NSError+Extensions.swift */,
|
||||||
|
D0C963FD24CC3812003BD330 /* Publisher+Extensions.swift */,
|
||||||
|
D081A40424D0F1A8001B016E /* String+Extensions.swift */,
|
||||||
|
D065F53A24D3B33A00741304 /* View+Extensions.swift */,
|
||||||
|
);
|
||||||
|
path = Extensions;
|
||||||
|
sourceTree = "<group>";
|
||||||
|
};
|
||||||
|
D0DC174824CFF13700A75C65 /* Mastodon API Stubs */ = {
|
||||||
|
isa = PBXGroup;
|
||||||
|
children = (
|
||||||
|
D0DC175424D00F0A00A75C65 /* AccessTokenEndpoint+Stubbing.swift */,
|
||||||
|
D0DC174924CFF15F00A75C65 /* AppAuthorizationEndpoint+Stubbing.swift */,
|
||||||
|
D0DC175124D008E300A75C65 /* MastodonTarget+Stubbing.swift */,
|
||||||
|
);
|
||||||
|
path = "Mastodon API Stubs";
|
||||||
|
sourceTree = "<group>";
|
||||||
|
};
|
||||||
|
D0ED1B6C24CE0EED00B4899C /* View Models */ = {
|
||||||
|
isa = PBXGroup;
|
||||||
|
children = (
|
||||||
|
D0ED1B6D24CE100C00B4899C /* AddIdentityViewModelTests.swift */,
|
||||||
|
);
|
||||||
|
path = "View Models";
|
||||||
|
sourceTree = "<group>";
|
||||||
|
};
|
||||||
|
D0ED1BB224CE3A1600B4899C /* Development Assets */ = {
|
||||||
|
isa = PBXGroup;
|
||||||
|
children = (
|
||||||
|
D0DC177624D0CF2600A75C65 /* FakeKeychain.swift */,
|
||||||
|
D0DC175724D0130800A75C65 /* HTTPStubs.swift */,
|
||||||
|
D0DC174824CFF13700A75C65 /* Mastodon API Stubs */,
|
||||||
|
D0DC174C24CFF1F100A75C65 /* Stubbing.swift */,
|
||||||
|
D0DC174524CFEC2000A75C65 /* StubbingURLProtocol.swift */,
|
||||||
|
D074577624D29006004758DB /* StubbingWebAuthenticationSession.swift */,
|
||||||
|
D074577924D29366004758DB /* URLSessionConfiguration+Extensions.swift */,
|
||||||
|
);
|
||||||
|
path = "Development Assets";
|
||||||
|
sourceTree = "<group>";
|
||||||
|
};
|
||||||
|
D0ED1BC624CF6CE300B4899C /* Mastodon API */ = {
|
||||||
|
isa = PBXGroup;
|
||||||
|
children = (
|
||||||
|
D0ED1BDC24CF982600B4899C /* AccessTokenEndpoint.swift */,
|
||||||
|
D0ED1BDF24CF98FB00B4899C /* AccountEndpoint.swift */,
|
||||||
|
D0ED1BD924CF963E00B4899C /* AppAuthorizationEndpoint.swift */,
|
||||||
|
D065F53D24D3D20300741304 /* InstanceEndpoint.swift */,
|
||||||
|
D0DC175A24D0154F00A75C65 /* MastodonAPI.swift */,
|
||||||
|
D0ED1BCA24CF744200B4899C /* MastodonClient.swift */,
|
||||||
|
D0ED1BCD24CF768200B4899C /* MastodonEndpoint.swift */,
|
||||||
|
D0ED1BD024CF779B00B4899C /* MastodonTarget.swift */,
|
||||||
|
);
|
||||||
|
path = "Mastodon API";
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
};
|
};
|
||||||
/* End PBXGroup section */
|
/* End PBXGroup section */
|
||||||
|
@ -161,12 +416,17 @@
|
||||||
D047FA8824C3E21200AF17C5 /* Sources */,
|
D047FA8824C3E21200AF17C5 /* Sources */,
|
||||||
D047FA8924C3E21200AF17C5 /* Frameworks */,
|
D047FA8924C3E21200AF17C5 /* Frameworks */,
|
||||||
D047FA8A24C3E21200AF17C5 /* Resources */,
|
D047FA8A24C3E21200AF17C5 /* Resources */,
|
||||||
|
D0666A2E24C67E6700F3F04B /* ShellScript */,
|
||||||
);
|
);
|
||||||
buildRules = (
|
buildRules = (
|
||||||
);
|
);
|
||||||
dependencies = (
|
dependencies = (
|
||||||
);
|
);
|
||||||
name = "Metatext (iOS)";
|
name = "Metatext (iOS)";
|
||||||
|
packageProductDependencies = (
|
||||||
|
D0666A4824C6C1A300F3F04B /* GRDB */,
|
||||||
|
D0DC175E24D016EA00A75C65 /* Alamofire */,
|
||||||
|
);
|
||||||
productName = "Metatext (iOS)";
|
productName = "Metatext (iOS)";
|
||||||
productReference = D047FA8C24C3E21200AF17C5 /* Metatext.app */;
|
productReference = D047FA8C24C3E21200AF17C5 /* Metatext.app */;
|
||||||
productType = "com.apple.product-type.application";
|
productType = "com.apple.product-type.application";
|
||||||
|
@ -178,51 +438,41 @@
|
||||||
D047FA9024C3E21200AF17C5 /* Sources */,
|
D047FA9024C3E21200AF17C5 /* Sources */,
|
||||||
D047FA9124C3E21200AF17C5 /* Frameworks */,
|
D047FA9124C3E21200AF17C5 /* Frameworks */,
|
||||||
D047FA9224C3E21200AF17C5 /* Resources */,
|
D047FA9224C3E21200AF17C5 /* Resources */,
|
||||||
|
D0666A2F24C67F1400F3F04B /* ShellScript */,
|
||||||
);
|
);
|
||||||
buildRules = (
|
buildRules = (
|
||||||
);
|
);
|
||||||
dependencies = (
|
dependencies = (
|
||||||
);
|
);
|
||||||
name = "Metatext (macOS)";
|
name = "Metatext (macOS)";
|
||||||
|
packageProductDependencies = (
|
||||||
|
D0666A7C24C7745A00F3F04B /* GRDB */,
|
||||||
|
D0DC176024D0171800A75C65 /* Alamofire */,
|
||||||
|
);
|
||||||
productName = "Metatext (macOS)";
|
productName = "Metatext (macOS)";
|
||||||
productReference = D047FA9424C3E21200AF17C5 /* Metatext.app */;
|
productReference = D047FA9424C3E21200AF17C5 /* Metatext.app */;
|
||||||
productType = "com.apple.product-type.application";
|
productType = "com.apple.product-type.application";
|
||||||
};
|
};
|
||||||
D047FA9B24C3E21200AF17C5 /* Tests iOS */ = {
|
D0666A2024C677B400F3F04B /* Tests */ = {
|
||||||
isa = PBXNativeTarget;
|
isa = PBXNativeTarget;
|
||||||
buildConfigurationList = D047FABC24C3E21200AF17C5 /* Build configuration list for PBXNativeTarget "Tests iOS" */;
|
buildConfigurationList = D0666A2824C677B400F3F04B /* Build configuration list for PBXNativeTarget "Tests" */;
|
||||||
buildPhases = (
|
buildPhases = (
|
||||||
D047FA9824C3E21200AF17C5 /* Sources */,
|
D0666A1D24C677B400F3F04B /* Sources */,
|
||||||
D047FA9924C3E21200AF17C5 /* Frameworks */,
|
D0666A1E24C677B400F3F04B /* Frameworks */,
|
||||||
D047FA9A24C3E21200AF17C5 /* Resources */,
|
D0666A1F24C677B400F3F04B /* Resources */,
|
||||||
);
|
);
|
||||||
buildRules = (
|
buildRules = (
|
||||||
);
|
);
|
||||||
dependencies = (
|
dependencies = (
|
||||||
D047FA9E24C3E21200AF17C5 /* PBXTargetDependency */,
|
D0666A2724C677B400F3F04B /* PBXTargetDependency */,
|
||||||
);
|
);
|
||||||
name = "Tests iOS";
|
name = Tests;
|
||||||
productName = "Tests iOS";
|
packageProductDependencies = (
|
||||||
productReference = D047FA9C24C3E21200AF17C5 /* Tests iOS.xctest */;
|
D065F53824D37E5100741304 /* CombineExpectations */,
|
||||||
productType = "com.apple.product-type.bundle.ui-testing";
|
|
||||||
};
|
|
||||||
D047FAA624C3E21200AF17C5 /* Tests macOS */ = {
|
|
||||||
isa = PBXNativeTarget;
|
|
||||||
buildConfigurationList = D047FABF24C3E21200AF17C5 /* Build configuration list for PBXNativeTarget "Tests macOS" */;
|
|
||||||
buildPhases = (
|
|
||||||
D047FAA324C3E21200AF17C5 /* Sources */,
|
|
||||||
D047FAA424C3E21200AF17C5 /* Frameworks */,
|
|
||||||
D047FAA524C3E21200AF17C5 /* Resources */,
|
|
||||||
);
|
);
|
||||||
buildRules = (
|
productName = "Unit Tests";
|
||||||
);
|
productReference = D0666A2124C677B400F3F04B /* Tests.xctest */;
|
||||||
dependencies = (
|
productType = "com.apple.product-type.bundle.unit-test";
|
||||||
D047FAA924C3E21200AF17C5 /* PBXTargetDependency */,
|
|
||||||
);
|
|
||||||
name = "Tests macOS";
|
|
||||||
productName = "Tests macOS";
|
|
||||||
productReference = D047FAA724C3E21200AF17C5 /* Tests macOS.xctest */;
|
|
||||||
productType = "com.apple.product-type.bundle.ui-testing";
|
|
||||||
};
|
};
|
||||||
/* End PBXNativeTarget section */
|
/* End PBXNativeTarget section */
|
||||||
|
|
||||||
|
@ -240,14 +490,11 @@
|
||||||
D047FA9324C3E21200AF17C5 = {
|
D047FA9324C3E21200AF17C5 = {
|
||||||
CreatedOnToolsVersion = 12.0;
|
CreatedOnToolsVersion = 12.0;
|
||||||
};
|
};
|
||||||
D047FA9B24C3E21200AF17C5 = {
|
D0666A2024C677B400F3F04B = {
|
||||||
CreatedOnToolsVersion = 12.0;
|
CreatedOnToolsVersion = 12.0;
|
||||||
|
LastSwiftMigration = 1200;
|
||||||
TestTargetID = D047FA8B24C3E21200AF17C5;
|
TestTargetID = D047FA8B24C3E21200AF17C5;
|
||||||
};
|
};
|
||||||
D047FAA624C3E21200AF17C5 = {
|
|
||||||
CreatedOnToolsVersion = 12.0;
|
|
||||||
TestTargetID = D047FA9324C3E21200AF17C5;
|
|
||||||
};
|
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
buildConfigurationList = D047FA8324C3E21000AF17C5 /* Build configuration list for PBXProject "Metatext" */;
|
buildConfigurationList = D047FA8324C3E21000AF17C5 /* Build configuration list for PBXProject "Metatext" */;
|
||||||
|
@ -259,14 +506,18 @@
|
||||||
Base,
|
Base,
|
||||||
);
|
);
|
||||||
mainGroup = D047FA7F24C3E21000AF17C5;
|
mainGroup = D047FA7F24C3E21000AF17C5;
|
||||||
|
packageReferences = (
|
||||||
|
D0666A4724C6C1A300F3F04B /* XCRemoteSwiftPackageReference "GRDB" */,
|
||||||
|
D0DC175D24D016EA00A75C65 /* XCRemoteSwiftPackageReference "Alamofire" */,
|
||||||
|
D065F53724D37E5100741304 /* XCRemoteSwiftPackageReference "CombineExpectations" */,
|
||||||
|
);
|
||||||
productRefGroup = D047FA8D24C3E21200AF17C5 /* Products */;
|
productRefGroup = D047FA8D24C3E21200AF17C5 /* Products */;
|
||||||
projectDirPath = "";
|
projectDirPath = "";
|
||||||
projectRoot = "";
|
projectRoot = "";
|
||||||
targets = (
|
targets = (
|
||||||
D047FA8B24C3E21200AF17C5 /* Metatext (iOS) */,
|
D047FA8B24C3E21200AF17C5 /* Metatext (iOS) */,
|
||||||
D047FA9324C3E21200AF17C5 /* Metatext (macOS) */,
|
D047FA9324C3E21200AF17C5 /* Metatext (macOS) */,
|
||||||
D047FA9B24C3E21200AF17C5 /* Tests iOS */,
|
D0666A2024C677B400F3F04B /* Tests */,
|
||||||
D047FAA624C3E21200AF17C5 /* Tests macOS */,
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
/* End PBXProject section */
|
/* End PBXProject section */
|
||||||
|
@ -277,6 +528,7 @@
|
||||||
buildActionMask = 2147483647;
|
buildActionMask = 2147483647;
|
||||||
files = (
|
files = (
|
||||||
D047FAB224C3E21200AF17C5 /* Assets.xcassets in Resources */,
|
D047FAB224C3E21200AF17C5 /* Assets.xcassets in Resources */,
|
||||||
|
D06B491F24D3F7FE00642749 /* Localizable.strings in Resources */,
|
||||||
);
|
);
|
||||||
runOnlyForDeploymentPostprocessing = 0;
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
};
|
};
|
||||||
|
@ -285,17 +537,11 @@
|
||||||
buildActionMask = 2147483647;
|
buildActionMask = 2147483647;
|
||||||
files = (
|
files = (
|
||||||
D047FAB324C3E21200AF17C5 /* Assets.xcassets in Resources */,
|
D047FAB324C3E21200AF17C5 /* Assets.xcassets in Resources */,
|
||||||
|
D06B492024D3FB8000642749 /* Localizable.strings in Resources */,
|
||||||
);
|
);
|
||||||
runOnlyForDeploymentPostprocessing = 0;
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
};
|
};
|
||||||
D047FA9A24C3E21200AF17C5 /* Resources */ = {
|
D0666A1F24C677B400F3F04B /* Resources */ = {
|
||||||
isa = PBXResourcesBuildPhase;
|
|
||||||
buildActionMask = 2147483647;
|
|
||||||
files = (
|
|
||||||
);
|
|
||||||
runOnlyForDeploymentPostprocessing = 0;
|
|
||||||
};
|
|
||||||
D047FAA524C3E21200AF17C5 /* Resources */ = {
|
|
||||||
isa = PBXResourcesBuildPhase;
|
isa = PBXResourcesBuildPhase;
|
||||||
buildActionMask = 2147483647;
|
buildActionMask = 2147483647;
|
||||||
files = (
|
files = (
|
||||||
|
@ -304,13 +550,95 @@
|
||||||
};
|
};
|
||||||
/* End PBXResourcesBuildPhase section */
|
/* End PBXResourcesBuildPhase section */
|
||||||
|
|
||||||
|
/* Begin PBXShellScriptBuildPhase section */
|
||||||
|
D0666A2E24C67E6700F3F04B /* ShellScript */ = {
|
||||||
|
isa = PBXShellScriptBuildPhase;
|
||||||
|
buildActionMask = 2147483647;
|
||||||
|
files = (
|
||||||
|
);
|
||||||
|
inputFileListPaths = (
|
||||||
|
);
|
||||||
|
inputPaths = (
|
||||||
|
);
|
||||||
|
outputFileListPaths = (
|
||||||
|
);
|
||||||
|
outputPaths = (
|
||||||
|
);
|
||||||
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
|
shellPath = /bin/sh;
|
||||||
|
shellScript = "if which swiftlint >/dev/null; then\n swiftlint\nelse\n echo \"warning: SwiftLint not installed, download from https://github.com/realm/SwiftLint\"\nfi\n";
|
||||||
|
};
|
||||||
|
D0666A2F24C67F1400F3F04B /* ShellScript */ = {
|
||||||
|
isa = PBXShellScriptBuildPhase;
|
||||||
|
buildActionMask = 2147483647;
|
||||||
|
files = (
|
||||||
|
);
|
||||||
|
inputFileListPaths = (
|
||||||
|
);
|
||||||
|
inputPaths = (
|
||||||
|
);
|
||||||
|
outputFileListPaths = (
|
||||||
|
);
|
||||||
|
outputPaths = (
|
||||||
|
);
|
||||||
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
|
shellPath = /bin/sh;
|
||||||
|
shellScript = "if which swiftlint >/dev/null; then\n swiftlint\nelse\n echo \"warning: SwiftLint not installed, download from https://github.com/realm/SwiftLint\"\nfi\n";
|
||||||
|
};
|
||||||
|
/* End PBXShellScriptBuildPhase section */
|
||||||
|
|
||||||
/* Begin PBXSourcesBuildPhase section */
|
/* Begin PBXSourcesBuildPhase section */
|
||||||
D047FA8824C3E21200AF17C5 /* Sources */ = {
|
D047FA8824C3E21200AF17C5 /* Sources */ = {
|
||||||
isa = PBXSourcesBuildPhase;
|
isa = PBXSourcesBuildPhase;
|
||||||
buildActionMask = 2147483647;
|
buildActionMask = 2147483647;
|
||||||
files = (
|
files = (
|
||||||
D047FAB024C3E21200AF17C5 /* ContentView.swift in Sources */,
|
D0DB6F0924C65AC000D965FE /* AddIdentityViewModel.swift in Sources */,
|
||||||
|
D0ED1BD724CF94B200B4899C /* Application.swift in Sources */,
|
||||||
D047FAAE24C3E21200AF17C5 /* MetatextApp.swift in Sources */,
|
D047FAAE24C3E21200AF17C5 /* MetatextApp.swift in Sources */,
|
||||||
|
D0BEC94724CA22C400E864C4 /* TimelineViewModel.swift in Sources */,
|
||||||
|
D0666A4E24C6C39600F3F04B /* Instance.swift in Sources */,
|
||||||
|
D0ED1BDA24CF963E00B4899C /* AppAuthorizationEndpoint.swift in Sources */,
|
||||||
|
D0ED1BE324CFA84400B4899C /* MastodonError.swift in Sources */,
|
||||||
|
D0666A6324C6DC6C00F3F04B /* AppAuthorization.swift in Sources */,
|
||||||
|
D065F53B24D3B33A00741304 /* View+Extensions.swift in Sources */,
|
||||||
|
D0DC174A24CFF15F00A75C65 /* AppAuthorizationEndpoint+Stubbing.swift in Sources */,
|
||||||
|
D0666A5A24C6C64100F3F04B /* MastodonEncoder.swift in Sources */,
|
||||||
|
D0666A5124C6C3BC00F3F04B /* Account.swift in Sources */,
|
||||||
|
D0ED1BE024CF98FB00B4899C /* AccountEndpoint.swift in Sources */,
|
||||||
|
D081A40524D0F1A8001B016E /* String+Extensions.swift in Sources */,
|
||||||
|
D0BEC93824C9632800E864C4 /* SceneViewModel.swift in Sources */,
|
||||||
|
D0ED1BC124CED48800B4899C /* HTTPClient.swift in Sources */,
|
||||||
|
D0666A4524C6BC0A00F3F04B /* DatabaseError.swift in Sources */,
|
||||||
|
D0ED1BDD24CF982600B4899C /* AccessTokenEndpoint.swift in Sources */,
|
||||||
|
D0666A4B24C6C37700F3F04B /* Identity.swift in Sources */,
|
||||||
|
D0666A5424C6C3E500F3F04B /* Emoji.swift in Sources */,
|
||||||
|
D0DC175524D00F0A00A75C65 /* AccessTokenEndpoint+Stubbing.swift in Sources */,
|
||||||
|
D0B23F0D24D210E90066F411 /* NSError+Extensions.swift in Sources */,
|
||||||
|
D0DC175224D008E300A75C65 /* MastodonTarget+Stubbing.swift in Sources */,
|
||||||
|
D0666A4224C6BB7B00F3F04B /* IdentityDatabase.swift in Sources */,
|
||||||
|
D0BEC94A24CA231200E864C4 /* TimelineView.swift in Sources */,
|
||||||
|
D0BEC93B24C96FD500E864C4 /* ContentView.swift in Sources */,
|
||||||
|
D0DC175824D0130800A75C65 /* HTTPStubs.swift in Sources */,
|
||||||
|
D0DC177724D0CF2600A75C65 /* FakeKeychain.swift in Sources */,
|
||||||
|
D0C963FB24CC359D003BD330 /* AlertItem.swift in Sources */,
|
||||||
|
D0DC174624CFEC2000A75C65 /* StubbingURLProtocol.swift in Sources */,
|
||||||
|
D0DC174D24CFF1F100A75C65 /* Stubbing.swift in Sources */,
|
||||||
|
D0666A5724C6C63400F3F04B /* MastodonDecoder.swift in Sources */,
|
||||||
|
D0DB6EF424C5228A00D965FE /* AddIdentityView.swift in Sources */,
|
||||||
|
D0DC177424D0B58800A75C65 /* Keychain.swift in Sources */,
|
||||||
|
D074577724D29006004758DB /* StubbingWebAuthenticationSession.swift in Sources */,
|
||||||
|
D0ED1BCE24CF768200B4899C /* MastodonEndpoint.swift in Sources */,
|
||||||
|
D074577A24D29366004758DB /* URLSessionConfiguration+Extensions.swift in Sources */,
|
||||||
|
D0ED1BB724CE47F400B4899C /* WebAuthenticationSession.swift in Sources */,
|
||||||
|
D0666A7224C6E0D300F3F04B /* Secrets.swift in Sources */,
|
||||||
|
D0BEC95124CA2B7E00E864C4 /* TabNavigation.swift in Sources */,
|
||||||
|
D0ED1BC424CED54D00B4899C /* HTTPTarget.swift in Sources */,
|
||||||
|
D0C963FE24CC3812003BD330 /* Publisher+Extensions.swift in Sources */,
|
||||||
|
D0DC175B24D0154F00A75C65 /* MastodonAPI.swift in Sources */,
|
||||||
|
D0ED1BD124CF779B00B4899C /* MastodonTarget.swift in Sources */,
|
||||||
|
D065F53E24D3D20300741304 /* InstanceEndpoint.swift in Sources */,
|
||||||
|
D0666A6F24C6DFB300F3F04B /* AccessToken.swift in Sources */,
|
||||||
|
D0ED1BCB24CF744200B4899C /* MastodonClient.swift in Sources */,
|
||||||
);
|
);
|
||||||
runOnlyForDeploymentPostprocessing = 0;
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
};
|
};
|
||||||
|
@ -318,39 +646,71 @@
|
||||||
isa = PBXSourcesBuildPhase;
|
isa = PBXSourcesBuildPhase;
|
||||||
buildActionMask = 2147483647;
|
buildActionMask = 2147483647;
|
||||||
files = (
|
files = (
|
||||||
D047FAB124C3E21200AF17C5 /* ContentView.swift in Sources */,
|
D0DB6F0A24C65AC000D965FE /* AddIdentityViewModel.swift in Sources */,
|
||||||
|
D0ED1BD824CF94B200B4899C /* Application.swift in Sources */,
|
||||||
D047FAAF24C3E21200AF17C5 /* MetatextApp.swift in Sources */,
|
D047FAAF24C3E21200AF17C5 /* MetatextApp.swift in Sources */,
|
||||||
|
D0BEC94824CA22C400E864C4 /* TimelineViewModel.swift in Sources */,
|
||||||
|
D0666A4F24C6C39600F3F04B /* Instance.swift in Sources */,
|
||||||
|
D0ED1BDB24CF963E00B4899C /* AppAuthorizationEndpoint.swift in Sources */,
|
||||||
|
D0ED1BE424CFA84400B4899C /* MastodonError.swift in Sources */,
|
||||||
|
D0666A6424C6DC6C00F3F04B /* AppAuthorization.swift in Sources */,
|
||||||
|
D065F53C24D3B33A00741304 /* View+Extensions.swift in Sources */,
|
||||||
|
D0DC174B24CFF15F00A75C65 /* AppAuthorizationEndpoint+Stubbing.swift in Sources */,
|
||||||
|
D0666A5B24C6C64100F3F04B /* MastodonEncoder.swift in Sources */,
|
||||||
|
D0666A5224C6C3BC00F3F04B /* Account.swift in Sources */,
|
||||||
|
D0ED1BE124CF98FB00B4899C /* AccountEndpoint.swift in Sources */,
|
||||||
|
D081A40624D0F1A8001B016E /* String+Extensions.swift in Sources */,
|
||||||
|
D0BEC93924C9632800E864C4 /* SceneViewModel.swift in Sources */,
|
||||||
|
D0ED1BC224CED48800B4899C /* HTTPClient.swift in Sources */,
|
||||||
|
D0666A4624C6BC0A00F3F04B /* DatabaseError.swift in Sources */,
|
||||||
|
D0ED1BDE24CF982600B4899C /* AccessTokenEndpoint.swift in Sources */,
|
||||||
|
D0666A4C24C6C37700F3F04B /* Identity.swift in Sources */,
|
||||||
|
D0666A5524C6C3E500F3F04B /* Emoji.swift in Sources */,
|
||||||
|
D0DC175624D00F0A00A75C65 /* AccessTokenEndpoint+Stubbing.swift in Sources */,
|
||||||
|
D0B23F0E24D210E90066F411 /* NSError+Extensions.swift in Sources */,
|
||||||
|
D0DC175324D008E300A75C65 /* MastodonTarget+Stubbing.swift in Sources */,
|
||||||
|
D0666A4324C6BB7B00F3F04B /* IdentityDatabase.swift in Sources */,
|
||||||
|
D0BEC94B24CA231200E864C4 /* TimelineView.swift in Sources */,
|
||||||
|
D0BEC93C24C96FD500E864C4 /* ContentView.swift in Sources */,
|
||||||
|
D0DC175924D0130800A75C65 /* HTTPStubs.swift in Sources */,
|
||||||
|
D0DC177824D0CF2600A75C65 /* FakeKeychain.swift in Sources */,
|
||||||
|
D0C963FC24CC359D003BD330 /* AlertItem.swift in Sources */,
|
||||||
|
D0DC174724CFEC2000A75C65 /* StubbingURLProtocol.swift in Sources */,
|
||||||
|
D0DC174E24CFF1F100A75C65 /* Stubbing.swift in Sources */,
|
||||||
|
D0666A5824C6C63400F3F04B /* MastodonDecoder.swift in Sources */,
|
||||||
|
D0DB6EF524C5233E00D965FE /* AddIdentityView.swift in Sources */,
|
||||||
|
D0DC177524D0B58800A75C65 /* Keychain.swift in Sources */,
|
||||||
|
D074577824D29006004758DB /* StubbingWebAuthenticationSession.swift in Sources */,
|
||||||
|
D0ED1BCF24CF768200B4899C /* MastodonEndpoint.swift in Sources */,
|
||||||
|
D074577B24D29366004758DB /* URLSessionConfiguration+Extensions.swift in Sources */,
|
||||||
|
D0ED1BB824CE47F400B4899C /* WebAuthenticationSession.swift in Sources */,
|
||||||
|
D0BEC94F24CA2B5300E864C4 /* SidebarNavigation.swift in Sources */,
|
||||||
|
D0666A7324C6E0D300F3F04B /* Secrets.swift in Sources */,
|
||||||
|
D0ED1BC524CED54D00B4899C /* HTTPTarget.swift in Sources */,
|
||||||
|
D0C963FF24CC3812003BD330 /* Publisher+Extensions.swift in Sources */,
|
||||||
|
D0DC175C24D0154F00A75C65 /* MastodonAPI.swift in Sources */,
|
||||||
|
D0ED1BD224CF779B00B4899C /* MastodonTarget.swift in Sources */,
|
||||||
|
D065F53F24D3D20300741304 /* InstanceEndpoint.swift in Sources */,
|
||||||
|
D0666A7024C6DFB300F3F04B /* AccessToken.swift in Sources */,
|
||||||
|
D0ED1BCC24CF744200B4899C /* MastodonClient.swift in Sources */,
|
||||||
);
|
);
|
||||||
runOnlyForDeploymentPostprocessing = 0;
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
};
|
};
|
||||||
D047FA9824C3E21200AF17C5 /* Sources */ = {
|
D0666A1D24C677B400F3F04B /* Sources */ = {
|
||||||
isa = PBXSourcesBuildPhase;
|
isa = PBXSourcesBuildPhase;
|
||||||
buildActionMask = 2147483647;
|
buildActionMask = 2147483647;
|
||||||
files = (
|
files = (
|
||||||
D047FAA124C3E21200AF17C5 /* Tests_iOS.swift in Sources */,
|
D0ED1B6E24CE100C00B4899C /* AddIdentityViewModelTests.swift in Sources */,
|
||||||
);
|
|
||||||
runOnlyForDeploymentPostprocessing = 0;
|
|
||||||
};
|
|
||||||
D047FAA324C3E21200AF17C5 /* Sources */ = {
|
|
||||||
isa = PBXSourcesBuildPhase;
|
|
||||||
buildActionMask = 2147483647;
|
|
||||||
files = (
|
|
||||||
D047FAAC24C3E21200AF17C5 /* Tests_macOS.swift in Sources */,
|
|
||||||
);
|
);
|
||||||
runOnlyForDeploymentPostprocessing = 0;
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
};
|
};
|
||||||
/* End PBXSourcesBuildPhase section */
|
/* End PBXSourcesBuildPhase section */
|
||||||
|
|
||||||
/* Begin PBXTargetDependency section */
|
/* Begin PBXTargetDependency section */
|
||||||
D047FA9E24C3E21200AF17C5 /* PBXTargetDependency */ = {
|
D0666A2724C677B400F3F04B /* PBXTargetDependency */ = {
|
||||||
isa = PBXTargetDependency;
|
isa = PBXTargetDependency;
|
||||||
target = D047FA8B24C3E21200AF17C5 /* Metatext (iOS) */;
|
target = D047FA8B24C3E21200AF17C5 /* Metatext (iOS) */;
|
||||||
targetProxy = D047FA9D24C3E21200AF17C5 /* PBXContainerItemProxy */;
|
targetProxy = D0666A2624C677B400F3F04B /* PBXContainerItemProxy */;
|
||||||
};
|
|
||||||
D047FAA924C3E21200AF17C5 /* PBXTargetDependency */ = {
|
|
||||||
isa = PBXTargetDependency;
|
|
||||||
target = D047FA9324C3E21200AF17C5 /* Metatext (macOS) */;
|
|
||||||
targetProxy = D047FAA824C3E21200AF17C5 /* PBXContainerItemProxy */;
|
|
||||||
};
|
};
|
||||||
/* End PBXTargetDependency section */
|
/* End PBXTargetDependency section */
|
||||||
|
|
||||||
|
@ -472,6 +832,7 @@
|
||||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||||
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
|
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
|
DEVELOPMENT_ASSET_PATHS = "Development\\ Assets";
|
||||||
DEVELOPMENT_TEAM = 82HL67AXQ2;
|
DEVELOPMENT_TEAM = 82HL67AXQ2;
|
||||||
ENABLE_PREVIEWS = YES;
|
ENABLE_PREVIEWS = YES;
|
||||||
INFOPLIST_FILE = iOS/Info.plist;
|
INFOPLIST_FILE = iOS/Info.plist;
|
||||||
|
@ -494,6 +855,7 @@
|
||||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||||
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
|
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
|
DEVELOPMENT_ASSET_PATHS = "Development\\ Assets";
|
||||||
DEVELOPMENT_TEAM = 82HL67AXQ2;
|
DEVELOPMENT_TEAM = 82HL67AXQ2;
|
||||||
ENABLE_PREVIEWS = YES;
|
ENABLE_PREVIEWS = YES;
|
||||||
INFOPLIST_FILE = iOS/Info.plist;
|
INFOPLIST_FILE = iOS/Info.plist;
|
||||||
|
@ -519,6 +881,7 @@
|
||||||
CODE_SIGN_ENTITLEMENTS = macOS/macOS.entitlements;
|
CODE_SIGN_ENTITLEMENTS = macOS/macOS.entitlements;
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
COMBINE_HIDPI_IMAGES = YES;
|
COMBINE_HIDPI_IMAGES = YES;
|
||||||
|
DEVELOPMENT_ASSET_PATHS = "Development\\ Assets";
|
||||||
DEVELOPMENT_TEAM = 82HL67AXQ2;
|
DEVELOPMENT_TEAM = 82HL67AXQ2;
|
||||||
ENABLE_HARDENED_RUNTIME = YES;
|
ENABLE_HARDENED_RUNTIME = YES;
|
||||||
ENABLE_PREVIEWS = YES;
|
ENABLE_PREVIEWS = YES;
|
||||||
|
@ -543,6 +906,7 @@
|
||||||
CODE_SIGN_ENTITLEMENTS = macOS/macOS.entitlements;
|
CODE_SIGN_ENTITLEMENTS = macOS/macOS.entitlements;
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
COMBINE_HIDPI_IMAGES = YES;
|
COMBINE_HIDPI_IMAGES = YES;
|
||||||
|
DEVELOPMENT_ASSET_PATHS = "Development\\ Assets";
|
||||||
DEVELOPMENT_TEAM = 82HL67AXQ2;
|
DEVELOPMENT_TEAM = 82HL67AXQ2;
|
||||||
ENABLE_HARDENED_RUNTIME = YES;
|
ENABLE_HARDENED_RUNTIME = YES;
|
||||||
ENABLE_PREVIEWS = YES;
|
ENABLE_PREVIEWS = YES;
|
||||||
|
@ -559,95 +923,54 @@
|
||||||
};
|
};
|
||||||
name = Release;
|
name = Release;
|
||||||
};
|
};
|
||||||
D047FABD24C3E21200AF17C5 /* Debug */ = {
|
D0666A2924C677B400F3F04B /* Debug */ = {
|
||||||
isa = XCBuildConfiguration;
|
isa = XCBuildConfiguration;
|
||||||
buildSettings = {
|
buildSettings = {
|
||||||
ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES;
|
BUNDLE_LOADER = "$(TEST_HOST)";
|
||||||
|
CLANG_ENABLE_MODULES = YES;
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
DEVELOPMENT_TEAM = 82HL67AXQ2;
|
DEVELOPMENT_TEAM = 82HL67AXQ2;
|
||||||
INFOPLIST_FILE = "Tests iOS/Info.plist";
|
INFOPLIST_FILE = Tests/Info.plist;
|
||||||
IPHONEOS_DEPLOYMENT_TARGET = 14.0;
|
IPHONEOS_DEPLOYMENT_TARGET = 14.0;
|
||||||
LD_RUNPATH_SEARCH_PATHS = (
|
LD_RUNPATH_SEARCH_PATHS = (
|
||||||
"$(inherited)",
|
"$(inherited)",
|
||||||
"@executable_path/Frameworks",
|
"@executable_path/Frameworks",
|
||||||
"@loader_path/Frameworks",
|
"@loader_path/Frameworks",
|
||||||
);
|
);
|
||||||
PRODUCT_BUNDLE_IDENTIFIER = "com.metabolist.Tests-iOS";
|
PRODUCT_BUNDLE_IDENTIFIER = "com.metabolist.Unit-Tests";
|
||||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||||
SDKROOT = iphoneos;
|
SDKROOT = iphoneos;
|
||||||
|
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
|
||||||
SWIFT_VERSION = 5.0;
|
SWIFT_VERSION = 5.0;
|
||||||
TARGETED_DEVICE_FAMILY = "1,2";
|
TARGETED_DEVICE_FAMILY = "1,2";
|
||||||
TEST_TARGET_NAME = "Metatext (iOS)";
|
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Metatext.app/Metatext";
|
||||||
};
|
};
|
||||||
name = Debug;
|
name = Debug;
|
||||||
};
|
};
|
||||||
D047FABE24C3E21200AF17C5 /* Release */ = {
|
D0666A2A24C677B400F3F04B /* Release */ = {
|
||||||
isa = XCBuildConfiguration;
|
isa = XCBuildConfiguration;
|
||||||
buildSettings = {
|
buildSettings = {
|
||||||
ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES;
|
BUNDLE_LOADER = "$(TEST_HOST)";
|
||||||
|
CLANG_ENABLE_MODULES = YES;
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
DEVELOPMENT_TEAM = 82HL67AXQ2;
|
DEVELOPMENT_TEAM = 82HL67AXQ2;
|
||||||
INFOPLIST_FILE = "Tests iOS/Info.plist";
|
INFOPLIST_FILE = Tests/Info.plist;
|
||||||
IPHONEOS_DEPLOYMENT_TARGET = 14.0;
|
IPHONEOS_DEPLOYMENT_TARGET = 14.0;
|
||||||
LD_RUNPATH_SEARCH_PATHS = (
|
LD_RUNPATH_SEARCH_PATHS = (
|
||||||
"$(inherited)",
|
"$(inherited)",
|
||||||
"@executable_path/Frameworks",
|
"@executable_path/Frameworks",
|
||||||
"@loader_path/Frameworks",
|
"@loader_path/Frameworks",
|
||||||
);
|
);
|
||||||
PRODUCT_BUNDLE_IDENTIFIER = "com.metabolist.Tests-iOS";
|
PRODUCT_BUNDLE_IDENTIFIER = "com.metabolist.Unit-Tests";
|
||||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||||
SDKROOT = iphoneos;
|
SDKROOT = iphoneos;
|
||||||
SWIFT_VERSION = 5.0;
|
SWIFT_VERSION = 5.0;
|
||||||
TARGETED_DEVICE_FAMILY = "1,2";
|
TARGETED_DEVICE_FAMILY = "1,2";
|
||||||
TEST_TARGET_NAME = "Metatext (iOS)";
|
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Metatext.app/Metatext";
|
||||||
VALIDATE_PRODUCT = YES;
|
VALIDATE_PRODUCT = YES;
|
||||||
};
|
};
|
||||||
name = Release;
|
name = Release;
|
||||||
};
|
};
|
||||||
D047FAC024C3E21200AF17C5 /* Debug */ = {
|
|
||||||
isa = XCBuildConfiguration;
|
|
||||||
buildSettings = {
|
|
||||||
ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES;
|
|
||||||
CODE_SIGN_STYLE = Automatic;
|
|
||||||
COMBINE_HIDPI_IMAGES = YES;
|
|
||||||
DEVELOPMENT_TEAM = 82HL67AXQ2;
|
|
||||||
INFOPLIST_FILE = "Tests macOS/Info.plist";
|
|
||||||
LD_RUNPATH_SEARCH_PATHS = (
|
|
||||||
"$(inherited)",
|
|
||||||
"@executable_path/../Frameworks",
|
|
||||||
"@loader_path/../Frameworks",
|
|
||||||
);
|
|
||||||
MACOSX_DEPLOYMENT_TARGET = 10.16;
|
|
||||||
PRODUCT_BUNDLE_IDENTIFIER = "com.metabolist.Tests-macOS";
|
|
||||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
|
||||||
SDKROOT = macosx;
|
|
||||||
SWIFT_VERSION = 5.0;
|
|
||||||
TEST_TARGET_NAME = "Metatext (macOS)";
|
|
||||||
};
|
|
||||||
name = Debug;
|
|
||||||
};
|
|
||||||
D047FAC124C3E21200AF17C5 /* Release */ = {
|
|
||||||
isa = XCBuildConfiguration;
|
|
||||||
buildSettings = {
|
|
||||||
ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES;
|
|
||||||
CODE_SIGN_STYLE = Automatic;
|
|
||||||
COMBINE_HIDPI_IMAGES = YES;
|
|
||||||
DEVELOPMENT_TEAM = 82HL67AXQ2;
|
|
||||||
INFOPLIST_FILE = "Tests macOS/Info.plist";
|
|
||||||
LD_RUNPATH_SEARCH_PATHS = (
|
|
||||||
"$(inherited)",
|
|
||||||
"@executable_path/../Frameworks",
|
|
||||||
"@loader_path/../Frameworks",
|
|
||||||
);
|
|
||||||
MACOSX_DEPLOYMENT_TARGET = 10.16;
|
|
||||||
PRODUCT_BUNDLE_IDENTIFIER = "com.metabolist.Tests-macOS";
|
|
||||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
|
||||||
SDKROOT = macosx;
|
|
||||||
SWIFT_VERSION = 5.0;
|
|
||||||
TEST_TARGET_NAME = "Metatext (macOS)";
|
|
||||||
};
|
|
||||||
name = Release;
|
|
||||||
};
|
|
||||||
/* End XCBuildConfiguration section */
|
/* End XCBuildConfiguration section */
|
||||||
|
|
||||||
/* Begin XCConfigurationList section */
|
/* Begin XCConfigurationList section */
|
||||||
|
@ -678,25 +1001,71 @@
|
||||||
defaultConfigurationIsVisible = 0;
|
defaultConfigurationIsVisible = 0;
|
||||||
defaultConfigurationName = Release;
|
defaultConfigurationName = Release;
|
||||||
};
|
};
|
||||||
D047FABC24C3E21200AF17C5 /* Build configuration list for PBXNativeTarget "Tests iOS" */ = {
|
D0666A2824C677B400F3F04B /* Build configuration list for PBXNativeTarget "Tests" */ = {
|
||||||
isa = XCConfigurationList;
|
isa = XCConfigurationList;
|
||||||
buildConfigurations = (
|
buildConfigurations = (
|
||||||
D047FABD24C3E21200AF17C5 /* Debug */,
|
D0666A2924C677B400F3F04B /* Debug */,
|
||||||
D047FABE24C3E21200AF17C5 /* Release */,
|
D0666A2A24C677B400F3F04B /* Release */,
|
||||||
);
|
|
||||||
defaultConfigurationIsVisible = 0;
|
|
||||||
defaultConfigurationName = Release;
|
|
||||||
};
|
|
||||||
D047FABF24C3E21200AF17C5 /* Build configuration list for PBXNativeTarget "Tests macOS" */ = {
|
|
||||||
isa = XCConfigurationList;
|
|
||||||
buildConfigurations = (
|
|
||||||
D047FAC024C3E21200AF17C5 /* Debug */,
|
|
||||||
D047FAC124C3E21200AF17C5 /* Release */,
|
|
||||||
);
|
);
|
||||||
defaultConfigurationIsVisible = 0;
|
defaultConfigurationIsVisible = 0;
|
||||||
defaultConfigurationName = Release;
|
defaultConfigurationName = Release;
|
||||||
};
|
};
|
||||||
/* End XCConfigurationList section */
|
/* End XCConfigurationList section */
|
||||||
|
|
||||||
|
/* Begin XCRemoteSwiftPackageReference section */
|
||||||
|
D065F53724D37E5100741304 /* XCRemoteSwiftPackageReference "CombineExpectations" */ = {
|
||||||
|
isa = XCRemoteSwiftPackageReference;
|
||||||
|
repositoryURL = "https://github.com/groue/CombineExpectations";
|
||||||
|
requirement = {
|
||||||
|
kind = upToNextMajorVersion;
|
||||||
|
minimumVersion = 0.5.0;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
D0666A4724C6C1A300F3F04B /* XCRemoteSwiftPackageReference "GRDB" */ = {
|
||||||
|
isa = XCRemoteSwiftPackageReference;
|
||||||
|
repositoryURL = "https://github.com/groue/GRDB.swift";
|
||||||
|
requirement = {
|
||||||
|
kind = upToNextMajorVersion;
|
||||||
|
minimumVersion = "5.0.0-beta.10";
|
||||||
|
};
|
||||||
|
};
|
||||||
|
D0DC175D24D016EA00A75C65 /* XCRemoteSwiftPackageReference "Alamofire" */ = {
|
||||||
|
isa = XCRemoteSwiftPackageReference;
|
||||||
|
repositoryURL = "https://github.com/Alamofire/Alamofire";
|
||||||
|
requirement = {
|
||||||
|
kind = upToNextMajorVersion;
|
||||||
|
minimumVersion = 5.2.2;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
/* End XCRemoteSwiftPackageReference section */
|
||||||
|
|
||||||
|
/* Begin XCSwiftPackageProductDependency section */
|
||||||
|
D065F53824D37E5100741304 /* CombineExpectations */ = {
|
||||||
|
isa = XCSwiftPackageProductDependency;
|
||||||
|
package = D065F53724D37E5100741304 /* XCRemoteSwiftPackageReference "CombineExpectations" */;
|
||||||
|
productName = CombineExpectations;
|
||||||
|
};
|
||||||
|
D0666A4824C6C1A300F3F04B /* GRDB */ = {
|
||||||
|
isa = XCSwiftPackageProductDependency;
|
||||||
|
package = D0666A4724C6C1A300F3F04B /* XCRemoteSwiftPackageReference "GRDB" */;
|
||||||
|
productName = GRDB;
|
||||||
|
};
|
||||||
|
D0666A7C24C7745A00F3F04B /* GRDB */ = {
|
||||||
|
isa = XCSwiftPackageProductDependency;
|
||||||
|
package = D0666A4724C6C1A300F3F04B /* XCRemoteSwiftPackageReference "GRDB" */;
|
||||||
|
productName = GRDB;
|
||||||
|
};
|
||||||
|
D0DC175E24D016EA00A75C65 /* Alamofire */ = {
|
||||||
|
isa = XCSwiftPackageProductDependency;
|
||||||
|
package = D0DC175D24D016EA00A75C65 /* XCRemoteSwiftPackageReference "Alamofire" */;
|
||||||
|
productName = Alamofire;
|
||||||
|
};
|
||||||
|
D0DC176024D0171800A75C65 /* Alamofire */ = {
|
||||||
|
isa = XCSwiftPackageProductDependency;
|
||||||
|
package = D0DC175D24D016EA00A75C65 /* XCRemoteSwiftPackageReference "Alamofire" */;
|
||||||
|
productName = Alamofire;
|
||||||
|
};
|
||||||
|
/* End XCSwiftPackageProductDependency section */
|
||||||
};
|
};
|
||||||
rootObject = D047FA8024C3E21000AF17C5 /* Project object */;
|
rootObject = D047FA8024C3E21000AF17C5 /* Project object */;
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,34 @@
|
||||||
|
{
|
||||||
|
"object": {
|
||||||
|
"pins": [
|
||||||
|
{
|
||||||
|
"package": "Alamofire",
|
||||||
|
"repositoryURL": "https://github.com/Alamofire/Alamofire",
|
||||||
|
"state": {
|
||||||
|
"branch": null,
|
||||||
|
"revision": "becd9a729a37bdbef5bc39dc3c702b99f9e3d046",
|
||||||
|
"version": "5.2.2"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"package": "CombineExpectations",
|
||||||
|
"repositoryURL": "https://github.com/groue/CombineExpectations",
|
||||||
|
"state": {
|
||||||
|
"branch": null,
|
||||||
|
"revision": "96d5604151c94b21fbca6877b237e80af9e821dd",
|
||||||
|
"version": "0.5.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"package": "GRDB",
|
||||||
|
"repositoryURL": "https://github.com/groue/GRDB.swift",
|
||||||
|
"state": {
|
||||||
|
"branch": null,
|
||||||
|
"revision": "ededd8668abd5a3c4c43cc9ebcfd611082b47f65",
|
||||||
|
"version": "5.0.0-beta.10"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"version": 1
|
||||||
|
}
|
|
@ -0,0 +1,109 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<Scheme
|
||||||
|
LastUpgradeVersion = "1200"
|
||||||
|
version = "1.3">
|
||||||
|
<BuildAction
|
||||||
|
parallelizeBuildables = "YES"
|
||||||
|
buildImplicitDependencies = "YES">
|
||||||
|
<BuildActionEntries>
|
||||||
|
<BuildActionEntry
|
||||||
|
buildForTesting = "YES"
|
||||||
|
buildForRunning = "YES"
|
||||||
|
buildForProfiling = "YES"
|
||||||
|
buildForArchiving = "YES"
|
||||||
|
buildForAnalyzing = "YES">
|
||||||
|
<BuildableReference
|
||||||
|
BuildableIdentifier = "primary"
|
||||||
|
BlueprintIdentifier = "D047FA8B24C3E21200AF17C5"
|
||||||
|
BuildableName = "Metatext.app"
|
||||||
|
BlueprintName = "Metatext (iOS)"
|
||||||
|
ReferencedContainer = "container:Metatext.xcodeproj">
|
||||||
|
</BuildableReference>
|
||||||
|
</BuildActionEntry>
|
||||||
|
</BuildActionEntries>
|
||||||
|
</BuildAction>
|
||||||
|
<TestAction
|
||||||
|
buildConfiguration = "Debug"
|
||||||
|
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
|
||||||
|
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
||||||
|
shouldUseLaunchSchemeArgsEnv = "YES"
|
||||||
|
codeCoverageEnabled = "YES">
|
||||||
|
<Testables>
|
||||||
|
<TestableReference
|
||||||
|
skipped = "NO">
|
||||||
|
<BuildableReference
|
||||||
|
BuildableIdentifier = "primary"
|
||||||
|
BlueprintIdentifier = "D047FA9B24C3E21200AF17C5"
|
||||||
|
BuildableName = "Tests iOS.xctest"
|
||||||
|
BlueprintName = "Tests iOS"
|
||||||
|
ReferencedContainer = "container:Metatext.xcodeproj">
|
||||||
|
</BuildableReference>
|
||||||
|
</TestableReference>
|
||||||
|
<TestableReference
|
||||||
|
skipped = "NO">
|
||||||
|
<BuildableReference
|
||||||
|
BuildableIdentifier = "primary"
|
||||||
|
BlueprintIdentifier = "D0666A0D24C6737800F3F04B"
|
||||||
|
BuildableName = "MetatextTests.xctest"
|
||||||
|
BlueprintName = "MetatextTests"
|
||||||
|
ReferencedContainer = "container:Metatext.xcodeproj">
|
||||||
|
</BuildableReference>
|
||||||
|
</TestableReference>
|
||||||
|
<TestableReference
|
||||||
|
skipped = "NO">
|
||||||
|
<BuildableReference
|
||||||
|
BuildableIdentifier = "primary"
|
||||||
|
BlueprintIdentifier = "D0666A2024C677B400F3F04B"
|
||||||
|
BuildableName = "Tests.xctest"
|
||||||
|
BlueprintName = "Tests"
|
||||||
|
ReferencedContainer = "container:Metatext.xcodeproj">
|
||||||
|
</BuildableReference>
|
||||||
|
</TestableReference>
|
||||||
|
</Testables>
|
||||||
|
</TestAction>
|
||||||
|
<LaunchAction
|
||||||
|
buildConfiguration = "Debug"
|
||||||
|
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
|
||||||
|
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
||||||
|
launchStyle = "0"
|
||||||
|
useCustomWorkingDirectory = "NO"
|
||||||
|
ignoresPersistentStateOnLaunch = "NO"
|
||||||
|
debugDocumentVersioning = "YES"
|
||||||
|
debugServiceExtension = "internal"
|
||||||
|
allowLocationSimulation = "YES">
|
||||||
|
<BuildableProductRunnable
|
||||||
|
runnableDebuggingMode = "0">
|
||||||
|
<BuildableReference
|
||||||
|
BuildableIdentifier = "primary"
|
||||||
|
BlueprintIdentifier = "D047FA8B24C3E21200AF17C5"
|
||||||
|
BuildableName = "Metatext.app"
|
||||||
|
BlueprintName = "Metatext (iOS)"
|
||||||
|
ReferencedContainer = "container:Metatext.xcodeproj">
|
||||||
|
</BuildableReference>
|
||||||
|
</BuildableProductRunnable>
|
||||||
|
</LaunchAction>
|
||||||
|
<ProfileAction
|
||||||
|
buildConfiguration = "Release"
|
||||||
|
shouldUseLaunchSchemeArgsEnv = "YES"
|
||||||
|
savedToolIdentifier = ""
|
||||||
|
useCustomWorkingDirectory = "NO"
|
||||||
|
debugDocumentVersioning = "YES">
|
||||||
|
<BuildableProductRunnable
|
||||||
|
runnableDebuggingMode = "0">
|
||||||
|
<BuildableReference
|
||||||
|
BuildableIdentifier = "primary"
|
||||||
|
BlueprintIdentifier = "D047FA8B24C3E21200AF17C5"
|
||||||
|
BuildableName = "Metatext.app"
|
||||||
|
BlueprintName = "Metatext (iOS)"
|
||||||
|
ReferencedContainer = "container:Metatext.xcodeproj">
|
||||||
|
</BuildableReference>
|
||||||
|
</BuildableProductRunnable>
|
||||||
|
</ProfileAction>
|
||||||
|
<AnalyzeAction
|
||||||
|
buildConfiguration = "Debug">
|
||||||
|
</AnalyzeAction>
|
||||||
|
<ArchiveAction
|
||||||
|
buildConfiguration = "Release"
|
||||||
|
revealArchiveInOrganizer = "YES">
|
||||||
|
</ArchiveAction>
|
||||||
|
</Scheme>
|
|
@ -1,19 +0,0 @@
|
||||||
<?xml version="1.0" encoding="UTF-8"?>
|
|
||||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
||||||
<plist version="1.0">
|
|
||||||
<dict>
|
|
||||||
<key>SchemeUserState</key>
|
|
||||||
<dict>
|
|
||||||
<key>Metatext (iOS).xcscheme_^#shared#^_</key>
|
|
||||||
<dict>
|
|
||||||
<key>orderHint</key>
|
|
||||||
<integer>0</integer>
|
|
||||||
</dict>
|
|
||||||
<key>Metatext (macOS).xcscheme_^#shared#^_</key>
|
|
||||||
<dict>
|
|
||||||
<key>orderHint</key>
|
|
||||||
<integer>1</integer>
|
|
||||||
</dict>
|
|
||||||
</dict>
|
|
||||||
</dict>
|
|
||||||
</plist>
|
|
15
Shared/Extensions/NSError+Extensions.swift
Normal file
15
Shared/Extensions/NSError+Extensions.swift
Normal file
|
@ -0,0 +1,15 @@
|
||||||
|
// Copyright © 2020 Metabolist. All rights reserved.
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
|
||||||
|
extension NSError {
|
||||||
|
convenience init(status: OSStatus) {
|
||||||
|
var userInfo: [String: Any]?
|
||||||
|
|
||||||
|
if let errorMessage = SecCopyErrorMessageString(status, nil) {
|
||||||
|
userInfo = [NSLocalizedDescriptionKey: errorMessage]
|
||||||
|
}
|
||||||
|
|
||||||
|
self.init(domain: NSOSStatusErrorDomain, code: Int(status), userInfo: userInfo)
|
||||||
|
}
|
||||||
|
}
|
19
Shared/Extensions/Publisher+Extensions.swift
Normal file
19
Shared/Extensions/Publisher+Extensions.swift
Normal file
|
@ -0,0 +1,19 @@
|
||||||
|
// Copyright © 2020 Metabolist. All rights reserved.
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
import Combine
|
||||||
|
|
||||||
|
extension Publisher {
|
||||||
|
func assignErrorsToAlertItem<Root>(
|
||||||
|
to keyPath: ReferenceWritableKeyPath<Root, AlertItem?>,
|
||||||
|
on object: Root) -> AnyPublisher<Output, Never> {
|
||||||
|
self.catch { error -> AnyPublisher<Output, Never> in
|
||||||
|
DispatchQueue.main.async {
|
||||||
|
object[keyPath: keyPath] = AlertItem(error: error)
|
||||||
|
}
|
||||||
|
|
||||||
|
return Empty().eraseToAnyPublisher()
|
||||||
|
}
|
||||||
|
.eraseToAnyPublisher()
|
||||||
|
}
|
||||||
|
}
|
21
Shared/Extensions/String+Extensions.swift
Normal file
21
Shared/Extensions/String+Extensions.swift
Normal file
|
@ -0,0 +1,21 @@
|
||||||
|
// Copyright © 2020 Metabolist. All rights reserved.
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
|
||||||
|
extension String {
|
||||||
|
private static let colonDoubleSlash = "://"
|
||||||
|
|
||||||
|
func url(scheme: String = "https") throws -> URL {
|
||||||
|
let url: URL?
|
||||||
|
|
||||||
|
if hasPrefix(scheme + Self.colonDoubleSlash) {
|
||||||
|
url = URL(string: self)
|
||||||
|
} else {
|
||||||
|
url = URL(string: scheme + Self.colonDoubleSlash + self)
|
||||||
|
}
|
||||||
|
|
||||||
|
guard let validURL = url else { throw URLError(.badURL) }
|
||||||
|
|
||||||
|
return validURL
|
||||||
|
}
|
||||||
|
}
|
12
Shared/Extensions/View+Extensions.swift
Normal file
12
Shared/Extensions/View+Extensions.swift
Normal file
|
@ -0,0 +1,12 @@
|
||||||
|
// Copyright © 2020 Metabolist. All rights reserved.
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
import SwiftUI
|
||||||
|
|
||||||
|
extension View {
|
||||||
|
func alertItem(_ alertItem: Binding<AlertItem?>) -> some View {
|
||||||
|
alert(item: alertItem) {
|
||||||
|
Alert(title: Text($0.error.localizedDescription))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
5
Shared/Localizations/Localizable.strings
Normal file
5
Shared/Localizations/Localizable.strings
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
// Copyright © 2020 Metabolist. All rights reserved.
|
||||||
|
|
||||||
|
"go" = "Go";
|
||||||
|
"add-identity.instance-url" = "Instance URL";
|
||||||
|
"oauth.error.code-not-found" = "OAuth error: code not found";
|
|
@ -4,9 +4,25 @@ import SwiftUI
|
||||||
|
|
||||||
@main
|
@main
|
||||||
struct MetatextApp: App {
|
struct MetatextApp: App {
|
||||||
|
private let identityDatabase: IdentityDatabase
|
||||||
|
private let secrets = Secrets(keychain: Keychain(service: "com.metabolist.metatext"))
|
||||||
|
|
||||||
|
init() {
|
||||||
|
do {
|
||||||
|
try identityDatabase = IdentityDatabase()
|
||||||
|
} catch {
|
||||||
|
fatalError("Failed to initialize identity database")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
var body: some Scene {
|
var body: some Scene {
|
||||||
WindowGroup {
|
WindowGroup {
|
||||||
ContentView()
|
ContentView()
|
||||||
|
.environmentObject(
|
||||||
|
SceneViewModel(
|
||||||
|
networkClient: MastodonClient(),
|
||||||
|
identityDatabase: identityDatabase,
|
||||||
|
secrets: secrets))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
9
Shared/Model/AccessToken.swift
Normal file
9
Shared/Model/AccessToken.swift
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
// Copyright © 2020 Metabolist. All rights reserved.
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
|
||||||
|
struct AccessToken: Codable {
|
||||||
|
let scope: String
|
||||||
|
let tokenType: String
|
||||||
|
let accessToken: String
|
||||||
|
}
|
32
Shared/Model/Account.swift
Normal file
32
Shared/Model/Account.swift
Normal file
|
@ -0,0 +1,32 @@
|
||||||
|
// Copyright © 2020 Metabolist. All rights reserved.
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
|
||||||
|
struct Account: Codable, Hashable {
|
||||||
|
struct Field: Codable, Hashable {
|
||||||
|
let name: String
|
||||||
|
let value: String
|
||||||
|
let verifiedAt: Date?
|
||||||
|
}
|
||||||
|
|
||||||
|
let id: String
|
||||||
|
let username: String
|
||||||
|
let acct: String
|
||||||
|
let displayName: String
|
||||||
|
let locked: Bool
|
||||||
|
let createdAt: Date
|
||||||
|
let followersCount: Int
|
||||||
|
let followingCount: Int
|
||||||
|
let statusesCount: Int
|
||||||
|
let note: String
|
||||||
|
let url: URL
|
||||||
|
let avatar: URL
|
||||||
|
let avatarStatic: URL
|
||||||
|
let header: URL
|
||||||
|
let headerStatic: URL
|
||||||
|
let fields: [Field]
|
||||||
|
let emojis: [Emoji]
|
||||||
|
let bot: Bool?
|
||||||
|
let moved: Bool?
|
||||||
|
let discoverable: Bool?
|
||||||
|
}
|
8
Shared/Model/AlertItem.swift
Normal file
8
Shared/Model/AlertItem.swift
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
// Copyright © 2020 Metabolist. All rights reserved.
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
|
||||||
|
struct AlertItem: Identifiable {
|
||||||
|
let id = UUID()
|
||||||
|
let error: Error
|
||||||
|
}
|
13
Shared/Model/AppAuthorization.swift
Normal file
13
Shared/Model/AppAuthorization.swift
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
// Copyright © 2020 Metabolist. All rights reserved.
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
|
||||||
|
struct AppAuthorization: Codable {
|
||||||
|
let id: String
|
||||||
|
let clientId: String
|
||||||
|
let clientSecret: String
|
||||||
|
let name: String
|
||||||
|
let redirectUri: String
|
||||||
|
let website: String?
|
||||||
|
let vapidKey: String?
|
||||||
|
}
|
8
Shared/Model/Application.swift
Normal file
8
Shared/Model/Application.swift
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
// Copyright © 2020 Metabolist. All rights reserved.
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
|
||||||
|
struct Application: Codable {
|
||||||
|
let name: String
|
||||||
|
let website: String
|
||||||
|
}
|
7
Shared/Model/DatabaseError.swift
Normal file
7
Shared/Model/DatabaseError.swift
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
// Copyright © 2020 Metabolist. All rights reserved.
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
|
||||||
|
enum DatabaseError: Error {
|
||||||
|
case documentsDirectoryNotFound
|
||||||
|
}
|
10
Shared/Model/Emoji.swift
Normal file
10
Shared/Model/Emoji.swift
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
// Copyright © 2020 Metabolist. All rights reserved.
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
|
||||||
|
struct Emoji: Codable, Hashable {
|
||||||
|
let shortcode: String
|
||||||
|
let staticUrl: URL
|
||||||
|
let url: URL
|
||||||
|
let visibleInPicker: Bool
|
||||||
|
}
|
40
Shared/Model/Identity.swift
Normal file
40
Shared/Model/Identity.swift
Normal file
|
@ -0,0 +1,40 @@
|
||||||
|
// Copyright © 2020 Metabolist. All rights reserved.
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
|
||||||
|
struct Identity: Codable, Hashable {
|
||||||
|
let id: String
|
||||||
|
let url: URL
|
||||||
|
let instance: Identity.Instance?
|
||||||
|
let account: Identity.Account?
|
||||||
|
}
|
||||||
|
|
||||||
|
extension Identity {
|
||||||
|
struct Instance: Codable, Hashable {
|
||||||
|
let uri: String
|
||||||
|
let streamingAPI: URL
|
||||||
|
let title: String
|
||||||
|
let thumbnail: URL?
|
||||||
|
}
|
||||||
|
|
||||||
|
struct Account: Codable, Hashable {
|
||||||
|
let id: String
|
||||||
|
let identityID: String
|
||||||
|
let username: String
|
||||||
|
let url: URL
|
||||||
|
let avatar: URL
|
||||||
|
let avatarStatic: URL
|
||||||
|
let header: URL
|
||||||
|
let headerStatic: URL
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension Identity {
|
||||||
|
var handle: String {
|
||||||
|
if let account = account, let host = account.url.host {
|
||||||
|
return account.url.lastPathComponent + "@" + host
|
||||||
|
}
|
||||||
|
|
||||||
|
return instance?.title ?? url.host ?? url.absoluteString
|
||||||
|
}
|
||||||
|
}
|
163
Shared/Model/IdentityDatabase.swift
Normal file
163
Shared/Model/IdentityDatabase.swift
Normal file
|
@ -0,0 +1,163 @@
|
||||||
|
// Copyright © 2020 Metabolist. All rights reserved.
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
import Combine
|
||||||
|
import GRDB
|
||||||
|
|
||||||
|
struct IdentityDatabase {
|
||||||
|
private let databaseQueue: DatabaseQueue
|
||||||
|
|
||||||
|
init(inMemory: Bool = false) throws {
|
||||||
|
guard
|
||||||
|
let documentsDirectory = NSSearchPathForDirectoriesInDomains(
|
||||||
|
.documentDirectory,
|
||||||
|
.userDomainMask, true)
|
||||||
|
.first
|
||||||
|
else { throw DatabaseError.documentsDirectoryNotFound }
|
||||||
|
|
||||||
|
if inMemory {
|
||||||
|
databaseQueue = DatabaseQueue()
|
||||||
|
} else {
|
||||||
|
databaseQueue = try DatabaseQueue(path: "\(documentsDirectory)/IdentityDatabase.sqlite3")
|
||||||
|
}
|
||||||
|
|
||||||
|
try Self.migrate(databaseQueue)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension IdentityDatabase {
|
||||||
|
func createIdentity(id: String, url: URL) -> AnyPublisher<Identity, Error> {
|
||||||
|
databaseQueue.writePublisher {
|
||||||
|
try StoredIdentity(id: id, url: url, instanceURI: nil).save($0)
|
||||||
|
|
||||||
|
return Identity(id: id, url: url, instance: nil, account: nil)
|
||||||
|
}
|
||||||
|
.eraseToAnyPublisher()
|
||||||
|
}
|
||||||
|
|
||||||
|
func updateInstance(_ instance: Instance, forIdentityID identityID: String) -> AnyPublisher<Identity?, Error> {
|
||||||
|
databaseQueue.writePublisher {
|
||||||
|
try Identity.Instance(
|
||||||
|
uri: instance.uri,
|
||||||
|
streamingAPI: instance.urls.streamingApi,
|
||||||
|
title: instance.title,
|
||||||
|
thumbnail: instance.thumbnail)
|
||||||
|
.save($0)
|
||||||
|
try StoredIdentity
|
||||||
|
.filter(Column("id") == identityID)
|
||||||
|
.updateAll($0, Column("instanceURI").set(to: instance.uri))
|
||||||
|
|
||||||
|
return try Self.fetchIdentity(id: identityID, db: $0)
|
||||||
|
}
|
||||||
|
.eraseToAnyPublisher()
|
||||||
|
}
|
||||||
|
|
||||||
|
func updateAccount(_ account: Account, forIdentityID identityID: String) -> AnyPublisher<Identity?, Error> {
|
||||||
|
databaseQueue.writePublisher {
|
||||||
|
try Identity.Account(
|
||||||
|
id: account.id,
|
||||||
|
identityID: identityID,
|
||||||
|
username: account.username,
|
||||||
|
url: account.url,
|
||||||
|
avatar: account.avatar,
|
||||||
|
avatarStatic: account.avatarStatic,
|
||||||
|
header: account.header,
|
||||||
|
headerStatic: account.headerStatic)
|
||||||
|
.save($0)
|
||||||
|
|
||||||
|
return try Self.fetchIdentity(id: identityID, db: $0)
|
||||||
|
}
|
||||||
|
.eraseToAnyPublisher()
|
||||||
|
}
|
||||||
|
|
||||||
|
func identity(id: String) throws -> Identity? {
|
||||||
|
try databaseQueue.read { try Self.fetchIdentity(id: id, db: $0) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private extension IdentityDatabase {
|
||||||
|
private static func migrate(_ writer: DatabaseWriter) throws {
|
||||||
|
var migrator = DatabaseMigrator()
|
||||||
|
|
||||||
|
migrator.registerMigration("createIdentities") { db in
|
||||||
|
try db.create(table: "instance", ifNotExists: true) { t in
|
||||||
|
t.column("uri", .text).notNull().primaryKey(onConflict: .replace)
|
||||||
|
t.column("streamingAPI", .text)
|
||||||
|
t.column("title", .text)
|
||||||
|
t.column("thumbnail", .text)
|
||||||
|
}
|
||||||
|
|
||||||
|
try db.create(table: "storedIdentity", ifNotExists: true) { t in
|
||||||
|
t.column("id", .text).notNull().primaryKey(onConflict: .replace)
|
||||||
|
t.column("url", .text).notNull()
|
||||||
|
t.column("instanceURI", .text)
|
||||||
|
.indexed()
|
||||||
|
.references("instance", column: "uri")
|
||||||
|
}
|
||||||
|
|
||||||
|
try db.create(table: "account", ifNotExists: true) { t in
|
||||||
|
t.column("id", .text).notNull().primaryKey(onConflict: .replace)
|
||||||
|
t.column("identityID", .text)
|
||||||
|
.notNull()
|
||||||
|
.indexed()
|
||||||
|
.references("storedIdentity", column: "id", onDelete: .cascade)
|
||||||
|
t.column("username", .text).notNull()
|
||||||
|
t.column("url", .text).notNull()
|
||||||
|
t.column("avatar", .text).notNull()
|
||||||
|
t.column("avatarStatic", .text).notNull()
|
||||||
|
t.column("header", .text).notNull()
|
||||||
|
t.column("headerStatic", .text).notNull()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
try migrator.migrate(writer)
|
||||||
|
}
|
||||||
|
|
||||||
|
private static func fetchIdentity(id: String, db: Database) throws -> Identity? {
|
||||||
|
if let result = try StoredIdentity
|
||||||
|
.filter(Column("id") == id)
|
||||||
|
.including(optional: StoredIdentity.instance)
|
||||||
|
.including(optional: StoredIdentity.account)
|
||||||
|
.asRequest(of: IdentityResult.self)
|
||||||
|
.fetchOne(db) {
|
||||||
|
return Identity(result: result)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private struct StoredIdentity: Codable, Hashable, TableRecord, FetchableRecord, PersistableRecord {
|
||||||
|
let id: String
|
||||||
|
let url: URL
|
||||||
|
let instanceURI: String?
|
||||||
|
}
|
||||||
|
|
||||||
|
extension StoredIdentity {
|
||||||
|
static let instance = belongsTo(Identity.Instance.self, key: "instance")
|
||||||
|
static let account = hasOne(Identity.Account.self, key: "account")
|
||||||
|
|
||||||
|
var instance: QueryInterfaceRequest<Identity.Instance> {
|
||||||
|
request(for: Self.instance)
|
||||||
|
}
|
||||||
|
|
||||||
|
var account: QueryInterfaceRequest<Identity.Account> {
|
||||||
|
request(for: Self.account)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private struct IdentityResult: Codable, Hashable, FetchableRecord {
|
||||||
|
let identity: StoredIdentity
|
||||||
|
let instance: Identity.Instance?
|
||||||
|
let account: Identity.Account?
|
||||||
|
}
|
||||||
|
|
||||||
|
private extension Identity {
|
||||||
|
init(result: IdentityResult) {
|
||||||
|
self.init(id: result.identity.id, url: result.identity.url, instance: result.instance, account: result.account)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension Identity.Instance: TableRecord, FetchableRecord, PersistableRecord {}
|
||||||
|
|
||||||
|
extension Identity.Account: TableRecord, FetchableRecord, PersistableRecord {}
|
30
Shared/Model/Instance.swift
Normal file
30
Shared/Model/Instance.swift
Normal file
|
@ -0,0 +1,30 @@
|
||||||
|
// Copyright © 2020 Metabolist. All rights reserved.
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
|
||||||
|
struct Instance: Codable, Hashable {
|
||||||
|
struct URLs: Codable, Hashable {
|
||||||
|
let streamingApi: URL
|
||||||
|
}
|
||||||
|
|
||||||
|
struct Stats: Codable, Hashable {
|
||||||
|
let userCount: Int
|
||||||
|
let statusCount: Int
|
||||||
|
let domainCount: Int
|
||||||
|
}
|
||||||
|
|
||||||
|
let uri: String
|
||||||
|
let title: String
|
||||||
|
let description: String
|
||||||
|
let shortDescription: String?
|
||||||
|
let email: String
|
||||||
|
let version: String
|
||||||
|
let languages: [String]
|
||||||
|
let registrations: Bool?
|
||||||
|
let approvalRequired: Bool?
|
||||||
|
let invitesEnabled: Bool?
|
||||||
|
let urls: URLs
|
||||||
|
let stats: Stats
|
||||||
|
let thumbnail: URL?
|
||||||
|
let contactAccount: Account?
|
||||||
|
}
|
64
Shared/Model/Keychain.swift
Normal file
64
Shared/Model/Keychain.swift
Normal file
|
@ -0,0 +1,64 @@
|
||||||
|
// Copyright © 2020 Metabolist. All rights reserved.
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
|
||||||
|
protocol KeychainType {
|
||||||
|
mutating func set(data: Data, forKey key: String) throws
|
||||||
|
mutating func deleteData(key: String) throws
|
||||||
|
func getData(key: String) throws -> Data?
|
||||||
|
}
|
||||||
|
|
||||||
|
struct Keychain {
|
||||||
|
let service: String
|
||||||
|
}
|
||||||
|
|
||||||
|
extension Keychain: KeychainType {
|
||||||
|
mutating func set(data: Data, forKey key: String) throws {
|
||||||
|
var query = queryDictionary(key: key)
|
||||||
|
|
||||||
|
query[kSecValueData as String] = data
|
||||||
|
|
||||||
|
let status = SecItemAdd(query as CFDictionary, nil)
|
||||||
|
|
||||||
|
if status != errSecSuccess {
|
||||||
|
throw NSError(status: status)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
mutating func deleteData(key: String) throws {
|
||||||
|
let status = SecItemDelete(queryDictionary(key: key) as CFDictionary)
|
||||||
|
|
||||||
|
if status != errSecSuccess {
|
||||||
|
throw NSError(status: status)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func getData(key: String) throws -> Data? {
|
||||||
|
var result: AnyObject?
|
||||||
|
var query = queryDictionary(key: key)
|
||||||
|
|
||||||
|
query[kSecMatchLimit as String] = kSecMatchLimitOne
|
||||||
|
query[kSecReturnData as String] = kCFBooleanTrue
|
||||||
|
|
||||||
|
let status = SecItemCopyMatching(query as CFDictionary, &result)
|
||||||
|
|
||||||
|
switch status {
|
||||||
|
case errSecSuccess:
|
||||||
|
return result as? Data
|
||||||
|
case errSecItemNotFound:
|
||||||
|
return nil
|
||||||
|
default:
|
||||||
|
throw NSError(status: status)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private extension Keychain {
|
||||||
|
private func queryDictionary(key: String) -> [String: Any] {
|
||||||
|
[
|
||||||
|
kSecAttrService as String: service,
|
||||||
|
kSecAttrAccount as String: key,
|
||||||
|
kSecClass as String: kSecClassGenericPassword
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
11
Shared/Model/MastodonError.swift
Normal file
11
Shared/Model/MastodonError.swift
Normal file
|
@ -0,0 +1,11 @@
|
||||||
|
// Copyright © 2020 Metabolist. All rights reserved.
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
|
||||||
|
struct MastodonError: Error, Codable {
|
||||||
|
let error: String
|
||||||
|
}
|
||||||
|
|
||||||
|
extension MastodonError: LocalizedError {
|
||||||
|
var errorDescription: String? { error }
|
||||||
|
}
|
70
Shared/Model/Secrets.swift
Normal file
70
Shared/Model/Secrets.swift
Normal file
|
@ -0,0 +1,70 @@
|
||||||
|
// Copyright © 2020 Metabolist. All rights reserved.
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
|
||||||
|
protocol SecretsStorable {
|
||||||
|
var dataStoredInSecrets: Data { get }
|
||||||
|
static func fromDataStoredInSecrets(_ data: Data) throws -> Self
|
||||||
|
}
|
||||||
|
|
||||||
|
enum SecretsStorableError: Error {
|
||||||
|
case conversionFromDataStoredInSecrets(Data)
|
||||||
|
}
|
||||||
|
|
||||||
|
class Secrets {
|
||||||
|
private var keychain: KeychainType
|
||||||
|
|
||||||
|
init(keychain: KeychainType) {
|
||||||
|
self.keychain = keychain
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension Secrets {
|
||||||
|
enum Item: String {
|
||||||
|
case clientID = "client-id"
|
||||||
|
case clientSecret = "client-secret"
|
||||||
|
case accessToken = "access-token"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension Secrets {
|
||||||
|
func set(_ data: SecretsStorable, forItem item: Item, forIdentityID identityID: String) throws {
|
||||||
|
try keychain.set(data: data.dataStoredInSecrets, forKey: Self.key(item: item, identityID: identityID))
|
||||||
|
}
|
||||||
|
|
||||||
|
func item<T: SecretsStorable>(_ item: Item, forIdentityID identityID: String) throws -> T? {
|
||||||
|
guard let data = try keychain.getData(key: Self.key(item: item, identityID: identityID)) else { return nil }
|
||||||
|
|
||||||
|
return try T.fromDataStoredInSecrets(data)
|
||||||
|
}
|
||||||
|
|
||||||
|
func delete(_ item: Item, forIdentityID identityID: String) throws {
|
||||||
|
try keychain.deleteData(key: Self.key(item: item, identityID: identityID))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private extension Secrets {
|
||||||
|
static func key(item: Item, identityID: String) -> String {
|
||||||
|
identityID + "." + item.rawValue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension Data: SecretsStorable {
|
||||||
|
var dataStoredInSecrets: Data { self }
|
||||||
|
|
||||||
|
static func fromDataStoredInSecrets(_ data: Data) throws -> Data {
|
||||||
|
data
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension String: SecretsStorable {
|
||||||
|
var dataStoredInSecrets: Data { Data(utf8) }
|
||||||
|
|
||||||
|
static func fromDataStoredInSecrets(_ data: Data) throws -> String {
|
||||||
|
guard let string = String(data: data, encoding: .utf8) else {
|
||||||
|
throw SecretsStorableError.conversionFromDataStoredInSecrets(data)
|
||||||
|
}
|
||||||
|
|
||||||
|
return string
|
||||||
|
}
|
||||||
|
}
|
57
Shared/Networking/HTTPClient.swift
Normal file
57
Shared/Networking/HTTPClient.swift
Normal file
|
@ -0,0 +1,57 @@
|
||||||
|
// Copyright © 2020 Metabolist. All rights reserved.
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
import Combine
|
||||||
|
import Alamofire
|
||||||
|
|
||||||
|
class HTTPClient {
|
||||||
|
private let session: Session
|
||||||
|
private let decoder: DataDecoder
|
||||||
|
|
||||||
|
init(
|
||||||
|
configuration: URLSessionConfiguration = URLSessionConfiguration.af.default,
|
||||||
|
decoder: DataDecoder = JSONDecoder()) {
|
||||||
|
self.session = Session(configuration: configuration)
|
||||||
|
self.decoder = decoder
|
||||||
|
}
|
||||||
|
|
||||||
|
func request<T: DecodableTarget>(_ target: T) -> AnyPublisher<T.ResultType, Error> {
|
||||||
|
requestPublisher(target).value().mapError { $0 as Error }.eraseToAnyPublisher()
|
||||||
|
}
|
||||||
|
|
||||||
|
func request<T: DecodableTarget, E: Error & Decodable>(
|
||||||
|
_ target: T,
|
||||||
|
decodeErrorsAs errorType: E.Type) -> AnyPublisher<T.ResultType, Error> {
|
||||||
|
let decoder = self.decoder
|
||||||
|
|
||||||
|
return requestPublisher(target)
|
||||||
|
.tryMap { response -> T.ResultType in
|
||||||
|
switch response.result {
|
||||||
|
case let .success(decoded): return decoded
|
||||||
|
case let .failure(error):
|
||||||
|
if
|
||||||
|
let data = response.data,
|
||||||
|
let decodedError = try? decoder.decode(E.self, from: data) {
|
||||||
|
throw decodedError
|
||||||
|
}
|
||||||
|
|
||||||
|
throw error
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.eraseToAnyPublisher()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private extension HTTPClient {
|
||||||
|
private func requestPublisher<T: DecodableTarget>(_ target: T) -> DataResponsePublisher<T.ResultType> {
|
||||||
|
#if DEBUG
|
||||||
|
if let url = try? target.asURLRequest().url {
|
||||||
|
StubbingURLProtocol.setTarget(target, forURL: url)
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
|
return session.request(target)
|
||||||
|
.validate()
|
||||||
|
.publishDecodable(type: T.ResultType.self, decoder: decoder)
|
||||||
|
}
|
||||||
|
}
|
29
Shared/Networking/HTTPTarget.swift
Normal file
29
Shared/Networking/HTTPTarget.swift
Normal file
|
@ -0,0 +1,29 @@
|
||||||
|
// Copyright © 2020 Metabolist. All rights reserved.
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
import Alamofire
|
||||||
|
|
||||||
|
protocol HTTPTarget: URLRequestConvertible {
|
||||||
|
var baseURL: URL { get }
|
||||||
|
var pathComponents: [String] { get }
|
||||||
|
var method: HTTPMethod { get }
|
||||||
|
var encoding: ParameterEncoding { get }
|
||||||
|
var parameters: [String: Any]? { get }
|
||||||
|
var headers: HTTPHeaders? { get }
|
||||||
|
}
|
||||||
|
|
||||||
|
extension HTTPTarget {
|
||||||
|
func asURLRequest() throws -> URLRequest {
|
||||||
|
var url = baseURL
|
||||||
|
|
||||||
|
for pathComponent in pathComponents {
|
||||||
|
url.appendPathComponent(pathComponent)
|
||||||
|
}
|
||||||
|
|
||||||
|
return try encoding.encode(try URLRequest(url: url, method: method, headers: headers), with: parameters)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protocol DecodableTarget: HTTPTarget {
|
||||||
|
associatedtype ResultType: Decodable
|
||||||
|
}
|
45
Shared/Networking/Mastodon API/AccessTokenEndpoint.swift
Normal file
45
Shared/Networking/Mastodon API/AccessTokenEndpoint.swift
Normal file
|
@ -0,0 +1,45 @@
|
||||||
|
// Copyright © 2020 Metabolist. All rights reserved.
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
import Alamofire
|
||||||
|
|
||||||
|
enum AccessTokenEndpoint {
|
||||||
|
case oauthToken(
|
||||||
|
clientID: String,
|
||||||
|
clientSecret: String,
|
||||||
|
code: String,
|
||||||
|
grantType: String,
|
||||||
|
scopes: String,
|
||||||
|
redirectURI: String
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
extension AccessTokenEndpoint: MastodonEndpoint {
|
||||||
|
typealias ResultType = AccessToken
|
||||||
|
|
||||||
|
var context: [String] { [] }
|
||||||
|
|
||||||
|
var pathComponentsInContext: [String] {
|
||||||
|
["oauth", "token"]
|
||||||
|
}
|
||||||
|
|
||||||
|
var method: HTTPMethod {
|
||||||
|
switch self {
|
||||||
|
case .oauthToken: return .post
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var parameters: [String: Any]? {
|
||||||
|
switch self {
|
||||||
|
case let .oauthToken(clientID, clientSecret, code, grantType, scopes, redirectURI):
|
||||||
|
return [
|
||||||
|
"client_id": clientID,
|
||||||
|
"client_secret": clientSecret,
|
||||||
|
"code": code,
|
||||||
|
"grant_type": grantType,
|
||||||
|
"scope": scopes,
|
||||||
|
"redirect_uri": redirectURI
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
28
Shared/Networking/Mastodon API/AccountEndpoint.swift
Normal file
28
Shared/Networking/Mastodon API/AccountEndpoint.swift
Normal file
|
@ -0,0 +1,28 @@
|
||||||
|
// Copyright © 2020 Metabolist. All rights reserved.
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
import Alamofire
|
||||||
|
|
||||||
|
enum AccountEndpoint {
|
||||||
|
case verifyCredentials
|
||||||
|
}
|
||||||
|
|
||||||
|
extension AccountEndpoint: MastodonEndpoint {
|
||||||
|
typealias ResultType = Account
|
||||||
|
|
||||||
|
var context: [String] {
|
||||||
|
defaultContext + ["accounts"]
|
||||||
|
}
|
||||||
|
|
||||||
|
var pathComponentsInContext: [String] {
|
||||||
|
switch self {
|
||||||
|
case .verifyCredentials: return ["verify_credentials"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var method: HTTPMethod {
|
||||||
|
switch self {
|
||||||
|
case .verifyCredentials: return .get
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,39 @@
|
||||||
|
// Copyright © 2020 Metabolist. All rights reserved.
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
import Alamofire
|
||||||
|
|
||||||
|
enum AppAuthorizationEndpoint {
|
||||||
|
case apps(clientName: String, redirectURI: String, scopes: String, website: URL?)
|
||||||
|
}
|
||||||
|
|
||||||
|
extension AppAuthorizationEndpoint: MastodonEndpoint {
|
||||||
|
typealias ResultType = AppAuthorization
|
||||||
|
|
||||||
|
var pathComponentsInContext: [String] {
|
||||||
|
switch self {
|
||||||
|
case .apps: return ["apps"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var method: HTTPMethod {
|
||||||
|
switch self {
|
||||||
|
case .apps: return .post
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var parameters: [String: Any]? {
|
||||||
|
switch self {
|
||||||
|
case let .apps(clientName, redirectURI, scopes, website):
|
||||||
|
var params = [
|
||||||
|
"client_name": clientName,
|
||||||
|
"redirect_uris": redirectURI,
|
||||||
|
"scopes": scopes
|
||||||
|
]
|
||||||
|
|
||||||
|
params["website"] = website?.absoluteString
|
||||||
|
|
||||||
|
return params
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
24
Shared/Networking/Mastodon API/InstanceEndpoint.swift
Normal file
24
Shared/Networking/Mastodon API/InstanceEndpoint.swift
Normal file
|
@ -0,0 +1,24 @@
|
||||||
|
// Copyright © 2020 Metabolist. All rights reserved.
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
import Alamofire
|
||||||
|
|
||||||
|
enum InstanceEndpoint {
|
||||||
|
case instance
|
||||||
|
}
|
||||||
|
|
||||||
|
extension InstanceEndpoint: MastodonEndpoint {
|
||||||
|
typealias ResultType = Instance
|
||||||
|
|
||||||
|
var pathComponentsInContext: [String] {
|
||||||
|
switch self {
|
||||||
|
case .instance: return ["instance"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var method: HTTPMethod {
|
||||||
|
switch self {
|
||||||
|
case .instance: return .get
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
28
Shared/Networking/Mastodon API/MastodonAPI.swift
Normal file
28
Shared/Networking/Mastodon API/MastodonAPI.swift
Normal file
|
@ -0,0 +1,28 @@
|
||||||
|
// Copyright © 2020 Metabolist. All rights reserved.
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
|
||||||
|
struct MastodonAPI {
|
||||||
|
static let dateFormat = "yyyy-MM-dd'T'HH:mm:ss.SSSZ"
|
||||||
|
|
||||||
|
struct OAuth {
|
||||||
|
static let clientName = "Metatext"
|
||||||
|
static let scopes = "read write follow push"
|
||||||
|
static let codeCallbackQueryItemName = "code"
|
||||||
|
static let grantType = "authorization_code"
|
||||||
|
static let callbackURLScheme = "metatext"
|
||||||
|
}
|
||||||
|
|
||||||
|
enum OAuthError {
|
||||||
|
case codeNotFound
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension MastodonAPI.OAuthError: LocalizedError {
|
||||||
|
var errorDescription: String? {
|
||||||
|
switch self {
|
||||||
|
case .codeNotFound:
|
||||||
|
return NSLocalizedString("oauth.error.code-not-found", comment: "")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
30
Shared/Networking/Mastodon API/MastodonClient.swift
Normal file
30
Shared/Networking/Mastodon API/MastodonClient.swift
Normal file
|
@ -0,0 +1,30 @@
|
||||||
|
// Copyright © 2020 Metabolist. All rights reserved.
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
import Combine
|
||||||
|
import Alamofire
|
||||||
|
|
||||||
|
class MastodonClient: HTTPClient {
|
||||||
|
var instanceURL: URL?
|
||||||
|
var accessToken: String?
|
||||||
|
|
||||||
|
init(configuration: URLSessionConfiguration = URLSessionConfiguration.af.default) {
|
||||||
|
super.init(configuration: configuration, decoder: MastodonDecoder())
|
||||||
|
}
|
||||||
|
|
||||||
|
override func request<T: DecodableTarget>(_ target: T) -> AnyPublisher<T.ResultType, Error> {
|
||||||
|
super.request(target, decodeErrorsAs: MastodonError.self)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension MastodonClient {
|
||||||
|
func request<E: MastodonEndpoint>(_ endpoint: E) -> AnyPublisher<E.ResultType, Error> {
|
||||||
|
guard let instanceURL = instanceURL else {
|
||||||
|
return Fail(error: URLError(.badURL)).eraseToAnyPublisher()
|
||||||
|
}
|
||||||
|
|
||||||
|
return super.request(
|
||||||
|
MastodonTarget(baseURL: instanceURL, endpoint: endpoint, accessToken: accessToken),
|
||||||
|
decodeErrorsAs: MastodonError.self)
|
||||||
|
}
|
||||||
|
}
|
42
Shared/Networking/Mastodon API/MastodonEndpoint.swift
Normal file
42
Shared/Networking/Mastodon API/MastodonEndpoint.swift
Normal file
|
@ -0,0 +1,42 @@
|
||||||
|
// Copyright © 2020 Metabolist. All rights reserved.
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
import Alamofire
|
||||||
|
|
||||||
|
protocol MastodonEndpoint {
|
||||||
|
associatedtype ResultType: Decodable
|
||||||
|
var APIVersion: String { get }
|
||||||
|
var context: [String] { get }
|
||||||
|
var pathComponentsInContext: [String] { get }
|
||||||
|
var method: HTTPMethod { get }
|
||||||
|
var encoding: ParameterEncoding { get }
|
||||||
|
var parameters: [String: Any]? { get }
|
||||||
|
var headers: HTTPHeaders? { get }
|
||||||
|
}
|
||||||
|
|
||||||
|
extension MastodonEndpoint {
|
||||||
|
var defaultContext: [String] {
|
||||||
|
["api", APIVersion]
|
||||||
|
}
|
||||||
|
|
||||||
|
var APIVersion: String { "v1" }
|
||||||
|
|
||||||
|
var context: [String] {
|
||||||
|
defaultContext
|
||||||
|
}
|
||||||
|
|
||||||
|
var pathComponents: [String] {
|
||||||
|
context + pathComponentsInContext
|
||||||
|
}
|
||||||
|
|
||||||
|
var encoding: ParameterEncoding {
|
||||||
|
switch method {
|
||||||
|
case .get: return URLEncoding.default
|
||||||
|
default: return JSONEncoding.default
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var parameters: [String: Any]? { nil }
|
||||||
|
|
||||||
|
var headers: HTTPHeaders? { nil }
|
||||||
|
}
|
36
Shared/Networking/Mastodon API/MastodonTarget.swift
Normal file
36
Shared/Networking/Mastodon API/MastodonTarget.swift
Normal file
|
@ -0,0 +1,36 @@
|
||||||
|
// Copyright © 2020 Metabolist. All rights reserved.
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
import Alamofire
|
||||||
|
|
||||||
|
struct MastodonTarget<E: MastodonEndpoint> {
|
||||||
|
let baseURL: URL
|
||||||
|
let endpoint: E
|
||||||
|
let accessToken: String?
|
||||||
|
}
|
||||||
|
|
||||||
|
extension MastodonTarget: DecodableTarget {
|
||||||
|
typealias ResultType = E.ResultType
|
||||||
|
|
||||||
|
var pathComponents: [String] { endpoint.pathComponents }
|
||||||
|
|
||||||
|
var method: HTTPMethod { endpoint.method }
|
||||||
|
|
||||||
|
var encoding: ParameterEncoding { endpoint.encoding }
|
||||||
|
|
||||||
|
var parameters: [String: Any]? { endpoint.parameters }
|
||||||
|
|
||||||
|
var headers: HTTPHeaders? {
|
||||||
|
var headers = endpoint.headers
|
||||||
|
|
||||||
|
if let accessToken = accessToken {
|
||||||
|
if headers == nil {
|
||||||
|
headers = HTTPHeaders()
|
||||||
|
}
|
||||||
|
|
||||||
|
headers?.add(.authorization(bearerToken: accessToken))
|
||||||
|
}
|
||||||
|
|
||||||
|
return headers
|
||||||
|
}
|
||||||
|
}
|
15
Shared/Networking/MastodonDecoder.swift
Normal file
15
Shared/Networking/MastodonDecoder.swift
Normal file
|
@ -0,0 +1,15 @@
|
||||||
|
// Copyright © 2020 Metabolist. All rights reserved.
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
|
||||||
|
class MastodonDecoder: JSONDecoder {
|
||||||
|
override init() {
|
||||||
|
super.init()
|
||||||
|
|
||||||
|
let dateFormatter = DateFormatter()
|
||||||
|
|
||||||
|
dateFormatter.dateFormat = MastodonAPI.dateFormat
|
||||||
|
dateDecodingStrategy = .formatted(dateFormatter)
|
||||||
|
keyDecodingStrategy = .convertFromSnakeCase
|
||||||
|
}
|
||||||
|
}
|
16
Shared/Networking/MastodonEncoder.swift
Normal file
16
Shared/Networking/MastodonEncoder.swift
Normal file
|
@ -0,0 +1,16 @@
|
||||||
|
// Copyright © 2020 Metabolist. All rights reserved.
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
|
||||||
|
class MastodonEncoder: JSONEncoder {
|
||||||
|
override init() {
|
||||||
|
super.init()
|
||||||
|
|
||||||
|
let dateFormatter = DateFormatter()
|
||||||
|
|
||||||
|
dateFormatter.dateFormat = MastodonAPI.dateFormat
|
||||||
|
dateEncodingStrategy = .formatted(dateFormatter)
|
||||||
|
keyEncodingStrategy = .convertToSnakeCase
|
||||||
|
outputFormatting = .sortedKeys
|
||||||
|
}
|
||||||
|
}
|
38
Shared/Networking/WebAuthenticationSession.swift
Normal file
38
Shared/Networking/WebAuthenticationSession.swift
Normal file
|
@ -0,0 +1,38 @@
|
||||||
|
// Copyright © 2020 Metabolist. All rights reserved.
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
import AuthenticationServices
|
||||||
|
import Combine
|
||||||
|
|
||||||
|
protocol WebAuthenticationSessionType: AnyObject {
|
||||||
|
init(url URL: URL,
|
||||||
|
callbackURLScheme: String?,
|
||||||
|
completionHandler: @escaping ASWebAuthenticationSession.CompletionHandler)
|
||||||
|
var presentationContextProvider: ASWebAuthenticationPresentationContextProviding? { get set }
|
||||||
|
@discardableResult func start() -> Bool
|
||||||
|
}
|
||||||
|
|
||||||
|
extension ASWebAuthenticationSession: WebAuthenticationSessionType {}
|
||||||
|
|
||||||
|
extension WebAuthenticationSessionType {
|
||||||
|
static func publisher(
|
||||||
|
url: URL,
|
||||||
|
callbackURLScheme: String?,
|
||||||
|
presentationContextProvider: ASWebAuthenticationPresentationContextProviding) -> AnyPublisher<URL?, Error> {
|
||||||
|
Future<URL?, Error> { promise in
|
||||||
|
let webAuthenticationSession = Self(
|
||||||
|
url: url,
|
||||||
|
callbackURLScheme: callbackURLScheme) { oauthCallbackURL, error in
|
||||||
|
if let error = error {
|
||||||
|
return promise(.failure(error))
|
||||||
|
}
|
||||||
|
|
||||||
|
return promise(.success(oauthCallbackURL))
|
||||||
|
}
|
||||||
|
|
||||||
|
webAuthenticationSession.presentationContextProvider = presentationContextProvider
|
||||||
|
webAuthenticationSession.start()
|
||||||
|
}
|
||||||
|
.eraseToAnyPublisher()
|
||||||
|
}
|
||||||
|
}
|
208
Shared/View Models/AddIdentityViewModel.swift
Normal file
208
Shared/View Models/AddIdentityViewModel.swift
Normal file
|
@ -0,0 +1,208 @@
|
||||||
|
// Copyright © 2020 Metabolist. All rights reserved.
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
import Combine
|
||||||
|
import AuthenticationServices
|
||||||
|
|
||||||
|
class AddIdentityViewModel: ObservableObject {
|
||||||
|
@Published var urlFieldText = ""
|
||||||
|
@Published var alertItem: AlertItem?
|
||||||
|
@Published private(set) var loading = false
|
||||||
|
private(set) var addedIdentity: AnyPublisher<Identity, Never>
|
||||||
|
|
||||||
|
private let networkClient: HTTPClient
|
||||||
|
private let identityDatabase: IdentityDatabase
|
||||||
|
private let secrets: Secrets
|
||||||
|
private let webAuthenticationSessionType: WebAuthenticationSessionType.Type
|
||||||
|
private let webAuthenticationSessionContextProvider = WebAuthenticationSessionContextProvider()
|
||||||
|
private let addedIdentityInput = PassthroughSubject<Identity, Never>()
|
||||||
|
private var cancellables = Set<AnyCancellable>()
|
||||||
|
|
||||||
|
init(
|
||||||
|
networkClient: HTTPClient,
|
||||||
|
identityDatabase: IdentityDatabase,
|
||||||
|
secrets: Secrets,
|
||||||
|
webAuthenticationSessionType: WebAuthenticationSessionType.Type = ASWebAuthenticationSession.self) {
|
||||||
|
self.networkClient = networkClient
|
||||||
|
self.identityDatabase = identityDatabase
|
||||||
|
self.secrets = secrets
|
||||||
|
self.webAuthenticationSessionType = webAuthenticationSessionType
|
||||||
|
addedIdentity = addedIdentityInput.eraseToAnyPublisher()
|
||||||
|
}
|
||||||
|
|
||||||
|
func goTapped() {
|
||||||
|
let identityID = UUID().uuidString
|
||||||
|
let instanceURL: URL
|
||||||
|
let redirectURL: URL
|
||||||
|
|
||||||
|
do {
|
||||||
|
instanceURL = try urlFieldText.url()
|
||||||
|
redirectURL = try identityID.url(scheme: MastodonAPI.OAuth.callbackURLScheme)
|
||||||
|
} catch {
|
||||||
|
alertItem = AlertItem(error: error)
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
authorizeApp(
|
||||||
|
identityID: identityID,
|
||||||
|
instanceURL: instanceURL,
|
||||||
|
redirectURL: redirectURL,
|
||||||
|
secrets: secrets)
|
||||||
|
.authenticationURL(instanceURL: instanceURL, redirectURL: redirectURL)
|
||||||
|
.authenticate(
|
||||||
|
webAuthenticationSessionType: webAuthenticationSessionType,
|
||||||
|
contextProvider: webAuthenticationSessionContextProvider,
|
||||||
|
callbackURLScheme: MastodonAPI.OAuth.callbackURLScheme)
|
||||||
|
.extractCode()
|
||||||
|
.requestAccessToken(
|
||||||
|
networkClient: networkClient,
|
||||||
|
identityID: identityID,
|
||||||
|
instanceURL: instanceURL)
|
||||||
|
.createIdentity(
|
||||||
|
id: identityID,
|
||||||
|
instanceURL: instanceURL,
|
||||||
|
identityDatabase: identityDatabase,
|
||||||
|
secrets: secrets)
|
||||||
|
.assignErrorsToAlertItem(to: \.alertItem, on: self)
|
||||||
|
.handleEvents(
|
||||||
|
receiveSubscription: { [weak self] _ in self?.loading = true },
|
||||||
|
receiveCompletion: { [weak self] _ in self?.loading = false })
|
||||||
|
.sink(receiveValue: addedIdentityInput.send)
|
||||||
|
.store(in: &cancellables)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private extension AddIdentityViewModel {
|
||||||
|
private class WebAuthenticationSessionContextProvider: NSObject, ASWebAuthenticationPresentationContextProviding {
|
||||||
|
func presentationAnchor(for session: ASWebAuthenticationSession) -> ASPresentationAnchor {
|
||||||
|
ASPresentationAnchor()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func authorizeApp(
|
||||||
|
identityID: String,
|
||||||
|
instanceURL: URL,
|
||||||
|
redirectURL: URL,
|
||||||
|
secrets: Secrets) -> AnyPublisher<AppAuthorization, Error> {
|
||||||
|
let endpoint = AppAuthorizationEndpoint.apps(
|
||||||
|
clientName: MastodonAPI.OAuth.clientName,
|
||||||
|
redirectURI: redirectURL.absoluteString,
|
||||||
|
scopes: MastodonAPI.OAuth.scopes,
|
||||||
|
website: nil)
|
||||||
|
let target = MastodonTarget(baseURL: instanceURL, endpoint: endpoint, accessToken: nil)
|
||||||
|
|
||||||
|
return networkClient.request(target)
|
||||||
|
.tryMap {
|
||||||
|
try secrets.set($0.clientId, forItem: .clientID, forIdentityID: identityID)
|
||||||
|
try secrets.set($0.clientSecret, forItem: .clientSecret, forIdentityID: identityID)
|
||||||
|
|
||||||
|
return $0
|
||||||
|
}
|
||||||
|
.eraseToAnyPublisher()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private extension Publisher where Output == AppAuthorization {
|
||||||
|
func authenticationURL(
|
||||||
|
instanceURL: URL,
|
||||||
|
redirectURL: URL) -> AnyPublisher<(AppAuthorization, URL), Error> {
|
||||||
|
tryMap { appAuthorization in
|
||||||
|
guard var authorizationURLComponents = URLComponents(url: instanceURL, resolvingAgainstBaseURL: true) else {
|
||||||
|
throw URLError(.badURL)
|
||||||
|
}
|
||||||
|
|
||||||
|
authorizationURLComponents.path = "/oauth/authorize"
|
||||||
|
authorizationURLComponents.queryItems = [
|
||||||
|
"client_id": appAuthorization.clientId,
|
||||||
|
"scope": MastodonAPI.OAuth.scopes,
|
||||||
|
"response_type": "code",
|
||||||
|
"redirect_uri": redirectURL.absoluteString
|
||||||
|
].map { URLQueryItem(name: $0, value: $1) }
|
||||||
|
|
||||||
|
guard let authorizationURL = authorizationURLComponents.url else {
|
||||||
|
throw URLError(.badURL)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (appAuthorization, authorizationURL)
|
||||||
|
}
|
||||||
|
.mapError { $0 as Error }
|
||||||
|
.eraseToAnyPublisher()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private extension Publisher where Output == (AppAuthorization, URL), Failure == Error {
|
||||||
|
func authenticate(
|
||||||
|
webAuthenticationSessionType: WebAuthenticationSessionType.Type,
|
||||||
|
contextProvider: ASWebAuthenticationPresentationContextProviding,
|
||||||
|
callbackURLScheme: String) -> AnyPublisher<(AppAuthorization, URL), Error> {
|
||||||
|
flatMap { appAuthorization, url in
|
||||||
|
webAuthenticationSessionType.publisher(
|
||||||
|
url: url,
|
||||||
|
callbackURLScheme: callbackURLScheme,
|
||||||
|
presentationContextProvider: contextProvider)
|
||||||
|
.tryCatch { error -> AnyPublisher<URL?, Error> in
|
||||||
|
if (error as? ASWebAuthenticationSessionError)?.code == .canceledLogin {
|
||||||
|
return Just(nil).setFailureType(to: Error.self).eraseToAnyPublisher()
|
||||||
|
}
|
||||||
|
|
||||||
|
throw error
|
||||||
|
}
|
||||||
|
.compactMap { $0 }
|
||||||
|
.map { (appAuthorization, $0) }
|
||||||
|
}
|
||||||
|
.eraseToAnyPublisher()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private extension Publisher where Output == (AppAuthorization, URL) {
|
||||||
|
// swiftlint:disable large_tuple
|
||||||
|
func extractCode() -> AnyPublisher<(AppAuthorization, URL, String), Error> {
|
||||||
|
tryMap { appAuthorization, url -> (AppAuthorization, URL, String) in
|
||||||
|
guard let queryItems = URLComponents(url: url, resolvingAgainstBaseURL: true)?.queryItems,
|
||||||
|
let code = queryItems.first(where: { $0.name == MastodonAPI.OAuth.codeCallbackQueryItemName })?.value
|
||||||
|
else { throw MastodonAPI.OAuthError.codeNotFound }
|
||||||
|
|
||||||
|
return (appAuthorization, url, code)
|
||||||
|
}
|
||||||
|
.eraseToAnyPublisher()
|
||||||
|
}
|
||||||
|
// swiftlint:enable large_tuple
|
||||||
|
}
|
||||||
|
|
||||||
|
private extension Publisher where Output == (AppAuthorization, URL, String), Failure == Error {
|
||||||
|
func requestAccessToken(
|
||||||
|
networkClient: HTTPClient,
|
||||||
|
identityID: String,
|
||||||
|
instanceURL: URL) -> AnyPublisher<AccessToken, Error> {
|
||||||
|
flatMap { appAuthorization, url, code -> AnyPublisher<AccessToken, Error> in
|
||||||
|
let endpoint = AccessTokenEndpoint.oauthToken(
|
||||||
|
clientID: appAuthorization.clientId,
|
||||||
|
clientSecret: appAuthorization.clientSecret,
|
||||||
|
code: code,
|
||||||
|
grantType: MastodonAPI.OAuth.grantType,
|
||||||
|
scopes: MastodonAPI.OAuth.scopes,
|
||||||
|
redirectURI: url.absoluteString)
|
||||||
|
let target = MastodonTarget(baseURL: instanceURL, endpoint: endpoint, accessToken: nil)
|
||||||
|
|
||||||
|
return networkClient.request(target)
|
||||||
|
}
|
||||||
|
.eraseToAnyPublisher()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private extension Publisher where Output == AccessToken {
|
||||||
|
func createIdentity(
|
||||||
|
id: String,
|
||||||
|
instanceURL: URL,
|
||||||
|
identityDatabase: IdentityDatabase,
|
||||||
|
secrets: Secrets) -> AnyPublisher<Identity, Error> {
|
||||||
|
tryMap { accessToken -> (String, URL) in
|
||||||
|
try secrets.set(accessToken.accessToken, forItem: .accessToken, forIdentityID: id)
|
||||||
|
|
||||||
|
return (id, instanceURL)
|
||||||
|
}
|
||||||
|
.flatMap(identityDatabase.createIdentity)
|
||||||
|
.eraseToAnyPublisher()
|
||||||
|
}
|
||||||
|
}
|
127
Shared/View Models/SceneViewModel.swift
Normal file
127
Shared/View Models/SceneViewModel.swift
Normal file
|
@ -0,0 +1,127 @@
|
||||||
|
// Copyright © 2020 Metabolist. All rights reserved.
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
import Combine
|
||||||
|
|
||||||
|
class SceneViewModel: ObservableObject {
|
||||||
|
@Published private(set) var identity: Identity? {
|
||||||
|
didSet {
|
||||||
|
if let identity = identity {
|
||||||
|
recentIdentityID = identity.id
|
||||||
|
networkClient.instanceURL = identity.url
|
||||||
|
|
||||||
|
do {
|
||||||
|
networkClient.accessToken = try secrets.item(.accessToken, forIdentityID: identity.id)
|
||||||
|
} catch {
|
||||||
|
alertItem = AlertItem(error: error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Published var alertItem: AlertItem?
|
||||||
|
var selectedTopLevelNavigation: TopLevelNavigation? = .timelines
|
||||||
|
|
||||||
|
private let networkClient: MastodonClient
|
||||||
|
private let identityDatabase: IdentityDatabase
|
||||||
|
private let secrets: Secrets
|
||||||
|
private let userDefaults: UserDefaults
|
||||||
|
private var cancellables = Set<AnyCancellable>()
|
||||||
|
|
||||||
|
init(networkClient: MastodonClient,
|
||||||
|
identityDatabase: IdentityDatabase,
|
||||||
|
secrets: Secrets,
|
||||||
|
userDefaults: UserDefaults = .standard) {
|
||||||
|
self.networkClient = networkClient
|
||||||
|
self.identityDatabase = identityDatabase
|
||||||
|
self.secrets = secrets
|
||||||
|
self.userDefaults = userDefaults
|
||||||
|
|
||||||
|
if let recentIdentityID = recentIdentityID {
|
||||||
|
identity = try? identityDatabase.identity(id: recentIdentityID)
|
||||||
|
refreshIdentity()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension SceneViewModel {
|
||||||
|
func refreshIdentity() {
|
||||||
|
guard let identity = identity else { return }
|
||||||
|
|
||||||
|
if networkClient.accessToken != nil {
|
||||||
|
networkClient.request(AccountEndpoint.verifyCredentials)
|
||||||
|
.map { ($0, identity.id) }
|
||||||
|
.flatMap(identityDatabase.updateAccount)
|
||||||
|
.assignErrorsToAlertItem(to: \.alertItem, on: self)
|
||||||
|
.assign(to: \.identity, on: self)
|
||||||
|
.store(in: &cancellables)
|
||||||
|
}
|
||||||
|
|
||||||
|
networkClient.request(InstanceEndpoint.instance)
|
||||||
|
.map { ($0, identity.id) }
|
||||||
|
.flatMap(identityDatabase.updateInstance)
|
||||||
|
.assignErrorsToAlertItem(to: \.alertItem, on: self)
|
||||||
|
.assign(to: \.identity, on: self)
|
||||||
|
.store(in: &cancellables)
|
||||||
|
}
|
||||||
|
|
||||||
|
func addIdentityViewModel() -> AddIdentityViewModel {
|
||||||
|
let addAccountViewModel = AddIdentityViewModel(
|
||||||
|
networkClient: networkClient,
|
||||||
|
identityDatabase: identityDatabase,
|
||||||
|
secrets: secrets)
|
||||||
|
|
||||||
|
addAccountViewModel.addedIdentity
|
||||||
|
.sink(receiveValue: addIdentity(_:))
|
||||||
|
.store(in: &cancellables)
|
||||||
|
|
||||||
|
return addAccountViewModel
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private extension SceneViewModel {
|
||||||
|
private static let recentIdentityIDKey = "recentIdentityID"
|
||||||
|
|
||||||
|
private var recentIdentityID: String? {
|
||||||
|
get { userDefaults.value(forKey: Self.recentIdentityIDKey) as? String }
|
||||||
|
set { userDefaults.set(newValue, forKey: Self.recentIdentityIDKey) }
|
||||||
|
}
|
||||||
|
|
||||||
|
private func addIdentity(_ identity: Identity) {
|
||||||
|
self.identity = identity
|
||||||
|
refreshIdentity()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension SceneViewModel {
|
||||||
|
enum TopLevelNavigation: CaseIterable {
|
||||||
|
case timelines
|
||||||
|
case search
|
||||||
|
case notifications
|
||||||
|
case messages
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension SceneViewModel.TopLevelNavigation {
|
||||||
|
var title: String {
|
||||||
|
switch self {
|
||||||
|
case .timelines: return "Timelines"
|
||||||
|
case .search: return "Search"
|
||||||
|
case .notifications: return "Notifications"
|
||||||
|
case .messages: return "Messages"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var systemImageName: String {
|
||||||
|
switch self {
|
||||||
|
case .timelines: return "house"
|
||||||
|
case .search: return "magnifyingglass"
|
||||||
|
case .notifications: return "bell"
|
||||||
|
case .messages: return "envelope"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension SceneViewModel.TopLevelNavigation: Identifiable {
|
||||||
|
var id: Self { self }
|
||||||
|
}
|
7
Shared/View Models/TimelineViewModel.swift
Normal file
7
Shared/View Models/TimelineViewModel.swift
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
// Copyright © 2020 Metabolist. All rights reserved.
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
|
||||||
|
class TimelineViewModel: ObservableObject {
|
||||||
|
|
||||||
|
}
|
48
Shared/Views/AddIdentityView.swift
Normal file
48
Shared/Views/AddIdentityView.swift
Normal file
|
@ -0,0 +1,48 @@
|
||||||
|
// Copyright © 2020 Metabolist. All rights reserved.
|
||||||
|
|
||||||
|
import SwiftUI
|
||||||
|
|
||||||
|
struct AddIdentityView: View {
|
||||||
|
@StateObject var viewModel: AddIdentityViewModel
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
Form {
|
||||||
|
#if os(iOS)
|
||||||
|
urlTextField
|
||||||
|
.autocapitalization(.none)
|
||||||
|
.disableAutocorrection(true)
|
||||||
|
.keyboardType(.URL)
|
||||||
|
#else
|
||||||
|
urlTextField
|
||||||
|
#endif
|
||||||
|
Group {
|
||||||
|
if viewModel.loading {
|
||||||
|
ProgressView()
|
||||||
|
} else {
|
||||||
|
Button(
|
||||||
|
action: viewModel.goTapped,
|
||||||
|
label: { Text("go") })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.frame(maxWidth: .infinity, alignment: .center)
|
||||||
|
}
|
||||||
|
.alertItem($viewModel.alertItem)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension AddIdentityView {
|
||||||
|
private var urlTextField: some View {
|
||||||
|
TextField("add-identity.instance-url", text: $viewModel.urlFieldText)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct AddAccountView_Previews: PreviewProvider {
|
||||||
|
static var previews: some View {
|
||||||
|
AddIdentityView(viewModel: AddIdentityViewModel(
|
||||||
|
networkClient: MastodonClient(configuration: .stubbing),
|
||||||
|
// swiftlint:disable force_try
|
||||||
|
identityDatabase: try! IdentityDatabase(inMemory: true),
|
||||||
|
// swiftlint:enable force_try
|
||||||
|
secrets: Secrets(keychain: FakeKeychain())))
|
||||||
|
}
|
||||||
|
}
|
42
Shared/Views/ContentView.swift
Normal file
42
Shared/Views/ContentView.swift
Normal file
|
@ -0,0 +1,42 @@
|
||||||
|
// Copyright © 2020 Metabolist. All rights reserved.
|
||||||
|
|
||||||
|
import SwiftUI
|
||||||
|
|
||||||
|
struct ContentView: View {
|
||||||
|
@EnvironmentObject var sceneViewModel: SceneViewModel
|
||||||
|
@Environment(\.scenePhase) private var scenePhase
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
if sceneViewModel.identity != nil {
|
||||||
|
mainNavigation
|
||||||
|
.onChange(of: scenePhase) {
|
||||||
|
if case .active = $0 {
|
||||||
|
sceneViewModel.refreshIdentity()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.alertItem($sceneViewModel.alertItem)
|
||||||
|
} else {
|
||||||
|
addIdentity
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private extension ContentView {
|
||||||
|
private var mainNavigation: some View {
|
||||||
|
#if os(macOS)
|
||||||
|
return SidebarNavigation().frame(minWidth: 900, maxWidth: .infinity, minHeight: 500, maxHeight: .infinity)
|
||||||
|
#else
|
||||||
|
return TabNavigation()
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
private var addIdentity: some View {
|
||||||
|
AddIdentityView(viewModel: sceneViewModel.addIdentityViewModel())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//struct ContentView_Previews: PreviewProvider {
|
||||||
|
// static var previews: some View {
|
||||||
|
// ContentView()
|
||||||
|
// }
|
||||||
|
//}
|
|
@ -2,14 +2,14 @@
|
||||||
|
|
||||||
import SwiftUI
|
import SwiftUI
|
||||||
|
|
||||||
struct ContentView: View {
|
struct TimelineView: View {
|
||||||
var body: some View {
|
var body: some View {
|
||||||
Text("Hello, world!").padding()
|
Text("Time of my life")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
struct ContentView_Previews: PreviewProvider {
|
struct TimelineView_Previews: PreviewProvider {
|
||||||
static var previews: some View {
|
static var previews: some View {
|
||||||
ContentView()
|
TimelineView()
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -1,37 +0,0 @@
|
||||||
// Copyright © 2020 Metabolist. All rights reserved.
|
|
||||||
|
|
||||||
import XCTest
|
|
||||||
|
|
||||||
class Tests_iOS: XCTestCase {
|
|
||||||
|
|
||||||
override func setUpWithError() throws {
|
|
||||||
// Put setup code here. This method is called before the invocation of each test method in the class.
|
|
||||||
|
|
||||||
// In UI tests it is usually best to stop immediately when a failure occurs.
|
|
||||||
continueAfterFailure = false
|
|
||||||
|
|
||||||
// In UI tests it’s important to set the initial state - such as interface orientation - required for your tests before they run. The setUp method is a good place to do this.
|
|
||||||
}
|
|
||||||
|
|
||||||
override func tearDownWithError() throws {
|
|
||||||
// Put teardown code here. This method is called after the invocation of each test method in the class.
|
|
||||||
}
|
|
||||||
|
|
||||||
func testExample() throws {
|
|
||||||
// UI tests must launch the application that they test.
|
|
||||||
let app = XCUIApplication()
|
|
||||||
app.launch()
|
|
||||||
|
|
||||||
// Use recording to get started writing UI tests.
|
|
||||||
// Use XCTAssert and related functions to verify your tests produce the correct results.
|
|
||||||
}
|
|
||||||
|
|
||||||
func testLaunchPerformance() throws {
|
|
||||||
if #available(macOS 10.15, iOS 13.0, tvOS 13.0, *) {
|
|
||||||
// This measures how long it takes to launch your application.
|
|
||||||
measure(metrics: [XCTApplicationLaunchMetric()]) {
|
|
||||||
XCUIApplication().launch()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,22 +0,0 @@
|
||||||
<?xml version="1.0" encoding="UTF-8"?>
|
|
||||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
||||||
<plist version="1.0">
|
|
||||||
<dict>
|
|
||||||
<key>CFBundleDevelopmentRegion</key>
|
|
||||||
<string>$(DEVELOPMENT_LANGUAGE)</string>
|
|
||||||
<key>CFBundleExecutable</key>
|
|
||||||
<string>$(EXECUTABLE_NAME)</string>
|
|
||||||
<key>CFBundleIdentifier</key>
|
|
||||||
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
|
|
||||||
<key>CFBundleInfoDictionaryVersion</key>
|
|
||||||
<string>6.0</string>
|
|
||||||
<key>CFBundleName</key>
|
|
||||||
<string>$(PRODUCT_NAME)</string>
|
|
||||||
<key>CFBundlePackageType</key>
|
|
||||||
<string>$(PRODUCT_BUNDLE_PACKAGE_TYPE)</string>
|
|
||||||
<key>CFBundleShortVersionString</key>
|
|
||||||
<string>1.0</string>
|
|
||||||
<key>CFBundleVersion</key>
|
|
||||||
<string>1</string>
|
|
||||||
</dict>
|
|
||||||
</plist>
|
|
|
@ -1,37 +0,0 @@
|
||||||
// Copyright © 2020 Metabolist. All rights reserved.
|
|
||||||
|
|
||||||
import XCTest
|
|
||||||
|
|
||||||
class Tests_macOS: XCTestCase {
|
|
||||||
|
|
||||||
override func setUpWithError() throws {
|
|
||||||
// Put setup code here. This method is called before the invocation of each test method in the class.
|
|
||||||
|
|
||||||
// In UI tests it is usually best to stop immediately when a failure occurs.
|
|
||||||
continueAfterFailure = false
|
|
||||||
|
|
||||||
// In UI tests it’s important to set the initial state - such as interface orientation - required for your tests before they run. The setUp method is a good place to do this.
|
|
||||||
}
|
|
||||||
|
|
||||||
override func tearDownWithError() throws {
|
|
||||||
// Put teardown code here. This method is called after the invocation of each test method in the class.
|
|
||||||
}
|
|
||||||
|
|
||||||
func testExample() throws {
|
|
||||||
// UI tests must launch the application that they test.
|
|
||||||
let app = XCUIApplication()
|
|
||||||
app.launch()
|
|
||||||
|
|
||||||
// Use recording to get started writing UI tests.
|
|
||||||
// Use XCTAssert and related functions to verify your tests produce the correct results.
|
|
||||||
}
|
|
||||||
|
|
||||||
func testLaunchPerformance() throws {
|
|
||||||
if #available(macOS 10.15, iOS 13.0, tvOS 13.0, *) {
|
|
||||||
// This measures how long it takes to launch your application.
|
|
||||||
measure(metrics: [XCTApplicationLaunchMetric()]) {
|
|
||||||
XCUIApplication().launch()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
78
Tests/View Models/AddIdentityViewModelTests.swift
Normal file
78
Tests/View Models/AddIdentityViewModelTests.swift
Normal file
|
@ -0,0 +1,78 @@
|
||||||
|
// Copyright © 2020 Metabolist. All rights reserved.
|
||||||
|
|
||||||
|
import XCTest
|
||||||
|
import Combine
|
||||||
|
import CombineExpectations
|
||||||
|
@testable import Metatext
|
||||||
|
|
||||||
|
class AddIdentityViewModelTests: XCTestCase {
|
||||||
|
var networkClient: MastodonClient!
|
||||||
|
var identityDatabase: IdentityDatabase!
|
||||||
|
var secrets: Secrets!
|
||||||
|
var cancellables = Set<AnyCancellable>()
|
||||||
|
|
||||||
|
override func setUpWithError() throws {
|
||||||
|
networkClient = MastodonClient(configuration: .stubbing)
|
||||||
|
identityDatabase = try IdentityDatabase(inMemory: true)
|
||||||
|
secrets = Secrets(keychain: FakeKeychain())
|
||||||
|
}
|
||||||
|
|
||||||
|
func testAddIdentity() throws {
|
||||||
|
let sut = AddIdentityViewModel(
|
||||||
|
networkClient: networkClient,
|
||||||
|
identityDatabase: identityDatabase,
|
||||||
|
secrets: secrets,
|
||||||
|
webAuthenticationSessionType: SuccessfulStubbingWebAuthenticationSession.self)
|
||||||
|
let recorder = sut.addedIdentity.record()
|
||||||
|
|
||||||
|
sut.urlFieldText = "https://mastodon.social"
|
||||||
|
sut.goTapped()
|
||||||
|
|
||||||
|
let addedIdentity = try wait(for: recorder.next(), timeout: 1)
|
||||||
|
|
||||||
|
XCTAssertEqual(try identityDatabase.identity(id: addedIdentity.id), addedIdentity)
|
||||||
|
XCTAssertEqual(addedIdentity.url, URL(string: "https://mastodon.social")!)
|
||||||
|
XCTAssertEqual(
|
||||||
|
try secrets.item(.clientID, forIdentityID: addedIdentity.id) as String?,
|
||||||
|
"AUTHORIZATION_CLIENT_ID_STUB_VALUE")
|
||||||
|
XCTAssertEqual(
|
||||||
|
try secrets.item(.clientSecret, forIdentityID: addedIdentity.id) as String?,
|
||||||
|
"AUTHORIZATION_CLIENT_SECRET_STUB_VALUE")
|
||||||
|
XCTAssertEqual(
|
||||||
|
try secrets.item(.accessToken, forIdentityID: addedIdentity.id) as String?,
|
||||||
|
"ACCESS_TOKEN_STUB_VALUE")
|
||||||
|
}
|
||||||
|
|
||||||
|
func testAddIdentityWithoutScheme() throws {
|
||||||
|
let sut = AddIdentityViewModel(
|
||||||
|
networkClient: networkClient,
|
||||||
|
identityDatabase: identityDatabase,
|
||||||
|
secrets: secrets,
|
||||||
|
webAuthenticationSessionType: SuccessfulStubbingWebAuthenticationSession.self)
|
||||||
|
let recorder = sut.addedIdentity.record()
|
||||||
|
|
||||||
|
sut.urlFieldText = "mastodon.social"
|
||||||
|
sut.goTapped()
|
||||||
|
|
||||||
|
let addedIdentity = try wait(for: recorder.next(), timeout: 1)
|
||||||
|
|
||||||
|
XCTAssertEqual(try identityDatabase.identity(id: addedIdentity.id), addedIdentity)
|
||||||
|
XCTAssertEqual(addedIdentity.url, URL(string: "https://mastodon.social")!)
|
||||||
|
}
|
||||||
|
|
||||||
|
func testInvalidURL() throws {
|
||||||
|
let sut = AddIdentityViewModel(
|
||||||
|
networkClient: networkClient,
|
||||||
|
identityDatabase: identityDatabase,
|
||||||
|
secrets: secrets,
|
||||||
|
webAuthenticationSessionType: SuccessfulStubbingWebAuthenticationSession.self)
|
||||||
|
let recorder = sut.$alertItem.dropFirst().record()
|
||||||
|
|
||||||
|
sut.urlFieldText = "🐘.social"
|
||||||
|
sut.goTapped()
|
||||||
|
|
||||||
|
let alertItem = try wait(for: recorder.next(), timeout: 1)
|
||||||
|
|
||||||
|
XCTAssertEqual((alertItem?.error as? URLError)?.code, URLError.badURL)
|
||||||
|
}
|
||||||
|
}
|
43
iOS/TabNavigation.swift
Normal file
43
iOS/TabNavigation.swift
Normal file
|
@ -0,0 +1,43 @@
|
||||||
|
// Copyright © 2020 Metabolist. All rights reserved.
|
||||||
|
|
||||||
|
import SwiftUI
|
||||||
|
|
||||||
|
struct TabNavigation: View {
|
||||||
|
@EnvironmentObject var sceneViewModel: SceneViewModel
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
TabView(selection: $sceneViewModel.selectedTopLevelNavigation) {
|
||||||
|
ForEach(SceneViewModel.TopLevelNavigation.allCases) { topLevelNavigation in
|
||||||
|
NavigationView {
|
||||||
|
view(topLevelNavigation: topLevelNavigation)
|
||||||
|
}
|
||||||
|
.tabItem {
|
||||||
|
Label(topLevelNavigation.title, systemImage: topLevelNavigation.systemImageName)
|
||||||
|
.accessibility(label: Text(topLevelNavigation.title))
|
||||||
|
}
|
||||||
|
.tag(topLevelNavigation)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private extension TabNavigation {
|
||||||
|
func view(topLevelNavigation: SceneViewModel.TopLevelNavigation) -> some View {
|
||||||
|
Group {
|
||||||
|
switch topLevelNavigation {
|
||||||
|
case .timelines:
|
||||||
|
TimelineView()
|
||||||
|
.navigationBarTitle(sceneViewModel.identity?.handle ?? "", displayMode: .inline)
|
||||||
|
default: Text(topLevelNavigation.title)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: Preview
|
||||||
|
|
||||||
|
struct TabNavigation_Previews: PreviewProvider {
|
||||||
|
static var previews: some View {
|
||||||
|
TabNavigation()
|
||||||
|
}
|
||||||
|
}
|
47
macOS/SidebarNavigation.swift
Normal file
47
macOS/SidebarNavigation.swift
Normal file
|
@ -0,0 +1,47 @@
|
||||||
|
// Copyright © 2020 Metabolist. All rights reserved.
|
||||||
|
|
||||||
|
import SwiftUI
|
||||||
|
|
||||||
|
struct SidebarNavigation: View {
|
||||||
|
@EnvironmentObject var sceneViewModel: SceneViewModel
|
||||||
|
|
||||||
|
var sidebar: some View {
|
||||||
|
List(selection: $sceneViewModel.selectedTopLevelNavigation) {
|
||||||
|
ForEach(SceneViewModel.TopLevelNavigation.allCases) { topLevelNavigation in
|
||||||
|
NavigationLink(destination: view(topLevelNavigation: topLevelNavigation)) {
|
||||||
|
Label(topLevelNavigation.title, systemImage: topLevelNavigation.systemImageName)
|
||||||
|
}
|
||||||
|
.accessibility(label: Text(topLevelNavigation.title))
|
||||||
|
.tag(topLevelNavigation)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.listStyle(SidebarListStyle())
|
||||||
|
}
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
NavigationView {
|
||||||
|
sidebar
|
||||||
|
.frame(minWidth: 100, idealWidth: 150, maxWidth: 200, maxHeight: .infinity)
|
||||||
|
Text("Content")
|
||||||
|
.frame(maxWidth: .infinity, maxHeight: .infinity)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private extension SidebarNavigation {
|
||||||
|
func view(topLevelNavigation: SceneViewModel.TopLevelNavigation) -> some View {
|
||||||
|
Group {
|
||||||
|
switch topLevelNavigation {
|
||||||
|
case .timelines:
|
||||||
|
TimelineView()
|
||||||
|
default: Text(topLevelNavigation.title)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct SidebarNavigation_Previews: PreviewProvider {
|
||||||
|
static var previews: some View {
|
||||||
|
SidebarNavigation()
|
||||||
|
}
|
||||||
|
}
|
|
@ -6,5 +6,7 @@
|
||||||
<true/>
|
<true/>
|
||||||
<key>com.apple.security.files.user-selected.read-only</key>
|
<key>com.apple.security.files.user-selected.read-only</key>
|
||||||
<true/>
|
<true/>
|
||||||
|
<key>com.apple.security.network.client</key>
|
||||||
|
<true/>
|
||||||
</dict>
|
</dict>
|
||||||
</plist>
|
</plist>
|
||||||
|
|
Loading…
Reference in a new issue