[WIP] Inline forums

This commit is contained in:
Ali 2022-11-18 02:16:33 +04:00
parent f9f194f04c
commit 6a708ac1c2
21 changed files with 2268 additions and 678 deletions

View File

@ -84,7 +84,8 @@ swift_library(
"//submodules/TelegramUI/Components/NotificationPeerExceptionController",
"//submodules/AnimationUI:AnimationUI",
"//submodules/PeerInfoUI",
"//submodules/TelegramUI/Components/ChatListHeaderComponent:ChatListHeaderComponent",
"//submodules/TelegramUI/Components/ChatListHeaderComponent",
"//submodules/TelegramUI/Components/ChatListTitleView",
],
visibility = [
"//visibility:public",

View File

@ -490,7 +490,7 @@ func chatContextMenuItems(context: AccountContext, peerId: PeerId, promoInfo: Ch
}
}
func chatForumTopicMenuItems(context: AccountContext, peerId: PeerId, threadId: Int64, isPinned: Bool?, isClosed: Bool?, chatListController: ChatListControllerImpl?, joined: Bool) -> Signal<[ContextMenuItem], NoError> {
func chatForumTopicMenuItems(context: AccountContext, peerId: PeerId, threadId: Int64, isPinned: Bool?, isClosed: Bool?, chatListController: ChatListControllerImpl?, joined: Bool, canSelect: Bool) -> Signal<[ContextMenuItem], NoError> {
let presentationData = context.sharedContext.currentPresentationData.with({ $0 })
let strings = presentationData.strings
@ -763,11 +763,13 @@ func chatForumTopicMenuItems(context: AccountContext, peerId: PeerId, threadId:
}
}
items.append(.separator)
items.append(.action(ContextMenuActionItem(text: strings.ChatList_Context_Select, textColor: .primary, icon: { theme in generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Select"), color: theme.contextMenu.primaryColor) }, action: { _, f in
f(.default)
chatListController?.selectPeerThread(peerId: peerId, threadId: threadId)
})))
if canSelect {
items.append(.separator)
items.append(.action(ContextMenuActionItem(text: strings.ChatList_Context_Select, textColor: .primary, icon: { theme in generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Select"), color: theme.contextMenu.primaryColor) }, action: { _, f in
f(.default)
chatListController?.selectPeerThread(peerId: peerId, threadId: threadId)
})))
}
return .single(items)
}

File diff suppressed because it is too large Load Diff

View File

@ -1268,7 +1268,7 @@ final class ChatListControllerNode: ASDisplayNode, UIGestureRecognizerDelegate {
guard let strongSelf = self, strongSelf.inlineStackContainerNode != nil else {
return []
}
let directions: InteractiveTransitionGestureRecognizerDirections = [.leftCenter, .rightCenter]
let directions: InteractiveTransitionGestureRecognizerDirections = [.rightCenter]
return directions
}, edgeWidth: .widthMultiplier(factor: 1.0 / 6.0, min: 22.0, max: 80.0))
inlineContentPanRecognizer.delegate = self
@ -1345,7 +1345,7 @@ final class ChatListControllerNode: ASDisplayNode, UIGestureRecognizerDelegate {
}
if let directionIsToRight = directionIsToRight, directionIsToRight {
self.setInlineChatList(location: nil)
self.controller?.setInlineChatList(location: nil)
} else {
self.inlineStackContainerTransitionFraction = 1.0
self.controller?.requestLayout(transition: .animated(duration: 0.4, curve: .spring))
@ -1482,12 +1482,14 @@ final class ChatListControllerNode: ASDisplayNode, UIGestureRecognizerDelegate {
return nil
}
let effectiveLocation = self.inlineStackContainerNode?.location ?? self.location
var filter: ChatListNodePeersFilter = []
if case .forum = self.location {
if case .forum = effectiveLocation {
filter.insert(.excludeRecent)
}
let contentNode = ChatListSearchContainerNode(context: self.context, animationCache: self.animationCache, animationRenderer: self.animationRenderer, filter: filter, location: location, displaySearchFilters: displaySearchFilters, hasDownloads: hasDownloads, initialFilter: initialFilter, openPeer: { [weak self] peer, _, threadId, dismissSearch in
let contentNode = ChatListSearchContainerNode(context: self.context, animationCache: self.animationCache, animationRenderer: self.animationRenderer, filter: filter, location: effectiveLocation, displaySearchFilters: displaySearchFilters, hasDownloads: hasDownloads, initialFilter: initialFilter, openPeer: { [weak self] peer, _, threadId, dismissSearch in
self?.requestOpenPeerFromSearch?(peer, threadId, dismissSearch)
}, openDisabledPeer: { _, _ in
}, openRecentPeerOptions: { [weak self] peer in

View File

@ -745,7 +745,7 @@ public enum ChatListSearchEntry: Comparable, Identifiable {
index = .chatList( EngineChatList.Item.Index.ChatList(pinningIndex: nil, messageIndex: message.index))
}
}
return ChatListItem(presentationData: presentationData, context: context, chatListLocation: .chatList(groupId: .root), filterData: nil, index: index, content: .peer(messages: [message], peer: peer, threadInfo: chatThreadInfo, combinedReadState: readState, isRemovedFromTotalUnreadCount: false, presence: nil, hasUnseenMentions: false, hasUnseenReactions: false, draftState: nil, inputActivities: nil, promoInfo: nil, ignoreUnreadBadge: true, displayAsMessage: false, hasFailedMessages: false, forumTopicData: nil, topForumTopicItems: []), editing: false, hasActiveRevealControls: false, selected: false, header: tagMask == nil ? header : nil, enableContextActions: false, hiddenOffset: false, interaction: interaction)
return ChatListItem(presentationData: presentationData, context: context, chatListLocation: location, filterData: nil, index: index, content: .peer(messages: [message], peer: peer, threadInfo: chatThreadInfo, combinedReadState: readState, isRemovedFromTotalUnreadCount: false, presence: nil, hasUnseenMentions: false, hasUnseenReactions: false, draftState: nil, inputActivities: nil, promoInfo: nil, ignoreUnreadBadge: true, displayAsMessage: false, hasFailedMessages: false, forumTopicData: nil, topForumTopicItems: []), editing: false, hasActiveRevealControls: false, selected: false, header: tagMask == nil ? header : nil, enableContextActions: false, hiddenOffset: false, interaction: interaction)
}
case let .addContact(phoneNumber, theme, strings):
return ContactsAddItem(theme: theme, strings: strings, phoneNumber: phoneNumber, header: ChatListSearchItemHeader(type: .phoneNumber, theme: theme, strings: strings, actionTitle: nil, action: nil), action: {

View File

@ -34,8 +34,8 @@ private func measureString(_ string: String) -> String {
}
final class ChatListBadgeNode: ASDisplayNode {
private let backgroundNode: ASImageNode
private let textNode: TextNode
let backgroundNode: ASImageNode
let textNode: TextNode
private let measureTextNode: TextNode
private var text: String?
@ -43,6 +43,8 @@ final class ChatListBadgeNode: ASDisplayNode {
private var isHiddenInternal = false
var disableBounce: Bool = false
override init() {
self.backgroundNode = ASImageNode()
self.backgroundNode.isLayerBacked = true
@ -97,7 +99,7 @@ final class ChatListBadgeNode: ASDisplayNode {
}
let badgeWidth = max(imageWidth, badgeWidth)
let previousBadgeWidth = !strongSelf.backgroundNode.frame.width.isZero ? strongSelf.backgroundNode.frame.width : badgeWidth
let previousBadgeWidth = !strongSelf.backgroundNode.bounds.width.isZero ? strongSelf.backgroundNode.bounds.width : badgeWidth
var animateTextNode = false
if animated {
@ -116,14 +118,16 @@ final class ChatListBadgeNode: ASDisplayNode {
if currentIsEmpty && !nextIsEmpty {
strongSelf.isHiddenInternal = false
if bounce {
strongSelf.layer.animateScale(from: 0.0001, to: 1.2, duration: 0.2, removeOnCompletion: false, completion: { [weak self] finished in
if let strongSelf = self {
strongSelf.layer.animateScale(from: 1.15, to: 1.0, duration: 0.12, removeOnCompletion: false)
}
})
} else {
strongSelf.layer.animateScale(from: 0.0001, to: 1.0, duration: 0.2, removeOnCompletion: false)
if !strongSelf.disableBounce {
if bounce {
strongSelf.layer.animateScale(from: 0.0001, to: 1.2, duration: 0.2, removeOnCompletion: false, completion: { [weak self] finished in
if let strongSelf = self {
strongSelf.layer.animateScale(from: 1.15, to: 1.0, duration: 0.12, removeOnCompletion: false)
}
})
} else {
strongSelf.layer.animateScale(from: 0.0001, to: 1.0, duration: 0.2, removeOnCompletion: false)
}
}
} else if !currentIsEmpty && !nextIsEmpty && currentContent?.text != content.text {
var animateScale = bounce
@ -134,7 +138,7 @@ final class ChatListBadgeNode: ASDisplayNode {
}
}
if animateScale {
if animateScale && !strongSelf.disableBounce {
strongSelf.layer.animateScale(from: 1.0, to: 1.2, duration: 0.12, removeOnCompletion: false, completion: { [weak self] finished in
if let strongSelf = self {
strongSelf.layer.animateScale(from: 1.2, to: 1.0, duration: 0.12, removeOnCompletion: false)
@ -157,12 +161,16 @@ final class ChatListBadgeNode: ASDisplayNode {
animateTextNode = true
} else if !currentIsEmpty && nextIsEmpty && !strongSelf.isHiddenInternal {
strongSelf.isHiddenInternal = true
strongSelf.layer.animateScale(from: 1.0, to: 0.0001, duration: 0.12, removeOnCompletion: false, completion: { [weak self] finished in
if let strongSelf = self {
strongSelf.isHidden = true
strongSelf.layer.removeAnimation(forKey: "transform.scale")
}
})
if !strongSelf.disableBounce {
strongSelf.layer.animateScale(from: 1.0, to: 0.0001, duration: 0.12, removeOnCompletion: false, completion: { [weak self] finished in
if let strongSelf = self {
strongSelf.isHidden = true
strongSelf.layer.removeAnimation(forKey: "transform.scale")
}
})
} else {
strongSelf.isHidden = true
}
}
} else {
if case .none = content {
@ -183,14 +191,16 @@ final class ChatListBadgeNode: ASDisplayNode {
let backgroundFrame = CGRect(x: 0.0, y: 0.0, width: badgeWidth, height: strongSelf.backgroundNode.image?.size.height ?? 0.0)
if let (textLayout, _) = textLayoutAndApply {
let badgeTextFrame = CGRect(origin: CGPoint(x: backgroundFrame.midX - textLayout.size.width / 2.0, y: backgroundFrame.minY + 2.0), size: textLayout.size)
strongSelf.textNode.frame = badgeTextFrame
let badgeTextFrame = CGRect(origin: CGPoint(x: backgroundFrame.midX - textLayout.size.width / 2.0, y: backgroundFrame.minY + floorToScreenPixels((backgroundFrame.height - textLayout.size.height) / 2.0)), size: textLayout.size)
strongSelf.textNode.position = badgeTextFrame.center
strongSelf.textNode.bounds = CGRect(origin: CGPoint(), size: badgeTextFrame.size)
if animateTextNode {
strongSelf.textNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.15)
strongSelf.textNode.layer.animatePosition(from: CGPoint(x: (previousBadgeWidth - badgeWidth) / 2.0, y: 8.0), to: CGPoint(), duration: 0.15, timingFunction: CAMediaTimingFunctionName.easeInEaseOut.rawValue, removeOnCompletion: false, additive: true)
}
}
strongSelf.backgroundNode.frame = backgroundFrame
strongSelf.backgroundNode.position = backgroundFrame.center
strongSelf.backgroundNode.bounds = CGRect(origin: CGPoint(), size: backgroundFrame.size)
if animated && badgeWidth != previousBadgeWidth {
let previousBackgroundFrame = CGRect(x: 0.0, y: 0.0, width: previousBadgeWidth, height: backgroundFrame.height)

View File

@ -816,6 +816,8 @@ class ChatListItemNode: ItemListRevealOptionsItemNode {
let statusNode: ChatListStatusNode
let badgeNode: ChatListBadgeNode
let mentionBadgeNode: ChatListBadgeNode
var avatarBadgeNode: ChatListBadgeNode?
var avatarBadgeBackground: ASImageNode?
let onlineNode: PeerOnlineMarkerNode
let pinnedIconNode: ASImageNode
var secretIconNode: ASImageNode?
@ -1115,9 +1117,14 @@ class ChatListItemNode: ItemListRevealOptionsItemNode {
})
self.contextContainer.shouldBegin = { [weak self] location in
guard let strongSelf = self else {
guard let strongSelf = self, let item = strongSelf.item else {
return false
}
if item.interaction.inlineNavigationLocation != nil {
return false
}
if let value = strongSelf.hitTest(location, with: nil), value === strongSelf.compoundTextButtonNode?.view {
strongSelf.contextContainer.targetNodeForActivationProgress = strongSelf.compoundHighlightingNode
} else {
@ -1373,6 +1380,7 @@ class ChatListItemNode: ItemListRevealOptionsItemNode {
let textFont = Font.regular(floor(item.presentationData.fontSize.itemListBaseFontSize * 15.0 / 17.0))
let dateFont = Font.regular(floor(item.presentationData.fontSize.itemListBaseFontSize * 14.0 / 17.0))
let badgeFont = Font.with(size: floor(item.presentationData.fontSize.itemListBaseFontSize * 14.0 / 17.0), design: .regular, weight: .regular, traits: [.monospacedNumbers])
let avatarBadgeFont = Font.with(size: 16.0, design: .regular, weight: .regular, traits: [.monospacedNumbers])
let account = item.context.account
var messages: [EngineMessage]
@ -1497,6 +1505,7 @@ class ChatListItemNode: ItemListRevealOptionsItemNode {
var statusState = ChatListStatusNodeState.none
var currentBadgeBackgroundImage: UIImage?
var currentAvatarBadgeBackgroundImage: UIImage?
var currentMentionBadgeImage: UIImage?
var currentPinnedIconImage: UIImage?
var currentMutedIconImage: UIImage?
@ -1547,6 +1556,9 @@ class ChatListItemNode: ItemListRevealOptionsItemNode {
}
let badgeDiameter = floor(item.presentationData.fontSize.baseDisplaySize * 20.0 / 17.0)
let avatarBadgeDiameter: CGFloat = 22.0
let currentAvatarBadgeCleanBackgroundImage: UIImage? = PresentationResourcesChatList.badgeBackgroundBorder(item.presentationData.theme, diameter: avatarBadgeDiameter + 4.0)
let leftInset: CGFloat = params.leftInset + avatarLeftInset
@ -1935,17 +1947,21 @@ class ChatListItemNode: ItemListRevealOptionsItemNode {
if unreadCount.isProvisonal {
badgeTextColor = theme.unreadBadgeInactiveBackgroundColor
currentBadgeBackgroundImage = PresentationResourcesChatList.badgeBackgroundInactiveProvisional(item.presentationData.theme, diameter: badgeDiameter)
currentAvatarBadgeBackgroundImage = PresentationResourcesChatList.badgeBackgroundInactiveProvisional(item.presentationData.theme, diameter: avatarBadgeDiameter)
} else {
badgeTextColor = theme.unreadBadgeInactiveTextColor
currentBadgeBackgroundImage = PresentationResourcesChatList.badgeBackgroundInactive(item.presentationData.theme, diameter: badgeDiameter)
currentAvatarBadgeBackgroundImage = PresentationResourcesChatList.badgeBackgroundInactive(item.presentationData.theme, diameter: avatarBadgeDiameter)
}
} else {
if unreadCount.isProvisonal {
badgeTextColor = theme.unreadBadgeActiveBackgroundColor
currentBadgeBackgroundImage = PresentationResourcesChatList.badgeBackgroundActiveProvisional(item.presentationData.theme, diameter: badgeDiameter)
currentAvatarBadgeBackgroundImage = PresentationResourcesChatList.badgeBackgroundActiveProvisional(item.presentationData.theme, diameter: avatarBadgeDiameter)
} else {
badgeTextColor = theme.unreadBadgeActiveTextColor
currentBadgeBackgroundImage = PresentationResourcesChatList.badgeBackgroundActive(item.presentationData.theme, diameter: badgeDiameter)
currentAvatarBadgeBackgroundImage = PresentationResourcesChatList.badgeBackgroundActive(item.presentationData.theme, diameter: avatarBadgeDiameter)
}
}
let unreadCountText = compactNumericCountString(Int(unreadCount.count), decimalSeparator: item.presentationData.dateTimeFormat.decimalSeparator)
@ -2490,6 +2506,61 @@ class ChatListItemNode: ItemListRevealOptionsItemNode {
}
}
if let inlineNavigationLocation = item.interaction.inlineNavigationLocation, badgeContent != .none {
var animateIn = false
let avatarBadgeBackground: ASImageNode
if let current = strongSelf.avatarBadgeBackground {
avatarBadgeBackground = current
} else {
avatarBadgeBackground = ASImageNode()
strongSelf.avatarBadgeBackground = avatarBadgeBackground
strongSelf.avatarNode.addSubnode(avatarBadgeBackground)
}
avatarBadgeBackground.image = currentAvatarBadgeCleanBackgroundImage
let avatarBadgeNode: ChatListBadgeNode
if let current = strongSelf.avatarBadgeNode {
avatarBadgeNode = current
} else {
animateIn = true
avatarBadgeNode = ChatListBadgeNode()
avatarBadgeNode.disableBounce = true
strongSelf.avatarBadgeNode = avatarBadgeNode
strongSelf.avatarNode.addSubnode(avatarBadgeNode)
}
let makeAvatarBadgeLayout = avatarBadgeNode.asyncLayout()
let (avatarBadgeLayout, avatarBadgeApply) = makeAvatarBadgeLayout(CGSize(width: rawContentWidth, height: CGFloat.greatestFiniteMagnitude), avatarBadgeDiameter, avatarBadgeFont, currentAvatarBadgeBackgroundImage, badgeContent)
let _ = avatarBadgeApply(animateBadges, false)
let avatarBadgeFrame = CGRect(origin: CGPoint(x: avatarFrame.width - avatarBadgeLayout.width, y: avatarFrame.height - avatarBadgeLayout.height), size: avatarBadgeLayout)
avatarBadgeNode.position = avatarBadgeFrame.center
avatarBadgeNode.bounds = CGRect(origin: CGPoint(), size: avatarBadgeFrame.size)
let avatarBadgeBackgroundFrame = avatarBadgeFrame.insetBy(dx: -2.0, dy: -2.0)
avatarBadgeBackground.position = avatarBadgeBackgroundFrame.center
avatarBadgeBackground.bounds = CGRect(origin: CGPoint(), size: avatarBadgeBackgroundFrame.size)
if animateIn {
ContainedViewLayoutTransition.immediate.updateSublayerTransformScale(node: avatarBadgeNode, scale: 0.001)
ContainedViewLayoutTransition.immediate.updateTransformScale(layer: avatarBadgeBackground.layer, scale: 0.001)
}
transition.updateSublayerTransformScale(node: avatarBadgeNode, scale: max(0.001, inlineNavigationLocation.progress))
transition.updateTransformScale(layer: avatarBadgeBackground.layer, scale: max(0.001, inlineNavigationLocation.progress))
} else if let avatarBadgeNode = strongSelf.avatarBadgeNode {
strongSelf.avatarBadgeNode = nil
transition.updateSublayerTransformScale(node: avatarBadgeNode, scale: 0.001, completion: { [weak avatarBadgeNode] _ in
avatarBadgeNode?.removeFromSupernode()
})
if let avatarBadgeBackground = strongSelf.avatarBadgeBackground {
strongSelf.avatarBadgeBackground = nil
transition.updateTransformScale(layer: avatarBadgeBackground.layer, scale: 0.001, completion: { [weak avatarBadgeBackground] _ in
avatarBadgeBackground?.removeFromSupernode()
})
}
}
if let threadInfo = threadInfo {
let avatarIconView: ComponentHostView<Empty>
if let current = strongSelf.avatarIconView {

View File

@ -110,24 +110,24 @@ public final class NavigationBarPresentationData {
}
}
enum NavigationPreviousAction: Equatable {
public enum NavigationPreviousAction: Equatable {
case item(UINavigationItem)
case close
static func ==(lhs: NavigationPreviousAction, rhs: NavigationPreviousAction) -> Bool {
public static func ==(lhs: NavigationPreviousAction, rhs: NavigationPreviousAction) -> Bool {
switch lhs {
case let .item(lhsItem):
if case let .item(rhsItem) = rhs, lhsItem === rhsItem {
return true
} else {
return false
}
case .close:
if case .close = rhs {
return true
} else {
return false
}
case let .item(lhsItem):
if case let .item(rhsItem) = rhs, lhsItem === rhsItem {
return true
} else {
return false
}
case .close:
if case .close = rhs {
return true
} else {
return false
}
}
}
}
@ -439,7 +439,6 @@ open class BlurredBackgroundView: UIView {
}
public protocol NavigationBarHeaderView: UIView {
func update(size: CGSize, transition: ContainedViewLayoutTransition)
}
open class NavigationBar: ASDisplayNode {
@ -657,7 +656,12 @@ open class NavigationBar: ASDisplayNode {
self.customHeaderContentView?.removeFromSuperview()
if let customHeaderContentView = self.customHeaderContentView {
self.view.addSubview(customHeaderContentView)
self.buttonsContainerNode.view.addSubview(customHeaderContentView)
self.backButtonNode.isHidden = true
self.backButtonArrow.isHidden = true
} else {
self.backButtonNode.isHidden = false
self.backButtonArrow.isHidden = false
}
}
}
@ -708,7 +712,7 @@ open class NavigationBar: ASDisplayNode {
}
var _previousItem: NavigationPreviousAction?
var previousItem: NavigationPreviousAction? {
public internal(set) var previousItem: NavigationPreviousAction? {
get {
return self._previousItem
} set(value) {
@ -1373,7 +1377,7 @@ open class NavigationBar: ASDisplayNode {
if let customHeaderContentView = self.customHeaderContentView {
let headerSize = CGSize(width: size.width, height: nominalHeight)
customHeaderContentView.update(size: headerSize, transition: transition)
//customHeaderContentView.update(size: headerSize, transition: transition)
transition.updateFrame(view: customHeaderContentView, frame: CGRect(origin: CGPoint(x: 0.0, y: contentVerticalOrigin), size: headerSize))
}

View File

@ -310,6 +310,7 @@ public enum PresentationResourceParameterKey: Hashable {
case badgeBackgroundInactiveReactions(CGFloat)
case chatListBadgeBackgroundInactiveMention(CGFloat)
case chatListBadgeBackgroundPinned(CGFloat)
case badgeBackgroundBorder(CGFloat)
case chatBubbleMediaCorner(incoming: Bool, mainRadius: CGFloat, inset: CGFloat)

View File

@ -235,6 +235,12 @@ public struct PresentationResourcesChatList {
})
}
public static func badgeBackgroundBorder(_ theme: PresentationTheme, diameter: CGFloat) -> UIImage? {
return theme.image(PresentationResourceParameterKey.badgeBackgroundBorder(diameter), { theme in
return generateStretchableFilledCircleImage(diameter: diameter, color: theme.chatList.pinnedItemBackgroundColor.blitOver(theme.chatList.backgroundColor, alpha: 1.0))
})
}
public static func mutedIcon(_ theme: PresentationTheme) -> UIImage? {
return theme.image(PresentationResourceKey.chatListMutedIcon.rawValue, { theme in
return generateTintedImage(image: UIImage(bundleImageName: "Chat List/PeerMutedIcon"), color: theme.chatList.muteIconColor)

View File

@ -302,6 +302,7 @@ swift_library(
"//submodules/TelegramUI/Components/ChatTitleView",
"//submodules/InviteLinksUI:InviteLinksUI",
"//submodules/TelegramUI/Components/NotificationPeerExceptionController",
"//submodules/TelegramUI/Components/ChatListHeaderComponent",
"//submodules/MediaPasteboardUI:MediaPasteboardUI",
] + select({
"@build_bazel_rules_apple//apple:ios_armv7": [],

View File

@ -10,9 +10,15 @@ swift_library(
"-warnings-as-errors",
],
deps = [
"//submodules/SSignalKit/SwiftSignalKit:SwiftSignalKit",
"//submodules/Display:Display",
"//submodules/ComponentFlow:ComponentFlow",
"//submodules/SSignalKit/SwiftSignalKit",
"//submodules/Display",
"//submodules/ComponentFlow",
"//submodules/TelegramPresentationData",
"//submodules/TelegramUI/Components/ChatListTitleView",
"//submodules/AccountContext",
"//submodules/AppBundle",
"//submodules/AsyncDisplayKit",
"//submodules/AnimationUI",
],
visibility = [
"//visibility:public",

View File

@ -2,14 +2,932 @@ import Foundation
import UIKit
import Display
import ComponentFlow
import TelegramPresentationData
import AccountContext
import ChatListTitleView
import AppBundle
/*public final class ChatListHeaderComponent: Component {
public final class View: UIView, NavigationBarHeaderView {
public func update(size: CGSize, transition: ContainedViewLayoutTransition) {
public final class HeaderNetworkStatusComponent: Component {
public enum Content: Equatable {
case connecting
case updating
}
public let content: Content
public let theme: PresentationTheme
public let strings: PresentationStrings
public init(
content: Content,
theme: PresentationTheme,
strings: PresentationStrings
) {
self.content = content
self.theme = theme
self.strings = strings
}
public static func ==(lhs: HeaderNetworkStatusComponent, rhs: HeaderNetworkStatusComponent) -> Bool {
if lhs.content != rhs.content {
return false
}
if lhs.theme !== rhs.theme {
return false
}
if lhs.strings !== rhs.strings {
return false
}
return true
}
public final class View: UIView {
private var component: HeaderNetworkStatusComponent?
private weak var state: EmptyComponentState?
override init(frame: CGRect) {
super.init(frame: frame)
}
required public init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func update(component: HeaderNetworkStatusComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: Transition) -> CGSize {
self.state = state
return availableSize
}
}
public func makeView() -> View {
return View(frame: CGRect())
}
public 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 ChatListHeaderComponent: Component {
public final class Content: Equatable {
public let title: String
public let titleComponent: AnyComponent<Empty>?
public let chatListTitle: NetworkStatusTitle?
public let leftButton: AnyComponentWithIdentity<NavigationButtonComponentEnvironment>?
public let rightButtons: [AnyComponentWithIdentity<NavigationButtonComponentEnvironment>]
public let backTitle: String?
public let backPressed: (() -> Void)?
public init(
title: String,
titleComponent: AnyComponent<Empty>?,
chatListTitle: NetworkStatusTitle?,
leftButton: AnyComponentWithIdentity<NavigationButtonComponentEnvironment>?,
rightButtons: [AnyComponentWithIdentity<NavigationButtonComponentEnvironment>],
backTitle: String?,
backPressed: (() -> Void)?
) {
self.title = title
self.titleComponent = titleComponent
self.chatListTitle = chatListTitle
self.leftButton = leftButton
self.rightButtons = rightButtons
self.backTitle = backTitle
self.backPressed = backPressed
}
public static func ==(lhs: Content, rhs: Content) -> Bool {
if lhs.title != rhs.title {
return false
}
if lhs.titleComponent != rhs.titleComponent {
return false
}
if lhs.chatListTitle != rhs.chatListTitle {
return false
}
if lhs.leftButton != rhs.leftButton {
return false
}
if lhs.rightButtons != rhs.rightButtons {
return false
}
if lhs.backTitle != rhs.backTitle {
return false
}
return true
}
}
public let sideInset: CGFloat
public let primaryContent: Content?
public let secondaryContent: Content?
public let secondaryTransition: CGFloat
public let networkStatus: HeaderNetworkStatusComponent.Content?
public let context: AccountContext
public let theme: PresentationTheme
public let strings: PresentationStrings
public let openStatusSetup: (UIView) -> Void
public let toggleIsLocked: () -> Void
public init(
sideInset: CGFloat,
primaryContent: Content?,
secondaryContent: Content?,
secondaryTransition: CGFloat,
networkStatus: HeaderNetworkStatusComponent.Content?,
context: AccountContext,
theme: PresentationTheme,
strings: PresentationStrings,
openStatusSetup: @escaping (UIView) -> Void,
toggleIsLocked: @escaping () -> Void
) {
self.sideInset = sideInset
self.primaryContent = primaryContent
self.secondaryContent = secondaryContent
self.secondaryTransition = secondaryTransition
self.context = context
self.networkStatus = networkStatus
self.theme = theme
self.strings = strings
self.openStatusSetup = openStatusSetup
self.toggleIsLocked = toggleIsLocked
}
public static func ==(lhs: ChatListHeaderComponent, rhs: ChatListHeaderComponent) -> Bool {
if lhs.sideInset != rhs.sideInset {
return false
}
if lhs.primaryContent != rhs.primaryContent {
return false
}
if lhs.secondaryContent != rhs.secondaryContent {
return false
}
if lhs.secondaryTransition != rhs.secondaryTransition {
return false
}
if lhs.networkStatus != rhs.networkStatus {
return false
}
if lhs.context !== rhs.context {
return false
}
if lhs.theme !== rhs.theme {
return false
}
if lhs.strings !== rhs.strings {
return false
}
return true
}
private final class BackButtonView: HighlightableButton {
private let onPressed: () -> Void
let arrowView: UIImageView
let titleOffsetContainer: UIView
let titleView: ImmediateTextView
private var currentColor: UIColor?
init(onPressed: @escaping () -> Void) {
self.onPressed = onPressed
self.arrowView = UIImageView()
self.titleOffsetContainer = UIView()
self.titleView = ImmediateTextView()
super.init(frame: CGRect())
self.addSubview(self.arrowView)
self.addSubview(self.titleOffsetContainer)
self.titleOffsetContainer.addSubview(self.titleView)
self.highligthedChanged = { [weak self] highlighted in
guard let self else {
return
}
if highlighted {
self.alpha = 0.6
} else {
self.layer.animateAlpha(from: 0.6, to: 1.0, duration: 0.2)
}
}
self.addTarget(self, action: #selector(self.pressed), for: .touchUpInside)
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
@objc private func pressed() {
self.onPressed()
}
func update(title: String, theme: PresentationTheme, availableSize: CGSize, transition: Transition) -> CGSize {
self.titleView.attributedText = NSAttributedString(string: title, font: Font.regular(17.0), textColor: theme.rootController.navigationBar.accentTextColor)
let titleSize = self.titleView.updateLayout(CGSize(width: 100.0, height: 44.0))
if self.currentColor != theme.rootController.navigationBar.accentTextColor {
self.currentColor = theme.rootController.navigationBar.accentTextColor
self.arrowView.image = NavigationBarTheme.generateBackArrowImage(color: theme.rootController.navigationBar.accentTextColor)
}
let iconSpacing: CGFloat = 8.0
let iconOffset: CGFloat = -7.0
let arrowSize = self.arrowView.image?.size ?? CGSize(width: 13.0, height: 22.0)
let arrowFrame = CGRect(origin: CGPoint(x: iconOffset, y: floor((availableSize.height - arrowSize.height) / 2.0)), size: arrowSize)
transition.setPosition(view: self.arrowView, position: arrowFrame.center)
transition.setBounds(view: self.arrowView, bounds: CGRect(origin: CGPoint(), size: arrowFrame.size))
transition.setFrame(view: self.titleView, frame: CGRect(origin: CGPoint(x: iconOffset + arrowSize.width + iconSpacing, y: floor((availableSize.height - titleSize.height) / 2.0)), size: titleSize))
return CGSize(width: iconOffset + arrowSize.width + iconSpacing + titleSize.width, height: availableSize.height)
}
}
private final class ContentView: UIView {
let backPressed: () -> Void
let openStatusSetup: (UIView) -> Void
let toggleIsLocked: () -> Void
let leftButtonOffsetContainer: UIView
var leftButtonViews: [AnyHashable: ComponentView<NavigationButtonComponentEnvironment>] = [:]
let rightButtonOffsetContainer: UIView
var rightButtonViews: [AnyHashable: ComponentView<NavigationButtonComponentEnvironment>] = [:]
var backButtonView: BackButtonView?
let titleOffsetContainer: UIView
let titleTextView: ImmediateTextView
var titleContentView: ComponentView<Empty>?
var chatListTitleView: ChatListTitleView?
init(
backPressed: @escaping () -> Void,
openStatusSetup: @escaping (UIView) -> Void,
toggleIsLocked: @escaping () -> Void
) {
self.backPressed = backPressed
self.openStatusSetup = openStatusSetup
self.toggleIsLocked = toggleIsLocked
self.leftButtonOffsetContainer = UIView()
self.rightButtonOffsetContainer = UIView()
self.titleOffsetContainer = UIView()
self.titleTextView = ImmediateTextView()
super.init(frame: CGRect())
self.addSubview(self.titleOffsetContainer)
self.addSubview(self.leftButtonOffsetContainer)
self.addSubview(self.rightButtonOffsetContainer)
self.titleOffsetContainer.addSubview(self.titleTextView)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
if let view = self.titleContentView?.view, let result = view.hitTest(self.convert(point, to: view), with: event) {
return result
}
if let view = self.chatListTitleView, let result = view.hitTest(self.convert(point, to: view), with: event) {
return result
}
if let backButtonView = self.backButtonView {
if let result = backButtonView.hitTest(self.convert(point, to: backButtonView), with: event) {
return result
}
}
for (_, buttonView) in self.leftButtonViews {
if let view = buttonView.view, let result = view.hitTest(self.convert(point, to: view), with: event) {
return result
}
}
for (_, buttonView) in self.rightButtonViews {
if let view = buttonView.view, let result = view.hitTest(self.convert(point, to: view), with: event) {
return result
}
}
return nil
}
func updateNavigationTransitionAsPrevious(nextView: ContentView, fraction: CGFloat, transition: Transition, completion: @escaping () -> Void) {
transition.setBounds(view: self.leftButtonOffsetContainer, bounds: CGRect(origin: CGPoint(x: fraction * self.bounds.width * 0.5, y: 0.0), size: self.leftButtonOffsetContainer.bounds.size), completion: { _ in
completion()
})
transition.setAlpha(view: self.rightButtonOffsetContainer, alpha: pow(1.0 - fraction, 2.0))
if let chatListTitleView = self.chatListTitleView, let nextBackButtonView = nextView.backButtonView {
let titleFrame = chatListTitleView.titleNode.view.convert(chatListTitleView.titleNode.bounds, to: self.titleOffsetContainer)
let backButtonTitleFrame = nextBackButtonView.convert(nextBackButtonView.titleView.frame, to: nextView)
let totalOffset = titleFrame.midX - backButtonTitleFrame.midX
transition.setBounds(view: self.titleOffsetContainer, bounds: CGRect(origin: CGPoint(x: totalOffset * fraction, y: 0.0), size: self.titleOffsetContainer.bounds.size))
transition.setAlpha(view: self.titleOffsetContainer, alpha: (1.0 - fraction))
}
}
func updateNavigationTransitionAsNext(previousView: ContentView, fraction: CGFloat, transition: Transition, completion: @escaping () -> Void) {
transition.setBounds(view: self.titleOffsetContainer, bounds: CGRect(origin: CGPoint(x: -(1.0 - fraction) * self.bounds.width, y: 0.0), size: self.titleOffsetContainer.bounds.size), completion: { _ in
completion()
})
transition.setBounds(view: self.rightButtonOffsetContainer, bounds: CGRect(origin: CGPoint(x: -(1.0 - fraction) * self.bounds.width, y: 0.0), size: self.rightButtonOffsetContainer.bounds.size))
if let backButtonView = self.backButtonView {
transition.setScale(view: backButtonView.arrowView, scale: pow(max(0.001, fraction), 2.0))
transition.setAlpha(view: backButtonView.arrowView, alpha: pow(fraction, 2.0))
if let previousChatListTitleView = previousView.chatListTitleView {
let previousTitleFrame = previousChatListTitleView.titleNode.view.convert(previousChatListTitleView.titleNode.bounds, to: previousView.titleOffsetContainer)
let backButtonTitleFrame = backButtonView.convert(backButtonView.titleView.frame, to: self)
let totalOffset = previousTitleFrame.midX - backButtonTitleFrame.midX
transition.setBounds(view: backButtonView.titleOffsetContainer, bounds: CGRect(origin: CGPoint(x: -totalOffset * (1.0 - fraction), y: 0.0), size: backButtonView.titleOffsetContainer.bounds.size))
transition.setAlpha(view: backButtonView.titleOffsetContainer, alpha: fraction)
}
}
}
func update(context: AccountContext, theme: PresentationTheme, strings: PresentationStrings, content: Content, backTitle: String?, sideInset: CGFloat, size: CGSize, transition: Transition) {
self.titleTextView.attributedText = NSAttributedString(string: content.title, font: Font.semibold(17.0), textColor: theme.rootController.navigationBar.primaryTextColor)
let buttonSpacing: CGFloat = 8.0
var leftOffset = sideInset
if let backTitle = backTitle {
var backButtonTransition = transition
let backButtonView: BackButtonView
if let current = self.backButtonView {
backButtonView = current
} else {
backButtonTransition = .immediate
backButtonView = BackButtonView(onPressed: { [weak self] in
guard let self else {
return
}
self.backPressed()
})
self.backButtonView = backButtonView
self.addSubview(backButtonView)
}
let backButtonSize = backButtonView.update(title: backTitle, theme: theme, availableSize: CGSize(width: 100.0, height: size.height), transition: backButtonTransition)
backButtonTransition.setFrame(view: backButtonView, frame: CGRect(origin: CGPoint(x: leftOffset, y: floor((size.height - backButtonSize.height) / 2.0)), size: backButtonSize))
leftOffset += backButtonSize.width + buttonSpacing
} else if let backButtonView = self.backButtonView {
self.backButtonView = nil
backButtonView.removeFromSuperview()
}
var validLeftButtons = Set<AnyHashable>()
if let leftButton = content.leftButton {
validLeftButtons.insert(leftButton.id)
var buttonTransition = transition
var animateButtonIn = false
let buttonView: ComponentView<NavigationButtonComponentEnvironment>
if let current = self.leftButtonViews[leftButton.id] {
buttonView = current
} else {
buttonTransition = .immediate
animateButtonIn = true
buttonView = ComponentView<NavigationButtonComponentEnvironment>()
self.leftButtonViews[leftButton.id] = buttonView
}
let buttonSize = buttonView.update(
transition: buttonTransition,
component: leftButton.component,
environment: {
NavigationButtonComponentEnvironment(theme: theme)
},
containerSize: CGSize(width: 100.0, height: size.height)
)
let buttonFrame = CGRect(origin: CGPoint(x: leftOffset, y: floor((size.height - buttonSize.height) / 2.0)), size: buttonSize)
if let buttonComponentView = buttonView.view {
if buttonComponentView.superview == nil {
self.leftButtonOffsetContainer.addSubview(buttonComponentView)
}
buttonTransition.setFrame(view: buttonComponentView, frame: buttonFrame)
if animateButtonIn {
transition.animateAlpha(view: buttonComponentView, from: 0.0, to: 1.0)
}
}
leftOffset = buttonFrame.maxX + buttonSpacing
}
var removeLeftButtons: [AnyHashable] = []
for (id, buttonView) in self.leftButtonViews {
if !validLeftButtons.contains(id) {
if let buttonComponentView = buttonView.view {
transition.setAlpha(view: buttonComponentView, alpha: 0.0, completion: { [weak buttonComponentView] _ in
buttonComponentView?.removeFromSuperview()
})
}
removeLeftButtons.append(id)
}
}
for id in removeLeftButtons {
self.leftButtonViews.removeValue(forKey: id)
}
var rightOffset = size.width - sideInset
var validRightButtons = Set<AnyHashable>()
for rightButton in content.rightButtons {
validRightButtons.insert(rightButton.id)
var buttonTransition = transition
var animateButtonIn = false
let buttonView: ComponentView<NavigationButtonComponentEnvironment>
if let current = self.rightButtonViews[rightButton.id] {
buttonView = current
} else {
buttonTransition = .immediate
animateButtonIn = true
buttonView = ComponentView<NavigationButtonComponentEnvironment>()
self.rightButtonViews[rightButton.id] = buttonView
}
let buttonSize = buttonView.update(
transition: buttonTransition,
component: rightButton.component,
environment: {
NavigationButtonComponentEnvironment(theme: theme)
},
containerSize: CGSize(width: 100.0, height: size.height)
)
let buttonFrame = CGRect(origin: CGPoint(x: rightOffset - buttonSize.width, y: floor((size.height - buttonSize.height) / 2.0)), size: buttonSize)
if let buttonComponentView = buttonView.view {
if buttonComponentView.superview == nil {
self.rightButtonOffsetContainer.addSubview(buttonComponentView)
}
buttonTransition.setFrame(view: buttonComponentView, frame: buttonFrame)
if animateButtonIn {
transition.animateAlpha(view: buttonComponentView, from: 0.0, to: 1.0)
}
}
rightOffset = buttonFrame.minX - buttonSpacing
}
var removeRightButtons: [AnyHashable] = []
for (id, buttonView) in self.rightButtonViews {
if !validRightButtons.contains(id) {
if let buttonComponentView = buttonView.view {
transition.setAlpha(view: buttonComponentView, alpha: 0.0, completion: { [weak buttonComponentView] _ in
buttonComponentView?.removeFromSuperview()
})
}
removeRightButtons.append(id)
}
}
for id in removeRightButtons {
self.rightButtonViews.removeValue(forKey: id)
}
let commonInset: CGFloat = max(leftOffset, size.width - rightOffset)
let remainingWidth = size.width - commonInset * 2.0
let titleTextSize = self.titleTextView.updateLayout(CGSize(width: remainingWidth, height: size.height))
let titleFrame = CGRect(origin: CGPoint(x: floor((size.width - titleTextSize.width) / 2.0), y: floor((size.height - titleTextSize.height) / 2.0)), size: titleTextSize)
transition.setFrame(view: self.titleTextView, frame: titleFrame)
if let titleComponent = content.titleComponent {
var titleContentTransition = transition
let titleContentView: ComponentView<Empty>
if let current = self.titleContentView {
titleContentView = current
} else {
titleContentTransition = .immediate
titleContentView = ComponentView<Empty>()
self.titleContentView = titleContentView
}
let titleContentSize = titleContentView.update(
transition: titleContentTransition,
component: titleComponent,
environment: {},
containerSize: CGSize(width: remainingWidth, height: size.height)
)
if let titleContentComponentView = titleContentView.view {
if titleContentComponentView.superview == nil {
self.titleOffsetContainer.addSubview(titleContentComponentView)
}
titleContentTransition.setFrame(view: titleContentComponentView, frame: CGRect(origin: CGPoint(x: floor((size.width - titleContentSize.width) / 2.0), y: floor((size.height - titleContentSize.height) / 2.0)), size: titleContentSize))
}
} else {
if let titleContentView = self.titleContentView {
self.titleContentView = nil
titleContentView.view?.removeFromSuperview()
}
}
if let chatListTitle = content.chatListTitle {
var chatListTitleTransition = transition
let chatListTitleView: ChatListTitleView
if let current = self.chatListTitleView {
chatListTitleView = current
} else {
chatListTitleTransition = .immediate
chatListTitleView = ChatListTitleView(context: context, theme: theme, strings: strings, animationCache: context.animationCache, animationRenderer: context.animationRenderer)
chatListTitleView.manualLayout = true
self.chatListTitleView = chatListTitleView
self.titleOffsetContainer.addSubview(chatListTitleView)
}
let chatListTitleContentSize = size
chatListTitleView.setTitle(chatListTitle, animated: false)
chatListTitleView.updateLayout(size: chatListTitleContentSize, clearBounds: CGRect(origin: CGPoint(), size: chatListTitleContentSize), transition: transition.containedViewLayoutTransition)
chatListTitleView.openStatusSetup = { [weak self] sourceView in
guard let self else {
return
}
self.openStatusSetup(sourceView)
}
chatListTitleView.toggleIsLocked = { [weak self] in
guard let self else {
return
}
self.toggleIsLocked()
}
chatListTitleTransition.setFrame(view: chatListTitleView, frame: CGRect(origin: CGPoint(x: floor((size.width - chatListTitleContentSize.width) / 2.0), y: floor((size.height - chatListTitleContentSize.height) / 2.0)), size: chatListTitleContentSize))
} else {
if let chatListTitleView = self.chatListTitleView {
self.chatListTitleView = nil
chatListTitleView.removeFromSuperview()
}
}
self.titleTextView.isHidden = self.chatListTitleView != nil || self.titleContentView != nil
}
}
public final class View: UIView, NavigationBarHeaderView {
private var component: ChatListHeaderComponent?
private weak var state: EmptyComponentState?
private var primaryContentView: ContentView?
private var secondaryContentView: ContentView?
override init(frame: CGRect) {
super.init(frame: frame)
}
required public init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func update(component: ChatListHeaderComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: Transition) -> CGSize {
self.state = state
let previousComponent = self.component
self.component = component
if let primaryContent = component.primaryContent {
var primaryContentTransition = transition
let primaryContentView: ContentView
if let current = self.primaryContentView {
primaryContentView = current
} else {
primaryContentTransition = .immediate
primaryContentView = ContentView(
backPressed: { [weak self] in
guard let self, let component = self.component else {
return
}
component.primaryContent?.backPressed?()
},
openStatusSetup: { [weak self] sourceView in
guard let self else {
return
}
self.component?.openStatusSetup(sourceView)
},
toggleIsLocked: { [weak self] in
guard let self else {
return
}
self.component?.toggleIsLocked()
}
)
self.primaryContentView = primaryContentView
self.addSubview(primaryContentView)
}
primaryContentView.update(context: component.context, theme: component.theme, strings: component.strings, content: primaryContent, backTitle: primaryContent.backTitle, sideInset: component.sideInset, size: availableSize, transition: primaryContentTransition)
primaryContentTransition.setFrame(view: primaryContentView, frame: CGRect(origin: CGPoint(), size: availableSize))
} else if let primaryContentView = self.primaryContentView {
self.primaryContentView = nil
primaryContentView.removeFromSuperview()
}
if let secondaryContent = component.secondaryContent {
var secondaryContentTransition = transition
let secondaryContentView: ContentView
if let current = self.secondaryContentView {
secondaryContentView = current
} else {
secondaryContentTransition = .immediate
secondaryContentView = ContentView(
backPressed: { [weak self] in
guard let self, let component = self.component else {
return
}
component.secondaryContent?.backPressed?()
},
openStatusSetup: { [weak self] sourceView in
guard let self else {
return
}
self.component?.openStatusSetup(sourceView)
},
toggleIsLocked: { [weak self] in
guard let self else {
return
}
self.component?.toggleIsLocked()
}
)
self.secondaryContentView = secondaryContentView
self.addSubview(secondaryContentView)
}
secondaryContentView.update(context: component.context, theme: component.theme, strings: component.strings, content: secondaryContent, backTitle: component.primaryContent?.title, sideInset: component.sideInset, size: availableSize, transition: secondaryContentTransition)
secondaryContentTransition.setFrame(view: secondaryContentView, frame: CGRect(origin: CGPoint(), size: availableSize))
if let primaryContentView = self.primaryContentView {
if let previousComponent = previousComponent, previousComponent.secondaryContent == nil {
primaryContentView.updateNavigationTransitionAsPrevious(nextView: secondaryContentView, fraction: 0.0, transition: .immediate, completion: {})
secondaryContentView.updateNavigationTransitionAsNext(previousView: primaryContentView, fraction: 0.0, transition: .immediate, completion: {})
}
primaryContentView.updateNavigationTransitionAsPrevious(nextView: secondaryContentView, fraction: component.secondaryTransition, transition: transition, completion: {})
secondaryContentView.updateNavigationTransitionAsNext(previousView: primaryContentView, fraction: component.secondaryTransition, transition: transition, completion: {})
}
} else if let secondaryContentView = self.secondaryContentView {
self.secondaryContentView = nil
if let primaryContentView = self.primaryContentView {
primaryContentView.updateNavigationTransitionAsPrevious(nextView: secondaryContentView, fraction: 0.0, transition: transition, completion: {})
secondaryContentView.updateNavigationTransitionAsNext(previousView: primaryContentView, fraction: 0.0, transition: transition, completion: { [weak secondaryContentView] in
secondaryContentView?.removeFromSuperview()
})
} else {
secondaryContentView.removeFromSuperview()
}
}
return availableSize
}
public func findTitleView() -> ChatListTitleView? {
return self.primaryContentView?.chatListTitleView
}
}
public func makeView() -> View {
return View(frame: CGRect())
}
public 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 NavigationButtonComponentEnvironment: Equatable {
public let theme: PresentationTheme
public init(theme: PresentationTheme) {
self.theme = theme
}
public static func ==(lhs: NavigationButtonComponentEnvironment, rhs: NavigationButtonComponentEnvironment) -> Bool {
if lhs.theme != rhs.theme {
return false
}
return true
}
}
public final class NavigationButtonComponent: Component {
public typealias EnvironmentType = NavigationButtonComponentEnvironment
public enum Content: Equatable {
case text(title: String, isBold: Bool)
case more
case icon(imageName: String)
case proxy(status: ChatTitleProxyStatus)
}
public let content: Content
public let pressed: (UIView) -> Void
public let contextAction: ((UIView, ContextGesture?) -> Void)?
public init(
content: Content,
pressed: @escaping (UIView) -> Void,
contextAction: ((UIView, ContextGesture?) -> Void)? = nil
) {
self.content = content
self.pressed = pressed
self.contextAction = contextAction
}
public static func ==(lhs: NavigationButtonComponent, rhs: NavigationButtonComponent) -> Bool {
if lhs.content != rhs.content {
return false
}
return true
}
public final class View: HighlightTrackingButton {
private var textView: ImmediateTextView?
private var iconView: UIImageView?
private var iconImageName: String?
private var proxyNode: ChatTitleProxyNode?
private var moreButton: MoreHeaderButton?
private var component: NavigationButtonComponent?
override init(frame: CGRect) {
super.init(frame: frame)
self.addTarget(self, action: #selector(self.pressed), for: .touchUpInside)
self.highligthedChanged = { [weak self] highlighted in
guard let self else {
return
}
if highlighted {
self.textView?.alpha = 0.6
self.proxyNode?.alpha = 0.6
self.iconView?.alpha = 0.6
} else {
self.textView?.alpha = 1.0
self.textView?.layer.animateAlpha(from: 0.6, to: 1.0, duration: 0.2)
self.proxyNode?.alpha = 1.0
self.proxyNode?.layer.animateAlpha(from: 0.6, to: 1.0, duration: 0.2)
self.iconView?.alpha = 1.0
self.iconView?.layer.animateAlpha(from: 0.6, to: 1.0, duration: 0.2)
}
}
}
required public init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
@objc private func pressed() {
self.component?.pressed(self)
}
func update(component: NavigationButtonComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<NavigationButtonComponentEnvironment>, transition: Transition) -> CGSize {
self.component = component
let theme = environment[NavigationButtonComponentEnvironment.self].value.theme
let iconOffset: CGFloat = 4.0
var textString: NSAttributedString?
var imageName: String?
var proxyStatus: ChatTitleProxyStatus?
var isMore: Bool = false
switch component.content {
case let .text(title, isBold):
textString = NSAttributedString(string: title, font: isBold ? Font.bold(17.0) : Font.regular(17.0), textColor: theme.rootController.navigationBar.accentTextColor)
case .more:
isMore = true
case let .icon(imageNameValue):
imageName = imageNameValue
case let .proxy(status):
proxyStatus = status
}
var size = CGSize(width: 0.0, height: availableSize.height)
if let textString = textString {
let textView: ImmediateTextView
if let current = self.textView {
textView = current
} else {
textView = ImmediateTextView()
textView.isUserInteractionEnabled = false
self.textView = textView
self.addSubview(textView)
}
textView.attributedText = textString
let textSize = textView.updateLayout(availableSize)
size.width = textSize.width
textView.frame = CGRect(origin: CGPoint(x: 0.0, y: floor((availableSize.height - textSize.height) / 2.0)), size: textSize)
} else if let textView = self.textView {
self.textView = nil
textView.removeFromSuperview()
}
if let imageName = imageName {
let iconView: UIImageView
if let current = self.iconView {
iconView = current
} else {
iconView = UIImageView()
iconView.isUserInteractionEnabled = false
self.iconView = iconView
self.addSubview(iconView)
}
if self.iconImageName != imageName {
self.iconImageName = imageName
iconView.image = generateTintedImage(image: UIImage(bundleImageName: imageName), color: theme.rootController.navigationBar.accentTextColor)
}
if let iconSize = iconView.image?.size {
size.width = iconSize.width
iconView.frame = CGRect(origin: CGPoint(x: iconOffset, y: floor((availableSize.height - iconSize.height) / 2.0)), size: iconSize)
}
} else if let iconView = self.iconView {
self.iconView = nil
iconView.removeFromSuperview()
self.iconImageName = nil
}
if let proxyStatus = proxyStatus {
let proxyNode: ChatTitleProxyNode
if let current = self.proxyNode {
proxyNode = current
} else {
proxyNode = ChatTitleProxyNode(theme: theme)
proxyNode.isUserInteractionEnabled = false
self.proxyNode = proxyNode
self.addSubnode(proxyNode)
}
let proxySize = CGSize(width: 30.0, height: 30.0)
size.width = proxySize.width
proxyNode.theme = theme
proxyNode.status = proxyStatus
proxyNode.frame = CGRect(origin: CGPoint(x: iconOffset, y: floor((availableSize.height - proxySize.height) / 2.0)), size: proxySize)
} else if let proxyNode = self.proxyNode {
self.proxyNode = nil
proxyNode.removeFromSupernode()
}
if isMore {
let moreButton: MoreHeaderButton
if let current = self.moreButton {
moreButton = current
} else {
moreButton = MoreHeaderButton(color: theme.rootController.navigationBar.buttonColor)
moreButton.isUserInteractionEnabled = true
moreButton.setContent(.more(MoreHeaderButton.optionsCircleImage(color: theme.rootController.navigationBar.buttonColor)))
moreButton.onPressed = { [weak self] in
guard let self, let component = self.component else {
return
}
component.pressed(self)
}
moreButton.contextAction = { [weak self] sourceNode, gesture in
guard let self, let component = self.component else {
return
}
component.contextAction?(self, gesture)
}
self.addSubnode(moreButton)
}
let buttonSize = CGSize(width: 26.0, height: 44.0)
size.width = buttonSize.width
moreButton.setContent(.more(MoreHeaderButton.optionsCircleImage(color: theme.rootController.navigationBar.buttonColor)))
moreButton.frame = CGRect(origin: CGPoint(x: iconOffset, y: floor((availableSize.height - buttonSize.height) / 2.0)), size: buttonSize)
} else if let moreButton = self.moreButton {
self.moreButton = nil
moreButton.removeFromSupernode()
}
return size
}
}
public func makeView() -> View {
return View(frame: CGRect())
}
public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment<NavigationButtonComponentEnvironment>, transition: Transition) -> CGSize {
return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition)
}
}
*/

View File

@ -0,0 +1,168 @@
import Foundation
import UIKit
import Display
import AsyncDisplayKit
import AnimationUI
public final class MoreHeaderButton: HighlightableButtonNode {
public enum Content {
case image(UIImage?)
case more(UIImage?)
}
public let referenceNode: ContextReferenceContentNode
public let containerNode: ContextControllerSourceNode
private let iconNode: ASImageNode
private var animationNode: AnimationNode?
public var contextAction: ((ASDisplayNode, ContextGesture?) -> Void)?
private var color: UIColor
public var onPressed: (() -> Void)?
public init(color: UIColor) {
self.color = color
self.referenceNode = ContextReferenceContentNode()
self.containerNode = ContextControllerSourceNode()
self.containerNode.animateScale = false
self.iconNode = ASImageNode()
self.iconNode.displaysAsynchronously = false
self.iconNode.displayWithoutProcessing = true
self.iconNode.contentMode = .scaleToFill
super.init()
self.containerNode.addSubnode(self.referenceNode)
self.referenceNode.addSubnode(self.iconNode)
self.addSubnode(self.containerNode)
self.containerNode.shouldBegin = { [weak self] location in
guard let strongSelf = self, let _ = strongSelf.contextAction else {
return false
}
return true
}
self.containerNode.activated = { [weak self] gesture, _ in
guard let strongSelf = self else {
return
}
strongSelf.contextAction?(strongSelf.containerNode, gesture)
}
self.containerNode.frame = CGRect(origin: CGPoint(), size: CGSize(width: 26.0, height: 44.0))
self.referenceNode.frame = self.containerNode.bounds
self.iconNode.image = MoreHeaderButton.optionsCircleImage(color: color)
if let image = self.iconNode.image {
self.iconNode.frame = CGRect(origin: CGPoint(x: floor((self.containerNode.bounds.width - image.size.width) / 2.0), y: floor((self.containerNode.bounds.height - image.size.height) / 2.0)), size: image.size)
}
self.hitTestSlop = UIEdgeInsets(top: 0.0, left: -4.0, bottom: 0.0, right: -4.0)
self.addTarget(self, action: #selector(self.pressed), forControlEvents: .touchUpInside)
}
@objc private func pressed() {
self.onPressed?()
}
private var content: Content?
public func setContent(_ content: Content, animated: Bool = false) {
if case .more = content, self.animationNode == nil {
let iconColor = self.color
let animationNode = AnimationNode(animation: "anim_profilemore", colors: ["Point 2.Group 1.Fill 1": iconColor,
"Point 3.Group 1.Fill 1": iconColor,
"Point 1.Group 1.Fill 1": iconColor], scale: 1.0)
let animationSize = CGSize(width: 22.0, height: 22.0)
animationNode.frame = CGRect(origin: CGPoint(x: floor((self.containerNode.bounds.width - animationSize.width) / 2.0), y: floor((self.containerNode.bounds.height - animationSize.height) / 2.0)), size: animationSize)
self.addSubnode(animationNode)
self.animationNode = animationNode
}
if animated {
if let snapshotView = self.referenceNode.view.snapshotContentTree() {
snapshotView.frame = self.referenceNode.frame
self.view.addSubview(snapshotView)
snapshotView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3, removeOnCompletion: false, completion: { [weak snapshotView] _ in
snapshotView?.removeFromSuperview()
})
snapshotView.layer.animateScale(from: 1.0, to: 0.1, duration: 0.3, removeOnCompletion: false)
self.iconNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.3)
self.iconNode.layer.animateScale(from: 0.1, to: 1.0, duration: 0.3)
self.animationNode?.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.3)
self.animationNode?.layer.animateScale(from: 0.1, to: 1.0, duration: 0.3)
}
switch content {
case let .image(image):
if let image = image {
self.iconNode.frame = CGRect(origin: CGPoint(x: floor((self.containerNode.bounds.width - image.size.width) / 2.0), y: floor((self.containerNode.bounds.height - image.size.height) / 2.0)), size: image.size)
}
self.iconNode.image = image
self.iconNode.isHidden = false
self.animationNode?.isHidden = true
case let .more(image):
if let image = image {
self.iconNode.frame = CGRect(origin: CGPoint(x: floor((self.containerNode.bounds.width - image.size.width) / 2.0), y: floor((self.containerNode.bounds.height - image.size.height) / 2.0)), size: image.size)
}
self.iconNode.image = image
self.iconNode.isHidden = false
self.animationNode?.isHidden = false
}
} else {
self.content = content
switch content {
case let .image(image):
if let image = image {
self.iconNode.frame = CGRect(origin: CGPoint(x: floor((self.containerNode.bounds.width - image.size.width) / 2.0), y: floor((self.containerNode.bounds.height - image.size.height) / 2.0)), size: image.size)
}
self.iconNode.image = image
self.iconNode.isHidden = false
self.animationNode?.isHidden = true
case let .more(image):
if let image = image {
self.iconNode.frame = CGRect(origin: CGPoint(x: floor((self.containerNode.bounds.width - image.size.width) / 2.0), y: floor((self.containerNode.bounds.height - image.size.height) / 2.0)), size: image.size)
}
self.iconNode.image = image
self.iconNode.isHidden = false
self.animationNode?.isHidden = false
}
}
}
override public func didLoad() {
super.didLoad()
self.view.isOpaque = false
}
override public func calculateSizeThatFits(_ constrainedSize: CGSize) -> CGSize {
return CGSize(width: 22.0, height: 44.0)
}
public func onLayout() {
}
public func play() {
self.animationNode?.playOnce()
}
public static func optionsCircleImage(color: UIColor) -> UIImage? {
return generateImage(CGSize(width: 22.0, height: 22.0), contextGenerator: { size, context in
context.clear(CGRect(origin: CGPoint(), size: size))
context.setStrokeColor(color.cgColor)
let lineWidth: CGFloat = 1.3
context.setLineWidth(lineWidth)
context.strokeEllipse(in: CGRect(origin: CGPoint(), size: size).insetBy(dx: lineWidth, dy: lineWidth))
})
}
}

View File

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

View File

@ -6,7 +6,7 @@ import TelegramPresentationData
import ActivityIndicator
import AppBundle
enum ChatTitleProxyStatus {
public enum ChatTitleProxyStatus {
case connecting
case connected
case available
@ -35,11 +35,11 @@ private func generateIcon(color: UIColor, connected: Bool, off: Bool) -> UIImage
})
}
final class ChatTitleProxyNode: ASDisplayNode {
public final class ChatTitleProxyNode: ASDisplayNode {
private let iconNode: ASImageNode
private let activityIndicator: ActivityIndicator
var theme: PresentationTheme {
public var theme: PresentationTheme {
didSet {
if self.theme !== oldValue {
switch self.status {
@ -55,7 +55,7 @@ final class ChatTitleProxyNode: ASDisplayNode {
}
}
var status: ChatTitleProxyStatus = .connected {
public var status: ChatTitleProxyStatus = .connected {
didSet {
if self.status != oldValue {
switch self.status {
@ -73,7 +73,7 @@ final class ChatTitleProxyNode: ASDisplayNode {
}
}
init(theme: PresentationTheme) {
public init(theme: PresentationTheme) {
self.theme = theme
self.iconNode = ASImageNode()

View File

@ -14,24 +14,42 @@ import AccountContext
private let titleFont = Font.with(size: 17.0, design: .regular, weight: .semibold, traits: [.monospacedNumbers])
struct NetworkStatusTitle: Equatable {
enum Status: Equatable {
public struct NetworkStatusTitle: Equatable {
public enum Status: Equatable {
case premium
case emoji(PeerEmojiStatus)
}
let text: String
let activity: Bool
let hasProxy: Bool
let connectsViaProxy: Bool
let isPasscodeSet: Bool
let isManuallyLocked: Bool
let peerStatus: Status?
public var text: String
public var activity: Bool
public var hasProxy: Bool
public var connectsViaProxy: Bool
public var isPasscodeSet: Bool
public var isManuallyLocked: Bool
public var peerStatus: Status?
public init(
text: String,
activity: Bool,
hasProxy: Bool,
connectsViaProxy: Bool,
isPasscodeSet: Bool,
isManuallyLocked: Bool,
peerStatus: Status?
) {
self.text = text
self.activity = activity
self.hasProxy = hasProxy
self.connectsViaProxy = connectsViaProxy
self.isPasscodeSet = isPasscodeSet
self.isManuallyLocked = isManuallyLocked
self.peerStatus = peerStatus
}
}
final class ChatListTitleView: UIView, NavigationBarTitleView, NavigationBarTitleTransitionNode {
public final class ChatListTitleView: UIView, NavigationBarTitleView, NavigationBarTitleTransitionNode {
private let context: AccountContext
private let titleNode: ImmediateTextNode
public let titleNode: ImmediateTextNode
private let lockView: ChatListTitleLockView
private weak var lockSnapshotView: UIView?
private let activityIndicator: ActivityIndicator
@ -42,12 +60,14 @@ final class ChatListTitleView: UIView, NavigationBarTitleView, NavigationBarTitl
private let animationCache: AnimationCache
private let animationRenderer: MultiAnimationRenderer
var openStatusSetup: ((UIView) -> Void)?
public var openStatusSetup: ((UIView) -> Void)?
private var validLayout: (CGSize, CGRect)?
public var manualLayout: Bool = false
private var _title: NetworkStatusTitle = NetworkStatusTitle(text: "", activity: false, hasProxy: false, connectsViaProxy: false, isPasscodeSet: false, isManuallyLocked: false, peerStatus: nil)
var title: NetworkStatusTitle {
public var title: NetworkStatusTitle {
get {
return self._title
}
@ -56,7 +76,7 @@ final class ChatListTitleView: UIView, NavigationBarTitleView, NavigationBarTitl
}
}
func setTitle(_ title: NetworkStatusTitle, animated: Bool) {
public func setTitle(_ title: NetworkStatusTitle, animated: Bool) {
let oldValue = self._title
self._title = title
@ -170,17 +190,19 @@ final class ChatListTitleView: UIView, NavigationBarTitleView, NavigationBarTitl
}
}
self.setNeedsLayout()
if !self.manualLayout {
self.setNeedsLayout()
}
}
}
var toggleIsLocked: (() -> Void)?
var openProxySettings: (() -> Void)?
public var toggleIsLocked: (() -> Void)?
public var openProxySettings: (() -> Void)?
private var isPasscodeSet = false
private var isManuallyLocked = false
var theme: PresentationTheme {
public var theme: PresentationTheme {
didSet {
self.titleNode.attributedText = NSAttributedString(string: self.title.text, font: titleFont, textColor: self.theme.rootController.navigationBar.primaryTextColor)
@ -191,13 +213,13 @@ final class ChatListTitleView: UIView, NavigationBarTitleView, NavigationBarTitl
}
}
var strings: PresentationStrings {
public var strings: PresentationStrings {
didSet {
self.proxyButton.accessibilityLabel = self.strings.VoiceOver_Navigation_ProxySettings
}
}
init(context: AccountContext, theme: PresentationTheme, strings: PresentationStrings, animationCache: AnimationCache, animationRenderer: MultiAnimationRenderer) {
public init(context: AccountContext, theme: PresentationTheme, strings: PresentationStrings, animationCache: AnimationCache, animationRenderer: MultiAnimationRenderer) {
self.context = context
self.theme = theme
self.strings = strings
@ -283,19 +305,19 @@ final class ChatListTitleView: UIView, NavigationBarTitleView, NavigationBarTitl
self.proxyButton.addTarget(self, action: #selector(self.proxyButtonPressed), for: .touchUpInside)
}
required init?(coder aDecoder: NSCoder) {
required public init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func layoutSubviews() {
override public func layoutSubviews() {
super.layoutSubviews()
if let (size, clearBounds) = self.validLayout {
if !self.manualLayout, let (size, clearBounds) = self.validLayout {
self.updateLayout(size: size, clearBounds: clearBounds, transition: .immediate)
}
}
func updateLayout(size: CGSize, clearBounds: CGRect, transition: ContainedViewLayoutTransition) {
public func updateLayout(size: CGSize, clearBounds: CGRect, transition: ContainedViewLayoutTransition) {
self.validLayout = (size, clearBounds)
var indicatorPadding: CGFloat = 0.0
@ -409,7 +431,7 @@ final class ChatListTitleView: UIView, NavigationBarTitleView, NavigationBarTitl
self.openProxySettings?()
}
func makeTransitionMirrorNode() -> ASDisplayNode {
public func makeTransitionMirrorNode() -> ASDisplayNode {
let snapshotView = self.snapshotView(afterScreenUpdates: false)
return ASDisplayNode(viewBlock: {
@ -417,17 +439,17 @@ final class ChatListTitleView: UIView, NavigationBarTitleView, NavigationBarTitl
}, didLoad: nil)
}
func animateLayoutTransition() {
public func animateLayoutTransition() {
}
var proxyButtonFrame: CGRect? {
public var proxyButtonFrame: CGRect? {
if !self.proxyNode.isHidden {
return proxyNode.frame
}
return nil
}
var lockViewFrame: CGRect? {
public var lockViewFrame: CGRect? {
if !self.lockView.isHidden && !self.lockView.frame.isEmpty {
return self.lockView.frame
} else {
@ -435,7 +457,7 @@ final class ChatListTitleView: UIView, NavigationBarTitleView, NavigationBarTitl
}
}
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
override public func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
if let titleCredibilityIconView = self.titleCredibilityIconView, !titleCredibilityIconView.isHidden, titleCredibilityIconView.alpha != 0.0 {
if titleCredibilityIconView.bounds.insetBy(dx: -8.0, dy: -8.0).contains(self.convert(point, to: titleCredibilityIconView)) {
if let result = titleCredibilityIconView.hitTest(titleCredibilityIconView.bounds.center, with: event) {

View File

@ -30,6 +30,7 @@ swift_library(
"//submodules/TelegramUI/Components/EmojiStatusComponent",
"//submodules/TelegramUI/Components/AnimationCache:AnimationCache",
"//submodules/TelegramUI/Components/MultiAnimationRenderer:MultiAnimationRenderer",
"//submodules/Components/ComponentDisplayAdapters:ComponentDisplayAdapters",
],
visibility = [
"//visibility:public",

View File

@ -21,11 +21,12 @@ import ComponentFlow
import EmojiStatusComponent
import AnimationCache
import MultiAnimationRenderer
import ComponentDisplayAdapters
private let titleFont = Font.with(size: 17.0, design: .regular, weight: .semibold, traits: [.monospacedNumbers])
private let subtitleFont = Font.regular(13.0)
public enum ChatTitleContent {
public enum ChatTitleContent: Equatable {
public enum ReplyThreadType {
case comments
case replies
@ -34,6 +35,48 @@ public enum ChatTitleContent {
case peer(peerView: PeerView, customTitle: String?, onlineMemberCount: Int32?, isScheduledMessages: Bool, isMuted: Bool?, customMessageCount: Int?)
case replyThread(type: ReplyThreadType, count: Int)
case custom(String, String?, Bool)
public static func ==(lhs: ChatTitleContent, rhs: ChatTitleContent) -> Bool {
switch lhs {
case let .peer(peerView, customTitle, onlineMemberCount, isScheduledMessages, isMuted, customMessageCount):
if case let .peer(rhsPeerView, rhsCustomTitle, rhsOnlineMemberCount, rhsIsScheduledMessages, rhsIsMuted, rhsCustomMessageCount) = rhs {
if peerView !== rhsPeerView {
return false
}
if customTitle != rhsCustomTitle {
return false
}
if onlineMemberCount != rhsOnlineMemberCount {
return false
}
if isScheduledMessages != rhsIsScheduledMessages {
return false
}
if isMuted != rhsIsMuted {
return false
}
if customMessageCount != rhsCustomMessageCount {
return false
}
return true
} else {
return false
}
case let .replyThread(type, count):
if case .replyThread(type, count) = rhs {
return true
} else {
return false
}
case let .custom(title, status, active):
if case .custom(title, status, active) = rhs {
return true
} else {
return false
}
}
}
}
private enum ChatTitleIcon {
@ -72,6 +115,7 @@ public final class ChatTitleView: UIView, NavigationBarTitleView {
private let button: HighlightTrackingButtonNode
var manualLayout: Bool = false
private var validLayout: (CGSize, CGRect)?
private var titleLeftIcon: ChatTitleIcon = .none
@ -89,7 +133,9 @@ public final class ChatTitleView: UIView, NavigationBarTitleView {
}
private func updateNetworkStatusNode(networkState: AccountNetworkState, layout: ContainerViewLayout?) {
self.setNeedsLayout()
if self.manualLayout {
self.setNeedsLayout()
}
}
public var networkState: AccountNetworkState = .online(proxy: nil) {
@ -306,7 +352,7 @@ public final class ChatTitleView: UIView, NavigationBarTitleView {
self.button.isUserInteractionEnabled = isEnabled
if !self.updateStatus() {
if updated {
if let (size, clearBounds) = self.validLayout {
if !self.manualLayout, let (size, clearBounds) = self.validLayout {
self.updateLayout(size: size, clearBounds: clearBounds, transition: .animated(duration: 0.2, curve: .easeInOut))
}
}
@ -559,7 +605,7 @@ public final class ChatTitleView: UIView, NavigationBarTitleView {
}
if self.activityNode.transitionToState(state, animation: .slide) {
if let (size, clearBounds) = self.validLayout {
if !self.manualLayout, let (size, clearBounds) = self.validLayout {
self.updateLayout(size: size, clearBounds: clearBounds, transition: .animated(duration: 0.3, curve: .spring))
}
return true
@ -642,7 +688,7 @@ public final class ChatTitleView: UIView, NavigationBarTitleView {
override public func layoutSubviews() {
super.layoutSubviews()
if let (size, clearBounds) = self.validLayout {
if !self.manualLayout, let (size, clearBounds) = self.validLayout {
self.updateLayout(size: size, clearBounds: clearBounds, transition: .immediate)
}
}
@ -657,7 +703,7 @@ public final class ChatTitleView: UIView, NavigationBarTitleView {
self.titleContent = titleContent
let _ = self.updateStatus()
if let (size, clearBounds) = self.validLayout {
if !self.manualLayout, let (size, clearBounds) = self.validLayout {
self.updateLayout(size: size, clearBounds: clearBounds, transition: .immediate)
}
}
@ -861,3 +907,121 @@ public final class ChatTitleView: UIView, NavigationBarTitleView {
snapshotView.layer.animatePosition(from: CGPoint(), to: CGPoint(x: 0.0, y: -20.0), duration: 0.5, timingFunction: kCAMediaTimingFunctionSpring, removeOnCompletion: false, additive: true)
}
}
public final class ChatTitleComponent: Component {
public let context: AccountContext
public let theme: PresentationTheme
public let strings: PresentationStrings
public let dateTimeFormat: PresentationDateTimeFormat
public let nameDisplayOrder: PresentationPersonNameOrder
public let content: ChatTitleContent
public let tapped: () -> Void
public let longTapped: () -> Void
public init(
context: AccountContext,
theme: PresentationTheme,
strings: PresentationStrings,
dateTimeFormat: PresentationDateTimeFormat,
nameDisplayOrder: PresentationPersonNameOrder,
content: ChatTitleContent,
tapped: @escaping () -> Void,
longTapped: @escaping () -> Void
) {
self.context = context
self.theme = theme
self.strings = strings
self.dateTimeFormat = dateTimeFormat
self.nameDisplayOrder = nameDisplayOrder
self.content = content
self.tapped = tapped
self.longTapped = longTapped
}
public static func ==(lhs: ChatTitleComponent, rhs: ChatTitleComponent) -> Bool {
if lhs.context !== rhs.context {
return false
}
if lhs.theme !== rhs.theme {
return false
}
if lhs.strings !== rhs.strings {
return false
}
if lhs.dateTimeFormat != rhs.dateTimeFormat {
return false
}
if lhs.nameDisplayOrder != rhs.nameDisplayOrder {
return false
}
if lhs.content != rhs.content {
return false
}
return true
}
public final class View: UIView {
private var contentView: ChatTitleView?
private var component: ChatTitleComponent?
override init(frame: CGRect) {
super.init(frame: frame)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func update(component: ChatTitleComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: Transition) -> CGSize {
self.component = component
let contentView: ChatTitleView
if let current = self.contentView {
contentView = current
} else {
contentView = ChatTitleView(
context: component.context,
theme: component.theme,
strings: component.strings,
dateTimeFormat: component.dateTimeFormat,
nameDisplayOrder: component.nameDisplayOrder,
animationCache: component.context.animationCache,
animationRenderer: component.context.animationRenderer
)
contentView.pressed = { [weak self] in
guard let self else {
return
}
self.component?.tapped()
}
contentView.longPressed = { [weak self] in
guard let self else {
return
}
self.component?.longTapped()
}
contentView.manualLayout = true
self.contentView = contentView
self.addSubview(contentView)
}
if contentView.titleContent != component.content {
contentView.titleContent = component.content
}
contentView.updateLayout(size: availableSize, clearBounds: CGRect(origin: CGPoint(), size: availableSize), transition: transition.containedViewLayoutTransition)
transition.setFrame(view: contentView, frame: CGRect(origin: CGPoint(), size: availableSize))
return availableSize
}
}
public func makeView() -> View {
return View(frame: CGRect())
}
public 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)
}
}

View File

@ -85,6 +85,7 @@ import ChatTitleView
import EmojiStatusComponent
import ChatTimerScreen
import MediaPasteboardUI
import ChatListHeaderComponent
#if DEBUG
import os.signpost