Compare commits
No commits in common. "c73784aa81af5025a5805c7d50cf64c1af1752de" and "2467297f046c396601663954428476f52235ba89" have entirely different histories.
c73784aa81
...
2467297f04
|
@ -7,53 +7,26 @@
|
||||||
|
|
||||||
import Foundation
|
import Foundation
|
||||||
|
|
||||||
public struct TimelineMarkers {
|
public struct TimelineMarkers: Decodable, Sendable {
|
||||||
private init() {}
|
public let home: Marker?
|
||||||
|
public let notifications: Marker?
|
||||||
|
|
||||||
public static func request<T: TimelineMarkerType>(timeline: T) -> Request<TimelineMarker<T.Payload>> {
|
public static func request(timelines: [Timeline]) -> Request<TimelineMarkers> {
|
||||||
Request(method: .get, path: "/api/v1/markers", queryParameters: ["timeline[]" => T.name])
|
return Request(method: .get, path: "/api/v1/markers", queryParameters: "timeline[]" => timelines.map(\.rawValue))
|
||||||
}
|
}
|
||||||
|
|
||||||
public static func update<T: TimelineMarkerType>(timeline: T, lastReadID: String) -> Request<Empty> {
|
public static func update(timeline: Timeline, lastReadID: String) -> Request<Empty> {
|
||||||
Request(method: .post, path: "/api/v1/markers", body: ParametersBody([
|
return Request(method: .post, path: "/api/v1/markers", body: ParametersBody([
|
||||||
"\(T.name)[last_read_id]" => lastReadID
|
"\(timeline.rawValue)[last_read_id]" => lastReadID,
|
||||||
]))
|
]))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public enum Timeline: String {
|
||||||
|
case home
|
||||||
|
case notifications
|
||||||
}
|
}
|
||||||
|
|
||||||
public struct TimelineMarker<Payload: TimelineMarkerTypePayload>: Decodable, Sendable {
|
public struct Marker: Decodable, Sendable {
|
||||||
let payload: Payload
|
|
||||||
|
|
||||||
public var lastReadID: String {
|
|
||||||
payload.payload.lastReadID
|
|
||||||
}
|
|
||||||
public var version: Int {
|
|
||||||
payload.payload.version
|
|
||||||
}
|
|
||||||
public var updatedAt: Date {
|
|
||||||
payload.payload.updatedAt
|
|
||||||
}
|
|
||||||
|
|
||||||
public init(from decoder: any Decoder) throws {
|
|
||||||
self.payload = try Payload(from: decoder)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public protocol TimelineMarkerTypePayload: Decodable, Sendable {
|
|
||||||
var payload: MarkerPayload { get }
|
|
||||||
}
|
|
||||||
|
|
||||||
public struct HomeMarkerPayload: TimelineMarkerTypePayload {
|
|
||||||
public var home: MarkerPayload
|
|
||||||
public var payload: MarkerPayload { home }
|
|
||||||
}
|
|
||||||
|
|
||||||
public struct NotificationsMarkerPayload: TimelineMarkerTypePayload {
|
|
||||||
public var notifications: MarkerPayload
|
|
||||||
public var payload: MarkerPayload { notifications }
|
|
||||||
}
|
|
||||||
|
|
||||||
public struct MarkerPayload: Decodable, Sendable {
|
|
||||||
public let lastReadID: String
|
public let lastReadID: String
|
||||||
public let version: Int
|
public let version: Int
|
||||||
public let updatedAt: Date
|
public let updatedAt: Date
|
||||||
|
@ -64,26 +37,4 @@ public struct MarkerPayload: Decodable, Sendable {
|
||||||
case updatedAt = "updated_at"
|
case updatedAt = "updated_at"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public protocol TimelineMarkerType {
|
|
||||||
static var name: String { get }
|
|
||||||
associatedtype Payload: TimelineMarkerTypePayload
|
|
||||||
}
|
|
||||||
|
|
||||||
extension TimelineMarkerType where Self == HomeMarker {
|
|
||||||
public static var home: Self { .init() }
|
|
||||||
}
|
|
||||||
|
|
||||||
extension TimelineMarkerType where Self == NotificationsMarker {
|
|
||||||
public static var notifications: Self { .init() }
|
|
||||||
}
|
|
||||||
|
|
||||||
public struct HomeMarker: TimelineMarkerType {
|
|
||||||
public typealias Payload = HomeMarkerPayload
|
|
||||||
public static var name: String { "home" }
|
|
||||||
}
|
|
||||||
|
|
||||||
public struct NotificationsMarker: TimelineMarkerType {
|
|
||||||
public typealias Payload = NotificationsMarkerPayload
|
|
||||||
public static var name: String { "notifications" }
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -12,9 +12,6 @@ import Combine
|
||||||
#if canImport(Sentry)
|
#if canImport(Sentry)
|
||||||
import Sentry
|
import Sentry
|
||||||
#endif
|
#endif
|
||||||
import OSLog
|
|
||||||
|
|
||||||
private let logger = Logger(subsystem: Bundle.main.bundleIdentifier!, category: "NotificationsCVC")
|
|
||||||
|
|
||||||
class NotificationsCollectionViewController: UIViewController, TimelineLikeCollectionViewController, CollectionViewController {
|
class NotificationsCollectionViewController: UIViewController, TimelineLikeCollectionViewController, CollectionViewController {
|
||||||
|
|
||||||
|
@ -35,9 +32,6 @@ class NotificationsCollectionViewController: UIViewController, TimelineLikeColle
|
||||||
private var newer: RequestRange?
|
private var newer: RequestRange?
|
||||||
private var older: RequestRange?
|
private var older: RequestRange?
|
||||||
|
|
||||||
var updatesNotificationsMarker: Bool = false
|
|
||||||
private var newestDisplayedNotification: Item?
|
|
||||||
|
|
||||||
init(allowedTypes: [Pachyderm.Notification.Kind], mastodonController: MastodonController) {
|
init(allowedTypes: [Pachyderm.Notification.Kind], mastodonController: MastodonController) {
|
||||||
self.allowedTypes = allowedTypes
|
self.allowedTypes = allowedTypes
|
||||||
self.mastodonController = mastodonController
|
self.mastodonController = mastodonController
|
||||||
|
@ -211,12 +205,6 @@ class NotificationsCollectionViewController: UIViewController, TimelineLikeColle
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override func viewWillDisappear(_ animated: Bool) {
|
|
||||||
super.viewWillDisappear(animated)
|
|
||||||
|
|
||||||
updateNotificationsMarkerIfNecessary()
|
|
||||||
}
|
|
||||||
|
|
||||||
@objc func refresh() {
|
@objc func refresh() {
|
||||||
Task { @MainActor in
|
Task { @MainActor in
|
||||||
if case .notLoadedInitial = controller.state {
|
if case .notLoadedInitial = controller.state {
|
||||||
|
@ -323,23 +311,6 @@ class NotificationsCollectionViewController: UIViewController, TimelineLikeColle
|
||||||
await apply(snapshot, animatingDifferences: true)
|
await apply(snapshot, animatingDifferences: true)
|
||||||
}
|
}
|
||||||
|
|
||||||
private func updateNotificationsMarkerIfNecessary() {
|
|
||||||
guard updatesNotificationsMarker,
|
|
||||||
case let .group(group, _, _) = newestDisplayedNotification,
|
|
||||||
let notification = group.notifications.first else {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
logger.debug("Updating notifications marker with \(notification.id)")
|
|
||||||
Task {
|
|
||||||
let req = TimelineMarkers.update(timeline: .notifications, lastReadID: notification.id)
|
|
||||||
do {
|
|
||||||
_ = try await mastodonController.run(req)
|
|
||||||
} catch {
|
|
||||||
logger.error("Failed to update notifications marker: \(String(describing: error))")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
extension NotificationsCollectionViewController {
|
extension NotificationsCollectionViewController {
|
||||||
|
@ -574,21 +545,6 @@ extension NotificationsCollectionViewController: UICollectionViewDelegate {
|
||||||
guard case .notifications = dataSource.sectionIdentifier(for: indexPath.section) else {
|
guard case .notifications = dataSource.sectionIdentifier(for: indexPath.section) else {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if updatesNotificationsMarker {
|
|
||||||
let shouldUpdateNewestDisplayedNotification: Bool
|
|
||||||
if let newestDisplayedNotification,
|
|
||||||
let currentNewestIndexPath = dataSource.indexPath(for: newestDisplayedNotification) {
|
|
||||||
shouldUpdateNewestDisplayedNotification = indexPath < currentNewestIndexPath
|
|
||||||
} else {
|
|
||||||
shouldUpdateNewestDisplayedNotification = true
|
|
||||||
}
|
|
||||||
if shouldUpdateNewestDisplayedNotification,
|
|
||||||
let item = dataSource.itemIdentifier(for: indexPath) {
|
|
||||||
newestDisplayedNotification = item
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let itemsInSection = collectionView.numberOfItems(inSection: indexPath.section)
|
let itemsInSection = collectionView.numberOfItems(inSection: indexPath.section)
|
||||||
if indexPath.row == itemsInSection - 1 {
|
if indexPath.row == itemsInSection - 1 {
|
||||||
Task {
|
Task {
|
||||||
|
@ -809,9 +765,3 @@ extension NotificationsCollectionViewController: StatusBarTappableViewController
|
||||||
return .stop
|
return .stop
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
extension NotificationsCollectionViewController: BackgroundableViewController {
|
|
||||||
func sceneDidEnterBackground() {
|
|
||||||
updateNotificationsMarkerIfNecessary()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
|
@ -24,7 +24,6 @@ class NotificationsPageViewController: SegmentedPageViewController<Notifications
|
||||||
let vc = NotificationsCollectionViewController(allowedTypes: page.allowedTypes, mastodonController: mastodonController)
|
let vc = NotificationsCollectionViewController(allowedTypes: page.allowedTypes, mastodonController: mastodonController)
|
||||||
vc.title = page.title
|
vc.title = page.title
|
||||||
vc.userActivity = page.userActivity(accountID: mastodonController.accountInfo!.id)
|
vc.userActivity = page.userActivity(accountID: mastodonController.accountInfo!.id)
|
||||||
vc.updatesNotificationsMarker = page == .all
|
|
||||||
return vc
|
return vc
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -387,6 +387,16 @@ class TimelineViewController: UIViewController, TimelineLikeCollectionViewContro
|
||||||
_ = try await mastodonController.run(req)
|
_ = try await mastodonController.run(req)
|
||||||
} catch {
|
} catch {
|
||||||
stateRestorationLogger.error("TimelineViewController: failed to update timeline marker: \(String(describing: error))")
|
stateRestorationLogger.error("TimelineViewController: failed to update timeline marker: \(String(describing: error))")
|
||||||
|
|
||||||
|
#if canImport(Sentry)
|
||||||
|
if let error = error as? Client.Error,
|
||||||
|
case .networkError(_) = error.type {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
let event = Event(error: error)
|
||||||
|
event.message = SentryMessage(formatted: "Failed to update timeline marker: \(String(describing: error))")
|
||||||
|
SentrySDK.capture(event: event)
|
||||||
|
#endif
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -563,9 +573,12 @@ class TimelineViewController: UIViewController, TimelineLikeCollectionViewContro
|
||||||
@MainActor
|
@MainActor
|
||||||
private func restoreStatusesFromMarkerPosition() async -> Bool {
|
private func restoreStatusesFromMarkerPosition() async -> Bool {
|
||||||
do {
|
do {
|
||||||
let (marker, _) = try await mastodonController.run(TimelineMarkers.request(timeline: .home))
|
let (marker, _) = try await mastodonController.run(TimelineMarkers.request(timelines: [.home]))
|
||||||
async let status = try await mastodonController.run(Client.getStatus(id: marker.lastReadID)).0
|
guard let home = marker.home else {
|
||||||
async let olderStatuses = try await mastodonController.run(Client.getStatuses(timeline: .home, range: .before(id: marker.lastReadID, count: Self.pageSize))).0
|
return false
|
||||||
|
}
|
||||||
|
async let status = try await mastodonController.run(Client.getStatus(id: home.lastReadID)).0
|
||||||
|
async let olderStatuses = try await mastodonController.run(Client.getStatuses(timeline: .home, range: .before(id: home.lastReadID, count: Self.pageSize))).0
|
||||||
|
|
||||||
let allStatuses = try await [status] + olderStatuses
|
let allStatuses = try await [status] + olderStatuses
|
||||||
await mastodonController.persistentContainer.addAll(statuses: allStatuses)
|
await mastodonController.persistentContainer.addAll(statuses: allStatuses)
|
||||||
|
@ -577,11 +590,21 @@ class TimelineViewController: UIViewController, TimelineLikeCollectionViewContro
|
||||||
await apply(snapshot, animatingDifferences: false)
|
await apply(snapshot, animatingDifferences: false)
|
||||||
collectionView.contentOffset = CGPoint(x: 0, y: -collectionView.adjustedContentInset.top)
|
collectionView.contentOffset = CGPoint(x: 0, y: -collectionView.adjustedContentInset.top)
|
||||||
|
|
||||||
stateRestorationLogger.debug("TimelineViewController: restored from timeline marker with last read ID: \(marker.lastReadID)")
|
stateRestorationLogger.debug("TimelineViewController: restored from timeline marker with last read ID: \(home.lastReadID)")
|
||||||
|
|
||||||
return true
|
return true
|
||||||
} catch {
|
} catch {
|
||||||
stateRestorationLogger.error("TimelineViewController: failed to load from timeline marker: \(String(describing: error))")
|
stateRestorationLogger.error("TimelineViewController: failed to load from timeline marker: \(String(describing: error))")
|
||||||
|
|
||||||
|
#if canImport(Sentry)
|
||||||
|
if let error = error as? Client.Error,
|
||||||
|
case .networkError(_) = error.type {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
let event = Event(error: error)
|
||||||
|
event.message = SentryMessage(formatted: "Failed to load from timeline marker: \(String(describing: error))")
|
||||||
|
SentrySDK.capture(event: event)
|
||||||
|
#endif
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue