diff --git a/Telegram/Telegram-iOS/en.lproj/Localizable.strings b/Telegram/Telegram-iOS/en.lproj/Localizable.strings index df972785cc..93633ba923 100644 --- a/Telegram/Telegram-iOS/en.lproj/Localizable.strings +++ b/Telegram/Telegram-iOS/en.lproj/Localizable.strings @@ -264,6 +264,7 @@ "PUSH_MESSAGE_SAME_WALLPAPER" = "%1$@ set the same wallpaper for the chat with you"; "PUSH_MESSAGE_UNIQUE_STARGIFT" = "%1$@ sent you a Gift"; +"PUSH_MESSAGE_STARGIFT_UPGRADE" = "%1$@ upgraded your Gift"; "PUSH_REMINDER_TITLE" = "🗓 Reminder"; "PUSH_SENDER_YOU" = "📅 You"; diff --git a/submodules/ChatListUI/Sources/Node/ChatListItem.swift b/submodules/ChatListUI/Sources/Node/ChatListItem.swift index 42b2ea76bc..1255aaea6c 100644 --- a/submodules/ChatListUI/Sources/Node/ChatListItem.swift +++ b/submodules/ChatListUI/Sources/Node/ChatListItem.swift @@ -1266,6 +1266,8 @@ public class ChatListItemNode: ItemListRevealOptionsItemNode { var verifiedIconComponent: EmojiStatusComponent? var credibilityIconView: ComponentHostView? var credibilityIconComponent: EmojiStatusComponent? + var statusIconView: ComponentHostView? + var statusIconComponent: EmojiStatusComponent? let mutedIconNode: ASImageNode var itemTagList: ComponentView? var actionButtonTitleNode: TextNode? @@ -2142,6 +2144,7 @@ public class ChatListItemNode: ItemListRevealOptionsItemNode { var currentMutedIconImage: UIImage? var currentCredibilityIconContent: EmojiStatusComponent.Content? var currentVerifiedIconContent: EmojiStatusComponent.Content? + var currentStatusIconContent: EmojiStatusComponent.Content? var currentSecretIconImage: UIImage? var currentForwardedIcon: UIImage? var currentStoryIcon: UIImage? @@ -3098,7 +3101,7 @@ public class ChatListItemNode: ItemListRevealOptionsItemNode { } else if peer.isFake { currentCredibilityIconContent = .text(color: item.presentationData.theme.chat.message.incoming.scamColor, string: item.presentationData.strings.Message_FakeAccount.uppercased()) } else if let emojiStatus = peer.emojiStatus, !premiumConfiguration.isPremiumDisabled { - currentCredibilityIconContent = .animation(content: .customEmoji(fileId: emojiStatus.fileId), size: CGSize(width: 32.0, height: 32.0), placeholderColor: item.presentationData.theme.list.mediaPlaceholderColor, themeColor: item.presentationData.theme.list.itemAccentColor, loopMode: .count(2)) + currentStatusIconContent = .animation(content: .customEmoji(fileId: emojiStatus.fileId), size: CGSize(width: 32.0, height: 32.0), placeholderColor: item.presentationData.theme.list.mediaPlaceholderColor, themeColor: item.presentationData.theme.list.itemAccentColor, loopMode: .count(2)) } else if peer.isPremium && !premiumConfiguration.isPremiumDisabled { currentCredibilityIconContent = .premium(color: item.presentationData.theme.list.itemAccentColor) } @@ -3126,7 +3129,7 @@ public class ChatListItemNode: ItemListRevealOptionsItemNode { } else if peer.isFake { currentCredibilityIconContent = .text(color: item.presentationData.theme.chat.message.incoming.scamColor, string: item.presentationData.strings.Message_FakeAccount.uppercased()) } else if let emojiStatus = peer.emojiStatus, !premiumConfiguration.isPremiumDisabled { - currentCredibilityIconContent = .animation(content: .customEmoji(fileId: emojiStatus.fileId), size: CGSize(width: 32.0, height: 32.0), placeholderColor: item.presentationData.theme.list.mediaPlaceholderColor, themeColor: item.presentationData.theme.list.itemAccentColor, loopMode: .count(2)) + currentStatusIconContent = .animation(content: .customEmoji(fileId: emojiStatus.fileId), size: CGSize(width: 32.0, height: 32.0), placeholderColor: item.presentationData.theme.list.mediaPlaceholderColor, themeColor: item.presentationData.theme.list.itemAccentColor, loopMode: .count(2)) } else if peer.isPremium && !premiumConfiguration.isPremiumDisabled { currentCredibilityIconContent = .premium(color: item.presentationData.theme.list.itemAccentColor) } @@ -3180,6 +3183,22 @@ public class ChatListItemNode: ItemListRevealOptionsItemNode { } } + if let currentStatusIconContent { + if titleIconsWidth.isZero { + titleIconsWidth += 4.0 + } else { + titleIconsWidth += 2.0 + } + switch currentStatusIconContent { + case let .text(_, string): + let textString = NSAttributedString(string: string, font: Font.bold(10.0), textColor: .black, paragraphAlignment: .center) + let stringRect = textString.boundingRect(with: CGSize(width: 100.0, height: 16.0), options: .usesLineFragmentOrigin, context: nil) + titleIconsWidth += floor(stringRect.width) + 11.0 + default: + titleIconsWidth += 8.0 + } + } + let layoutOffset: CGFloat = 0.0 let rawContentWidth = params.width - leftInset - params.rightInset - 10.0 - editingOffset @@ -4514,6 +4533,41 @@ public class ChatListItemNode: ItemListRevealOptionsItemNode { } else { lastLineRect = CGRect(origin: CGPoint(), size: titleLayout.size) } + + if let currentStatusIconContent { + let statusIconView: ComponentHostView + if let current = strongSelf.statusIconView { + statusIconView = current + } else { + statusIconView = ComponentHostView() + strongSelf.statusIconView = statusIconView + strongSelf.mainContentContainerNode.view.addSubview(statusIconView) + } + + let statusIconComponent = EmojiStatusComponent( + context: item.context, + animationCache: item.interaction.animationCache, + animationRenderer: item.interaction.animationRenderer, + content: currentStatusIconContent, + isVisibleForAnimations: strongSelf.visibilityStatus && item.context.sharedContext.energyUsageSettings.loopEmoji, + action: nil + ) + strongSelf.statusIconComponent = statusIconComponent + + let iconOrigin: CGFloat = nextTitleIconOrigin + let containerSize = CGSize(width: 20.0, height: 20.0) + let iconSize = statusIconView.update( + transition: .immediate, + component: AnyComponent(statusIconComponent), + environment: {}, + containerSize: containerSize + ) + transition.updateFrame(view: statusIconView, frame: CGRect(origin: CGPoint(x: iconOrigin, y: floorToScreenPixels(titleFrame.maxY - lastLineRect.height * 0.5 - iconSize.height / 2.0) - UIScreenPixel), size: iconSize)) + nextTitleIconOrigin += statusIconView.bounds.width + 4.0 + } else if let statusIconView = strongSelf.statusIconView { + strongSelf.statusIconView = nil + statusIconView.removeFromSuperview() + } if let currentCredibilityIconContent { let credibilityIconView: ComponentHostView diff --git a/submodules/TelegramUI/Components/ChatTitleView/Sources/ChatTitleView.swift b/submodules/TelegramUI/Components/ChatTitleView/Sources/ChatTitleView.swift index 213efc4f53..00f6f152a7 100644 --- a/submodules/TelegramUI/Components/ChatTitleView/Sources/ChatTitleView.swift +++ b/submodules/TelegramUI/Components/ChatTitleView/Sources/ChatTitleView.swift @@ -950,12 +950,13 @@ public final class ChatTitleView: UIView, NavigationBarTitleView { titleTransition = .immediate } + let iconSpacing: CGFloat = 2.0 let titleSideInset: CGFloat = 6.0 var titleFrame: CGRect if size.height > 40.0 { var titleInsets: UIEdgeInsets = .zero if case .emojiStatus = self.titleVerifiedIcon, verifiedIconWidth > 0.0 { - titleInsets.left = verifiedIconWidth + 2.0 + titleInsets.left = verifiedIconWidth + iconSpacing } var titleSize = self.titleTextNode.updateLayout(size: CGSize(width: clearBounds.width - leftIconWidth - credibilityIconWidth - verifiedIconWidth - statusIconWidth - rightIconWidth - titleSideInset * 2.0, height: size.height), insets: titleInsets, animated: titleTransition.isAnimated) diff --git a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoHeaderNode.swift b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoHeaderNode.swift index b9fca53ec9..10aaec713f 100644 --- a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoHeaderNode.swift +++ b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoHeaderNode.swift @@ -123,6 +123,11 @@ final class PeerInfoHeaderNode: ASDisplayNode { let titleExpandedVerifiedIconView: ComponentHostView var titleExpandedVerifiedIconSize: CGSize? + let titleStatusIconView: ComponentHostView + var statusIconSize: CGSize? + let titleExpandedStatusIconView: ComponentHostView + var titleExpandedStatusIconSize: CGSize? + let subtitleNodeContainer: ASDisplayNode let subtitleNodeRawContainer: ASDisplayNode let subtitleNode: MultiScaleTextNode @@ -217,6 +222,12 @@ final class PeerInfoHeaderNode: ASDisplayNode { self.titleExpandedVerifiedIconView = ComponentHostView() self.titleNode.stateNode(forKey: TitleNodeStateExpanded)?.view.addSubview(self.titleExpandedVerifiedIconView) + self.titleStatusIconView = ComponentHostView() + self.titleNode.stateNode(forKey: TitleNodeStateRegular)?.view.addSubview(self.titleStatusIconView) + + self.titleExpandedStatusIconView = ComponentHostView() + self.titleNode.stateNode(forKey: TitleNodeStateExpanded)?.view.addSubview(self.titleExpandedStatusIconView) + self.subtitleNodeContainer = ASDisplayNode() self.subtitleNodeRawContainer = ASDisplayNode() self.subtitleNode = MultiScaleTextNode(stateKeys: [TitleNodeStateRegular, TitleNodeStateExpanded]) @@ -465,6 +476,7 @@ final class PeerInfoHeaderNode: ASDisplayNode { private var currentCredibilityIcon: CredibilityIcon? private var currentVerifiedIcon: CredibilityIcon? + private var currentStatusIcon: CredibilityIcon? private var currentPanelStatusData: PeerInfoStatusData? func update(width: CGFloat, containerHeight: CGFloat, containerInset: CGFloat, statusBarHeight: CGFloat, navigationHeight: CGFloat, isModalOverlay: Bool, isMediaOnly: Bool, contentOffset: CGFloat, paneContainerY: CGFloat, presentationData: PresentationData, peer: Peer?, cachedData: CachedPeerData?, threadData: MessageHistoryThreadData?, peerNotificationSettings: TelegramPeerNotificationSettings?, threadNotificationSettings: TelegramPeerNotificationSettings?, globalNotificationSettings: EngineGlobalNotificationSettings?, statusData: PeerInfoStatusData?, panelStatusData: (PeerInfoStatusData?, PeerInfoStatusData?, CGFloat?), isSecretChat: Bool, isContact: Bool, isSettings: Bool, state: PeerInfoState, metrics: LayoutMetrics, deviceMetrics: DeviceMetrics, transition: ContainedViewLayoutTransition, additive: Bool, animateHeader: Bool) -> CGFloat { @@ -521,9 +533,10 @@ final class PeerInfoHeaderNode: ASDisplayNode { self.presentationData = presentationData let premiumConfiguration = PremiumConfiguration.with(appConfiguration: self.context.currentAppConfiguration.with { $0 }) - var credibilityIcon: CredibilityIcon + var credibilityIcon: CredibilityIcon = .none var verifiedIcon: CredibilityIcon = .none - if let peer = peer { + var statusIcon: CredibilityIcon = .none + if let peer { if peer.id == self.context.account.peerId && !self.isSettings && !self.isMyProfile { credibilityIcon = .none } else if peer.isFake { @@ -531,7 +544,7 @@ final class PeerInfoHeaderNode: ASDisplayNode { } else if peer.isScam { credibilityIcon = .scam } else if let emojiStatus = peer.emojiStatus, !premiumConfiguration.isPremiumDisabled { - credibilityIcon = .emojiStatus(emojiStatus) + statusIcon = .emojiStatus(emojiStatus) } else if peer.isPremium && !premiumConfiguration.isPremiumDisabled && (peer.id != self.context.account.peerId || self.isSettings || self.isMyProfile) { credibilityIcon = .premium } else { @@ -543,8 +556,6 @@ final class PeerInfoHeaderNode: ASDisplayNode { if let verificationIconFileId = peer.verificationIconFileId { verifiedIcon = .emojiStatus(PeerEmojiStatus(fileId: verificationIconFileId, expirationDate: nil)) } - } else { - credibilityIcon = .none } var isForum = false @@ -808,7 +819,70 @@ final class PeerInfoHeaderNode: ASDisplayNode { guard let strongSelf = self else { return } - strongSelf.displayPremiumIntro?(strongSelf.titleCredibilityIconView, currentEmojiStatus, strongSelf.emojiStatusFileAndPackTitle.get(), false) + if case .premium = strongSelf.currentCredibilityIcon { + strongSelf.displayPremiumIntro?(strongSelf.titleCredibilityIconView, currentEmojiStatus, strongSelf.emojiStatusFileAndPackTitle.get(), false) + } + } + )), + environment: {}, + containerSize: CGSize(width: 26.0, height: 26.0) + ) + let expandedIconSize = self.titleExpandedCredibilityIconView.update( + transition: ComponentTransition(navigationTransition), + component: AnyComponent(EmojiStatusComponent( + context: self.context, + animationCache: self.animationCache, + animationRenderer: self.animationRenderer, + content: emojiExpandedStatusContent, + isVisibleForAnimations: true, + useSharedAnimation: true, + action: { [weak self] in + guard let strongSelf = self else { + return + } + if case .premium = strongSelf.currentCredibilityIcon { + strongSelf.displayPremiumIntro?(strongSelf.titleExpandedCredibilityIconView, currentEmojiStatus, strongSelf.emojiStatusFileAndPackTitle.get(), true) + } + } + )), + environment: {}, + containerSize: CGSize(width: 26.0, height: 26.0) + ) + + self.credibilityIconSize = iconSize + self.titleExpandedCredibilityIconSize = expandedIconSize + } + + do { + self.currentStatusIcon = statusIcon + + var currentEmojiStatus: PeerEmojiStatus? + let emojiRegularStatusContent: EmojiStatusComponent.Content + let emojiExpandedStatusContent: EmojiStatusComponent.Content + switch statusIcon { + case let .emojiStatus(emojiStatus): + currentEmojiStatus = emojiStatus + emojiRegularStatusContent = .animation(content: .customEmoji(fileId: emojiStatus.fileId), size: CGSize(width: 80.0, height: 80.0), placeholderColor: presentationData.theme.list.mediaPlaceholderColor, themeColor: navigationContentsAccentColor, loopMode: .forever) + emojiExpandedStatusContent = .animation(content: .customEmoji(fileId: emojiStatus.fileId), size: CGSize(width: 80.0, height: 80.0), placeholderColor: navigationContentsAccentColor, themeColor: navigationContentsAccentColor, loopMode: .forever) + default: + emojiRegularStatusContent = .none + emojiExpandedStatusContent = .none + } + + let iconSize = self.titleStatusIconView.update( + transition: ComponentTransition(navigationTransition), + component: AnyComponent(EmojiStatusComponent( + context: self.context, + animationCache: self.animationCache, + animationRenderer: self.animationRenderer, + content: emojiRegularStatusContent, + isVisibleForAnimations: true, + useSharedAnimation: true, + action: { [weak self] in + guard let strongSelf = self else { + return + } + strongSelf.displayPremiumIntro?(strongSelf.titleStatusIconView, currentEmojiStatus, strongSelf.emojiStatusFileAndPackTitle.get(), false) }, emojiFileUpdated: { [weak self] emojiFile in guard let strongSelf = self else { @@ -851,7 +925,7 @@ final class PeerInfoHeaderNode: ASDisplayNode { environment: {}, containerSize: CGSize(width: 26.0, height: 26.0) ) - let expandedIconSize = self.titleExpandedCredibilityIconView.update( + let expandedIconSize = self.titleExpandedStatusIconView.update( transition: ComponentTransition(navigationTransition), component: AnyComponent(EmojiStatusComponent( context: self.context, @@ -864,15 +938,15 @@ final class PeerInfoHeaderNode: ASDisplayNode { guard let strongSelf = self else { return } - strongSelf.displayPremiumIntro?(strongSelf.titleExpandedCredibilityIconView, currentEmojiStatus, strongSelf.emojiStatusFileAndPackTitle.get(), true) + strongSelf.displayPremiumIntro?(strongSelf.titleExpandedStatusIconView, currentEmojiStatus, strongSelf.emojiStatusFileAndPackTitle.get(), true) } )), environment: {}, containerSize: CGSize(width: 26.0, height: 26.0) ) - self.credibilityIconSize = iconSize - self.titleExpandedCredibilityIconSize = expandedIconSize + self.statusIconSize = iconSize + self.titleExpandedStatusIconSize = expandedIconSize } do { @@ -1307,6 +1381,25 @@ final class PeerInfoHeaderNode: ASDisplayNode { var nextIconX: CGFloat = titleSize.width var nextExpandedIconX: CGFloat = titleExpandedSize.width + if let statusIconSize = self.statusIconSize, let titleExpandedStatusIconSize = self.titleExpandedStatusIconSize, statusIconSize.width > 0.0 { + let offset = (statusIconSize.width + 4.0) / 2.0 + + let leftOffset: CGFloat = nextIconX + 4.0 + let leftExpandedOffset: CGFloat = nextExpandedIconX + 4.0 + titleHorizontalOffset -= offset + + var collapsedTransitionOffset: CGFloat = 0.0 + if let navigationTransition = self.navigationTransition { + collapsedTransitionOffset = -10.0 * navigationTransition.fraction + } + + transition.updateFrame(view: self.titleStatusIconView, frame: CGRect(origin: CGPoint(x: leftOffset + collapsedTransitionOffset, y: floor((titleSize.height - statusIconSize.height) / 2.0)), size: statusIconSize)) + transition.updateFrame(view: self.titleExpandedStatusIconView, frame: CGRect(origin: CGPoint(x: leftExpandedOffset, y: floor((titleExpandedSize.height - titleExpandedStatusIconSize.height) / 2.0) + 1.0), size: titleExpandedStatusIconSize)) + + nextIconX += 4.0 + statusIconSize.width + nextExpandedIconX += 4.0 + titleExpandedStatusIconSize.width + } + if let credibilityIconSize = self.credibilityIconSize, let titleExpandedCredibilityIconSize = self.titleExpandedCredibilityIconSize, credibilityIconSize.width > 0.0 { let offset = (credibilityIconSize.width + 4.0) / 2.0 @@ -1325,7 +1418,7 @@ final class PeerInfoHeaderNode: ASDisplayNode { nextIconX += 4.0 + credibilityIconSize.width nextExpandedIconX += 4.0 + titleExpandedCredibilityIconSize.width } - + if let verifiedIconSize = self.verifiedIconSize, let titleExpandedVerifiedIconSize = self.titleExpandedVerifiedIconSize, verifiedIconSize.width > 0.0 { let leftOffset: CGFloat let leftExpandedOffset: CGFloat @@ -2220,7 +2313,7 @@ final class PeerInfoHeaderNode: ASDisplayNode { if !(self.state?.isEditing ?? false) { switch self.currentCredibilityIcon { - case .premium, .emojiStatus: + case .premium: let iconFrame = self.titleCredibilityIconView.convert(self.titleCredibilityIconView.bounds, to: self.view) let expandedIconFrame = self.titleExpandedCredibilityIconView.convert(self.titleExpandedCredibilityIconView.bounds, to: self.view) if expandedIconFrame.contains(point) && self.isAvatarExpanded { @@ -2231,6 +2324,18 @@ final class PeerInfoHeaderNode: ASDisplayNode { default: break } + switch self.currentStatusIcon { + case .emojiStatus: + let iconFrame = self.titleStatusIconView.convert(self.titleStatusIconView.bounds, to: self.view) + let expandedIconFrame = self.titleExpandedStatusIconView.convert(self.titleExpandedStatusIconView.bounds, to: self.view) + if expandedIconFrame.contains(point) && self.isAvatarExpanded { + return self.titleExpandedStatusIconView.hitTest(self.view.convert(point, to: self.titleExpandedStatusIconView), with: event) + } else if iconFrame.contains(point) { + return self.titleStatusIconView.hitTest(self.view.convert(point, to: self.titleStatusIconView), with: event) + } + default: + break + } } if let subtitleBackgroundButton = self.subtitleBackgroundButton, subtitleBackgroundButton.view.convert(subtitleBackgroundButton.bounds, to: self.view).contains(point) {