Rewrite Sidebar select next unread in Combine
This commit is contained in:
parent
36aba33c3d
commit
75b9264d44
|
@ -0,0 +1,151 @@
|
|||
//
|
||||
// DemandBuffer.swift
|
||||
// CombineExt
|
||||
//
|
||||
// Created by Shai Mishali on 21/02/2020.
|
||||
// Copyright © 2020 Combine Community. All rights reserved.
|
||||
//
|
||||
|
||||
#if canImport(Combine)
|
||||
import Combine
|
||||
import class Foundation.NSRecursiveLock
|
||||
|
||||
/// A buffer responsible for managing the demand of a downstream
|
||||
/// subscriber for an upstream publisher
|
||||
///
|
||||
/// It buffers values and completion events and forwards them dynamically
|
||||
/// according to the demand requested by the downstream
|
||||
///
|
||||
/// In a sense, the subscription only relays the requests for demand, as well
|
||||
/// the events emitted by the upstream — to this buffer, which manages
|
||||
/// the entire behavior and backpressure contract
|
||||
@available(OSX 10.15, iOS 13.0, tvOS 13.0, watchOS 6.0, *)
|
||||
class DemandBuffer<S: Subscriber> {
|
||||
private let lock = NSRecursiveLock()
|
||||
private var buffer = [S.Input]()
|
||||
private let subscriber: S
|
||||
private var completion: Subscribers.Completion<S.Failure>?
|
||||
private var demandState = Demand()
|
||||
|
||||
/// Initialize a new demand buffer for a provided downstream subscriber
|
||||
///
|
||||
/// - parameter subscriber: The downstream subscriber demanding events
|
||||
init(subscriber: S) {
|
||||
self.subscriber = subscriber
|
||||
}
|
||||
|
||||
/// Buffer an upstream value to later be forwarded to
|
||||
/// the downstream subscriber, once it demands it
|
||||
///
|
||||
/// - parameter value: Upstream value to buffer
|
||||
///
|
||||
/// - returns: The demand fulfilled by the bufferr
|
||||
func buffer(value: S.Input) -> Subscribers.Demand {
|
||||
precondition(self.completion == nil,
|
||||
"How could a completed publisher sent values?! Beats me 🤷♂️")
|
||||
|
||||
switch demandState.requested {
|
||||
case .unlimited:
|
||||
return subscriber.receive(value)
|
||||
default:
|
||||
buffer.append(value)
|
||||
return flush()
|
||||
}
|
||||
}
|
||||
|
||||
/// Complete the demand buffer with an upstream completion event
|
||||
///
|
||||
/// This method will deplete the buffer immediately,
|
||||
/// based on the currently accumulated demand, and relay the
|
||||
/// completion event down as soon as demand is fulfilled
|
||||
///
|
||||
/// - parameter completion: Completion event
|
||||
func complete(completion: Subscribers.Completion<S.Failure>) {
|
||||
precondition(self.completion == nil,
|
||||
"Completion have already occured, which is quite awkward 🥺")
|
||||
|
||||
self.completion = completion
|
||||
_ = flush()
|
||||
}
|
||||
|
||||
/// Signal to the buffer that the downstream requested new demand
|
||||
///
|
||||
/// - note: The buffer will attempt to flush as many events rqeuested
|
||||
/// by the downstream at this point
|
||||
func demand(_ demand: Subscribers.Demand) -> Subscribers.Demand {
|
||||
flush(adding: demand)
|
||||
}
|
||||
|
||||
/// Flush buffered events to the downstream based on the current
|
||||
/// state of the downstream's demand
|
||||
///
|
||||
/// - parameter newDemand: The new demand to add. If `nil`, the flush isn't the
|
||||
/// result of an explicit demand change
|
||||
///
|
||||
/// - note: After fulfilling the downstream's request, if completion
|
||||
/// has already occured, the buffer will be cleared and the
|
||||
/// completion event will be sent to the downstream subscriber
|
||||
private func flush(adding newDemand: Subscribers.Demand? = nil) -> Subscribers.Demand {
|
||||
lock.lock()
|
||||
defer { lock.unlock() }
|
||||
|
||||
if let newDemand = newDemand {
|
||||
demandState.requested += newDemand
|
||||
}
|
||||
|
||||
// If buffer isn't ready for flushing, return immediately
|
||||
guard demandState.requested > 0 || newDemand == Subscribers.Demand.none else { return .none }
|
||||
|
||||
while !buffer.isEmpty && demandState.processed < demandState.requested {
|
||||
demandState.requested += subscriber.receive(buffer.remove(at: 0))
|
||||
demandState.processed += 1
|
||||
}
|
||||
|
||||
if let completion = completion {
|
||||
// Completion event was already sent
|
||||
buffer = []
|
||||
demandState = .init()
|
||||
self.completion = nil
|
||||
subscriber.receive(completion: completion)
|
||||
return .none
|
||||
}
|
||||
|
||||
let sentDemand = demandState.requested - demandState.sent
|
||||
demandState.sent += sentDemand
|
||||
return sentDemand
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Private Helpers
|
||||
@available(OSX 10.15, iOS 13.0, tvOS 13.0, watchOS 6.0, *)
|
||||
private extension DemandBuffer {
|
||||
/// A model that tracks the downstream's
|
||||
/// accumulated demand state
|
||||
struct Demand {
|
||||
var processed: Subscribers.Demand = .none
|
||||
var requested: Subscribers.Demand = .none
|
||||
var sent: Subscribers.Demand = .none
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Internally-scoped helpers
|
||||
@available(OSX 10.15, iOS 13.0, tvOS 13.0, watchOS 6.0, *)
|
||||
extension Subscription {
|
||||
/// Reqeust demand if it's not empty
|
||||
///
|
||||
/// - parameter demand: Requested demand
|
||||
func requestIfNeeded(_ demand: Subscribers.Demand) {
|
||||
guard demand > .none else { return }
|
||||
request(demand)
|
||||
}
|
||||
}
|
||||
|
||||
@available(OSX 10.15, iOS 13.0, tvOS 13.0, watchOS 6.0, *)
|
||||
extension Optional where Wrapped == Subscription {
|
||||
/// Cancel the Optional subscription and nullify it
|
||||
mutating func kill() {
|
||||
self?.cancel()
|
||||
self = nil
|
||||
}
|
||||
}
|
||||
#endif
|
|
@ -0,0 +1,101 @@
|
|||
//
|
||||
// Sink.swift
|
||||
// CombineExt
|
||||
//
|
||||
// Created by Shai Mishali on 14/03/2020.
|
||||
// Copyright © 2020 Combine Community. All rights reserved.
|
||||
//
|
||||
|
||||
#if canImport(Combine)
|
||||
import Combine
|
||||
|
||||
/// A generic sink using an underlying demand buffer to balance
|
||||
/// the demand of a downstream subscriber for the events of an
|
||||
/// upstream publisher
|
||||
@available(OSX 10.15, iOS 13.0, tvOS 13.0, watchOS 6.0, *)
|
||||
class Sink<Upstream: Publisher, Downstream: Subscriber>: Subscriber {
|
||||
typealias TransformFailure = (Upstream.Failure) -> Downstream.Failure?
|
||||
typealias TransformOutput = (Upstream.Output) -> Downstream.Input?
|
||||
|
||||
private(set) var buffer: DemandBuffer<Downstream>
|
||||
private var upstreamSubscription: Subscription?
|
||||
private let transformOutput: TransformOutput?
|
||||
private let transformFailure: TransformFailure?
|
||||
|
||||
/// Initialize a new sink subscribing to the upstream publisher and
|
||||
/// fulfilling the demand of the downstream subscriber using a backpresurre
|
||||
/// demand-maintaining buffer.
|
||||
///
|
||||
/// - parameter upstream: The upstream publisher
|
||||
/// - parameter downstream: The downstream subscriber
|
||||
/// - parameter transformOutput: Transform the upstream publisher's output type to the downstream's input type
|
||||
/// - parameter transformFailure: Transform the upstream failure type to the downstream's failure type
|
||||
///
|
||||
/// - note: You **must** provide the two transformation functions above if you're using
|
||||
/// the default `Sink` implementation. Otherwise, you must subclass `Sink` with your own
|
||||
/// publisher's sink and manage the buffer accordingly.
|
||||
init(upstream: Upstream,
|
||||
downstream: Downstream,
|
||||
transformOutput: TransformOutput? = nil,
|
||||
transformFailure: TransformFailure? = nil) {
|
||||
self.buffer = DemandBuffer(subscriber: downstream)
|
||||
self.transformOutput = transformOutput
|
||||
self.transformFailure = transformFailure
|
||||
upstream.subscribe(self)
|
||||
}
|
||||
|
||||
func demand(_ demand: Subscribers.Demand) {
|
||||
let newDemand = buffer.demand(demand)
|
||||
upstreamSubscription?.requestIfNeeded(newDemand)
|
||||
}
|
||||
|
||||
func receive(subscription: Subscription) {
|
||||
upstreamSubscription = subscription
|
||||
}
|
||||
|
||||
func receive(_ input: Upstream.Output) -> Subscribers.Demand {
|
||||
guard let transform = transformOutput else {
|
||||
fatalError("""
|
||||
❌ Missing output transformation
|
||||
=========================
|
||||
|
||||
You must either:
|
||||
- Provide a transformation function from the upstream's output to the downstream's input; or
|
||||
- Subclass `Sink` with your own publisher's Sink and manage the buffer yourself
|
||||
""")
|
||||
}
|
||||
|
||||
guard let input = transform(input) else { return .none }
|
||||
return buffer.buffer(value: input)
|
||||
}
|
||||
|
||||
func receive(completion: Subscribers.Completion<Upstream.Failure>) {
|
||||
switch completion {
|
||||
case .finished:
|
||||
buffer.complete(completion: .finished)
|
||||
case .failure(let error):
|
||||
guard let transform = transformFailure else {
|
||||
fatalError("""
|
||||
❌ Missing failure transformation
|
||||
=========================
|
||||
|
||||
You must either:
|
||||
- Provide a transformation function from the upstream's failure to the downstream's failuer; or
|
||||
- Subclass `Sink` with your own publisher's Sink and manage the buffer yourself
|
||||
""")
|
||||
}
|
||||
|
||||
guard let error = transform(error) else { return }
|
||||
buffer.complete(completion: .failure(error))
|
||||
}
|
||||
|
||||
cancelUpstream()
|
||||
}
|
||||
|
||||
func cancelUpstream() {
|
||||
upstreamSubscription.kill()
|
||||
}
|
||||
|
||||
deinit { cancelUpstream() }
|
||||
}
|
||||
#endif
|
|
@ -0,0 +1,238 @@
|
|||
//
|
||||
// WithLatestFrom.swift
|
||||
// CombineExt
|
||||
//
|
||||
// Created by Shai Mishali on 29/08/2019.
|
||||
// Copyright © 2020 Combine Community. All rights reserved.
|
||||
//
|
||||
|
||||
#if canImport(Combine)
|
||||
import Combine
|
||||
|
||||
// MARK: - Operator methods
|
||||
@available(OSX 10.15, iOS 13.0, tvOS 13.0, watchOS 6.0, *)
|
||||
public extension Publisher {
|
||||
/// Merges two publishers into a single publisher by combining each value
|
||||
/// from self with the latest value from the second publisher, if any.
|
||||
///
|
||||
/// - parameter other: A second publisher source.
|
||||
/// - parameter resultSelector: Function to invoke for each value from the self combined
|
||||
/// with the latest value from the second source, if any.
|
||||
///
|
||||
/// - returns: A publisher containing the result of combining each value of the self
|
||||
/// with the latest value from the second publisher, if any, using the
|
||||
/// specified result selector function.
|
||||
func withLatestFrom<Other: Publisher, Result>(_ other: Other,
|
||||
resultSelector: @escaping (Output, Other.Output) -> Result)
|
||||
-> Publishers.WithLatestFrom<Self, Other, Result> {
|
||||
return .init(upstream: self, second: other, resultSelector: resultSelector)
|
||||
}
|
||||
|
||||
/// Merges three publishers into a single publisher by combining each value
|
||||
/// from self with the latest value from the second and third publisher, if any.
|
||||
///
|
||||
/// - parameter other: A second publisher source.
|
||||
/// - parameter other1: A third publisher source.
|
||||
/// - parameter resultSelector: Function to invoke for each value from the self combined
|
||||
/// with the latest value from the second and third source, if any.
|
||||
///
|
||||
/// - returns: A publisher containing the result of combining each value of the self
|
||||
/// with the latest value from the second and third publisher, if any, using the
|
||||
/// specified result selector function.
|
||||
func withLatestFrom<Other: Publisher, Other1: Publisher, Result>(_ other: Other,
|
||||
_ other1: Other1,
|
||||
resultSelector: @escaping (Output, (Other.Output, Other1.Output)) -> Result)
|
||||
-> Publishers.WithLatestFrom<Self, AnyPublisher<(Other.Output, Other1.Output), Self.Failure>, Result>
|
||||
where Other.Failure == Failure, Other1.Failure == Failure {
|
||||
let combined = other.combineLatest(other1)
|
||||
.eraseToAnyPublisher()
|
||||
return .init(upstream: self, second: combined, resultSelector: resultSelector)
|
||||
}
|
||||
|
||||
/// Merges four publishers into a single publisher by combining each value
|
||||
/// from self with the latest value from the second, third and fourth publisher, if any.
|
||||
///
|
||||
/// - parameter other: A second publisher source.
|
||||
/// - parameter other1: A third publisher source.
|
||||
/// - parameter other2: A fourth publisher source.
|
||||
/// - parameter resultSelector: Function to invoke for each value from the self combined
|
||||
/// with the latest value from the second, third and fourth source, if any.
|
||||
///
|
||||
/// - returns: A publisher containing the result of combining each value of the self
|
||||
/// with the latest value from the second, third and fourth publisher, if any, using the
|
||||
/// specified result selector function.
|
||||
func withLatestFrom<Other: Publisher, Other1: Publisher, Other2: Publisher, Result>(_ other: Other,
|
||||
_ other1: Other1,
|
||||
_ other2: Other2,
|
||||
resultSelector: @escaping (Output, (Other.Output, Other1.Output, Other2.Output)) -> Result)
|
||||
-> Publishers.WithLatestFrom<Self, AnyPublisher<(Other.Output, Other1.Output, Other2.Output), Self.Failure>, Result>
|
||||
where Other.Failure == Failure, Other1.Failure == Failure, Other2.Failure == Failure {
|
||||
let combined = other.combineLatest(other1, other2)
|
||||
.eraseToAnyPublisher()
|
||||
return .init(upstream: self, second: combined, resultSelector: resultSelector)
|
||||
}
|
||||
|
||||
/// Upon an emission from self, emit the latest value from the
|
||||
/// second publisher, if any exists.
|
||||
///
|
||||
/// - parameter other: A second publisher source.
|
||||
///
|
||||
/// - returns: A publisher containing the latest value from the second publisher, if any.
|
||||
func withLatestFrom<Other: Publisher>(_ other: Other)
|
||||
-> Publishers.WithLatestFrom<Self, Other, Other.Output> {
|
||||
return .init(upstream: self, second: other) { $1 }
|
||||
}
|
||||
|
||||
/// Upon an emission from self, emit the latest value from the
|
||||
/// second and third publisher, if any exists.
|
||||
///
|
||||
/// - parameter other: A second publisher source.
|
||||
/// - parameter other1: A third publisher source.
|
||||
///
|
||||
/// - returns: A publisher containing the latest value from the second and third publisher, if any.
|
||||
func withLatestFrom<Other: Publisher, Other1: Publisher>(_ other: Other,
|
||||
_ other1: Other1)
|
||||
-> Publishers.WithLatestFrom<Self, AnyPublisher<(Other.Output, Other1.Output), Self.Failure>, (Other.Output, Other1.Output)>
|
||||
where Other.Failure == Failure, Other1.Failure == Failure {
|
||||
withLatestFrom(other, other1) { $1 }
|
||||
}
|
||||
|
||||
/// Upon an emission from self, emit the latest value from the
|
||||
/// second, third and forth publisher, if any exists.
|
||||
///
|
||||
/// - parameter other: A second publisher source.
|
||||
/// - parameter other1: A third publisher source.
|
||||
/// - parameter other2: A forth publisher source.
|
||||
///
|
||||
/// - returns: A publisher containing the latest value from the second, third and forth publisher, if any.
|
||||
func withLatestFrom<Other: Publisher, Other1: Publisher, Other2: Publisher>(_ other: Other,
|
||||
_ other1: Other1,
|
||||
_ other2: Other2)
|
||||
-> Publishers.WithLatestFrom<Self, AnyPublisher<(Other.Output, Other1.Output, Other2.Output), Self.Failure>, (Other.Output, Other1.Output, Other2.Output)>
|
||||
where Other.Failure == Failure, Other1.Failure == Failure, Other2.Failure == Failure {
|
||||
withLatestFrom(other, other1, other2) { $1 }
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Publisher
|
||||
@available(OSX 10.15, iOS 13.0, tvOS 13.0, watchOS 6.0, *)
|
||||
public extension Publishers {
|
||||
struct WithLatestFrom<Upstream: Publisher,
|
||||
Other: Publisher,
|
||||
Output>: Publisher where Upstream.Failure == Other.Failure {
|
||||
public typealias Failure = Upstream.Failure
|
||||
public typealias ResultSelector = (Upstream.Output, Other.Output) -> Output
|
||||
|
||||
private let upstream: Upstream
|
||||
private let second: Other
|
||||
private let resultSelector: ResultSelector
|
||||
private var latestValue: Other.Output?
|
||||
|
||||
init(upstream: Upstream,
|
||||
second: Other,
|
||||
resultSelector: @escaping ResultSelector) {
|
||||
self.upstream = upstream
|
||||
self.second = second
|
||||
self.resultSelector = resultSelector
|
||||
}
|
||||
|
||||
public func receive<S: Subscriber>(subscriber: S) where Failure == S.Failure, Output == S.Input {
|
||||
subscriber.receive(subscription: Subscription(upstream: upstream,
|
||||
downstream: subscriber,
|
||||
second: second,
|
||||
resultSelector: resultSelector))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Subscription
|
||||
@available(OSX 10.15, iOS 13.0, tvOS 13.0, watchOS 6.0, *)
|
||||
private extension Publishers.WithLatestFrom {
|
||||
class Subscription<Downstream: Subscriber>: Combine.Subscription, CustomStringConvertible where Downstream.Input == Output, Downstream.Failure == Failure {
|
||||
private let resultSelector: ResultSelector
|
||||
private var sink: Sink<Upstream, Downstream>?
|
||||
|
||||
private let upstream: Upstream
|
||||
private let downstream: Downstream
|
||||
private let second: Other
|
||||
|
||||
// Secondary (other) publisher
|
||||
private var latestValue: Other.Output?
|
||||
private var otherSubscription: Cancellable?
|
||||
private var preInitialDemand = Subscribers.Demand.none
|
||||
|
||||
init(upstream: Upstream,
|
||||
downstream: Downstream,
|
||||
second: Other,
|
||||
resultSelector: @escaping ResultSelector) {
|
||||
self.upstream = upstream
|
||||
self.second = second
|
||||
self.downstream = downstream
|
||||
self.resultSelector = resultSelector
|
||||
|
||||
trackLatestFromSecond { [weak self] in
|
||||
guard let self = self else { return }
|
||||
self.request(self.preInitialDemand)
|
||||
self.preInitialDemand = .none
|
||||
}
|
||||
}
|
||||
|
||||
func request(_ demand: Subscribers.Demand) {
|
||||
guard latestValue != nil else {
|
||||
preInitialDemand += demand
|
||||
return
|
||||
}
|
||||
|
||||
self.sink?.demand(demand)
|
||||
}
|
||||
|
||||
// Create an internal subscription to the `Other` publisher,
|
||||
// constantly tracking its latest value
|
||||
private func trackLatestFromSecond(onInitialValue: @escaping () -> Void) {
|
||||
var gotInitialValue = false
|
||||
|
||||
let subscriber = AnySubscriber<Other.Output, Other.Failure>(
|
||||
receiveSubscription: { [weak self] subscription in
|
||||
self?.otherSubscription = subscription
|
||||
subscription.request(.unlimited)
|
||||
},
|
||||
receiveValue: { [weak self] value in
|
||||
guard let self = self else { return .none }
|
||||
self.latestValue = value
|
||||
|
||||
if !gotInitialValue {
|
||||
// When getting initial value, start pulling values
|
||||
// from upstream in the main sink
|
||||
self.sink = Sink(upstream: self.upstream,
|
||||
downstream: self.downstream,
|
||||
transformOutput: { [weak self] value in
|
||||
guard let self = self,
|
||||
let other = self.latestValue else { return nil }
|
||||
|
||||
return self.resultSelector(value, other)
|
||||
},
|
||||
transformFailure: { $0 })
|
||||
|
||||
// Signal initial value to start fulfilling downstream demand
|
||||
gotInitialValue = true
|
||||
onInitialValue()
|
||||
}
|
||||
|
||||
return .unlimited
|
||||
},
|
||||
receiveCompletion: nil)
|
||||
|
||||
self.second.subscribe(subscriber)
|
||||
}
|
||||
|
||||
var description: String {
|
||||
return "WithLatestFrom.Subscription<\(Output.self), \(Failure.self)>"
|
||||
}
|
||||
|
||||
func cancel() {
|
||||
sink = nil
|
||||
otherSubscription?.cancel()
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif
|
|
@ -57,7 +57,7 @@ final class SceneModel: ObservableObject {
|
|||
func goToNextUnread() {
|
||||
if !timelineModel.goToNextUnread() {
|
||||
timelineModel.isSelectNextUnread = true
|
||||
sidebarModel.goToNextUnread()
|
||||
sidebarModel.selectNextUnread.send(true)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -21,6 +21,7 @@ class SidebarModel: ObservableObject, UndoableCommandRunner {
|
|||
weak var delegate: SidebarModelDelegate?
|
||||
|
||||
var sidebarItemsPublisher: AnyPublisher<[SidebarItem], Never>?
|
||||
var selectNextUnread = PassthroughSubject<Bool, Never>()
|
||||
|
||||
@Published var selectedFeedIdentifiers = Set<FeedIdentifier>()
|
||||
@Published var selectedFeedIdentifier: FeedIdentifier? = .none
|
||||
|
@ -35,27 +36,16 @@ class SidebarModel: ObservableObject, UndoableCommandRunner {
|
|||
init() {
|
||||
subscribeToSelectedFeedChanges()
|
||||
subscribeToRebuildSidebarItemsEvents()
|
||||
}
|
||||
|
||||
// MARK: API
|
||||
|
||||
func goToNextUnread() {
|
||||
// guard let startFeed = selectedFeeds.first ?? sidebarItems.first?.children.first?.feed else { return }
|
||||
//
|
||||
// if !goToNextUnread(startingAt: startFeed) {
|
||||
// if let firstFeed = sidebarItems.first?.children.first?.feed {
|
||||
// goToNextUnread(startingAt: firstFeed)
|
||||
// }
|
||||
// }
|
||||
subscribeToNextUnread()
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// MARK: Side Context Menu Actions
|
||||
|
||||
extension SidebarModel {
|
||||
|
||||
func markAllAsRead(feed: Feed) {
|
||||
|
||||
var articles = Set<Article>()
|
||||
let fetchedArticles = try! feed.fetchArticles()
|
||||
for article in fetchedArticles {
|
||||
|
@ -190,6 +180,20 @@ private extension SidebarModel {
|
|||
.eraseToAnyPublisher()
|
||||
}
|
||||
|
||||
func subscribeToNextUnread() {
|
||||
guard let sidebarItemsPublisher = sidebarItemsPublisher else { return }
|
||||
|
||||
selectNextUnread
|
||||
.withLatestFrom(sidebarItemsPublisher, $selectedFeeds)
|
||||
.compactMap { [weak self] (sidebarItems, selectedFeeds) in
|
||||
return self?.nextUnread(sidebarItems: sidebarItems, selectedFeeds: selectedFeeds)
|
||||
}
|
||||
.sink { [weak self] nextFeedID in
|
||||
self?.select(nextFeedID)
|
||||
}
|
||||
.store(in: &cancellables)
|
||||
}
|
||||
|
||||
// MARK: Sidebar Building
|
||||
|
||||
func sort(_ folders: Set<Folder>) -> [Folder] {
|
||||
|
@ -254,38 +258,47 @@ private extension SidebarModel {
|
|||
}
|
||||
}
|
||||
|
||||
// @discardableResult
|
||||
// func goToNextUnread(startingAt: Feed) -> Bool {
|
||||
//
|
||||
// var foundStartFeed = false
|
||||
// var nextSidebarItem: SidebarItem? = nil
|
||||
// for section in sidebarItems {
|
||||
// if nextSidebarItem == nil {
|
||||
// section.visit { sidebarItem in
|
||||
// if !foundStartFeed && sidebarItem.feed?.feedID == startingAt.feedID {
|
||||
// foundStartFeed = true
|
||||
// return false
|
||||
// }
|
||||
// if foundStartFeed && sidebarItem.unreadCount > 0 {
|
||||
// nextSidebarItem = sidebarItem
|
||||
// return true
|
||||
// }
|
||||
// return false
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// if let nextFeedID = nextSidebarItem?.feed?.feedID {
|
||||
// select(nextFeedID)
|
||||
// return true
|
||||
// }
|
||||
//
|
||||
// return false
|
||||
// }
|
||||
//
|
||||
// func select(_ feedID: FeedIdentifier) {
|
||||
// selectedFeedIdentifiers = Set([feedID])
|
||||
// selectedFeedIdentifier = feedID
|
||||
// }
|
||||
func nextUnread(sidebarItems: [SidebarItem], selectedFeeds: [Feed]) -> FeedIdentifier? {
|
||||
guard let startFeed = selectedFeeds.first ?? sidebarItems.first?.children.first?.feed else { return nil }
|
||||
|
||||
if let feedID = nextUnread(sidebarItems: sidebarItems, startingAt: startFeed) {
|
||||
return feedID
|
||||
} else {
|
||||
if let firstFeed = sidebarItems.first?.children.first?.feed {
|
||||
return nextUnread(sidebarItems: sidebarItems, startingAt: firstFeed)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
func nextUnread(sidebarItems: [SidebarItem], startingAt: Feed) -> FeedIdentifier? {
|
||||
var foundStartFeed = false
|
||||
var nextSidebarItem: SidebarItem? = nil
|
||||
|
||||
for section in sidebarItems {
|
||||
if nextSidebarItem == nil {
|
||||
section.visit { sidebarItem in
|
||||
if !foundStartFeed && sidebarItem.feed?.feedID == startingAt.feedID {
|
||||
foundStartFeed = true
|
||||
return false
|
||||
}
|
||||
if foundStartFeed && sidebarItem.unreadCount > 0 {
|
||||
nextSidebarItem = sidebarItem
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nextSidebarItem?.feed?.feedID
|
||||
}
|
||||
|
||||
func select(_ feedID: FeedIdentifier) {
|
||||
selectedFeedIdentifiers = Set([feedID])
|
||||
selectedFeedIdentifier = feedID
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -332,6 +332,12 @@
|
|||
51A5769624AE617200078888 /* ArticleContainerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51A5769524AE617200078888 /* ArticleContainerView.swift */; };
|
||||
51A5769724AE617200078888 /* ArticleContainerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51A5769524AE617200078888 /* ArticleContainerView.swift */; };
|
||||
51A66685238075AE00CB272D /* AddWebFeedDefaultContainer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51A66684238075AE00CB272D /* AddWebFeedDefaultContainer.swift */; };
|
||||
51A8001224CA0FC700F41F1D /* Sink.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51A8001124CA0FC700F41F1D /* Sink.swift */; };
|
||||
51A8001324CA0FC700F41F1D /* Sink.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51A8001124CA0FC700F41F1D /* Sink.swift */; };
|
||||
51A8001524CA0FEC00F41F1D /* DemandBuffer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51A8001424CA0FEC00F41F1D /* DemandBuffer.swift */; };
|
||||
51A8001624CA0FEC00F41F1D /* DemandBuffer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51A8001424CA0FEC00F41F1D /* DemandBuffer.swift */; };
|
||||
51A8FFED24CA0CF400F41F1D /* WIthLatestFrom.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51A8FFEC24CA0CF400F41F1D /* WIthLatestFrom.swift */; };
|
||||
51A8FFEE24CA0CF400F41F1D /* WIthLatestFrom.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51A8FFEC24CA0CF400F41F1D /* WIthLatestFrom.swift */; };
|
||||
51A9A5E12380C4FE0033AADF /* AppDefaults.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51C45255226507D200C03939 /* AppDefaults.swift */; };
|
||||
51A9A5E42380C8880033AADF /* ShareFolderPickerAccountCell.xib in Resources */ = {isa = PBXBuildFile; fileRef = 51A9A5E32380C8870033AADF /* ShareFolderPickerAccountCell.xib */; };
|
||||
51A9A5E62380C8B20033AADF /* ShareFolderPickerFolderCell.xib in Resources */ = {isa = PBXBuildFile; fileRef = 51A9A5E52380C8B20033AADF /* ShareFolderPickerFolderCell.xib */; };
|
||||
|
@ -2023,6 +2029,9 @@
|
|||
51A16996235E10D700EB091F /* FeedbinAccountViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FeedbinAccountViewController.swift; sourceTree = "<group>"; };
|
||||
51A5769524AE617200078888 /* ArticleContainerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ArticleContainerView.swift; sourceTree = "<group>"; };
|
||||
51A66684238075AE00CB272D /* AddWebFeedDefaultContainer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddWebFeedDefaultContainer.swift; sourceTree = "<group>"; };
|
||||
51A8001124CA0FC700F41F1D /* Sink.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Sink.swift; sourceTree = "<group>"; };
|
||||
51A8001424CA0FEC00F41F1D /* DemandBuffer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DemandBuffer.swift; sourceTree = "<group>"; };
|
||||
51A8FFEC24CA0CF400F41F1D /* WIthLatestFrom.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WIthLatestFrom.swift; sourceTree = "<group>"; };
|
||||
51A9A5E32380C8870033AADF /* ShareFolderPickerAccountCell.xib */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.xib; path = ShareFolderPickerAccountCell.xib; sourceTree = "<group>"; };
|
||||
51A9A5E52380C8B20033AADF /* ShareFolderPickerFolderCell.xib */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.xib; path = ShareFolderPickerFolderCell.xib; sourceTree = "<group>"; };
|
||||
51A9A5E72380CA130033AADF /* ShareFolderPickerCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShareFolderPickerCell.swift; sourceTree = "<group>"; };
|
||||
|
@ -3016,6 +3025,16 @@
|
|||
path = Article;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
51A8001024CA0FAE00F41F1D /* CombineExt */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
51A8001424CA0FEC00F41F1D /* DemandBuffer.swift */,
|
||||
51A8001124CA0FC700F41F1D /* Sink.swift */,
|
||||
51A8FFEC24CA0CF400F41F1D /* WIthLatestFrom.swift */,
|
||||
);
|
||||
path = CombineExt;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
51B5C85A23F22A7A00032075 /* CommonExtension */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
|
@ -3080,6 +3099,7 @@
|
|||
51C0513824A77DF800194D5E /* Assets.xcassets */,
|
||||
17930ED224AF10CD00A9BA52 /* Add */,
|
||||
51A576B924AE617B00078888 /* Article */,
|
||||
51A8001024CA0FAE00F41F1D /* CombineExt */,
|
||||
51919FB124AAB95300541E64 /* Images */,
|
||||
17897AA724C281520014BA03 /* Inspector */,
|
||||
514E6BFD24AD252400AC6F6E /* Previews */,
|
||||
|
@ -4297,46 +4317,46 @@
|
|||
TargetAttributes = {
|
||||
51314636235A7BBE00387FDC = {
|
||||
CreatedOnToolsVersion = 11.2;
|
||||
DevelopmentTeam = FQLBNX3GP7;
|
||||
DevelopmentTeam = SHJK2V3AJG;
|
||||
LastSwiftMigration = 1120;
|
||||
ProvisioningStyle = Automatic;
|
||||
};
|
||||
513C5CE5232571C2003D4054 = {
|
||||
CreatedOnToolsVersion = 11.0;
|
||||
DevelopmentTeam = FQLBNX3GP7;
|
||||
DevelopmentTeam = SHJK2V3AJG;
|
||||
ProvisioningStyle = Automatic;
|
||||
};
|
||||
518B2ED12351B3DD00400001 = {
|
||||
CreatedOnToolsVersion = 11.2;
|
||||
DevelopmentTeam = FQLBNX3GP7;
|
||||
DevelopmentTeam = SHJK2V3AJG;
|
||||
ProvisioningStyle = Automatic;
|
||||
TestTargetID = 840D617B2029031C009BC708;
|
||||
};
|
||||
51C0513C24A77DF800194D5E = {
|
||||
CreatedOnToolsVersion = 12.0;
|
||||
DevelopmentTeam = FQLBNX3GP7;
|
||||
DevelopmentTeam = SHJK2V3AJG;
|
||||
ProvisioningStyle = Automatic;
|
||||
};
|
||||
51C0514324A77DF800194D5E = {
|
||||
CreatedOnToolsVersion = 12.0;
|
||||
DevelopmentTeam = FQLBNX3GP7;
|
||||
DevelopmentTeam = SHJK2V3AJG;
|
||||
ProvisioningStyle = Automatic;
|
||||
};
|
||||
6581C73220CED60000F4AD34 = {
|
||||
DevelopmentTeam = FQLBNX3GP7;
|
||||
DevelopmentTeam = SHJK2V3AJG;
|
||||
ProvisioningStyle = Automatic;
|
||||
};
|
||||
65ED3FA2235DEF6C0081F399 = {
|
||||
DevelopmentTeam = FQLBNX3GP7;
|
||||
DevelopmentTeam = SHJK2V3AJG;
|
||||
ProvisioningStyle = Automatic;
|
||||
};
|
||||
65ED4090235DEF770081F399 = {
|
||||
DevelopmentTeam = FQLBNX3GP7;
|
||||
DevelopmentTeam = SHJK2V3AJG;
|
||||
ProvisioningStyle = Automatic;
|
||||
};
|
||||
840D617B2029031C009BC708 = {
|
||||
CreatedOnToolsVersion = 9.3;
|
||||
DevelopmentTeam = FQLBNX3GP7;
|
||||
DevelopmentTeam = SHJK2V3AJG;
|
||||
ProvisioningStyle = Automatic;
|
||||
SystemCapabilities = {
|
||||
com.apple.BackgroundModes = {
|
||||
|
@ -4346,7 +4366,7 @@
|
|||
};
|
||||
849C645F1ED37A5D003D8FC0 = {
|
||||
CreatedOnToolsVersion = 8.2.1;
|
||||
DevelopmentTeam = FQLBNX3GP7;
|
||||
DevelopmentTeam = SHJK2V3AJG;
|
||||
ProvisioningStyle = Automatic;
|
||||
SystemCapabilities = {
|
||||
com.apple.HardenedRuntime = {
|
||||
|
@ -4356,7 +4376,7 @@
|
|||
};
|
||||
849C64701ED37A5D003D8FC0 = {
|
||||
CreatedOnToolsVersion = 8.2.1;
|
||||
DevelopmentTeam = FQLBNX3GP7;
|
||||
DevelopmentTeam = SHJK2V3AJG;
|
||||
ProvisioningStyle = Automatic;
|
||||
TestTargetID = 849C645F1ED37A5D003D8FC0;
|
||||
};
|
||||
|
@ -5174,6 +5194,7 @@
|
|||
65082A5424C73D2F009FA994 /* AccountCredentialsError.swift in Sources */,
|
||||
51E4990B24A808C500B667CB /* ImageDownloader.swift in Sources */,
|
||||
51E498F424A8085D00B667CB /* SmartFeedDelegate.swift in Sources */,
|
||||
51A8001524CA0FEC00F41F1D /* DemandBuffer.swift in Sources */,
|
||||
514E6BFF24AD255D00AC6F6E /* PreviewArticles.swift in Sources */,
|
||||
51E4993024A8676400B667CB /* ArticleSorter.swift in Sources */,
|
||||
51408B7E24A9EC6F0073CF4E /* SidebarItem.swift in Sources */,
|
||||
|
@ -5196,6 +5217,7 @@
|
|||
51E4990124A808BB00B667CB /* FaviconURLFinder.swift in Sources */,
|
||||
51E4991D24A8092100B667CB /* NSAttributedString+NetNewsWire.swift in Sources */,
|
||||
65082A2F24C72AC8009FA994 /* SettingsCredentialsAccountView.swift in Sources */,
|
||||
51A8FFED24CA0CF400F41F1D /* WIthLatestFrom.swift in Sources */,
|
||||
51E499FD24A9137600B667CB /* SidebarModel.swift in Sources */,
|
||||
5181C66224B0C326002E0F70 /* SettingsModel.swift in Sources */,
|
||||
51E4995324A8734D00B667CB /* RedditFeedProvider-Extensions.swift in Sources */,
|
||||
|
@ -5248,6 +5270,7 @@
|
|||
5177472224B38CAE00EB0F74 /* ArticleExtractorButtonState.swift in Sources */,
|
||||
5177471A24B3863000EB0F74 /* WebViewProvider.swift in Sources */,
|
||||
51E4992124A8095000B667CB /* RSImage-Extensions.swift in Sources */,
|
||||
51A8001224CA0FC700F41F1D /* Sink.swift in Sources */,
|
||||
51E4990324A808BB00B667CB /* FaviconDownloader.swift in Sources */,
|
||||
172199ED24AB2E0100A31D04 /* SafariView.swift in Sources */,
|
||||
65ACE48624B477C9003AE06A /* SettingsAccountLabelView.swift in Sources */,
|
||||
|
@ -5284,11 +5307,13 @@
|
|||
51B8BCC324C25C3E00360B00 /* SidebarContextMenu.swift in Sources */,
|
||||
51E498C724A8085D00B667CB /* StarredFeedDelegate.swift in Sources */,
|
||||
5194736F24BBB937001A2939 /* HiddenModifier.swift in Sources */,
|
||||
51A8001624CA0FEC00F41F1D /* DemandBuffer.swift in Sources */,
|
||||
51919FB724AABCA100541E64 /* IconImageView.swift in Sources */,
|
||||
51B54A6924B54A490014348B /* IconView.swift in Sources */,
|
||||
17897ACB24C281A40014BA03 /* InspectorView.swift in Sources */,
|
||||
51E498FA24A808BA00B667CB /* SingleFaviconDownloader.swift in Sources */,
|
||||
1727B39924C1368D00A4DBDC /* LayoutPreferencesView.swift in Sources */,
|
||||
51A8FFEE24CA0CF400F41F1D /* WIthLatestFrom.swift in Sources */,
|
||||
51E4993F24A8713B00B667CB /* ArticleStatusSyncTimer.swift in Sources */,
|
||||
51E4993724A8680E00B667CB /* Reachability.swift in Sources */,
|
||||
51B80F4424BE58BF00C6C32D /* SharingServiceDelegate.swift in Sources */,
|
||||
|
@ -5407,6 +5432,7 @@
|
|||
1769E32724BC5B6C000E1E8E /* AddAccountModel.swift in Sources */,
|
||||
1729529424AA1CAA00D65E66 /* AdvancedPreferencesView.swift in Sources */,
|
||||
5177470424B2657F00EB0F74 /* TimelineToolbarModifier.swift in Sources */,
|
||||
51A8001324CA0FC700F41F1D /* Sink.swift in Sources */,
|
||||
51E4992D24A8676300B667CB /* FetchRequestOperation.swift in Sources */,
|
||||
51E4992424A8098400B667CB /* SmartFeedPasteboardWriter.swift in Sources */,
|
||||
51E4991424A808FF00B667CB /* ArticleStringFormatter.swift in Sources */,
|
||||
|
|
Loading…
Reference in New Issue