diff --git a/submodules/ChatListUI/Sources/ChatListController.swift b/submodules/ChatListUI/Sources/ChatListController.swift index 13a09c4599..67b635c904 100644 --- a/submodules/ChatListUI/Sources/ChatListController.swift +++ b/submodules/ChatListUI/Sources/ChatListController.swift @@ -7209,7 +7209,10 @@ private final class ChatListLocationContext { strings: presentationData.strings, dateTimeFormat: presentationData.dateTimeFormat, nameDisplayOrder: presentationData.nameDisplayOrder, + displayBackground: false, content: .custom(presentationData.strings.ChatList_SelectedTopics(Int32(stateAndFilterId.state.selectedThreadIds.count)), nil, false), + activities: nil, + networkState: nil, tapped: { }, longTapped: { @@ -7222,7 +7225,10 @@ private final class ChatListLocationContext { strings: presentationData.strings, dateTimeFormat: presentationData.dateTimeFormat, nameDisplayOrder: presentationData.nameDisplayOrder, + displayBackground: false, content: .peer(peerView: ChatTitleContent.PeerData(peerView: peerView), customTitle: nil, customSubtitle: nil, onlineMemberCount: onlineMemberCount, isScheduledMessages: false, isMuted: nil, customMessageCount: nil, isEnabled: true), + activities: nil, + networkState: nil, tapped: { [weak self] in guard let self else { return diff --git a/submodules/Display/Source/NavigationBarTitleView.swift b/submodules/Display/Source/NavigationBarTitleView.swift index 408cfb2f61..ee8fa822e5 100644 --- a/submodules/Display/Source/NavigationBarTitleView.swift +++ b/submodules/Display/Source/NavigationBarTitleView.swift @@ -4,5 +4,5 @@ import UIKit public protocol NavigationBarTitleView { func animateLayoutTransition() - func updateLayout(size: CGSize, clearBounds: CGRect, transition: ContainedViewLayoutTransition) -> CGRect + func updateLayout(size: CGSize, clearBounds: CGRect, transition: ContainedViewLayoutTransition) } diff --git a/submodules/GalleryUI/Sources/GalleryTitleView.swift b/submodules/GalleryUI/Sources/GalleryTitleView.swift index 8fd42e7c49..d30da98d38 100644 --- a/submodules/GalleryUI/Sources/GalleryTitleView.swift +++ b/submodules/GalleryUI/Sources/GalleryTitleView.swift @@ -41,7 +41,7 @@ final class GalleryTitleView: UIView, NavigationBarTitleView { self.dateNode.attributedText = NSAttributedString(string: dateText, font: dateFont, textColor: .white) } - func updateLayout(size: CGSize, clearBounds: CGRect, transition: ContainedViewLayoutTransition) -> CGRect { + func updateLayout(size: CGSize, clearBounds: CGRect, transition: ContainedViewLayoutTransition) { let leftInset: CGFloat = 0.0 let rightInset: CGFloat = 0.0 @@ -55,8 +55,6 @@ final class GalleryTitleView: UIView, NavigationBarTitleView { self.authorNameNode.frame = CGRect(origin: CGPoint(x: floor((size.width - authorNameSize.width) / 2.0), y: floor((size.height - dateSize.height - authorNameSize.height - labelsSpacing) / 2.0)), size: authorNameSize) self.dateNode.frame = CGRect(origin: CGPoint(x: floor((size.width - dateSize.width) / 2.0), y: floor((size.height - dateSize.height - authorNameSize.height - labelsSpacing) / 2.0) + authorNameSize.height + labelsSpacing), size: dateSize) } - - return CGRect() } func animateLayoutTransition() { diff --git a/submodules/ItemListUI/Sources/ItemListController.swift b/submodules/ItemListUI/Sources/ItemListController.swift index 4da1eb95f7..10ac002792 100644 --- a/submodules/ItemListUI/Sources/ItemListController.swift +++ b/submodules/ItemListUI/Sources/ItemListController.swift @@ -719,7 +719,7 @@ private final class ItemListTextWithSubtitleTitleView: UIView, NavigationBarTitl } } - func updateLayout(size: CGSize, clearBounds: CGRect, transition: ContainedViewLayoutTransition) -> CGRect { + func updateLayout(size: CGSize, clearBounds: CGRect, transition: ContainedViewLayoutTransition) { self.validLayout = (size, clearBounds) let titleSize = self.titleNode.updateLayout(size) @@ -731,8 +731,6 @@ private final class ItemListTextWithSubtitleTitleView: UIView, NavigationBarTitl self.titleNode.frame = titleFrame self.subtitleNode.frame = subtitleFrame - - return titleFrame } func animateLayoutTransition() { diff --git a/submodules/TelegramUI/Components/AnimatedTextComponent/BUILD b/submodules/TelegramUI/Components/AnimatedTextComponent/BUILD index f6ee40df82..782ad3beb0 100644 --- a/submodules/TelegramUI/Components/AnimatedTextComponent/BUILD +++ b/submodules/TelegramUI/Components/AnimatedTextComponent/BUILD @@ -15,6 +15,7 @@ swift_library( "//submodules/ComponentFlow", "//submodules/TelegramPresentationData", "//submodules/Components/BundleIconComponent", + "//submodules/Components/MultilineTextComponent", ], visibility = [ "//visibility:public", diff --git a/submodules/TelegramUI/Components/AnimatedTextComponent/Sources/AnimatedTextComponent.swift b/submodules/TelegramUI/Components/AnimatedTextComponent/Sources/AnimatedTextComponent.swift index cc44a9c264..282759a735 100644 --- a/submodules/TelegramUI/Components/AnimatedTextComponent/Sources/AnimatedTextComponent.swift +++ b/submodules/TelegramUI/Components/AnimatedTextComponent/Sources/AnimatedTextComponent.swift @@ -4,6 +4,7 @@ import Display import ComponentFlow import TelegramPresentationData import BundleIconComponent +import MultilineTextComponent extension ComponentTransition { func animateBlur(layer: CALayer, from: CGFloat, to: CGFloat, delay: Double = 0.0, removeOnCompletion: Bool = true, completion: ((Bool) -> Void)? = nil) { @@ -46,6 +47,7 @@ public final class AnimatedTextComponent: Component { public let items: [Item] public let noDelay: Bool public let animateScale: Bool + public let animateSlide: Bool public let preferredDirectionIsDown: Bool public let blur: Bool @@ -55,6 +57,7 @@ public final class AnimatedTextComponent: Component { items: [Item], noDelay: Bool = false, animateScale: Bool = true, + animateSlide: Bool = true, preferredDirectionIsDown: Bool = false, blur: Bool = false ) { @@ -63,6 +66,7 @@ public final class AnimatedTextComponent: Component { self.items = items self.noDelay = noDelay self.animateScale = animateScale + self.animateSlide = animateSlide self.preferredDirectionIsDown = preferredDirectionIsDown self.blur = blur } @@ -83,6 +87,9 @@ public final class AnimatedTextComponent: Component { if lhs.animateScale != rhs.animateScale { return false } + if lhs.animateSlide != rhs.animateSlide { + return false + } if lhs.preferredDirectionIsDown != rhs.preferredDirectionIsDown { return false } @@ -238,10 +245,8 @@ public final class AnimatedTextComponent: Component { var characterOffset: CGPoint = .zero switch character { case let .text(text): - characterComponent = AnyComponent(Text( - text: String(text), - font: component.font, - color: component.color + characterComponent = AnyComponent(MultilineTextComponent( + text: .plain(NSAttributedString(string: text, font: component.font, textColor: component.color)) )) case let .icon(iconName, tint, offset): characterComponent = AnyComponent(BundleIconComponent( @@ -305,7 +310,9 @@ public final class AnimatedTextComponent: Component { if component.blur { ComponentTransition.easeInOut(duration: 0.2).animateBlur(layer: characterComponentView.layer, from: transitionBlurRadius, to: 0.0, delay: delayNorm * delayWidth) } - characterComponentView.layer.animatePosition(from: CGPoint(x: 0.0, y: characterSize.height * offsetNorm), to: CGPoint(), duration: 0.4, delay: delayNorm * delayWidth, timingFunction: kCAMediaTimingFunctionSpring, additive: true) + if component.animateSlide { + characterComponentView.layer.animatePosition(from: CGPoint(x: 0.0, y: characterSize.height * offsetNorm), to: CGPoint(), duration: 0.4, delay: delayNorm * delayWidth, timingFunction: kCAMediaTimingFunctionSpring, additive: true) + } characterComponentView.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.18, delay: delayNorm * delayWidth) } } @@ -342,7 +349,9 @@ public final class AnimatedTextComponent: Component { } else { targetY = characterComponentView.center.y - characterComponentView.bounds.height * offsetNorm } - outScaleTransition.setPosition(view: characterComponentView, position: CGPoint(x: characterComponentView.center.x, y: targetY), delay: delayNorm * delayWidth) + if component.animateSlide { + outScaleTransition.setPosition(view: characterComponentView, position: CGPoint(x: characterComponentView.center.x, y: targetY), delay: delayNorm * delayWidth) + } outAlphaTransition.setAlpha(view: characterComponentView, alpha: 0.0, delay: delayNorm * delayWidth, completion: { [weak characterComponentView] _ in characterComponentView?.removeFromSuperview() }) diff --git a/submodules/TelegramUI/Components/ChatListHeaderComponent/Sources/ChatListHeaderComponent.swift b/submodules/TelegramUI/Components/ChatListHeaderComponent/Sources/ChatListHeaderComponent.swift index 11c8d8b3b0..89876f6a9b 100644 --- a/submodules/TelegramUI/Components/ChatListHeaderComponent/Sources/ChatListHeaderComponent.swift +++ b/submodules/TelegramUI/Components/ChatListHeaderComponent/Sources/ChatListHeaderComponent.swift @@ -670,7 +670,7 @@ public final class ChatListHeaderComponent: Component { chatListTitleView.theme = theme chatListTitleView.strings = strings chatListTitleView.setTitle(chatListTitle, animated: false) - let titleContentRect = chatListTitleView.updateLayout(size: chatListTitleContentSize, clearBounds: CGRect(origin: CGPoint(), size: chatListTitleContentSize), transition: transition.containedViewLayoutTransition) + let titleContentRect = chatListTitleView.updateLayoutInternal(size: chatListTitleContentSize, clearBounds: CGRect(origin: CGPoint(), size: chatListTitleContentSize), transition: transition.containedViewLayoutTransition) centerContentWidth = floor((chatListTitleContentSize.width * 0.5 - titleContentRect.minX) * 2.0) let centerOffset = sideContentWidth * 0.5 diff --git a/submodules/TelegramUI/Components/ChatListTitleView/Sources/ChatListTitleView.swift b/submodules/TelegramUI/Components/ChatListTitleView/Sources/ChatListTitleView.swift index 0fc758a8b8..98c5db24d6 100644 --- a/submodules/TelegramUI/Components/ChatListTitleView/Sources/ChatListTitleView.swift +++ b/submodules/TelegramUI/Components/ChatListTitleView/Sources/ChatListTitleView.swift @@ -326,7 +326,11 @@ public final class ChatListTitleView: UIView, NavigationBarTitleView, Navigation } } - public func updateLayout(size: CGSize, clearBounds: CGRect, transition: ContainedViewLayoutTransition) -> CGRect { + public func updateLayout(size: CGSize, clearBounds: CGRect, transition: ContainedViewLayoutTransition) { + let _ = self.updateLayoutInternal(size: size, clearBounds: clearBounds, transition: transition) + } + + public func updateLayoutInternal(size: CGSize, clearBounds: CGRect, transition: ContainedViewLayoutTransition) -> CGRect { self.validLayout = (size, clearBounds) var indicatorPadding: CGFloat = 0.0 diff --git a/submodules/TelegramUI/Components/ChatTitleView/BUILD b/submodules/TelegramUI/Components/ChatTitleView/BUILD index a136c8871c..aafb2e9ff0 100644 --- a/submodules/TelegramUI/Components/ChatTitleView/BUILD +++ b/submodules/TelegramUI/Components/ChatTitleView/BUILD @@ -32,6 +32,7 @@ swift_library( "//submodules/TelegramUI/Components/MultiAnimationRenderer:MultiAnimationRenderer", "//submodules/Components/ComponentDisplayAdapters:ComponentDisplayAdapters", "//submodules/TelegramUI/Components/GlassBackgroundComponent", + "//submodules/TelegramUI/Components/AnimatedTextComponent", ], visibility = [ "//visibility:public", diff --git a/submodules/TelegramUI/Components/ChatTitleView/Sources/ChatTitleComponent.swift b/submodules/TelegramUI/Components/ChatTitleView/Sources/ChatTitleComponent.swift new file mode 100644 index 0000000000..b17a89d9db --- /dev/null +++ b/submodules/TelegramUI/Components/ChatTitleView/Sources/ChatTitleComponent.swift @@ -0,0 +1,1150 @@ +import Foundation +import UIKit +import Display +import ComponentFlow +import TelegramPresentationData +import AccountContext +import TelegramUIPreferences +import Postbox +import TelegramCore +import PeerPresenceStatusManager +import ChatTitleActivityNode +import AnimatedTextComponent +import PhoneNumberFormat +import TelegramStringFormatting +import EmojiStatusComponent +import GlassBackgroundComponent + +public final class ChatNavigationBarTitleView: UIView, NavigationBarTitleView { + private struct Params: Equatable { + let size: CGSize + let clearBounds: CGRect + + init(size: CGSize, clearBounds: CGRect) { + self.size = size + self.clearBounds = clearBounds + } + } + + private final class ContentData { + let context: AccountContext + let theme: PresentationTheme + let strings: PresentationStrings + let dateTimeFormat: PresentationDateTimeFormat + let nameDisplayOrder: PresentationPersonNameOrder + let content: ChatTitleContent + + init(context: AccountContext, theme: PresentationTheme, strings: PresentationStrings, dateTimeFormat: PresentationDateTimeFormat, nameDisplayOrder: PresentationPersonNameOrder, content: ChatTitleContent) { + self.context = context + self.theme = theme + self.strings = strings + self.dateTimeFormat = dateTimeFormat + self.nameDisplayOrder = nameDisplayOrder + self.content = content + } + } + + private let title = ComponentView() + + private var contentData: ContentData? + private var params: Params? + private var activities: ChatTitleComponent.Activities? + private var networkState: AccountNetworkState? + + public var tapAction: (() -> Void)? + public var longTapAction: (() -> Void)? + + override public init(frame: CGRect) { + super.init(frame: frame) + } + + required public init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + public func animateLayoutTransition() { + } + + public func prepareSnapshotState() -> ChatTitleView.SnapshotState? { + //return titleView.contentView?.snapshotView(afterScreenUpdates: false) + return nil + } + + public func animateFromSnapshot(_ snapshotState: ChatTitleView.SnapshotState, direction: ChatTitleView.AnimateFromSnapshotDirection) { + guard let titleView = self.title.view as? ChatTitleComponent.View else { + return + } + //titleView.contentView?.animateFromSnapshot(snapshotState, direction: direction) + titleView.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) + } + + public func update( + context: AccountContext, + theme: PresentationTheme, + strings: PresentationStrings, + dateTimeFormat: PresentationDateTimeFormat, + nameDisplayOrder: PresentationPersonNameOrder, + content: ChatTitleContent, + transition: ComponentTransition + ) { + self.contentData = ContentData( + context: context, + theme: theme, + strings: strings, + dateTimeFormat: dateTimeFormat, + nameDisplayOrder: nameDisplayOrder, + content: content + ) + self.update(transition: transition) + } + + public func updateActivities(activities: ChatTitleComponent.Activities?, transition: ComponentTransition) { + if self.activities != activities { + self.activities = activities + self.update(transition: transition) + } + } + + public func updateNetworkState(networkState: AccountNetworkState, transition: ComponentTransition) { + if self.networkState != networkState { + self.networkState = networkState + self.update(transition: transition) + } + } + + private func update(transition: ComponentTransition) { + guard let contentData, let params else { + return + } + self.update(params: params, contentData: contentData, transition: transition) + } + + private func update(params: Params, contentData: ContentData, transition: ComponentTransition) { + let _ = self.title.update( + transition: transition, + component: AnyComponent(ChatTitleComponent( + context: contentData.context, + theme: contentData.theme, + strings: contentData.strings, + dateTimeFormat: contentData.dateTimeFormat, + nameDisplayOrder: contentData.nameDisplayOrder, + displayBackground: true, + content: contentData.content, + activities: self.activities, + networkState: self.networkState, + tapped: { [weak self] in + guard let self else { + return + } + self.tapAction?() + }, + longTapped: { [weak self] in + guard let self else { + return + } + self.longTapAction?() + } + )), + environment: {}, + containerSize: params.size + ) + if let titleView = self.title.view { + if titleView.superview == nil { + self.addSubview(titleView) + } + transition.setFrame(view: titleView, frame: CGRect(origin: CGPoint(), size: params.size)) + } + } + + public func updateLayout(size: CGSize, clearBounds: CGRect, transition: ContainedViewLayoutTransition) { + let params = Params(size: size, clearBounds: clearBounds) + if self.params != params { + self.params = params + if let contentData { + self.update(params: params, contentData: contentData, transition: ComponentTransition(transition)) + } + } + } +} + +public final class ChatTitleComponent: Component { + public struct Activities: Equatable { + public struct Item: Equatable { + public let peer: EnginePeer + public let activity: PeerInputActivity + + public init(peer: EnginePeer, activity: PeerInputActivity) { + self.peer = peer + self.activity = activity + } + } + + public let peerId: EnginePeer.Id + public let items: [Item] + + public init(peerId: EnginePeer.Id, items: [Item]) { + self.peerId = peerId + self.items = items + } + } + + public let context: AccountContext + public let theme: PresentationTheme + public let strings: PresentationStrings + public let dateTimeFormat: PresentationDateTimeFormat + public let nameDisplayOrder: PresentationPersonNameOrder + public let displayBackground: Bool + public let content: ChatTitleContent + public let activities: Activities? + public let networkState: AccountNetworkState? + public let tapped: () -> Void + public let longTapped: () -> Void + + public init( + context: AccountContext, + theme: PresentationTheme, + strings: PresentationStrings, + dateTimeFormat: PresentationDateTimeFormat, + nameDisplayOrder: PresentationPersonNameOrder, + displayBackground: Bool, + content: ChatTitleContent, + activities: Activities?, + networkState: AccountNetworkState?, + tapped: @escaping () -> Void, + longTapped: @escaping () -> Void + ) { + self.context = context + self.theme = theme + self.strings = strings + self.dateTimeFormat = dateTimeFormat + self.nameDisplayOrder = nameDisplayOrder + self.displayBackground = displayBackground + self.content = content + self.activities = activities + self.networkState = networkState + 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.displayBackground != rhs.displayBackground { + return false + } + if lhs.content != rhs.content { + return false + } + if lhs.activities != rhs.activities { + return false + } + if lhs.networkState != rhs.networkState { + return false + } + return true + } + + public final class View: UIView { + private var backgroundView: GlassBackgroundView? + private let contentContainer: UIView + private let title = ComponentView() + private var subtitleNode: ChatTitleActivityNode? + private var leftIcon: ComponentView? + private var rightIcon: ComponentView? + private var credibilityIcon: ComponentView? + private var verifiedIcon: ComponentView? + private var statusIcon: ComponentView? + + private var presenceManager: PeerPresenceStatusManager? + + private var component: ChatTitleComponent? + private weak var state: EmptyComponentState? + + override init(frame: CGRect) { + self.contentContainer = UIView() + self.contentContainer.clipsToBounds = true + + super.init(frame: frame) + + self.presenceManager = PeerPresenceStatusManager(update: { [weak self] in + guard let self else { + return + } + self.state?.updated(transition: .spring(duration: 0.4)) + }) + + let recognizer = TapLongTapOrDoubleTapGestureRecognizer(target: self, action: #selector(self.onTapGesture(_:))) + recognizer.tapActionAtPoint = { _ in + return .waitForSingleTap + } + self.contentContainer.addGestureRecognizer(recognizer) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + @objc private func onTapGesture(_ recognizer: TapLongTapOrDoubleTapGestureRecognizer) { + if let (gesture, _) = recognizer.lastRecognizedGestureAndLocation { + switch gesture { + case .tap: + self.component?.tapped() + case .longTap: + self.component?.longTapped() + default: + break + } + } + } + + func update(component: ChatTitleComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { + let statusIconsSpacing: CGFloat = 4.0 + let leftTitleIconSpacing: CGFloat = 3.0 + let rightTitleIconSpacing: CGFloat = 3.0 + let containerSideInset: CGFloat = 14.0 + + self.component = component + self.state = state + + var titleSegments: [AnimatedTextComponent.Item] = [] + var titleLeftIcon: TitleIconComponent.Kind? + var titleRightIcon: TitleIconComponent.Kind? + var titleCredibilityIcon: ChatTitleCredibilityIcon = .none + var titleVerifiedIcon: ChatTitleCredibilityIcon = .none + var titleStatusIcon: ChatTitleCredibilityIcon = .none + var isEnabled = true + switch component.content { + case let .peer(peerView, customTitle, _, _, isScheduledMessages, isMuted, _, isEnabledValue): + if peerView.peerId.isReplies { + titleSegments = [AnimatedTextComponent.Item( + id: AnyHashable(0), + isUnbreakable: true, + content: .text(component.strings.DialogList_Replies) + )] + isEnabled = false + } else if isScheduledMessages { + if peerView.peerId == component.context.account.peerId { + titleSegments = [AnimatedTextComponent.Item( + id: AnyHashable(0), + isUnbreakable: true, + content: .text(component.strings.ScheduledMessages_RemindersTitle) + )] + } else { + titleSegments = [AnimatedTextComponent.Item( + id: AnyHashable(0), + isUnbreakable: true, + content: .text(component.strings.ScheduledMessages_Title) + )] + } + isEnabled = false + } else { + if let peer = peerView.peer { + if let customTitle { + titleSegments = [AnimatedTextComponent.Item( + id: AnyHashable(0), + isUnbreakable: true, + content: .text(customTitle) + )] + } else if peerView.peerId == component.context.account.peerId { + if peerView.isSavedMessages { + titleSegments = [AnimatedTextComponent.Item( + id: AnyHashable(0), + isUnbreakable: true, + content: .text(component.strings.Conversation_MyNotes) + )] + } else { + titleSegments = [AnimatedTextComponent.Item( + id: AnyHashable(0), + isUnbreakable: true, + content: .text(component.strings.Conversation_SavedMessages) + )] + } + } else if peerView.peerId.isAnonymousSavedMessages { + titleSegments = [AnimatedTextComponent.Item( + id: AnyHashable(0), + isUnbreakable: true, + content: .text(component.strings.ChatList_AuthorHidden) + )] + } else { + if !peerView.isContact, let user = peer as? TelegramUser, !user.flags.contains(.isSupport), user.botInfo == nil, let phone = user.phone, !phone.isEmpty { + titleSegments = [AnimatedTextComponent.Item( + id: AnyHashable(0), + isUnbreakable: true, + content: .text(formatPhoneNumber(context: component.context, number: phone)) + )] + } else { + titleSegments = [AnimatedTextComponent.Item( + id: AnyHashable(0), + isUnbreakable: true, + content: .text(EnginePeer(peer).displayTitle(strings: component.strings, displayOrder: component.nameDisplayOrder)) + )] + } + } + if peer.id != component.context.account.peerId { + let premiumConfiguration = PremiumConfiguration.with(appConfiguration: component.context.currentAppConfiguration.with({ $0 })) + if peer.isFake { + titleCredibilityIcon = .fake + } else if peer.isScam { + titleCredibilityIcon = .scam + } else if let emojiStatus = peer.emojiStatus { + titleStatusIcon = .emojiStatus(emojiStatus) + } else if peer.isPremium && !premiumConfiguration.isPremiumDisabled { + titleCredibilityIcon = .premium + } + + if peer.isVerified { + titleCredibilityIcon = .verified + } + if let verificationIconFileId = peer.verificationIconFileId { + titleVerifiedIcon = .emojiStatus(PeerEmojiStatus(content: .emoji(fileId: verificationIconFileId), expirationDate: nil)) + } + } + } + if peerView.peerId.namespace == Namespaces.Peer.SecretChat { + titleLeftIcon = .lock + } + if let isMuted { + if isMuted { + titleRightIcon = .mute + } + } else { + if let notificationSettings = peerView.notificationSettings { + if case let .muted(until) = notificationSettings.muteState, until >= Int32(CFAbsoluteTimeGetCurrent() + NSTimeIntervalSince1970) { + if titleCredibilityIcon != .verified { + titleRightIcon = .mute + } + } + } + } + if peerView.peerId.isVerificationCodes { + isEnabled = false + } else { + isEnabled = isEnabledValue + } + } + case let .replyThread(type, count): + if count > 0 { + var commentsPart: String + switch type { + case .comments: + commentsPart = component.strings.Conversation_TitleComments(Int32(count)) + case .replies: + commentsPart = component.strings.Conversation_TitleReplies(Int32(count)) + } + + if commentsPart.contains("[") && commentsPart.contains("]") { + if let startIndex = commentsPart.firstIndex(of: "["), let endIndex = commentsPart.firstIndex(of: "]") { + commentsPart.removeSubrange(startIndex ... endIndex) + } + } else { + commentsPart = commentsPart.trimmingCharacters(in: CharacterSet(charactersIn: "0123456789-,.")) + } + + let rawTextAndRanges: PresentationStrings.FormattedString + switch type { + case .comments: + rawTextAndRanges = component.strings.Conversation_TitleCommentsFormat("\(count)", commentsPart) + case .replies: + rawTextAndRanges = component.strings.Conversation_TitleRepliesFormat("\(count)", commentsPart) + } + + let rawText = rawTextAndRanges.string + + var textIndex = 0 + var latestIndex = 0 + for indexAndRange in rawTextAndRanges.ranges { + let index = indexAndRange.index + let range = indexAndRange.range + + var lowerSegmentIndex = range.lowerBound + if index != 0 { + lowerSegmentIndex = min(lowerSegmentIndex, latestIndex) + } else { + if latestIndex < range.lowerBound { + let part = String(rawText[rawText.index(rawText.startIndex, offsetBy: latestIndex) ..< rawText.index(rawText.startIndex, offsetBy: range.lowerBound)]) + + titleSegments.append(AnimatedTextComponent.Item( + id: AnyHashable(textIndex), + isUnbreakable: true, + content: .text(part) + )) + textIndex += 1 + } + } + latestIndex = range.upperBound + + let part = String(rawText[rawText.index(rawText.startIndex, offsetBy: lowerSegmentIndex) ..< rawText.index(rawText.startIndex, offsetBy: min(rawText.count, range.upperBound))]) + if index == 0 { + titleSegments.append(AnimatedTextComponent.Item( + id: AnyHashable(textIndex), + isUnbreakable: false, + content: .text(part) + )) + textIndex += 1 + } else { + titleSegments.append(AnimatedTextComponent.Item( + id: AnyHashable(textIndex), + isUnbreakable: true, + content: .text(part) + )) + textIndex += 1 + } + } + if latestIndex < rawText.count { + let part = String(rawText[rawText.index(rawText.startIndex, offsetBy: latestIndex)...]) + titleSegments.append(AnimatedTextComponent.Item( + id: AnyHashable(textIndex), + isUnbreakable: true, + content: .text(part) + )) + textIndex += 1 + } + } else { + switch type { + case .comments: + titleSegments = [AnimatedTextComponent.Item( + id: AnyHashable(0), + isUnbreakable: true, + content: .text(component.strings.Conversation_TitleCommentsEmpty) + )] + case .replies: + titleSegments = [AnimatedTextComponent.Item( + id: AnyHashable(0), + isUnbreakable: true, + content: .text(component.strings.Conversation_TitleRepliesEmpty) + )] + } + } + + isEnabled = false + case let .custom(text, _, enabled): + titleSegments = [AnimatedTextComponent.Item( + id: AnyHashable(0), + isUnbreakable: true, + content: .text(text) + )] + isEnabled = enabled + } + + var accessibilityText = "" + for segment in titleSegments { + switch segment.content { + case let .number(value, _): + accessibilityText.append("\(value)") + case let .text(string): + accessibilityText.append(string) + case .icon: + break + } + } + self.accessibilityLabel = accessibilityText + + var inputActivitiesAllowed = true + switch component.content { + case let .peer(peerView, _, _, _, isScheduledMessages, _, _, _): + if let peer = peerView.peer { + if peer.id == component.context.account.peerId || isScheduledMessages || peer.id.isRepliesOrVerificationCodes { + inputActivitiesAllowed = false + } + } + case .replyThread: + inputActivitiesAllowed = true + default: + inputActivitiesAllowed = false + } + + let subtitleFont = Font.regular(12.0) + var state: ChatTitleActivityNodeState = .none + switch component.networkState { + case .waitingForNetwork, .connecting, .updating: + var infoText: String + switch component.networkState { + case .waitingForNetwork: + infoText = component.strings.ChatState_WaitingForNetwork + case .connecting: + infoText = component.strings.ChatState_Connecting + case .updating: + infoText = component.strings.ChatState_Updating + case .online, .none: + infoText = "" + } + state = .info(NSAttributedString(string: infoText, font: subtitleFont, textColor: component.theme.chat.inputPanel.inputControlColor), .generic) + case .online, .none: + if let inputActivities = component.activities, !inputActivities.items.isEmpty, inputActivitiesAllowed { + var stringValue = "" + var mergedActivity = inputActivities.items[0].activity + for item in inputActivities.items { + if item.activity != mergedActivity { + mergedActivity = .typingText + break + } + } + if inputActivities.peerId.namespace == Namespaces.Peer.CloudUser || inputActivities.peerId.namespace == Namespaces.Peer.SecretChat { + switch mergedActivity { + case .typingText: + stringValue = component.strings.Conversation_typing + case .uploadingFile: + stringValue = component.strings.Activity_UploadingDocument + case .recordingVoice: + stringValue = component.strings.Activity_RecordingAudio + case .uploadingPhoto: + stringValue = component.strings.Activity_UploadingPhoto + case .uploadingVideo: + stringValue = component.strings.Activity_UploadingVideo + case .playingGame: + stringValue = component.strings.Activity_PlayingGame + case .recordingInstantVideo: + stringValue = component.strings.Activity_RecordingVideoMessage + case .uploadingInstantVideo: + stringValue = component.strings.Activity_UploadingVideoMessage + case .choosingSticker: + stringValue = component.strings.Activity_ChoosingSticker + case let .seeingEmojiInteraction(emoticon): + stringValue = component.strings.Activity_EnjoyingAnimations(emoticon).string + case .speakingInGroupCall, .interactingWithEmoji: + stringValue = "" + } + } else { + if inputActivities.items.count > 1 { + let peerTitle = inputActivities.items[0].peer.compactDisplayTitle + if inputActivities.items.count == 2 { + let secondPeerTitle = inputActivities.items[1].peer.compactDisplayTitle + stringValue = component.strings.Chat_MultipleTypingPair(peerTitle, secondPeerTitle).string + } else { + stringValue = component.strings.Chat_MultipleTypingMore(peerTitle, String(inputActivities.items.count - 1)).string + } + } else if let item = inputActivities.items.first { + stringValue = item.peer.compactDisplayTitle + } + } + let color = component.theme.rootController.navigationBar.accentTextColor + let string = NSAttributedString(string: stringValue, font: subtitleFont, textColor: color) + switch mergedActivity { + case .typingText: + state = .typingText(string, color) + case .recordingVoice: + state = .recordingVoice(string, color) + case .recordingInstantVideo: + state = .recordingVideo(string, color) + case .uploadingFile, .uploadingInstantVideo, .uploadingPhoto, .uploadingVideo: + state = .uploading(string, color) + case .playingGame: + state = .playingGame(string, color) + case .speakingInGroupCall, .interactingWithEmoji: + state = .typingText(string, color) + case .choosingSticker: + state = .choosingSticker(string, color) + case .seeingEmojiInteraction: + state = .choosingSticker(string, color) + } + } else { + switch component.content { + case let .peer(peerView, customTitle, customSubtitle, onlineMemberCount, isScheduledMessages, _, customMessageCount, _): + if let customSubtitle { + let string = NSAttributedString(string: customSubtitle, font: subtitleFont, textColor: component.theme.chat.inputPanel.inputControlColor) + state = .info(string, .generic) + } else if let customMessageCount = customMessageCount, customMessageCount != 0 { + let string = NSAttributedString(string: component.strings.Conversation_Messages(Int32(customMessageCount)), font: subtitleFont, textColor: component.theme.chat.inputPanel.inputControlColor) + state = .info(string, .generic) + } else if let peer = peerView.peer { + let servicePeer = isServicePeer(peer) + if peer.id == component.context.account.peerId || isScheduledMessages || peer.id.isRepliesOrVerificationCodes { + let string = NSAttributedString(string: "", font: subtitleFont, textColor: component.theme.chat.inputPanel.inputControlColor) + state = .info(string, .generic) + } else if let user = peer as? TelegramUser { + if user.isDeleted { + state = .none + } else if servicePeer { + let string = NSAttributedString(string: "", font: subtitleFont, textColor: component.theme.chat.inputPanel.inputControlColor) + state = .info(string, .generic) + } else if user.flags.contains(.isSupport) { + let statusText = component.strings.Bot_GenericSupportStatus + let string = NSAttributedString(string: statusText, font: subtitleFont, textColor: component.theme.chat.inputPanel.inputControlColor) + state = .info(string, .generic) + } else if let _ = user.botInfo { + let statusText: String + if let subscriberCount = user.subscriberCount { + statusText = component.strings.Conversation_StatusBotSubscribers(subscriberCount) + } else { + statusText = component.strings.Bot_GenericBotStatus + } + + let string = NSAttributedString(string: statusText, font: subtitleFont, textColor: component.theme.chat.inputPanel.inputControlColor) + state = .info(string, .generic) + } else if let peer = peerView.peer { + let timestamp = CFAbsoluteTimeGetCurrent() + NSTimeIntervalSince1970 + let userPresence: TelegramUserPresence + if let presence = peerView.peerPresences[peer.id] as? TelegramUserPresence { + userPresence = presence + self.presenceManager?.reset(presence: EnginePeer.Presence(presence)) + } else { + userPresence = TelegramUserPresence(status: .none, lastActivity: 0) + } + let (string, activity) = stringAndActivityForUserPresence(strings: component.strings, dateTimeFormat: component.dateTimeFormat, presence: EnginePeer.Presence(userPresence), relativeTo: Int32(timestamp)) + let attributedString = NSAttributedString(string: string, font: subtitleFont, textColor: activity ? component.theme.rootController.navigationBar.accentTextColor : component.theme.chat.inputPanel.inputControlColor) + state = .info(attributedString, activity ? .online : .lastSeenTime) + } else { + let string = NSAttributedString(string: "", font: subtitleFont, textColor: component.theme.chat.inputPanel.inputControlColor) + state = .info(string, .generic) + } + } else if let group = peer as? TelegramGroup { + var onlineCount = 0 + if let cachedGroupData = peerView.cachedData as? CachedGroupData, let participants = cachedGroupData.participants { + let timestamp = CFAbsoluteTimeGetCurrent() + NSTimeIntervalSince1970 + for participant in participants.participants { + if let presence = peerView.peerPresences[participant.peerId] as? TelegramUserPresence { + let relativeStatus = relativeUserPresenceStatus(EnginePeer.Presence(presence), relativeTo: Int32(timestamp)) + switch relativeStatus { + case .online: + onlineCount += 1 + default: + break + } + } + } + } + if onlineCount > 1 { + let string = NSMutableAttributedString() + + string.append(NSAttributedString(string: "\(component.strings.Conversation_StatusMembers(Int32(group.participantCount))), ", font: subtitleFont, textColor: component.theme.chat.inputPanel.inputControlColor)) + string.append(NSAttributedString(string: component.strings.Conversation_StatusOnline(Int32(onlineCount)), font: subtitleFont, textColor: component.theme.chat.inputPanel.inputControlColor)) + state = .info(string, .generic) + } else { + let string = NSAttributedString(string: component.strings.Conversation_StatusMembers(Int32(group.participantCount)), font: subtitleFont, textColor: component.theme.chat.inputPanel.inputControlColor) + state = .info(string, .generic) + } + } else if let channel = peer as? TelegramChannel { + if channel.isForumOrMonoForum, customTitle != nil { + let string = NSAttributedString(string: EnginePeer(peer).displayTitle(strings: component.strings, displayOrder: component.nameDisplayOrder), font: subtitleFont, textColor: component.theme.chat.inputPanel.inputControlColor) + state = .info(string, .generic) + } else if let cachedChannelData = peerView.cachedData as? CachedChannelData, let memberCount = onlineMemberCount.total ?? cachedChannelData.participantsSummary.memberCount { + if memberCount == 0 { + let string: NSAttributedString + if case .group = channel.info { + string = NSAttributedString(string: component.strings.Group_Status, font: subtitleFont, textColor: component.theme.chat.inputPanel.inputControlColor) + } else { + string = NSAttributedString(string: component.strings.Channel_Status, font: subtitleFont, textColor: component.theme.chat.inputPanel.inputControlColor) + } + state = .info(string, .generic) + } else { + if case .group = channel.info, let onlineMemberCount = onlineMemberCount.recent, onlineMemberCount > 1 { + let string = NSMutableAttributedString() + + string.append(NSAttributedString(string: "\(component.strings.Conversation_StatusMembers(Int32(memberCount))), ", font: subtitleFont, textColor: component.theme.chat.inputPanel.inputControlColor)) + string.append(NSAttributedString(string: component.strings.Conversation_StatusOnline(Int32(onlineMemberCount)), font: subtitleFont, textColor: component.theme.chat.inputPanel.inputControlColor)) + state = .info(string, .generic) + } else { + let membersString: String + if case .group = channel.info { + membersString = component.strings.Conversation_StatusMembers(memberCount) + } else { + membersString = component.strings.Conversation_StatusSubscribers(memberCount) + } + let string = NSAttributedString(string: membersString, font: subtitleFont, textColor: component.theme.chat.inputPanel.inputControlColor) + state = .info(string, .generic) + } + } + } else { + switch channel.info { + case .group: + let string = NSAttributedString(string: component.strings.Group_Status, font: subtitleFont, textColor: component.theme.chat.inputPanel.inputControlColor) + state = .info(string, .generic) + case .broadcast: + let string = NSAttributedString(string: component.strings.Channel_Status, font: subtitleFont, textColor: component.theme.chat.inputPanel.inputControlColor) + state = .info(string, .generic) + } + } + } + } + case let .custom(_, subtitle?, _): + let string = NSAttributedString(string: subtitle, font: subtitleFont, textColor: component.theme.chat.inputPanel.inputControlColor) + state = .info(string, .generic) + default: + break + } + + self.accessibilityValue = state.string + } + } + + var rightIconSize: CGSize? + if let titleRightIcon { + let rightIcon: ComponentView + var rightIconTransition = transition + if let current = self.rightIcon { + rightIcon = current + } else { + rightIconTransition = rightIconTransition.withAnimation(.none) + rightIcon = ComponentView() + self.rightIcon = rightIcon + } + rightIconSize = rightIcon.update( + transition: rightIconTransition, + component: AnyComponent(TitleIconComponent( + kind: titleRightIcon, + color: component.theme.chat.inputPanel.inputControlColor + )), + environment: {}, + containerSize: CGSize(width: 100.0, height: 100.0) + ) + } else if let rightIcon = self.rightIcon { + self.rightIcon = nil + if let rightIconView = rightIcon.view { + transition.setScale(view: rightIconView, scale: 0.001) + transition.setAlpha(view: rightIconView, alpha: 0.0, completion: { [weak rightIconView] _ in + rightIconView?.removeFromSuperview() + }) + } + } + + var leftIconSize: CGSize? + if let titleLeftIcon { + let leftIcon: ComponentView + var leftIconTransition = transition + if let current = self.leftIcon { + leftIcon = current + } else { + leftIconTransition = leftIconTransition.withAnimation(.none) + leftIcon = ComponentView() + self.leftIcon = leftIcon + } + leftIconSize = leftIcon.update( + transition: leftIconTransition, + component: AnyComponent(TitleIconComponent( + kind: titleLeftIcon, + color: component.theme.chat.inputPanel.panelControlColor + )), + environment: {}, + containerSize: CGSize(width: 100.0, height: 100.0) + ) + } else if let leftIcon = self.leftIcon { + self.leftIcon = nil + if let leftIconView = leftIcon.view { + transition.setScale(view: leftIconView, scale: 0.001) + transition.setAlpha(view: leftIconView, alpha: 0.0, completion: { [weak leftIconView] _ in + leftIconView?.removeFromSuperview() + }) + } + } + + let mapTitleIcon: (ChatTitleCredibilityIcon) -> EmojiStatusComponent.Content? = { value in + switch value { + case .none: + return nil + case .premium: + return .premium(color: component.theme.list.itemAccentColor) + case .verified: + return .verified(fillColor: component.theme.list.itemCheckColors.fillColor, foregroundColor: component.theme.list.itemCheckColors.foregroundColor, sizeType: .large) + case .fake: + return .text(color: component.theme.chat.message.incoming.scamColor, string: component.strings.Message_FakeAccount.uppercased()) + case .scam: + return .text(color: component.theme.chat.message.incoming.scamColor, string: component.strings.Message_ScamAccount.uppercased()) + case let .emojiStatus(emojiStatus): + return .animation(content: .customEmoji(fileId: emojiStatus.fileId), size: CGSize(width: 32.0, height: 32.0), placeholderColor: component.theme.list.mediaPlaceholderColor, themeColor: component.theme.list.itemAccentColor, loopMode: .count(2)) + } + } + + var credibilityIconSize: CGSize? + if let titleCredibilityIcon = mapTitleIcon(titleCredibilityIcon) { + let credibilityIcon: ComponentView + if let current = self.credibilityIcon { + credibilityIcon = current + } else { + credibilityIcon = ComponentView() + self.credibilityIcon = credibilityIcon + } + credibilityIconSize = credibilityIcon.update( + transition: .immediate, + component: AnyComponent(EmojiStatusComponent( + context: component.context, + animationCache: component.context.animationCache, + animationRenderer: component.context.animationRenderer, + content: titleCredibilityIcon, + isVisibleForAnimations: true, + action: nil + )), + environment: {}, + containerSize: CGSize(width: 20.0, height: 20.0) + ) + } else if let credibilityIcon = self.credibilityIcon { + self.credibilityIcon = nil + if let credibilityIconView = credibilityIcon.view { + transition.setScale(view: credibilityIconView, scale: 0.001) + transition.setAlpha(view: credibilityIconView, alpha: 0.0, completion: { [weak credibilityIconView] _ in + credibilityIconView?.removeFromSuperview() + }) + } + } + + var statusIconSize: CGSize? + if let titleStatusIcon = mapTitleIcon(titleStatusIcon) { + let statusIcon: ComponentView + if let current = self.statusIcon { + statusIcon = current + } else { + statusIcon = ComponentView() + self.statusIcon = statusIcon + } + statusIconSize = statusIcon.update( + transition: .immediate, + component: AnyComponent(EmojiStatusComponent( + context: component.context, + animationCache: component.context.animationCache, + animationRenderer: component.context.animationRenderer, + content: titleStatusIcon, + isVisibleForAnimations: true, + action: nil + )), + environment: {}, + containerSize: CGSize(width: 20.0, height: 20.0) + ) + } else if let statusIcon = self.statusIcon { + self.statusIcon = nil + if let statusIconView = statusIcon.view { + transition.setScale(view: statusIconView, scale: 0.001) + transition.setAlpha(view: statusIconView, alpha: 0.0, completion: { [weak statusIconView] _ in + statusIconView?.removeFromSuperview() + }) + } + } + + var verifiedIconSize: CGSize? + if let titleVerifiedIcon = mapTitleIcon(titleVerifiedIcon) { + let verifiedIcon: ComponentView + if let current = self.verifiedIcon { + verifiedIcon = current + } else { + verifiedIcon = ComponentView() + self.verifiedIcon = verifiedIcon + } + verifiedIconSize = verifiedIcon.update( + transition: .immediate, + component: AnyComponent(EmojiStatusComponent( + context: component.context, + animationCache: component.context.animationCache, + animationRenderer: component.context.animationRenderer, + content: titleVerifiedIcon, + isVisibleForAnimations: true, + action: nil + )), + environment: {}, + containerSize: CGSize(width: 20.0, height: 20.0) + ) + } else if let verifiedIcon = self.verifiedIcon { + self.verifiedIcon = nil + if let verifiedIconView = verifiedIcon.view { + transition.setScale(view: verifiedIconView, scale: 0.001) + transition.setAlpha(view: verifiedIconView, alpha: 0.0, completion: { [weak verifiedIconView] _ in + verifiedIconView?.removeFromSuperview() + }) + } + } + + let subtitleNode: ChatTitleActivityNode + if let current = self.subtitleNode { + subtitleNode = current + } else { + subtitleNode = ChatTitleActivityNode() + self.subtitleNode = subtitleNode + subtitleNode.isUserInteractionEnabled = false + self.contentContainer.addSubview(subtitleNode.view) + } + + var titleLeftIconsWidth: CGFloat = 0.0 + if let leftIconSize { + titleLeftIconsWidth += leftIconSize.width + leftTitleIconSpacing + } + if let verifiedIconSize { + titleLeftIconsWidth += verifiedIconSize.width + statusIconsSpacing + } + + var titleRightIconsWidth: CGFloat = 0.0 + if let rightIconSize { + titleRightIconsWidth += rightIconSize.width + rightTitleIconSpacing + } + if let credibilityIconSize { + titleRightIconsWidth += credibilityIconSize.width + statusIconsSpacing + } + if let statusIconSize { + titleRightIconsWidth += statusIconSize.width + statusIconsSpacing + } + + let maxTitleWidth = availableSize.width - titleLeftIconsWidth - titleRightIconsWidth - containerSideInset * 2.0 + + let titleSize = self.title.update( + transition: transition, + component: AnyComponent(AnimatedTextComponent( + font: Font.semibold(17.0), + color: component.theme.chat.inputPanel.panelControlColor, + items: titleSegments, + noDelay: true, + animateScale: false, + animateSlide: false, + blur: true + )), + environment: {}, + containerSize: CGSize(width: maxTitleWidth, height: 100.0) + ) + + let _ = subtitleNode.transitionToState(state, animation: transition.animation.isImmediate ? .none : .slide) + let subtitleSize = subtitleNode.updateLayout(CGSize(width: availableSize.width - containerSideInset * 2.0, height: 100.0), alignment: .center) + + var contentSize = titleSize + contentSize.width += titleLeftIconsWidth + titleRightIconsWidth + contentSize.width = max(contentSize.width, subtitleSize.width) + contentSize.height += subtitleSize.height + + let containerSize = CGSize(width: contentSize.width + containerSideInset * 2.0, height: 44.0) + let containerFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((availableSize.width - containerSize.width) * 0.5), y: floorToScreenPixels((availableSize.height - containerSize.height) * 0.5)), size: containerSize) + + var titleFrame = CGRect(origin: CGPoint(x: floor((containerFrame.width - titleSize.width) * 0.5), y: floor((containerFrame.height - contentSize.height) * 0.5)), size: titleSize) + if titleFrame.origin.x + titleFrame.width + titleRightIconsWidth > containerFrame.width - containerSideInset { + titleFrame.origin.x = containerFrame.width - containerSideInset - titleFrame.width - titleRightIconsWidth + } + if titleFrame.origin.x - titleLeftIconsWidth < containerSideInset { + titleFrame.origin.x = containerSideInset + titleLeftIconsWidth + } + if let titleView = self.title.view { + if titleView.superview == nil { + titleView.isUserInteractionEnabled = false + self.contentContainer.addSubview(titleView) + } + transition.setFrame(view: titleView, frame: titleFrame) + } + + let subtitleFrame = CGRect(origin: CGPoint(x: floor((containerFrame.width - subtitleSize.width) * 0.5), y: titleFrame.maxY), size: subtitleSize) + // Internally, the status view has zero width + transition.setFrame(view: subtitleNode.view, frame: CGRect(origin: CGPoint(x: subtitleFrame.midX, y: subtitleFrame.minY), size: CGSize(width: 0.0, height: subtitleFrame.height))) + + var nextLeftIconX: CGFloat = titleFrame.minX + + if let leftIconSize, let leftIconView = self.leftIcon?.view { + let leftIconFrame = CGRect(origin: CGPoint(x: nextLeftIconX - leftTitleIconSpacing - leftIconSize.width, y: titleFrame.minY + leftTitleIconSpacing), size: leftIconSize) + if leftIconView.superview == nil { + leftIconView.isUserInteractionEnabled = false + self.contentContainer.addSubview(leftIconView) + leftIconView.frame = leftIconFrame + ComponentTransition.immediate.setScale(view: leftIconView, scale: 0.001) + leftIconView.alpha = 0.0 + } + transition.setPosition(view: leftIconView, position: leftIconFrame.center) + transition.setBounds(view: leftIconView, bounds: CGRect(origin: CGPoint(), size: leftIconFrame.size)) + transition.setAlpha(view: leftIconView, alpha: 1.0) + transition.setScale(view: leftIconView, scale: 1.0) + } + + if let verifiedIconSize, let verifiedIconView = self.verifiedIcon?.view { + let verifiedIconFrame = CGRect(origin: CGPoint(x: nextLeftIconX - statusIconsSpacing - verifiedIconSize.width, y: titleFrame.minY), size: verifiedIconSize) + if verifiedIconView.superview == nil { + verifiedIconView.isUserInteractionEnabled = false + self.contentContainer.addSubview(verifiedIconView) + verifiedIconView.frame = verifiedIconFrame + ComponentTransition.immediate.setScale(view: verifiedIconView, scale: 0.001) + verifiedIconView.alpha = 0.0 + } + transition.setPosition(view: verifiedIconView, position: verifiedIconFrame.center) + transition.setBounds(view: verifiedIconView, bounds: CGRect(origin: CGPoint(), size: verifiedIconFrame.size)) + transition.setAlpha(view: verifiedIconView, alpha: 1.0) + transition.setScale(view: verifiedIconView, scale: 1.0) + nextLeftIconX -= statusIconsSpacing + verifiedIconSize.width + } + + var nextRightIconX: CGFloat = titleFrame.maxX + + if let credibilityIconSize, let credibilityIconView = self.credibilityIcon?.view { + let credibilityIconFrame = CGRect(origin: CGPoint(x: nextRightIconX + statusIconsSpacing, y: titleFrame.minY), size: credibilityIconSize) + if credibilityIconView.superview == nil { + credibilityIconView.isUserInteractionEnabled = false + self.contentContainer.addSubview(credibilityIconView) + credibilityIconView.frame = credibilityIconFrame + ComponentTransition.immediate.setScale(view: credibilityIconView, scale: 0.001) + credibilityIconView.alpha = 0.0 + } + transition.setPosition(view: credibilityIconView, position: credibilityIconFrame.center) + transition.setBounds(view: credibilityIconView, bounds: CGRect(origin: CGPoint(), size: credibilityIconFrame.size)) + transition.setAlpha(view: credibilityIconView, alpha: 1.0) + transition.setScale(view: credibilityIconView, scale: 1.0) + nextRightIconX += statusIconsSpacing + credibilityIconSize.width + } + + if let statusIconSize, let statusIconView = self.statusIcon?.view { + let statusIconFrame = CGRect(origin: CGPoint(x: nextRightIconX + statusIconsSpacing, y: titleFrame.minY), size: statusIconSize) + if statusIconView.superview == nil { + statusIconView.isUserInteractionEnabled = false + self.contentContainer.addSubview(statusIconView) + statusIconView.frame = statusIconFrame + ComponentTransition.immediate.setScale(view: statusIconView, scale: 0.001) + statusIconView.alpha = 0.0 + } + transition.setPosition(view: statusIconView, position: statusIconFrame.center) + transition.setBounds(view: statusIconView, bounds: CGRect(origin: CGPoint(), size: statusIconFrame.size)) + transition.setAlpha(view: statusIconView, alpha: 1.0) + transition.setScale(view: statusIconView, scale: 1.0) + nextRightIconX += statusIconsSpacing + statusIconSize.width + } + + if let rightIconSize, let rightIconView = self.rightIcon?.view { + let rightIconFrame = CGRect(origin: CGPoint(x: nextRightIconX + rightTitleIconSpacing, y: titleFrame.minY + 5.0), size: rightIconSize) + if rightIconView.superview == nil { + rightIconView.isUserInteractionEnabled = false + self.contentContainer.addSubview(rightIconView) + rightIconView.frame = rightIconFrame + ComponentTransition.immediate.setScale(view: rightIconView, scale: 0.001) + rightIconView.alpha = 0.0 + } + transition.setPosition(view: rightIconView, position: rightIconFrame.center) + transition.setBounds(view: rightIconView, bounds: CGRect(origin: CGPoint(), size: rightIconFrame.size)) + transition.setAlpha(view: rightIconView, alpha: 1.0) + transition.setScale(view: rightIconView, scale: 1.0) + nextRightIconX += rightTitleIconSpacing + rightIconSize.width + } + + if component.displayBackground { + let backgroundView: GlassBackgroundView + if let current = self.backgroundView { + backgroundView = current + } else { + backgroundView = GlassBackgroundView() + self.backgroundView = backgroundView + self.addSubview(backgroundView) + backgroundView.contentView.addSubview(self.contentContainer) + } + transition.setFrame(view: backgroundView, frame: containerFrame) + backgroundView.update(size: containerFrame.size, cornerRadius: containerFrame.height * 0.5, isDark: component.theme.overallDarkAppearance, tintColor: .init(kind: .panel, color: UIColor(white: component.theme.overallDarkAppearance ? 0.0 : 1.0, alpha: 0.6)), isInteractive: isEnabled, transition: transition) + transition.setFrame(view: self.contentContainer, frame: CGRect(origin: CGPoint(), size: containerFrame.size)) + self.contentContainer.layer.cornerRadius = containerFrame.height * 0.5 + } else { + if let backgroundView = self.backgroundView { + self.backgroundView = nil + backgroundView.removeFromSuperview() + self.addSubview(self.contentContainer) + } + transition.setFrame(view: self.contentContainer, frame: containerFrame) + self.contentContainer.layer.cornerRadius = 0.0 + } + + return availableSize + } + } + + public func makeView() -> View { + return View(frame: CGRect()) + } + + public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { + return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) + } +} diff --git a/submodules/TelegramUI/Components/ChatTitleView/Sources/ChatTitleView.swift b/submodules/TelegramUI/Components/ChatTitleView/Sources/ChatTitleView.swift index 70bf826571..07fd3e3423 100644 --- a/submodules/TelegramUI/Components/ChatTitleView/Sources/ChatTitleView.swift +++ b/submodules/TelegramUI/Components/ChatTitleView/Sources/ChatTitleView.swift @@ -14,7 +14,6 @@ import PeerPresenceStatusManager import ChatTitleActivityNode import LocalizedPeerData import PhoneNumberFormat -import ChatTitleActivityNode import AnimatedCountLabelNode import AccountContext import ComponentFlow @@ -23,6 +22,7 @@ import AnimationCache import MultiAnimationRenderer import ComponentDisplayAdapters import GlassBackgroundComponent +import AnimatedTextComponent private let titleFont = Font.with(size: 17.0, design: .regular, weight: .semibold, traits: [.monospacedNumbers]) private let subtitleFont = Font.regular(13.0) @@ -145,13 +145,13 @@ public enum ChatTitleContent: Equatable { } } -private enum ChatTitleIcon { +enum ChatTitleIcon { case none case lock case mute } -private enum ChatTitleCredibilityIcon: Equatable { +enum ChatTitleCredibilityIcon: Equatable { case none case fake case scam @@ -205,7 +205,7 @@ public final class ChatTitleView: UIView, NavigationBarTitleView { private var pointerInteraction: PointerInteraction? - public var inputActivities: (PeerId, [(Peer, PeerInputActivity)])? { + public var inputActivities: ChatTitleComponent.Activities? { didSet { let _ = self.updateStatus() } @@ -506,51 +506,51 @@ public final class ChatTitleView: UIView, NavigationBarTitleView { } state = .info(NSAttributedString(string: infoText, font: subtitleFont, textColor: titleTheme.rootController.navigationBar.secondaryTextColor), .generic) case .online: - if let (peerId, inputActivities) = self.inputActivities, !inputActivities.isEmpty, inputActivitiesAllowed { + if let inputActivities = self.inputActivities, !inputActivities.items.isEmpty, inputActivitiesAllowed { var stringValue = "" - var mergedActivity = inputActivities[0].1 - for (_, activity) in inputActivities { - if activity != mergedActivity { + var mergedActivity = inputActivities.items[0].activity + for item in inputActivities.items { + if item.activity != mergedActivity { mergedActivity = .typingText break } } - if peerId.namespace == Namespaces.Peer.CloudUser || peerId.namespace == Namespaces.Peer.SecretChat { + if inputActivities.peerId.namespace == Namespaces.Peer.CloudUser || inputActivities.peerId.namespace == Namespaces.Peer.SecretChat { switch mergedActivity { - case .typingText: - stringValue = strings.Conversation_typing - case .uploadingFile: - stringValue = strings.Activity_UploadingDocument - case .recordingVoice: - stringValue = strings.Activity_RecordingAudio - case .uploadingPhoto: - stringValue = strings.Activity_UploadingPhoto - case .uploadingVideo: - stringValue = strings.Activity_UploadingVideo - case .playingGame: - stringValue = strings.Activity_PlayingGame - case .recordingInstantVideo: - stringValue = strings.Activity_RecordingVideoMessage - case .uploadingInstantVideo: - stringValue = strings.Activity_UploadingVideoMessage - case .choosingSticker: - stringValue = strings.Activity_ChoosingSticker - case let .seeingEmojiInteraction(emoticon): - stringValue = strings.Activity_EnjoyingAnimations(emoticon).string - case .speakingInGroupCall, .interactingWithEmoji: - stringValue = "" + case .typingText: + stringValue = strings.Conversation_typing + case .uploadingFile: + stringValue = strings.Activity_UploadingDocument + case .recordingVoice: + stringValue = strings.Activity_RecordingAudio + case .uploadingPhoto: + stringValue = strings.Activity_UploadingPhoto + case .uploadingVideo: + stringValue = strings.Activity_UploadingVideo + case .playingGame: + stringValue = strings.Activity_PlayingGame + case .recordingInstantVideo: + stringValue = strings.Activity_RecordingVideoMessage + case .uploadingInstantVideo: + stringValue = strings.Activity_UploadingVideoMessage + case .choosingSticker: + stringValue = strings.Activity_ChoosingSticker + case let .seeingEmojiInteraction(emoticon): + stringValue = strings.Activity_EnjoyingAnimations(emoticon).string + case .speakingInGroupCall, .interactingWithEmoji: + stringValue = "" } } else { - if inputActivities.count > 1 { - let peerTitle = EnginePeer(inputActivities[0].0).compactDisplayTitle - if inputActivities.count == 2 { - let secondPeerTitle = EnginePeer(inputActivities[1].0).compactDisplayTitle - stringValue = strings.Chat_MultipleTypingPair(peerTitle, secondPeerTitle).string + if inputActivities.items.count > 1 { + let peerTitle = inputActivities.items[0].peer.compactDisplayTitle + if inputActivities.items.count == 2 { + let secondPeerTitle = inputActivities.items[1].peer.compactDisplayTitle + stringValue = self.strings.Chat_MultipleTypingPair(peerTitle, secondPeerTitle).string } else { - stringValue = strings.Chat_MultipleTypingMore(peerTitle, String(inputActivities.count - 1)).string + stringValue = self.strings.Chat_MultipleTypingMore(peerTitle, String(inputActivities.items.count - 1)).string } - } else if let (peer, _) = inputActivities.first { - stringValue = EnginePeer(peer).compactDisplayTitle + } else if let item = inputActivities.items.first { + stringValue = item.peer.compactDisplayTitle } } let color = titleTheme.rootController.navigationBar.accentTextColor @@ -839,7 +839,7 @@ public final class ChatTitleView: UIView, NavigationBarTitleView { } } - public func updateLayout(size: CGSize, clearBounds: CGRect, transition: ContainedViewLayoutTransition) -> CGRect { + public func updateLayout(size: CGSize, clearBounds: CGRect, transition: ContainedViewLayoutTransition) { self.validLayout = (size, clearBounds) self.button.frame = clearBounds @@ -1068,8 +1068,6 @@ public final class ChatTitleView: UIView, NavigationBarTitleView { let componentTransition = ComponentTransition(transition) componentTransition.setFrame(view: self.backgroundView, frame: backgroundFrame) self.backgroundView.update(size: backgroundFrame.size, cornerRadius: backgroundFrame.height * 0.5, isDark: self.theme.overallDarkAppearance, tintColor: .init(kind: .panel, color: UIColor(white: self.theme.overallDarkAppearance ? 0.0 : 1.0, alpha: 0.6)), isInteractive: false, transition: componentTransition) - - return titleFrame } @objc private func buttonPressed() { @@ -1144,122 +1142,3 @@ public final class ChatTitleView: UIView, NavigationBarTitleView { snapshotView.layer.animatePosition(from: CGPoint(), to: CGPoint(x: -offset.x, y: -offset.y), 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 { - public private(set) 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, transition: ComponentTransition) -> 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.updateThemeAndStrings(theme: component.theme, strings: component.strings) - - let _ = 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, transition: ComponentTransition) -> CGSize { - return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) - } -} diff --git a/submodules/TelegramUI/Components/ChatTitleView/Sources/TitleIconComponent.swift b/submodules/TelegramUI/Components/ChatTitleView/Sources/TitleIconComponent.swift new file mode 100644 index 0000000000..53f34d7f8c --- /dev/null +++ b/submodules/TelegramUI/Components/ChatTitleView/Sources/TitleIconComponent.swift @@ -0,0 +1,96 @@ +import Foundation +import UIKit +import Display +import ComponentFlow +import TelegramPresentationData + +final class TitleIconComponent: Component { + enum Kind { + case mute + case lock + } + + let kind: Kind + let color: UIColor + + init( + kind: Kind, + color: UIColor + ) { + self.kind = kind + self.color = color + } + + static func ==(lhs: TitleIconComponent, rhs: TitleIconComponent) -> Bool { + if lhs.kind != rhs.kind { + return false + } + if lhs.color != rhs.color { + return false + } + return true + } + + final class View: UIView { + let iconView: UIImageView + + var component: TitleIconComponent? + + override init(frame: CGRect) { + self.iconView = UIImageView() + + super.init(frame: frame) + + self.addSubview(self.iconView) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + func update(component: TitleIconComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { + if self.component?.kind != component.kind { + switch component.kind { + case .mute: + self.iconView.image = generateImage(CGSize(width: 9.0, height: 9.0), rotatedContext: { size, context in + context.clear(CGRect(origin: CGPoint(), size: size)) + context.setFillColor(UIColor.white.cgColor) + + let _ = try? drawSvgPath(context, path: "M2.97607626,2.27306995 L5.1424026,0.18411241 C5.25492443,0.0756092198 5.40753677,0.0146527621 5.56666667,0.0146527621 C5.89803752,0.0146527621 6.16666667,0.273688014 6.16666667,0.593224191 L6.16666667,5.47790407 L8.86069303,8.18395735 C9.05193038,8.37604845 9.04547086,8.68126082 8.84626528,8.86566828 C8.6470597,9.05007573 8.33054317,9.0438469 8.13930581,8.85175581 L0.139306972,0.816042647 C-0.0519303838,0.623951552 -0.0454708626,0.318739175 0.153734717,0.134331724 C0.352940296,-0.0500757275 0.669456833,-0.0438469035 0.860694189,0.148244192 L2.97607626,2.27306995 Z M0.933196438,2.75856564 L6.16666667,8.01539958 L6.16666667,8.40677707 C6.16666667,8.56022375 6.10345256,8.70738566 5.99093074,8.81588885 C5.75661616,9.04183505 5.37671717,9.04183505 5.1424026,8.81588885 L2.59763107,6.36200202 C2.53511895,6.30172247 2.45033431,6.26785777 2.36192881,6.26785777 L1.16666667,6.26785777 C0.614381917,6.26785777 0.166666667,5.83613235 0.166666667,5.30357206 L0.166666667,3.6964292 C0.166666667,3.24138962 0.493527341,2.85996592 0.933196438,2.75856564 Z ") + })?.withRenderingMode(.alwaysTemplate) + case .lock: + self.iconView.image = generateImage(CGSize(width: 9.0, height: 13.0), rotatedContext: { size, context in + context.clear(CGRect(origin: CGPoint(), size: size)) + + context.translateBy(x: 0.0, y: 1.0) + + context.setFillColor(UIColor.white.cgColor) + context.setStrokeColor(UIColor.white.cgColor) + context.setLineWidth(1.32) + + let _ = try? drawSvgPath(context, path: "M4.5,0.600000024 C5.88071187,0.600000024 7,1.88484952 7,3.46979169 L7,7.39687502 C7,8.9818172 5.88071187,10.2666667 4.5,10.2666667 C3.11928813,10.2666667 2,8.9818172 2,7.39687502 L2,3.46979169 C2,1.88484952 3.11928813,0.600000024 4.5,0.600000024 S ") + let _ = try? drawSvgPath(context, path: "M1.32,5.65999985 L7.68,5.65999985 C8.40901587,5.65999985 9,6.25098398 9,6.97999985 L9,10.6733332 C9,11.4023491 8.40901587,11.9933332 7.68,11.9933332 L1.32,11.9933332 C0.59098413,11.9933332 1.11022302e-16,11.4023491 0,10.6733332 L2.22044605e-16,6.97999985 C1.11022302e-16,6.25098398 0.59098413,5.65999985 1.32,5.65999985 Z ") + })?.withRenderingMode(.alwaysTemplate) + } + } + + let size = CGSize(width: 14.0, height: 14.0) + + if let image = self.iconView.image { + let iconFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((size.width - image.size.width) * 0.5), y: floorToScreenPixels((size.height - image.size.height) * 0.5)), size: image.size) + self.iconView.frame = iconFrame + } + self.iconView.tintColor = component.color + + return size + } + } + + func makeView() -> View { + return View(frame: CGRect()) + } + + func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { + return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) + } +} diff --git a/submodules/TelegramUI/Components/NavigationBarImpl/Sources/NavigationBarImpl.swift b/submodules/TelegramUI/Components/NavigationBarImpl/Sources/NavigationBarImpl.swift index 6ef8d6419c..7d84c646a4 100644 --- a/submodules/TelegramUI/Components/NavigationBarImpl/Sources/NavigationBarImpl.swift +++ b/submodules/TelegramUI/Components/NavigationBarImpl/Sources/NavigationBarImpl.swift @@ -959,7 +959,7 @@ public final class NavigationBarImpl: ASDisplayNode, NavigationBar { if let titleView = titleView as? NavigationBarTitleView { let titleWidth = size.width - (leftTitleInset > 0.0 ? leftTitleInset : rightTitleInset) - (rightTitleInset > 0.0 ? rightTitleInset : leftTitleInset) - let _ = titleView.updateLayout(size: titleFrame.size, clearBounds: CGRect(origin: CGPoint(x: leftTitleInset - titleFrame.minX, y: 0.0), size: CGSize(width: titleWidth, height: titleFrame.height)), transition: titleViewTransition) + titleView.updateLayout(size: titleFrame.size, clearBounds: CGRect(origin: CGPoint(x: leftTitleInset - titleFrame.minX, y: 0.0), size: CGSize(width: titleWidth, height: titleFrame.height)), transition: titleViewTransition) } do { @@ -1135,7 +1135,7 @@ public final class NavigationBarImpl: ASDisplayNode, NavigationBar { return nil } - if self.passthroughTouches && (result == self.view || result == self.buttonsContainerNode.view) { + if self.passthroughTouches && (result == self.view || result == self.buttonsContainerNode.view || result == self.backgroundNode.view || result == self.backgroundNode.backgroundView) { return nil } diff --git a/submodules/TelegramUI/Sources/Chat/ChatControllerLoadDisplayNode.swift b/submodules/TelegramUI/Sources/Chat/ChatControllerLoadDisplayNode.swift index 13110275e8..b531ec14e3 100644 --- a/submodules/TelegramUI/Sources/Chat/ChatControllerLoadDisplayNode.swift +++ b/submodules/TelegramUI/Sources/Chat/ChatControllerLoadDisplayNode.swift @@ -125,6 +125,8 @@ import PostSuggestionsSettingsScreen import ChatSendStarsScreen import ChatSendAsContextMenu import GlobalControlPanelsContext +import ComponentFlow +import ComponentDisplayAdapters extension ChatControllerImpl { func reloadChatLocation(chatLocation: ChatLocation, chatLocationContextHolder: Atomic, historyNode: ChatHistoryListNodeImpl, apply: @escaping ((ContainedViewLayoutTransition?) -> Void) -> Void) { @@ -229,17 +231,6 @@ extension ChatControllerImpl { } self.navigationBar?.userInfo = contentData.state.navigationUserInfo - if self.chatTitleView?.titleContent != contentData.state.chatTitleContent { - var animateTitleContents = false - if !synchronous, case let .messageOptions(_, _, info) = self.subject, case .reply = info { - animateTitleContents = true - } - if animateTitleContents && self.chatTitleView?.titleContent != nil { - self.chatTitleView?.animateLayoutTransition() - } - self.chatTitleView?.titleContent = contentData.state.chatTitleContent - } - if let infoAvatar = contentData.state.infoAvatar { switch infoAvatar { case let .peer(peer, imageOverride, contextActionIsEnabled, accessibilityLabel): @@ -385,6 +376,9 @@ extension ChatControllerImpl { if previousState.translationState?.isEnabled != contentData.state.translationState?.isEnabled { animated = true } + if previousState.chatTitleContent != contentData.state.chatTitleContent { + animated = true + } var transition: ContainedViewLayoutTransition = animated ? .animated(duration: 0.4, curve: .spring) : .immediate if let forceAnimationTransition { @@ -394,6 +388,22 @@ extension ChatControllerImpl { transition = .immediate } + if let chatTitleContent = contentData.state.chatTitleContent { + var titleTransition = ComponentTransition(transition) + if case .messageOptions = self.subject { + titleTransition = titleTransition.withAnimation(.none) + } + self.chatTitleView?.update( + context: self.context, + theme: self.presentationData.theme, + strings: self.presentationData.strings, + dateTimeFormat: self.presentationData.dateTimeFormat, + nameDisplayOrder: self.presentationData.nameDisplayOrder, + content: chatTitleContent, + transition: titleTransition + ) + } + self.updateChatPresentationInterfaceState(transition: transition, interactive: false, { presentationInterfaceState in var presentationInterfaceState = presentationInterfaceState presentationInterfaceState = presentationInterfaceState.updatedPeer({ _ in @@ -4787,7 +4797,18 @@ extension ChatControllerImpl { return true } }) - strongSelf.chatTitleView?.inputActivities = (peerId, displayActivities) + strongSelf.chatTitleView?.updateActivities( + activities: ChatTitleComponent.Activities( + peerId: peerId, + items: displayActivities.map { item -> ChatTitleComponent.Activities.Item in + return ChatTitleComponent.Activities.Item( + peer: EnginePeer(item.0), + activity: item.1 + ) + } + ), + transition: .spring(duration: 0.4) + ) strongSelf.peerInputActivitiesPromise.set(.single(activities)) diff --git a/submodules/TelegramUI/Sources/ChatController.swift b/submodules/TelegramUI/Sources/ChatController.swift index e42dca63ef..608fb13665 100644 --- a/submodules/TelegramUI/Sources/ChatController.swift +++ b/submodules/TelegramUI/Sources/ChatController.swift @@ -295,7 +295,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G let chatThemePromise = Promise() let chatWallpaperPromise = Promise() - var chatTitleView: ChatTitleView? + var chatTitleView: ChatNavigationBarTitleView? var leftNavigationButton: ChatNavigationButton? var rightNavigationButton: ChatNavigationButton? var secondaryRightNavigationButton: ChatNavigationButton? @@ -5139,14 +5139,10 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G return true } - self.chatTitleView = ChatTitleView(context: self.context, theme: self.presentationData.theme, strings: self.presentationData.strings, dateTimeFormat: self.presentationData.dateTimeFormat, nameDisplayOrder: self.presentationData.nameDisplayOrder, animationCache: controllerInteraction.presentationContext.animationCache, animationRenderer: controllerInteraction.presentationContext.animationRenderer) - - if case .messageOptions = self.subject { - self.chatTitleView?.disableAnimations = true - } + self.chatTitleView = ChatNavigationBarTitleView(frame: CGRect()) self.navigationItem.titleView = self.chatTitleView - self.chatTitleView?.longPressed = { [weak self] in + self.chatTitleView?.longTapAction = { [weak self] in if let strongSelf = self, let peerView = strongSelf.contentData?.state.peerView, let peer = peerView.peers[peerView.peerId], peer.restrictionText(platform: "ios", contentSettings: strongSelf.context.currentContentSettings.with { $0 }) == nil && !strongSelf.presentationInterfaceState.isNotAccessible { if case .standard(.previewing) = strongSelf.mode { } else { @@ -5532,7 +5528,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G self.moreBarButton.addTarget(self, action: #selector(self.moreButtonPressed), forControlEvents: .touchUpInside) self.navigationItem.titleView = self.chatTitleView - self.chatTitleView?.pressed = { [weak self] in + self.chatTitleView?.tapAction = { [weak self] in self?.navigationButtonAction(.openChatInfo(expandAvatar: false, section: nil)) } @@ -6098,7 +6094,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G self.networkStateDisposable = (context.account.networkState |> deliverOnMainQueue).startStrict(next: { [weak self] state in if let strongSelf = self, case .standard(.default) = strongSelf.presentationInterfaceState.mode { - strongSelf.chatTitleView?.networkState = state + strongSelf.chatTitleView?.updateNetworkState(networkState: state, transition: .spring(duration: 0.4)) } }) @@ -6331,8 +6327,6 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G self.navigationBar?.updatePresentationData(NavigationBarPresentationData(theme: navigationBarTheme, strings: NavigationBarStrings(presentationStrings: self.presentationData.strings)), transition: .immediate) - self.chatTitleView?.updateThemeAndStrings(theme: presentationTheme, strings: self.presentationData.strings) - self.moreBarButton.updateColor(color: presentationTheme.chat.inputPanel.panelControlColor) } @@ -7197,7 +7191,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G self.storedAnimateFromSnapshotState = nil if let titleViewSnapshotState = snapshotState.titleViewSnapshotState { - self.chatTitleView?.animateFromSnapshot(titleViewSnapshotState) + self.chatTitleView?.animateFromSnapshot(titleViewSnapshotState, direction: .up) } if let avatarSnapshotState = snapshotState.avatarSnapshotState { (self.chatInfoNavigationButton?.buttonItem.customDisplayNode as? ChatAvatarNavigationNode)?.animateFromSnapshot(avatarSnapshotState) @@ -7396,7 +7390,6 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G super.containerLayoutUpdated(layout, transition: transition) self.validLayout = layout - self.chatTitleView?.layout = layout switch self.presentationInterfaceState.mode { case .standard, .inline: diff --git a/submodules/TelegramUI/Sources/ChatControllerEditChat.swift b/submodules/TelegramUI/Sources/ChatControllerEditChat.swift index 50f1593cef..85b0428962 100644 --- a/submodules/TelegramUI/Sources/ChatControllerEditChat.swift +++ b/submodules/TelegramUI/Sources/ChatControllerEditChat.swift @@ -48,7 +48,16 @@ extension ChatControllerImpl { contentNode.setErrorText(errorText: self.presentationData.strings.QuickReply_ShortcutExistsInlineError) } } else { - self.chatTitleView?.titleContent = .custom("\(value)", nil, false) + self.chatTitleView?.update( + context: self.context, + theme: self.presentationData.theme, + strings: self.presentationData.strings, + dateTimeFormat: self.presentationData.dateTimeFormat, + nameDisplayOrder: self.presentationData.nameDisplayOrder, + content: .custom("\(value)", nil, false), + transition: .immediate + ) + alertController?.view.endEditing(true) alertController?.dismissAnimated() @@ -93,7 +102,15 @@ extension ChatControllerImpl { } else { linkUrl = link.url } - self.chatTitleView?.titleContent = .custom(value.isEmpty ? self.presentationData.strings.Business_Links_EditLinkTitle : value, linkUrl, false) + self.chatTitleView?.update( + context: self.context, + theme: self.presentationData.theme, + strings: self.presentationData.strings, + dateTimeFormat: self.presentationData.dateTimeFormat, + nameDisplayOrder: self.presentationData.nameDisplayOrder, + content: .custom(value.isEmpty ? self.presentationData.strings.Business_Links_EditLinkTitle : value, linkUrl, false), + transition: .immediate + ) if case let .customChatContents(customChatContents) = self.subject { customChatContents.businessLinkUpdate(message: link.message, entities: link.entities, title: value.isEmpty ? nil : value) }