mirror of
https://github.com/Swiftgram/Telegram-iOS.git
synced 2025-06-16 05:55:20 +00:00
[WIP] Inline forums
This commit is contained in:
parent
f9f194f04c
commit
6a708ac1c2
@ -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",
|
||||
|
@ -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
@ -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
|
||||
|
@ -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: {
|
||||
|
@ -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)
|
||||
|
@ -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 {
|
||||
|
@ -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))
|
||||
}
|
||||
|
||||
|
@ -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)
|
||||
|
||||
|
@ -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)
|
||||
|
@ -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": [],
|
||||
|
@ -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",
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
*/
|
||||
|
@ -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))
|
||||
})
|
||||
}
|
||||
}
|
30
submodules/TelegramUI/Components/ChatListTitleView/BUILD
Normal file
30
submodules/TelegramUI/Components/ChatListTitleView/BUILD
Normal 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",
|
||||
],
|
||||
)
|
@ -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()
|
@ -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) {
|
@ -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",
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
@ -85,6 +85,7 @@ import ChatTitleView
|
||||
import EmojiStatusComponent
|
||||
import ChatTimerScreen
|
||||
import MediaPasteboardUI
|
||||
import ChatListHeaderComponent
|
||||
|
||||
#if DEBUG
|
||||
import os.signpost
|
||||
|
Loading…
x
Reference in New Issue
Block a user