mirror of
https://github.com/metabolist/metatext.git
synced 2025-02-17 14:35:13 +00:00
The great id cleanup
This commit is contained in:
parent
9ac6ed2d93
commit
15d6e10edc
63 changed files with 345 additions and 309 deletions
|
@ -1,5 +1,6 @@
|
||||||
disabled_rules:
|
disabled_rules:
|
||||||
- identifier_name
|
- identifier_name
|
||||||
|
- type_name
|
||||||
# Swift 5.3
|
# Swift 5.3
|
||||||
- multiple_closures_with_trailing_closure
|
- multiple_closures_with_trailing_closure
|
||||||
- no_space_in_method_call
|
- no_space_in_method_call
|
||||||
|
|
|
@ -4,13 +4,17 @@ import Foundation
|
||||||
import GRDB
|
import GRDB
|
||||||
|
|
||||||
public struct AccountList: Codable, FetchableRecord, PersistableRecord {
|
public struct AccountList: Codable, FetchableRecord, PersistableRecord {
|
||||||
let id: UUID
|
let id: Id
|
||||||
|
|
||||||
public init() {
|
public init() {
|
||||||
id = UUID()
|
id = Id()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public extension AccountList {
|
||||||
|
typealias Id = UUID
|
||||||
|
}
|
||||||
|
|
||||||
extension AccountList {
|
extension AccountList {
|
||||||
static let joins = hasMany(AccountListJoin.self).order(AccountListJoin.Columns.index)
|
static let joins = hasMany(AccountListJoin.self).order(AccountListJoin.Columns.index)
|
||||||
static let accounts = hasMany(
|
static let accounts = hasMany(
|
||||||
|
|
|
@ -2,10 +2,11 @@
|
||||||
|
|
||||||
import Foundation
|
import Foundation
|
||||||
import GRDB
|
import GRDB
|
||||||
|
import Mastodon
|
||||||
|
|
||||||
struct AccountListJoin: Codable, FetchableRecord, PersistableRecord {
|
struct AccountListJoin: Codable, FetchableRecord, PersistableRecord {
|
||||||
let accountId: String
|
let accountId: Account.Id
|
||||||
let listId: UUID
|
let listId: AccountList.Id
|
||||||
let index: Int
|
let index: Int
|
||||||
|
|
||||||
static let account = belongsTo(AccountRecord.self)
|
static let account = belongsTo(AccountRecord.self)
|
||||||
|
|
|
@ -2,10 +2,11 @@
|
||||||
|
|
||||||
import Foundation
|
import Foundation
|
||||||
import GRDB
|
import GRDB
|
||||||
|
import Mastodon
|
||||||
|
|
||||||
struct AccountPinnedStatusJoin: Codable, FetchableRecord, PersistableRecord {
|
struct AccountPinnedStatusJoin: Codable, FetchableRecord, PersistableRecord {
|
||||||
let accountId: String
|
let accountId: Account.Id
|
||||||
let statusId: String
|
let statusId: Status.Id
|
||||||
let index: Int
|
let index: Int
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -5,7 +5,7 @@ import GRDB
|
||||||
import Mastodon
|
import Mastodon
|
||||||
|
|
||||||
struct AccountRecord: Codable, Hashable {
|
struct AccountRecord: Codable, Hashable {
|
||||||
let id: String
|
let id: Account.Id
|
||||||
let username: String
|
let username: String
|
||||||
let acct: String
|
let acct: String
|
||||||
let displayName: String
|
let displayName: String
|
||||||
|
@ -24,7 +24,7 @@ struct AccountRecord: Codable, Hashable {
|
||||||
let emojis: [Emoji]
|
let emojis: [Emoji]
|
||||||
let bot: Bool
|
let bot: Bool
|
||||||
let discoverable: Bool
|
let discoverable: Bool
|
||||||
let movedId: String?
|
let movedId: Account.Id?
|
||||||
}
|
}
|
||||||
|
|
||||||
extension AccountRecord {
|
extension AccountRecord {
|
||||||
|
|
|
@ -12,15 +12,15 @@ public struct ContentDatabase {
|
||||||
|
|
||||||
private let databaseWriter: DatabaseWriter
|
private let databaseWriter: DatabaseWriter
|
||||||
|
|
||||||
public init(identityID: UUID, inMemory: Bool, keychain: Keychain.Type) throws {
|
public init(id: Identity.Id, inMemory: Bool, keychain: Keychain.Type) throws {
|
||||||
if inMemory {
|
if inMemory {
|
||||||
databaseWriter = DatabaseQueue()
|
databaseWriter = DatabaseQueue()
|
||||||
} else {
|
} else {
|
||||||
let path = try Self.fileURL(identityID: identityID).path
|
let path = try Self.fileURL(id: id).path
|
||||||
var configuration = Configuration()
|
var configuration = Configuration()
|
||||||
|
|
||||||
configuration.prepareDatabase {
|
configuration.prepareDatabase {
|
||||||
try $0.usePassphrase(Secrets.databaseKey(identityID: identityID, keychain: keychain))
|
try $0.usePassphrase(Secrets.databaseKey(identityId: id, keychain: keychain))
|
||||||
}
|
}
|
||||||
|
|
||||||
databaseWriter = try DatabasePool(path: path, configuration: configuration)
|
databaseWriter = try DatabasePool(path: path, configuration: configuration)
|
||||||
|
@ -39,8 +39,8 @@ public struct ContentDatabase {
|
||||||
}
|
}
|
||||||
|
|
||||||
public extension ContentDatabase {
|
public extension ContentDatabase {
|
||||||
static func delete(forIdentityID identityID: UUID) throws {
|
static func delete(id: Identity.Id) throws {
|
||||||
try FileManager.default.removeItem(at: fileURL(identityID: identityID))
|
try FileManager.default.removeItem(at: fileURL(id: id))
|
||||||
}
|
}
|
||||||
|
|
||||||
func insert(status: Status) -> AnyPublisher<Never, Error> {
|
func insert(status: Status) -> AnyPublisher<Never, Error> {
|
||||||
|
@ -58,7 +58,7 @@ public extension ContentDatabase {
|
||||||
|
|
||||||
try timelineRecord.save($0)
|
try timelineRecord.save($0)
|
||||||
|
|
||||||
let maxIDPresent = try String.fetchOne($0, timelineRecord.statuses.select(max(StatusRecord.Columns.id)))
|
let maxIdPresent = try String.fetchOne($0, timelineRecord.statuses.select(max(StatusRecord.Columns.id)))
|
||||||
|
|
||||||
for status in statuses {
|
for status in statuses {
|
||||||
try status.save($0)
|
try status.save($0)
|
||||||
|
@ -66,13 +66,13 @@ public extension ContentDatabase {
|
||||||
try TimelineStatusJoin(timelineId: timeline.id, statusId: status.id).save($0)
|
try TimelineStatusJoin(timelineId: timeline.id, statusId: status.id).save($0)
|
||||||
}
|
}
|
||||||
|
|
||||||
if let maxIDPresent = maxIDPresent,
|
if let maxIdPresent = maxIdPresent,
|
||||||
let minIDInserted = statuses.map(\.id).min(),
|
let minIdInserted = statuses.map(\.id).min(),
|
||||||
minIDInserted > maxIDPresent {
|
minIdInserted > maxIdPresent {
|
||||||
try LoadMoreRecord(
|
try LoadMoreRecord(
|
||||||
timelineId: timeline.id,
|
timelineId: timeline.id,
|
||||||
afterStatusId: minIDInserted,
|
afterStatusId: minIdInserted,
|
||||||
beforeStatusId: maxIDPresent)
|
beforeStatusId: maxIdPresent)
|
||||||
.save($0)
|
.save($0)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -86,18 +86,18 @@ public extension ContentDatabase {
|
||||||
|
|
||||||
switch direction {
|
switch direction {
|
||||||
case .up:
|
case .up:
|
||||||
if let maxIDInserted = statuses.map(\.id).max(), maxIDInserted < loadMore.afterStatusId {
|
if let maxIdInserted = statuses.map(\.id).max(), maxIdInserted < loadMore.afterStatusId {
|
||||||
try LoadMoreRecord(
|
try LoadMoreRecord(
|
||||||
timelineId: loadMore.timeline.id,
|
timelineId: loadMore.timeline.id,
|
||||||
afterStatusId: loadMore.afterStatusId,
|
afterStatusId: loadMore.afterStatusId,
|
||||||
beforeStatusId: maxIDInserted)
|
beforeStatusId: maxIdInserted)
|
||||||
.save($0)
|
.save($0)
|
||||||
}
|
}
|
||||||
case .down:
|
case .down:
|
||||||
if let minIDInserted = statuses.map(\.id).min(), minIDInserted > loadMore.beforeStatusId {
|
if let minIdInserted = statuses.map(\.id).min(), minIdInserted > loadMore.beforeStatusId {
|
||||||
try LoadMoreRecord(
|
try LoadMoreRecord(
|
||||||
timelineId: loadMore.timeline.id,
|
timelineId: loadMore.timeline.id,
|
||||||
afterStatusId: minIDInserted,
|
afterStatusId: minIdInserted,
|
||||||
beforeStatusId: loadMore.beforeStatusId)
|
beforeStatusId: loadMore.beforeStatusId)
|
||||||
.save($0)
|
.save($0)
|
||||||
}
|
}
|
||||||
|
@ -107,25 +107,25 @@ public extension ContentDatabase {
|
||||||
.eraseToAnyPublisher()
|
.eraseToAnyPublisher()
|
||||||
}
|
}
|
||||||
|
|
||||||
func insert(context: Context, parentID: String) -> AnyPublisher<Never, Error> {
|
func insert(context: Context, parentId: Status.Id) -> AnyPublisher<Never, Error> {
|
||||||
databaseWriter.writePublisher {
|
databaseWriter.writePublisher {
|
||||||
for (index, status) in context.ancestors.enumerated() {
|
for (index, status) in context.ancestors.enumerated() {
|
||||||
try status.save($0)
|
try status.save($0)
|
||||||
try StatusAncestorJoin(parentId: parentID, statusId: status.id, index: index).save($0)
|
try StatusAncestorJoin(parentId: parentId, statusId: status.id, index: index).save($0)
|
||||||
}
|
}
|
||||||
|
|
||||||
for (index, status) in context.descendants.enumerated() {
|
for (index, status) in context.descendants.enumerated() {
|
||||||
try status.save($0)
|
try status.save($0)
|
||||||
try StatusDescendantJoin(parentId: parentID, statusId: status.id, index: index).save($0)
|
try StatusDescendantJoin(parentId: parentId, statusId: status.id, index: index).save($0)
|
||||||
}
|
}
|
||||||
|
|
||||||
try StatusAncestorJoin.filter(
|
try StatusAncestorJoin.filter(
|
||||||
StatusAncestorJoin.Columns.parentId == parentID
|
StatusAncestorJoin.Columns.parentId == parentId
|
||||||
&& !context.ancestors.map(\.id).contains(StatusAncestorJoin.Columns.statusId))
|
&& !context.ancestors.map(\.id).contains(StatusAncestorJoin.Columns.statusId))
|
||||||
.deleteAll($0)
|
.deleteAll($0)
|
||||||
|
|
||||||
try StatusDescendantJoin.filter(
|
try StatusDescendantJoin.filter(
|
||||||
StatusDescendantJoin.Columns.parentId == parentID
|
StatusDescendantJoin.Columns.parentId == parentId
|
||||||
&& !context.descendants.map(\.id).contains(StatusDescendantJoin.Columns.statusId))
|
&& !context.descendants.map(\.id).contains(StatusDescendantJoin.Columns.statusId))
|
||||||
.deleteAll($0)
|
.deleteAll($0)
|
||||||
}
|
}
|
||||||
|
@ -133,15 +133,15 @@ public extension ContentDatabase {
|
||||||
.eraseToAnyPublisher()
|
.eraseToAnyPublisher()
|
||||||
}
|
}
|
||||||
|
|
||||||
func insert(pinnedStatuses: [Status], accountID: String) -> AnyPublisher<Never, Error> {
|
func insert(pinnedStatuses: [Status], accountId: Account.Id) -> AnyPublisher<Never, Error> {
|
||||||
databaseWriter.writePublisher {
|
databaseWriter.writePublisher {
|
||||||
for (index, status) in pinnedStatuses.enumerated() {
|
for (index, status) in pinnedStatuses.enumerated() {
|
||||||
try status.save($0)
|
try status.save($0)
|
||||||
try AccountPinnedStatusJoin(accountId: accountID, statusId: status.id, index: index).save($0)
|
try AccountPinnedStatusJoin(accountId: accountId, statusId: status.id, index: index).save($0)
|
||||||
}
|
}
|
||||||
|
|
||||||
try AccountPinnedStatusJoin.filter(
|
try AccountPinnedStatusJoin.filter(
|
||||||
AccountPinnedStatusJoin.Columns.accountId == accountID
|
AccountPinnedStatusJoin.Columns.accountId == accountId
|
||||||
&& !pinnedStatuses.map(\.id).contains(AccountPinnedStatusJoin.Columns.statusId))
|
&& !pinnedStatuses.map(\.id).contains(AccountPinnedStatusJoin.Columns.statusId))
|
||||||
.deleteAll($0)
|
.deleteAll($0)
|
||||||
}
|
}
|
||||||
|
@ -185,7 +185,7 @@ public extension ContentDatabase {
|
||||||
.eraseToAnyPublisher()
|
.eraseToAnyPublisher()
|
||||||
}
|
}
|
||||||
|
|
||||||
func deleteList(id: String) -> AnyPublisher<Never, Error> {
|
func deleteList(id: List.Id) -> AnyPublisher<Never, Error> {
|
||||||
databaseWriter.writePublisher(updates: TimelineRecord.filter(TimelineRecord.Columns.listId == id).deleteAll)
|
databaseWriter.writePublisher(updates: TimelineRecord.filter(TimelineRecord.Columns.listId == id).deleteAll)
|
||||||
.ignoreOutput()
|
.ignoreOutput()
|
||||||
.eraseToAnyPublisher()
|
.eraseToAnyPublisher()
|
||||||
|
@ -209,7 +209,7 @@ public extension ContentDatabase {
|
||||||
.eraseToAnyPublisher()
|
.eraseToAnyPublisher()
|
||||||
}
|
}
|
||||||
|
|
||||||
func deleteFilter(id: String) -> AnyPublisher<Never, Error> {
|
func deleteFilter(id: Filter.Id) -> AnyPublisher<Never, Error> {
|
||||||
databaseWriter.writePublisher(updates: Filter.filter(Filter.Columns.id == id).deleteAll)
|
databaseWriter.writePublisher(updates: Filter.filter(Filter.Columns.id == id).deleteAll)
|
||||||
.ignoreOutput()
|
.ignoreOutput()
|
||||||
.eraseToAnyPublisher()
|
.eraseToAnyPublisher()
|
||||||
|
@ -225,9 +225,9 @@ public extension ContentDatabase {
|
||||||
.eraseToAnyPublisher()
|
.eraseToAnyPublisher()
|
||||||
}
|
}
|
||||||
|
|
||||||
func contextObservation(parentID: String) -> AnyPublisher<[[CollectionItem]], Error> {
|
func contextObservation(id: Status.Id) -> AnyPublisher<[[CollectionItem]], Error> {
|
||||||
ValueObservation.tracking(
|
ValueObservation.tracking(
|
||||||
ContextItemsInfo.request(StatusRecord.filter(StatusRecord.Columns.id == parentID)).fetchOne)
|
ContextItemsInfo.request(StatusRecord.filter(StatusRecord.Columns.id == id)).fetchOne)
|
||||||
.removeDuplicates()
|
.removeDuplicates()
|
||||||
.publisher(in: databaseWriter)
|
.publisher(in: databaseWriter)
|
||||||
.combineLatest(activeFiltersPublisher)
|
.combineLatest(activeFiltersPublisher)
|
||||||
|
@ -252,7 +252,7 @@ public extension ContentDatabase {
|
||||||
.eraseToAnyPublisher()
|
.eraseToAnyPublisher()
|
||||||
}
|
}
|
||||||
|
|
||||||
func accountObservation(id: String) -> AnyPublisher<Account, Error> {
|
func accountObservation(id: Account.Id) -> AnyPublisher<Account, Error> {
|
||||||
ValueObservation.tracking(AccountInfo.request(AccountRecord.filter(AccountRecord.Columns.id == id)).fetchOne)
|
ValueObservation.tracking(AccountInfo.request(AccountRecord.filter(AccountRecord.Columns.id == id)).fetchOne)
|
||||||
.removeDuplicates()
|
.removeDuplicates()
|
||||||
.publisher(in: databaseWriter)
|
.publisher(in: databaseWriter)
|
||||||
|
@ -271,8 +271,8 @@ public extension ContentDatabase {
|
||||||
}
|
}
|
||||||
|
|
||||||
private extension ContentDatabase {
|
private extension ContentDatabase {
|
||||||
static func fileURL(identityID: UUID) throws -> URL {
|
static func fileURL(id: Identity.Id) throws -> URL {
|
||||||
try FileManager.default.databaseDirectoryURL(name: identityID.uuidString)
|
try FileManager.default.databaseDirectoryURL(name: id.uuidString)
|
||||||
}
|
}
|
||||||
|
|
||||||
static func clean(_ databaseWriter: DatabaseWriter) throws {
|
static func clean(_ databaseWriter: DatabaseWriter) throws {
|
||||||
|
|
|
@ -5,9 +5,9 @@ import GRDB
|
||||||
import Mastodon
|
import Mastodon
|
||||||
|
|
||||||
struct LoadMoreRecord: Codable, Hashable {
|
struct LoadMoreRecord: Codable, Hashable {
|
||||||
let timelineId: String
|
let timelineId: Timeline.Id
|
||||||
let afterStatusId: String
|
let afterStatusId: Status.Id
|
||||||
let beforeStatusId: String
|
let beforeStatusId: Status.Id
|
||||||
}
|
}
|
||||||
|
|
||||||
extension LoadMoreRecord {
|
extension LoadMoreRecord {
|
||||||
|
|
|
@ -2,10 +2,11 @@
|
||||||
|
|
||||||
import Foundation
|
import Foundation
|
||||||
import GRDB
|
import GRDB
|
||||||
|
import Mastodon
|
||||||
|
|
||||||
struct StatusAncestorJoin: Codable, FetchableRecord, PersistableRecord {
|
struct StatusAncestorJoin: Codable, FetchableRecord, PersistableRecord {
|
||||||
let parentId: String
|
let parentId: Status.Id
|
||||||
let statusId: String
|
let statusId: Status.Id
|
||||||
let index: Int
|
let index: Int
|
||||||
|
|
||||||
static let status = belongsTo(StatusRecord.self, using: ForeignKey([Columns.statusId]))
|
static let status = belongsTo(StatusRecord.self, using: ForeignKey([Columns.statusId]))
|
||||||
|
|
|
@ -2,10 +2,11 @@
|
||||||
|
|
||||||
import Foundation
|
import Foundation
|
||||||
import GRDB
|
import GRDB
|
||||||
|
import Mastodon
|
||||||
|
|
||||||
struct StatusDescendantJoin: Codable, FetchableRecord, PersistableRecord {
|
struct StatusDescendantJoin: Codable, FetchableRecord, PersistableRecord {
|
||||||
let parentId: String
|
let parentId: Status.Id
|
||||||
let statusId: String
|
let statusId: Status.Id
|
||||||
let index: Int
|
let index: Int
|
||||||
|
|
||||||
static let status = belongsTo(StatusRecord.self, using: ForeignKey([Columns.statusId]))
|
static let status = belongsTo(StatusRecord.self, using: ForeignKey([Columns.statusId]))
|
||||||
|
|
|
@ -5,10 +5,10 @@ import GRDB
|
||||||
import Mastodon
|
import Mastodon
|
||||||
|
|
||||||
struct StatusRecord: Codable, Hashable {
|
struct StatusRecord: Codable, Hashable {
|
||||||
let id: String
|
let id: Status.Id
|
||||||
let uri: String
|
let uri: String
|
||||||
let createdAt: Date
|
let createdAt: Date
|
||||||
let accountId: String
|
let accountId: Account.Id
|
||||||
let content: HTML
|
let content: HTML
|
||||||
let visibility: Status.Visibility
|
let visibility: Status.Visibility
|
||||||
let sensitive: Bool
|
let sensitive: Bool
|
||||||
|
@ -22,9 +22,9 @@ struct StatusRecord: Codable, Hashable {
|
||||||
let repliesCount: Int
|
let repliesCount: Int
|
||||||
let application: Application?
|
let application: Application?
|
||||||
let url: URL?
|
let url: URL?
|
||||||
let inReplyToId: String?
|
let inReplyToId: Status.Id?
|
||||||
let inReplyToAccountId: String?
|
let inReplyToAccountId: Account.Id?
|
||||||
let reblogId: String?
|
let reblogId: Status.Id?
|
||||||
let poll: Poll?
|
let poll: Poll?
|
||||||
let card: Card?
|
let card: Card?
|
||||||
let language: String?
|
let language: String?
|
||||||
|
|
|
@ -5,11 +5,11 @@ import GRDB
|
||||||
import Mastodon
|
import Mastodon
|
||||||
|
|
||||||
struct TimelineRecord: Codable, Hashable {
|
struct TimelineRecord: Codable, Hashable {
|
||||||
let id: String
|
let id: Timeline.Id
|
||||||
let listId: String?
|
let listId: List.Id?
|
||||||
let listTitle: String?
|
let listTitle: String?
|
||||||
let tag: String?
|
let tag: String?
|
||||||
let accountId: String?
|
let accountId: Account.Id?
|
||||||
let profileCollection: ProfileCollection?
|
let profileCollection: ProfileCollection?
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -2,10 +2,11 @@
|
||||||
|
|
||||||
import Foundation
|
import Foundation
|
||||||
import GRDB
|
import GRDB
|
||||||
|
import Mastodon
|
||||||
|
|
||||||
struct TimelineStatusJoin: Codable, FetchableRecord, PersistableRecord {
|
struct TimelineStatusJoin: Codable, FetchableRecord, PersistableRecord {
|
||||||
let timelineId: String
|
let timelineId: Timeline.Id
|
||||||
let statusId: String
|
let statusId: Status.Id
|
||||||
|
|
||||||
static let status = belongsTo(StatusRecord.self)
|
static let status = belongsTo(StatusRecord.self)
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,7 +4,7 @@ import Foundation
|
||||||
import Mastodon
|
import Mastodon
|
||||||
|
|
||||||
public struct Identity: Codable, Hashable, Identifiable {
|
public struct Identity: Codable, Hashable, Identifiable {
|
||||||
public let id: UUID
|
public let id: Id
|
||||||
public let url: URL
|
public let url: URL
|
||||||
public let authenticated: Bool
|
public let authenticated: Bool
|
||||||
public let pending: Bool
|
public let pending: Bool
|
||||||
|
@ -17,6 +17,8 @@ public struct Identity: Codable, Hashable, Identifiable {
|
||||||
}
|
}
|
||||||
|
|
||||||
public extension Identity {
|
public extension Identity {
|
||||||
|
typealias Id = UUID
|
||||||
|
|
||||||
struct Instance: Codable, Hashable {
|
struct Instance: Codable, Hashable {
|
||||||
public let uri: String
|
public let uri: String
|
||||||
public let streamingAPI: URL
|
public let streamingAPI: URL
|
||||||
|
@ -25,8 +27,8 @@ public extension Identity {
|
||||||
}
|
}
|
||||||
|
|
||||||
struct Account: Codable, Hashable {
|
struct Account: Codable, Hashable {
|
||||||
public let id: String
|
public let id: Mastodon.Account.Id
|
||||||
public let identityID: UUID
|
public let identityId: Identity.Id
|
||||||
public let username: String
|
public let username: String
|
||||||
public let displayName: String
|
public let displayName: String
|
||||||
public let url: URL
|
public let url: URL
|
||||||
|
|
|
@ -4,12 +4,12 @@ import Foundation
|
||||||
import Mastodon
|
import Mastodon
|
||||||
|
|
||||||
public struct IdentityFixture {
|
public struct IdentityFixture {
|
||||||
public let id: UUID
|
public let id: Identity.Id
|
||||||
public let instanceURL: URL
|
public let instanceURL: URL
|
||||||
public let instance: Instance?
|
public let instance: Instance?
|
||||||
public let account: Account?
|
public let account: Account?
|
||||||
|
|
||||||
public init(id: UUID, instanceURL: URL, instance: Instance?, account: Account?) {
|
public init(id: Identity.Id, instanceURL: URL, instance: Instance?, account: Account?) {
|
||||||
self.id = id
|
self.id = id
|
||||||
self.instanceURL = instanceURL
|
self.instanceURL = instanceURL
|
||||||
self.instance = instance
|
self.instance = instance
|
||||||
|
|
|
@ -5,8 +5,8 @@ import Mastodon
|
||||||
|
|
||||||
public struct LoadMore: Hashable {
|
public struct LoadMore: Hashable {
|
||||||
public let timeline: Timeline
|
public let timeline: Timeline
|
||||||
public let afterStatusId: String
|
public let afterStatusId: Status.Id
|
||||||
public let beforeStatusId: String
|
public let beforeStatusId: Status.Id
|
||||||
}
|
}
|
||||||
|
|
||||||
public extension LoadMore {
|
public extension LoadMore {
|
||||||
|
|
|
@ -9,10 +9,12 @@ public enum Timeline: Hashable {
|
||||||
case federated
|
case federated
|
||||||
case list(List)
|
case list(List)
|
||||||
case tag(String)
|
case tag(String)
|
||||||
case profile(accountId: String, profileCollection: ProfileCollection)
|
case profile(accountId: Account.Id, profileCollection: ProfileCollection)
|
||||||
}
|
}
|
||||||
|
|
||||||
public extension Timeline {
|
public extension Timeline {
|
||||||
|
typealias Id = String
|
||||||
|
|
||||||
static let unauthenticatedDefaults: [Timeline] = [.local, .federated]
|
static let unauthenticatedDefaults: [Timeline] = [.local, .federated]
|
||||||
static let authenticatedDefaults: [Timeline] = [.home, .local, .federated]
|
static let authenticatedDefaults: [Timeline] = [.home, .local, .federated]
|
||||||
|
|
||||||
|
@ -29,7 +31,7 @@ public extension Timeline {
|
||||||
}
|
}
|
||||||
|
|
||||||
extension Timeline: Identifiable {
|
extension Timeline: Identifiable {
|
||||||
public var id: String {
|
public var id: Id {
|
||||||
switch self {
|
switch self {
|
||||||
case .home:
|
case .home:
|
||||||
return "home"
|
return "home"
|
||||||
|
|
|
@ -28,7 +28,7 @@ extension IdentityDatabase {
|
||||||
|
|
||||||
try db.create(table: "account", ifNotExists: true) { t in
|
try db.create(table: "account", ifNotExists: true) { t in
|
||||||
t.column("id", .text).primaryKey(onConflict: .replace)
|
t.column("id", .text).primaryKey(onConflict: .replace)
|
||||||
t.column("identityID", .text).notNull()
|
t.column("identityId", .text).notNull()
|
||||||
.references("identityRecord", onDelete: .cascade)
|
.references("identityRecord", onDelete: .cascade)
|
||||||
t.column("username", .text).notNull()
|
t.column("username", .text).notNull()
|
||||||
t.column("displayName", .text).notNull()
|
t.column("displayName", .text).notNull()
|
||||||
|
|
|
@ -22,7 +22,7 @@ public struct IdentityDatabase {
|
||||||
var configuration = Configuration()
|
var configuration = Configuration()
|
||||||
|
|
||||||
configuration.prepareDatabase {
|
configuration.prepareDatabase {
|
||||||
try $0.usePassphrase(Secrets.databaseKey(identityID: nil, keychain: keychain))
|
try $0.usePassphrase(Secrets.databaseKey(identityId: nil, keychain: keychain))
|
||||||
}
|
}
|
||||||
|
|
||||||
databaseWriter = try DatabasePool(path: path, configuration: configuration)
|
databaseWriter = try DatabasePool(path: path, configuration: configuration)
|
||||||
|
@ -33,7 +33,7 @@ public struct IdentityDatabase {
|
||||||
}
|
}
|
||||||
|
|
||||||
public extension IdentityDatabase {
|
public extension IdentityDatabase {
|
||||||
func createIdentity(id: UUID, url: URL, authenticated: Bool, pending: Bool) -> AnyPublisher<Never, Error> {
|
func createIdentity(id: Identity.Id, url: URL, authenticated: Bool, pending: Bool) -> AnyPublisher<Never, Error> {
|
||||||
databaseWriter.writePublisher(
|
databaseWriter.writePublisher(
|
||||||
updates: IdentityRecord(
|
updates: IdentityRecord(
|
||||||
id: id,
|
id: id,
|
||||||
|
@ -50,23 +50,23 @@ public extension IdentityDatabase {
|
||||||
.eraseToAnyPublisher()
|
.eraseToAnyPublisher()
|
||||||
}
|
}
|
||||||
|
|
||||||
func deleteIdentity(id: UUID) -> AnyPublisher<Never, Error> {
|
func deleteIdentity(id: Identity.Id) -> AnyPublisher<Never, Error> {
|
||||||
databaseWriter.writePublisher(updates: IdentityRecord.filter(IdentityRecord.Columns.id == id).deleteAll)
|
databaseWriter.writePublisher(updates: IdentityRecord.filter(IdentityRecord.Columns.id == id).deleteAll)
|
||||||
.ignoreOutput()
|
.ignoreOutput()
|
||||||
.eraseToAnyPublisher()
|
.eraseToAnyPublisher()
|
||||||
}
|
}
|
||||||
|
|
||||||
func updateLastUsedAt(identityID: UUID) -> AnyPublisher<Never, Error> {
|
func updateLastUsedAt(id: Identity.Id) -> AnyPublisher<Never, Error> {
|
||||||
databaseWriter.writePublisher {
|
databaseWriter.writePublisher {
|
||||||
try IdentityRecord
|
try IdentityRecord
|
||||||
.filter(IdentityRecord.Columns.id == identityID)
|
.filter(IdentityRecord.Columns.id == id)
|
||||||
.updateAll($0, IdentityRecord.Columns.lastUsedAt.set(to: Date()))
|
.updateAll($0, IdentityRecord.Columns.lastUsedAt.set(to: Date()))
|
||||||
}
|
}
|
||||||
.ignoreOutput()
|
.ignoreOutput()
|
||||||
.eraseToAnyPublisher()
|
.eraseToAnyPublisher()
|
||||||
}
|
}
|
||||||
|
|
||||||
func updateInstance(_ instance: Instance, forIdentityID identityID: UUID) -> AnyPublisher<Never, Error> {
|
func updateInstance(_ instance: Instance, id: Identity.Id) -> AnyPublisher<Never, Error> {
|
||||||
databaseWriter.writePublisher {
|
databaseWriter.writePublisher {
|
||||||
try Identity.Instance(
|
try Identity.Instance(
|
||||||
uri: instance.uri,
|
uri: instance.uri,
|
||||||
|
@ -75,18 +75,18 @@ public extension IdentityDatabase {
|
||||||
thumbnail: instance.thumbnail)
|
thumbnail: instance.thumbnail)
|
||||||
.save($0)
|
.save($0)
|
||||||
try IdentityRecord
|
try IdentityRecord
|
||||||
.filter(IdentityRecord.Columns.id == identityID)
|
.filter(IdentityRecord.Columns.id == id)
|
||||||
.updateAll($0, IdentityRecord.Columns.instanceURI.set(to: instance.uri))
|
.updateAll($0, IdentityRecord.Columns.instanceURI.set(to: instance.uri))
|
||||||
}
|
}
|
||||||
.ignoreOutput()
|
.ignoreOutput()
|
||||||
.eraseToAnyPublisher()
|
.eraseToAnyPublisher()
|
||||||
}
|
}
|
||||||
|
|
||||||
func updateAccount(_ account: Account, forIdentityID identityID: UUID) -> AnyPublisher<Never, Error> {
|
func updateAccount(_ account: Account, id: Identity.Id) -> AnyPublisher<Never, Error> {
|
||||||
databaseWriter.writePublisher(
|
databaseWriter.writePublisher(
|
||||||
updates: Identity.Account(
|
updates: Identity.Account(
|
||||||
id: account.id,
|
id: account.id,
|
||||||
identityID: identityID,
|
identityId: id,
|
||||||
username: account.username,
|
username: account.username,
|
||||||
displayName: account.displayName,
|
displayName: account.displayName,
|
||||||
url: account.url,
|
url: account.url,
|
||||||
|
@ -100,7 +100,7 @@ public extension IdentityDatabase {
|
||||||
.eraseToAnyPublisher()
|
.eraseToAnyPublisher()
|
||||||
}
|
}
|
||||||
|
|
||||||
func confirmIdentity(id: UUID) -> AnyPublisher<Never, Error> {
|
func confirmIdentity(id: Identity.Id) -> AnyPublisher<Never, Error> {
|
||||||
databaseWriter.writePublisher {
|
databaseWriter.writePublisher {
|
||||||
try IdentityRecord
|
try IdentityRecord
|
||||||
.filter(IdentityRecord.Columns.id == id)
|
.filter(IdentityRecord.Columns.id == id)
|
||||||
|
@ -110,43 +110,41 @@ public extension IdentityDatabase {
|
||||||
.eraseToAnyPublisher()
|
.eraseToAnyPublisher()
|
||||||
}
|
}
|
||||||
|
|
||||||
func updatePreferences(_ preferences: Mastodon.Preferences,
|
func updatePreferences(_ preferences: Mastodon.Preferences, id: Identity.Id) -> AnyPublisher<Never, Error> {
|
||||||
forIdentityID identityID: UUID) -> AnyPublisher<Never, Error> {
|
|
||||||
databaseWriter.writePublisher {
|
databaseWriter.writePublisher {
|
||||||
guard let storedPreferences = try IdentityRecord.filter(IdentityRecord.Columns.id == identityID)
|
guard let storedPreferences = try IdentityRecord.filter(IdentityRecord.Columns.id == id)
|
||||||
.fetchOne($0)?
|
.fetchOne($0)?
|
||||||
.preferences else {
|
.preferences else {
|
||||||
throw IdentityDatabaseError.identityNotFound
|
throw IdentityDatabaseError.identityNotFound
|
||||||
}
|
}
|
||||||
|
|
||||||
try Self.writePreferences(storedPreferences.updated(from: preferences), id: identityID)($0)
|
try Self.writePreferences(storedPreferences.updated(from: preferences), id: id)($0)
|
||||||
}
|
}
|
||||||
.ignoreOutput()
|
.ignoreOutput()
|
||||||
.eraseToAnyPublisher()
|
.eraseToAnyPublisher()
|
||||||
}
|
}
|
||||||
|
|
||||||
func updatePreferences(_ preferences: Identity.Preferences,
|
func updatePreferences(_ preferences: Identity.Preferences, id: Identity.Id) -> AnyPublisher<Never, Error> {
|
||||||
forIdentityID identityID: UUID) -> AnyPublisher<Never, Error> {
|
databaseWriter.writePublisher(updates: Self.writePreferences(preferences, id: id))
|
||||||
databaseWriter.writePublisher(updates: Self.writePreferences(preferences, id: identityID))
|
|
||||||
.ignoreOutput()
|
.ignoreOutput()
|
||||||
.eraseToAnyPublisher()
|
.eraseToAnyPublisher()
|
||||||
}
|
}
|
||||||
|
|
||||||
func updatePushSubscription(alerts: PushSubscription.Alerts,
|
func updatePushSubscription(alerts: PushSubscription.Alerts,
|
||||||
deviceToken: Data? = nil,
|
deviceToken: Data? = nil,
|
||||||
forIdentityID identityID: UUID) -> AnyPublisher<Never, Error> {
|
id: Identity.Id) -> AnyPublisher<Never, Error> {
|
||||||
databaseWriter.writePublisher {
|
databaseWriter.writePublisher {
|
||||||
let data = try IdentityRecord.databaseJSONEncoder(
|
let data = try IdentityRecord.databaseJSONEncoder(
|
||||||
for: IdentityRecord.Columns.pushSubscriptionAlerts.name)
|
for: IdentityRecord.Columns.pushSubscriptionAlerts.name)
|
||||||
.encode(alerts)
|
.encode(alerts)
|
||||||
|
|
||||||
try IdentityRecord
|
try IdentityRecord
|
||||||
.filter(IdentityRecord.Columns.id == identityID)
|
.filter(IdentityRecord.Columns.id == id)
|
||||||
.updateAll($0, IdentityRecord.Columns.pushSubscriptionAlerts.set(to: data))
|
.updateAll($0, IdentityRecord.Columns.pushSubscriptionAlerts.set(to: data))
|
||||||
|
|
||||||
if let deviceToken = deviceToken {
|
if let deviceToken = deviceToken {
|
||||||
try IdentityRecord
|
try IdentityRecord
|
||||||
.filter(IdentityRecord.Columns.id == identityID)
|
.filter(IdentityRecord.Columns.id == id)
|
||||||
.updateAll($0, IdentityRecord.Columns.lastRegisteredDeviceToken.set(to: deviceToken))
|
.updateAll($0, IdentityRecord.Columns.lastRegisteredDeviceToken.set(to: deviceToken))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -154,7 +152,7 @@ public extension IdentityDatabase {
|
||||||
.eraseToAnyPublisher()
|
.eraseToAnyPublisher()
|
||||||
}
|
}
|
||||||
|
|
||||||
func identityObservation(id: UUID, immediate: Bool) -> AnyPublisher<Identity, Error> {
|
func identityObservation(id: Identity.Id, immediate: Bool) -> AnyPublisher<Identity, Error> {
|
||||||
ValueObservation.tracking(
|
ValueObservation.tracking(
|
||||||
IdentityInfo.request(IdentityRecord.filter(IdentityRecord.Columns.id == id)).fetchOne)
|
IdentityInfo.request(IdentityRecord.filter(IdentityRecord.Columns.id == id)).fetchOne)
|
||||||
.removeDuplicates()
|
.removeDuplicates()
|
||||||
|
@ -176,7 +174,7 @@ public extension IdentityDatabase {
|
||||||
.eraseToAnyPublisher()
|
.eraseToAnyPublisher()
|
||||||
}
|
}
|
||||||
|
|
||||||
func recentIdentitiesObservation(excluding: UUID) -> AnyPublisher<[Identity], Error> {
|
func recentIdentitiesObservation(excluding: Identity.Id) -> AnyPublisher<[Identity], Error> {
|
||||||
ValueObservation.tracking(
|
ValueObservation.tracking(
|
||||||
IdentityInfo.request(IdentityRecord.order(IdentityRecord.Columns.lastUsedAt.desc))
|
IdentityInfo.request(IdentityRecord.order(IdentityRecord.Columns.lastUsedAt.desc))
|
||||||
.filter(IdentityRecord.Columns.id != excluding)
|
.filter(IdentityRecord.Columns.id != excluding)
|
||||||
|
@ -188,7 +186,7 @@ public extension IdentityDatabase {
|
||||||
.eraseToAnyPublisher()
|
.eraseToAnyPublisher()
|
||||||
}
|
}
|
||||||
|
|
||||||
func immediateMostRecentlyUsedIdentityIDObservation() -> AnyPublisher<UUID?, Error> {
|
func immediateMostRecentlyUsedIdentityIdObservation() -> AnyPublisher<Identity.Id?, Error> {
|
||||||
ValueObservation.tracking(
|
ValueObservation.tracking(
|
||||||
IdentityRecord.select(IdentityRecord.Columns.id)
|
IdentityRecord.select(IdentityRecord.Columns.id)
|
||||||
.order(IdentityRecord.Columns.lastUsedAt.desc).fetchOne)
|
.order(IdentityRecord.Columns.lastUsedAt.desc).fetchOne)
|
||||||
|
@ -210,7 +208,7 @@ public extension IdentityDatabase {
|
||||||
private extension IdentityDatabase {
|
private extension IdentityDatabase {
|
||||||
static let name = "identity"
|
static let name = "identity"
|
||||||
|
|
||||||
static func writePreferences(_ preferences: Identity.Preferences, id: UUID) -> (Database) throws -> Void {
|
static func writePreferences(_ preferences: Identity.Preferences, id: Identity.Id) -> (Database) throws -> Void {
|
||||||
{
|
{
|
||||||
let data = try IdentityRecord.databaseJSONEncoder(
|
let data = try IdentityRecord.databaseJSONEncoder(
|
||||||
for: IdentityRecord.Columns.preferences.name).encode(preferences)
|
for: IdentityRecord.Columns.preferences.name).encode(preferences)
|
||||||
|
|
|
@ -5,7 +5,7 @@ import GRDB
|
||||||
import Mastodon
|
import Mastodon
|
||||||
|
|
||||||
struct IdentityRecord: Codable, Hashable, FetchableRecord, PersistableRecord {
|
struct IdentityRecord: Codable, Hashable, FetchableRecord, PersistableRecord {
|
||||||
let id: UUID
|
let id: Identity.Id
|
||||||
let url: URL
|
let url: URL
|
||||||
let authenticated: Bool
|
let authenticated: Bool
|
||||||
let pending: Bool
|
let pending: Bool
|
||||||
|
|
|
@ -2,7 +2,7 @@
|
||||||
|
|
||||||
import Foundation
|
import Foundation
|
||||||
|
|
||||||
public typealias HTTPStub = Result<(URLResponse, Data), Error>
|
public typealias HTTPStub = Result<(HTTPURLResponse, Data), Error>
|
||||||
|
|
||||||
public protocol Stubbing {
|
public protocol Stubbing {
|
||||||
func stub(url: URL) -> HTTPStub?
|
func stub(url: URL) -> HTTPStub?
|
||||||
|
|
|
@ -9,7 +9,7 @@ public final class Account: Codable, Identifiable {
|
||||||
public let verifiedAt: Date?
|
public let verifiedAt: Date?
|
||||||
}
|
}
|
||||||
|
|
||||||
public let id: String
|
public let id: Id
|
||||||
public let username: String
|
public let username: String
|
||||||
public let acct: String
|
public let acct: String
|
||||||
public let displayName: String
|
public let displayName: String
|
||||||
|
@ -30,7 +30,7 @@ public final class Account: Codable, Identifiable {
|
||||||
@DecodableDefault.False public private(set) var discoverable: Bool
|
@DecodableDefault.False public private(set) var discoverable: Bool
|
||||||
public var moved: Account?
|
public var moved: Account?
|
||||||
|
|
||||||
public init(id: String,
|
public init(id: Id,
|
||||||
username: String,
|
username: String,
|
||||||
acct: String,
|
acct: String,
|
||||||
displayName: String,
|
displayName: String,
|
||||||
|
@ -73,6 +73,10 @@ public final class Account: Codable, Identifiable {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public extension Account {
|
||||||
|
typealias Id = String
|
||||||
|
}
|
||||||
|
|
||||||
extension Account: Hashable {
|
extension Account: Hashable {
|
||||||
public static func == (lhs: Account, rhs: Account) -> Bool {
|
public static func == (lhs: Account, rhs: Account) -> Bool {
|
||||||
return lhs.id == rhs.id &&
|
return lhs.id == rhs.id &&
|
||||||
|
|
|
@ -3,7 +3,7 @@
|
||||||
import Foundation
|
import Foundation
|
||||||
|
|
||||||
public struct AppAuthorization: Codable {
|
public struct AppAuthorization: Codable {
|
||||||
public let id: String
|
public let id: Id
|
||||||
public let clientId: String
|
public let clientId: String
|
||||||
public let clientSecret: String
|
public let clientSecret: String
|
||||||
public let name: String
|
public let name: String
|
||||||
|
@ -11,3 +11,7 @@ public struct AppAuthorization: Codable {
|
||||||
public let website: String?
|
public let website: String?
|
||||||
public let vapidKey: String?
|
public let vapidKey: String?
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public extension AppAuthorization {
|
||||||
|
typealias Id = String
|
||||||
|
}
|
||||||
|
|
|
@ -32,7 +32,7 @@ public struct Attachment: Codable, Hashable {
|
||||||
}
|
}
|
||||||
// swiftlint:enable nesting
|
// swiftlint:enable nesting
|
||||||
|
|
||||||
public let id: String
|
public let id: Id
|
||||||
public let type: AttachmentType
|
public let type: AttachmentType
|
||||||
public let url: URL
|
public let url: URL
|
||||||
public let remoteUrl: URL?
|
public let remoteUrl: URL?
|
||||||
|
@ -41,3 +41,7 @@ public struct Attachment: Codable, Hashable {
|
||||||
public let meta: Meta?
|
public let meta: Meta?
|
||||||
public let description: String?
|
public let description: String?
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public extension Attachment {
|
||||||
|
typealias Id = String
|
||||||
|
}
|
||||||
|
|
|
@ -14,7 +14,7 @@ public struct Filter: Codable, Hashable, Identifiable {
|
||||||
public static var unknownCase: Self { .unknown }
|
public static var unknownCase: Self { .unknown }
|
||||||
}
|
}
|
||||||
|
|
||||||
public let id: String
|
public let id: Id
|
||||||
public var phrase: String
|
public var phrase: String
|
||||||
public var context: [Context]
|
public var context: [Context]
|
||||||
public var expiresAt: Date?
|
public var expiresAt: Date?
|
||||||
|
@ -23,8 +23,10 @@ public struct Filter: Codable, Hashable, Identifiable {
|
||||||
}
|
}
|
||||||
|
|
||||||
public extension Filter {
|
public extension Filter {
|
||||||
static let newFilterID: String = "com.metabolist.metatext.new-filter-id"
|
typealias Id = String
|
||||||
static let new = Self(id: newFilterID,
|
|
||||||
|
static let newFilterId: Id = "com.metabolist.metatext.new-filter-id"
|
||||||
|
static let new = Self(id: newFilterId,
|
||||||
phrase: "",
|
phrase: "",
|
||||||
context: [],
|
context: [],
|
||||||
expiresAt: nil,
|
expiresAt: nil,
|
||||||
|
|
|
@ -3,11 +3,15 @@
|
||||||
import Foundation
|
import Foundation
|
||||||
|
|
||||||
public struct List: Codable, Hashable, Identifiable {
|
public struct List: Codable, Hashable, Identifiable {
|
||||||
public let id: String
|
public let id: Id
|
||||||
public let title: String
|
public let title: String
|
||||||
|
|
||||||
public init(id: String, title: String) {
|
public init(id: Id, title: String) {
|
||||||
self.id = id
|
self.id = id
|
||||||
self.title = title
|
self.title = title
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public extension List {
|
||||||
|
typealias Id = String
|
||||||
|
}
|
||||||
|
|
|
@ -6,5 +6,5 @@ public struct Mention: Codable, Hashable {
|
||||||
public let url: URL
|
public let url: URL
|
||||||
public let username: String
|
public let username: String
|
||||||
public let acct: String
|
public let acct: String
|
||||||
public let id: String
|
public let id: Account.Id
|
||||||
}
|
}
|
||||||
|
|
|
@ -8,7 +8,7 @@ public struct Poll: Codable, Hashable {
|
||||||
public var votesCount: Int
|
public var votesCount: Int
|
||||||
}
|
}
|
||||||
|
|
||||||
public let id: String
|
public let id: Id
|
||||||
public let expiresAt: Date
|
public let expiresAt: Date
|
||||||
public let expired: Bool
|
public let expired: Bool
|
||||||
public let multiple: Bool
|
public let multiple: Bool
|
||||||
|
@ -19,3 +19,7 @@ public struct Poll: Codable, Hashable {
|
||||||
public let options: [Option]
|
public let options: [Option]
|
||||||
public let emojis: [Emoji]
|
public let emojis: [Emoji]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public extension Poll {
|
||||||
|
typealias Id = String
|
||||||
|
}
|
||||||
|
|
|
@ -13,7 +13,7 @@ public final class Status: Codable, Identifiable {
|
||||||
public static var unknownCase: Self { .unknown }
|
public static var unknownCase: Self { .unknown }
|
||||||
}
|
}
|
||||||
|
|
||||||
public let id: String
|
public let id: Status.Id
|
||||||
public let uri: String
|
public let uri: String
|
||||||
public let createdAt: Date
|
public let createdAt: Date
|
||||||
public let account: Account
|
public let account: Account
|
||||||
|
@ -30,8 +30,8 @@ public final class Status: Codable, Identifiable {
|
||||||
@DecodableDefault.Zero public private(set) var repliesCount: Int
|
@DecodableDefault.Zero public private(set) var repliesCount: Int
|
||||||
public let application: Application?
|
public let application: Application?
|
||||||
public let url: URL?
|
public let url: URL?
|
||||||
public let inReplyToId: String?
|
public let inReplyToId: Status.Id?
|
||||||
public let inReplyToAccountId: String?
|
public let inReplyToAccountId: Account.Id?
|
||||||
public let reblog: Status?
|
public let reblog: Status?
|
||||||
public let poll: Poll?
|
public let poll: Poll?
|
||||||
public let card: Card?
|
public let card: Card?
|
||||||
|
@ -44,7 +44,7 @@ public final class Status: Codable, Identifiable {
|
||||||
public let pinned: Bool?
|
public let pinned: Bool?
|
||||||
|
|
||||||
public init(
|
public init(
|
||||||
id: String,
|
id: Status.Id,
|
||||||
uri: String,
|
uri: String,
|
||||||
createdAt: Date,
|
createdAt: Date,
|
||||||
account: Account,
|
account: Account,
|
||||||
|
@ -61,8 +61,8 @@ public final class Status: Codable, Identifiable {
|
||||||
repliesCount: Int,
|
repliesCount: Int,
|
||||||
application: Application?,
|
application: Application?,
|
||||||
url: URL?,
|
url: URL?,
|
||||||
inReplyToId: String?,
|
inReplyToId: Status.Id?,
|
||||||
inReplyToAccountId: String?,
|
inReplyToAccountId: Account.Id?,
|
||||||
reblog: Status?,
|
reblog: Status?,
|
||||||
poll: Poll?,
|
poll: Poll?,
|
||||||
card: Card?,
|
card: Card?,
|
||||||
|
@ -106,6 +106,8 @@ public final class Status: Codable, Identifiable {
|
||||||
}
|
}
|
||||||
|
|
||||||
public extension Status {
|
public extension Status {
|
||||||
|
typealias Id = String
|
||||||
|
|
||||||
var displayStatus: Status {
|
var displayStatus: Status {
|
||||||
reblog ?? self
|
reblog ?? self
|
||||||
}
|
}
|
||||||
|
|
|
@ -6,7 +6,7 @@ import Mastodon
|
||||||
|
|
||||||
public enum AccessTokenEndpoint {
|
public enum AccessTokenEndpoint {
|
||||||
case oauthToken(
|
case oauthToken(
|
||||||
clientID: String,
|
clientId: String,
|
||||||
clientSecret: String,
|
clientSecret: String,
|
||||||
grantType: String,
|
grantType: String,
|
||||||
scopes: String,
|
scopes: String,
|
||||||
|
@ -58,9 +58,9 @@ extension AccessTokenEndpoint: Endpoint {
|
||||||
|
|
||||||
public var jsonBody: [String: Any]? {
|
public var jsonBody: [String: Any]? {
|
||||||
switch self {
|
switch self {
|
||||||
case let .oauthToken(clientID, clientSecret, grantType, scopes, code, redirectURI):
|
case let .oauthToken(clientId, clientSecret, grantType, scopes, code, redirectURI):
|
||||||
var params = [
|
var params = [
|
||||||
"client_id": clientID,
|
"client_id": clientId,
|
||||||
"client_secret": clientSecret,
|
"client_secret": clientSecret,
|
||||||
"grant_type": grantType,
|
"grant_type": grantType,
|
||||||
"scope": scopes]
|
"scope": scopes]
|
||||||
|
|
|
@ -6,7 +6,7 @@ import Mastodon
|
||||||
|
|
||||||
public enum AccountEndpoint {
|
public enum AccountEndpoint {
|
||||||
case verifyCredentials
|
case verifyCredentials
|
||||||
case accounts(id: String)
|
case accounts(id: Account.Id)
|
||||||
}
|
}
|
||||||
|
|
||||||
extension AccountEndpoint: Endpoint {
|
extension AccountEndpoint: Endpoint {
|
||||||
|
|
|
@ -5,8 +5,8 @@ import HTTP
|
||||||
import Mastodon
|
import Mastodon
|
||||||
|
|
||||||
public enum AccountsEndpoint {
|
public enum AccountsEndpoint {
|
||||||
case statusRebloggedBy(id: String)
|
case rebloggedBy(id: Status.Id)
|
||||||
case statusFavouritedBy(id: String)
|
case favouritedBy(id: Status.Id)
|
||||||
}
|
}
|
||||||
|
|
||||||
extension AccountsEndpoint: Endpoint {
|
extension AccountsEndpoint: Endpoint {
|
||||||
|
@ -14,23 +14,23 @@ extension AccountsEndpoint: Endpoint {
|
||||||
|
|
||||||
public var context: [String] {
|
public var context: [String] {
|
||||||
switch self {
|
switch self {
|
||||||
case .statusRebloggedBy, .statusFavouritedBy:
|
case .rebloggedBy, .favouritedBy:
|
||||||
return defaultContext + ["statuses"]
|
return defaultContext + ["statuses"]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public var pathComponentsInContext: [String] {
|
public var pathComponentsInContext: [String] {
|
||||||
switch self {
|
switch self {
|
||||||
case let .statusRebloggedBy(id):
|
case let .rebloggedBy(id):
|
||||||
return [id, "reblogged_by"]
|
return [id, "reblogged_by"]
|
||||||
case let .statusFavouritedBy(id):
|
case let .favouritedBy(id):
|
||||||
return [id, "favourited_by"]
|
return [id, "favourited_by"]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public var method: HTTPMethod {
|
public var method: HTTPMethod {
|
||||||
switch self {
|
switch self {
|
||||||
case .statusRebloggedBy, .statusFavouritedBy:
|
case .rebloggedBy, .favouritedBy:
|
||||||
return .get
|
return .get
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,7 +5,7 @@ import HTTP
|
||||||
import Mastodon
|
import Mastodon
|
||||||
|
|
||||||
public enum ContextEndpoint {
|
public enum ContextEndpoint {
|
||||||
case context(id: String)
|
case context(id: Status.Id)
|
||||||
}
|
}
|
||||||
|
|
||||||
extension ContextEndpoint: Endpoint {
|
extension ContextEndpoint: Endpoint {
|
||||||
|
|
|
@ -5,9 +5,9 @@ import HTTP
|
||||||
import Mastodon
|
import Mastodon
|
||||||
|
|
||||||
public enum DeletionEndpoint {
|
public enum DeletionEndpoint {
|
||||||
case oauthRevoke(token: String, clientID: String, clientSecret: String)
|
case oauthRevoke(token: String, clientId: String, clientSecret: String)
|
||||||
case list(id: String)
|
case list(id: List.Id)
|
||||||
case filter(id: String)
|
case filter(id: Filter.Id)
|
||||||
}
|
}
|
||||||
|
|
||||||
extension DeletionEndpoint: Endpoint {
|
extension DeletionEndpoint: Endpoint {
|
||||||
|
@ -44,8 +44,8 @@ extension DeletionEndpoint: Endpoint {
|
||||||
|
|
||||||
public var jsonBody: [String: Any]? {
|
public var jsonBody: [String: Any]? {
|
||||||
switch self {
|
switch self {
|
||||||
case let .oauthRevoke(token, clientID, clientSecret):
|
case let .oauthRevoke(token, clientId, clientSecret):
|
||||||
return ["token": token, "client_id": clientID, "client_secret": clientSecret]
|
return ["token": token, "client_id": clientId, "client_secret": clientSecret]
|
||||||
case .list, .filter:
|
case .list, .filter:
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
|
@ -12,7 +12,7 @@ public enum FilterEndpoint {
|
||||||
wholeWord: Bool,
|
wholeWord: Bool,
|
||||||
expiresIn: Date?)
|
expiresIn: Date?)
|
||||||
case update(
|
case update(
|
||||||
id: String,
|
id: Filter.Id,
|
||||||
phrase: String,
|
phrase: String,
|
||||||
context: [Filter.Context],
|
context: [Filter.Context],
|
||||||
irreversible: Bool,
|
irreversible: Bool,
|
||||||
|
|
|
@ -6,16 +6,16 @@ import Mastodon
|
||||||
|
|
||||||
public struct Paged<T: Endpoint> {
|
public struct Paged<T: Endpoint> {
|
||||||
public let endpoint: T
|
public let endpoint: T
|
||||||
public let maxID: String?
|
public let maxId: String?
|
||||||
public let minID: String?
|
public let minId: String?
|
||||||
public let sinceID: String?
|
public let sinceId: String?
|
||||||
public let limit: Int?
|
public let limit: Int?
|
||||||
|
|
||||||
public init(_ endpoint: T, maxID: String? = nil, minID: String? = nil, sinceID: String? = nil, limit: Int? = nil) {
|
public init(_ endpoint: T, maxId: String? = nil, minId: String? = nil, sinceId: String? = nil, limit: Int? = nil) {
|
||||||
self.endpoint = endpoint
|
self.endpoint = endpoint
|
||||||
self.maxID = maxID
|
self.maxId = maxId
|
||||||
self.minID = minID
|
self.minId = minId
|
||||||
self.sinceID = sinceID
|
self.sinceId = sinceId
|
||||||
self.limit = limit
|
self.limit = limit
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -34,9 +34,9 @@ extension Paged: Endpoint {
|
||||||
public var queryParameters: [String: String]? {
|
public var queryParameters: [String: String]? {
|
||||||
var queryParameters = endpoint.queryParameters ?? [String: String]()
|
var queryParameters = endpoint.queryParameters ?? [String: String]()
|
||||||
|
|
||||||
queryParameters["max_id"] = maxID
|
queryParameters["max_id"] = maxId
|
||||||
queryParameters["min_id"] = minID
|
queryParameters["min_id"] = minId
|
||||||
queryParameters["since_id"] = sinceID
|
queryParameters["since_id"] = sinceId
|
||||||
|
|
||||||
if let limit = limit {
|
if let limit = limit {
|
||||||
queryParameters["limit"] = String(limit)
|
queryParameters["limit"] = String(limit)
|
||||||
|
@ -50,9 +50,9 @@ extension Paged: Endpoint {
|
||||||
|
|
||||||
public struct PagedResult<T: Decodable>: Decodable {
|
public struct PagedResult<T: Decodable>: Decodable {
|
||||||
public struct Info: Decodable {
|
public struct Info: Decodable {
|
||||||
public let maxID: String?
|
public let maxId: String?
|
||||||
public let minID: String?
|
public let minId: String?
|
||||||
public let sinceID: String?
|
public let sinceId: String?
|
||||||
}
|
}
|
||||||
|
|
||||||
public let result: T
|
public let result: T
|
||||||
|
|
|
@ -5,9 +5,9 @@ import HTTP
|
||||||
import Mastodon
|
import Mastodon
|
||||||
|
|
||||||
public enum StatusEndpoint {
|
public enum StatusEndpoint {
|
||||||
case status(id: String)
|
case status(id: Status.Id)
|
||||||
case favourite(id: String)
|
case favourite(id: Status.Id)
|
||||||
case unfavourite(id: String)
|
case unfavourite(id: Status.Id)
|
||||||
}
|
}
|
||||||
|
|
||||||
extension StatusEndpoint: Endpoint {
|
extension StatusEndpoint: Endpoint {
|
||||||
|
|
|
@ -8,8 +8,8 @@ public enum StatusesEndpoint {
|
||||||
case timelinesPublic(local: Bool)
|
case timelinesPublic(local: Bool)
|
||||||
case timelinesTag(String)
|
case timelinesTag(String)
|
||||||
case timelinesHome
|
case timelinesHome
|
||||||
case timelinesList(id: String)
|
case timelinesList(id: List.Id)
|
||||||
case accountsStatuses(id: String, excludeReplies: Bool, onlyMedia: Bool, pinned: Bool)
|
case accountsStatuses(id: Account.Id, excludeReplies: Bool, onlyMedia: Bool, pinned: Bool)
|
||||||
}
|
}
|
||||||
|
|
||||||
extension StatusesEndpoint: Endpoint {
|
extension StatusesEndpoint: Endpoint {
|
||||||
|
|
|
@ -39,17 +39,17 @@ extension MastodonAPIClient {
|
||||||
|
|
||||||
public func pagedRequest<E: Endpoint>(
|
public func pagedRequest<E: Endpoint>(
|
||||||
_ endpoint: E,
|
_ endpoint: E,
|
||||||
maxID: String? = nil,
|
maxId: String? = nil,
|
||||||
minID: String? = nil,
|
minId: String? = nil,
|
||||||
sinceID: String? = nil,
|
sinceId: String? = nil,
|
||||||
limit: Int? = nil) -> AnyPublisher<PagedResult<E.ResultType>, Error> {
|
limit: Int? = nil) -> AnyPublisher<PagedResult<E.ResultType>, Error> {
|
||||||
let pagedTarget = target(endpoint: Paged(endpoint, maxID: maxID, minID: minID, sinceID: sinceID, limit: limit))
|
let pagedTarget = target(endpoint: Paged(endpoint, maxId: maxId, minId: minId, sinceId: sinceId, limit: limit))
|
||||||
let dataTask = dataTaskPublisher(pagedTarget).share()
|
let dataTask = dataTaskPublisher(pagedTarget).share()
|
||||||
let decoded = dataTask.map(\.data).decode(type: E.ResultType.self, decoder: decoder)
|
let decoded = dataTask.map(\.data).decode(type: E.ResultType.self, decoder: decoder)
|
||||||
let info = dataTask.map { _, response -> PagedResult<E.ResultType>.Info in
|
let info = dataTask.map { _, response -> PagedResult<E.ResultType>.Info in
|
||||||
var maxID: String?
|
var maxId: String?
|
||||||
var minID: String?
|
var minId: String?
|
||||||
var sinceID: String?
|
var sinceId: String?
|
||||||
|
|
||||||
if let links = response.value(forHTTPHeaderField: "Link") {
|
if let links = response.value(forHTTPHeaderField: "Link") {
|
||||||
let queryItems = Self.linkDataDetector.matches(
|
let queryItems = Self.linkDataDetector.matches(
|
||||||
|
@ -62,12 +62,12 @@ extension MastodonAPIClient {
|
||||||
}
|
}
|
||||||
.reduce([], +)
|
.reduce([], +)
|
||||||
|
|
||||||
maxID = queryItems.first { $0.name == "max_id" }?.value
|
maxId = queryItems.first { $0.name == "max_id" }?.value
|
||||||
minID = queryItems.first { $0.name == "min_id" }?.value
|
minId = queryItems.first { $0.name == "min_id" }?.value
|
||||||
sinceID = queryItems.first { $0.name == "since_id" }?.value
|
sinceId = queryItems.first { $0.name == "since_id" }?.value
|
||||||
}
|
}
|
||||||
|
|
||||||
return PagedResult.Info(maxID: maxID, minID: minID, sinceID: sinceID)
|
return PagedResult.Info(maxId: maxId, minId: minId, sinceId: sinceId)
|
||||||
}
|
}
|
||||||
|
|
||||||
return decoded.zip(info).map(PagedResult.init(result:info:)).eraseToAnyPublisher()
|
return decoded.zip(info).map(PagedResult.init(result:info:)).eraseToAnyPublisher()
|
||||||
|
|
|
@ -62,7 +62,7 @@ enum NotificationServiceError: Error {
|
||||||
}
|
}
|
||||||
|
|
||||||
private extension NotificationService {
|
private extension NotificationService {
|
||||||
static let identityIDUserInfoKey = "i"
|
static let identityIdUserInfoKey = "i"
|
||||||
static let encryptedMessageUserInfoKey = "m"
|
static let encryptedMessageUserInfoKey = "m"
|
||||||
static let saltUserInfoKey = "s"
|
static let saltUserInfoKey = "s"
|
||||||
static let serverPublicKeyUserInfoKey = "k"
|
static let serverPublicKeyUserInfoKey = "k"
|
||||||
|
@ -82,8 +82,8 @@ private extension NotificationService {
|
||||||
|
|
||||||
static func extractAndDecrypt(userInfo: [AnyHashable: Any]) throws -> Data {
|
static func extractAndDecrypt(userInfo: [AnyHashable: Any]) throws -> Data {
|
||||||
guard
|
guard
|
||||||
let identityIDString = userInfo[identityIDUserInfoKey] as? String,
|
let identityIdString = userInfo[identityIdUserInfoKey] as? String,
|
||||||
let identityID = UUID(uuidString: identityIDString),
|
let identityId = UUID(uuidString: identityIdString),
|
||||||
let encryptedMessageBase64 = (userInfo[encryptedMessageUserInfoKey] as? String)?.URLSafeBase64ToBase64(),
|
let encryptedMessageBase64 = (userInfo[encryptedMessageUserInfoKey] as? String)?.URLSafeBase64ToBase64(),
|
||||||
let encryptedMessage = Data(base64Encoded: encryptedMessageBase64),
|
let encryptedMessage = Data(base64Encoded: encryptedMessageBase64),
|
||||||
let saltBase64 = (userInfo[saltUserInfoKey] as? String)?.URLSafeBase64ToBase64(),
|
let saltBase64 = (userInfo[saltUserInfoKey] as? String)?.URLSafeBase64ToBase64(),
|
||||||
|
@ -92,7 +92,7 @@ private extension NotificationService {
|
||||||
let serverPublicKeyData = Data(base64Encoded: serverPublicKeyBase64)
|
let serverPublicKeyData = Data(base64Encoded: serverPublicKeyBase64)
|
||||||
else { throw NotificationServiceError.userInfoDataAbsent }
|
else { throw NotificationServiceError.userInfoDataAbsent }
|
||||||
|
|
||||||
let secretsService = Secrets(identityID: identityID, keychain: LiveKeychain.self)
|
let secretsService = Secrets(identityId: identityId, keychain: LiveKeychain.self)
|
||||||
|
|
||||||
guard
|
guard
|
||||||
let auth = try secretsService.getPushAuth(),
|
let auth = try secretsService.getPushAuth(),
|
||||||
|
|
|
@ -14,11 +14,11 @@ enum SecretsStorableError: Error {
|
||||||
}
|
}
|
||||||
|
|
||||||
public struct Secrets {
|
public struct Secrets {
|
||||||
public let identityID: UUID
|
public let identityId: UUID
|
||||||
private let keychain: Keychain.Type
|
private let keychain: Keychain.Type
|
||||||
|
|
||||||
public init(identityID: UUID, keychain: Keychain.Type) {
|
public init(identityId: UUID, keychain: Keychain.Type) {
|
||||||
self.identityID = identityID
|
self.identityId = identityId
|
||||||
self.keychain = keychain
|
self.keychain = keychain
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -26,7 +26,7 @@ public struct Secrets {
|
||||||
public extension Secrets {
|
public extension Secrets {
|
||||||
enum Item: String, CaseIterable {
|
enum Item: String, CaseIterable {
|
||||||
case instanceURL
|
case instanceURL
|
||||||
case clientID
|
case clientId
|
||||||
case clientSecret
|
case clientSecret
|
||||||
case accessToken
|
case accessToken
|
||||||
case pushKey
|
case pushKey
|
||||||
|
@ -56,12 +56,12 @@ extension Secrets.Item {
|
||||||
|
|
||||||
public extension Secrets {
|
public extension Secrets {
|
||||||
// https://www.zetetic.net/sqlcipher/sqlcipher-api/#key
|
// https://www.zetetic.net/sqlcipher/sqlcipher-api/#key
|
||||||
static func databaseKey(identityID: UUID?, keychain: Keychain.Type) throws -> String {
|
static func databaseKey(identityId: UUID?, keychain: Keychain.Type) throws -> String {
|
||||||
let passphraseData: Data
|
let passphraseData: Data
|
||||||
let scopedSecrets: Secrets?
|
let scopedSecrets: Secrets?
|
||||||
|
|
||||||
if let identityID = identityID {
|
if let identityId = identityId {
|
||||||
scopedSecrets = Secrets(identityID: identityID, keychain: keychain)
|
scopedSecrets = Secrets(identityId: identityId, keychain: keychain)
|
||||||
} else {
|
} else {
|
||||||
scopedSecrets = nil
|
scopedSecrets = nil
|
||||||
}
|
}
|
||||||
|
@ -114,12 +114,12 @@ public extension Secrets {
|
||||||
try set(instanceURL, forItem: .instanceURL)
|
try set(instanceURL, forItem: .instanceURL)
|
||||||
}
|
}
|
||||||
|
|
||||||
func getClientID() throws -> String {
|
func getClientId() throws -> String {
|
||||||
try item(.clientID)
|
try item(.clientId)
|
||||||
}
|
}
|
||||||
|
|
||||||
func setClientID(_ clientID: String) throws {
|
func setClientId(_ clientId: String) throws {
|
||||||
try set(clientID, forItem: .clientID)
|
try set(clientId, forItem: .clientId)
|
||||||
}
|
}
|
||||||
|
|
||||||
func getClientSecret() throws -> String {
|
func getClientSecret() throws -> String {
|
||||||
|
@ -200,7 +200,7 @@ private extension Secrets {
|
||||||
}
|
}
|
||||||
|
|
||||||
func scopedKey(item: Item) -> String {
|
func scopedKey(item: Item) -> String {
|
||||||
identityID.uuidString + "." + item.rawValue
|
identityId.uuidString + "." + item.rawValue
|
||||||
}
|
}
|
||||||
|
|
||||||
func set(_ data: SecretsStorable, forItem item: Item) throws {
|
func set(_ data: SecretsStorable, forItem item: Item) throws {
|
||||||
|
|
|
@ -8,14 +8,14 @@ import MastodonAPI
|
||||||
|
|
||||||
public struct AccountListService {
|
public struct AccountListService {
|
||||||
public let sections: AnyPublisher<[[CollectionItem]], Error>
|
public let sections: AnyPublisher<[[CollectionItem]], Error>
|
||||||
public let nextPageMaxIDs: AnyPublisher<String, Never>
|
public let nextPageMaxId: AnyPublisher<String, Never>
|
||||||
public let navigationService: NavigationService
|
public let navigationService: NavigationService
|
||||||
|
|
||||||
private let list: AccountList
|
private let list: AccountList
|
||||||
private let endpoint: AccountsEndpoint
|
private let endpoint: AccountsEndpoint
|
||||||
private let mastodonAPIClient: MastodonAPIClient
|
private let mastodonAPIClient: MastodonAPIClient
|
||||||
private let contentDatabase: ContentDatabase
|
private let contentDatabase: ContentDatabase
|
||||||
private let nextPageMaxIDsSubject = PassthroughSubject<String, Never>()
|
private let nextPageMaxIdSubject = PassthroughSubject<String, Never>()
|
||||||
|
|
||||||
init(endpoint: AccountsEndpoint, mastodonAPIClient: MastodonAPIClient, contentDatabase: ContentDatabase) {
|
init(endpoint: AccountsEndpoint, mastodonAPIClient: MastodonAPIClient, contentDatabase: ContentDatabase) {
|
||||||
list = AccountList()
|
list = AccountList()
|
||||||
|
@ -25,18 +25,18 @@ public struct AccountListService {
|
||||||
sections = contentDatabase.accountListObservation(list)
|
sections = contentDatabase.accountListObservation(list)
|
||||||
.map { [$0.map(CollectionItem.account)] }
|
.map { [$0.map(CollectionItem.account)] }
|
||||||
.eraseToAnyPublisher()
|
.eraseToAnyPublisher()
|
||||||
nextPageMaxIDs = nextPageMaxIDsSubject.eraseToAnyPublisher()
|
nextPageMaxId = nextPageMaxIdSubject.eraseToAnyPublisher()
|
||||||
navigationService = NavigationService(mastodonAPIClient: mastodonAPIClient, contentDatabase: contentDatabase)
|
navigationService = NavigationService(mastodonAPIClient: mastodonAPIClient, contentDatabase: contentDatabase)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
extension AccountListService: CollectionService {
|
extension AccountListService: CollectionService {
|
||||||
public func request(maxID: String?, minID: String?) -> AnyPublisher<Never, Error> {
|
public func request(maxId: String?, minId: String?) -> AnyPublisher<Never, Error> {
|
||||||
mastodonAPIClient.pagedRequest(endpoint, maxID: maxID, minID: minID)
|
mastodonAPIClient.pagedRequest(endpoint, maxId: maxId, minId: minId)
|
||||||
.handleEvents(receiveOutput: {
|
.handleEvents(receiveOutput: {
|
||||||
guard let maxID = $0.info.maxID else { return }
|
guard let maxId = $0.info.maxId else { return }
|
||||||
|
|
||||||
nextPageMaxIDsSubject.send(maxID)
|
nextPageMaxIdSubject.send(maxId)
|
||||||
})
|
})
|
||||||
.flatMap { contentDatabase.append(accounts: $0.result, toList: list) }
|
.flatMap { contentDatabase.append(accounts: $0.result, toList: list) }
|
||||||
.eraseToAnyPublisher()
|
.eraseToAnyPublisher()
|
||||||
|
|
|
@ -8,11 +8,11 @@ import MastodonAPI
|
||||||
import Secrets
|
import Secrets
|
||||||
|
|
||||||
public struct AllIdentitiesService {
|
public struct AllIdentitiesService {
|
||||||
public let identitiesCreated: AnyPublisher<UUID, Never>
|
public let identitiesCreated: AnyPublisher<Identity.Id, Never>
|
||||||
|
|
||||||
private let environment: AppEnvironment
|
private let environment: AppEnvironment
|
||||||
private let database: IdentityDatabase
|
private let database: IdentityDatabase
|
||||||
private let identitiesCreatedSubject = PassthroughSubject<UUID, Never>()
|
private let identitiesCreatedSubject = PassthroughSubject<Identity.Id, Never>()
|
||||||
|
|
||||||
public init(environment: AppEnvironment) throws {
|
public init(environment: AppEnvironment) throws {
|
||||||
self.environment = environment
|
self.environment = environment
|
||||||
|
@ -30,17 +30,17 @@ public extension AllIdentitiesService {
|
||||||
case browsing
|
case browsing
|
||||||
}
|
}
|
||||||
|
|
||||||
func identityService(id: UUID) throws -> IdentityService {
|
func identityService(id: Identity.Id) throws -> IdentityService {
|
||||||
try IdentityService(id: id, database: database, environment: environment)
|
try IdentityService(id: id, database: database, environment: environment)
|
||||||
}
|
}
|
||||||
|
|
||||||
func immediateMostRecentlyUsedIdentityIDObservation() -> AnyPublisher<UUID?, Error> {
|
func immediateMostRecentlyUsedIdentityIdObservation() -> AnyPublisher<Identity.Id?, Error> {
|
||||||
database.immediateMostRecentlyUsedIdentityIDObservation()
|
database.immediateMostRecentlyUsedIdentityIdObservation()
|
||||||
}
|
}
|
||||||
|
|
||||||
func createIdentity(url: URL, kind: IdentityCreation) -> AnyPublisher<Never, Error> {
|
func createIdentity(url: URL, kind: IdentityCreation) -> AnyPublisher<Never, Error> {
|
||||||
let id = environment.uuid()
|
let id = environment.uuid()
|
||||||
let secrets = Secrets(identityID: id, keychain: environment.keychain)
|
let secrets = Secrets(identityId: id, keychain: environment.keychain)
|
||||||
|
|
||||||
do {
|
do {
|
||||||
try secrets.setInstanceURL(url)
|
try secrets.setInstanceURL(url)
|
||||||
|
@ -76,7 +76,7 @@ public extension AllIdentitiesService {
|
||||||
|
|
||||||
return authenticationPublisher
|
return authenticationPublisher
|
||||||
.tryMap {
|
.tryMap {
|
||||||
try secrets.setClientID($0.clientId)
|
try secrets.setClientId($0.clientId)
|
||||||
try secrets.setClientSecret($0.clientSecret)
|
try secrets.setClientSecret($0.clientSecret)
|
||||||
try secrets.setAccessToken($1.accessToken)
|
try secrets.setAccessToken($1.accessToken)
|
||||||
}
|
}
|
||||||
|
@ -84,13 +84,13 @@ public extension AllIdentitiesService {
|
||||||
.eraseToAnyPublisher()
|
.eraseToAnyPublisher()
|
||||||
}
|
}
|
||||||
|
|
||||||
func deleteIdentity(id: UUID) -> AnyPublisher<Never, Error> {
|
func deleteIdentity(id: Identity.Id) -> AnyPublisher<Never, Error> {
|
||||||
database.deleteIdentity(id: id)
|
database.deleteIdentity(id: id)
|
||||||
.collect()
|
.collect()
|
||||||
.tryMap { _ -> AnyPublisher<Never, Error> in
|
.tryMap { _ -> AnyPublisher<Never, Error> in
|
||||||
try ContentDatabase.delete(forIdentityID: id)
|
try ContentDatabase.delete(id: id)
|
||||||
|
|
||||||
let secrets = Secrets(identityID: id, keychain: environment.keychain)
|
let secrets = Secrets(identityId: id, keychain: environment.keychain)
|
||||||
|
|
||||||
defer { secrets.deleteAllItems() }
|
defer { secrets.deleteAllItems() }
|
||||||
|
|
||||||
|
@ -100,7 +100,7 @@ public extension AllIdentitiesService {
|
||||||
instanceURL: try secrets.getInstanceURL())
|
instanceURL: try secrets.getInstanceURL())
|
||||||
.request(DeletionEndpoint.oauthRevoke(
|
.request(DeletionEndpoint.oauthRevoke(
|
||||||
token: try secrets.getAccessToken(),
|
token: try secrets.getAccessToken(),
|
||||||
clientID: try secrets.getClientID(),
|
clientId: try secrets.getClientId(),
|
||||||
clientSecret: try secrets.getClientSecret()))
|
clientSecret: try secrets.getClientSecret()))
|
||||||
.ignoreOutput()
|
.ignoreOutput()
|
||||||
.eraseToAnyPublisher()
|
.eraseToAnyPublisher()
|
||||||
|
|
|
@ -29,7 +29,8 @@ extension AuthenticationService {
|
||||||
.eraseToAnyPublisher()
|
.eraseToAnyPublisher()
|
||||||
}
|
}
|
||||||
|
|
||||||
func register(_ registration: Registration, id: UUID) -> AnyPublisher<(AppAuthorization, AccessToken), Error> {
|
func register(_ registration: Registration,
|
||||||
|
id: Identity.Id) -> AnyPublisher<(AppAuthorization, AccessToken), Error> {
|
||||||
let redirectURI = OAuth.registrationCallbackURL.appendingPathComponent(id.uuidString)
|
let redirectURI = OAuth.registrationCallbackURL.appendingPathComponent(id.uuidString)
|
||||||
let authorization = appAuthorization(redirectURI: redirectURI)
|
let authorization = appAuthorization(redirectURI: redirectURI)
|
||||||
.share()
|
.share()
|
||||||
|
@ -38,7 +39,7 @@ extension AuthenticationService {
|
||||||
authorization.flatMap { appAuthorization -> AnyPublisher<AccessToken, Error> in
|
authorization.flatMap { appAuthorization -> AnyPublisher<AccessToken, Error> in
|
||||||
mastodonAPIClient.request(
|
mastodonAPIClient.request(
|
||||||
AccessTokenEndpoint.oauthToken(
|
AccessTokenEndpoint.oauthToken(
|
||||||
clientID: appAuthorization.clientId,
|
clientId: appAuthorization.clientId,
|
||||||
clientSecret: appAuthorization.clientSecret,
|
clientSecret: appAuthorization.clientSecret,
|
||||||
grantType: OAuth.registrationGrantType,
|
grantType: OAuth.registrationGrantType,
|
||||||
scopes: OAuth.scopes,
|
scopes: OAuth.scopes,
|
||||||
|
@ -134,7 +135,7 @@ private extension AuthenticationService {
|
||||||
.flatMap {
|
.flatMap {
|
||||||
mastodonAPIClient.request(
|
mastodonAPIClient.request(
|
||||||
AccessTokenEndpoint.oauthToken(
|
AccessTokenEndpoint.oauthToken(
|
||||||
clientID: appAuthorization.clientId,
|
clientId: appAuthorization.clientId,
|
||||||
clientSecret: appAuthorization.clientSecret,
|
clientSecret: appAuthorization.clientSecret,
|
||||||
grantType: OAuth.authorizationCodeGrantType,
|
grantType: OAuth.authorizationCodeGrantType,
|
||||||
scopes: OAuth.scopes,
|
scopes: OAuth.scopes,
|
||||||
|
|
|
@ -4,17 +4,17 @@ import Combine
|
||||||
|
|
||||||
public protocol CollectionService {
|
public protocol CollectionService {
|
||||||
var sections: AnyPublisher<[[CollectionItem]], Error> { get }
|
var sections: AnyPublisher<[[CollectionItem]], Error> { get }
|
||||||
var nextPageMaxIDs: AnyPublisher<String, Never> { get }
|
var nextPageMaxId: AnyPublisher<String, Never> { get }
|
||||||
var title: AnyPublisher<String, Never> { get }
|
var title: AnyPublisher<String, Never> { get }
|
||||||
var navigationService: NavigationService { get }
|
var navigationService: NavigationService { get }
|
||||||
var contextParentID: String? { get }
|
var contextParentId: String? { get }
|
||||||
func request(maxID: String?, minID: String?) -> AnyPublisher<Never, Error>
|
func request(maxId: String?, minId: String?) -> AnyPublisher<Never, Error>
|
||||||
}
|
}
|
||||||
|
|
||||||
extension CollectionService {
|
extension CollectionService {
|
||||||
public var nextPageMaxIDs: AnyPublisher<String, Never> { Empty().eraseToAnyPublisher() }
|
public var nextPageMaxId: AnyPublisher<String, Never> { Empty().eraseToAnyPublisher() }
|
||||||
|
|
||||||
public var title: AnyPublisher<String, Never> { Empty().eraseToAnyPublisher() }
|
public var title: AnyPublisher<String, Never> { Empty().eraseToAnyPublisher() }
|
||||||
|
|
||||||
public var contextParentID: String? { nil }
|
public var contextParentId: String? { nil }
|
||||||
}
|
}
|
||||||
|
|
|
@ -9,27 +9,27 @@ import MastodonAPI
|
||||||
public struct ContextService {
|
public struct ContextService {
|
||||||
public let sections: AnyPublisher<[[CollectionItem]], Error>
|
public let sections: AnyPublisher<[[CollectionItem]], Error>
|
||||||
public let navigationService: NavigationService
|
public let navigationService: NavigationService
|
||||||
public var contextParentID: String? { parentID }
|
public var contextParentId: String? { id }
|
||||||
|
|
||||||
private let parentID: String
|
private let id: Status.Id
|
||||||
private let mastodonAPIClient: MastodonAPIClient
|
private let mastodonAPIClient: MastodonAPIClient
|
||||||
private let contentDatabase: ContentDatabase
|
private let contentDatabase: ContentDatabase
|
||||||
|
|
||||||
init(parentID: String, mastodonAPIClient: MastodonAPIClient, contentDatabase: ContentDatabase) {
|
init(id: Status.Id, mastodonAPIClient: MastodonAPIClient, contentDatabase: ContentDatabase) {
|
||||||
self.parentID = parentID
|
self.id = id
|
||||||
self.mastodonAPIClient = mastodonAPIClient
|
self.mastodonAPIClient = mastodonAPIClient
|
||||||
self.contentDatabase = contentDatabase
|
self.contentDatabase = contentDatabase
|
||||||
sections = contentDatabase.contextObservation(parentID: parentID)
|
sections = contentDatabase.contextObservation(id: id)
|
||||||
navigationService = NavigationService(mastodonAPIClient: mastodonAPIClient, contentDatabase: contentDatabase)
|
navigationService = NavigationService(mastodonAPIClient: mastodonAPIClient, contentDatabase: contentDatabase)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
extension ContextService: CollectionService {
|
extension ContextService: CollectionService {
|
||||||
public func request(maxID: String?, minID: String?) -> AnyPublisher<Never, Error> {
|
public func request(maxId: String?, minId: String?) -> AnyPublisher<Never, Error> {
|
||||||
mastodonAPIClient.request(StatusEndpoint.status(id: parentID))
|
mastodonAPIClient.request(StatusEndpoint.status(id: id))
|
||||||
.flatMap(contentDatabase.insert(status:))
|
.flatMap(contentDatabase.insert(status:))
|
||||||
.merge(with: mastodonAPIClient.request(ContextEndpoint.context(id: parentID))
|
.merge(with: mastodonAPIClient.request(ContextEndpoint.context(id: id))
|
||||||
.flatMap { contentDatabase.insert(context: $0, parentID: parentID) })
|
.flatMap { contentDatabase.insert(context: $0, parentId: id) })
|
||||||
.eraseToAnyPublisher()
|
.eraseToAnyPublisher()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -9,25 +9,25 @@ import MastodonAPI
|
||||||
import Secrets
|
import Secrets
|
||||||
|
|
||||||
public struct IdentityService {
|
public struct IdentityService {
|
||||||
private let identityID: UUID
|
private let id: Identity.Id
|
||||||
private let identityDatabase: IdentityDatabase
|
private let identityDatabase: IdentityDatabase
|
||||||
private let contentDatabase: ContentDatabase
|
private let contentDatabase: ContentDatabase
|
||||||
private let environment: AppEnvironment
|
private let environment: AppEnvironment
|
||||||
private let mastodonAPIClient: MastodonAPIClient
|
private let mastodonAPIClient: MastodonAPIClient
|
||||||
private let secrets: Secrets
|
private let secrets: Secrets
|
||||||
|
|
||||||
init(id: UUID, database: IdentityDatabase, environment: AppEnvironment) throws {
|
init(id: Identity.Id, database: IdentityDatabase, environment: AppEnvironment) throws {
|
||||||
identityID = id
|
self.id = id
|
||||||
identityDatabase = database
|
identityDatabase = database
|
||||||
self.environment = environment
|
self.environment = environment
|
||||||
secrets = Secrets(
|
secrets = Secrets(
|
||||||
identityID: id,
|
identityId: id,
|
||||||
keychain: environment.keychain)
|
keychain: environment.keychain)
|
||||||
mastodonAPIClient = MastodonAPIClient(session: environment.session,
|
mastodonAPIClient = MastodonAPIClient(session: environment.session,
|
||||||
instanceURL: try secrets.getInstanceURL())
|
instanceURL: try secrets.getInstanceURL())
|
||||||
mastodonAPIClient.accessToken = try? secrets.getAccessToken()
|
mastodonAPIClient.accessToken = try? secrets.getAccessToken()
|
||||||
|
|
||||||
contentDatabase = try ContentDatabase(identityID: id,
|
contentDatabase = try ContentDatabase(id: id,
|
||||||
inMemory: environment.inMemoryContent,
|
inMemory: environment.inMemoryContent,
|
||||||
keychain: environment.keychain)
|
keychain: environment.keychain)
|
||||||
}
|
}
|
||||||
|
@ -35,29 +35,29 @@ public struct IdentityService {
|
||||||
|
|
||||||
public extension IdentityService {
|
public extension IdentityService {
|
||||||
func updateLastUse() -> AnyPublisher<Never, Error> {
|
func updateLastUse() -> AnyPublisher<Never, Error> {
|
||||||
identityDatabase.updateLastUsedAt(identityID: identityID)
|
identityDatabase.updateLastUsedAt(id: id)
|
||||||
}
|
}
|
||||||
|
|
||||||
func verifyCredentials() -> AnyPublisher<Never, Error> {
|
func verifyCredentials() -> AnyPublisher<Never, Error> {
|
||||||
mastodonAPIClient.request(AccountEndpoint.verifyCredentials)
|
mastodonAPIClient.request(AccountEndpoint.verifyCredentials)
|
||||||
.flatMap { identityDatabase.updateAccount($0, forIdentityID: identityID) }
|
.flatMap { identityDatabase.updateAccount($0, id: id) }
|
||||||
.eraseToAnyPublisher()
|
.eraseToAnyPublisher()
|
||||||
}
|
}
|
||||||
|
|
||||||
func refreshServerPreferences() -> AnyPublisher<Never, Error> {
|
func refreshServerPreferences() -> AnyPublisher<Never, Error> {
|
||||||
mastodonAPIClient.request(PreferencesEndpoint.preferences)
|
mastodonAPIClient.request(PreferencesEndpoint.preferences)
|
||||||
.flatMap { identityDatabase.updatePreferences($0, forIdentityID: identityID) }
|
.flatMap { identityDatabase.updatePreferences($0, id: id) }
|
||||||
.eraseToAnyPublisher()
|
.eraseToAnyPublisher()
|
||||||
}
|
}
|
||||||
|
|
||||||
func refreshInstance() -> AnyPublisher<Never, Error> {
|
func refreshInstance() -> AnyPublisher<Never, Error> {
|
||||||
mastodonAPIClient.request(InstanceEndpoint.instance)
|
mastodonAPIClient.request(InstanceEndpoint.instance)
|
||||||
.flatMap { identityDatabase.updateInstance($0, forIdentityID: identityID) }
|
.flatMap { identityDatabase.updateInstance($0, id: id) }
|
||||||
.eraseToAnyPublisher()
|
.eraseToAnyPublisher()
|
||||||
}
|
}
|
||||||
|
|
||||||
func confirmIdentity() -> AnyPublisher<Never, Error> {
|
func confirmIdentity() -> AnyPublisher<Never, Error> {
|
||||||
identityDatabase.confirmIdentity(id: identityID)
|
identityDatabase.confirmIdentity(id: id)
|
||||||
}
|
}
|
||||||
|
|
||||||
func identitiesObservation() -> AnyPublisher<[Identity], Error> {
|
func identitiesObservation() -> AnyPublisher<[Identity], Error> {
|
||||||
|
@ -65,7 +65,7 @@ public extension IdentityService {
|
||||||
}
|
}
|
||||||
|
|
||||||
func recentIdentitiesObservation() -> AnyPublisher<[Identity], Error> {
|
func recentIdentitiesObservation() -> AnyPublisher<[Identity], Error> {
|
||||||
identityDatabase.recentIdentitiesObservation(excluding: identityID)
|
identityDatabase.recentIdentitiesObservation(excluding: id)
|
||||||
}
|
}
|
||||||
|
|
||||||
func refreshLists() -> AnyPublisher<Never, Error> {
|
func refreshLists() -> AnyPublisher<Never, Error> {
|
||||||
|
@ -80,7 +80,7 @@ public extension IdentityService {
|
||||||
.eraseToAnyPublisher()
|
.eraseToAnyPublisher()
|
||||||
}
|
}
|
||||||
|
|
||||||
func deleteList(id: String) -> AnyPublisher<Never, Error> {
|
func deleteList(id: List.Id) -> AnyPublisher<Never, Error> {
|
||||||
mastodonAPIClient.request(DeletionEndpoint.list(id: id))
|
mastodonAPIClient.request(DeletionEndpoint.list(id: id))
|
||||||
.map { _ in id }
|
.map { _ in id }
|
||||||
.flatMap(contentDatabase.deleteList(id:))
|
.flatMap(contentDatabase.deleteList(id:))
|
||||||
|
@ -88,7 +88,7 @@ public extension IdentityService {
|
||||||
}
|
}
|
||||||
|
|
||||||
func observation(immediate: Bool) -> AnyPublisher<Identity, Error> {
|
func observation(immediate: Bool) -> AnyPublisher<Identity, Error> {
|
||||||
identityDatabase.identityObservation(id: identityID, immediate: immediate)
|
identityDatabase.identityObservation(id: id, immediate: immediate)
|
||||||
}
|
}
|
||||||
|
|
||||||
func listsObservation() -> AnyPublisher<[Timeline], Error> {
|
func listsObservation() -> AnyPublisher<[Timeline], Error> {
|
||||||
|
@ -122,7 +122,7 @@ public extension IdentityService {
|
||||||
.eraseToAnyPublisher()
|
.eraseToAnyPublisher()
|
||||||
}
|
}
|
||||||
|
|
||||||
func deleteFilter(id: String) -> AnyPublisher<Never, Error> {
|
func deleteFilter(id: Filter.Id) -> AnyPublisher<Never, Error> {
|
||||||
mastodonAPIClient.request(DeletionEndpoint.filter(id: id))
|
mastodonAPIClient.request(DeletionEndpoint.filter(id: id))
|
||||||
.flatMap { _ in contentDatabase.deleteFilter(id: id) }
|
.flatMap { _ in contentDatabase.deleteFilter(id: id) }
|
||||||
.eraseToAnyPublisher()
|
.eraseToAnyPublisher()
|
||||||
|
@ -137,7 +137,7 @@ public extension IdentityService {
|
||||||
}
|
}
|
||||||
|
|
||||||
func updatePreferences(_ preferences: Identity.Preferences) -> AnyPublisher<Never, Error> {
|
func updatePreferences(_ preferences: Identity.Preferences) -> AnyPublisher<Never, Error> {
|
||||||
identityDatabase.updatePreferences(preferences, forIdentityID: identityID)
|
identityDatabase.updatePreferences(preferences, id: id)
|
||||||
.collect()
|
.collect()
|
||||||
.filter { _ in preferences.useServerPostingReadingPreferences }
|
.filter { _ in preferences.useServerPostingReadingPreferences }
|
||||||
.flatMap { _ in refreshServerPreferences() }
|
.flatMap { _ in refreshServerPreferences() }
|
||||||
|
@ -157,7 +157,7 @@ public extension IdentityService {
|
||||||
|
|
||||||
let endpoint = Self.pushSubscriptionEndpointURL
|
let endpoint = Self.pushSubscriptionEndpointURL
|
||||||
.appendingPathComponent(deviceToken.base16EncodedString())
|
.appendingPathComponent(deviceToken.base16EncodedString())
|
||||||
.appendingPathComponent(identityID.uuidString)
|
.appendingPathComponent(id.uuidString)
|
||||||
|
|
||||||
return mastodonAPIClient.request(
|
return mastodonAPIClient.request(
|
||||||
PushSubscriptionEndpoint.create(
|
PushSubscriptionEndpoint.create(
|
||||||
|
@ -165,15 +165,15 @@ public extension IdentityService {
|
||||||
publicKey: publicKey,
|
publicKey: publicKey,
|
||||||
auth: auth,
|
auth: auth,
|
||||||
alerts: alerts))
|
alerts: alerts))
|
||||||
.map { ($0.alerts, deviceToken, identityID) }
|
.map { ($0.alerts, deviceToken, id) }
|
||||||
.flatMap(identityDatabase.updatePushSubscription(alerts:deviceToken:forIdentityID:))
|
.flatMap(identityDatabase.updatePushSubscription(alerts:deviceToken:id:))
|
||||||
.eraseToAnyPublisher()
|
.eraseToAnyPublisher()
|
||||||
}
|
}
|
||||||
|
|
||||||
func updatePushSubscription(alerts: PushSubscription.Alerts) -> AnyPublisher<Never, Error> {
|
func updatePushSubscription(alerts: PushSubscription.Alerts) -> AnyPublisher<Never, Error> {
|
||||||
mastodonAPIClient.request(PushSubscriptionEndpoint.update(alerts: alerts))
|
mastodonAPIClient.request(PushSubscriptionEndpoint.update(alerts: alerts))
|
||||||
.map { ($0.alerts, nil, identityID) }
|
.map { ($0.alerts, nil, id) }
|
||||||
.flatMap(identityDatabase.updatePushSubscription(alerts:deviceToken:forIdentityID:))
|
.flatMap(identityDatabase.updatePushSubscription(alerts:deviceToken:id:))
|
||||||
.eraseToAnyPublisher()
|
.eraseToAnyPublisher()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -20,8 +20,8 @@ public extension LoadMoreService {
|
||||||
func request(direction: LoadMore.Direction) -> AnyPublisher<Never, Error> {
|
func request(direction: LoadMore.Direction) -> AnyPublisher<Never, Error> {
|
||||||
mastodonAPIClient.pagedRequest(
|
mastodonAPIClient.pagedRequest(
|
||||||
loadMore.timeline.endpoint,
|
loadMore.timeline.endpoint,
|
||||||
maxID: direction == .down ? loadMore.afterStatusId : nil,
|
maxId: direction == .down ? loadMore.afterStatusId : nil,
|
||||||
minID: direction == .up ? loadMore.beforeStatusId : nil)
|
minId: direction == .up ? loadMore.beforeStatusId : nil)
|
||||||
.flatMap {
|
.flatMap {
|
||||||
contentDatabase.insert(
|
contentDatabase.insert(
|
||||||
statuses: $0.result,
|
statuses: $0.result,
|
||||||
|
|
|
@ -36,10 +36,10 @@ public extension NavigationService {
|
||||||
mastodonAPIClient: mastodonAPIClient,
|
mastodonAPIClient: mastodonAPIClient,
|
||||||
contentDatabase: contentDatabase)))
|
contentDatabase: contentDatabase)))
|
||||||
.eraseToAnyPublisher()
|
.eraseToAnyPublisher()
|
||||||
} else if let accountID = accountID(url: url) {
|
} else if let accountId = accountId(url: url) {
|
||||||
return Just(.profile(profileService(id: accountID))).eraseToAnyPublisher()
|
return Just(.profile(profileService(id: accountId))).eraseToAnyPublisher()
|
||||||
} else if mastodonAPIClient.instanceURL.host == url.host, let statusID = url.statusID {
|
} else if mastodonAPIClient.instanceURL.host == url.host, let statusId = url.statusId {
|
||||||
return Just(.collection(contextService(id: statusID))).eraseToAnyPublisher()
|
return Just(.collection(contextService(id: statusId))).eraseToAnyPublisher()
|
||||||
}
|
}
|
||||||
|
|
||||||
if url.shouldWebfinger {
|
if url.shouldWebfinger {
|
||||||
|
@ -49,11 +49,11 @@ public extension NavigationService {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func contextService(id: String) -> ContextService {
|
func contextService(id: Status.Id) -> ContextService {
|
||||||
ContextService(parentID: id, mastodonAPIClient: mastodonAPIClient, contentDatabase: contentDatabase)
|
ContextService(id: id, mastodonAPIClient: mastodonAPIClient, contentDatabase: contentDatabase)
|
||||||
}
|
}
|
||||||
|
|
||||||
func profileService(id: String) -> ProfileService {
|
func profileService(id: Account.Id) -> ProfileService {
|
||||||
ProfileService(id: id, mastodonAPIClient: mastodonAPIClient, contentDatabase: contentDatabase)
|
ProfileService(id: id, mastodonAPIClient: mastodonAPIClient, contentDatabase: contentDatabase)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -86,12 +86,12 @@ private extension NavigationService {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func accountID(url: URL) -> String? {
|
func accountId(url: URL) -> String? {
|
||||||
if let mentionID = status?.mentions.first(where: { $0.url.path.lowercased() == url.path.lowercased() })?.id {
|
if let mentionId = status?.mentions.first(where: { $0.url.path.lowercased() == url.path.lowercased() })?.id {
|
||||||
return mentionID
|
return mentionId
|
||||||
} else if
|
} else if
|
||||||
mastodonAPIClient.instanceURL.host == url.host {
|
mastodonAPIClient.instanceURL.host == url.host {
|
||||||
return url.accountID
|
return url.accountId
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
|
@ -131,21 +131,21 @@ private extension URL {
|
||||||
|| (pathComponents.count == 3 && pathComponents[0...1] == ["/", "users"])
|
|| (pathComponents.count == 3 && pathComponents[0...1] == ["/", "users"])
|
||||||
}
|
}
|
||||||
|
|
||||||
var accountID: String? {
|
var accountId: Account.Id? {
|
||||||
if let accountID = pathComponents.last, pathComponents == ["/", "web", "accounts", accountID] {
|
if let accountId = pathComponents.last, pathComponents == ["/", "web", "accounts", accountId] {
|
||||||
return accountID
|
return accountId
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
var statusID: String? {
|
var statusId: Status.Id? {
|
||||||
guard let statusID = pathComponents.last else { return nil }
|
guard let statusId = pathComponents.last else { return nil }
|
||||||
|
|
||||||
if pathComponents.count == 3, pathComponents[1].starts(with: "@") {
|
if pathComponents.count == 3, pathComponents[1].starts(with: "@") {
|
||||||
return statusID
|
return statusId
|
||||||
} else if pathComponents == ["/", "web", "statuses", statusID] {
|
} else if pathComponents == ["/", "web", "statuses", statusId] {
|
||||||
return statusID
|
return statusId
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
|
@ -160,6 +160,6 @@ private extension URL {
|
||||||
}
|
}
|
||||||
|
|
||||||
var shouldWebfinger: Bool {
|
var shouldWebfinger: Bool {
|
||||||
isAccountURL || accountID != nil || statusID != nil || tag != nil
|
isAccountURL || accountId != nil || statusId != nil || tag != nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -9,7 +9,7 @@ import MastodonAPI
|
||||||
public struct ProfileService {
|
public struct ProfileService {
|
||||||
public let accountServicePublisher: AnyPublisher<AccountService, Error>
|
public let accountServicePublisher: AnyPublisher<AccountService, Error>
|
||||||
|
|
||||||
private let accountID: String
|
private let id: Account.Id
|
||||||
private let mastodonAPIClient: MastodonAPIClient
|
private let mastodonAPIClient: MastodonAPIClient
|
||||||
private let contentDatabase: ContentDatabase
|
private let contentDatabase: ContentDatabase
|
||||||
|
|
||||||
|
@ -21,20 +21,20 @@ public struct ProfileService {
|
||||||
contentDatabase: contentDatabase)
|
contentDatabase: contentDatabase)
|
||||||
}
|
}
|
||||||
|
|
||||||
init(id: String, mastodonAPIClient: MastodonAPIClient, contentDatabase: ContentDatabase) {
|
init(id: Account.Id, mastodonAPIClient: MastodonAPIClient, contentDatabase: ContentDatabase) {
|
||||||
self.init(id: id, account: nil, mastodonAPIClient: mastodonAPIClient, contentDatabase: contentDatabase)
|
self.init(id: id, account: nil, mastodonAPIClient: mastodonAPIClient, contentDatabase: contentDatabase)
|
||||||
}
|
}
|
||||||
|
|
||||||
private init(
|
private init(
|
||||||
id: String,
|
id: Account.Id,
|
||||||
account: Account?,
|
account: Account?,
|
||||||
mastodonAPIClient: MastodonAPIClient,
|
mastodonAPIClient: MastodonAPIClient,
|
||||||
contentDatabase: ContentDatabase) {
|
contentDatabase: ContentDatabase) {
|
||||||
accountID = id
|
self.id = id
|
||||||
self.mastodonAPIClient = mastodonAPIClient
|
self.mastodonAPIClient = mastodonAPIClient
|
||||||
self.contentDatabase = contentDatabase
|
self.contentDatabase = contentDatabase
|
||||||
|
|
||||||
var accountPublisher = contentDatabase.accountObservation(id: accountID)
|
var accountPublisher = contentDatabase.accountObservation(id: id)
|
||||||
|
|
||||||
if let account = account {
|
if let account = account {
|
||||||
accountPublisher = accountPublisher
|
accountPublisher = accountPublisher
|
||||||
|
@ -52,7 +52,7 @@ public struct ProfileService {
|
||||||
public extension ProfileService {
|
public extension ProfileService {
|
||||||
func timelineService(profileCollection: ProfileCollection) -> TimelineService {
|
func timelineService(profileCollection: ProfileCollection) -> TimelineService {
|
||||||
TimelineService(
|
TimelineService(
|
||||||
timeline: .profile(accountId: accountID, profileCollection: profileCollection),
|
timeline: .profile(accountId: id, profileCollection: profileCollection),
|
||||||
mastodonAPIClient: mastodonAPIClient,
|
mastodonAPIClient: mastodonAPIClient,
|
||||||
contentDatabase: contentDatabase)
|
contentDatabase: contentDatabase)
|
||||||
}
|
}
|
||||||
|
@ -60,11 +60,11 @@ public extension ProfileService {
|
||||||
func fetchPinnedStatuses() -> AnyPublisher<Never, Error> {
|
func fetchPinnedStatuses() -> AnyPublisher<Never, Error> {
|
||||||
mastodonAPIClient.request(
|
mastodonAPIClient.request(
|
||||||
StatusesEndpoint.accountsStatuses(
|
StatusesEndpoint.accountsStatuses(
|
||||||
id: accountID,
|
id: id,
|
||||||
excludeReplies: true,
|
excludeReplies: true,
|
||||||
onlyMedia: false,
|
onlyMedia: false,
|
||||||
pinned: true))
|
pinned: true))
|
||||||
.flatMap { contentDatabase.insert(pinnedStatuses: $0, accountID: accountID) }
|
.flatMap { contentDatabase.insert(pinnedStatuses: $0, accountId: id) }
|
||||||
.eraseToAnyPublisher()
|
.eraseToAnyPublisher()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -34,14 +34,14 @@ public extension StatusService {
|
||||||
|
|
||||||
func rebloggedByService() -> AccountListService {
|
func rebloggedByService() -> AccountListService {
|
||||||
AccountListService(
|
AccountListService(
|
||||||
endpoint: .statusRebloggedBy(id: status.id),
|
endpoint: .rebloggedBy(id: status.id),
|
||||||
mastodonAPIClient: mastodonAPIClient,
|
mastodonAPIClient: mastodonAPIClient,
|
||||||
contentDatabase: contentDatabase)
|
contentDatabase: contentDatabase)
|
||||||
}
|
}
|
||||||
|
|
||||||
func favoritedByService() -> AccountListService {
|
func favoritedByService() -> AccountListService {
|
||||||
AccountListService(
|
AccountListService(
|
||||||
endpoint: .statusFavouritedBy(id: status.id),
|
endpoint: .favouritedBy(id: status.id),
|
||||||
mastodonAPIClient: mastodonAPIClient,
|
mastodonAPIClient: mastodonAPIClient,
|
||||||
contentDatabase: contentDatabase)
|
contentDatabase: contentDatabase)
|
||||||
}
|
}
|
||||||
|
|
|
@ -9,14 +9,13 @@ import MastodonAPI
|
||||||
public struct TimelineService {
|
public struct TimelineService {
|
||||||
public let sections: AnyPublisher<[[CollectionItem]], Error>
|
public let sections: AnyPublisher<[[CollectionItem]], Error>
|
||||||
public let navigationService: NavigationService
|
public let navigationService: NavigationService
|
||||||
public let nextPageMaxIDs: AnyPublisher<String, Never>
|
public let nextPageMaxId: AnyPublisher<String, Never>
|
||||||
public let title: AnyPublisher<String, Never>
|
public let title: AnyPublisher<String, Never>
|
||||||
public let contextParentID: String? = nil
|
|
||||||
|
|
||||||
private let timeline: Timeline
|
private let timeline: Timeline
|
||||||
private let mastodonAPIClient: MastodonAPIClient
|
private let mastodonAPIClient: MastodonAPIClient
|
||||||
private let contentDatabase: ContentDatabase
|
private let contentDatabase: ContentDatabase
|
||||||
private let nextPageMaxIDsSubject = PassthroughSubject<String, Never>()
|
private let nextPageMaxIdSubject = PassthroughSubject<String, Never>()
|
||||||
|
|
||||||
init(timeline: Timeline, mastodonAPIClient: MastodonAPIClient, contentDatabase: ContentDatabase) {
|
init(timeline: Timeline, mastodonAPIClient: MastodonAPIClient, contentDatabase: ContentDatabase) {
|
||||||
self.timeline = timeline
|
self.timeline = timeline
|
||||||
|
@ -24,7 +23,7 @@ public struct TimelineService {
|
||||||
self.contentDatabase = contentDatabase
|
self.contentDatabase = contentDatabase
|
||||||
sections = contentDatabase.observation(timeline: timeline)
|
sections = contentDatabase.observation(timeline: timeline)
|
||||||
navigationService = NavigationService(mastodonAPIClient: mastodonAPIClient, contentDatabase: contentDatabase)
|
navigationService = NavigationService(mastodonAPIClient: mastodonAPIClient, contentDatabase: contentDatabase)
|
||||||
nextPageMaxIDs = nextPageMaxIDsSubject.eraseToAnyPublisher()
|
nextPageMaxId = nextPageMaxIdSubject.eraseToAnyPublisher()
|
||||||
|
|
||||||
if case let .tag(tag) = timeline {
|
if case let .tag(tag) = timeline {
|
||||||
title = Just("#".appending(tag)).eraseToAnyPublisher()
|
title = Just("#".appending(tag)).eraseToAnyPublisher()
|
||||||
|
@ -35,12 +34,12 @@ public struct TimelineService {
|
||||||
}
|
}
|
||||||
|
|
||||||
extension TimelineService: CollectionService {
|
extension TimelineService: CollectionService {
|
||||||
public func request(maxID: String?, minID: String?) -> AnyPublisher<Never, Error> {
|
public func request(maxId: String?, minId: String?) -> AnyPublisher<Never, Error> {
|
||||||
mastodonAPIClient.pagedRequest(timeline.endpoint, maxID: maxID, minID: minID)
|
mastodonAPIClient.pagedRequest(timeline.endpoint, maxId: maxId, minId: minId)
|
||||||
.handleEvents(receiveOutput: {
|
.handleEvents(receiveOutput: {
|
||||||
guard let maxID = $0.info.maxID else { return }
|
guard let maxId = $0.info.maxId else { return }
|
||||||
|
|
||||||
nextPageMaxIDsSubject.send(maxID)
|
nextPageMaxIdSubject.send(maxId)
|
||||||
})
|
})
|
||||||
.flatMap { contentDatabase.insert(statuses: $0.result, timeline: timeline) }
|
.flatMap { contentDatabase.insert(statuses: $0.result, timeline: timeline) }
|
||||||
.eraseToAnyPublisher()
|
.eraseToAnyPublisher()
|
||||||
|
|
|
@ -42,7 +42,7 @@ class InstanceURLServiceTests: XCTestCase {
|
||||||
updatedFilter.insert("instance.filtered")
|
updatedFilter.insert("instance.filtered")
|
||||||
|
|
||||||
let updatedFilterData = try JSONEncoder().encode(updatedFilter)
|
let updatedFilterData = try JSONEncoder().encode(updatedFilter)
|
||||||
let stub: HTTPStub = .success((URLResponse(), updatedFilterData))
|
let stub: HTTPStub = .success((HTTPURLResponse(), updatedFilterData))
|
||||||
|
|
||||||
StubbingURLProtocol.setStub(stub, forURL: URL(string: "https://filter.metabolist.com/filter")!)
|
StubbingURLProtocol.setStub(stub, forURL: URL(string: "https://filter.metabolist.com/filter")!)
|
||||||
|
|
||||||
|
|
|
@ -74,7 +74,7 @@ class TableViewController: UITableViewController {
|
||||||
override func viewWillAppear(_ animated: Bool) {
|
override func viewWillAppear(_ animated: Bool) {
|
||||||
super.viewWillAppear(animated)
|
super.viewWillAppear(animated)
|
||||||
|
|
||||||
viewModel.request(maxID: nil, minID: nil)
|
viewModel.request(maxId: nil, minId: nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
override func scrollViewDidScroll(_ scrollView: UIScrollView) {
|
override func scrollViewDidScroll(_ scrollView: UIScrollView) {
|
||||||
|
@ -130,13 +130,13 @@ class TableViewController: UITableViewController {
|
||||||
extension TableViewController: UITableViewDataSourcePrefetching {
|
extension TableViewController: UITableViewDataSourcePrefetching {
|
||||||
func tableView(_ tableView: UITableView, prefetchRowsAt indexPaths: [IndexPath]) {
|
func tableView(_ tableView: UITableView, prefetchRowsAt indexPaths: [IndexPath]) {
|
||||||
guard
|
guard
|
||||||
let maxID = viewModel.nextPageMaxID,
|
let maxId = viewModel.nextPageMaxId,
|
||||||
let indexPath = indexPaths.last,
|
let indexPath = indexPaths.last,
|
||||||
indexPath.section == dataSource.numberOfSections(in: tableView) - 1,
|
indexPath.section == dataSource.numberOfSections(in: tableView) - 1,
|
||||||
indexPath.row == dataSource.tableView(tableView, numberOfRowsInSection: indexPath.section) - 1
|
indexPath.row == dataSource.tableView(tableView, numberOfRowsInSection: indexPath.section) - 1
|
||||||
else { return }
|
else { return }
|
||||||
|
|
||||||
viewModel.request(maxID: maxID, minID: nil)
|
viewModel.request(maxId: maxId, minId: nil)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -14,9 +14,9 @@ import ViewModels
|
||||||
// swiftlint:disable force_try
|
// swiftlint:disable force_try
|
||||||
|
|
||||||
let db: IdentityDatabase = {
|
let db: IdentityDatabase = {
|
||||||
let id = UUID()
|
let id = Identity.Id()
|
||||||
let db = try! IdentityDatabase(inMemory: true, keychain: MockKeychain.self)
|
let db = try! IdentityDatabase(inMemory: true, keychain: MockKeychain.self)
|
||||||
let secrets = Secrets(identityID: id, keychain: MockKeychain.self)
|
let secrets = Secrets(identityId: id, keychain: MockKeychain.self)
|
||||||
|
|
||||||
try! secrets.setInstanceURL(.previewInstanceURL)
|
try! secrets.setInstanceURL(.previewInstanceURL)
|
||||||
try! secrets.setAccessToken(UUID().uuidString)
|
try! secrets.setAccessToken(UUID().uuidString)
|
||||||
|
@ -25,11 +25,11 @@ let db: IdentityDatabase = {
|
||||||
.receive(on: ImmediateScheduler.shared)
|
.receive(on: ImmediateScheduler.shared)
|
||||||
.sink { _ in } receiveValue: { _ in }
|
.sink { _ in } receiveValue: { _ in }
|
||||||
|
|
||||||
_ = db.updateInstance(.preview, forIdentityID: id)
|
_ = db.updateInstance(.preview, id: id)
|
||||||
.receive(on: ImmediateScheduler.shared)
|
.receive(on: ImmediateScheduler.shared)
|
||||||
.sink { _ in } receiveValue: { _ in }
|
.sink { _ in } receiveValue: { _ in }
|
||||||
|
|
||||||
_ = db.updateAccount(.preview, forIdentityID: id)
|
_ = db.updateAccount(.preview, id: id)
|
||||||
.receive(on: ImmediateScheduler.shared)
|
.receive(on: ImmediateScheduler.shared)
|
||||||
.sink { _ in } receiveValue: { _ in }
|
.sink { _ in } receiveValue: { _ in }
|
||||||
|
|
||||||
|
|
|
@ -7,7 +7,7 @@ import ServiceLayer
|
||||||
|
|
||||||
final public class CollectionItemsViewModel: ObservableObject {
|
final public class CollectionItemsViewModel: ObservableObject {
|
||||||
@Published public var alertItem: AlertItem?
|
@Published public var alertItem: AlertItem?
|
||||||
public private(set) var nextPageMaxID: String?
|
public private(set) var nextPageMaxId: String?
|
||||||
public private(set) var maintainScrollPositionOfItem: CollectionItemIdentifier?
|
public private(set) var maintainScrollPositionOfItem: CollectionItemIdentifier?
|
||||||
|
|
||||||
private let items = CurrentValueSubject<[[CollectionItem]], Never>([])
|
private let items = CurrentValueSubject<[[CollectionItem]], Never>([])
|
||||||
|
@ -27,8 +27,8 @@ final public class CollectionItemsViewModel: ObservableObject {
|
||||||
.sink { _ in }
|
.sink { _ in }
|
||||||
.store(in: &cancellables)
|
.store(in: &cancellables)
|
||||||
|
|
||||||
collectionService.nextPageMaxIDs
|
collectionService.nextPageMaxId
|
||||||
.sink { [weak self] in self?.nextPageMaxID = $0 }
|
.sink { [weak self] in self?.nextPageMaxId = $0 }
|
||||||
.store(in: &cancellables)
|
.store(in: &cancellables)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -46,8 +46,8 @@ extension CollectionItemsViewModel: CollectionViewModel {
|
||||||
|
|
||||||
public var navigationEvents: AnyPublisher<NavigationEvent, Never> { navigationEventsSubject.eraseToAnyPublisher() }
|
public var navigationEvents: AnyPublisher<NavigationEvent, Never> { navigationEventsSubject.eraseToAnyPublisher() }
|
||||||
|
|
||||||
public func request(maxID: String? = nil, minID: String? = nil) {
|
public func request(maxId: String? = nil, minId: String? = nil) {
|
||||||
collectionService.request(maxID: maxID, minID: minID)
|
collectionService.request(maxId: maxId, minId: minId)
|
||||||
.receive(on: DispatchQueue.main)
|
.receive(on: DispatchQueue.main)
|
||||||
.assignErrorsToAlertItem(to: \.alertItem, on: self)
|
.assignErrorsToAlertItem(to: \.alertItem, on: self)
|
||||||
.handleEvents(
|
.handleEvents(
|
||||||
|
@ -80,7 +80,7 @@ extension CollectionItemsViewModel: CollectionViewModel {
|
||||||
|
|
||||||
public func canSelect(indexPath: IndexPath) -> Bool {
|
public func canSelect(indexPath: IndexPath) -> Bool {
|
||||||
if case let .status(configuration) = items.value[indexPath.section][indexPath.item],
|
if case let .status(configuration) = items.value[indexPath.section][indexPath.item],
|
||||||
configuration.status.id == collectionService.contextParentID {
|
configuration.status.id == collectionService.contextParentId {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -102,7 +102,7 @@ extension CollectionItemsViewModel: CollectionViewModel {
|
||||||
cache(viewModel: viewModel, forItem: item)
|
cache(viewModel: viewModel, forItem: item)
|
||||||
}
|
}
|
||||||
|
|
||||||
viewModel.isContextParent = configuration.status.id == collectionService.contextParentID
|
viewModel.isContextParent = configuration.status.id == collectionService.contextParentId
|
||||||
viewModel.isPinned = configuration.pinned
|
viewModel.isPinned = configuration.pinned
|
||||||
viewModel.isReplyInContext = configuration.isReplyInContext
|
viewModel.isReplyInContext = configuration.isReplyInContext
|
||||||
viewModel.hasReplyFollowing = configuration.hasReplyFollowing
|
viewModel.hasReplyFollowing = configuration.hasReplyFollowing
|
||||||
|
@ -154,12 +154,12 @@ private extension CollectionItemsViewModel {
|
||||||
maintainScrollPositionOfItem = nil // clear old value
|
maintainScrollPositionOfItem = nil // clear old value
|
||||||
|
|
||||||
// Maintain scroll position of parent after initial load of context
|
// Maintain scroll position of parent after initial load of context
|
||||||
if let contextParentID = collectionService.contextParentID {
|
if let contextParentId = collectionService.contextParentId {
|
||||||
let contextParentIdentifier = CollectionItemIdentifier(id: contextParentID, kind: .status, info: [:])
|
let contextParentIdentifier = CollectionItemIdentifier(id: contextParentId, kind: .status, info: [:])
|
||||||
let onlyContextParentID = [[], [contextParentIdentifier], []]
|
let onlyContextParentId = [[], [contextParentIdentifier], []]
|
||||||
|
|
||||||
if items.value.isEmpty
|
if items.value.isEmpty
|
||||||
|| items.value.map({ $0.map(CollectionItemIdentifier.init(item:)) }) == onlyContextParentID {
|
|| items.value.map({ $0.map(CollectionItemIdentifier.init(item:)) }) == onlyContextParentId {
|
||||||
maintainScrollPositionOfItem = contextParentIdentifier
|
maintainScrollPositionOfItem = contextParentIdentifier
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -9,9 +9,9 @@ public protocol CollectionViewModel {
|
||||||
var alertItems: AnyPublisher<AlertItem, Never> { get }
|
var alertItems: AnyPublisher<AlertItem, Never> { get }
|
||||||
var loading: AnyPublisher<Bool, Never> { get }
|
var loading: AnyPublisher<Bool, Never> { get }
|
||||||
var navigationEvents: AnyPublisher<NavigationEvent, Never> { get }
|
var navigationEvents: AnyPublisher<NavigationEvent, Never> { get }
|
||||||
var nextPageMaxID: String? { get }
|
var nextPageMaxId: String? { get }
|
||||||
var maintainScrollPositionOfItem: CollectionItemIdentifier? { get }
|
var maintainScrollPositionOfItem: CollectionItemIdentifier? { get }
|
||||||
func request(maxID: String?, minID: String?)
|
func request(maxId: String?, minId: String?)
|
||||||
func select(indexPath: IndexPath)
|
func select(indexPath: IndexPath)
|
||||||
func canSelect(indexPath: IndexPath) -> Bool
|
func canSelect(indexPath: IndexPath) -> Bool
|
||||||
func viewModel(indexPath: IndexPath) -> CollectionItemViewModel
|
func viewModel(indexPath: IndexPath) -> CollectionItemViewModel
|
||||||
|
|
|
@ -28,7 +28,7 @@ public final class EditFilterViewModel: ObservableObject {
|
||||||
}
|
}
|
||||||
|
|
||||||
public extension EditFilterViewModel {
|
public extension EditFilterViewModel {
|
||||||
var isNew: Bool { filter.id == Filter.newFilterID }
|
var isNew: Bool { filter.id == Filter.newFilterId }
|
||||||
|
|
||||||
var isSaveDisabled: Bool { filter.phrase == "" || filter.context.isEmpty }
|
var isSaveDisabled: Bool { filter.phrase == "" || filter.context.isEmpty }
|
||||||
|
|
||||||
|
|
|
@ -5,7 +5,7 @@ import Foundation
|
||||||
import ServiceLayer
|
import ServiceLayer
|
||||||
|
|
||||||
public final class IdentitiesViewModel: ObservableObject {
|
public final class IdentitiesViewModel: ObservableObject {
|
||||||
public let currentIdentityID: UUID
|
public let currentIdentityId: Identity.Id
|
||||||
@Published public var authenticated = [Identity]()
|
@Published public var authenticated = [Identity]()
|
||||||
@Published public var unauthenticated = [Identity]()
|
@Published public var unauthenticated = [Identity]()
|
||||||
@Published public var pending = [Identity]()
|
@Published public var pending = [Identity]()
|
||||||
|
@ -16,7 +16,7 @@ public final class IdentitiesViewModel: ObservableObject {
|
||||||
|
|
||||||
public init(identification: Identification) {
|
public init(identification: Identification) {
|
||||||
self.identification = identification
|
self.identification = identification
|
||||||
currentIdentityID = identification.identity.id
|
currentIdentityId = identification.identity.id
|
||||||
|
|
||||||
let observation = identification.service.identitiesObservation()
|
let observation = identification.service.identitiesObservation()
|
||||||
.assignErrorsToAlertItem(to: \.alertItem, on: self)
|
.assignErrorsToAlertItem(to: \.alertItem, on: self)
|
||||||
|
|
|
@ -66,23 +66,23 @@ extension ProfileViewModel: CollectionViewModel {
|
||||||
.eraseToAnyPublisher()
|
.eraseToAnyPublisher()
|
||||||
}
|
}
|
||||||
|
|
||||||
public var nextPageMaxID: String? {
|
public var nextPageMaxId: String? {
|
||||||
collectionViewModel.value.nextPageMaxID
|
collectionViewModel.value.nextPageMaxId
|
||||||
}
|
}
|
||||||
|
|
||||||
public var maintainScrollPositionOfItem: CollectionItemIdentifier? {
|
public var maintainScrollPositionOfItem: CollectionItemIdentifier? {
|
||||||
collectionViewModel.value.maintainScrollPositionOfItem
|
collectionViewModel.value.maintainScrollPositionOfItem
|
||||||
}
|
}
|
||||||
|
|
||||||
public func request(maxID: String?, minID: String?) {
|
public func request(maxId: String?, minId: String?) {
|
||||||
if case .statuses = collection, maxID == nil {
|
if case .statuses = collection, maxId == nil {
|
||||||
profileService.fetchPinnedStatuses()
|
profileService.fetchPinnedStatuses()
|
||||||
.assignErrorsToAlertItem(to: \.alertItem, on: self)
|
.assignErrorsToAlertItem(to: \.alertItem, on: self)
|
||||||
.sink { _ in }
|
.sink { _ in }
|
||||||
.store(in: &cancellables)
|
.store(in: &cancellables)
|
||||||
}
|
}
|
||||||
|
|
||||||
collectionViewModel.value.request(maxID: maxID, minID: minID)
|
collectionViewModel.value.request(maxId: maxId, minId: minId)
|
||||||
}
|
}
|
||||||
|
|
||||||
public func select(indexPath: IndexPath) {
|
public func select(indexPath: IndexPath) {
|
||||||
|
|
|
@ -7,7 +7,7 @@ import ServiceLayer
|
||||||
public final class RootViewModel: ObservableObject {
|
public final class RootViewModel: ObservableObject {
|
||||||
@Published public private(set) var navigationViewModel: NavigationViewModel?
|
@Published public private(set) var navigationViewModel: NavigationViewModel?
|
||||||
|
|
||||||
@Published private var mostRecentlyUsedIdentityID: UUID?
|
@Published private var mostRecentlyUsedIdentityId: Identity.Id?
|
||||||
private let environment: AppEnvironment
|
private let environment: AppEnvironment
|
||||||
private let allIdentitiesService: AllIdentitiesService
|
private let allIdentitiesService: AllIdentitiesService
|
||||||
private let userNotificationService: UserNotificationService
|
private let userNotificationService: UserNotificationService
|
||||||
|
@ -21,11 +21,11 @@ public final class RootViewModel: ObservableObject {
|
||||||
userNotificationService = UserNotificationService(environment: environment)
|
userNotificationService = UserNotificationService(environment: environment)
|
||||||
self.registerForRemoteNotifications = registerForRemoteNotifications
|
self.registerForRemoteNotifications = registerForRemoteNotifications
|
||||||
|
|
||||||
allIdentitiesService.immediateMostRecentlyUsedIdentityIDObservation()
|
allIdentitiesService.immediateMostRecentlyUsedIdentityIdObservation()
|
||||||
.replaceError(with: nil)
|
.replaceError(with: nil)
|
||||||
.assign(to: &$mostRecentlyUsedIdentityID)
|
.assign(to: &$mostRecentlyUsedIdentityId)
|
||||||
|
|
||||||
identitySelected(id: mostRecentlyUsedIdentityID, immediate: true)
|
identitySelected(id: mostRecentlyUsedIdentityId, immediate: true)
|
||||||
|
|
||||||
allIdentitiesService.identitiesCreated
|
allIdentitiesService.identitiesCreated
|
||||||
.sink { [weak self] in self?.identitySelected(id: $0) }
|
.sink { [weak self] in self?.identitySelected(id: $0) }
|
||||||
|
@ -42,11 +42,11 @@ public final class RootViewModel: ObservableObject {
|
||||||
}
|
}
|
||||||
|
|
||||||
public extension RootViewModel {
|
public extension RootViewModel {
|
||||||
func identitySelected(id: UUID?) {
|
func identitySelected(id: Identity.Id?) {
|
||||||
identitySelected(id: id, immediate: false)
|
identitySelected(id: id, immediate: false)
|
||||||
}
|
}
|
||||||
|
|
||||||
func deleteIdentity(id: UUID) {
|
func deleteIdentity(id: Identity.Id) {
|
||||||
allIdentitiesService.deleteIdentity(id: id)
|
allIdentitiesService.deleteIdentity(id: id)
|
||||||
.sink { _ in } receiveValue: { _ in }
|
.sink { _ in } receiveValue: { _ in }
|
||||||
.store(in: &cancellables)
|
.store(in: &cancellables)
|
||||||
|
@ -60,7 +60,7 @@ public extension RootViewModel {
|
||||||
}
|
}
|
||||||
|
|
||||||
private extension RootViewModel {
|
private extension RootViewModel {
|
||||||
func identitySelected(id: UUID?, immediate: Bool) {
|
func identitySelected(id: Identity.Id?, immediate: Bool) {
|
||||||
navigationViewModel?.presentingSecondaryNavigation = false
|
navigationViewModel?.presentingSecondaryNavigation = false
|
||||||
|
|
||||||
guard
|
guard
|
||||||
|
@ -74,7 +74,7 @@ private extension RootViewModel {
|
||||||
let observation = identityService.observation(immediate: immediate)
|
let observation = identityService.observation(immediate: immediate)
|
||||||
.catch { [weak self] _ -> Empty<Identity, Never> in
|
.catch { [weak self] _ -> Empty<Identity, Never> in
|
||||||
DispatchQueue.main.async {
|
DispatchQueue.main.async {
|
||||||
self?.identitySelected(id: self?.mostRecentlyUsedIdentityID, immediate: false)
|
self?.identitySelected(id: self?.mostRecentlyUsedIdentityId, immediate: false)
|
||||||
}
|
}
|
||||||
|
|
||||||
return Empty()
|
return Empty()
|
||||||
|
|
|
@ -18,12 +18,12 @@ class AddIdentityViewModelTests: XCTestCase {
|
||||||
let sut = AddIdentityViewModel(
|
let sut = AddIdentityViewModel(
|
||||||
allIdentitiesService: allIdentitiesService,
|
allIdentitiesService: allIdentitiesService,
|
||||||
instanceURLService: InstanceURLService(environment: environment))
|
instanceURLService: InstanceURLService(environment: environment))
|
||||||
let addedIDRecorder = allIdentitiesService.identitiesCreated.record()
|
let addedIdRecorder = allIdentitiesService.identitiesCreated.record()
|
||||||
|
|
||||||
sut.urlFieldText = "https://mastodon.social"
|
sut.urlFieldText = "https://mastodon.social"
|
||||||
sut.logInTapped()
|
sut.logInTapped()
|
||||||
|
|
||||||
_ = try wait(for: addedIDRecorder.next(), timeout: 1)
|
_ = try wait(for: addedIdRecorder.next(), timeout: 1)
|
||||||
}
|
}
|
||||||
|
|
||||||
func testAddIdentityWithoutScheme() throws {
|
func testAddIdentityWithoutScheme() throws {
|
||||||
|
@ -33,12 +33,12 @@ class AddIdentityViewModelTests: XCTestCase {
|
||||||
let sut = AddIdentityViewModel(
|
let sut = AddIdentityViewModel(
|
||||||
allIdentitiesService: allIdentitiesService,
|
allIdentitiesService: allIdentitiesService,
|
||||||
instanceURLService: InstanceURLService(environment: environment))
|
instanceURLService: InstanceURLService(environment: environment))
|
||||||
let addedIDRecorder = allIdentitiesService.identitiesCreated.record()
|
let addedIdRecorder = allIdentitiesService.identitiesCreated.record()
|
||||||
|
|
||||||
sut.urlFieldText = "mastodon.social"
|
sut.urlFieldText = "mastodon.social"
|
||||||
sut.logInTapped()
|
sut.logInTapped()
|
||||||
|
|
||||||
_ = try wait(for: addedIDRecorder.next(), timeout: 1)
|
_ = try wait(for: addedIdRecorder.next(), timeout: 1)
|
||||||
}
|
}
|
||||||
|
|
||||||
func testInvalidURL() throws {
|
func testInvalidURL() throws {
|
||||||
|
|
|
@ -81,7 +81,7 @@ private extension AccountHeaderView {
|
||||||
segmentedControl.insertSegment(
|
segmentedControl.insertSegment(
|
||||||
action: UIAction(title: collection.title) { [weak self] _ in
|
action: UIAction(title: collection.title) { [weak self] _ in
|
||||||
self?.viewModel?.collection = collection
|
self?.viewModel?.collection = collection
|
||||||
self?.viewModel?.request(maxID: nil, minID: nil)
|
self?.viewModel?.request(maxId: nil, minId: nil)
|
||||||
},
|
},
|
||||||
at: index,
|
at: index,
|
||||||
animated: false)
|
animated: false)
|
||||||
|
|
|
@ -47,7 +47,7 @@ private extension IdentitiesView {
|
||||||
} label: {
|
} label: {
|
||||||
row(identity: identity)
|
row(identity: identity)
|
||||||
}
|
}
|
||||||
.disabled(identity.id == viewModel.currentIdentityID)
|
.disabled(identity.id == viewModel.currentIdentityId)
|
||||||
.buttonStyle(PlainButtonStyle())
|
.buttonStyle(PlainButtonStyle())
|
||||||
}
|
}
|
||||||
.onDelete {
|
.onDelete {
|
||||||
|
@ -94,7 +94,7 @@ private extension IdentitiesView {
|
||||||
Spacer()
|
Spacer()
|
||||||
}
|
}
|
||||||
Spacer()
|
Spacer()
|
||||||
if identity.id == viewModel.currentIdentityID {
|
if identity.id == viewModel.currentIdentityId {
|
||||||
Image(systemName: "checkmark.circle")
|
Image(systemName: "checkmark.circle")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue