mirror of
https://github.com/metabolist/metatext.git
synced 2024-11-22 00:01:00 +00:00
Startup and syncing
This commit is contained in:
parent
c7e4186749
commit
ed82cffd91
24 changed files with 427 additions and 43 deletions
|
@ -105,6 +105,11 @@ extension ContentDatabase {
|
||||||
t.column("wholeWord", .boolean).notNull()
|
t.column("wholeWord", .boolean).notNull()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
try db.create(table: "lastReadIdRecord") { t in
|
||||||
|
t.column("markerTimeline", .text).primaryKey(onConflict: .replace)
|
||||||
|
t.column("id", .text).notNull()
|
||||||
|
}
|
||||||
|
|
||||||
try db.create(table: "statusAncestorJoin") { t in
|
try db.create(table: "statusAncestorJoin") { t in
|
||||||
t.column("parentId", .text).indexed().notNull()
|
t.column("parentId", .text).indexed().notNull()
|
||||||
.references("statusRecord", onDelete: .cascade)
|
.references("statusRecord", onDelete: .cascade)
|
||||||
|
|
|
@ -7,12 +7,17 @@ import Keychain
|
||||||
import Mastodon
|
import Mastodon
|
||||||
import Secrets
|
import Secrets
|
||||||
|
|
||||||
|
// swiftlint:disable file_length
|
||||||
public struct ContentDatabase {
|
public struct ContentDatabase {
|
||||||
public let activeFiltersPublisher: AnyPublisher<[Filter], Error>
|
public let activeFiltersPublisher: AnyPublisher<[Filter], Error>
|
||||||
|
|
||||||
private let databaseWriter: DatabaseWriter
|
private let databaseWriter: DatabaseWriter
|
||||||
|
|
||||||
public init(id: Identity.Id, inMemory: Bool, keychain: Keychain.Type) throws {
|
public init(id: Identity.Id,
|
||||||
|
useHomeTimelineLastReadId: Bool,
|
||||||
|
useNotificationsLastReadId: Bool,
|
||||||
|
inMemory: Bool,
|
||||||
|
keychain: Keychain.Type) throws {
|
||||||
if inMemory {
|
if inMemory {
|
||||||
databaseWriter = DatabaseQueue()
|
databaseWriter = DatabaseQueue()
|
||||||
} else {
|
} else {
|
||||||
|
@ -27,7 +32,10 @@ public struct ContentDatabase {
|
||||||
}
|
}
|
||||||
|
|
||||||
try Self.migrator.migrate(databaseWriter)
|
try Self.migrator.migrate(databaseWriter)
|
||||||
try Self.clean(databaseWriter)
|
try Self.clean(
|
||||||
|
databaseWriter,
|
||||||
|
useHomeTimelineLastReadId: useHomeTimelineLastReadId,
|
||||||
|
useNotificationsLastReadId: useNotificationsLastReadId)
|
||||||
|
|
||||||
activeFiltersPublisher = ValueObservation.tracking {
|
activeFiltersPublisher = ValueObservation.tracking {
|
||||||
try Filter.filter(Filter.Columns.expiresAt == nil || Filter.Columns.expiresAt > Date()).fetchAll($0)
|
try Filter.filter(Filter.Columns.expiresAt == nil || Filter.Columns.expiresAt > Date()).fetchAll($0)
|
||||||
|
@ -278,6 +286,12 @@ public extension ContentDatabase {
|
||||||
.eraseToAnyPublisher()
|
.eraseToAnyPublisher()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func setLastReadId(_ id: String, markerTimeline: Marker.Timeline) -> AnyPublisher<Never, Error> {
|
||||||
|
databaseWriter.writePublisher(updates: LastReadIdRecord(markerTimeline: markerTimeline, id: id).save)
|
||||||
|
.ignoreOutput()
|
||||||
|
.eraseToAnyPublisher()
|
||||||
|
}
|
||||||
|
|
||||||
func timelinePublisher(_ timeline: Timeline) -> AnyPublisher<[[CollectionItem]], Error> {
|
func timelinePublisher(_ timeline: Timeline) -> AnyPublisher<[[CollectionItem]], Error> {
|
||||||
ValueObservation.tracking(
|
ValueObservation.tracking(
|
||||||
TimelineItemsInfo.request(TimelineRecord.filter(TimelineRecord.Columns.id == timeline.id)).fetchOne)
|
TimelineItemsInfo.request(TimelineRecord.filter(TimelineRecord.Columns.id == timeline.id)).fetchOne)
|
||||||
|
@ -331,19 +345,64 @@ public extension ContentDatabase {
|
||||||
.publisher(in: databaseWriter)
|
.publisher(in: databaseWriter)
|
||||||
.eraseToAnyPublisher()
|
.eraseToAnyPublisher()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func lastReadId(_ markerTimeline: Marker.Timeline) -> String? {
|
||||||
|
try? databaseWriter.read {
|
||||||
|
try String.fetchOne(
|
||||||
|
$0,
|
||||||
|
LastReadIdRecord.filter(LastReadIdRecord.Columns.markerTimeline == markerTimeline.rawValue)
|
||||||
|
.select(LastReadIdRecord.Columns.id))
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private extension ContentDatabase {
|
private extension ContentDatabase {
|
||||||
|
static let cleanAfterLastReadIdCount = 40
|
||||||
static func fileURL(id: Identity.Id) throws -> URL {
|
static func fileURL(id: Identity.Id) throws -> URL {
|
||||||
try FileManager.default.databaseDirectoryURL(name: id.uuidString)
|
try FileManager.default.databaseDirectoryURL(name: id.uuidString)
|
||||||
}
|
}
|
||||||
|
|
||||||
static func clean(_ databaseWriter: DatabaseWriter) throws {
|
static func clean(_ databaseWriter: DatabaseWriter,
|
||||||
|
useHomeTimelineLastReadId: Bool,
|
||||||
|
useNotificationsLastReadId: Bool) throws {
|
||||||
try databaseWriter.write {
|
try databaseWriter.write {
|
||||||
try TimelineRecord.deleteAll($0)
|
if useHomeTimelineLastReadId {
|
||||||
try StatusRecord.deleteAll($0)
|
try TimelineRecord.filter(TimelineRecord.Columns.id != Timeline.home.id).deleteAll($0)
|
||||||
try AccountRecord.deleteAll($0)
|
var statusIds = try Status.Id.fetchAll(
|
||||||
|
$0,
|
||||||
|
TimelineStatusJoin.select(TimelineStatusJoin.Columns.statusId)
|
||||||
|
.order(TimelineStatusJoin.Columns.statusId.desc))
|
||||||
|
|
||||||
|
if let lastReadId = try Status.Id.fetchOne(
|
||||||
|
$0,
|
||||||
|
LastReadIdRecord.filter(LastReadIdRecord.Columns.markerTimeline == Marker.Timeline.home.rawValue)
|
||||||
|
.select(LastReadIdRecord.Columns.id))
|
||||||
|
?? statusIds.first,
|
||||||
|
let index = statusIds.firstIndex(of: lastReadId) {
|
||||||
|
statusIds = Array(statusIds.prefix(index + Self.cleanAfterLastReadIdCount))
|
||||||
|
}
|
||||||
|
|
||||||
|
statusIds += try Status.Id.fetchAll(
|
||||||
|
$0,
|
||||||
|
StatusRecord.filter(statusIds.contains(StatusRecord.Columns.id)
|
||||||
|
&& StatusRecord.Columns.reblogId != nil)
|
||||||
|
.select(StatusRecord.Columns.reblogId))
|
||||||
|
try StatusRecord.filter(!statusIds.contains(StatusRecord.Columns.id) ).deleteAll($0)
|
||||||
|
var accountIds = try Account.Id.fetchAll($0, StatusRecord.select(StatusRecord.Columns.accountId))
|
||||||
|
accountIds += try Account.Id.fetchAll(
|
||||||
|
$0,
|
||||||
|
AccountRecord.filter(accountIds.contains(AccountRecord.Columns.id)
|
||||||
|
&& AccountRecord.Columns.movedId != nil)
|
||||||
|
.select(AccountRecord.Columns.movedId))
|
||||||
|
try AccountRecord.filter(!accountIds.contains(AccountRecord.Columns.id)).deleteAll($0)
|
||||||
|
} else {
|
||||||
|
try TimelineRecord.deleteAll($0)
|
||||||
|
try StatusRecord.deleteAll($0)
|
||||||
|
try AccountRecord.deleteAll($0)
|
||||||
|
}
|
||||||
|
|
||||||
try AccountList.deleteAll($0)
|
try AccountList.deleteAll($0)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// swiftlint:enable file_length
|
||||||
|
|
17
DB/Sources/DB/Content/LastReadIdRecord.swift
Normal file
17
DB/Sources/DB/Content/LastReadIdRecord.swift
Normal file
|
@ -0,0 +1,17 @@
|
||||||
|
// Copyright © 2020 Metabolist. All rights reserved.
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
import GRDB
|
||||||
|
import Mastodon
|
||||||
|
|
||||||
|
struct LastReadIdRecord: ContentDatabaseRecord, Hashable {
|
||||||
|
let markerTimeline: Marker.Timeline
|
||||||
|
let id: String
|
||||||
|
}
|
||||||
|
|
||||||
|
extension LastReadIdRecord {
|
||||||
|
enum Columns {
|
||||||
|
static let markerTimeline = Column(LastReadIdRecord.CodingKeys.markerTimeline)
|
||||||
|
static let id = Column(LastReadIdRecord.CodingKeys.id)
|
||||||
|
}
|
||||||
|
}
|
|
@ -6,7 +6,7 @@ public protocol Target {
|
||||||
var baseURL: URL { get }
|
var baseURL: URL { get }
|
||||||
var pathComponents: [String] { get }
|
var pathComponents: [String] { get }
|
||||||
var method: HTTPMethod { get }
|
var method: HTTPMethod { get }
|
||||||
var queryParameters: [String: String]? { get }
|
var queryParameters: [URLQueryItem] { get }
|
||||||
var jsonBody: [String: Any]? { get }
|
var jsonBody: [String: Any]? { get }
|
||||||
var headers: [String: String]? { get }
|
var headers: [String: String]? { get }
|
||||||
}
|
}
|
||||||
|
@ -19,9 +19,8 @@ public extension Target {
|
||||||
url.appendPathComponent(pathComponent)
|
url.appendPathComponent(pathComponent)
|
||||||
}
|
}
|
||||||
|
|
||||||
if var components = URLComponents(url: url, resolvingAgainstBaseURL: true),
|
if var components = URLComponents(url: url, resolvingAgainstBaseURL: true), !queryParameters.isEmpty {
|
||||||
let queryItems = queryParameters?.map(URLQueryItem.init(name:value:)) {
|
components.queryItems = queryParameters
|
||||||
components.queryItems = queryItems
|
|
||||||
|
|
||||||
if let queryComponentURL = components.url {
|
if let queryComponentURL = components.url {
|
||||||
url = queryComponentURL
|
url = queryComponentURL
|
||||||
|
|
|
@ -67,6 +67,13 @@
|
||||||
"preferences.notification-types.reblog" = "Reblog";
|
"preferences.notification-types.reblog" = "Reblog";
|
||||||
"preferences.notification-types.mention" = "Mention";
|
"preferences.notification-types.mention" = "Mention";
|
||||||
"preferences.notification-types.poll" = "Poll";
|
"preferences.notification-types.poll" = "Poll";
|
||||||
|
"preferences.startup-and-syncing" = "Startup and Syncing";
|
||||||
|
"preferences.startup-and-syncing.home-timeline" = "Home timeline";
|
||||||
|
"preferences.startup-and-syncing.notifications-tab" = "Notifications tab";
|
||||||
|
"preferences.startup-and-syncing.position-on-startup" = "Position on startup";
|
||||||
|
"preferences.startup-and-syncing.remember-position" = "Remember position";
|
||||||
|
"preferences.startup-and-syncing.sync-position" = "Sync position with web and other devices";
|
||||||
|
"preferences.startup-and-syncing.newest" = "Load newest";
|
||||||
"filters.active" = "Active";
|
"filters.active" = "Active";
|
||||||
"filters.expired" = "Expired";
|
"filters.expired" = "Expired";
|
||||||
"filter.add-new" = "Add New Filter";
|
"filter.add-new" = "Add New Filter";
|
||||||
|
|
16
Mastodon/Sources/Mastodon/Entities/Marker.swift
Normal file
16
Mastodon/Sources/Mastodon/Entities/Marker.swift
Normal file
|
@ -0,0 +1,16 @@
|
||||||
|
// Copyright © 2020 Metabolist. All rights reserved.
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
|
||||||
|
public struct Marker: Codable, Hashable {
|
||||||
|
public let lastReadId: String
|
||||||
|
public let updatedAt: Date
|
||||||
|
public let version: Int
|
||||||
|
}
|
||||||
|
|
||||||
|
public extension Marker {
|
||||||
|
enum Timeline: String, Codable {
|
||||||
|
case home
|
||||||
|
case notifications
|
||||||
|
}
|
||||||
|
}
|
|
@ -9,7 +9,7 @@ public protocol Endpoint {
|
||||||
var context: [String] { get }
|
var context: [String] { get }
|
||||||
var pathComponentsInContext: [String] { get }
|
var pathComponentsInContext: [String] { get }
|
||||||
var method: HTTPMethod { get }
|
var method: HTTPMethod { get }
|
||||||
var queryParameters: [String: String]? { get }
|
var queryParameters: [URLQueryItem] { get }
|
||||||
var jsonBody: [String: Any]? { get }
|
var jsonBody: [String: Any]? { get }
|
||||||
var headers: [String: String]? { get }
|
var headers: [String: String]? { get }
|
||||||
}
|
}
|
||||||
|
@ -29,7 +29,7 @@ public extension Endpoint {
|
||||||
context + pathComponentsInContext
|
context + pathComponentsInContext
|
||||||
}
|
}
|
||||||
|
|
||||||
var queryParameters: [String: String]? { nil }
|
var queryParameters: [URLQueryItem] { [] }
|
||||||
|
|
||||||
var jsonBody: [String: Any]? { nil }
|
var jsonBody: [String: Any]? { nil }
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,45 @@
|
||||||
|
// Copyright © 2020 Metabolist. All rights reserved.
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
import HTTP
|
||||||
|
import Mastodon
|
||||||
|
|
||||||
|
public enum MarkersEndpoint {
|
||||||
|
case get(Set<Marker.Timeline>)
|
||||||
|
case post([Marker.Timeline: String])
|
||||||
|
}
|
||||||
|
|
||||||
|
extension MarkersEndpoint: Endpoint {
|
||||||
|
public typealias ResultType = [String: Marker]
|
||||||
|
|
||||||
|
public var pathComponentsInContext: [String] {
|
||||||
|
["markers"]
|
||||||
|
}
|
||||||
|
|
||||||
|
public var queryParameters: [URLQueryItem] {
|
||||||
|
switch self {
|
||||||
|
case let .get(timelines):
|
||||||
|
return Array(timelines).map { URLQueryItem(name: "timeline[]", value: $0.rawValue) }
|
||||||
|
case .post:
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public var jsonBody: [String: Any]? {
|
||||||
|
switch self {
|
||||||
|
case .get:
|
||||||
|
return nil
|
||||||
|
case let .post(lastReadIds):
|
||||||
|
return Dictionary(uniqueKeysWithValues: lastReadIds.map { ($0.rawValue, ["last_read_id": $1]) })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public var method: HTTPMethod {
|
||||||
|
switch self {
|
||||||
|
case .get:
|
||||||
|
return .get
|
||||||
|
case .post:
|
||||||
|
return .post
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -31,15 +31,23 @@ extension Paged: Endpoint {
|
||||||
|
|
||||||
public var method: HTTPMethod { endpoint.method }
|
public var method: HTTPMethod { endpoint.method }
|
||||||
|
|
||||||
public var queryParameters: [String: String]? {
|
public var queryParameters: [URLQueryItem] {
|
||||||
var queryParameters = endpoint.queryParameters ?? [String: String]()
|
var queryParameters = endpoint.queryParameters
|
||||||
|
|
||||||
queryParameters["max_id"] = maxId
|
if let maxId = maxId {
|
||||||
queryParameters["min_id"] = minId
|
queryParameters.append(.init(name: "max_id", value: maxId))
|
||||||
queryParameters["since_id"] = sinceId
|
}
|
||||||
|
|
||||||
|
if let minId = minId {
|
||||||
|
queryParameters.append(.init(name: "min_id", value: minId))
|
||||||
|
}
|
||||||
|
|
||||||
|
if let sinceId = sinceId {
|
||||||
|
queryParameters.append(.init(name: "since_id", value: sinceId))
|
||||||
|
}
|
||||||
|
|
||||||
if let limit = limit {
|
if let limit = limit {
|
||||||
queryParameters["limit"] = String(limit)
|
queryParameters.append(.init(name: "limit", value: String(limit)))
|
||||||
}
|
}
|
||||||
|
|
||||||
return queryParameters
|
return queryParameters
|
||||||
|
|
|
@ -32,13 +32,13 @@ extension ResultsEndpoint: Endpoint {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public var queryParameters: [String: String]? {
|
public var queryParameters: [URLQueryItem] {
|
||||||
switch self {
|
switch self {
|
||||||
case let .search(query, resolve):
|
case let .search(query, resolve):
|
||||||
var params = ["q": query]
|
var params = [URLQueryItem(name: "q", value: query)]
|
||||||
|
|
||||||
if resolve {
|
if resolve {
|
||||||
params["resolve"] = String(true)
|
params.append(.init(name: "resolve", value: "true"))
|
||||||
}
|
}
|
||||||
|
|
||||||
return params
|
return params
|
||||||
|
|
|
@ -39,16 +39,16 @@ extension StatusesEndpoint: Endpoint {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public var queryParameters: [String: String]? {
|
public var queryParameters: [URLQueryItem] {
|
||||||
switch self {
|
switch self {
|
||||||
case let .timelinesPublic(local):
|
case let .timelinesPublic(local):
|
||||||
return ["local": String(local)]
|
return [URLQueryItem(name: "local", value: String(local))]
|
||||||
case let .accountsStatuses(_, excludeReplies, onlyMedia, pinned):
|
case let .accountsStatuses(_, excludeReplies, onlyMedia, pinned):
|
||||||
return ["exclude_replies": String(excludeReplies),
|
return [URLQueryItem(name: "exclude_replies", value: String(excludeReplies)),
|
||||||
"only_media": String(onlyMedia),
|
URLQueryItem(name: "only_media", value: String(onlyMedia)),
|
||||||
"pinned": String(pinned)]
|
URLQueryItem(name: "pinned", value: String(pinned))]
|
||||||
default:
|
default:
|
||||||
return nil
|
return []
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -22,7 +22,7 @@ extension MastodonAPITarget: DecodableTarget {
|
||||||
|
|
||||||
public var method: HTTPMethod { endpoint.method }
|
public var method: HTTPMethod { endpoint.method }
|
||||||
|
|
||||||
public var queryParameters: [String: String]? { endpoint.queryParameters }
|
public var queryParameters: [URLQueryItem] { endpoint.queryParameters }
|
||||||
|
|
||||||
public var jsonBody: [String: Any]? { endpoint.jsonBody }
|
public var jsonBody: [String: Any]? { endpoint.jsonBody }
|
||||||
|
|
||||||
|
|
|
@ -15,6 +15,7 @@
|
||||||
D01F41E424F8889700D55A2D /* StatusAttachmentsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D01F41E224F8889700D55A2D /* StatusAttachmentsView.swift */; };
|
D01F41E424F8889700D55A2D /* StatusAttachmentsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D01F41E224F8889700D55A2D /* StatusAttachmentsView.swift */; };
|
||||||
D02E1F95250B13210071AD56 /* SafariView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D02E1F94250B13210071AD56 /* SafariView.swift */; };
|
D02E1F95250B13210071AD56 /* SafariView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D02E1F94250B13210071AD56 /* SafariView.swift */; };
|
||||||
D03B1B2A253818F3008F964B /* MediaPreferencesView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D03B1B29253818F3008F964B /* MediaPreferencesView.swift */; };
|
D03B1B2A253818F3008F964B /* MediaPreferencesView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D03B1B29253818F3008F964B /* MediaPreferencesView.swift */; };
|
||||||
|
D04226FD2546AC0B000980A3 /* StartupAndSyncingPreferencesView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D04226FC2546AC0B000980A3 /* StartupAndSyncingPreferencesView.swift */; };
|
||||||
D0625E59250F092900502611 /* StatusListCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0625E58250F092900502611 /* StatusListCell.swift */; };
|
D0625E59250F092900502611 /* StatusListCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0625E58250F092900502611 /* StatusListCell.swift */; };
|
||||||
D0625E5D250F0B5C00502611 /* StatusContentConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0625E5C250F0B5C00502611 /* StatusContentConfiguration.swift */; };
|
D0625E5D250F0B5C00502611 /* StatusContentConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0625E5C250F0B5C00502611 /* StatusContentConfiguration.swift */; };
|
||||||
D06B492324D4611300642749 /* KingfisherSwiftUI in Frameworks */ = {isa = PBXBuildFile; productRef = D06B492224D4611300642749 /* KingfisherSwiftUI */; };
|
D06B492324D4611300642749 /* KingfisherSwiftUI in Frameworks */ = {isa = PBXBuildFile; productRef = D06B492224D4611300642749 /* KingfisherSwiftUI */; };
|
||||||
|
@ -118,6 +119,7 @@
|
||||||
D01F41E224F8889700D55A2D /* StatusAttachmentsView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = StatusAttachmentsView.swift; sourceTree = "<group>"; };
|
D01F41E224F8889700D55A2D /* StatusAttachmentsView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = StatusAttachmentsView.swift; sourceTree = "<group>"; };
|
||||||
D02E1F94250B13210071AD56 /* SafariView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SafariView.swift; sourceTree = "<group>"; };
|
D02E1F94250B13210071AD56 /* SafariView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SafariView.swift; sourceTree = "<group>"; };
|
||||||
D03B1B29253818F3008F964B /* MediaPreferencesView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MediaPreferencesView.swift; sourceTree = "<group>"; };
|
D03B1B29253818F3008F964B /* MediaPreferencesView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MediaPreferencesView.swift; sourceTree = "<group>"; };
|
||||||
|
D04226FC2546AC0B000980A3 /* StartupAndSyncingPreferencesView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StartupAndSyncingPreferencesView.swift; sourceTree = "<group>"; };
|
||||||
D047FA8C24C3E21200AF17C5 /* Metatext.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Metatext.app; sourceTree = BUILT_PRODUCTS_DIR; };
|
D047FA8C24C3E21200AF17C5 /* Metatext.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Metatext.app; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||||
D0625E58250F092900502611 /* StatusListCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatusListCell.swift; sourceTree = "<group>"; };
|
D0625E58250F092900502611 /* StatusListCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatusListCell.swift; sourceTree = "<group>"; };
|
||||||
D0625E5C250F0B5C00502611 /* StatusContentConfiguration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatusContentConfiguration.swift; sourceTree = "<group>"; };
|
D0625E5C250F0B5C00502611 /* StatusContentConfiguration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatusContentConfiguration.swift; sourceTree = "<group>"; };
|
||||||
|
@ -348,6 +350,7 @@
|
||||||
D0C7D42724F76169001EBDBB /* RootView.swift */,
|
D0C7D42724F76169001EBDBB /* RootView.swift */,
|
||||||
D02E1F94250B13210071AD56 /* SafariView.swift */,
|
D02E1F94250B13210071AD56 /* SafariView.swift */,
|
||||||
D0C7D42924F76169001EBDBB /* SecondaryNavigationView.swift */,
|
D0C7D42924F76169001EBDBB /* SecondaryNavigationView.swift */,
|
||||||
|
D04226FC2546AC0B000980A3 /* StartupAndSyncingPreferencesView.swift */,
|
||||||
D0625E55250F086B00502611 /* Status */,
|
D0625E55250F086B00502611 /* Status */,
|
||||||
D0C7D42524F76169001EBDBB /* TableView.swift */,
|
D0C7D42524F76169001EBDBB /* TableView.swift */,
|
||||||
D0C7D42E24F76169001EBDBB /* TabNavigationView.swift */,
|
D0C7D42E24F76169001EBDBB /* TabNavigationView.swift */,
|
||||||
|
@ -632,6 +635,7 @@
|
||||||
D0C7D49724F7616A001EBDBB /* IdentitiesView.swift in Sources */,
|
D0C7D49724F7616A001EBDBB /* IdentitiesView.swift in Sources */,
|
||||||
D01EF22425182B1F00650C6B /* AccountHeaderView.swift in Sources */,
|
D01EF22425182B1F00650C6B /* AccountHeaderView.swift in Sources */,
|
||||||
D0EA59482522B8B600804347 /* ViewConstants.swift in Sources */,
|
D0EA59482522B8B600804347 /* ViewConstants.swift in Sources */,
|
||||||
|
D04226FD2546AC0B000980A3 /* StartupAndSyncingPreferencesView.swift in Sources */,
|
||||||
D0C7D49824F7616A001EBDBB /* CustomEmojiText.swift in Sources */,
|
D0C7D49824F7616A001EBDBB /* CustomEmojiText.swift in Sources */,
|
||||||
D08B8D3D253F929E00B1EBEF /* ImageViewController.swift in Sources */,
|
D08B8D3D253F929E00B1EBEF /* ImageViewController.swift in Sources */,
|
||||||
D0B8510C25259E56004E0744 /* LoadMoreCell.swift in Sources */,
|
D0B8510C25259E56004E0744 /* LoadMoreCell.swift in Sources */,
|
||||||
|
|
|
@ -8,6 +8,7 @@ public protocol CollectionService {
|
||||||
var nextPageMaxId: 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 markerTimeline: Marker.Timeline? { get }
|
||||||
func request(maxId: String?, minId: String?) -> AnyPublisher<Never, Error>
|
func request(maxId: String?, minId: String?) -> AnyPublisher<Never, Error>
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -15,4 +16,6 @@ extension CollectionService {
|
||||||
public var nextPageMaxId: 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 markerTimeline: Marker.Timeline? { nil }
|
||||||
}
|
}
|
||||||
|
|
|
@ -27,9 +27,14 @@ public struct IdentityService {
|
||||||
instanceURL: try secrets.getInstanceURL())
|
instanceURL: try secrets.getInstanceURL())
|
||||||
mastodonAPIClient.accessToken = try? secrets.getAccessToken()
|
mastodonAPIClient.accessToken = try? secrets.getAccessToken()
|
||||||
|
|
||||||
contentDatabase = try ContentDatabase(id: id,
|
let appPreferences = AppPreferences(environment: environment)
|
||||||
inMemory: environment.inMemoryContent,
|
|
||||||
keychain: environment.keychain)
|
contentDatabase = try ContentDatabase(
|
||||||
|
id: id,
|
||||||
|
useHomeTimelineLastReadId: appPreferences.homeTimelineBehavior == .rememberPosition,
|
||||||
|
useNotificationsLastReadId: appPreferences.notificationsTabBehavior == .rememberPosition,
|
||||||
|
inMemory: environment.inMemoryContent,
|
||||||
|
keychain: environment.keychain)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -87,6 +92,29 @@ public extension IdentityService {
|
||||||
.eraseToAnyPublisher()
|
.eraseToAnyPublisher()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func getMarker(_ markerTimeline: Marker.Timeline) -> AnyPublisher<Marker, Error> {
|
||||||
|
mastodonAPIClient.request(MarkersEndpoint.get([markerTimeline]))
|
||||||
|
.compactMap { $0[markerTimeline.rawValue] }
|
||||||
|
.eraseToAnyPublisher()
|
||||||
|
}
|
||||||
|
|
||||||
|
func getLocalLastReadId(_ markerTimeline: Marker.Timeline) -> String? {
|
||||||
|
contentDatabase.lastReadId(markerTimeline)
|
||||||
|
}
|
||||||
|
|
||||||
|
func setLastReadId(_ id: String, forMarker markerTimeline: Marker.Timeline) -> AnyPublisher<Never, Error> {
|
||||||
|
switch AppPreferences(environment: environment).positionBehavior(markerTimeline: markerTimeline) {
|
||||||
|
case .rememberPosition:
|
||||||
|
return contentDatabase.setLastReadId(id, markerTimeline: markerTimeline)
|
||||||
|
case .syncPosition:
|
||||||
|
return mastodonAPIClient.request(MarkersEndpoint.post([markerTimeline: id]))
|
||||||
|
.ignoreOutput()
|
||||||
|
.eraseToAnyPublisher()
|
||||||
|
case .newest:
|
||||||
|
return Empty().eraseToAnyPublisher()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func identityPublisher(immediate: Bool) -> AnyPublisher<Identity, Error> {
|
func identityPublisher(immediate: Bool) -> AnyPublisher<Identity, Error> {
|
||||||
identityDatabase.identityPublisher(id: id, immediate: immediate)
|
identityDatabase.identityPublisher(id: id, immediate: immediate)
|
||||||
}
|
}
|
||||||
|
|
|
@ -73,7 +73,7 @@ private struct UpdatedFilterTarget: DecodableTarget {
|
||||||
let baseURL = URL(string: "https://filter.metabolist.com")!
|
let baseURL = URL(string: "https://filter.metabolist.com")!
|
||||||
let pathComponents = ["filter"]
|
let pathComponents = ["filter"]
|
||||||
let method = HTTPMethod.get
|
let method = HTTPMethod.get
|
||||||
let queryParameters: [String: String]? = nil
|
let queryParameters: [URLQueryItem] = []
|
||||||
let jsonBody: [String: Any]? = nil
|
let jsonBody: [String: Any]? = nil
|
||||||
let headers: [String: String]? = nil
|
let headers: [String: String]? = nil
|
||||||
}
|
}
|
||||||
|
|
|
@ -15,15 +15,27 @@ public struct TimelineService {
|
||||||
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 nextPageMaxIdSubject = PassthroughSubject<String, Never>()
|
private let nextPageMaxIdSubject: CurrentValueSubject<String, Never>
|
||||||
|
|
||||||
init(timeline: Timeline, mastodonAPIClient: MastodonAPIClient, contentDatabase: ContentDatabase) {
|
init(timeline: Timeline, mastodonAPIClient: MastodonAPIClient, contentDatabase: ContentDatabase) {
|
||||||
self.timeline = timeline
|
self.timeline = timeline
|
||||||
self.mastodonAPIClient = mastodonAPIClient
|
self.mastodonAPIClient = mastodonAPIClient
|
||||||
self.contentDatabase = contentDatabase
|
self.contentDatabase = contentDatabase
|
||||||
|
|
||||||
|
let nextPageMaxIdSubject = CurrentValueSubject<String, Never>(String(Int.max))
|
||||||
|
|
||||||
|
self.nextPageMaxIdSubject = nextPageMaxIdSubject
|
||||||
sections = contentDatabase.timelinePublisher(timeline)
|
sections = contentDatabase.timelinePublisher(timeline)
|
||||||
|
.handleEvents(receiveOutput: {
|
||||||
|
guard case let .status(status, _) = $0.last?.last,
|
||||||
|
status.id < nextPageMaxIdSubject.value
|
||||||
|
else { return }
|
||||||
|
|
||||||
|
nextPageMaxIdSubject.send(status.id)
|
||||||
|
})
|
||||||
|
.eraseToAnyPublisher()
|
||||||
navigationService = NavigationService(mastodonAPIClient: mastodonAPIClient, contentDatabase: contentDatabase)
|
navigationService = NavigationService(mastodonAPIClient: mastodonAPIClient, contentDatabase: contentDatabase)
|
||||||
nextPageMaxId = nextPageMaxIdSubject.eraseToAnyPublisher()
|
nextPageMaxId = nextPageMaxIdSubject.dropFirst().eraseToAnyPublisher()
|
||||||
|
|
||||||
if case let .tag(tag) = timeline {
|
if case let .tag(tag) = timeline {
|
||||||
title = Just("#".appending(tag)).eraseToAnyPublisher()
|
title = Just("#".appending(tag)).eraseToAnyPublisher()
|
||||||
|
@ -34,10 +46,19 @@ public struct TimelineService {
|
||||||
}
|
}
|
||||||
|
|
||||||
extension TimelineService: CollectionService {
|
extension TimelineService: CollectionService {
|
||||||
|
public var markerTimeline: Marker.Timeline? {
|
||||||
|
switch timeline {
|
||||||
|
case .home:
|
||||||
|
return .home
|
||||||
|
default:
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
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, maxId < nextPageMaxIdSubject.value else { return }
|
||||||
|
|
||||||
nextPageMaxIdSubject.send(maxId)
|
nextPageMaxIdSubject.send(maxId)
|
||||||
})
|
})
|
||||||
|
|
|
@ -2,6 +2,7 @@
|
||||||
|
|
||||||
import CodableBloomFilter
|
import CodableBloomFilter
|
||||||
import Foundation
|
import Foundation
|
||||||
|
import Mastodon
|
||||||
|
|
||||||
public struct AppPreferences {
|
public struct AppPreferences {
|
||||||
private let userDefaults: UserDefaults
|
private let userDefaults: UserDefaults
|
||||||
|
@ -30,6 +31,14 @@ public extension AppPreferences {
|
||||||
public var id: String { rawValue }
|
public var id: String { rawValue }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
enum PositionBehavior: String, CaseIterable, Identifiable {
|
||||||
|
case rememberPosition
|
||||||
|
case syncPosition
|
||||||
|
case newest
|
||||||
|
|
||||||
|
public var id: String { rawValue }
|
||||||
|
}
|
||||||
|
|
||||||
var useSystemReduceMotionForMedia: Bool {
|
var useSystemReduceMotionForMedia: Bool {
|
||||||
get { self[.useSystemReduceMotionForMedia] ?? true }
|
get { self[.useSystemReduceMotionForMedia] ?? true }
|
||||||
set { self[.useSystemReduceMotionForMedia] = newValue }
|
set { self[.useSystemReduceMotionForMedia] = newValue }
|
||||||
|
@ -76,9 +85,42 @@ public extension AppPreferences {
|
||||||
set { self[.autoplayVideos] = newValue.rawValue }
|
set { self[.autoplayVideos] = newValue.rawValue }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var homeTimelineBehavior: PositionBehavior {
|
||||||
|
get {
|
||||||
|
if let rawValue = self[.homeTimelineBehavior] as String?,
|
||||||
|
let value = PositionBehavior(rawValue: rawValue) {
|
||||||
|
return value
|
||||||
|
}
|
||||||
|
|
||||||
|
return .rememberPosition
|
||||||
|
}
|
||||||
|
set { self[.homeTimelineBehavior] = newValue.rawValue }
|
||||||
|
}
|
||||||
|
|
||||||
|
var notificationsTabBehavior: PositionBehavior {
|
||||||
|
get {
|
||||||
|
if let rawValue = self[.notificationsTabBehavior] as String?,
|
||||||
|
let value = PositionBehavior(rawValue: rawValue) {
|
||||||
|
return value
|
||||||
|
}
|
||||||
|
|
||||||
|
return .newest
|
||||||
|
}
|
||||||
|
set { self[.notificationsTabBehavior] = newValue.rawValue }
|
||||||
|
}
|
||||||
|
|
||||||
var shouldReduceMotion: Bool {
|
var shouldReduceMotion: Bool {
|
||||||
systemReduceMotion() && useSystemReduceMotionForMedia
|
systemReduceMotion() && useSystemReduceMotionForMedia
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func positionBehavior(markerTimeline: Marker.Timeline) -> PositionBehavior {
|
||||||
|
switch markerTimeline {
|
||||||
|
case .home:
|
||||||
|
return homeTimelineBehavior
|
||||||
|
case .notifications:
|
||||||
|
return notificationsTabBehavior
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
extension AppPreferences {
|
extension AppPreferences {
|
||||||
|
@ -103,6 +145,8 @@ private extension AppPreferences {
|
||||||
case animateHeaders
|
case animateHeaders
|
||||||
case autoplayGIFs
|
case autoplayGIFs
|
||||||
case autoplayVideos
|
case autoplayVideos
|
||||||
|
case homeTimelineBehavior
|
||||||
|
case notificationsTabBehavior
|
||||||
}
|
}
|
||||||
|
|
||||||
subscript<T>(index: Item) -> T? {
|
subscript<T>(index: Item) -> T? {
|
||||||
|
|
|
@ -6,6 +6,7 @@ import SafariServices
|
||||||
import SwiftUI
|
import SwiftUI
|
||||||
import ViewModels
|
import ViewModels
|
||||||
|
|
||||||
|
// swiftlint:disable file_length
|
||||||
class TableViewController: UITableViewController {
|
class TableViewController: UITableViewController {
|
||||||
var transitionViewTag = -1
|
var transitionViewTag = -1
|
||||||
|
|
||||||
|
@ -272,13 +273,16 @@ private extension TableViewController {
|
||||||
if
|
if
|
||||||
let item = update.maintainScrollPosition,
|
let item = update.maintainScrollPosition,
|
||||||
let indexPath = self.dataSource.indexPath(for: item) {
|
let indexPath = self.dataSource.indexPath(for: item) {
|
||||||
self.tableView.contentInset.bottom = max(
|
if self.viewModel.shouldAdjustContentInset {
|
||||||
0,
|
self.tableView.contentInset.bottom = max(
|
||||||
self.tableView.frame.height
|
0,
|
||||||
- self.tableView.contentSize.height
|
self.tableView.frame.height
|
||||||
- self.tableView.safeAreaInsets.top
|
- self.tableView.contentSize.height
|
||||||
- self.tableView.safeAreaInsets.bottom)
|
- self.tableView.safeAreaInsets.top
|
||||||
+ self.tableView.rectForRow(at: indexPath).minY
|
- self.tableView.safeAreaInsets.bottom)
|
||||||
|
+ self.tableView.rectForRow(at: indexPath).minY
|
||||||
|
}
|
||||||
|
|
||||||
self.tableView.scrollToRow(at: indexPath, at: .top, animated: false)
|
self.tableView.scrollToRow(at: indexPath, at: .top, animated: false)
|
||||||
|
|
||||||
if let offsetFromNavigationBar = offsetFromNavigationBar {
|
if let offsetFromNavigationBar = offsetFromNavigationBar {
|
||||||
|
@ -399,3 +403,4 @@ private extension TableViewController {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// swiftlint:enable file_length
|
||||||
|
|
|
@ -18,7 +18,10 @@ final public class CollectionItemsViewModel: ObservableObject {
|
||||||
private let expandAllSubject: CurrentValueSubject<ExpandAllState, Never>
|
private let expandAllSubject: CurrentValueSubject<ExpandAllState, Never>
|
||||||
private var maintainScrollPosition: CollectionItem?
|
private var maintainScrollPosition: CollectionItem?
|
||||||
private var topVisibleIndexPath = IndexPath(item: 0, section: 0)
|
private var topVisibleIndexPath = IndexPath(item: 0, section: 0)
|
||||||
|
private let lastReadId = CurrentValueSubject<String?, Never>(nil)
|
||||||
private var lastSelectedLoadMore: LoadMore?
|
private var lastSelectedLoadMore: LoadMore?
|
||||||
|
private var hasRequestedUsingMarker = false
|
||||||
|
private var hasRememberedPosition = false
|
||||||
private var cancellables = Set<AnyCancellable>()
|
private var cancellables = Set<AnyCancellable>()
|
||||||
|
|
||||||
public init(collectionService: CollectionService, identification: Identification) {
|
public init(collectionService: CollectionService, identification: Identification) {
|
||||||
|
@ -38,6 +41,15 @@ final public class CollectionItemsViewModel: ObservableObject {
|
||||||
collectionService.nextPageMaxId
|
collectionService.nextPageMaxId
|
||||||
.sink { [weak self] in self?.nextPageMaxId = $0 }
|
.sink { [weak self] in self?.nextPageMaxId = $0 }
|
||||||
.store(in: &cancellables)
|
.store(in: &cancellables)
|
||||||
|
|
||||||
|
if let markerTimeline = collectionService.markerTimeline {
|
||||||
|
lastReadId.compactMap { $0 }
|
||||||
|
.removeDuplicates()
|
||||||
|
.debounce(for: 0.5, scheduler: DispatchQueue.global())
|
||||||
|
.flatMap { identification.service.setLastReadId($0, forMarker: markerTimeline) }
|
||||||
|
.sink { _ in } receiveValue: { _ in }
|
||||||
|
.store(in: &cancellables)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -62,8 +74,32 @@ extension CollectionItemsViewModel: CollectionViewModel {
|
||||||
|
|
||||||
public var events: AnyPublisher<CollectionItemEvent, Never> { eventsSubject.eraseToAnyPublisher() }
|
public var events: AnyPublisher<CollectionItemEvent, Never> { eventsSubject.eraseToAnyPublisher() }
|
||||||
|
|
||||||
|
public var shouldAdjustContentInset: Bool { collectionService is ContextService }
|
||||||
|
|
||||||
public func request(maxId: String? = nil, minId: String? = nil) {
|
public func request(maxId: String? = nil, minId: String? = nil) {
|
||||||
collectionService.request(maxId: maxId, minId: minId)
|
let publisher: AnyPublisher<Never, Error>
|
||||||
|
|
||||||
|
if let markerTimeline = collectionService.markerTimeline,
|
||||||
|
identification.appPreferences.positionBehavior(markerTimeline: markerTimeline) == .syncPosition,
|
||||||
|
!hasRequestedUsingMarker {
|
||||||
|
publisher = identification.service.getMarker(markerTimeline)
|
||||||
|
.flatMap { [weak self] in
|
||||||
|
self?.collectionService.request(maxId: $0.lastReadId, minId: nil) ?? Empty().eraseToAnyPublisher()
|
||||||
|
}
|
||||||
|
.catch { [weak self] _ in
|
||||||
|
self?.collectionService.request(maxId: nil, minId: nil) ?? Empty().eraseToAnyPublisher()
|
||||||
|
}
|
||||||
|
.collect()
|
||||||
|
.flatMap { [weak self] _ in
|
||||||
|
self?.collectionService.request(maxId: nil, minId: nil) ?? Empty().eraseToAnyPublisher()
|
||||||
|
}
|
||||||
|
.eraseToAnyPublisher()
|
||||||
|
self.hasRequestedUsingMarker = true
|
||||||
|
} else {
|
||||||
|
publisher = collectionService.request(maxId: maxId, minId: minId)
|
||||||
|
}
|
||||||
|
|
||||||
|
publisher
|
||||||
.receive(on: DispatchQueue.main)
|
.receive(on: DispatchQueue.main)
|
||||||
.assignErrorsToAlertItem(to: \.alertItem, on: self)
|
.assignErrorsToAlertItem(to: \.alertItem, on: self)
|
||||||
.handleEvents(
|
.handleEvents(
|
||||||
|
@ -95,6 +131,15 @@ extension CollectionItemsViewModel: CollectionViewModel {
|
||||||
|
|
||||||
public func viewedAtTop(indexPath: IndexPath) {
|
public func viewedAtTop(indexPath: IndexPath) {
|
||||||
topVisibleIndexPath = indexPath
|
topVisibleIndexPath = indexPath
|
||||||
|
|
||||||
|
if items.value.count > indexPath.section, items.value[indexPath.section].count > indexPath.item {
|
||||||
|
switch items.value[indexPath.section][indexPath.item] {
|
||||||
|
case let .status(status, _):
|
||||||
|
lastReadId.send(status.id)
|
||||||
|
default:
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public func canSelect(indexPath: IndexPath) -> Bool {
|
public func canSelect(indexPath: IndexPath) -> Bool {
|
||||||
|
@ -196,9 +241,27 @@ private extension CollectionItemsViewModel {
|
||||||
viewModelCache = viewModelCache.filter { itemsSet.contains($0.key) }
|
viewModelCache = viewModelCache.filter { itemsSet.contains($0.key) }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// swiftlint:disable:next cyclomatic_complexity function_body_length
|
||||||
func itemForScrollPositionMaintenance(newItems: [[CollectionItem]]) -> CollectionItem? {
|
func itemForScrollPositionMaintenance(newItems: [[CollectionItem]]) -> CollectionItem? {
|
||||||
let flatNewItems = newItems.reduce([], +)
|
let flatNewItems = newItems.reduce([], +)
|
||||||
|
|
||||||
|
if let markerTimeline = collectionService.markerTimeline,
|
||||||
|
identification.appPreferences.positionBehavior(markerTimeline: markerTimeline) == .rememberPosition,
|
||||||
|
let localLastReadId = identification.service.getLocalLastReadId(markerTimeline),
|
||||||
|
!hasRememberedPosition,
|
||||||
|
let lastReadItem = flatNewItems.first(where: {
|
||||||
|
switch $0 {
|
||||||
|
case let .status(status, _):
|
||||||
|
return status.id == localLastReadId
|
||||||
|
default:
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}) {
|
||||||
|
hasRememberedPosition = true
|
||||||
|
|
||||||
|
return lastReadItem
|
||||||
|
}
|
||||||
|
|
||||||
if collectionService is ContextService,
|
if collectionService is ContextService,
|
||||||
items.value.isEmpty || items.value.map(\.count) == [0, 1, 0],
|
items.value.isEmpty || items.value.map(\.count) == [0, 1, 0],
|
||||||
let contextParent = flatNewItems.first(where: {
|
let contextParent = flatNewItems.first(where: {
|
||||||
|
|
|
@ -10,6 +10,7 @@ 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 events: AnyPublisher<CollectionItemEvent, Never> { get }
|
var events: AnyPublisher<CollectionItemEvent, Never> { get }
|
||||||
|
var shouldAdjustContentInset: Bool { get }
|
||||||
var nextPageMaxId: String? { get }
|
var nextPageMaxId: String? { get }
|
||||||
func request(maxId: String?, minId: String?)
|
func request(maxId: String?, minId: String?)
|
||||||
func viewedAtTop(indexPath: IndexPath)
|
func viewedAtTop(indexPath: IndexPath)
|
||||||
|
|
|
@ -81,6 +81,10 @@ extension ProfileViewModel: CollectionViewModel {
|
||||||
.eraseToAnyPublisher()
|
.eraseToAnyPublisher()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public var shouldAdjustContentInset: Bool {
|
||||||
|
collectionViewModel.value.shouldAdjustContentInset
|
||||||
|
}
|
||||||
|
|
||||||
public var nextPageMaxId: String? {
|
public var nextPageMaxId: String? {
|
||||||
collectionViewModel.value.nextPageMaxId
|
collectionViewModel.value.nextPageMaxId
|
||||||
}
|
}
|
||||||
|
|
|
@ -26,6 +26,8 @@ struct PreferencesView: View {
|
||||||
NavigationLink("preferences.media",
|
NavigationLink("preferences.media",
|
||||||
destination: MediaPreferencesView(
|
destination: MediaPreferencesView(
|
||||||
viewModel: .init(identification: identification)))
|
viewModel: .init(identification: identification)))
|
||||||
|
NavigationLink("preferences.startup-and-syncing",
|
||||||
|
destination: StartupAndSyncingPreferencesView())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.navigationTitle("preferences")
|
.navigationTitle("preferences")
|
||||||
|
|
53
Views/StartupAndSyncingPreferencesView.swift
Normal file
53
Views/StartupAndSyncingPreferencesView.swift
Normal file
|
@ -0,0 +1,53 @@
|
||||||
|
// Copyright © 2020 Metabolist. All rights reserved.
|
||||||
|
|
||||||
|
import SwiftUI
|
||||||
|
import ViewModels
|
||||||
|
|
||||||
|
struct StartupAndSyncingPreferencesView: View {
|
||||||
|
@EnvironmentObject var identification: Identification
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
Form {
|
||||||
|
Section(header: Text("preferences.startup-and-syncing.home-timeline")) {
|
||||||
|
Picker("preferences.startup-and-syncing.position-on-startup",
|
||||||
|
selection: $identification.appPreferences.homeTimelineBehavior) {
|
||||||
|
ForEach(AppPreferences.PositionBehavior.allCases) { option in
|
||||||
|
Text(option.localizedStringKey).tag(option)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Section(header: Text("preferences.startup-and-syncing.notifications-tab")) {
|
||||||
|
Picker("preferences.startup-and-syncing.position-on-startup",
|
||||||
|
selection: $identification.appPreferences.notificationsTabBehavior) {
|
||||||
|
ForEach(AppPreferences.PositionBehavior.allCases) { option in
|
||||||
|
Text(option.localizedStringKey).tag(option)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension AppPreferences.PositionBehavior {
|
||||||
|
var localizedStringKey: LocalizedStringKey {
|
||||||
|
switch self {
|
||||||
|
case .rememberPosition:
|
||||||
|
return "preferences.startup-and-syncing.remember-position"
|
||||||
|
case .syncPosition:
|
||||||
|
return "preferences.startup-and-syncing.sync-position"
|
||||||
|
case .newest:
|
||||||
|
return "preferences.startup-and-syncing.newest"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#if DEBUG
|
||||||
|
import PreviewViewModels
|
||||||
|
|
||||||
|
struct StartupAndSyncingPreferencesView_Previews: PreviewProvider {
|
||||||
|
static var previews: some View {
|
||||||
|
StartupAndSyncingPreferencesView()
|
||||||
|
.environmentObject(Identification.preview)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#endif
|
Loading…
Reference in a new issue