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() {
|
func goToNextUnread() {
|
||||||
if !timelineModel.goToNextUnread() {
|
if !timelineModel.goToNextUnread() {
|
||||||
timelineModel.isSelectNextUnread = true
|
timelineModel.isSelectNextUnread = true
|
||||||
sidebarModel.goToNextUnread()
|
sidebarModel.selectNextUnread.send(true)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -21,6 +21,7 @@ class SidebarModel: ObservableObject, UndoableCommandRunner {
|
||||||
weak var delegate: SidebarModelDelegate?
|
weak var delegate: SidebarModelDelegate?
|
||||||
|
|
||||||
var sidebarItemsPublisher: AnyPublisher<[SidebarItem], Never>?
|
var sidebarItemsPublisher: AnyPublisher<[SidebarItem], Never>?
|
||||||
|
var selectNextUnread = PassthroughSubject<Bool, Never>()
|
||||||
|
|
||||||
@Published var selectedFeedIdentifiers = Set<FeedIdentifier>()
|
@Published var selectedFeedIdentifiers = Set<FeedIdentifier>()
|
||||||
@Published var selectedFeedIdentifier: FeedIdentifier? = .none
|
@Published var selectedFeedIdentifier: FeedIdentifier? = .none
|
||||||
|
@ -35,27 +36,16 @@ class SidebarModel: ObservableObject, UndoableCommandRunner {
|
||||||
init() {
|
init() {
|
||||||
subscribeToSelectedFeedChanges()
|
subscribeToSelectedFeedChanges()
|
||||||
subscribeToRebuildSidebarItemsEvents()
|
subscribeToRebuildSidebarItemsEvents()
|
||||||
}
|
subscribeToNextUnread()
|
||||||
|
|
||||||
// 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)
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: Side Context Menu Actions
|
// MARK: Side Context Menu Actions
|
||||||
|
|
||||||
extension SidebarModel {
|
extension SidebarModel {
|
||||||
|
|
||||||
func markAllAsRead(feed: Feed) {
|
func markAllAsRead(feed: Feed) {
|
||||||
|
|
||||||
var articles = Set<Article>()
|
var articles = Set<Article>()
|
||||||
let fetchedArticles = try! feed.fetchArticles()
|
let fetchedArticles = try! feed.fetchArticles()
|
||||||
for article in fetchedArticles {
|
for article in fetchedArticles {
|
||||||
|
@ -190,6 +180,20 @@ private extension SidebarModel {
|
||||||
.eraseToAnyPublisher()
|
.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
|
// MARK: Sidebar Building
|
||||||
|
|
||||||
func sort(_ folders: Set<Folder>) -> [Folder] {
|
func sort(_ folders: Set<Folder>) -> [Folder] {
|
||||||
|
@ -254,38 +258,47 @@ private extension SidebarModel {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// @discardableResult
|
func nextUnread(sidebarItems: [SidebarItem], selectedFeeds: [Feed]) -> FeedIdentifier? {
|
||||||
// func goToNextUnread(startingAt: Feed) -> Bool {
|
guard let startFeed = selectedFeeds.first ?? sidebarItems.first?.children.first?.feed else { return nil }
|
||||||
//
|
|
||||||
// var foundStartFeed = false
|
if let feedID = nextUnread(sidebarItems: sidebarItems, startingAt: startFeed) {
|
||||||
// var nextSidebarItem: SidebarItem? = nil
|
return feedID
|
||||||
// for section in sidebarItems {
|
} else {
|
||||||
// if nextSidebarItem == nil {
|
if let firstFeed = sidebarItems.first?.children.first?.feed {
|
||||||
// section.visit { sidebarItem in
|
return nextUnread(sidebarItems: sidebarItems, startingAt: firstFeed)
|
||||||
// if !foundStartFeed && sidebarItem.feed?.feedID == startingAt.feedID {
|
}
|
||||||
// foundStartFeed = true
|
}
|
||||||
// return false
|
|
||||||
// }
|
return nil
|
||||||
// if foundStartFeed && sidebarItem.unreadCount > 0 {
|
}
|
||||||
// nextSidebarItem = sidebarItem
|
|
||||||
// return true
|
@discardableResult
|
||||||
// }
|
func nextUnread(sidebarItems: [SidebarItem], startingAt: Feed) -> FeedIdentifier? {
|
||||||
// return false
|
var foundStartFeed = false
|
||||||
// }
|
var nextSidebarItem: SidebarItem? = nil
|
||||||
// }
|
|
||||||
// }
|
for section in sidebarItems {
|
||||||
//
|
if nextSidebarItem == nil {
|
||||||
// if let nextFeedID = nextSidebarItem?.feed?.feedID {
|
section.visit { sidebarItem in
|
||||||
// select(nextFeedID)
|
if !foundStartFeed && sidebarItem.feed?.feedID == startingAt.feedID {
|
||||||
// return true
|
foundStartFeed = true
|
||||||
// }
|
return false
|
||||||
//
|
}
|
||||||
// return false
|
if foundStartFeed && sidebarItem.unreadCount > 0 {
|
||||||
// }
|
nextSidebarItem = sidebarItem
|
||||||
//
|
return true
|
||||||
// func select(_ feedID: FeedIdentifier) {
|
}
|
||||||
// selectedFeedIdentifiers = Set([feedID])
|
return false
|
||||||
// selectedFeedIdentifier = feedID
|
}
|
||||||
// }
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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 */; };
|
51A5769624AE617200078888 /* ArticleContainerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51A5769524AE617200078888 /* ArticleContainerView.swift */; };
|
||||||
51A5769724AE617200078888 /* 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 */; };
|
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 */; };
|
51A9A5E12380C4FE0033AADF /* AppDefaults.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51C45255226507D200C03939 /* AppDefaults.swift */; };
|
||||||
51A9A5E42380C8880033AADF /* ShareFolderPickerAccountCell.xib in Resources */ = {isa = PBXBuildFile; fileRef = 51A9A5E32380C8870033AADF /* ShareFolderPickerAccountCell.xib */; };
|
51A9A5E42380C8880033AADF /* ShareFolderPickerAccountCell.xib in Resources */ = {isa = PBXBuildFile; fileRef = 51A9A5E32380C8870033AADF /* ShareFolderPickerAccountCell.xib */; };
|
||||||
51A9A5E62380C8B20033AADF /* ShareFolderPickerFolderCell.xib in Resources */ = {isa = PBXBuildFile; fileRef = 51A9A5E52380C8B20033AADF /* ShareFolderPickerFolderCell.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>"; };
|
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>"; };
|
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>"; };
|
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>"; };
|
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>"; };
|
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>"; };
|
51A9A5E72380CA130033AADF /* ShareFolderPickerCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShareFolderPickerCell.swift; sourceTree = "<group>"; };
|
||||||
|
@ -3016,6 +3025,16 @@
|
||||||
path = Article;
|
path = Article;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
};
|
};
|
||||||
|
51A8001024CA0FAE00F41F1D /* CombineExt */ = {
|
||||||
|
isa = PBXGroup;
|
||||||
|
children = (
|
||||||
|
51A8001424CA0FEC00F41F1D /* DemandBuffer.swift */,
|
||||||
|
51A8001124CA0FC700F41F1D /* Sink.swift */,
|
||||||
|
51A8FFEC24CA0CF400F41F1D /* WIthLatestFrom.swift */,
|
||||||
|
);
|
||||||
|
path = CombineExt;
|
||||||
|
sourceTree = "<group>";
|
||||||
|
};
|
||||||
51B5C85A23F22A7A00032075 /* CommonExtension */ = {
|
51B5C85A23F22A7A00032075 /* CommonExtension */ = {
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
|
@ -3080,6 +3099,7 @@
|
||||||
51C0513824A77DF800194D5E /* Assets.xcassets */,
|
51C0513824A77DF800194D5E /* Assets.xcassets */,
|
||||||
17930ED224AF10CD00A9BA52 /* Add */,
|
17930ED224AF10CD00A9BA52 /* Add */,
|
||||||
51A576B924AE617B00078888 /* Article */,
|
51A576B924AE617B00078888 /* Article */,
|
||||||
|
51A8001024CA0FAE00F41F1D /* CombineExt */,
|
||||||
51919FB124AAB95300541E64 /* Images */,
|
51919FB124AAB95300541E64 /* Images */,
|
||||||
17897AA724C281520014BA03 /* Inspector */,
|
17897AA724C281520014BA03 /* Inspector */,
|
||||||
514E6BFD24AD252400AC6F6E /* Previews */,
|
514E6BFD24AD252400AC6F6E /* Previews */,
|
||||||
|
@ -4297,46 +4317,46 @@
|
||||||
TargetAttributes = {
|
TargetAttributes = {
|
||||||
51314636235A7BBE00387FDC = {
|
51314636235A7BBE00387FDC = {
|
||||||
CreatedOnToolsVersion = 11.2;
|
CreatedOnToolsVersion = 11.2;
|
||||||
DevelopmentTeam = FQLBNX3GP7;
|
DevelopmentTeam = SHJK2V3AJG;
|
||||||
LastSwiftMigration = 1120;
|
LastSwiftMigration = 1120;
|
||||||
ProvisioningStyle = Automatic;
|
ProvisioningStyle = Automatic;
|
||||||
};
|
};
|
||||||
513C5CE5232571C2003D4054 = {
|
513C5CE5232571C2003D4054 = {
|
||||||
CreatedOnToolsVersion = 11.0;
|
CreatedOnToolsVersion = 11.0;
|
||||||
DevelopmentTeam = FQLBNX3GP7;
|
DevelopmentTeam = SHJK2V3AJG;
|
||||||
ProvisioningStyle = Automatic;
|
ProvisioningStyle = Automatic;
|
||||||
};
|
};
|
||||||
518B2ED12351B3DD00400001 = {
|
518B2ED12351B3DD00400001 = {
|
||||||
CreatedOnToolsVersion = 11.2;
|
CreatedOnToolsVersion = 11.2;
|
||||||
DevelopmentTeam = FQLBNX3GP7;
|
DevelopmentTeam = SHJK2V3AJG;
|
||||||
ProvisioningStyle = Automatic;
|
ProvisioningStyle = Automatic;
|
||||||
TestTargetID = 840D617B2029031C009BC708;
|
TestTargetID = 840D617B2029031C009BC708;
|
||||||
};
|
};
|
||||||
51C0513C24A77DF800194D5E = {
|
51C0513C24A77DF800194D5E = {
|
||||||
CreatedOnToolsVersion = 12.0;
|
CreatedOnToolsVersion = 12.0;
|
||||||
DevelopmentTeam = FQLBNX3GP7;
|
DevelopmentTeam = SHJK2V3AJG;
|
||||||
ProvisioningStyle = Automatic;
|
ProvisioningStyle = Automatic;
|
||||||
};
|
};
|
||||||
51C0514324A77DF800194D5E = {
|
51C0514324A77DF800194D5E = {
|
||||||
CreatedOnToolsVersion = 12.0;
|
CreatedOnToolsVersion = 12.0;
|
||||||
DevelopmentTeam = FQLBNX3GP7;
|
DevelopmentTeam = SHJK2V3AJG;
|
||||||
ProvisioningStyle = Automatic;
|
ProvisioningStyle = Automatic;
|
||||||
};
|
};
|
||||||
6581C73220CED60000F4AD34 = {
|
6581C73220CED60000F4AD34 = {
|
||||||
DevelopmentTeam = FQLBNX3GP7;
|
DevelopmentTeam = SHJK2V3AJG;
|
||||||
ProvisioningStyle = Automatic;
|
ProvisioningStyle = Automatic;
|
||||||
};
|
};
|
||||||
65ED3FA2235DEF6C0081F399 = {
|
65ED3FA2235DEF6C0081F399 = {
|
||||||
DevelopmentTeam = FQLBNX3GP7;
|
DevelopmentTeam = SHJK2V3AJG;
|
||||||
ProvisioningStyle = Automatic;
|
ProvisioningStyle = Automatic;
|
||||||
};
|
};
|
||||||
65ED4090235DEF770081F399 = {
|
65ED4090235DEF770081F399 = {
|
||||||
DevelopmentTeam = FQLBNX3GP7;
|
DevelopmentTeam = SHJK2V3AJG;
|
||||||
ProvisioningStyle = Automatic;
|
ProvisioningStyle = Automatic;
|
||||||
};
|
};
|
||||||
840D617B2029031C009BC708 = {
|
840D617B2029031C009BC708 = {
|
||||||
CreatedOnToolsVersion = 9.3;
|
CreatedOnToolsVersion = 9.3;
|
||||||
DevelopmentTeam = FQLBNX3GP7;
|
DevelopmentTeam = SHJK2V3AJG;
|
||||||
ProvisioningStyle = Automatic;
|
ProvisioningStyle = Automatic;
|
||||||
SystemCapabilities = {
|
SystemCapabilities = {
|
||||||
com.apple.BackgroundModes = {
|
com.apple.BackgroundModes = {
|
||||||
|
@ -4346,7 +4366,7 @@
|
||||||
};
|
};
|
||||||
849C645F1ED37A5D003D8FC0 = {
|
849C645F1ED37A5D003D8FC0 = {
|
||||||
CreatedOnToolsVersion = 8.2.1;
|
CreatedOnToolsVersion = 8.2.1;
|
||||||
DevelopmentTeam = FQLBNX3GP7;
|
DevelopmentTeam = SHJK2V3AJG;
|
||||||
ProvisioningStyle = Automatic;
|
ProvisioningStyle = Automatic;
|
||||||
SystemCapabilities = {
|
SystemCapabilities = {
|
||||||
com.apple.HardenedRuntime = {
|
com.apple.HardenedRuntime = {
|
||||||
|
@ -4356,7 +4376,7 @@
|
||||||
};
|
};
|
||||||
849C64701ED37A5D003D8FC0 = {
|
849C64701ED37A5D003D8FC0 = {
|
||||||
CreatedOnToolsVersion = 8.2.1;
|
CreatedOnToolsVersion = 8.2.1;
|
||||||
DevelopmentTeam = FQLBNX3GP7;
|
DevelopmentTeam = SHJK2V3AJG;
|
||||||
ProvisioningStyle = Automatic;
|
ProvisioningStyle = Automatic;
|
||||||
TestTargetID = 849C645F1ED37A5D003D8FC0;
|
TestTargetID = 849C645F1ED37A5D003D8FC0;
|
||||||
};
|
};
|
||||||
|
@ -5174,6 +5194,7 @@
|
||||||
65082A5424C73D2F009FA994 /* AccountCredentialsError.swift in Sources */,
|
65082A5424C73D2F009FA994 /* AccountCredentialsError.swift in Sources */,
|
||||||
51E4990B24A808C500B667CB /* ImageDownloader.swift in Sources */,
|
51E4990B24A808C500B667CB /* ImageDownloader.swift in Sources */,
|
||||||
51E498F424A8085D00B667CB /* SmartFeedDelegate.swift in Sources */,
|
51E498F424A8085D00B667CB /* SmartFeedDelegate.swift in Sources */,
|
||||||
|
51A8001524CA0FEC00F41F1D /* DemandBuffer.swift in Sources */,
|
||||||
514E6BFF24AD255D00AC6F6E /* PreviewArticles.swift in Sources */,
|
514E6BFF24AD255D00AC6F6E /* PreviewArticles.swift in Sources */,
|
||||||
51E4993024A8676400B667CB /* ArticleSorter.swift in Sources */,
|
51E4993024A8676400B667CB /* ArticleSorter.swift in Sources */,
|
||||||
51408B7E24A9EC6F0073CF4E /* SidebarItem.swift in Sources */,
|
51408B7E24A9EC6F0073CF4E /* SidebarItem.swift in Sources */,
|
||||||
|
@ -5196,6 +5217,7 @@
|
||||||
51E4990124A808BB00B667CB /* FaviconURLFinder.swift in Sources */,
|
51E4990124A808BB00B667CB /* FaviconURLFinder.swift in Sources */,
|
||||||
51E4991D24A8092100B667CB /* NSAttributedString+NetNewsWire.swift in Sources */,
|
51E4991D24A8092100B667CB /* NSAttributedString+NetNewsWire.swift in Sources */,
|
||||||
65082A2F24C72AC8009FA994 /* SettingsCredentialsAccountView.swift in Sources */,
|
65082A2F24C72AC8009FA994 /* SettingsCredentialsAccountView.swift in Sources */,
|
||||||
|
51A8FFED24CA0CF400F41F1D /* WIthLatestFrom.swift in Sources */,
|
||||||
51E499FD24A9137600B667CB /* SidebarModel.swift in Sources */,
|
51E499FD24A9137600B667CB /* SidebarModel.swift in Sources */,
|
||||||
5181C66224B0C326002E0F70 /* SettingsModel.swift in Sources */,
|
5181C66224B0C326002E0F70 /* SettingsModel.swift in Sources */,
|
||||||
51E4995324A8734D00B667CB /* RedditFeedProvider-Extensions.swift in Sources */,
|
51E4995324A8734D00B667CB /* RedditFeedProvider-Extensions.swift in Sources */,
|
||||||
|
@ -5248,6 +5270,7 @@
|
||||||
5177472224B38CAE00EB0F74 /* ArticleExtractorButtonState.swift in Sources */,
|
5177472224B38CAE00EB0F74 /* ArticleExtractorButtonState.swift in Sources */,
|
||||||
5177471A24B3863000EB0F74 /* WebViewProvider.swift in Sources */,
|
5177471A24B3863000EB0F74 /* WebViewProvider.swift in Sources */,
|
||||||
51E4992124A8095000B667CB /* RSImage-Extensions.swift in Sources */,
|
51E4992124A8095000B667CB /* RSImage-Extensions.swift in Sources */,
|
||||||
|
51A8001224CA0FC700F41F1D /* Sink.swift in Sources */,
|
||||||
51E4990324A808BB00B667CB /* FaviconDownloader.swift in Sources */,
|
51E4990324A808BB00B667CB /* FaviconDownloader.swift in Sources */,
|
||||||
172199ED24AB2E0100A31D04 /* SafariView.swift in Sources */,
|
172199ED24AB2E0100A31D04 /* SafariView.swift in Sources */,
|
||||||
65ACE48624B477C9003AE06A /* SettingsAccountLabelView.swift in Sources */,
|
65ACE48624B477C9003AE06A /* SettingsAccountLabelView.swift in Sources */,
|
||||||
|
@ -5284,11 +5307,13 @@
|
||||||
51B8BCC324C25C3E00360B00 /* SidebarContextMenu.swift in Sources */,
|
51B8BCC324C25C3E00360B00 /* SidebarContextMenu.swift in Sources */,
|
||||||
51E498C724A8085D00B667CB /* StarredFeedDelegate.swift in Sources */,
|
51E498C724A8085D00B667CB /* StarredFeedDelegate.swift in Sources */,
|
||||||
5194736F24BBB937001A2939 /* HiddenModifier.swift in Sources */,
|
5194736F24BBB937001A2939 /* HiddenModifier.swift in Sources */,
|
||||||
|
51A8001624CA0FEC00F41F1D /* DemandBuffer.swift in Sources */,
|
||||||
51919FB724AABCA100541E64 /* IconImageView.swift in Sources */,
|
51919FB724AABCA100541E64 /* IconImageView.swift in Sources */,
|
||||||
51B54A6924B54A490014348B /* IconView.swift in Sources */,
|
51B54A6924B54A490014348B /* IconView.swift in Sources */,
|
||||||
17897ACB24C281A40014BA03 /* InspectorView.swift in Sources */,
|
17897ACB24C281A40014BA03 /* InspectorView.swift in Sources */,
|
||||||
51E498FA24A808BA00B667CB /* SingleFaviconDownloader.swift in Sources */,
|
51E498FA24A808BA00B667CB /* SingleFaviconDownloader.swift in Sources */,
|
||||||
1727B39924C1368D00A4DBDC /* LayoutPreferencesView.swift in Sources */,
|
1727B39924C1368D00A4DBDC /* LayoutPreferencesView.swift in Sources */,
|
||||||
|
51A8FFEE24CA0CF400F41F1D /* WIthLatestFrom.swift in Sources */,
|
||||||
51E4993F24A8713B00B667CB /* ArticleStatusSyncTimer.swift in Sources */,
|
51E4993F24A8713B00B667CB /* ArticleStatusSyncTimer.swift in Sources */,
|
||||||
51E4993724A8680E00B667CB /* Reachability.swift in Sources */,
|
51E4993724A8680E00B667CB /* Reachability.swift in Sources */,
|
||||||
51B80F4424BE58BF00C6C32D /* SharingServiceDelegate.swift in Sources */,
|
51B80F4424BE58BF00C6C32D /* SharingServiceDelegate.swift in Sources */,
|
||||||
|
@ -5407,6 +5432,7 @@
|
||||||
1769E32724BC5B6C000E1E8E /* AddAccountModel.swift in Sources */,
|
1769E32724BC5B6C000E1E8E /* AddAccountModel.swift in Sources */,
|
||||||
1729529424AA1CAA00D65E66 /* AdvancedPreferencesView.swift in Sources */,
|
1729529424AA1CAA00D65E66 /* AdvancedPreferencesView.swift in Sources */,
|
||||||
5177470424B2657F00EB0F74 /* TimelineToolbarModifier.swift in Sources */,
|
5177470424B2657F00EB0F74 /* TimelineToolbarModifier.swift in Sources */,
|
||||||
|
51A8001324CA0FC700F41F1D /* Sink.swift in Sources */,
|
||||||
51E4992D24A8676300B667CB /* FetchRequestOperation.swift in Sources */,
|
51E4992D24A8676300B667CB /* FetchRequestOperation.swift in Sources */,
|
||||||
51E4992424A8098400B667CB /* SmartFeedPasteboardWriter.swift in Sources */,
|
51E4992424A8098400B667CB /* SmartFeedPasteboardWriter.swift in Sources */,
|
||||||
51E4991424A808FF00B667CB /* ArticleStringFormatter.swift in Sources */,
|
51E4991424A808FF00B667CB /* ArticleStringFormatter.swift in Sources */,
|
||||||
|
|
Loading…
Reference in New Issue