mirror of
https://github.com/Swiftgram/Telegram-iOS.git
synced 2025-11-07 09:20:08 +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/TelegramUI/Components/NotificationPeerExceptionController",
|
||||||
"//submodules/AnimationUI:AnimationUI",
|
"//submodules/AnimationUI:AnimationUI",
|
||||||
"//submodules/PeerInfoUI",
|
"//submodules/PeerInfoUI",
|
||||||
"//submodules/TelegramUI/Components/ChatListHeaderComponent:ChatListHeaderComponent",
|
"//submodules/TelegramUI/Components/ChatListHeaderComponent",
|
||||||
|
"//submodules/TelegramUI/Components/ChatListTitleView",
|
||||||
],
|
],
|
||||||
visibility = [
|
visibility = [
|
||||||
"//visibility:public",
|
"//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 presentationData = context.sharedContext.currentPresentationData.with({ $0 })
|
||||||
let strings = presentationData.strings
|
let strings = presentationData.strings
|
||||||
|
|
||||||
@ -763,11 +763,13 @@ func chatForumTopicMenuItems(context: AccountContext, peerId: PeerId, threadId:
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if canSelect {
|
||||||
items.append(.separator)
|
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
|
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)
|
f(.default)
|
||||||
chatListController?.selectPeerThread(peerId: peerId, threadId: threadId)
|
chatListController?.selectPeerThread(peerId: peerId, threadId: threadId)
|
||||||
})))
|
})))
|
||||||
|
}
|
||||||
|
|
||||||
return .single(items)
|
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 {
|
guard let strongSelf = self, strongSelf.inlineStackContainerNode != nil else {
|
||||||
return []
|
return []
|
||||||
}
|
}
|
||||||
let directions: InteractiveTransitionGestureRecognizerDirections = [.leftCenter, .rightCenter]
|
let directions: InteractiveTransitionGestureRecognizerDirections = [.rightCenter]
|
||||||
return directions
|
return directions
|
||||||
}, edgeWidth: .widthMultiplier(factor: 1.0 / 6.0, min: 22.0, max: 80.0))
|
}, edgeWidth: .widthMultiplier(factor: 1.0 / 6.0, min: 22.0, max: 80.0))
|
||||||
inlineContentPanRecognizer.delegate = self
|
inlineContentPanRecognizer.delegate = self
|
||||||
@ -1345,7 +1345,7 @@ final class ChatListControllerNode: ASDisplayNode, UIGestureRecognizerDelegate {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if let directionIsToRight = directionIsToRight, directionIsToRight {
|
if let directionIsToRight = directionIsToRight, directionIsToRight {
|
||||||
self.setInlineChatList(location: nil)
|
self.controller?.setInlineChatList(location: nil)
|
||||||
} else {
|
} else {
|
||||||
self.inlineStackContainerTransitionFraction = 1.0
|
self.inlineStackContainerTransitionFraction = 1.0
|
||||||
self.controller?.requestLayout(transition: .animated(duration: 0.4, curve: .spring))
|
self.controller?.requestLayout(transition: .animated(duration: 0.4, curve: .spring))
|
||||||
@ -1482,12 +1482,14 @@ final class ChatListControllerNode: ASDisplayNode, UIGestureRecognizerDelegate {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let effectiveLocation = self.inlineStackContainerNode?.location ?? self.location
|
||||||
|
|
||||||
var filter: ChatListNodePeersFilter = []
|
var filter: ChatListNodePeersFilter = []
|
||||||
if case .forum = self.location {
|
if case .forum = effectiveLocation {
|
||||||
filter.insert(.excludeRecent)
|
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)
|
self?.requestOpenPeerFromSearch?(peer, threadId, dismissSearch)
|
||||||
}, openDisabledPeer: { _, _ in
|
}, openDisabledPeer: { _, _ in
|
||||||
}, openRecentPeerOptions: { [weak self] peer 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))
|
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):
|
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: {
|
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 {
|
final class ChatListBadgeNode: ASDisplayNode {
|
||||||
private let backgroundNode: ASImageNode
|
let backgroundNode: ASImageNode
|
||||||
private let textNode: TextNode
|
let textNode: TextNode
|
||||||
private let measureTextNode: TextNode
|
private let measureTextNode: TextNode
|
||||||
|
|
||||||
private var text: String?
|
private var text: String?
|
||||||
@ -43,6 +43,8 @@ final class ChatListBadgeNode: ASDisplayNode {
|
|||||||
|
|
||||||
private var isHiddenInternal = false
|
private var isHiddenInternal = false
|
||||||
|
|
||||||
|
var disableBounce: Bool = false
|
||||||
|
|
||||||
override init() {
|
override init() {
|
||||||
self.backgroundNode = ASImageNode()
|
self.backgroundNode = ASImageNode()
|
||||||
self.backgroundNode.isLayerBacked = true
|
self.backgroundNode.isLayerBacked = true
|
||||||
@ -97,7 +99,7 @@ final class ChatListBadgeNode: ASDisplayNode {
|
|||||||
}
|
}
|
||||||
|
|
||||||
let badgeWidth = max(imageWidth, badgeWidth)
|
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
|
var animateTextNode = false
|
||||||
if animated {
|
if animated {
|
||||||
@ -116,6 +118,7 @@ final class ChatListBadgeNode: ASDisplayNode {
|
|||||||
|
|
||||||
if currentIsEmpty && !nextIsEmpty {
|
if currentIsEmpty && !nextIsEmpty {
|
||||||
strongSelf.isHiddenInternal = false
|
strongSelf.isHiddenInternal = false
|
||||||
|
if !strongSelf.disableBounce {
|
||||||
if bounce {
|
if bounce {
|
||||||
strongSelf.layer.animateScale(from: 0.0001, to: 1.2, duration: 0.2, removeOnCompletion: false, completion: { [weak self] finished in
|
strongSelf.layer.animateScale(from: 0.0001, to: 1.2, duration: 0.2, removeOnCompletion: false, completion: { [weak self] finished in
|
||||||
if let strongSelf = self {
|
if let strongSelf = self {
|
||||||
@ -125,6 +128,7 @@ final class ChatListBadgeNode: ASDisplayNode {
|
|||||||
} else {
|
} else {
|
||||||
strongSelf.layer.animateScale(from: 0.0001, to: 1.0, duration: 0.2, removeOnCompletion: false)
|
strongSelf.layer.animateScale(from: 0.0001, to: 1.0, duration: 0.2, removeOnCompletion: false)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
} else if !currentIsEmpty && !nextIsEmpty && currentContent?.text != content.text {
|
} else if !currentIsEmpty && !nextIsEmpty && currentContent?.text != content.text {
|
||||||
var animateScale = bounce
|
var animateScale = bounce
|
||||||
strongSelf.isHiddenInternal = false
|
strongSelf.isHiddenInternal = false
|
||||||
@ -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
|
strongSelf.layer.animateScale(from: 1.0, to: 1.2, duration: 0.12, removeOnCompletion: false, completion: { [weak self] finished in
|
||||||
if let strongSelf = self {
|
if let strongSelf = self {
|
||||||
strongSelf.layer.animateScale(from: 1.2, to: 1.0, duration: 0.12, removeOnCompletion: false)
|
strongSelf.layer.animateScale(from: 1.2, to: 1.0, duration: 0.12, removeOnCompletion: false)
|
||||||
@ -157,12 +161,16 @@ final class ChatListBadgeNode: ASDisplayNode {
|
|||||||
animateTextNode = true
|
animateTextNode = true
|
||||||
} else if !currentIsEmpty && nextIsEmpty && !strongSelf.isHiddenInternal {
|
} else if !currentIsEmpty && nextIsEmpty && !strongSelf.isHiddenInternal {
|
||||||
strongSelf.isHiddenInternal = true
|
strongSelf.isHiddenInternal = true
|
||||||
|
if !strongSelf.disableBounce {
|
||||||
strongSelf.layer.animateScale(from: 1.0, to: 0.0001, duration: 0.12, removeOnCompletion: false, completion: { [weak self] finished in
|
strongSelf.layer.animateScale(from: 1.0, to: 0.0001, duration: 0.12, removeOnCompletion: false, completion: { [weak self] finished in
|
||||||
if let strongSelf = self {
|
if let strongSelf = self {
|
||||||
strongSelf.isHidden = true
|
strongSelf.isHidden = true
|
||||||
strongSelf.layer.removeAnimation(forKey: "transform.scale")
|
strongSelf.layer.removeAnimation(forKey: "transform.scale")
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
} else {
|
||||||
|
strongSelf.isHidden = true
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
if case .none = content {
|
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)
|
let backgroundFrame = CGRect(x: 0.0, y: 0.0, width: badgeWidth, height: strongSelf.backgroundNode.image?.size.height ?? 0.0)
|
||||||
if let (textLayout, _) = textLayoutAndApply {
|
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)
|
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.frame = badgeTextFrame
|
strongSelf.textNode.position = badgeTextFrame.center
|
||||||
|
strongSelf.textNode.bounds = CGRect(origin: CGPoint(), size: badgeTextFrame.size)
|
||||||
if animateTextNode {
|
if animateTextNode {
|
||||||
strongSelf.textNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.15)
|
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.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 {
|
if animated && badgeWidth != previousBadgeWidth {
|
||||||
let previousBackgroundFrame = CGRect(x: 0.0, y: 0.0, width: previousBadgeWidth, height: backgroundFrame.height)
|
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 statusNode: ChatListStatusNode
|
||||||
let badgeNode: ChatListBadgeNode
|
let badgeNode: ChatListBadgeNode
|
||||||
let mentionBadgeNode: ChatListBadgeNode
|
let mentionBadgeNode: ChatListBadgeNode
|
||||||
|
var avatarBadgeNode: ChatListBadgeNode?
|
||||||
|
var avatarBadgeBackground: ASImageNode?
|
||||||
let onlineNode: PeerOnlineMarkerNode
|
let onlineNode: PeerOnlineMarkerNode
|
||||||
let pinnedIconNode: ASImageNode
|
let pinnedIconNode: ASImageNode
|
||||||
var secretIconNode: ASImageNode?
|
var secretIconNode: ASImageNode?
|
||||||
@ -1115,9 +1117,14 @@ class ChatListItemNode: ItemListRevealOptionsItemNode {
|
|||||||
})
|
})
|
||||||
|
|
||||||
self.contextContainer.shouldBegin = { [weak self] location in
|
self.contextContainer.shouldBegin = { [weak self] location in
|
||||||
guard let strongSelf = self else {
|
guard let strongSelf = self, let item = strongSelf.item else {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if item.interaction.inlineNavigationLocation != nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
if let value = strongSelf.hitTest(location, with: nil), value === strongSelf.compoundTextButtonNode?.view {
|
if let value = strongSelf.hitTest(location, with: nil), value === strongSelf.compoundTextButtonNode?.view {
|
||||||
strongSelf.contextContainer.targetNodeForActivationProgress = strongSelf.compoundHighlightingNode
|
strongSelf.contextContainer.targetNodeForActivationProgress = strongSelf.compoundHighlightingNode
|
||||||
} else {
|
} else {
|
||||||
@ -1373,6 +1380,7 @@ class ChatListItemNode: ItemListRevealOptionsItemNode {
|
|||||||
let textFont = Font.regular(floor(item.presentationData.fontSize.itemListBaseFontSize * 15.0 / 17.0))
|
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 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 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
|
let account = item.context.account
|
||||||
var messages: [EngineMessage]
|
var messages: [EngineMessage]
|
||||||
@ -1497,6 +1505,7 @@ class ChatListItemNode: ItemListRevealOptionsItemNode {
|
|||||||
var statusState = ChatListStatusNodeState.none
|
var statusState = ChatListStatusNodeState.none
|
||||||
|
|
||||||
var currentBadgeBackgroundImage: UIImage?
|
var currentBadgeBackgroundImage: UIImage?
|
||||||
|
var currentAvatarBadgeBackgroundImage: UIImage?
|
||||||
var currentMentionBadgeImage: UIImage?
|
var currentMentionBadgeImage: UIImage?
|
||||||
var currentPinnedIconImage: UIImage?
|
var currentPinnedIconImage: UIImage?
|
||||||
var currentMutedIconImage: UIImage?
|
var currentMutedIconImage: UIImage?
|
||||||
@ -1547,6 +1556,9 @@ class ChatListItemNode: ItemListRevealOptionsItemNode {
|
|||||||
}
|
}
|
||||||
|
|
||||||
let badgeDiameter = floor(item.presentationData.fontSize.baseDisplaySize * 20.0 / 17.0)
|
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
|
let leftInset: CGFloat = params.leftInset + avatarLeftInset
|
||||||
|
|
||||||
@ -1935,17 +1947,21 @@ class ChatListItemNode: ItemListRevealOptionsItemNode {
|
|||||||
if unreadCount.isProvisonal {
|
if unreadCount.isProvisonal {
|
||||||
badgeTextColor = theme.unreadBadgeInactiveBackgroundColor
|
badgeTextColor = theme.unreadBadgeInactiveBackgroundColor
|
||||||
currentBadgeBackgroundImage = PresentationResourcesChatList.badgeBackgroundInactiveProvisional(item.presentationData.theme, diameter: badgeDiameter)
|
currentBadgeBackgroundImage = PresentationResourcesChatList.badgeBackgroundInactiveProvisional(item.presentationData.theme, diameter: badgeDiameter)
|
||||||
|
currentAvatarBadgeBackgroundImage = PresentationResourcesChatList.badgeBackgroundInactiveProvisional(item.presentationData.theme, diameter: avatarBadgeDiameter)
|
||||||
} else {
|
} else {
|
||||||
badgeTextColor = theme.unreadBadgeInactiveTextColor
|
badgeTextColor = theme.unreadBadgeInactiveTextColor
|
||||||
currentBadgeBackgroundImage = PresentationResourcesChatList.badgeBackgroundInactive(item.presentationData.theme, diameter: badgeDiameter)
|
currentBadgeBackgroundImage = PresentationResourcesChatList.badgeBackgroundInactive(item.presentationData.theme, diameter: badgeDiameter)
|
||||||
|
currentAvatarBadgeBackgroundImage = PresentationResourcesChatList.badgeBackgroundInactive(item.presentationData.theme, diameter: avatarBadgeDiameter)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
if unreadCount.isProvisonal {
|
if unreadCount.isProvisonal {
|
||||||
badgeTextColor = theme.unreadBadgeActiveBackgroundColor
|
badgeTextColor = theme.unreadBadgeActiveBackgroundColor
|
||||||
currentBadgeBackgroundImage = PresentationResourcesChatList.badgeBackgroundActiveProvisional(item.presentationData.theme, diameter: badgeDiameter)
|
currentBadgeBackgroundImage = PresentationResourcesChatList.badgeBackgroundActiveProvisional(item.presentationData.theme, diameter: badgeDiameter)
|
||||||
|
currentAvatarBadgeBackgroundImage = PresentationResourcesChatList.badgeBackgroundActiveProvisional(item.presentationData.theme, diameter: avatarBadgeDiameter)
|
||||||
} else {
|
} else {
|
||||||
badgeTextColor = theme.unreadBadgeActiveTextColor
|
badgeTextColor = theme.unreadBadgeActiveTextColor
|
||||||
currentBadgeBackgroundImage = PresentationResourcesChatList.badgeBackgroundActive(item.presentationData.theme, diameter: badgeDiameter)
|
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)
|
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 {
|
if let threadInfo = threadInfo {
|
||||||
let avatarIconView: ComponentHostView<Empty>
|
let avatarIconView: ComponentHostView<Empty>
|
||||||
if let current = strongSelf.avatarIconView {
|
if let current = strongSelf.avatarIconView {
|
||||||
|
|||||||
@ -110,11 +110,11 @@ public final class NavigationBarPresentationData {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
enum NavigationPreviousAction: Equatable {
|
public enum NavigationPreviousAction: Equatable {
|
||||||
case item(UINavigationItem)
|
case item(UINavigationItem)
|
||||||
case close
|
case close
|
||||||
|
|
||||||
static func ==(lhs: NavigationPreviousAction, rhs: NavigationPreviousAction) -> Bool {
|
public static func ==(lhs: NavigationPreviousAction, rhs: NavigationPreviousAction) -> Bool {
|
||||||
switch lhs {
|
switch lhs {
|
||||||
case let .item(lhsItem):
|
case let .item(lhsItem):
|
||||||
if case let .item(rhsItem) = rhs, lhsItem === rhsItem {
|
if case let .item(rhsItem) = rhs, lhsItem === rhsItem {
|
||||||
@ -439,7 +439,6 @@ open class BlurredBackgroundView: UIView {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public protocol NavigationBarHeaderView: UIView {
|
public protocol NavigationBarHeaderView: UIView {
|
||||||
func update(size: CGSize, transition: ContainedViewLayoutTransition)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
open class NavigationBar: ASDisplayNode {
|
open class NavigationBar: ASDisplayNode {
|
||||||
@ -657,7 +656,12 @@ open class NavigationBar: ASDisplayNode {
|
|||||||
self.customHeaderContentView?.removeFromSuperview()
|
self.customHeaderContentView?.removeFromSuperview()
|
||||||
|
|
||||||
if let customHeaderContentView = self.customHeaderContentView {
|
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?
|
||||||
var previousItem: NavigationPreviousAction? {
|
public internal(set) var previousItem: NavigationPreviousAction? {
|
||||||
get {
|
get {
|
||||||
return self._previousItem
|
return self._previousItem
|
||||||
} set(value) {
|
} set(value) {
|
||||||
@ -1373,7 +1377,7 @@ open class NavigationBar: ASDisplayNode {
|
|||||||
|
|
||||||
if let customHeaderContentView = self.customHeaderContentView {
|
if let customHeaderContentView = self.customHeaderContentView {
|
||||||
let headerSize = CGSize(width: size.width, height: nominalHeight)
|
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))
|
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 badgeBackgroundInactiveReactions(CGFloat)
|
||||||
case chatListBadgeBackgroundInactiveMention(CGFloat)
|
case chatListBadgeBackgroundInactiveMention(CGFloat)
|
||||||
case chatListBadgeBackgroundPinned(CGFloat)
|
case chatListBadgeBackgroundPinned(CGFloat)
|
||||||
|
case badgeBackgroundBorder(CGFloat)
|
||||||
|
|
||||||
case chatBubbleMediaCorner(incoming: Bool, mainRadius: CGFloat, inset: 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? {
|
public static func mutedIcon(_ theme: PresentationTheme) -> UIImage? {
|
||||||
return theme.image(PresentationResourceKey.chatListMutedIcon.rawValue, { theme in
|
return theme.image(PresentationResourceKey.chatListMutedIcon.rawValue, { theme in
|
||||||
return generateTintedImage(image: UIImage(bundleImageName: "Chat List/PeerMutedIcon"), color: theme.chatList.muteIconColor)
|
return generateTintedImage(image: UIImage(bundleImageName: "Chat List/PeerMutedIcon"), color: theme.chatList.muteIconColor)
|
||||||
|
|||||||
@ -302,6 +302,7 @@ swift_library(
|
|||||||
"//submodules/TelegramUI/Components/ChatTitleView",
|
"//submodules/TelegramUI/Components/ChatTitleView",
|
||||||
"//submodules/InviteLinksUI:InviteLinksUI",
|
"//submodules/InviteLinksUI:InviteLinksUI",
|
||||||
"//submodules/TelegramUI/Components/NotificationPeerExceptionController",
|
"//submodules/TelegramUI/Components/NotificationPeerExceptionController",
|
||||||
|
"//submodules/TelegramUI/Components/ChatListHeaderComponent",
|
||||||
"//submodules/MediaPasteboardUI:MediaPasteboardUI",
|
"//submodules/MediaPasteboardUI:MediaPasteboardUI",
|
||||||
] + select({
|
] + select({
|
||||||
"@build_bazel_rules_apple//apple:ios_armv7": [],
|
"@build_bazel_rules_apple//apple:ios_armv7": [],
|
||||||
|
|||||||
@ -10,9 +10,15 @@ swift_library(
|
|||||||
"-warnings-as-errors",
|
"-warnings-as-errors",
|
||||||
],
|
],
|
||||||
deps = [
|
deps = [
|
||||||
"//submodules/SSignalKit/SwiftSignalKit:SwiftSignalKit",
|
"//submodules/SSignalKit/SwiftSignalKit",
|
||||||
"//submodules/Display:Display",
|
"//submodules/Display",
|
||||||
"//submodules/ComponentFlow:ComponentFlow",
|
"//submodules/ComponentFlow",
|
||||||
|
"//submodules/TelegramPresentationData",
|
||||||
|
"//submodules/TelegramUI/Components/ChatListTitleView",
|
||||||
|
"//submodules/AccountContext",
|
||||||
|
"//submodules/AppBundle",
|
||||||
|
"//submodules/AsyncDisplayKit",
|
||||||
|
"//submodules/AnimationUI",
|
||||||
],
|
],
|
||||||
visibility = [
|
visibility = [
|
||||||
"//visibility:public",
|
"//visibility:public",
|
||||||
|
|||||||
@ -2,14 +2,932 @@ import Foundation
|
|||||||
import UIKit
|
import UIKit
|
||||||
import Display
|
import Display
|
||||||
import ComponentFlow
|
import ComponentFlow
|
||||||
|
import TelegramPresentationData
|
||||||
|
import AccountContext
|
||||||
|
import ChatListTitleView
|
||||||
|
import AppBundle
|
||||||
|
|
||||||
|
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 ChatListHeaderComponent: Component {
|
|
||||||
public final class View: UIView, NavigationBarHeaderView {
|
public final class View: UIView, NavigationBarHeaderView {
|
||||||
public func update(size: CGSize, transition: ContainedViewLayoutTransition) {
|
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 ActivityIndicator
|
||||||
import AppBundle
|
import AppBundle
|
||||||
|
|
||||||
enum ChatTitleProxyStatus {
|
public enum ChatTitleProxyStatus {
|
||||||
case connecting
|
case connecting
|
||||||
case connected
|
case connected
|
||||||
case available
|
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 iconNode: ASImageNode
|
||||||
private let activityIndicator: ActivityIndicator
|
private let activityIndicator: ActivityIndicator
|
||||||
|
|
||||||
var theme: PresentationTheme {
|
public var theme: PresentationTheme {
|
||||||
didSet {
|
didSet {
|
||||||
if self.theme !== oldValue {
|
if self.theme !== oldValue {
|
||||||
switch self.status {
|
switch self.status {
|
||||||
@ -55,7 +55,7 @@ final class ChatTitleProxyNode: ASDisplayNode {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var status: ChatTitleProxyStatus = .connected {
|
public var status: ChatTitleProxyStatus = .connected {
|
||||||
didSet {
|
didSet {
|
||||||
if self.status != oldValue {
|
if self.status != oldValue {
|
||||||
switch self.status {
|
switch self.status {
|
||||||
@ -73,7 +73,7 @@ final class ChatTitleProxyNode: ASDisplayNode {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
init(theme: PresentationTheme) {
|
public init(theme: PresentationTheme) {
|
||||||
self.theme = theme
|
self.theme = theme
|
||||||
|
|
||||||
self.iconNode = ASImageNode()
|
self.iconNode = ASImageNode()
|
||||||
@ -14,24 +14,42 @@ import AccountContext
|
|||||||
|
|
||||||
private let titleFont = Font.with(size: 17.0, design: .regular, weight: .semibold, traits: [.monospacedNumbers])
|
private let titleFont = Font.with(size: 17.0, design: .regular, weight: .semibold, traits: [.monospacedNumbers])
|
||||||
|
|
||||||
struct NetworkStatusTitle: Equatable {
|
public struct NetworkStatusTitle: Equatable {
|
||||||
enum Status: Equatable {
|
public enum Status: Equatable {
|
||||||
case premium
|
case premium
|
||||||
case emoji(PeerEmojiStatus)
|
case emoji(PeerEmojiStatus)
|
||||||
}
|
}
|
||||||
|
|
||||||
let text: String
|
public var text: String
|
||||||
let activity: Bool
|
public var activity: Bool
|
||||||
let hasProxy: Bool
|
public var hasProxy: Bool
|
||||||
let connectsViaProxy: Bool
|
public var connectsViaProxy: Bool
|
||||||
let isPasscodeSet: Bool
|
public var isPasscodeSet: Bool
|
||||||
let isManuallyLocked: Bool
|
public var isManuallyLocked: Bool
|
||||||
let peerStatus: Status?
|
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 context: AccountContext
|
||||||
private let titleNode: ImmediateTextNode
|
public let titleNode: ImmediateTextNode
|
||||||
private let lockView: ChatListTitleLockView
|
private let lockView: ChatListTitleLockView
|
||||||
private weak var lockSnapshotView: UIView?
|
private weak var lockSnapshotView: UIView?
|
||||||
private let activityIndicator: ActivityIndicator
|
private let activityIndicator: ActivityIndicator
|
||||||
@ -42,12 +60,14 @@ final class ChatListTitleView: UIView, NavigationBarTitleView, NavigationBarTitl
|
|||||||
private let animationCache: AnimationCache
|
private let animationCache: AnimationCache
|
||||||
private let animationRenderer: MultiAnimationRenderer
|
private let animationRenderer: MultiAnimationRenderer
|
||||||
|
|
||||||
var openStatusSetup: ((UIView) -> Void)?
|
public var openStatusSetup: ((UIView) -> Void)?
|
||||||
|
|
||||||
private var validLayout: (CGSize, CGRect)?
|
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)
|
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 {
|
get {
|
||||||
return self._title
|
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
|
let oldValue = self._title
|
||||||
self._title = title
|
self._title = title
|
||||||
|
|
||||||
@ -170,17 +190,19 @@ final class ChatListTitleView: UIView, NavigationBarTitleView, NavigationBarTitl
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if !self.manualLayout {
|
||||||
self.setNeedsLayout()
|
self.setNeedsLayout()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
var toggleIsLocked: (() -> Void)?
|
public var toggleIsLocked: (() -> Void)?
|
||||||
var openProxySettings: (() -> Void)?
|
public var openProxySettings: (() -> Void)?
|
||||||
|
|
||||||
private var isPasscodeSet = false
|
private var isPasscodeSet = false
|
||||||
private var isManuallyLocked = false
|
private var isManuallyLocked = false
|
||||||
|
|
||||||
var theme: PresentationTheme {
|
public var theme: PresentationTheme {
|
||||||
didSet {
|
didSet {
|
||||||
self.titleNode.attributedText = NSAttributedString(string: self.title.text, font: titleFont, textColor: self.theme.rootController.navigationBar.primaryTextColor)
|
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 {
|
didSet {
|
||||||
self.proxyButton.accessibilityLabel = self.strings.VoiceOver_Navigation_ProxySettings
|
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.context = context
|
||||||
self.theme = theme
|
self.theme = theme
|
||||||
self.strings = strings
|
self.strings = strings
|
||||||
@ -283,19 +305,19 @@ final class ChatListTitleView: UIView, NavigationBarTitleView, NavigationBarTitl
|
|||||||
self.proxyButton.addTarget(self, action: #selector(self.proxyButtonPressed), for: .touchUpInside)
|
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")
|
fatalError("init(coder:) has not been implemented")
|
||||||
}
|
}
|
||||||
|
|
||||||
override func layoutSubviews() {
|
override public func layoutSubviews() {
|
||||||
super.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)
|
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)
|
self.validLayout = (size, clearBounds)
|
||||||
|
|
||||||
var indicatorPadding: CGFloat = 0.0
|
var indicatorPadding: CGFloat = 0.0
|
||||||
@ -409,7 +431,7 @@ final class ChatListTitleView: UIView, NavigationBarTitleView, NavigationBarTitl
|
|||||||
self.openProxySettings?()
|
self.openProxySettings?()
|
||||||
}
|
}
|
||||||
|
|
||||||
func makeTransitionMirrorNode() -> ASDisplayNode {
|
public func makeTransitionMirrorNode() -> ASDisplayNode {
|
||||||
let snapshotView = self.snapshotView(afterScreenUpdates: false)
|
let snapshotView = self.snapshotView(afterScreenUpdates: false)
|
||||||
|
|
||||||
return ASDisplayNode(viewBlock: {
|
return ASDisplayNode(viewBlock: {
|
||||||
@ -417,17 +439,17 @@ final class ChatListTitleView: UIView, NavigationBarTitleView, NavigationBarTitl
|
|||||||
}, didLoad: nil)
|
}, didLoad: nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
func animateLayoutTransition() {
|
public func animateLayoutTransition() {
|
||||||
}
|
}
|
||||||
|
|
||||||
var proxyButtonFrame: CGRect? {
|
public var proxyButtonFrame: CGRect? {
|
||||||
if !self.proxyNode.isHidden {
|
if !self.proxyNode.isHidden {
|
||||||
return proxyNode.frame
|
return proxyNode.frame
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
var lockViewFrame: CGRect? {
|
public var lockViewFrame: CGRect? {
|
||||||
if !self.lockView.isHidden && !self.lockView.frame.isEmpty {
|
if !self.lockView.isHidden && !self.lockView.frame.isEmpty {
|
||||||
return self.lockView.frame
|
return self.lockView.frame
|
||||||
} else {
|
} 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 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 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) {
|
if let result = titleCredibilityIconView.hitTest(titleCredibilityIconView.bounds.center, with: event) {
|
||||||
@ -30,6 +30,7 @@ swift_library(
|
|||||||
"//submodules/TelegramUI/Components/EmojiStatusComponent",
|
"//submodules/TelegramUI/Components/EmojiStatusComponent",
|
||||||
"//submodules/TelegramUI/Components/AnimationCache:AnimationCache",
|
"//submodules/TelegramUI/Components/AnimationCache:AnimationCache",
|
||||||
"//submodules/TelegramUI/Components/MultiAnimationRenderer:MultiAnimationRenderer",
|
"//submodules/TelegramUI/Components/MultiAnimationRenderer:MultiAnimationRenderer",
|
||||||
|
"//submodules/Components/ComponentDisplayAdapters:ComponentDisplayAdapters",
|
||||||
],
|
],
|
||||||
visibility = [
|
visibility = [
|
||||||
"//visibility:public",
|
"//visibility:public",
|
||||||
|
|||||||
@ -21,11 +21,12 @@ import ComponentFlow
|
|||||||
import EmojiStatusComponent
|
import EmojiStatusComponent
|
||||||
import AnimationCache
|
import AnimationCache
|
||||||
import MultiAnimationRenderer
|
import MultiAnimationRenderer
|
||||||
|
import ComponentDisplayAdapters
|
||||||
|
|
||||||
private let titleFont = Font.with(size: 17.0, design: .regular, weight: .semibold, traits: [.monospacedNumbers])
|
private let titleFont = Font.with(size: 17.0, design: .regular, weight: .semibold, traits: [.monospacedNumbers])
|
||||||
private let subtitleFont = Font.regular(13.0)
|
private let subtitleFont = Font.regular(13.0)
|
||||||
|
|
||||||
public enum ChatTitleContent {
|
public enum ChatTitleContent: Equatable {
|
||||||
public enum ReplyThreadType {
|
public enum ReplyThreadType {
|
||||||
case comments
|
case comments
|
||||||
case replies
|
case replies
|
||||||
@ -34,6 +35,48 @@ public enum ChatTitleContent {
|
|||||||
case peer(peerView: PeerView, customTitle: String?, onlineMemberCount: Int32?, isScheduledMessages: Bool, isMuted: Bool?, customMessageCount: Int?)
|
case peer(peerView: PeerView, customTitle: String?, onlineMemberCount: Int32?, isScheduledMessages: Bool, isMuted: Bool?, customMessageCount: Int?)
|
||||||
case replyThread(type: ReplyThreadType, count: Int)
|
case replyThread(type: ReplyThreadType, count: Int)
|
||||||
case custom(String, String?, Bool)
|
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 {
|
private enum ChatTitleIcon {
|
||||||
@ -72,6 +115,7 @@ public final class ChatTitleView: UIView, NavigationBarTitleView {
|
|||||||
|
|
||||||
private let button: HighlightTrackingButtonNode
|
private let button: HighlightTrackingButtonNode
|
||||||
|
|
||||||
|
var manualLayout: Bool = false
|
||||||
private var validLayout: (CGSize, CGRect)?
|
private var validLayout: (CGSize, CGRect)?
|
||||||
|
|
||||||
private var titleLeftIcon: ChatTitleIcon = .none
|
private var titleLeftIcon: ChatTitleIcon = .none
|
||||||
@ -89,8 +133,10 @@ public final class ChatTitleView: UIView, NavigationBarTitleView {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private func updateNetworkStatusNode(networkState: AccountNetworkState, layout: ContainerViewLayout?) {
|
private func updateNetworkStatusNode(networkState: AccountNetworkState, layout: ContainerViewLayout?) {
|
||||||
|
if self.manualLayout {
|
||||||
self.setNeedsLayout()
|
self.setNeedsLayout()
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public var networkState: AccountNetworkState = .online(proxy: nil) {
|
public var networkState: AccountNetworkState = .online(proxy: nil) {
|
||||||
didSet {
|
didSet {
|
||||||
@ -306,7 +352,7 @@ public final class ChatTitleView: UIView, NavigationBarTitleView {
|
|||||||
self.button.isUserInteractionEnabled = isEnabled
|
self.button.isUserInteractionEnabled = isEnabled
|
||||||
if !self.updateStatus() {
|
if !self.updateStatus() {
|
||||||
if updated {
|
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))
|
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 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))
|
self.updateLayout(size: size, clearBounds: clearBounds, transition: .animated(duration: 0.3, curve: .spring))
|
||||||
}
|
}
|
||||||
return true
|
return true
|
||||||
@ -642,7 +688,7 @@ public final class ChatTitleView: UIView, NavigationBarTitleView {
|
|||||||
override public func layoutSubviews() {
|
override public func layoutSubviews() {
|
||||||
super.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)
|
self.updateLayout(size: size, clearBounds: clearBounds, transition: .immediate)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -657,7 +703,7 @@ public final class ChatTitleView: UIView, NavigationBarTitleView {
|
|||||||
self.titleContent = titleContent
|
self.titleContent = titleContent
|
||||||
let _ = self.updateStatus()
|
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)
|
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)
|
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 EmojiStatusComponent
|
||||||
import ChatTimerScreen
|
import ChatTimerScreen
|
||||||
import MediaPasteboardUI
|
import MediaPasteboardUI
|
||||||
|
import ChatListHeaderComponent
|
||||||
|
|
||||||
#if DEBUG
|
#if DEBUG
|
||||||
import os.signpost
|
import os.signpost
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user