mirror of
https://github.com/Swiftgram/Telegram-iOS.git
synced 2025-06-16 05:55:20 +00:00
486 lines
20 KiB
Swift
486 lines
20 KiB
Swift
import Foundation
|
|
import TelegramCore
|
|
import Postbox
|
|
import SwiftSignalKit
|
|
import Display
|
|
|
|
import SafariServices
|
|
|
|
private enum FeedGroupingControllerTransitionType {
|
|
case initial
|
|
case initialLoad
|
|
case generic
|
|
case load
|
|
}
|
|
|
|
private struct FeedGroupingControllerTransition {
|
|
let type: FeedGroupingControllerTransitionType
|
|
let deletions: [ListViewDeleteItem]
|
|
let insertions: [ListViewInsertItem]
|
|
let updates: [ListViewUpdateItem]
|
|
}
|
|
|
|
private final class FeedGroupingControllerOpaqueState {
|
|
let entries: [FeedGroupingEntry]
|
|
let canLoadEarlier: Bool
|
|
|
|
init(entries: [FeedGroupingEntry], canLoadEarlier: Bool) {
|
|
self.entries = entries
|
|
self.canLoadEarlier = canLoadEarlier
|
|
}
|
|
}
|
|
|
|
private final class FeedGroupingControllerArguments {
|
|
let account: Account
|
|
let togglePeer: (Peer, Bool) -> Void
|
|
let ungroupAll: () -> Void
|
|
|
|
init(account: Account, togglePeer: @escaping (Peer, Bool) -> Void, ungroupAll: @escaping () -> Void) {
|
|
self.account = account
|
|
self.togglePeer = togglePeer
|
|
self.ungroupAll = ungroupAll
|
|
}
|
|
}
|
|
|
|
private enum FeedGroupingEntryId: Hashable {
|
|
case index(Int)
|
|
case peer(PeerId)
|
|
|
|
static func ==(lhs: FeedGroupingEntryId, rhs: FeedGroupingEntryId) -> Bool {
|
|
switch lhs {
|
|
case let .index(value):
|
|
if case .index(value) = rhs {
|
|
return true
|
|
} else {
|
|
return false
|
|
}
|
|
case let .peer(id):
|
|
if case .peer(id) = rhs {
|
|
return true
|
|
} else {
|
|
return false
|
|
}
|
|
}
|
|
}
|
|
|
|
var hashValue: Int {
|
|
switch self {
|
|
case let .index(value):
|
|
return value.hashValue
|
|
case let .peer(id):
|
|
return id.hashValue
|
|
}
|
|
}
|
|
}
|
|
|
|
private enum FeedGroupingSection: ItemListSectionId {
|
|
case peers
|
|
case ungroup
|
|
}
|
|
|
|
private enum FeedGroupingEntry: ItemListNodeEntry {
|
|
case groupHeader(PresentationTheme, String)
|
|
case peer(PresentationTheme, PresentationStrings, PresentationDateTimeFormat, PresentationPersonNameOrder, Int, Peer, Bool)
|
|
case ungroup(PresentationTheme, String)
|
|
|
|
var section: ItemListSectionId {
|
|
switch self {
|
|
case .groupHeader, .peer:
|
|
return FeedGroupingSection.peers.rawValue
|
|
case .ungroup:
|
|
return FeedGroupingSection.ungroup.rawValue
|
|
}
|
|
}
|
|
|
|
var stableId: FeedGroupingEntryId {
|
|
switch self {
|
|
case .groupHeader:
|
|
return .index(0)
|
|
case let .peer(_, _, _, _, _, peer, _):
|
|
return .peer(peer.id)
|
|
case .ungroup:
|
|
return .index(1)
|
|
}
|
|
}
|
|
|
|
static func ==(lhs: FeedGroupingEntry, rhs: FeedGroupingEntry) -> Bool {
|
|
switch lhs {
|
|
case let .groupHeader(lhsTheme, lhsText):
|
|
if case let .groupHeader(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText {
|
|
return true
|
|
} else {
|
|
return false
|
|
}
|
|
case let .peer(lhsTheme, lhsStrings, lhsDateTimeFormat, lhsNameOrder, lhsIndex, lhsPeer, lhsValue):
|
|
if case let .peer(rhsTheme, rhsStrings, rhsDateTimeFormat, rhsNameOrder, rhsIndex, rhsPeer, rhsValue) = rhs {
|
|
if lhsTheme !== rhsTheme {
|
|
return false
|
|
}
|
|
if lhsStrings !== rhsStrings {
|
|
return false
|
|
}
|
|
if lhsDateTimeFormat != rhsDateTimeFormat {
|
|
return false
|
|
}
|
|
if lhsNameOrder != rhsNameOrder {
|
|
return false
|
|
}
|
|
if lhsIndex != rhsIndex {
|
|
return false
|
|
}
|
|
if !lhsPeer.isEqual(rhsPeer) {
|
|
return false
|
|
}
|
|
if lhsValue != rhsValue {
|
|
return false
|
|
}
|
|
return true
|
|
} else {
|
|
return false
|
|
}
|
|
case let .ungroup(lhsTheme, lhsText):
|
|
if case let .ungroup(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText {
|
|
return true
|
|
} else {
|
|
return false
|
|
}
|
|
}
|
|
}
|
|
|
|
static func <(lhs: FeedGroupingEntry, rhs: FeedGroupingEntry) -> Bool {
|
|
switch lhs {
|
|
case .groupHeader:
|
|
return true
|
|
case let .peer(_, _, _, _, index, _, _):
|
|
switch rhs {
|
|
case .groupHeader:
|
|
return false
|
|
case let .peer(_, _, _, _, rhsIndex, _, _):
|
|
return index < rhsIndex
|
|
default:
|
|
return true
|
|
}
|
|
case .ungroup:
|
|
return false
|
|
}
|
|
}
|
|
|
|
func item(_ arguments: FeedGroupingControllerArguments) -> ListViewItem {
|
|
switch self {
|
|
case let .groupHeader(theme, text):
|
|
return ItemListSectionHeaderItem(theme: theme, text: text, sectionId: self.section)
|
|
case let .peer(theme, strings, dateTimeFormat, nameDisplayOrder, _, peer, value):
|
|
return ItemListPeerItem(theme: theme, strings: strings, dateTimeFormat: dateTimeFormat, nameDisplayOrder: nameDisplayOrder, account: arguments.account, peer: peer, presence: nil, text: .none, label: .none, editing: ItemListPeerItemEditing(editable: false, editing: false, revealed: false), switchValue: ItemListPeerItemSwitch(value: value, style: .standard), enabled: true, sectionId: self.section, action: nil, setPeerIdWithRevealedOptions: { _, _ in }, removePeer: { _ in }, toggleUpdated: { value in
|
|
arguments.togglePeer(peer, value)
|
|
})
|
|
case let .ungroup(theme, text):
|
|
return ItemListActionItem(theme: theme, title: text, kind: .destructive, alignment: .natural, sectionId: self.section, style: .blocks, action: {
|
|
arguments.ungroupAll()
|
|
})
|
|
}
|
|
}
|
|
}
|
|
|
|
private final class FeedGroupingPeerState {
|
|
let peer: Peer
|
|
let included: Bool
|
|
|
|
init(peer: Peer, included: Bool) {
|
|
self.peer = peer
|
|
self.included = included
|
|
}
|
|
}
|
|
|
|
private final class FeedGroupingEntriesState {
|
|
let entries: [FeedGroupingEntry]
|
|
|
|
init(entries: [FeedGroupingEntry]) {
|
|
self.entries = entries
|
|
}
|
|
}
|
|
|
|
private final class FeedGroupingState {
|
|
let theme: PresentationTheme
|
|
let strings: PresentationStrings
|
|
let dateTimeFormat: PresentationDateTimeFormat
|
|
let nameDisplayOrder: PresentationPersonNameOrder
|
|
let peers: [FeedGroupingPeerState]
|
|
|
|
init(theme: PresentationTheme, strings: PresentationStrings, dateTimeFormat: PresentationDateTimeFormat, nameDisplayOrder: PresentationPersonNameOrder, peers: [FeedGroupingPeerState]) {
|
|
self.theme = theme
|
|
self.strings = strings
|
|
self.dateTimeFormat = dateTimeFormat
|
|
self.nameDisplayOrder = nameDisplayOrder
|
|
self.peers = peers
|
|
}
|
|
|
|
func withUpdatedPeers(_ peers: [FeedGroupingPeerState]) -> FeedGroupingState {
|
|
return FeedGroupingState(theme: self.theme, strings: self.strings, dateTimeFormat: self.dateTimeFormat, nameDisplayOrder: self.nameDisplayOrder, peers: peers)
|
|
}
|
|
}
|
|
|
|
private func entriesStateFromState(_ state: FeedGroupingState) -> FeedGroupingEntriesState {
|
|
var entries: [FeedGroupingEntry] = []
|
|
if !state.peers.isEmpty {
|
|
entries.append(.groupHeader(state.theme, "GROUP CHANNELS"))
|
|
var index = 0
|
|
for peer in state.peers {
|
|
entries.append(.peer(state.theme, state.strings, state.dateTimeFormat, state.nameDisplayOrder, index, peer.peer, peer.included))
|
|
index += 1
|
|
}
|
|
entries.append(.ungroup(state.theme, "Ungroup All Channels"))
|
|
}
|
|
return FeedGroupingEntriesState(entries: entries)
|
|
}
|
|
|
|
private func preparedItemListNodeEntryTransition(from fromEntries: [FeedGroupingEntry], to toEntries: [FeedGroupingEntry], arguments: FeedGroupingControllerArguments, type: FeedGroupingControllerTransitionType) -> FeedGroupingControllerTransition {
|
|
let (deleteIndices, indicesAndItems, updateIndices) = mergeListsStableWithUpdates(leftList: fromEntries, rightList: toEntries)
|
|
|
|
let deletions = deleteIndices.map { ListViewDeleteItem(index: $0, directionHint: nil) }
|
|
let insertions = indicesAndItems.map { ListViewInsertItem(index: $0.0, previousIndex: $0.2, item: $0.1.item(arguments), directionHint: nil) }
|
|
let updates = updateIndices.map { ListViewUpdateItem(index: $0.0, previousIndex: $0.2, item: $0.1.item(arguments), directionHint: nil) }
|
|
|
|
return FeedGroupingControllerTransition(type: type, deletions: deletions, insertions: insertions, updates: updates)
|
|
}
|
|
|
|
final class FeedGroupingControllerNode: ViewControllerTracingNode {
|
|
private let context: AccountContext
|
|
private let groupId: PeerGroupId
|
|
private var presentationData: PresentationData
|
|
private let ungroupedAll: () -> Void
|
|
|
|
let readyPromise = ValuePromise<Bool>()
|
|
private var ready: Bool = false {
|
|
didSet {
|
|
if self.ready && !oldValue {
|
|
self.readyPromise.set(self.ready)
|
|
}
|
|
}
|
|
}
|
|
|
|
private var presentationDataDisposable: Disposable?
|
|
|
|
private var containerLayout: (ContainerViewLayout, CGFloat)?
|
|
|
|
private let listNode: ListView
|
|
private let blockingOverlay: ASDisplayNode
|
|
|
|
private var _stateValue: FeedGroupingState
|
|
private let statePromise = Promise<FeedGroupingState>()
|
|
|
|
private var _entriesStateValue = FeedGroupingEntriesState(entries: [])
|
|
private let entriesStatePromise = Promise<FeedGroupingEntriesState>()
|
|
|
|
private var enqueuedTransitions: [FeedGroupingControllerTransition] = []
|
|
|
|
private let peersDisposable = MetaDisposable()
|
|
|
|
private var transitionDisposable: Disposable?
|
|
|
|
init(context: AccountContext, groupId: PeerGroupId, presentationData: PresentationData, ungroupedAll: @escaping () -> Void) {
|
|
self.context = context
|
|
self.groupId = groupId
|
|
self.presentationData = presentationData
|
|
self.ungroupedAll = ungroupedAll
|
|
|
|
self.listNode = ListView()
|
|
self.listNode.isHidden = true
|
|
|
|
self.blockingOverlay = ASDisplayNode()
|
|
self.blockingOverlay.backgroundColor = self.presentationData.theme.list.blocksBackgroundColor
|
|
|
|
self._stateValue = FeedGroupingState(theme: self.presentationData.theme, strings: self.presentationData.strings, dateTimeFormat: self.presentationData.dateTimeFormat, nameDisplayOrder: self.presentationData.nameDisplayOrder, peers: [])
|
|
self.statePromise.set(.single(self._stateValue))
|
|
|
|
super.init()
|
|
|
|
self.backgroundColor = self.presentationData.theme.list.blocksBackgroundColor
|
|
|
|
self.addSubnode(self.listNode)
|
|
self.addSubnode(self.blockingOverlay)
|
|
|
|
self.listNode.displayedItemRangeChanged = { [weak self] displayedRange, opaqueTransactionState in
|
|
if let strongSelf = self {
|
|
/*if let state = (opaqueTransactionState as? ChatRecentActionsListOpaqueState), state.canLoadEarlier {
|
|
if let visible = displayedRange.visibleRange {
|
|
let indexRange = (state.entries.count - 1 - visible.lastIndex, state.entries.count - 1 - visible.firstIndex)
|
|
if indexRange.0 < 5 {
|
|
strongSelf.context.loadMoreEntries()
|
|
}
|
|
}
|
|
}*/
|
|
}
|
|
}
|
|
|
|
let previousState = Atomic<FeedGroupingEntriesState?>(value: nil)
|
|
|
|
let arguments = FeedGroupingControllerArguments(account: context.account, togglePeer: { [weak self] peer, value in
|
|
if let strongSelf = self {
|
|
strongSelf.updateState({ current in
|
|
var peers = current.peers
|
|
var index = 0
|
|
for listPeer in peers {
|
|
if listPeer.peer.id == peer.id {
|
|
peers[index] = FeedGroupingPeerState(peer: listPeer.peer, included: value)
|
|
break
|
|
}
|
|
index += 1
|
|
}
|
|
return current.withUpdatedPeers(peers)
|
|
})
|
|
let _ = updatePeerGroupIdInteractively(postbox: strongSelf.context.account.postbox, peerId: peer.id, groupId: value ? strongSelf.groupId : nil).start()
|
|
}
|
|
}, ungroupAll: { [weak self] in
|
|
if let strongSelf = self {
|
|
|
|
let _ = (clearPeerGroupInteractively(postbox: strongSelf.context.account.postbox, groupId: strongSelf.groupId)
|
|
|> deliverOnMainQueue).start(completed: {
|
|
self?.ungroupedAll()
|
|
})
|
|
}
|
|
})
|
|
|
|
self.transitionDisposable = (self.entriesStatePromise.get()
|
|
|> mapToQueue { state -> Signal<FeedGroupingControllerTransition, NoError> in
|
|
let previous = previousState.swap(state)
|
|
let type: FeedGroupingControllerTransitionType
|
|
if let previous = previous {
|
|
if previous.entries.isEmpty {
|
|
type = .initialLoad
|
|
} else {
|
|
type = .generic
|
|
}
|
|
} else {
|
|
type = .initial
|
|
}
|
|
return .single(preparedItemListNodeEntryTransition(from: previous?.entries ?? [], to: state.entries, arguments: arguments, type: type))
|
|
}
|
|
|> deliverOnMainQueue).start(next: { [weak self] transition in
|
|
return self?.enqueueTransition(transition)
|
|
})
|
|
|
|
self.updateState({ state in
|
|
return state
|
|
})
|
|
|
|
self.peersDisposable.set((availableGroupFeedPeers(postbox: self.context.account.postbox, network: self.context.account.network, groupId: groupId)
|
|
|> deliverOnMainQueue).start(next: { [weak self] result in
|
|
if let strongSelf = self {
|
|
strongSelf.updateState({ state in
|
|
return state.withUpdatedPeers(result.map { peer, value in
|
|
return FeedGroupingPeerState(peer: peer, included: value)
|
|
})
|
|
})
|
|
}
|
|
}))
|
|
}
|
|
|
|
deinit {
|
|
self.transitionDisposable?.dispose()
|
|
}
|
|
|
|
private func updateState(_ f: (FeedGroupingState) -> FeedGroupingState) {
|
|
let updatedState = f(self._stateValue)
|
|
self._stateValue = updatedState
|
|
self._entriesStateValue = entriesStateFromState(updatedState)
|
|
self.entriesStatePromise.set(.single(self._entriesStateValue))
|
|
}
|
|
|
|
func containerLayoutUpdated(_ layout: ContainerViewLayout, navigationBarHeight: CGFloat, transition: ContainedViewLayoutTransition) {
|
|
let isFirstLayout = self.containerLayout == nil
|
|
|
|
self.containerLayout = (layout, navigationBarHeight)
|
|
|
|
var insets = layout.insets(options: [.input])
|
|
insets.top += navigationBarHeight
|
|
|
|
transition.updateBounds(node: self.listNode, bounds: CGRect(origin: CGPoint(), size: layout.size))
|
|
transition.updatePosition(node: self.listNode, position: CGRect(origin: CGPoint(), size: layout.size).center)
|
|
|
|
var duration: Double = 0.0
|
|
var curve: UInt = 0
|
|
switch transition {
|
|
case .immediate:
|
|
break
|
|
case let .animated(animationDuration, animationCurve):
|
|
duration = animationDuration
|
|
switch animationCurve {
|
|
case .easeInOut:
|
|
break
|
|
case .spring:
|
|
curve = 7
|
|
}
|
|
}
|
|
|
|
let listViewCurve: ListViewAnimationCurve
|
|
if curve == 7 {
|
|
listViewCurve = .Spring(duration: duration)
|
|
} else {
|
|
listViewCurve = .Default(duration: duration)
|
|
}
|
|
|
|
let listInsets = UIEdgeInsets(top: insets.top, left: layout.safeInsets.right, bottom: insets.bottom, right: layout.safeInsets.left)
|
|
let updateSizeAndInsets = ListViewUpdateSizeAndInsets(size: layout.size, insets: listInsets, duration: duration, curve: listViewCurve)
|
|
|
|
self.listNode.transaction(deleteIndices: [], insertIndicesAndItems: [], updateIndicesAndItems: [], options: [.Synchronous, .LowLatency], scrollToItem: nil, additionalScrollDistance: 0.0, updateSizeAndInsets: updateSizeAndInsets, stationaryItemRange: nil, updateOpaqueState: nil, completion: { _ in })
|
|
|
|
if isFirstLayout {
|
|
self.dequeueTransitions()
|
|
}
|
|
}
|
|
|
|
private func enqueueTransition(_ transition: FeedGroupingControllerTransition) {
|
|
self.enqueuedTransitions.append(transition)
|
|
if self.containerLayout != nil {
|
|
self.dequeueTransitions()
|
|
}
|
|
}
|
|
|
|
private func dequeueTransitions() {
|
|
while true {
|
|
if let transition = self.enqueuedTransitions.first {
|
|
self.enqueuedTransitions.remove(at: 0)
|
|
|
|
var options = ListViewDeleteAndInsertOptions()
|
|
switch transition.type {
|
|
case .initial:
|
|
options.insert(.LowLatency)
|
|
case .generic:
|
|
options.insert(.AnimateInsertion)
|
|
case .load, .initialLoad:
|
|
break
|
|
}
|
|
|
|
self.listNode.transaction(deleteIndices: transition.deletions, insertIndicesAndItems: transition.insertions, updateIndicesAndItems: transition.updates, options: options, updateSizeAndInsets: nil, updateOpaqueState: nil, completion: { [weak self] _ in
|
|
if let strongSelf = self {
|
|
strongSelf.ready = true
|
|
if transition.type == .initialLoad {
|
|
strongSelf.listNode.isHidden = false
|
|
strongSelf.listNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.25)
|
|
}
|
|
/*if displayingResults != !strongSelf.listNode.isHidden {
|
|
strongSelf.listNode.isHidden = !displayingResults
|
|
strongSelf.backgroundColor = displayingResults ? strongSelf.presentationData.theme.list.plainBackgroundColor : nil
|
|
|
|
strongSelf.emptyNode.isHidden = displayingResults
|
|
if !displayingResults {
|
|
var text: String = ""
|
|
if let query = strongSelf.filter.query {
|
|
text = strongSelf.presentationData.strings.Channel_AdminLog_EmptyFilterQueryText(query).0
|
|
} else {
|
|
text = strongSelf.presentationData.strings.Channel_AdminLog_EmptyFilterText
|
|
}
|
|
strongSelf.emptyNode.setup(title: strongSelf.presentationData.strings.Channel_AdminLog_EmptyFilterTitle, text: text)
|
|
}
|
|
//strongSelf.isLoading = isEmpty && !displayingResults
|
|
}*/
|
|
}
|
|
})
|
|
} else {
|
|
break
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|