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 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 parentTitleState = ComponentState() private let title = ComponentView() private var contentData: ContentData? private var activities: ChatTitleComponent.Activities? private var networkState: AccountNetworkState? public var requestUpdate: ((ContainedViewLayoutTransition) -> Void)? 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) { self.requestUpdate?(transition.containedViewLayoutTransition) } public func updateLayout(availableSize: CGSize, transition: ContainedViewLayoutTransition) -> CGSize { let transition = ComponentTransition(transition) if let contentData = self.contentData { let titleSize = 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: availableSize ) if let titleView = self.title.view { if titleView.superview == nil { self.title.parentState = self.parentTitleState self.parentTitleState._updated = { [weak self] transition, _ in guard let self else { return } self.requestUpdate?(transition.containedViewLayoutTransition) } self.addSubview(titleView) } transition.setFrame(view: titleView, frame: CGRect(origin: CGPoint(), size: titleSize)) } return titleSize } else { return availableSize } } } 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(textItems, _, enabled): titleSegments = textItems.map { item -> AnimatedTextComponent.Item in let mappedContent: AnimatedTextComponent.Item.Content switch item.content { case let .number(value, minDigits): mappedContent = .number(value, minDigits: minDigits) case let .text(text): mappedContent = .text(text) } return AnimatedTextComponent.Item( id: item.id, isUnbreakable: item.isUnbreakable, content: mappedContent ) } 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: false, animateScale: true, animateSlide: true, 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: 0.0, y: floorToScreenPixels((availableSize.height - containerSize.height) * 0.5)), size: containerSize) let titleFrame = CGRect(origin: CGPoint(x: titleLeftIconsWidth + floor((containerFrame.width - titleSize.width - titleLeftIconsWidth - titleRightIconsWidth) * 0.5), y: floor((containerFrame.height - contentSize.height) * 0.5)), size: titleSize) 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() } if self.contentContainer.superview !== self { self.addSubview(self.contentContainer) } transition.setFrame(view: self.contentContainer, frame: containerFrame) self.contentContainer.layer.cornerRadius = 0.0 } return CGSize(width: containerSize.width, height: availableSize.height) } } 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) } }