mirror of
https://github.com/metabolist/metatext.git
synced 2024-11-23 00:30: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/
|
||||
*.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;
|
||||
classes = {
|
||||
};
|
||||
objectVersion = 50;
|
||||
objectVersion = 52;
|
||||
objects = {
|
||||
|
||||
/* 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 */; };
|
||||
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 */; };
|
||||
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 */
|
||||
|
||||
/* Begin PBXContainerItemProxy section */
|
||||
D047FA9D24C3E21200AF17C5 /* PBXContainerItemProxy */ = {
|
||||
D0666A2624C677B400F3F04B /* PBXContainerItemProxy */ = {
|
||||
isa = PBXContainerItemProxy;
|
||||
containerPortal = D047FA8024C3E21000AF17C5 /* Project object */;
|
||||
proxyType = 1;
|
||||
remoteGlobalIDString = D047FA8B24C3E21200AF17C5;
|
||||
remoteInfo = "Metatext (iOS)";
|
||||
};
|
||||
D047FAA824C3E21200AF17C5 /* PBXContainerItemProxy */ = {
|
||||
isa = PBXContainerItemProxy;
|
||||
containerPortal = D047FA8024C3E21000AF17C5 /* Project object */;
|
||||
proxyType = 1;
|
||||
remoteGlobalIDString = D047FA9324C3E21200AF17C5;
|
||||
remoteInfo = "Metatext (macOS)";
|
||||
};
|
||||
/* End PBXContainerItemProxy section */
|
||||
|
||||
/* Begin PBXFileReference section */
|
||||
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>"; };
|
||||
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>"; };
|
||||
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>"; };
|
||||
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; };
|
||||
D047FAA024C3E21200AF17C5 /* Tests_iOS.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Tests_iOS.swift; sourceTree = "<group>"; };
|
||||
D047FAA224C3E21200AF17C5 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
|
||||
D047FAA724C3E21200AF17C5 /* Tests macOS.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = "Tests macOS.xctest"; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
D047FAAB24C3E21200AF17C5 /* Tests_macOS.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Tests_macOS.swift; sourceTree = "<group>"; };
|
||||
D047FAAD24C3E21200AF17C5 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
|
||||
D065F53A24D3B33A00741304 /* View+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "View+Extensions.swift"; sourceTree = "<group>"; };
|
||||
D065F53D24D3D20300741304 /* InstanceEndpoint.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InstanceEndpoint.swift; sourceTree = "<group>"; };
|
||||
D0666A2124C677B400F3F04B /* Tests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = Tests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
D0666A2524C677B400F3F04B /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
|
||||
D0666A4124C6BB7B00F3F04B /* IdentityDatabase.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IdentityDatabase.swift; 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 */
|
||||
|
||||
/* Begin PBXFrameworksBuildPhase section */
|
||||
|
@ -56,6 +189,8 @@
|
|||
isa = PBXFrameworksBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
D0666A4924C6C1A300F3F04B /* GRDB in Frameworks */,
|
||||
D0DC175F24D016EA00A75C65 /* Alamofire in Frameworks */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
|
@ -63,20 +198,16 @@
|
|||
isa = PBXFrameworksBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
D0DC176124D0171800A75C65 /* Alamofire in Frameworks */,
|
||||
D0666A7D24C7745A00F3F04B /* GRDB in Frameworks */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
D047FA9924C3E21200AF17C5 /* Frameworks */ = {
|
||||
isa = PBXFrameworksBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
D047FAA424C3E21200AF17C5 /* Frameworks */ = {
|
||||
D0666A1E24C677B400F3F04B /* Frameworks */ = {
|
||||
isa = PBXFrameworksBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
D065F53924D37E5100741304 /* CombineExpectations in Frameworks */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
|
@ -86,21 +217,27 @@
|
|||
D047FA7F24C3E21000AF17C5 = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
D047FA8424C3E21000AF17C5 /* Shared */,
|
||||
D0ED1BB224CE3A1600B4899C /* Development Assets */,
|
||||
D0666A7924C7745A00F3F04B /* Frameworks */,
|
||||
D047FA8E24C3E21200AF17C5 /* iOS */,
|
||||
D047FA9524C3E21200AF17C5 /* macOS */,
|
||||
D047FA9F24C3E21200AF17C5 /* Tests iOS */,
|
||||
D047FAAA24C3E21200AF17C5 /* Tests macOS */,
|
||||
D047FA8D24C3E21200AF17C5 /* Products */,
|
||||
D047FA8424C3E21000AF17C5 /* Shared */,
|
||||
D0666A2224C677B400F3F04B /* Tests */,
|
||||
);
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
D047FA8424C3E21000AF17C5 /* Shared */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
D047FA8524C3E21000AF17C5 /* MetatextApp.swift */,
|
||||
D047FA8624C3E21000AF17C5 /* ContentView.swift */,
|
||||
D047FA8724C3E21200AF17C5 /* Assets.xcassets */,
|
||||
D0DB6F1624C665B400D965FE /* Extensions */,
|
||||
D06B491D24D3F78A00642749 /* Localizations */,
|
||||
D047FA8524C3E21000AF17C5 /* MetatextApp.swift */,
|
||||
D0666A3A24C6B56200F3F04B /* Model */,
|
||||
D0DB6EFA24C5730600D965FE /* Networking */,
|
||||
D0DB6EFB24C658E400D965FE /* View Models */,
|
||||
D0DB6EF024C5224F00D965FE /* Views */,
|
||||
);
|
||||
path = Shared;
|
||||
sourceTree = "<group>";
|
||||
|
@ -110,8 +247,7 @@
|
|||
children = (
|
||||
D047FA8C24C3E21200AF17C5 /* Metatext.app */,
|
||||
D047FA9424C3E21200AF17C5 /* Metatext.app */,
|
||||
D047FA9C24C3E21200AF17C5 /* Tests iOS.xctest */,
|
||||
D047FAA724C3E21200AF17C5 /* Tests macOS.xctest */,
|
||||
D0666A2124C677B400F3F04B /* Tests.xctest */,
|
||||
);
|
||||
name = Products;
|
||||
sourceTree = "<group>";
|
||||
|
@ -120,6 +256,7 @@
|
|||
isa = PBXGroup;
|
||||
children = (
|
||||
D047FA8F24C3E21200AF17C5 /* Info.plist */,
|
||||
D0BEC95024CA2B7E00E864C4 /* TabNavigation.swift */,
|
||||
);
|
||||
path = iOS;
|
||||
sourceTree = "<group>";
|
||||
|
@ -129,26 +266,144 @@
|
|||
children = (
|
||||
D047FA9624C3E21200AF17C5 /* Info.plist */,
|
||||
D047FA9724C3E21200AF17C5 /* macOS.entitlements */,
|
||||
D0BEC94E24CA2B5300E864C4 /* SidebarNavigation.swift */,
|
||||
);
|
||||
path = macOS;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
D047FA9F24C3E21200AF17C5 /* Tests iOS */ = {
|
||||
D0666A2224C677B400F3F04B /* Tests */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
D047FAA024C3E21200AF17C5 /* Tests_iOS.swift */,
|
||||
D047FAA224C3E21200AF17C5 /* Info.plist */,
|
||||
D0ED1B6C24CE0EED00B4899C /* View Models */,
|
||||
D0666A2524C677B400F3F04B /* Info.plist */,
|
||||
);
|
||||
path = "Tests iOS";
|
||||
path = Tests;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
D047FAAA24C3E21200AF17C5 /* Tests macOS */ = {
|
||||
D0666A3A24C6B56200F3F04B /* Model */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
D047FAAB24C3E21200AF17C5 /* Tests_macOS.swift */,
|
||||
D047FAAD24C3E21200AF17C5 /* Info.plist */,
|
||||
D0666A6E24C6DFB300F3F04B /* AccessToken.swift */,
|
||||
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>";
|
||||
};
|
||||
/* End PBXGroup section */
|
||||
|
@ -161,12 +416,17 @@
|
|||
D047FA8824C3E21200AF17C5 /* Sources */,
|
||||
D047FA8924C3E21200AF17C5 /* Frameworks */,
|
||||
D047FA8A24C3E21200AF17C5 /* Resources */,
|
||||
D0666A2E24C67E6700F3F04B /* ShellScript */,
|
||||
);
|
||||
buildRules = (
|
||||
);
|
||||
dependencies = (
|
||||
);
|
||||
name = "Metatext (iOS)";
|
||||
packageProductDependencies = (
|
||||
D0666A4824C6C1A300F3F04B /* GRDB */,
|
||||
D0DC175E24D016EA00A75C65 /* Alamofire */,
|
||||
);
|
||||
productName = "Metatext (iOS)";
|
||||
productReference = D047FA8C24C3E21200AF17C5 /* Metatext.app */;
|
||||
productType = "com.apple.product-type.application";
|
||||
|
@ -178,51 +438,41 @@
|
|||
D047FA9024C3E21200AF17C5 /* Sources */,
|
||||
D047FA9124C3E21200AF17C5 /* Frameworks */,
|
||||
D047FA9224C3E21200AF17C5 /* Resources */,
|
||||
D0666A2F24C67F1400F3F04B /* ShellScript */,
|
||||
);
|
||||
buildRules = (
|
||||
);
|
||||
dependencies = (
|
||||
);
|
||||
name = "Metatext (macOS)";
|
||||
packageProductDependencies = (
|
||||
D0666A7C24C7745A00F3F04B /* GRDB */,
|
||||
D0DC176024D0171800A75C65 /* Alamofire */,
|
||||
);
|
||||
productName = "Metatext (macOS)";
|
||||
productReference = D047FA9424C3E21200AF17C5 /* Metatext.app */;
|
||||
productType = "com.apple.product-type.application";
|
||||
};
|
||||
D047FA9B24C3E21200AF17C5 /* Tests iOS */ = {
|
||||
D0666A2024C677B400F3F04B /* Tests */ = {
|
||||
isa = PBXNativeTarget;
|
||||
buildConfigurationList = D047FABC24C3E21200AF17C5 /* Build configuration list for PBXNativeTarget "Tests iOS" */;
|
||||
buildConfigurationList = D0666A2824C677B400F3F04B /* Build configuration list for PBXNativeTarget "Tests" */;
|
||||
buildPhases = (
|
||||
D047FA9824C3E21200AF17C5 /* Sources */,
|
||||
D047FA9924C3E21200AF17C5 /* Frameworks */,
|
||||
D047FA9A24C3E21200AF17C5 /* Resources */,
|
||||
D0666A1D24C677B400F3F04B /* Sources */,
|
||||
D0666A1E24C677B400F3F04B /* Frameworks */,
|
||||
D0666A1F24C677B400F3F04B /* Resources */,
|
||||
);
|
||||
buildRules = (
|
||||
);
|
||||
dependencies = (
|
||||
D047FA9E24C3E21200AF17C5 /* PBXTargetDependency */,
|
||||
D0666A2724C677B400F3F04B /* PBXTargetDependency */,
|
||||
);
|
||||
name = "Tests iOS";
|
||||
productName = "Tests iOS";
|
||||
productReference = D047FA9C24C3E21200AF17C5 /* Tests iOS.xctest */;
|
||||
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 */,
|
||||
name = Tests;
|
||||
packageProductDependencies = (
|
||||
D065F53824D37E5100741304 /* CombineExpectations */,
|
||||
);
|
||||
buildRules = (
|
||||
);
|
||||
dependencies = (
|
||||
D047FAA924C3E21200AF17C5 /* PBXTargetDependency */,
|
||||
);
|
||||
name = "Tests macOS";
|
||||
productName = "Tests macOS";
|
||||
productReference = D047FAA724C3E21200AF17C5 /* Tests macOS.xctest */;
|
||||
productType = "com.apple.product-type.bundle.ui-testing";
|
||||
productName = "Unit Tests";
|
||||
productReference = D0666A2124C677B400F3F04B /* Tests.xctest */;
|
||||
productType = "com.apple.product-type.bundle.unit-test";
|
||||
};
|
||||
/* End PBXNativeTarget section */
|
||||
|
||||
|
@ -240,14 +490,11 @@
|
|||
D047FA9324C3E21200AF17C5 = {
|
||||
CreatedOnToolsVersion = 12.0;
|
||||
};
|
||||
D047FA9B24C3E21200AF17C5 = {
|
||||
D0666A2024C677B400F3F04B = {
|
||||
CreatedOnToolsVersion = 12.0;
|
||||
LastSwiftMigration = 1200;
|
||||
TestTargetID = D047FA8B24C3E21200AF17C5;
|
||||
};
|
||||
D047FAA624C3E21200AF17C5 = {
|
||||
CreatedOnToolsVersion = 12.0;
|
||||
TestTargetID = D047FA9324C3E21200AF17C5;
|
||||
};
|
||||
};
|
||||
};
|
||||
buildConfigurationList = D047FA8324C3E21000AF17C5 /* Build configuration list for PBXProject "Metatext" */;
|
||||
|
@ -259,14 +506,18 @@
|
|||
Base,
|
||||
);
|
||||
mainGroup = D047FA7F24C3E21000AF17C5;
|
||||
packageReferences = (
|
||||
D0666A4724C6C1A300F3F04B /* XCRemoteSwiftPackageReference "GRDB" */,
|
||||
D0DC175D24D016EA00A75C65 /* XCRemoteSwiftPackageReference "Alamofire" */,
|
||||
D065F53724D37E5100741304 /* XCRemoteSwiftPackageReference "CombineExpectations" */,
|
||||
);
|
||||
productRefGroup = D047FA8D24C3E21200AF17C5 /* Products */;
|
||||
projectDirPath = "";
|
||||
projectRoot = "";
|
||||
targets = (
|
||||
D047FA8B24C3E21200AF17C5 /* Metatext (iOS) */,
|
||||
D047FA9324C3E21200AF17C5 /* Metatext (macOS) */,
|
||||
D047FA9B24C3E21200AF17C5 /* Tests iOS */,
|
||||
D047FAA624C3E21200AF17C5 /* Tests macOS */,
|
||||
D0666A2024C677B400F3F04B /* Tests */,
|
||||
);
|
||||
};
|
||||
/* End PBXProject section */
|
||||
|
@ -277,6 +528,7 @@
|
|||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
D047FAB224C3E21200AF17C5 /* Assets.xcassets in Resources */,
|
||||
D06B491F24D3F7FE00642749 /* Localizable.strings in Resources */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
|
@ -285,17 +537,11 @@
|
|||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
D047FAB324C3E21200AF17C5 /* Assets.xcassets in Resources */,
|
||||
D06B492024D3FB8000642749 /* Localizable.strings in Resources */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
D047FA9A24C3E21200AF17C5 /* Resources */ = {
|
||||
isa = PBXResourcesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
D047FAA524C3E21200AF17C5 /* Resources */ = {
|
||||
D0666A1F24C677B400F3F04B /* Resources */ = {
|
||||
isa = PBXResourcesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
|
@ -304,13 +550,95 @@
|
|||
};
|
||||
/* 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 */
|
||||
D047FA8824C3E21200AF17C5 /* Sources */ = {
|
||||
isa = PBXSourcesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
D047FAB024C3E21200AF17C5 /* ContentView.swift in Sources */,
|
||||
D0DB6F0924C65AC000D965FE /* AddIdentityViewModel.swift in Sources */,
|
||||
D0ED1BD724CF94B200B4899C /* Application.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;
|
||||
};
|
||||
|
@ -318,39 +646,71 @@
|
|||
isa = PBXSourcesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
D047FAB124C3E21200AF17C5 /* ContentView.swift in Sources */,
|
||||
D0DB6F0A24C65AC000D965FE /* AddIdentityViewModel.swift in Sources */,
|
||||
D0ED1BD824CF94B200B4899C /* Application.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;
|
||||
};
|
||||
D047FA9824C3E21200AF17C5 /* Sources */ = {
|
||||
D0666A1D24C677B400F3F04B /* Sources */ = {
|
||||
isa = PBXSourcesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
D047FAA124C3E21200AF17C5 /* Tests_iOS.swift in Sources */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
D047FAA324C3E21200AF17C5 /* Sources */ = {
|
||||
isa = PBXSourcesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
D047FAAC24C3E21200AF17C5 /* Tests_macOS.swift in Sources */,
|
||||
D0ED1B6E24CE100C00B4899C /* AddIdentityViewModelTests.swift in Sources */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
/* End PBXSourcesBuildPhase section */
|
||||
|
||||
/* Begin PBXTargetDependency section */
|
||||
D047FA9E24C3E21200AF17C5 /* PBXTargetDependency */ = {
|
||||
D0666A2724C677B400F3F04B /* PBXTargetDependency */ = {
|
||||
isa = PBXTargetDependency;
|
||||
target = D047FA8B24C3E21200AF17C5 /* Metatext (iOS) */;
|
||||
targetProxy = D047FA9D24C3E21200AF17C5 /* PBXContainerItemProxy */;
|
||||
};
|
||||
D047FAA924C3E21200AF17C5 /* PBXTargetDependency */ = {
|
||||
isa = PBXTargetDependency;
|
||||
target = D047FA9324C3E21200AF17C5 /* Metatext (macOS) */;
|
||||
targetProxy = D047FAA824C3E21200AF17C5 /* PBXContainerItemProxy */;
|
||||
targetProxy = D0666A2624C677B400F3F04B /* PBXContainerItemProxy */;
|
||||
};
|
||||
/* End PBXTargetDependency section */
|
||||
|
||||
|
@ -472,6 +832,7 @@
|
|||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
DEVELOPMENT_ASSET_PATHS = "Development\\ Assets";
|
||||
DEVELOPMENT_TEAM = 82HL67AXQ2;
|
||||
ENABLE_PREVIEWS = YES;
|
||||
INFOPLIST_FILE = iOS/Info.plist;
|
||||
|
@ -494,6 +855,7 @@
|
|||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
DEVELOPMENT_ASSET_PATHS = "Development\\ Assets";
|
||||
DEVELOPMENT_TEAM = 82HL67AXQ2;
|
||||
ENABLE_PREVIEWS = YES;
|
||||
INFOPLIST_FILE = iOS/Info.plist;
|
||||
|
@ -519,6 +881,7 @@
|
|||
CODE_SIGN_ENTITLEMENTS = macOS/macOS.entitlements;
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
COMBINE_HIDPI_IMAGES = YES;
|
||||
DEVELOPMENT_ASSET_PATHS = "Development\\ Assets";
|
||||
DEVELOPMENT_TEAM = 82HL67AXQ2;
|
||||
ENABLE_HARDENED_RUNTIME = YES;
|
||||
ENABLE_PREVIEWS = YES;
|
||||
|
@ -543,6 +906,7 @@
|
|||
CODE_SIGN_ENTITLEMENTS = macOS/macOS.entitlements;
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
COMBINE_HIDPI_IMAGES = YES;
|
||||
DEVELOPMENT_ASSET_PATHS = "Development\\ Assets";
|
||||
DEVELOPMENT_TEAM = 82HL67AXQ2;
|
||||
ENABLE_HARDENED_RUNTIME = YES;
|
||||
ENABLE_PREVIEWS = YES;
|
||||
|
@ -559,95 +923,54 @@
|
|||
};
|
||||
name = Release;
|
||||
};
|
||||
D047FABD24C3E21200AF17C5 /* Debug */ = {
|
||||
D0666A2924C677B400F3F04B /* Debug */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES;
|
||||
BUNDLE_LOADER = "$(TEST_HOST)";
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
DEVELOPMENT_TEAM = 82HL67AXQ2;
|
||||
INFOPLIST_FILE = "Tests iOS/Info.plist";
|
||||
INFOPLIST_FILE = Tests/Info.plist;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 14.0;
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
"@loader_path/Frameworks",
|
||||
);
|
||||
PRODUCT_BUNDLE_IDENTIFIER = "com.metabolist.Tests-iOS";
|
||||
PRODUCT_BUNDLE_IDENTIFIER = "com.metabolist.Unit-Tests";
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SDKROOT = iphoneos;
|
||||
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
|
||||
SWIFT_VERSION = 5.0;
|
||||
TARGETED_DEVICE_FAMILY = "1,2";
|
||||
TEST_TARGET_NAME = "Metatext (iOS)";
|
||||
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Metatext.app/Metatext";
|
||||
};
|
||||
name = Debug;
|
||||
};
|
||||
D047FABE24C3E21200AF17C5 /* Release */ = {
|
||||
D0666A2A24C677B400F3F04B /* Release */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES;
|
||||
BUNDLE_LOADER = "$(TEST_HOST)";
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
DEVELOPMENT_TEAM = 82HL67AXQ2;
|
||||
INFOPLIST_FILE = "Tests iOS/Info.plist";
|
||||
INFOPLIST_FILE = Tests/Info.plist;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 14.0;
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
"@loader_path/Frameworks",
|
||||
);
|
||||
PRODUCT_BUNDLE_IDENTIFIER = "com.metabolist.Tests-iOS";
|
||||
PRODUCT_BUNDLE_IDENTIFIER = "com.metabolist.Unit-Tests";
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SDKROOT = iphoneos;
|
||||
SWIFT_VERSION = 5.0;
|
||||
TARGETED_DEVICE_FAMILY = "1,2";
|
||||
TEST_TARGET_NAME = "Metatext (iOS)";
|
||||
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Metatext.app/Metatext";
|
||||
VALIDATE_PRODUCT = YES;
|
||||
};
|
||||
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 */
|
||||
|
||||
/* Begin XCConfigurationList section */
|
||||
|
@ -678,25 +1001,71 @@
|
|||
defaultConfigurationIsVisible = 0;
|
||||
defaultConfigurationName = Release;
|
||||
};
|
||||
D047FABC24C3E21200AF17C5 /* Build configuration list for PBXNativeTarget "Tests iOS" */ = {
|
||||
D0666A2824C677B400F3F04B /* Build configuration list for PBXNativeTarget "Tests" */ = {
|
||||
isa = XCConfigurationList;
|
||||
buildConfigurations = (
|
||||
D047FABD24C3E21200AF17C5 /* Debug */,
|
||||
D047FABE24C3E21200AF17C5 /* Release */,
|
||||
);
|
||||
defaultConfigurationIsVisible = 0;
|
||||
defaultConfigurationName = Release;
|
||||
};
|
||||
D047FABF24C3E21200AF17C5 /* Build configuration list for PBXNativeTarget "Tests macOS" */ = {
|
||||
isa = XCConfigurationList;
|
||||
buildConfigurations = (
|
||||
D047FAC024C3E21200AF17C5 /* Debug */,
|
||||
D047FAC124C3E21200AF17C5 /* Release */,
|
||||
D0666A2924C677B400F3F04B /* Debug */,
|
||||
D0666A2A24C677B400F3F04B /* Release */,
|
||||
);
|
||||
defaultConfigurationIsVisible = 0;
|
||||
defaultConfigurationName = Release;
|
||||
};
|
||||
/* 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 */;
|
||||
}
|
||||
|
|
|
@ -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
|
||||
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 {
|
||||
WindowGroup {
|
||||
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
|
||||
|
||||
struct ContentView: View {
|
||||
struct TimelineView: 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 {
|
||||
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()
|
||||
}
|
||||
}
|
|
@ -2,9 +2,11 @@
|
|||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>com.apple.security.app-sandbox</key>
|
||||
<true/>
|
||||
<key>com.apple.security.files.user-selected.read-only</key>
|
||||
<true/>
|
||||
<key>com.apple.security.app-sandbox</key>
|
||||
<true/>
|
||||
<key>com.apple.security.files.user-selected.read-only</key>
|
||||
<true/>
|
||||
<key>com.apple.security.network.client</key>
|
||||
<true/>
|
||||
</dict>
|
||||
</plist>
|
||||
|
|
Loading…
Reference in a new issue