[WIP] Topic APIs

This commit is contained in:
Ali
2022-09-28 12:15:06 +02:00
parent d7d3b1b9cb
commit d01a7853fa
60 changed files with 4264 additions and 1861 deletions

View File

@@ -0,0 +1,33 @@
load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library")
swift_library(
name = "ForumTopicListScreen",
module_name = "ForumTopicListScreen",
srcs = glob([
"Sources/**/*.swift",
]),
copts = [
"-warnings-as-errors",
],
deps = [
"//submodules/AsyncDisplayKit:AsyncDisplayKit",
"//submodules/Display:Display",
"//submodules/Postbox:Postbox",
"//submodules/TelegramCore:TelegramCore",
"//submodules/SSignalKit/SwiftSignalKit:SwiftSignalKit",
"//submodules/ComponentFlow:ComponentFlow",
"//submodules/TelegramUI/Components/AnimationCache:AnimationCache",
"//submodules/TelegramUI/Components/MultiAnimationRenderer:MultiAnimationRenderer",
"//submodules/TelegramUI/Components/EmojiTextAttachmentView:EmojiTextAttachmentView",
"//submodules/Components/ComponentDisplayAdapters:ComponentDisplayAdapters",
"//submodules/Components/MultilineTextComponent:MultilineTextComponent",
"//submodules/TelegramPresentationData:TelegramPresentationData",
"//submodules/AccountContext:AccountContext",
"//submodules/AppBundle:AppBundle",
"//submodules/TelegramStringFormatting:TelegramStringFormatting",
"//submodules/PresentationDataUtils:PresentationDataUtils",
],
visibility = [
"//visibility:public",
],
)

View File

@@ -0,0 +1,495 @@
import Foundation
import UIKit
import Display
import AsyncDisplayKit
import ComponentFlow
import SwiftSignalKit
import AnimationCache
import MultiAnimationRenderer
import ComponentDisplayAdapters
import TelegramPresentationData
import AccountContext
import Postbox
import TelegramCore
private final class ForumTopicListItemComponent: Component {
let context: AccountContext
let theme: PresentationTheme
let strings: PresentationStrings
let item: ForumChannelTopics.Item
let action: () -> Void
init(
context: AccountContext,
theme: PresentationTheme,
strings: PresentationStrings,
item: ForumChannelTopics.Item,
action: @escaping () -> Void
) {
self.context = context
self.theme = theme
self.strings = strings
self.item = item
self.action = action
}
static func ==(lhs: ForumTopicListItemComponent, rhs: ForumTopicListItemComponent) -> Bool {
if lhs.context !== rhs.context {
return false
}
if lhs.theme !== rhs.theme {
return false
}
if lhs.strings !== rhs.strings {
return false
}
if lhs.item != rhs.item {
return false
}
return true
}
final class View: HighlightTrackingButton {
private var highlightedBackgroundLayer: SimpleLayer?
private let title: ComponentView<Empty>
private var component: ForumTopicListItemComponent?
override init(frame: CGRect) {
self.title = ComponentView()
super.init(frame: frame)
self.addTarget(self, action: #selector(self.pressed), for: .touchUpInside)
self.highligthedChanged = { [weak self] highlighted in
if let self, let component = self.component {
if highlighted {
if let superview = self.superview {
superview.bringSubviewToFront(self)
}
let highlightedBackgroundLayer: SimpleLayer
if let current = self.highlightedBackgroundLayer {
highlightedBackgroundLayer = current
} else {
highlightedBackgroundLayer = SimpleLayer()
self.highlightedBackgroundLayer = highlightedBackgroundLayer
highlightedBackgroundLayer.backgroundColor = component.theme.list.itemHighlightedBackgroundColor.cgColor
highlightedBackgroundLayer.frame = CGRect(origin: CGPoint(x: 0.0, y: -UIScreenPixel), size: CGSize(width: self.bounds.width, height: self.bounds.height + UIScreenPixel))
self.layer.insertSublayer(highlightedBackgroundLayer, at: 0)
}
highlightedBackgroundLayer.removeAllAnimations()
highlightedBackgroundLayer.opacity = 1.0
} else {
if let highlightedBackgroundLayer = self.highlightedBackgroundLayer {
self.highlightedBackgroundLayer = nil
highlightedBackgroundLayer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3, removeOnCompletion: false, completion: { [weak highlightedBackgroundLayer] _ in
highlightedBackgroundLayer?.removeFromSuperlayer()
})
}
}
}
}
}
required init?(coder: NSCoder) {
preconditionFailure()
}
@objc private func pressed() {
self.component?.action()
}
func update(component: ForumTopicListItemComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: Transition) -> CGSize {
self.component = component
let titleSize = self.title.update(
transition: .immediate,
component: AnyComponent(Text(
text: component.item.info.title,
font: Font.regular(17.0),
color: component.theme.list.itemPrimaryTextColor
)),
environment: {},
containerSize: availableSize
)
if let titleView = self.title.view {
if titleView.superview == nil {
self.addSubview(titleView)
titleView.isUserInteractionEnabled = false
}
transition.setFrame(view: titleView, frame: CGRect(origin: CGPoint(x: 11.0, y: floor((availableSize.height - titleSize.height) / 2.0)), size: titleSize))
}
return availableSize
}
}
func makeView() -> View {
return View(frame: CGRect())
}
func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: Transition) -> CGSize {
return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition)
}
}
private final class ForumTopicListComponent: Component {
let context: AccountContext
let theme: PresentationTheme
let strings: PresentationStrings
let items: [ForumChannelTopics.Item]
let navigationHeight: CGFloat
let action: (ForumChannelTopics.Item) -> Void
init(
context: AccountContext,
theme: PresentationTheme,
strings: PresentationStrings,
items: [ForumChannelTopics.Item],
navigationHeight: CGFloat,
action: @escaping (ForumChannelTopics.Item) -> Void
) {
self.context = context
self.theme = theme
self.strings = strings
self.items = items
self.navigationHeight = navigationHeight
self.action = action
}
static func ==(lhs: ForumTopicListComponent, rhs: ForumTopicListComponent) -> Bool {
if lhs.context !== rhs.context {
return false
}
if lhs.theme !== rhs.theme {
return false
}
if lhs.strings !== rhs.strings {
return false
}
if lhs.items != rhs.items {
return false
}
if lhs.navigationHeight != rhs.navigationHeight {
return false
}
return true
}
final class View: UIView, UIScrollViewDelegate {
private struct ItemLayout {
let containerSize: CGSize
let itemHeight: CGFloat
let contentSize: CGSize
let itemsInsets: UIEdgeInsets
init(containerSize: CGSize, navigationHeight: CGFloat, itemCount: Int) {
self.itemHeight = 44.0
self.containerSize = containerSize
self.itemsInsets = UIEdgeInsets(top: navigationHeight, left: 0.0, bottom: 0.0, right: 0.0)
self.contentSize = CGSize(width: containerSize.width, height: self.itemsInsets.top + self.itemsInsets.bottom + CGFloat(itemCount) * self.itemHeight)
}
func frame(at index: Int) -> CGRect {
return CGRect(origin: CGPoint(x: 0.0, y: self.itemsInsets.top + CGFloat(index) * self.itemHeight), size: CGSize(width: self.containerSize.width, height: self.itemHeight))
}
}
private final class ItemView {
let host: ComponentView<Empty>
let separatorLayer: SimpleLayer
init() {
self.host = ComponentView()
self.separatorLayer = SimpleLayer()
}
}
private let scrollView: UIScrollView
private var component: ForumTopicListComponent?
private var itemLayout: ItemLayout?
private var ignoreScrolling: Bool = false
private var visibleItemViews: [Int64: ItemView] = [:]
override init(frame: CGRect) {
self.scrollView = UIScrollView()
super.init(frame: frame)
self.scrollView.layer.anchorPoint = CGPoint()
self.scrollView.delaysContentTouches = true
self.scrollView.clipsToBounds = false
if #available(iOSApplicationExtension 11.0, iOS 11.0, *) {
self.scrollView.contentInsetAdjustmentBehavior = .never
}
if #available(iOS 13.0, *) {
self.scrollView.automaticallyAdjustsScrollIndicatorInsets = false
}
self.scrollView.showsVerticalScrollIndicator = true
self.scrollView.showsHorizontalScrollIndicator = false
self.scrollView.alwaysBounceHorizontal = false
self.scrollView.alwaysBounceVertical = true
self.scrollView.scrollsToTop = false
self.scrollView.delegate = self
self.scrollView.clipsToBounds = true
self.scrollView.canCancelContentTouches = true
self.addSubview(self.scrollView)
}
required init?(coder: NSCoder) {
preconditionFailure()
}
func scrollViewDidScroll(_ scrollView: UIScrollView) {
if !self.ignoreScrolling {
self.updateVisibleItems(transition: .immediate, synchronous: false)
}
}
private func updateVisibleItems(transition: Transition, synchronous: Bool) {
guard let component = self.component, let itemLayout = self.itemLayout else {
return
}
var validIds = Set<Int64>()
let visibleBounds = self.scrollView.bounds
for index in 0 ..< component.items.count {
let itemFrame = itemLayout.frame(at: index)
if !visibleBounds.intersects(itemFrame) {
continue
}
let item = component.items[index]
validIds.insert(item.id)
let itemView: ItemView
var itemTransition = transition
if let current = self.visibleItemViews[item.id] {
itemView = current
} else {
itemTransition = .immediate
itemView = ItemView()
self.visibleItemViews[item.id] = itemView
}
let id = item.id
let _ = itemView.host.update(
transition: itemTransition,
component: AnyComponent(ForumTopicListItemComponent(
context: component.context,
theme: component.theme,
strings: component.strings,
item: item,
action: { [weak self] in
guard let strongSelf = self, let component = strongSelf.component else {
return
}
for item in component.items {
if item.id == id {
component.action(item)
break
}
}
}
)),
environment: {},
containerSize: itemFrame.size
)
if let itemComponentView = itemView.host.view {
if itemComponentView.superview == nil {
self.scrollView.addSubview(itemComponentView)
self.scrollView.layer.addSublayer(itemView.separatorLayer)
}
itemTransition.setFrame(view: itemComponentView, frame: itemFrame)
let separatorInset: CGFloat
if index == component.items.count - 1 {
separatorInset = 0.0
} else {
separatorInset = 16.0
}
itemView.separatorLayer.backgroundColor = component.theme.list.itemPlainSeparatorColor.cgColor
itemTransition.setFrame(layer: itemView.separatorLayer, frame: CGRect(origin: CGPoint(x: separatorInset, y: itemFrame.maxY - UIScreenPixel), size: CGSize(width: itemLayout.contentSize.width - separatorInset, height: UIScreenPixel)))
}
}
var removedIds: [Int64] = []
for (id, itemView) in self.visibleItemViews {
if !validIds.contains(id) {
itemView.host.view?.removeFromSuperview()
itemView.separatorLayer.removeFromSuperlayer()
removedIds.append(id)
}
}
for id in removedIds {
self.visibleItemViews.removeValue(forKey: id)
}
}
func update(component: ForumTopicListComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: Transition) -> CGSize {
self.component = component
let itemLayout = ItemLayout(containerSize: availableSize, navigationHeight: component.navigationHeight, itemCount: component.items.count)
self.itemLayout = itemLayout
self.ignoreScrolling = true
self.scrollView.frame = CGRect(origin: CGPoint(), size: availableSize)
if self.scrollView.contentSize != itemLayout.contentSize {
self.scrollView.contentSize = itemLayout.contentSize
}
if self.scrollView.scrollIndicatorInsets != itemLayout.itemsInsets {
self.scrollView.scrollIndicatorInsets = itemLayout.itemsInsets
}
self.ignoreScrolling = false
self.updateVisibleItems(transition: transition, synchronous: false)
return availableSize
}
}
func makeView() -> View {
return View(frame: CGRect())
}
func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: Transition) -> CGSize {
return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition)
}
}
public final class ForumTopicListScreen: ViewController {
private final class Node: ViewControllerTracingNode {
private weak var controller: ForumTopicListScreen?
private let context: AccountContext
private let id: EnginePeer.Id
private var presentationData: PresentationData
private let topicList: ComponentView<Empty>
private let forumChannelContext: ForumChannelTopics
private var stateDisposable: Disposable?
private var currentState: ForumChannelTopics.State?
private var currentLayout: (layout: ContainerViewLayout, navigationHeight: CGFloat)?
init(controller: ForumTopicListScreen, context: AccountContext, id: EnginePeer.Id) {
self.controller = controller
self.context = context
self.id = id
self.presentationData = self.context.sharedContext.currentPresentationData.with { $0 }
self.topicList = ComponentView()
self.forumChannelContext = ForumChannelTopics(account: self.context.account, peerId: self.id)
super.init()
self.backgroundColor = self.presentationData.theme.list.plainBackgroundColor
self.stateDisposable = (self.forumChannelContext.state
|> deliverOnMainQueue).start(next: { [weak self] state in
guard let strongSelf = self else {
return
}
strongSelf.currentState = state
strongSelf.update(transition: .immediate)
})
}
deinit {
self.stateDisposable?.dispose()
}
func createPressed() {
self.forumChannelContext.createTopic(title: "Topic#\(Int.random(in: 0 ..< 100000))")
}
private func update(transition: Transition) {
if let currentLayout = self.currentLayout {
self.containerLayoutUpdated(layout: currentLayout.layout, navigationHeight: currentLayout.navigationHeight, transition: transition)
}
}
func containerLayoutUpdated(layout: ContainerViewLayout, navigationHeight: CGFloat, transition: Transition) {
self.currentLayout = (layout, navigationHeight)
let _ = self.topicList.update(
transition: transition,
component: AnyComponent(ForumTopicListComponent(
context: self.context,
theme: self.presentationData.theme,
strings: self.presentationData.strings,
items: self.currentState?.items ?? [],
navigationHeight: navigationHeight,
action: { [weak self] item in
guard let strongSelf = self else {
return
}
strongSelf.controller?.openTopic(item)
}
)),
environment: {},
containerSize: layout.size
)
if let topicListView = self.topicList.view {
if topicListView.superview == nil {
if let navigationBar = self.controller?.navigationBar {
self.view.insertSubview(topicListView, belowSubview: navigationBar.view)
} else {
self.view.addSubview(topicListView)
}
}
transition.setFrame(view: topicListView, frame: CGRect(origin: CGPoint(), size: layout.size))
}
}
}
private var node: Node {
return self.displayNode as! Node
}
private let context: AccountContext
private let id: EnginePeer.Id
private var presentationData: PresentationData
private let openTopic: (ForumChannelTopics.Item) -> Void
public init(context: AccountContext, id: EnginePeer.Id, openTopic: @escaping (ForumChannelTopics.Item) -> Void) {
self.context = context
self.id = id
self.presentationData = self.context.sharedContext.currentPresentationData.with { $0 }
self.openTopic = openTopic
super.init(navigationBarPresentationData: NavigationBarPresentationData(presentationData: self.presentationData))
//TODO:localize
self.title = "Forum"
self.navigationItem.setRightBarButton(UIBarButtonItem(title: "Create", style: .plain, target: self, action: #selector(self.createPressed)), animated: false)
}
public required init(coder aDecoder: NSCoder) {
preconditionFailure()
}
@objc private func createPressed() {
self.node.createPressed()
}
override public func loadDisplayNode() {
self.displayNode = Node(controller: self, context: self.context, id: self.id)
self.displayNodeDidLoad()
}
override public func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) {
super.containerLayoutUpdated(layout, transition: transition)
self.node.containerLayoutUpdated(layout: layout, navigationHeight: self.navigationLayout(layout: layout).navigationFrame.maxY, transition: Transition(transition))
}
}