mirror of
https://github.com/metabolist/metatext.git
synced 2024-11-21 15:50:59 +00:00
Modularize database code
This commit is contained in:
parent
280ec03ee9
commit
06b84a0aa7
28 changed files with 526 additions and 423 deletions
5
DB/.gitignore
vendored
Normal file
5
DB/.gitignore
vendored
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
.DS_Store
|
||||||
|
/.build
|
||||||
|
/Packages
|
||||||
|
/*.xcodeproj
|
||||||
|
xcuserdata/
|
28
DB/Package.swift
Normal file
28
DB/Package.swift
Normal file
|
@ -0,0 +1,28 @@
|
||||||
|
// swift-tools-version:5.3
|
||||||
|
|
||||||
|
import PackageDescription
|
||||||
|
|
||||||
|
let package = Package(
|
||||||
|
name: "DB",
|
||||||
|
platforms: [
|
||||||
|
.iOS(.v14),
|
||||||
|
.macOS(.v11)
|
||||||
|
],
|
||||||
|
products: [
|
||||||
|
.library(
|
||||||
|
name: "DB",
|
||||||
|
targets: ["DB"])
|
||||||
|
],
|
||||||
|
dependencies: [
|
||||||
|
.package(name: "GRDB", url: "https://github.com/groue/GRDB.swift.git", .upToNextMajor(from: "5.0.0-beta.10")),
|
||||||
|
.package(path: "Mastodon")
|
||||||
|
],
|
||||||
|
targets: [
|
||||||
|
.target(
|
||||||
|
name: "DB",
|
||||||
|
dependencies: ["GRDB", "Mastodon"]),
|
||||||
|
.testTarget(
|
||||||
|
name: "DBTests",
|
||||||
|
dependencies: ["DB"])
|
||||||
|
]
|
||||||
|
)
|
|
@ -5,12 +5,11 @@ import Combine
|
||||||
import GRDB
|
import GRDB
|
||||||
import Mastodon
|
import Mastodon
|
||||||
|
|
||||||
// swiftlint:disable file_length
|
public struct ContentDatabase {
|
||||||
struct ContentDatabase {
|
|
||||||
private let databaseQueue: DatabaseQueue
|
private let databaseQueue: DatabaseQueue
|
||||||
|
|
||||||
init(identityID: UUID, environment: AppEnvironment) throws {
|
public init(identityID: UUID, inMemory: Bool) throws {
|
||||||
if environment.inMemoryContent {
|
if inMemory {
|
||||||
databaseQueue = DatabaseQueue()
|
databaseQueue = DatabaseQueue()
|
||||||
} else {
|
} else {
|
||||||
databaseQueue = try DatabaseQueue(path: try Self.fileURL(identityID: identityID).path)
|
databaseQueue = try DatabaseQueue(path: try Self.fileURL(identityID: identityID).path)
|
||||||
|
@ -20,7 +19,7 @@ struct ContentDatabase {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
extension ContentDatabase {
|
public extension ContentDatabase {
|
||||||
static func delete(forIdentityID identityID: UUID) throws {
|
static func delete(forIdentityID identityID: UUID) throws {
|
||||||
try FileManager.default.removeItem(at: try fileURL(identityID: identityID))
|
try FileManager.default.removeItem(at: try fileURL(identityID: identityID))
|
||||||
}
|
}
|
||||||
|
@ -287,274 +286,3 @@ private extension ContentDatabase {
|
||||||
}
|
}
|
||||||
// swiftlint:enable function_body_length
|
// swiftlint:enable function_body_length
|
||||||
}
|
}
|
||||||
|
|
||||||
extension Account: FetchableRecord, PersistableRecord {
|
|
||||||
public static func databaseJSONDecoder(for column: String) -> JSONDecoder {
|
|
||||||
APIDecoder()
|
|
||||||
}
|
|
||||||
|
|
||||||
public static func databaseJSONEncoder(for column: String) -> JSONEncoder {
|
|
||||||
APIEncoder()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private struct TimelineStatusJoin: Codable, FetchableRecord, PersistableRecord {
|
|
||||||
let timelineId: String
|
|
||||||
let statusId: String
|
|
||||||
|
|
||||||
static let status = belongsTo(StoredStatus.self)
|
|
||||||
}
|
|
||||||
|
|
||||||
extension Timeline: FetchableRecord, PersistableRecord {
|
|
||||||
enum Columns: String, ColumnExpression {
|
|
||||||
case id, listTitle
|
|
||||||
}
|
|
||||||
|
|
||||||
public init(row: Row) {
|
|
||||||
switch (row[Columns.id] as String, row[Columns.listTitle] as String?) {
|
|
||||||
case (Timeline.home.id, _):
|
|
||||||
self = .home
|
|
||||||
case (Timeline.local.id, _):
|
|
||||||
self = .local
|
|
||||||
case (Timeline.federated.id, _):
|
|
||||||
self = .federated
|
|
||||||
case (let id, .some(let title)):
|
|
||||||
self = .list(MastodonList(id: id, title: title))
|
|
||||||
default:
|
|
||||||
self = .tag(row[Columns.id])
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public func encode(to container: inout PersistenceContainer) {
|
|
||||||
container[Columns.id] = id
|
|
||||||
|
|
||||||
if case let .list(list) = self {
|
|
||||||
container[Columns.listTitle] = list.title
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private extension Timeline {
|
|
||||||
static let statusJoins = hasMany(TimelineStatusJoin.self)
|
|
||||||
static let statuses = hasMany(
|
|
||||||
StoredStatus.self,
|
|
||||||
through: statusJoins,
|
|
||||||
using: TimelineStatusJoin.status)
|
|
||||||
.order(Column("createdAt").desc)
|
|
||||||
|
|
||||||
var statuses: QueryInterfaceRequest<StatusResult> {
|
|
||||||
request(for: Self.statuses).statusResultRequest
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private struct StatusContextJoin: Codable, FetchableRecord, PersistableRecord {
|
|
||||||
enum Section: String, Codable {
|
|
||||||
case ancestors
|
|
||||||
case descendants
|
|
||||||
}
|
|
||||||
|
|
||||||
let parentId: String
|
|
||||||
let statusId: String
|
|
||||||
let section: Section
|
|
||||||
let index: Int
|
|
||||||
|
|
||||||
static let status = belongsTo(StoredStatus.self, using: ForeignKey([Column("statusId")]))
|
|
||||||
}
|
|
||||||
|
|
||||||
private extension StoredStatus {
|
|
||||||
static let ancestorJoins = hasMany(StatusContextJoin.self, using: ForeignKey([Column("parentID")]))
|
|
||||||
.filter(Column("section") == StatusContextJoin.Section.ancestors.rawValue)
|
|
||||||
.order(Column("index"))
|
|
||||||
static let descendantJoins = hasMany(StatusContextJoin.self, using: ForeignKey([Column("parentID")]))
|
|
||||||
.filter(Column("section") == StatusContextJoin.Section.descendants.rawValue)
|
|
||||||
.order(Column("index"))
|
|
||||||
static let ancestors = hasMany(StoredStatus.self,
|
|
||||||
through: ancestorJoins,
|
|
||||||
using: StatusContextJoin.status)
|
|
||||||
static let descendants = hasMany(StoredStatus.self,
|
|
||||||
through: descendantJoins,
|
|
||||||
using: StatusContextJoin.status)
|
|
||||||
|
|
||||||
var ancestors: QueryInterfaceRequest<StatusResult> {
|
|
||||||
request(for: Self.ancestors).statusResultRequest
|
|
||||||
}
|
|
||||||
|
|
||||||
var descendants: QueryInterfaceRequest<StatusResult> {
|
|
||||||
request(for: Self.descendants).statusResultRequest
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private extension QueryInterfaceRequest where RowDecoder == StoredStatus {
|
|
||||||
var statusResultRequest: QueryInterfaceRequest<StatusResult> {
|
|
||||||
including(required: StoredStatus.account)
|
|
||||||
.including(optional: StoredStatus.reblogAccount)
|
|
||||||
.including(optional: StoredStatus.reblog)
|
|
||||||
.asRequest(of: StatusResult.self)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
extension Filter: FetchableRecord, PersistableRecord {
|
|
||||||
public static func databaseJSONDecoder(for column: String) -> JSONDecoder {
|
|
||||||
APIDecoder()
|
|
||||||
}
|
|
||||||
|
|
||||||
public static func databaseJSONEncoder(for column: String) -> JSONEncoder {
|
|
||||||
APIEncoder()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
struct StoredStatus: Codable, Hashable {
|
|
||||||
let id: String
|
|
||||||
let uri: String
|
|
||||||
let createdAt: Date
|
|
||||||
let accountId: String
|
|
||||||
let content: HTML
|
|
||||||
let visibility: Status.Visibility
|
|
||||||
let sensitive: Bool
|
|
||||||
let spoilerText: String
|
|
||||||
let mediaAttachments: [Attachment]
|
|
||||||
let mentions: [Mention]
|
|
||||||
let tags: [Tag]
|
|
||||||
let emojis: [Emoji]
|
|
||||||
let reblogsCount: Int
|
|
||||||
let favouritesCount: Int
|
|
||||||
let repliesCount: Int
|
|
||||||
let application: Application?
|
|
||||||
let url: URL?
|
|
||||||
let inReplyToId: String?
|
|
||||||
let inReplyToAccountId: String?
|
|
||||||
let reblogId: String?
|
|
||||||
let poll: Poll?
|
|
||||||
let card: Card?
|
|
||||||
let language: String?
|
|
||||||
let text: String?
|
|
||||||
let favourited: Bool
|
|
||||||
let reblogged: Bool
|
|
||||||
let muted: Bool
|
|
||||||
let bookmarked: Bool
|
|
||||||
let pinned: Bool?
|
|
||||||
}
|
|
||||||
|
|
||||||
private extension StoredStatus {
|
|
||||||
static let account = belongsTo(Account.self, key: "account")
|
|
||||||
static let reblogAccount = hasOne(Account.self, through: Self.reblog, using: Self.account, key: "reblogAccount")
|
|
||||||
static let reblog = belongsTo(StoredStatus.self, key: "reblog")
|
|
||||||
|
|
||||||
var account: QueryInterfaceRequest<Account> {
|
|
||||||
request(for: Self.account)
|
|
||||||
}
|
|
||||||
|
|
||||||
var reblogAccount: QueryInterfaceRequest<Account> {
|
|
||||||
request(for: Self.reblogAccount)
|
|
||||||
}
|
|
||||||
|
|
||||||
var reblog: QueryInterfaceRequest<StoredStatus> {
|
|
||||||
request(for: Self.reblog)
|
|
||||||
}
|
|
||||||
|
|
||||||
init(status: Status) {
|
|
||||||
id = status.id
|
|
||||||
uri = status.uri
|
|
||||||
createdAt = status.createdAt
|
|
||||||
accountId = status.account.id
|
|
||||||
content = status.content
|
|
||||||
visibility = status.visibility
|
|
||||||
sensitive = status.sensitive
|
|
||||||
spoilerText = status.spoilerText
|
|
||||||
mediaAttachments = status.mediaAttachments
|
|
||||||
mentions = status.mentions
|
|
||||||
tags = status.tags
|
|
||||||
emojis = status.emojis
|
|
||||||
reblogsCount = status.reblogsCount
|
|
||||||
favouritesCount = status.favouritesCount
|
|
||||||
repliesCount = status.repliesCount
|
|
||||||
application = status.application
|
|
||||||
url = status.url
|
|
||||||
inReplyToId = status.inReplyToId
|
|
||||||
inReplyToAccountId = status.inReplyToAccountId
|
|
||||||
reblogId = status.reblog?.id
|
|
||||||
poll = status.poll
|
|
||||||
card = status.card
|
|
||||||
language = status.language
|
|
||||||
text = status.text
|
|
||||||
favourited = status.favourited
|
|
||||||
reblogged = status.reblogged
|
|
||||||
muted = status.muted
|
|
||||||
bookmarked = status.bookmarked
|
|
||||||
pinned = status.pinned
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
extension StoredStatus: FetchableRecord, PersistableRecord {
|
|
||||||
static func databaseJSONDecoder(for column: String) -> JSONDecoder {
|
|
||||||
APIDecoder()
|
|
||||||
}
|
|
||||||
|
|
||||||
static func databaseJSONEncoder(for column: String) -> JSONEncoder {
|
|
||||||
APIEncoder()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
struct StatusResult: Codable, Hashable, FetchableRecord {
|
|
||||||
let account: Account
|
|
||||||
let status: StoredStatus
|
|
||||||
let reblogAccount: Account?
|
|
||||||
let reblog: StoredStatus?
|
|
||||||
}
|
|
||||||
|
|
||||||
private extension Status {
|
|
||||||
func save(_ db: Database) throws {
|
|
||||||
try account.save(db)
|
|
||||||
|
|
||||||
if let reblog = reblog {
|
|
||||||
try reblog.account.save(db)
|
|
||||||
try StoredStatus(status: reblog).save(db)
|
|
||||||
}
|
|
||||||
|
|
||||||
try StoredStatus(status: self).save(db)
|
|
||||||
}
|
|
||||||
|
|
||||||
convenience init(statusResult: StatusResult) {
|
|
||||||
var reblog: Status?
|
|
||||||
|
|
||||||
if let reblogResult = statusResult.reblog, let reblogAccount = statusResult.reblogAccount {
|
|
||||||
reblog = Status(storedStatus: reblogResult, account: reblogAccount, reblog: nil)
|
|
||||||
}
|
|
||||||
|
|
||||||
self.init(storedStatus: statusResult.status, account: statusResult.account, reblog: reblog)
|
|
||||||
}
|
|
||||||
|
|
||||||
convenience init(storedStatus: StoredStatus, account: Account, reblog: Status?) {
|
|
||||||
self.init(
|
|
||||||
id: storedStatus.id,
|
|
||||||
uri: storedStatus.uri,
|
|
||||||
createdAt: storedStatus.createdAt,
|
|
||||||
account: account,
|
|
||||||
content: storedStatus.content,
|
|
||||||
visibility: storedStatus.visibility,
|
|
||||||
sensitive: storedStatus.sensitive,
|
|
||||||
spoilerText: storedStatus.spoilerText,
|
|
||||||
mediaAttachments: storedStatus.mediaAttachments,
|
|
||||||
mentions: storedStatus.mentions,
|
|
||||||
tags: storedStatus.tags,
|
|
||||||
emojis: storedStatus.emojis,
|
|
||||||
reblogsCount: storedStatus.reblogsCount,
|
|
||||||
favouritesCount: storedStatus.favouritesCount,
|
|
||||||
repliesCount: storedStatus.repliesCount,
|
|
||||||
application: storedStatus.application,
|
|
||||||
url: storedStatus.url,
|
|
||||||
inReplyToId: storedStatus.inReplyToId,
|
|
||||||
inReplyToAccountId: storedStatus.inReplyToAccountId,
|
|
||||||
reblog: reblog,
|
|
||||||
poll: storedStatus.poll,
|
|
||||||
card: storedStatus.card,
|
|
||||||
language: storedStatus.language,
|
|
||||||
text: storedStatus.text,
|
|
||||||
favourited: storedStatus.favourited,
|
|
||||||
reblogged: storedStatus.reblogged,
|
|
||||||
muted: storedStatus.muted,
|
|
||||||
bookmarked: storedStatus.bookmarked,
|
|
||||||
pinned: storedStatus.pinned)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// swiftlint:enable file_length
|
|
18
DB/Sources/DB/Content/StatusContextJoin.swift
Normal file
18
DB/Sources/DB/Content/StatusContextJoin.swift
Normal file
|
@ -0,0 +1,18 @@
|
||||||
|
// Copyright © 2020 Metabolist. All rights reserved.
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
import GRDB
|
||||||
|
|
||||||
|
struct StatusContextJoin: Codable, FetchableRecord, PersistableRecord {
|
||||||
|
enum Section: String, Codable {
|
||||||
|
case ancestors
|
||||||
|
case descendants
|
||||||
|
}
|
||||||
|
|
||||||
|
let parentId: String
|
||||||
|
let statusId: String
|
||||||
|
let section: Section
|
||||||
|
let index: Int
|
||||||
|
|
||||||
|
static let status = belongsTo(StoredStatus.self, using: ForeignKey([Column("statusId")]))
|
||||||
|
}
|
21
DB/Sources/DB/Content/StatusResult.swift
Normal file
21
DB/Sources/DB/Content/StatusResult.swift
Normal file
|
@ -0,0 +1,21 @@
|
||||||
|
// Copyright © 2020 Metabolist. All rights reserved.
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
import GRDB
|
||||||
|
import Mastodon
|
||||||
|
|
||||||
|
struct StatusResult: Codable, Hashable, FetchableRecord {
|
||||||
|
let account: Account
|
||||||
|
let status: StoredStatus
|
||||||
|
let reblogAccount: Account?
|
||||||
|
let reblog: StoredStatus?
|
||||||
|
}
|
||||||
|
|
||||||
|
extension QueryInterfaceRequest where RowDecoder == StoredStatus {
|
||||||
|
var statusResultRequest: QueryInterfaceRequest<StatusResult> {
|
||||||
|
including(required: StoredStatus.account)
|
||||||
|
.including(optional: StoredStatus.reblogAccount)
|
||||||
|
.including(optional: StoredStatus.reblog)
|
||||||
|
.asRequest(of: StatusResult.self)
|
||||||
|
}
|
||||||
|
}
|
117
DB/Sources/DB/Content/StoredStatus.swift
Normal file
117
DB/Sources/DB/Content/StoredStatus.swift
Normal file
|
@ -0,0 +1,117 @@
|
||||||
|
// Copyright © 2020 Metabolist. All rights reserved.
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
import GRDB
|
||||||
|
import Mastodon
|
||||||
|
|
||||||
|
struct StoredStatus: Codable, Hashable {
|
||||||
|
let id: String
|
||||||
|
let uri: String
|
||||||
|
let createdAt: Date
|
||||||
|
let accountId: String
|
||||||
|
let content: HTML
|
||||||
|
let visibility: Status.Visibility
|
||||||
|
let sensitive: Bool
|
||||||
|
let spoilerText: String
|
||||||
|
let mediaAttachments: [Attachment]
|
||||||
|
let mentions: [Mention]
|
||||||
|
let tags: [Tag]
|
||||||
|
let emojis: [Emoji]
|
||||||
|
let reblogsCount: Int
|
||||||
|
let favouritesCount: Int
|
||||||
|
let repliesCount: Int
|
||||||
|
let application: Application?
|
||||||
|
let url: URL?
|
||||||
|
let inReplyToId: String?
|
||||||
|
let inReplyToAccountId: String?
|
||||||
|
let reblogId: String?
|
||||||
|
let poll: Poll?
|
||||||
|
let card: Card?
|
||||||
|
let language: String?
|
||||||
|
let text: String?
|
||||||
|
let favourited: Bool
|
||||||
|
let reblogged: Bool
|
||||||
|
let muted: Bool
|
||||||
|
let bookmarked: Bool
|
||||||
|
let pinned: Bool?
|
||||||
|
}
|
||||||
|
|
||||||
|
extension StoredStatus: FetchableRecord, PersistableRecord {
|
||||||
|
static func databaseJSONDecoder(for column: String) -> JSONDecoder {
|
||||||
|
APIDecoder()
|
||||||
|
}
|
||||||
|
|
||||||
|
static func databaseJSONEncoder(for column: String) -> JSONEncoder {
|
||||||
|
APIEncoder()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension StoredStatus {
|
||||||
|
static let account = belongsTo(Account.self, key: "account")
|
||||||
|
static let reblogAccount = hasOne(Account.self, through: Self.reblog, using: Self.account, key: "reblogAccount")
|
||||||
|
static let reblog = belongsTo(StoredStatus.self, key: "reblog")
|
||||||
|
static let ancestorJoins = hasMany(StatusContextJoin.self, using: ForeignKey([Column("parentID")]))
|
||||||
|
.filter(Column("section") == StatusContextJoin.Section.ancestors.rawValue)
|
||||||
|
.order(Column("index"))
|
||||||
|
static let descendantJoins = hasMany(StatusContextJoin.self, using: ForeignKey([Column("parentID")]))
|
||||||
|
.filter(Column("section") == StatusContextJoin.Section.descendants.rawValue)
|
||||||
|
.order(Column("index"))
|
||||||
|
static let ancestors = hasMany(StoredStatus.self,
|
||||||
|
through: ancestorJoins,
|
||||||
|
using: StatusContextJoin.status)
|
||||||
|
static let descendants = hasMany(StoredStatus.self,
|
||||||
|
through: descendantJoins,
|
||||||
|
using: StatusContextJoin.status)
|
||||||
|
|
||||||
|
var account: QueryInterfaceRequest<Account> {
|
||||||
|
request(for: Self.account)
|
||||||
|
}
|
||||||
|
|
||||||
|
var reblogAccount: QueryInterfaceRequest<Account> {
|
||||||
|
request(for: Self.reblogAccount)
|
||||||
|
}
|
||||||
|
|
||||||
|
var reblog: QueryInterfaceRequest<StoredStatus> {
|
||||||
|
request(for: Self.reblog)
|
||||||
|
}
|
||||||
|
|
||||||
|
var ancestors: QueryInterfaceRequest<StatusResult> {
|
||||||
|
request(for: Self.ancestors).statusResultRequest
|
||||||
|
}
|
||||||
|
|
||||||
|
var descendants: QueryInterfaceRequest<StatusResult> {
|
||||||
|
request(for: Self.descendants).statusResultRequest
|
||||||
|
}
|
||||||
|
|
||||||
|
init(status: Status) {
|
||||||
|
id = status.id
|
||||||
|
uri = status.uri
|
||||||
|
createdAt = status.createdAt
|
||||||
|
accountId = status.account.id
|
||||||
|
content = status.content
|
||||||
|
visibility = status.visibility
|
||||||
|
sensitive = status.sensitive
|
||||||
|
spoilerText = status.spoilerText
|
||||||
|
mediaAttachments = status.mediaAttachments
|
||||||
|
mentions = status.mentions
|
||||||
|
tags = status.tags
|
||||||
|
emojis = status.emojis
|
||||||
|
reblogsCount = status.reblogsCount
|
||||||
|
favouritesCount = status.favouritesCount
|
||||||
|
repliesCount = status.repliesCount
|
||||||
|
application = status.application
|
||||||
|
url = status.url
|
||||||
|
inReplyToId = status.inReplyToId
|
||||||
|
inReplyToAccountId = status.inReplyToAccountId
|
||||||
|
reblogId = status.reblog?.id
|
||||||
|
poll = status.poll
|
||||||
|
card = status.card
|
||||||
|
language = status.language
|
||||||
|
text = status.text
|
||||||
|
favourited = status.favourited
|
||||||
|
reblogged = status.reblogged
|
||||||
|
muted = status.muted
|
||||||
|
bookmarked = status.bookmarked
|
||||||
|
pinned = status.pinned
|
||||||
|
}
|
||||||
|
}
|
11
DB/Sources/DB/Content/TimelineStatusJoin.swift
Normal file
11
DB/Sources/DB/Content/TimelineStatusJoin.swift
Normal file
|
@ -0,0 +1,11 @@
|
||||||
|
// Copyright © 2020 Metabolist. All rights reserved.
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
import GRDB
|
||||||
|
|
||||||
|
struct TimelineStatusJoin: Codable, FetchableRecord, PersistableRecord {
|
||||||
|
let timelineId: String
|
||||||
|
let statusId: String
|
||||||
|
|
||||||
|
static let status = belongsTo(StoredStatus.self)
|
||||||
|
}
|
71
DB/Sources/DB/Entities/Identity.swift
Normal file
71
DB/Sources/DB/Entities/Identity.swift
Normal file
|
@ -0,0 +1,71 @@
|
||||||
|
// Copyright © 2020 Metabolist. All rights reserved.
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
import Mastodon
|
||||||
|
|
||||||
|
public struct Identity: Codable, Hashable, Identifiable {
|
||||||
|
public let id: UUID
|
||||||
|
public let url: URL
|
||||||
|
public let lastUsedAt: Date
|
||||||
|
public let preferences: Identity.Preferences
|
||||||
|
public let instance: Identity.Instance?
|
||||||
|
public let account: Identity.Account?
|
||||||
|
public let lastRegisteredDeviceToken: String?
|
||||||
|
public let pushSubscriptionAlerts: PushSubscription.Alerts
|
||||||
|
}
|
||||||
|
|
||||||
|
public extension Identity {
|
||||||
|
struct Instance: Codable, Hashable {
|
||||||
|
public let uri: String
|
||||||
|
public let streamingAPI: URL
|
||||||
|
public let title: String
|
||||||
|
public let thumbnail: URL?
|
||||||
|
}
|
||||||
|
|
||||||
|
struct Account: Codable, Hashable {
|
||||||
|
public let id: String
|
||||||
|
public let identityID: UUID
|
||||||
|
public let username: String
|
||||||
|
public let displayName: String
|
||||||
|
public let url: URL
|
||||||
|
public let avatar: URL
|
||||||
|
public let avatarStatic: URL
|
||||||
|
public let header: URL
|
||||||
|
public let headerStatic: URL
|
||||||
|
public let emojis: [Emoji]
|
||||||
|
}
|
||||||
|
|
||||||
|
struct Preferences: Codable, Hashable {
|
||||||
|
@DecodableDefault.True public var useServerPostingReadingPreferences
|
||||||
|
@DecodableDefault.StatusVisibilityPublic public var postingDefaultVisibility: Status.Visibility
|
||||||
|
@DecodableDefault.False public var postingDefaultSensitive
|
||||||
|
public var postingDefaultLanguage: String?
|
||||||
|
@DecodableDefault.ExpandMediaDefault public var readingExpandMedia: Mastodon.Preferences.ExpandMedia
|
||||||
|
@DecodableDefault.False public var readingExpandSpoilers
|
||||||
|
}
|
||||||
|
|
||||||
|
var handle: String {
|
||||||
|
if let account = account, let host = account.url.host {
|
||||||
|
return account.url.lastPathComponent + "@" + host
|
||||||
|
}
|
||||||
|
|
||||||
|
return instance?.title ?? url.host ?? url.absoluteString
|
||||||
|
}
|
||||||
|
|
||||||
|
var image: URL? { account?.avatar ?? instance?.thumbnail }
|
||||||
|
}
|
||||||
|
|
||||||
|
public extension Identity.Preferences {
|
||||||
|
func updated(from serverPreferences: Preferences) -> Self {
|
||||||
|
var mutable = self
|
||||||
|
|
||||||
|
if useServerPostingReadingPreferences {
|
||||||
|
mutable.postingDefaultVisibility = serverPreferences.postingDefaultVisibility
|
||||||
|
mutable.postingDefaultSensitive = serverPreferences.postingDefaultSensitive
|
||||||
|
mutable.readingExpandMedia = serverPreferences.readingExpandMedia
|
||||||
|
mutable.readingExpandSpoilers = serverPreferences.readingExpandSpoilers
|
||||||
|
}
|
||||||
|
|
||||||
|
return mutable
|
||||||
|
}
|
||||||
|
}
|
18
DB/Sources/DB/Entities/IdentityFixture.swift
Normal file
18
DB/Sources/DB/Entities/IdentityFixture.swift
Normal file
|
@ -0,0 +1,18 @@
|
||||||
|
// Copyright © 2020 Metabolist. All rights reserved.
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
import Mastodon
|
||||||
|
|
||||||
|
public struct IdentityFixture {
|
||||||
|
public let id: UUID
|
||||||
|
public let instanceURL: URL
|
||||||
|
public let instance: Instance?
|
||||||
|
public let account: Account?
|
||||||
|
|
||||||
|
public init(id: UUID, instanceURL: URL, instance: Instance?, account: Account?) {
|
||||||
|
self.id = id
|
||||||
|
self.instanceURL = instanceURL
|
||||||
|
self.instance = instance
|
||||||
|
self.account = account
|
||||||
|
}
|
||||||
|
}
|
15
DB/Sources/DB/Extensions/Account+Extensions.swift
Normal file
15
DB/Sources/DB/Extensions/Account+Extensions.swift
Normal file
|
@ -0,0 +1,15 @@
|
||||||
|
// Copyright © 2020 Metabolist. All rights reserved.
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
import GRDB
|
||||||
|
import Mastodon
|
||||||
|
|
||||||
|
extension Account: FetchableRecord, PersistableRecord {
|
||||||
|
public static func databaseJSONDecoder(for column: String) -> JSONDecoder {
|
||||||
|
APIDecoder()
|
||||||
|
}
|
||||||
|
|
||||||
|
public static func databaseJSONEncoder(for column: String) -> JSONEncoder {
|
||||||
|
APIEncoder()
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,9 +1,4 @@
|
||||||
//
|
// Copyright © 2020 Metabolist. All rights reserved.
|
||||||
// File.swift
|
|
||||||
//
|
|
||||||
//
|
|
||||||
// Created by Justin Mazzocchi on 9/2/20.
|
|
||||||
//
|
|
||||||
|
|
||||||
import Foundation
|
import Foundation
|
||||||
|
|
15
DB/Sources/DB/Extensions/Filter+Extensions.swift
Normal file
15
DB/Sources/DB/Extensions/Filter+Extensions.swift
Normal file
|
@ -0,0 +1,15 @@
|
||||||
|
// Copyright © 2020 Metabolist. All rights reserved.
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
import GRDB
|
||||||
|
import Mastodon
|
||||||
|
|
||||||
|
extension Filter: FetchableRecord, PersistableRecord {
|
||||||
|
public static func databaseJSONDecoder(for column: String) -> JSONDecoder {
|
||||||
|
APIDecoder()
|
||||||
|
}
|
||||||
|
|
||||||
|
public static func databaseJSONEncoder(for column: String) -> JSONEncoder {
|
||||||
|
APIEncoder()
|
||||||
|
}
|
||||||
|
}
|
22
DB/Sources/DB/Extensions/Identity+Internal.swift
Normal file
22
DB/Sources/DB/Extensions/Identity+Internal.swift
Normal file
|
@ -0,0 +1,22 @@
|
||||||
|
// Copyright © 2020 Metabolist. All rights reserved.
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
import GRDB
|
||||||
|
|
||||||
|
extension Identity {
|
||||||
|
init(result: IdentityResult) {
|
||||||
|
self.init(
|
||||||
|
id: result.identity.id,
|
||||||
|
url: result.identity.url,
|
||||||
|
lastUsedAt: result.identity.lastUsedAt,
|
||||||
|
preferences: result.identity.preferences,
|
||||||
|
instance: result.instance,
|
||||||
|
account: result.account,
|
||||||
|
lastRegisteredDeviceToken: result.identity.lastRegisteredDeviceToken,
|
||||||
|
pushSubscriptionAlerts: result.pushSubscriptionAlerts)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension Identity.Instance: FetchableRecord, PersistableRecord {}
|
||||||
|
|
||||||
|
extension Identity.Account: FetchableRecord, PersistableRecord {}
|
61
DB/Sources/DB/Extensions/Status+ Extensions.swift
Normal file
61
DB/Sources/DB/Extensions/Status+ Extensions.swift
Normal file
|
@ -0,0 +1,61 @@
|
||||||
|
// Copyright © 2020 Metabolist. All rights reserved.
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
import GRDB
|
||||||
|
import Mastodon
|
||||||
|
|
||||||
|
extension Status {
|
||||||
|
func save(_ db: Database) throws {
|
||||||
|
try account.save(db)
|
||||||
|
|
||||||
|
if let reblog = reblog {
|
||||||
|
try reblog.account.save(db)
|
||||||
|
try StoredStatus(status: reblog).save(db)
|
||||||
|
}
|
||||||
|
|
||||||
|
try StoredStatus(status: self).save(db)
|
||||||
|
}
|
||||||
|
|
||||||
|
convenience init(statusResult: StatusResult) {
|
||||||
|
var reblog: Status?
|
||||||
|
|
||||||
|
if let reblogResult = statusResult.reblog, let reblogAccount = statusResult.reblogAccount {
|
||||||
|
reblog = Status(storedStatus: reblogResult, account: reblogAccount, reblog: nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
self.init(storedStatus: statusResult.status, account: statusResult.account, reblog: reblog)
|
||||||
|
}
|
||||||
|
|
||||||
|
convenience init(storedStatus: StoredStatus, account: Account, reblog: Status?) {
|
||||||
|
self.init(
|
||||||
|
id: storedStatus.id,
|
||||||
|
uri: storedStatus.uri,
|
||||||
|
createdAt: storedStatus.createdAt,
|
||||||
|
account: account,
|
||||||
|
content: storedStatus.content,
|
||||||
|
visibility: storedStatus.visibility,
|
||||||
|
sensitive: storedStatus.sensitive,
|
||||||
|
spoilerText: storedStatus.spoilerText,
|
||||||
|
mediaAttachments: storedStatus.mediaAttachments,
|
||||||
|
mentions: storedStatus.mentions,
|
||||||
|
tags: storedStatus.tags,
|
||||||
|
emojis: storedStatus.emojis,
|
||||||
|
reblogsCount: storedStatus.reblogsCount,
|
||||||
|
favouritesCount: storedStatus.favouritesCount,
|
||||||
|
repliesCount: storedStatus.repliesCount,
|
||||||
|
application: storedStatus.application,
|
||||||
|
url: storedStatus.url,
|
||||||
|
inReplyToId: storedStatus.inReplyToId,
|
||||||
|
inReplyToAccountId: storedStatus.inReplyToAccountId,
|
||||||
|
reblog: reblog,
|
||||||
|
poll: storedStatus.poll,
|
||||||
|
card: storedStatus.card,
|
||||||
|
language: storedStatus.language,
|
||||||
|
text: storedStatus.text,
|
||||||
|
favourited: storedStatus.favourited,
|
||||||
|
reblogged: storedStatus.reblogged,
|
||||||
|
muted: storedStatus.muted,
|
||||||
|
bookmarked: storedStatus.bookmarked,
|
||||||
|
pinned: storedStatus.pinned)
|
||||||
|
}
|
||||||
|
}
|
47
DB/Sources/DB/Extensions/Timeline+Extensions.swift
Normal file
47
DB/Sources/DB/Extensions/Timeline+Extensions.swift
Normal file
|
@ -0,0 +1,47 @@
|
||||||
|
// Copyright © 2020 Metabolist. All rights reserved.
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
import GRDB
|
||||||
|
import Mastodon
|
||||||
|
|
||||||
|
extension Timeline: FetchableRecord, PersistableRecord {
|
||||||
|
enum Columns: String, ColumnExpression {
|
||||||
|
case id, listTitle
|
||||||
|
}
|
||||||
|
|
||||||
|
public init(row: Row) {
|
||||||
|
switch (row[Columns.id] as String, row[Columns.listTitle] as String?) {
|
||||||
|
case (Timeline.home.id, _):
|
||||||
|
self = .home
|
||||||
|
case (Timeline.local.id, _):
|
||||||
|
self = .local
|
||||||
|
case (Timeline.federated.id, _):
|
||||||
|
self = .federated
|
||||||
|
case (let id, .some(let title)):
|
||||||
|
self = .list(MastodonList(id: id, title: title))
|
||||||
|
default:
|
||||||
|
self = .tag(row[Columns.id])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public func encode(to container: inout PersistenceContainer) {
|
||||||
|
container[Columns.id] = id
|
||||||
|
|
||||||
|
if case let .list(list) = self {
|
||||||
|
container[Columns.listTitle] = list.title
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension Timeline {
|
||||||
|
static let statusJoins = hasMany(TimelineStatusJoin.self)
|
||||||
|
static let statuses = hasMany(
|
||||||
|
StoredStatus.self,
|
||||||
|
through: statusJoins,
|
||||||
|
using: TimelineStatusJoin.status)
|
||||||
|
.order(Column("createdAt").desc)
|
||||||
|
|
||||||
|
var statuses: QueryInterfaceRequest<StatusResult> {
|
||||||
|
request(for: Self.statuses).statusResultRequest
|
||||||
|
}
|
||||||
|
}
|
|
@ -5,15 +5,15 @@ import Combine
|
||||||
import GRDB
|
import GRDB
|
||||||
import Mastodon
|
import Mastodon
|
||||||
|
|
||||||
enum IdentityDatabaseError: Error {
|
public enum IdentityDatabaseError: Error {
|
||||||
case identityNotFound
|
case identityNotFound
|
||||||
}
|
}
|
||||||
|
|
||||||
struct IdentityDatabase {
|
public struct IdentityDatabase {
|
||||||
private let databaseQueue: DatabaseQueue
|
private let databaseQueue: DatabaseQueue
|
||||||
|
|
||||||
init(environment: AppEnvironment) throws {
|
public init(inMemory: Bool, fixture: IdentityFixture?) throws {
|
||||||
if environment.inMemoryContent {
|
if inMemory {
|
||||||
databaseQueue = DatabaseQueue()
|
databaseQueue = DatabaseQueue()
|
||||||
} else {
|
} else {
|
||||||
let databaseURL = try FileManager.default.databaseDirectoryURL().appendingPathComponent("Identities.sqlite")
|
let databaseURL = try FileManager.default.databaseDirectoryURL().appendingPathComponent("Identities.sqlite")
|
||||||
|
@ -23,13 +23,13 @@ struct IdentityDatabase {
|
||||||
|
|
||||||
try Self.migrate(databaseQueue)
|
try Self.migrate(databaseQueue)
|
||||||
|
|
||||||
if let fixture = environment.identityFixture {
|
if let fixture = fixture {
|
||||||
try populate(fixture: fixture)
|
try populate(fixture: fixture)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
extension IdentityDatabase {
|
public extension IdentityDatabase {
|
||||||
func createIdentity(id: UUID, url: URL) -> AnyPublisher<Never, Error> {
|
func createIdentity(id: UUID, url: URL) -> AnyPublisher<Never, Error> {
|
||||||
databaseQueue.writePublisher(
|
databaseQueue.writePublisher(
|
||||||
updates: StoredIdentity(
|
updates: StoredIdentity(
|
||||||
|
@ -235,7 +235,7 @@ private extension IdentityDatabase {
|
||||||
try migrator.migrate(writer)
|
try migrator.migrate(writer)
|
||||||
}
|
}
|
||||||
|
|
||||||
func populate(fixture: AppEnvironment.IdentityFixture) throws {
|
func populate(fixture: IdentityFixture) throws {
|
||||||
_ = createIdentity(id: fixture.id, url: fixture.instanceURL)
|
_ = createIdentity(id: fixture.id, url: fixture.instanceURL)
|
||||||
.receive(on: ImmediateScheduler.shared)
|
.receive(on: ImmediateScheduler.shared)
|
||||||
.sink { _ in } receiveValue: { _ in }
|
.sink { _ in } receiveValue: { _ in }
|
||||||
|
@ -253,51 +253,3 @@ private extension IdentityDatabase {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private struct StoredIdentity: Codable, Hashable, FetchableRecord, PersistableRecord {
|
|
||||||
let id: UUID
|
|
||||||
let url: URL
|
|
||||||
let lastUsedAt: Date
|
|
||||||
let preferences: Identity.Preferences
|
|
||||||
let instanceURI: String?
|
|
||||||
let lastRegisteredDeviceToken: String?
|
|
||||||
let pushSubscriptionAlerts: PushSubscription.Alerts
|
|
||||||
}
|
|
||||||
|
|
||||||
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?
|
|
||||||
let pushSubscriptionAlerts: PushSubscription.Alerts
|
|
||||||
}
|
|
||||||
|
|
||||||
private extension Identity {
|
|
||||||
init(result: IdentityResult) {
|
|
||||||
self.init(
|
|
||||||
id: result.identity.id,
|
|
||||||
url: result.identity.url,
|
|
||||||
lastUsedAt: result.identity.lastUsedAt,
|
|
||||||
preferences: result.identity.preferences,
|
|
||||||
instance: result.instance,
|
|
||||||
account: result.account,
|
|
||||||
lastRegisteredDeviceToken: result.identity.lastRegisteredDeviceToken,
|
|
||||||
pushSubscriptionAlerts: result.pushSubscriptionAlerts)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
extension Identity.Instance: FetchableRecord, PersistableRecord {}
|
|
||||||
|
|
||||||
extension Identity.Account: FetchableRecord, PersistableRecord {}
|
|
12
DB/Sources/DB/Identity/IdentityResult.swift
Normal file
12
DB/Sources/DB/Identity/IdentityResult.swift
Normal file
|
@ -0,0 +1,12 @@
|
||||||
|
// Copyright © 2020 Metabolist. All rights reserved.
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
import GRDB
|
||||||
|
import Mastodon
|
||||||
|
|
||||||
|
struct IdentityResult: Codable, Hashable, FetchableRecord {
|
||||||
|
let identity: StoredIdentity
|
||||||
|
let instance: Identity.Instance?
|
||||||
|
let account: Identity.Account?
|
||||||
|
let pushSubscriptionAlerts: PushSubscription.Alerts
|
||||||
|
}
|
28
DB/Sources/DB/Identity/StoredIdentity.swift
Normal file
28
DB/Sources/DB/Identity/StoredIdentity.swift
Normal file
|
@ -0,0 +1,28 @@
|
||||||
|
// Copyright © 2020 Metabolist. All rights reserved.
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
import GRDB
|
||||||
|
import Mastodon
|
||||||
|
|
||||||
|
struct StoredIdentity: Codable, Hashable, FetchableRecord, PersistableRecord {
|
||||||
|
let id: UUID
|
||||||
|
let url: URL
|
||||||
|
let lastUsedAt: Date
|
||||||
|
let preferences: Identity.Preferences
|
||||||
|
let instanceURI: String?
|
||||||
|
let lastRegisteredDeviceToken: String?
|
||||||
|
let pushSubscriptionAlerts: PushSubscription.Alerts
|
||||||
|
}
|
||||||
|
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
10
DB/Tests/DBTests/DBTests.swift
Normal file
10
DB/Tests/DBTests/DBTests.swift
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
import XCTest
|
||||||
|
@testable import DB
|
||||||
|
|
||||||
|
final class DBTests: XCTestCase {
|
||||||
|
func testExample() {
|
||||||
|
// This is an example of a functional test case.
|
||||||
|
// Use XCTAssert and related functions to verify your tests produce the correct
|
||||||
|
// results.
|
||||||
|
}
|
||||||
|
}
|
|
@ -85,6 +85,7 @@
|
||||||
D047FA8C24C3E21200AF17C5 /* Metatext.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Metatext.app; sourceTree = BUILT_PRODUCTS_DIR; };
|
D047FA8C24C3E21200AF17C5 /* Metatext.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Metatext.app; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||||
D0666A2124C677B400F3F04B /* Tests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = Tests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
|
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>"; };
|
D0666A2524C677B400F3F04B /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
|
||||||
|
D085C3BB25008DEC008A6C5E /* DB */ = {isa = PBXFileReference; lastKnownFileType = folder; path = DB; sourceTree = "<group>"; };
|
||||||
D0BDF66524FD7A6400C7FA1C /* ServiceLayer */ = {isa = PBXFileReference; lastKnownFileType = folder; path = ServiceLayer; sourceTree = "<group>"; };
|
D0BDF66524FD7A6400C7FA1C /* ServiceLayer */ = {isa = PBXFileReference; lastKnownFileType = folder; path = ServiceLayer; sourceTree = "<group>"; };
|
||||||
D0BEB1F224F8EE8C001B0F04 /* AttachmentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AttachmentView.swift; sourceTree = "<group>"; };
|
D0BEB1F224F8EE8C001B0F04 /* AttachmentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AttachmentView.swift; sourceTree = "<group>"; };
|
||||||
D0BEB1F624F9A84B001B0F04 /* LoadingTableFooterView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoadingTableFooterView.swift; sourceTree = "<group>"; };
|
D0BEB1F624F9A84B001B0F04 /* LoadingTableFooterView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoadingTableFooterView.swift; sourceTree = "<group>"; };
|
||||||
|
@ -176,6 +177,7 @@
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
D0C7D45224F76169001EBDBB /* Assets.xcassets */,
|
D0C7D45224F76169001EBDBB /* Assets.xcassets */,
|
||||||
|
D085C3BB25008DEC008A6C5E /* DB */,
|
||||||
D0C7D46824F76169001EBDBB /* Extensions */,
|
D0C7D46824F76169001EBDBB /* Extensions */,
|
||||||
D0666A7924C7745A00F3F04B /* Frameworks */,
|
D0666A7924C7745A00F3F04B /* Frameworks */,
|
||||||
D0BFDAF524FC7C5300C86618 /* HTTP */,
|
D0BFDAF524FC7C5300C86618 /* HTTP */,
|
||||||
|
|
|
@ -18,13 +18,13 @@ let package = Package(
|
||||||
],
|
],
|
||||||
dependencies: [
|
dependencies: [
|
||||||
.package(url: "https://github.com/groue/CombineExpectations.git", .upToNextMajor(from: "0.5.0")),
|
.package(url: "https://github.com/groue/CombineExpectations.git", .upToNextMajor(from: "0.5.0")),
|
||||||
.package(name: "GRDB", url: "https://github.com/groue/GRDB.swift.git", .upToNextMajor(from: "5.0.0-beta.10")),
|
.package(path: "DB"),
|
||||||
.package(path: "Mastodon")
|
.package(path: "Mastodon")
|
||||||
],
|
],
|
||||||
targets: [
|
targets: [
|
||||||
.target(
|
.target(
|
||||||
name: "ServiceLayer",
|
name: "ServiceLayer",
|
||||||
dependencies: ["GRDB", "Mastodon"]),
|
dependencies: ["DB"]),
|
||||||
.target(
|
.target(
|
||||||
name: "ServiceLayerMocks",
|
name: "ServiceLayerMocks",
|
||||||
dependencies: ["ServiceLayer", .product(name: "MastodonStubs", package: "Mastodon")]),
|
dependencies: ["ServiceLayer", .product(name: "MastodonStubs", package: "Mastodon")]),
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
// Copyright © 2020 Metabolist. All rights reserved.
|
// Copyright © 2020 Metabolist. All rights reserved.
|
||||||
|
|
||||||
|
import DB
|
||||||
import Foundation
|
import Foundation
|
||||||
import Combine
|
import Combine
|
||||||
import Mastodon
|
import Mastodon
|
||||||
|
@ -11,7 +12,8 @@ public struct AllIdentitiesService {
|
||||||
private let environment: AppEnvironment
|
private let environment: AppEnvironment
|
||||||
|
|
||||||
public init(environment: AppEnvironment) throws {
|
public init(environment: AppEnvironment) throws {
|
||||||
self.identityDatabase = try IdentityDatabase(environment: environment)
|
self.identityDatabase = try IdentityDatabase(inMemory: environment.inMemoryContent,
|
||||||
|
fixture: environment.identityFixture)
|
||||||
self.environment = environment
|
self.environment = environment
|
||||||
|
|
||||||
mostRecentlyUsedIdentityID = identityDatabase.mostRecentlyUsedIdentityIDObservation()
|
mostRecentlyUsedIdentityID = identityDatabase.mostRecentlyUsedIdentityIDObservation()
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
// Copyright © 2020 Metabolist. All rights reserved.
|
// Copyright © 2020 Metabolist. All rights reserved.
|
||||||
|
|
||||||
|
import DB
|
||||||
import Foundation
|
import Foundation
|
||||||
import HTTP
|
import HTTP
|
||||||
import Mastodon
|
import Mastodon
|
||||||
|
@ -32,20 +33,6 @@ public struct AppEnvironment {
|
||||||
}
|
}
|
||||||
|
|
||||||
public extension AppEnvironment {
|
public extension AppEnvironment {
|
||||||
struct IdentityFixture {
|
|
||||||
public let id: UUID
|
|
||||||
public let instanceURL: URL
|
|
||||||
public let instance: Instance?
|
|
||||||
public let account: Account?
|
|
||||||
|
|
||||||
public init(id: UUID, instanceURL: URL, instance: Instance?, account: Account?) {
|
|
||||||
self.id = id
|
|
||||||
self.instanceURL = instanceURL
|
|
||||||
self.instance = instance
|
|
||||||
self.account = account
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
static func live(userNotificationCenter: UNUserNotificationCenter) -> Self {
|
static func live(userNotificationCenter: UNUserNotificationCenter) -> Self {
|
||||||
Self(
|
Self(
|
||||||
session: Session(configuration: .default),
|
session: Session(configuration: .default),
|
||||||
|
|
|
@ -1,71 +1,5 @@
|
||||||
// Copyright © 2020 Metabolist. All rights reserved.
|
// Copyright © 2020 Metabolist. All rights reserved.
|
||||||
|
|
||||||
import Foundation
|
import DB
|
||||||
import Mastodon
|
|
||||||
|
|
||||||
public struct Identity: Codable, Hashable, Identifiable {
|
public typealias Identity = DB.Identity
|
||||||
public let id: UUID
|
|
||||||
public let url: URL
|
|
||||||
public let lastUsedAt: Date
|
|
||||||
public let preferences: Identity.Preferences
|
|
||||||
public let instance: Identity.Instance?
|
|
||||||
public let account: Identity.Account?
|
|
||||||
public let lastRegisteredDeviceToken: String?
|
|
||||||
public let pushSubscriptionAlerts: PushSubscription.Alerts
|
|
||||||
}
|
|
||||||
|
|
||||||
public extension Identity {
|
|
||||||
struct Instance: Codable, Hashable {
|
|
||||||
public let uri: String
|
|
||||||
public let streamingAPI: URL
|
|
||||||
public let title: String
|
|
||||||
public let thumbnail: URL?
|
|
||||||
}
|
|
||||||
|
|
||||||
struct Account: Codable, Hashable {
|
|
||||||
public let id: String
|
|
||||||
public let identityID: UUID
|
|
||||||
public let username: String
|
|
||||||
public let displayName: String
|
|
||||||
public let url: URL
|
|
||||||
public let avatar: URL
|
|
||||||
public let avatarStatic: URL
|
|
||||||
public let header: URL
|
|
||||||
public let headerStatic: URL
|
|
||||||
public let emojis: [Emoji]
|
|
||||||
}
|
|
||||||
|
|
||||||
struct Preferences: Codable, Hashable {
|
|
||||||
@DecodableDefault.True public var useServerPostingReadingPreferences
|
|
||||||
@DecodableDefault.StatusVisibilityPublic public var postingDefaultVisibility: Status.Visibility
|
|
||||||
@DecodableDefault.False public var postingDefaultSensitive
|
|
||||||
public var postingDefaultLanguage: String?
|
|
||||||
@DecodableDefault.ExpandMediaDefault public var readingExpandMedia: Mastodon.Preferences.ExpandMedia
|
|
||||||
@DecodableDefault.False public var readingExpandSpoilers
|
|
||||||
}
|
|
||||||
|
|
||||||
var handle: String {
|
|
||||||
if let account = account, let host = account.url.host {
|
|
||||||
return account.url.lastPathComponent + "@" + host
|
|
||||||
}
|
|
||||||
|
|
||||||
return instance?.title ?? url.host ?? url.absoluteString
|
|
||||||
}
|
|
||||||
|
|
||||||
var image: URL? { account?.avatar ?? instance?.thumbnail }
|
|
||||||
}
|
|
||||||
|
|
||||||
public extension Identity.Preferences {
|
|
||||||
func updated(from serverPreferences: Preferences) -> Self {
|
|
||||||
var mutable = self
|
|
||||||
|
|
||||||
if useServerPostingReadingPreferences {
|
|
||||||
mutable.postingDefaultVisibility = serverPreferences.postingDefaultVisibility
|
|
||||||
mutable.postingDefaultSensitive = serverPreferences.postingDefaultSensitive
|
|
||||||
mutable.readingExpandMedia = serverPreferences.readingExpandMedia
|
|
||||||
mutable.readingExpandSpoilers = serverPreferences.readingExpandSpoilers
|
|
||||||
}
|
|
||||||
|
|
||||||
return mutable
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
// Copyright © 2020 Metabolist. All rights reserved.
|
// Copyright © 2020 Metabolist. All rights reserved.
|
||||||
|
|
||||||
|
import DB
|
||||||
import Foundation
|
import Foundation
|
||||||
import Combine
|
import Combine
|
||||||
import Mastodon
|
import Mastodon
|
||||||
|
@ -39,7 +40,7 @@ public class IdentityService {
|
||||||
networkClient.instanceURL = identity.url
|
networkClient.instanceURL = identity.url
|
||||||
networkClient.accessToken = try? secretsService.item(.accessToken)
|
networkClient.accessToken = try? secretsService.item(.accessToken)
|
||||||
|
|
||||||
contentDatabase = try ContentDatabase(identityID: identityID, environment: environment)
|
contentDatabase = try ContentDatabase(identityID: identityID, inMemory: environment.inMemoryContent)
|
||||||
|
|
||||||
observation.catch { [weak self] error -> Empty<Identity, Never> in
|
observation.catch { [weak self] error -> Empty<Identity, Never> in
|
||||||
self?.observationErrorsInput.send(error)
|
self?.observationErrorsInput.send(error)
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
// Copyright © 2020 Metabolist. All rights reserved.
|
// Copyright © 2020 Metabolist. All rights reserved.
|
||||||
|
|
||||||
import Combine
|
import Combine
|
||||||
|
import DB
|
||||||
import Foundation
|
import Foundation
|
||||||
import Mastodon
|
import Mastodon
|
||||||
|
|
||||||
|
|
|
@ -2,6 +2,7 @@
|
||||||
|
|
||||||
import Foundation
|
import Foundation
|
||||||
import Combine
|
import Combine
|
||||||
|
import DB
|
||||||
import Mastodon
|
import Mastodon
|
||||||
|
|
||||||
public struct StatusService {
|
public struct StatusService {
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
// Copyright © 2020 Metabolist. All rights reserved.
|
// Copyright © 2020 Metabolist. All rights reserved.
|
||||||
|
|
||||||
|
import DB
|
||||||
import Foundation
|
import Foundation
|
||||||
import HTTP
|
import HTTP
|
||||||
import ServiceLayer
|
import ServiceLayer
|
||||||
|
|
Loading…
Reference in a new issue