Import and refactor stuff from old UIKit project

This commit is contained in:
Justin Mazzocchi 2020-07-29 16:50:30 -07:00
parent db27aaa206
commit f384c659df
No known key found for this signature in database
GPG Key ID: E223E6937AAFB01C
61 changed files with 2532 additions and 293 deletions

1
.gitignore vendored
View File

@ -1 +1,2 @@
xcuserdata/
*.xcodeproj/xcuserdata

2
.swiftlint.yml Normal file
View File

@ -0,0 +1,2 @@
disabled_rules:
identifier_name

View 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]
}
}

View 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)
}
}

View File

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

View File

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

View File

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

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

View 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() {}
}

View 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)
}
}

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

View File

@ -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 */;
}

View File

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

View File

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

View File

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

View 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)
}
}

View 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()
}
}

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

View 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))
}
}
}

View 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";

View File

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

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

View 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?
}

View File

@ -0,0 +1,8 @@
// Copyright © 2020 Metabolist. All rights reserved.
import Foundation
struct AlertItem: Identifiable {
let id = UUID()
let error: Error
}

View 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?
}

View File

@ -0,0 +1,8 @@
// Copyright © 2020 Metabolist. All rights reserved.
import Foundation
struct Application: Codable {
let name: String
let website: String
}

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

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

View 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 {}

View 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?
}

View 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
]
}
}

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

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

View 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)
}
}

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

View 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
]
}
}
}

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

View File

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

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

View 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: "")
}
}
}

View 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)
}
}

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

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

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

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

View 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()
}
}

View 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()
}
}

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

View File

@ -0,0 +1,7 @@
// Copyright © 2020 Metabolist. All rights reserved.
import Foundation
class TimelineViewModel: ObservableObject {
}

View 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())))
}
}

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

View File

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

View File

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

View File

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

View File

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

View 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
View 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()
}
}

View 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()
}
}

View File

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