Load more WIP
This commit is contained in:
parent
9507343511
commit
7f937601b1
|
@ -74,6 +74,7 @@ extension ContentDatabase {
|
||||||
try db.create(table: "loadMoreRecord") { t in
|
try db.create(table: "loadMoreRecord") { t in
|
||||||
t.column("timelineId").notNull().references("timelineRecord", onDelete: .cascade)
|
t.column("timelineId").notNull().references("timelineRecord", onDelete: .cascade)
|
||||||
t.column("afterStatusId", .text).notNull()
|
t.column("afterStatusId", .text).notNull()
|
||||||
|
t.column("beforeStatusId", .text).notNull()
|
||||||
|
|
||||||
t.primaryKey(["timelineId", "afterStatusId"], onConflict: .replace)
|
t.primaryKey(["timelineId", "afterStatusId"], onConflict: .replace)
|
||||||
}
|
}
|
||||||
|
|
|
@ -49,7 +49,10 @@ public extension ContentDatabase {
|
||||||
.eraseToAnyPublisher()
|
.eraseToAnyPublisher()
|
||||||
}
|
}
|
||||||
|
|
||||||
func insert(statuses: [Status], timeline: Timeline) -> AnyPublisher<Never, Error> {
|
func insert(
|
||||||
|
statuses: [Status],
|
||||||
|
timeline: Timeline,
|
||||||
|
loadMoreAndDirection: (LoadMore, LoadMore.Direction)? = nil) -> AnyPublisher<Never, Error> {
|
||||||
databaseWriter.writePublisher {
|
databaseWriter.writePublisher {
|
||||||
let timelineRecord = TimelineRecord(timeline: timeline)
|
let timelineRecord = TimelineRecord(timeline: timeline)
|
||||||
|
|
||||||
|
@ -66,7 +69,38 @@ public extension ContentDatabase {
|
||||||
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(timelineId: timeline.id, afterStatusId: minIDInserted).save($0)
|
try LoadMoreRecord(
|
||||||
|
timelineId: timeline.id,
|
||||||
|
afterStatusId: minIDInserted,
|
||||||
|
beforeStatusId: maxIDPresent)
|
||||||
|
.save($0)
|
||||||
|
}
|
||||||
|
|
||||||
|
guard let (loadMore, direction) = loadMoreAndDirection else { return }
|
||||||
|
|
||||||
|
try LoadMoreRecord(
|
||||||
|
timelineId: loadMore.timeline.id,
|
||||||
|
afterStatusId: loadMore.afterStatusId,
|
||||||
|
beforeStatusId: loadMore.beforeStatusId)
|
||||||
|
.delete($0)
|
||||||
|
|
||||||
|
switch direction {
|
||||||
|
case .up:
|
||||||
|
if let maxIDInserted = statuses.map(\.id).max(), maxIDInserted < loadMore.afterStatusId {
|
||||||
|
try LoadMoreRecord(
|
||||||
|
timelineId: loadMore.timeline.id,
|
||||||
|
afterStatusId: loadMore.afterStatusId,
|
||||||
|
beforeStatusId: maxIDInserted)
|
||||||
|
.save($0)
|
||||||
|
}
|
||||||
|
case .down:
|
||||||
|
if let minIDInserted = statuses.map(\.id).min(), minIDInserted > loadMore.beforeStatusId {
|
||||||
|
try LoadMoreRecord(
|
||||||
|
timelineId: loadMore.timeline.id,
|
||||||
|
afterStatusId: minIDInserted,
|
||||||
|
beforeStatusId: loadMore.beforeStatusId)
|
||||||
|
.save($0)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.ignoreOutput()
|
.ignoreOutput()
|
||||||
|
|
|
@ -7,12 +7,14 @@ import Mastodon
|
||||||
struct LoadMoreRecord: Codable, Hashable {
|
struct LoadMoreRecord: Codable, Hashable {
|
||||||
let timelineId: String
|
let timelineId: String
|
||||||
let afterStatusId: String
|
let afterStatusId: String
|
||||||
|
let beforeStatusId: String
|
||||||
}
|
}
|
||||||
|
|
||||||
extension LoadMoreRecord {
|
extension LoadMoreRecord {
|
||||||
enum Columns {
|
enum Columns {
|
||||||
static let timelineId = Column(LoadMoreRecord.CodingKeys.timelineId)
|
static let timelineId = Column(LoadMoreRecord.CodingKeys.timelineId)
|
||||||
static let afterStatusId = Column(LoadMoreRecord.CodingKeys.afterStatusId)
|
static let afterStatusId = Column(LoadMoreRecord.CodingKeys.afterStatusId)
|
||||||
|
static let beforeStatusId = Column(LoadMoreRecord.CodingKeys.beforeStatusId)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -42,7 +42,10 @@ extension TimelineItemsInfo {
|
||||||
}) else { continue }
|
}) else { continue }
|
||||||
|
|
||||||
timelineItems.insert(
|
timelineItems.insert(
|
||||||
.loadMore(LoadMore(timeline: timeline, afterStatusId: loadMoreRecord.afterStatusId)),
|
.loadMore(LoadMore(
|
||||||
|
timeline: timeline,
|
||||||
|
afterStatusId: loadMoreRecord.afterStatusId,
|
||||||
|
beforeStatusId: loadMoreRecord.beforeStatusId)),
|
||||||
at: index)
|
at: index)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -38,7 +38,7 @@ extension TimelineRecord {
|
||||||
StatusRecord.self,
|
StatusRecord.self,
|
||||||
through: statusJoins,
|
through: statusJoins,
|
||||||
using: TimelineStatusJoin.status)
|
using: TimelineStatusJoin.status)
|
||||||
.order(StatusRecord.Columns.createdAt.desc)
|
.order(StatusRecord.Columns.id.desc)
|
||||||
static let account = belongsTo(AccountRecord.self, using: ForeignKey([Columns.accountId]))
|
static let account = belongsTo(AccountRecord.self, using: ForeignKey([Columns.accountId]))
|
||||||
static let loadMores = hasMany(LoadMoreRecord.self)
|
static let loadMores = hasMany(LoadMoreRecord.self)
|
||||||
|
|
||||||
|
|
|
@ -1,12 +1,12 @@
|
||||||
// Copyright © 2020 Metabolist. All rights reserved.
|
// Copyright © 2020 Metabolist. All rights reserved.
|
||||||
|
|
||||||
import Foundation
|
import Foundation
|
||||||
import GRDB
|
|
||||||
import Mastodon
|
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: String
|
||||||
|
public let beforeStatusId: String
|
||||||
}
|
}
|
||||||
|
|
||||||
public extension LoadMore {
|
public extension LoadMore {
|
||||||
|
|
|
@ -54,6 +54,8 @@
|
||||||
D0E2C1D124FD97F000854680 /* ViewModels in Frameworks */ = {isa = PBXBuildFile; productRef = D0E2C1D024FD97F000854680 /* ViewModels */; };
|
D0E2C1D124FD97F000854680 /* ViewModels in Frameworks */ = {isa = PBXBuildFile; productRef = D0E2C1D024FD97F000854680 /* ViewModels */; };
|
||||||
D0E5361C24E3EB4D00FB1CE1 /* NotificationService.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0E5361B24E3EB4D00FB1CE1 /* NotificationService.swift */; };
|
D0E5361C24E3EB4D00FB1CE1 /* NotificationService.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0E5361B24E3EB4D00FB1CE1 /* NotificationService.swift */; };
|
||||||
D0E5362024E3EB4D00FB1CE1 /* Notification Service Extension.appex in Embed App Extensions */ = {isa = PBXBuildFile; fileRef = D0E5361924E3EB4D00FB1CE1 /* Notification Service Extension.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; };
|
D0E5362024E3EB4D00FB1CE1 /* Notification Service Extension.appex in Embed App Extensions */ = {isa = PBXBuildFile; fileRef = D0E5361924E3EB4D00FB1CE1 /* Notification Service Extension.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; };
|
||||||
|
D0E569DB2529319100FA1D72 /* LoadMoreView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0E569DA2529319100FA1D72 /* LoadMoreView.swift */; };
|
||||||
|
D0E569E0252931B100FA1D72 /* LoadMoreContentConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0E569DF252931B100FA1D72 /* LoadMoreContentConfiguration.swift */; };
|
||||||
D0EA59402522AC8700804347 /* CardView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0EA593F2522AC8700804347 /* CardView.swift */; };
|
D0EA59402522AC8700804347 /* CardView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0EA593F2522AC8700804347 /* CardView.swift */; };
|
||||||
D0EA59482522B8B600804347 /* ViewConstants.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0EA59472522B8B600804347 /* ViewConstants.swift */; };
|
D0EA59482522B8B600804347 /* ViewConstants.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0EA59472522B8B600804347 /* ViewConstants.swift */; };
|
||||||
D0F0B10E251A868200942152 /* AccountView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0F0B10D251A868200942152 /* AccountView.swift */; };
|
D0F0B10E251A868200942152 /* AccountView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0F0B10D251A868200942152 /* AccountView.swift */; };
|
||||||
|
@ -154,6 +156,8 @@
|
||||||
D0E5361B24E3EB4D00FB1CE1 /* NotificationService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationService.swift; sourceTree = "<group>"; };
|
D0E5361B24E3EB4D00FB1CE1 /* NotificationService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationService.swift; sourceTree = "<group>"; };
|
||||||
D0E5361D24E3EB4D00FB1CE1 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
|
D0E5361D24E3EB4D00FB1CE1 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
|
||||||
D0E5362824E4A06B00FB1CE1 /* Notification Service Extension.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = "Notification Service Extension.entitlements"; sourceTree = "<group>"; };
|
D0E5362824E4A06B00FB1CE1 /* Notification Service Extension.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = "Notification Service Extension.entitlements"; sourceTree = "<group>"; };
|
||||||
|
D0E569DA2529319100FA1D72 /* LoadMoreView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoadMoreView.swift; sourceTree = "<group>"; };
|
||||||
|
D0E569DF252931B100FA1D72 /* LoadMoreContentConfiguration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoadMoreContentConfiguration.swift; sourceTree = "<group>"; };
|
||||||
D0EA593F2522AC8700804347 /* CardView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CardView.swift; sourceTree = "<group>"; };
|
D0EA593F2522AC8700804347 /* CardView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CardView.swift; sourceTree = "<group>"; };
|
||||||
D0EA59472522B8B600804347 /* ViewConstants.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewConstants.swift; sourceTree = "<group>"; };
|
D0EA59472522B8B600804347 /* ViewConstants.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewConstants.swift; sourceTree = "<group>"; };
|
||||||
D0F0B10D251A868200942152 /* AccountView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountView.swift; sourceTree = "<group>"; };
|
D0F0B10D251A868200942152 /* AccountView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountView.swift; sourceTree = "<group>"; };
|
||||||
|
@ -291,6 +295,8 @@
|
||||||
D0BEB1FE24F9E5BB001B0F04 /* ListsView.swift */,
|
D0BEB1FE24F9E5BB001B0F04 /* ListsView.swift */,
|
||||||
D0BEB1F624F9A84B001B0F04 /* LoadingTableFooterView.swift */,
|
D0BEB1F624F9A84B001B0F04 /* LoadingTableFooterView.swift */,
|
||||||
D0B8510B25259E56004E0744 /* LoadMoreCell.swift */,
|
D0B8510B25259E56004E0744 /* LoadMoreCell.swift */,
|
||||||
|
D0E569DF252931B100FA1D72 /* LoadMoreContentConfiguration.swift */,
|
||||||
|
D0E569DA2529319100FA1D72 /* LoadMoreView.swift */,
|
||||||
D0C7D42D24F76169001EBDBB /* NotificationTypesPreferencesView.swift */,
|
D0C7D42D24F76169001EBDBB /* NotificationTypesPreferencesView.swift */,
|
||||||
D0C7D42824F76169001EBDBB /* PostingReadingPreferencesView.swift */,
|
D0C7D42824F76169001EBDBB /* PostingReadingPreferencesView.swift */,
|
||||||
D0C7D42624F76169001EBDBB /* PreferencesView.swift */,
|
D0C7D42624F76169001EBDBB /* PreferencesView.swift */,
|
||||||
|
@ -534,6 +540,7 @@
|
||||||
D0C7D49C24F7616A001EBDBB /* RootView.swift in Sources */,
|
D0C7D49C24F7616A001EBDBB /* RootView.swift in Sources */,
|
||||||
D0F0B126251A90F400942152 /* AccountListCell.swift in Sources */,
|
D0F0B126251A90F400942152 /* AccountListCell.swift in Sources */,
|
||||||
D0B32F50250B373600311912 /* RegistrationView.swift in Sources */,
|
D0B32F50250B373600311912 /* RegistrationView.swift in Sources */,
|
||||||
|
D0E569E0252931B100FA1D72 /* LoadMoreContentConfiguration.swift in Sources */,
|
||||||
D0F0B136251AA12700942152 /* CollectionItemKind+Extensions.swift in Sources */,
|
D0F0B136251AA12700942152 /* CollectionItemKind+Extensions.swift in Sources */,
|
||||||
D0625E5D250F0B5C00502611 /* StatusContentConfiguration.swift in Sources */,
|
D0625E5D250F0B5C00502611 /* StatusContentConfiguration.swift in Sources */,
|
||||||
D0BEB1F324F8EE8C001B0F04 /* AttachmentView.swift in Sources */,
|
D0BEB1F324F8EE8C001B0F04 /* AttachmentView.swift in Sources */,
|
||||||
|
@ -544,6 +551,7 @@
|
||||||
D0C7D4D624F7616A001EBDBB /* NSMutableAttributedString+Extensions.swift in Sources */,
|
D0C7D4D624F7616A001EBDBB /* NSMutableAttributedString+Extensions.swift in Sources */,
|
||||||
D0625E5F250F0CFF00502611 /* StatusView.swift in Sources */,
|
D0625E5F250F0CFF00502611 /* StatusView.swift in Sources */,
|
||||||
D0625E59250F092900502611 /* StatusListCell.swift in Sources */,
|
D0625E59250F092900502611 /* StatusListCell.swift in Sources */,
|
||||||
|
D0E569DB2529319100FA1D72 /* LoadMoreView.swift in Sources */,
|
||||||
D0C7D49D24F7616A001EBDBB /* PostingReadingPreferencesView.swift in Sources */,
|
D0C7D49D24F7616A001EBDBB /* PostingReadingPreferencesView.swift in Sources */,
|
||||||
D0B5FE9B251583DB00478838 /* ProfileCollection+Extensions.swift in Sources */,
|
D0B5FE9B251583DB00478838 /* ProfileCollection+Extensions.swift in Sources */,
|
||||||
D0C7D49E24F7616A001EBDBB /* SecondaryNavigationView.swift in Sources */,
|
D0C7D49E24F7616A001EBDBB /* SecondaryNavigationView.swift in Sources */,
|
||||||
|
|
|
@ -21,8 +21,13 @@ public extension LoadMoreService {
|
||||||
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.afterStatusId : nil)
|
minID: direction == .up ? loadMore.beforeStatusId : nil)
|
||||||
.flatMap { contentDatabase.insert(statuses: $0.result, timeline: loadMore.timeline) }
|
.flatMap {
|
||||||
|
contentDatabase.insert(
|
||||||
|
statuses: $0.result,
|
||||||
|
timeline: loadMore.timeline,
|
||||||
|
loadMoreAndDirection: (loadMore, direction))
|
||||||
|
}
|
||||||
.eraseToAnyPublisher()
|
.eraseToAnyPublisher()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -74,6 +74,10 @@ public extension NavigationService {
|
||||||
func accountService(account: Account) -> AccountService {
|
func accountService(account: Account) -> AccountService {
|
||||||
AccountService(account: account, mastodonAPIClient: mastodonAPIClient, contentDatabase: contentDatabase)
|
AccountService(account: account, mastodonAPIClient: mastodonAPIClient, contentDatabase: contentDatabase)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func loadMoreService(loadMore: LoadMore) -> LoadMoreService {
|
||||||
|
LoadMoreService(loadMore: loadMore, mastodonAPIClient: mastodonAPIClient, contentDatabase: contentDatabase)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private extension NavigationService {
|
private extension NavigationService {
|
||||||
|
|
|
@ -28,11 +28,7 @@ class TableViewController: UITableViewController {
|
||||||
case (let accountListCell as AccountListCell, let accountViewModel as AccountViewModel):
|
case (let accountListCell as AccountListCell, let accountViewModel as AccountViewModel):
|
||||||
accountListCell.viewModel = accountViewModel
|
accountListCell.viewModel = accountViewModel
|
||||||
case (let loadMoreCell as LoadMoreCell, let loadMoreViewModel as LoadMoreViewModel):
|
case (let loadMoreCell as LoadMoreCell, let loadMoreViewModel as LoadMoreViewModel):
|
||||||
var contentConfiguration = loadMoreCell.defaultContentConfiguration()
|
loadMoreCell.viewModel = loadMoreViewModel
|
||||||
|
|
||||||
contentConfiguration.text = NSLocalizedString("load-more", comment: "")
|
|
||||||
|
|
||||||
loadMoreCell.contentConfiguration = contentConfiguration
|
|
||||||
default:
|
default:
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,7 +1,31 @@
|
||||||
// Copyright © 2020 Metabolist. All rights reserved.
|
// Copyright © 2020 Metabolist. All rights reserved.
|
||||||
|
|
||||||
import Combine
|
import Combine
|
||||||
|
import ServiceLayer
|
||||||
|
|
||||||
public class LoadMoreViewModel: ObservableObject {
|
public struct LoadMoreViewModel {
|
||||||
@Published var loading = false
|
public let loading: AnyPublisher<Bool, Never>
|
||||||
|
public let events: AnyPublisher<AnyPublisher<CollectionItemEvent, Error>, Never>
|
||||||
|
|
||||||
|
private let loadMoreService: LoadMoreService
|
||||||
|
private let eventsSubject = PassthroughSubject<AnyPublisher<CollectionItemEvent, Error>, Never>()
|
||||||
|
private let loadingSubject = PassthroughSubject<Bool, Never>()
|
||||||
|
|
||||||
|
init(loadMoreService: LoadMoreService) {
|
||||||
|
self.loadMoreService = loadMoreService
|
||||||
|
loading = loadingSubject.eraseToAnyPublisher()
|
||||||
|
events = eventsSubject.eraseToAnyPublisher()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension LoadMoreViewModel {
|
||||||
|
func loadMore() {
|
||||||
|
eventsSubject.send(
|
||||||
|
loadMoreService.request(direction: .down)
|
||||||
|
.handleEvents(
|
||||||
|
receiveSubscription: { _ in loadingSubject.send(true) },
|
||||||
|
receiveCompletion: { _ in loadingSubject.send(false) })
|
||||||
|
.map { _ in CollectionItemEvent.ignorableOutput }
|
||||||
|
.eraseToAnyPublisher())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -67,8 +67,8 @@ extension StatusListViewModel: CollectionViewModel {
|
||||||
statusListService: statusListService
|
statusListService: statusListService
|
||||||
.navigationService
|
.navigationService
|
||||||
.contextStatusListService(id: configuration.status.displayStatus.id))))
|
.contextStatusListService(id: configuration.status.displayStatus.id))))
|
||||||
default:
|
case .loadMore:
|
||||||
break
|
loadMoreViewModel(item: item)?.loadMore()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -85,7 +85,7 @@ extension StatusListViewModel: CollectionViewModel {
|
||||||
case .status:
|
case .status:
|
||||||
return statusViewModel(item: item)
|
return statusViewModel(item: item)
|
||||||
case .loadMore:
|
case .loadMore:
|
||||||
return LoadMoreViewModel()
|
return loadMoreViewModel(item: item)
|
||||||
default:
|
default:
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
@ -127,6 +127,33 @@ private extension StatusListViewModel {
|
||||||
return statusViewModel
|
return statusViewModel
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func loadMoreViewModel(item: CollectionItemIdentifier) -> LoadMoreViewModel? {
|
||||||
|
guard let timelineItem = timelineItems[item],
|
||||||
|
case let .loadMore(loadMore) = timelineItem
|
||||||
|
else { return nil }
|
||||||
|
|
||||||
|
if let cachedViewModel = viewModelCache[timelineItem]?.0 as? LoadMoreViewModel {
|
||||||
|
return cachedViewModel
|
||||||
|
}
|
||||||
|
|
||||||
|
let loadMoreViewModel = LoadMoreViewModel(
|
||||||
|
loadMoreService: statusListService.navigationService.loadMoreService(loadMore: loadMore))
|
||||||
|
|
||||||
|
viewModelCache[timelineItem] = (loadMoreViewModel, loadMoreViewModel.events
|
||||||
|
.flatMap { $0 }
|
||||||
|
.assignErrorsToAlertItem(to: \.alertItem, on: self)
|
||||||
|
.sink { [weak self] in
|
||||||
|
guard
|
||||||
|
let self = self,
|
||||||
|
let event = NavigationEvent($0)
|
||||||
|
else { return }
|
||||||
|
|
||||||
|
self.navigationEventsSubject.send(event)
|
||||||
|
})
|
||||||
|
|
||||||
|
return loadMoreViewModel
|
||||||
|
}
|
||||||
|
|
||||||
func process(sections: [[Timeline.Item]]) {
|
func process(sections: [[Timeline.Item]]) {
|
||||||
determineIfScrollPositionShouldBeMaintained(newSections: sections)
|
determineIfScrollPositionShouldBeMaintained(newSections: sections)
|
||||||
|
|
||||||
|
|
|
@ -15,9 +15,12 @@ class AccountListCell: UITableViewCell {
|
||||||
override func layoutSubviews() {
|
override func layoutSubviews() {
|
||||||
super.layoutSubviews()
|
super.layoutSubviews()
|
||||||
|
|
||||||
let isPhoneIdiom = UIDevice.current.userInterfaceIdiom == .phone
|
if UIDevice.current.userInterfaceIdiom == .phone {
|
||||||
|
separatorInset.left = 0
|
||||||
separatorInset.right = isPhoneIdiom ? 0 : layoutMargins.right
|
separatorInset.right = 0
|
||||||
separatorInset.left = isPhoneIdiom ? 0 : layoutMargins.left
|
} else {
|
||||||
|
separatorInset.left = layoutMargins.left
|
||||||
|
separatorInset.right = layoutMargins.right
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,11 +1,26 @@
|
||||||
// Copyright © 2020 Metabolist. All rights reserved.
|
// Copyright © 2020 Metabolist. All rights reserved.
|
||||||
|
|
||||||
import UIKit
|
import UIKit
|
||||||
|
import ViewModels
|
||||||
|
|
||||||
class LoadMoreCell: UITableViewCell {
|
class LoadMoreCell: UITableViewCell {
|
||||||
|
var viewModel: LoadMoreViewModel?
|
||||||
|
|
||||||
|
override func updateConfiguration(using state: UICellConfigurationState) {
|
||||||
|
guard let viewModel = viewModel else { return }
|
||||||
|
|
||||||
|
contentConfiguration = LoadMoreContentConfiguration(viewModel: viewModel)
|
||||||
|
}
|
||||||
|
|
||||||
override func layoutSubviews() {
|
override func layoutSubviews() {
|
||||||
super.layoutSubviews()
|
super.layoutSubviews()
|
||||||
|
|
||||||
separatorInset.left = UIDevice.current.userInterfaceIdiom == .phone ? 0 : layoutMargins.left
|
if UIDevice.current.userInterfaceIdiom == .phone {
|
||||||
|
separatorInset.left = 0
|
||||||
|
separatorInset.right = 0
|
||||||
|
} else {
|
||||||
|
separatorInset.left = layoutMargins.left
|
||||||
|
separatorInset.right = layoutMargins.right
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,18 @@
|
||||||
|
// Copyright © 2020 Metabolist. All rights reserved.
|
||||||
|
|
||||||
|
import UIKit
|
||||||
|
import ViewModels
|
||||||
|
|
||||||
|
struct LoadMoreContentConfiguration {
|
||||||
|
let viewModel: LoadMoreViewModel
|
||||||
|
}
|
||||||
|
|
||||||
|
extension LoadMoreContentConfiguration: UIContentConfiguration {
|
||||||
|
func makeContentView() -> UIView & UIContentView {
|
||||||
|
LoadMoreView(configuration: self)
|
||||||
|
}
|
||||||
|
|
||||||
|
func updated(for state: UIConfigurationState) -> LoadMoreContentConfiguration {
|
||||||
|
self
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,91 @@
|
||||||
|
// Copyright © 2020 Metabolist. All rights reserved.
|
||||||
|
|
||||||
|
import Combine
|
||||||
|
import UIKit
|
||||||
|
|
||||||
|
class LoadMoreView: UIView {
|
||||||
|
private let label = UILabel()
|
||||||
|
private let activityIndicatorView = UIActivityIndicatorView()
|
||||||
|
private var loadMoreConfiguration: LoadMoreContentConfiguration
|
||||||
|
private var loadingCancellable: AnyCancellable?
|
||||||
|
|
||||||
|
init(configuration: LoadMoreContentConfiguration) {
|
||||||
|
self.loadMoreConfiguration = configuration
|
||||||
|
|
||||||
|
super.init(frame: .zero)
|
||||||
|
|
||||||
|
initialSetup()
|
||||||
|
}
|
||||||
|
|
||||||
|
@available(*, unavailable)
|
||||||
|
required init?(coder: NSCoder) {
|
||||||
|
fatalError("init(coder:) has not been implemented")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension LoadMoreView: UIContentView {
|
||||||
|
var configuration: UIContentConfiguration {
|
||||||
|
get { loadMoreConfiguration }
|
||||||
|
set {
|
||||||
|
guard let loadMoreConfiguration = newValue as? LoadMoreContentConfiguration else { return }
|
||||||
|
|
||||||
|
self.loadMoreConfiguration = loadMoreConfiguration
|
||||||
|
|
||||||
|
applyLoadMoreConfiguration()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private extension LoadMoreView {
|
||||||
|
func initialSetup() {
|
||||||
|
let leadingArrowImageView = UIImageView()
|
||||||
|
let trailingArrowImageView = UIImageView()
|
||||||
|
|
||||||
|
for arrowImageView in [leadingArrowImageView, trailingArrowImageView] {
|
||||||
|
addSubview(arrowImageView)
|
||||||
|
arrowImageView.translatesAutoresizingMaskIntoConstraints = false
|
||||||
|
arrowImageView.image = UIImage(
|
||||||
|
systemName: "arrow.up.circle",
|
||||||
|
withConfiguration: UIImage.SymbolConfiguration(
|
||||||
|
pointSize: UIFont.preferredFont(forTextStyle: .title2).pointSize))
|
||||||
|
arrowImageView.setContentHuggingPriority(.required, for: .horizontal)
|
||||||
|
}
|
||||||
|
|
||||||
|
addSubview(label)
|
||||||
|
label.translatesAutoresizingMaskIntoConstraints = false
|
||||||
|
label.textAlignment = .center
|
||||||
|
label.font = .preferredFont(forTextStyle: .title2)
|
||||||
|
label.adjustsFontForContentSizeCategory = true
|
||||||
|
label.textColor = label.tintColor
|
||||||
|
label.text = NSLocalizedString("load-more", comment: "")
|
||||||
|
label.setContentHuggingPriority(.defaultLow, for: .horizontal)
|
||||||
|
|
||||||
|
addSubview(activityIndicatorView)
|
||||||
|
activityIndicatorView.translatesAutoresizingMaskIntoConstraints = false
|
||||||
|
activityIndicatorView.hidesWhenStopped = true
|
||||||
|
|
||||||
|
NSLayoutConstraint.activate([
|
||||||
|
leadingArrowImageView.leadingAnchor.constraint(equalTo: readableContentGuide.leadingAnchor),
|
||||||
|
leadingArrowImageView.topAnchor.constraint(equalTo: readableContentGuide.topAnchor),
|
||||||
|
leadingArrowImageView.bottomAnchor.constraint(equalTo: readableContentGuide.bottomAnchor),
|
||||||
|
label.leadingAnchor.constraint(equalTo: leadingArrowImageView.trailingAnchor),
|
||||||
|
label.topAnchor.constraint(greaterThanOrEqualTo: readableContentGuide.topAnchor),
|
||||||
|
label.bottomAnchor.constraint(greaterThanOrEqualTo: readableContentGuide.bottomAnchor),
|
||||||
|
label.trailingAnchor.constraint(equalTo: trailingArrowImageView.leadingAnchor),
|
||||||
|
trailingArrowImageView.topAnchor.constraint(equalTo: readableContentGuide.topAnchor),
|
||||||
|
trailingArrowImageView.bottomAnchor.constraint(equalTo: readableContentGuide.bottomAnchor),
|
||||||
|
trailingArrowImageView.trailingAnchor.constraint(equalTo: readableContentGuide.trailingAnchor),
|
||||||
|
activityIndicatorView.centerXAnchor.constraint(equalTo: centerXAnchor),
|
||||||
|
activityIndicatorView.centerYAnchor.constraint(equalTo: centerYAnchor)
|
||||||
|
])
|
||||||
|
}
|
||||||
|
|
||||||
|
func applyLoadMoreConfiguration() {
|
||||||
|
loadingCancellable = loadMoreConfiguration.viewModel.loading.sink { [weak self] in
|
||||||
|
guard let self = self else { return }
|
||||||
|
|
||||||
|
self.label.isHidden = $0
|
||||||
|
$0 ? self.activityIndicatorView.startAnimating() : self.activityIndicatorView.stopAnimating()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -125,7 +125,7 @@ private extension StatusView {
|
||||||
])
|
])
|
||||||
|
|
||||||
for constraint in separatorConstraints {
|
for constraint in separatorConstraints {
|
||||||
constraint.constant = 1 / UIScreen.main.scale
|
constraint.constant = .hairline
|
||||||
}
|
}
|
||||||
|
|
||||||
avatarImageView.kf.indicatorType = .activity
|
avatarImageView.kf.indicatorType = .activity
|
||||||
|
|
|
@ -6,6 +6,7 @@ extension CGFloat {
|
||||||
static let defaultSpacing: Self = 8
|
static let defaultSpacing: Self = 8
|
||||||
static let compactSpacing: Self = 4
|
static let compactSpacing: Self = 4
|
||||||
static let defaultCornerRadius: Self = 8
|
static let defaultCornerRadius: Self = 8
|
||||||
|
static let hairline = 1 / UIScreen.main.scale
|
||||||
}
|
}
|
||||||
|
|
||||||
extension TimeInterval {
|
extension TimeInterval {
|
||||||
|
|
Loading…
Reference in New Issue