diff --git a/Telegram/Telegram-iOS/en.lproj/Localizable.strings b/Telegram/Telegram-iOS/en.lproj/Localizable.strings index a90ddeb3da..2ffba44cff 100644 --- a/Telegram/Telegram-iOS/en.lproj/Localizable.strings +++ b/Telegram/Telegram-iOS/en.lproj/Localizable.strings @@ -13789,4 +13789,12 @@ Sorry for the inconvenience."; "Notification.StarsGift.SentSomeone" = "Someone sent you a gift"; "Notification.StarsGift.UpgradeChannel" = "A gift was turned into a unique collectible"; +"Gift.View.TonGiftInfo" = "This gift is owned by a TON account. [View >]()"; "Gift.View.ViewTonAddressUrl" = "https://tonviewer.com/%@"; +"Gift.View.CopiedAddress" = "TON address copied to clipboard."; + +"NameColor.AddProfileIcons" = "Add Icons To Profile"; +"NameColor.AddRepliesIcons" = "Add Icons To Replies"; +"NameColor.GiftTitle" = "USE A GIFT"; +"NameColor.GiftInfo" = "Apply your collectible's unique look to your profile."; +"NameColor.WearCollectible" = "Wear Collectible"; diff --git a/submodules/AccountContext/Sources/ContactSelectionController.swift b/submodules/AccountContext/Sources/ContactSelectionController.swift index d74999152d..140a078ed1 100644 --- a/submodules/AccountContext/Sources/ContactSelectionController.swift +++ b/submodules/AccountContext/Sources/ContactSelectionController.swift @@ -16,7 +16,7 @@ public protocol ContactSelectionController: ViewController { public enum ContactSelectionControllerMode { case generic - case starsGifting(birthdays: [EnginePeer.Id: TelegramBirthday]?, hasActions: Bool, showSelf: Bool) + case starsGifting(birthdays: [EnginePeer.Id: TelegramBirthday]?, hasActions: Bool, showSelf: Bool, selfSubtitle: String?) } public struct ContactListAdditionalOption: Equatable { diff --git a/submodules/ContactListUI/Sources/ContactListNode.swift b/submodules/ContactListUI/Sources/ContactListNode.swift index 387ec278af..62d66a48ab 100644 --- a/submodules/ContactListUI/Sources/ContactListNode.swift +++ b/submodules/ContactListUI/Sources/ContactListNode.swift @@ -573,7 +573,7 @@ private func contactListNodeEntries(accountPeer: EnginePeer?, peers: [ContactLis index += 1 } } - case let .custom(showSelf, sections): + case let .custom(showSelf, selfSubtitle, sections): if !topPeers.isEmpty { var index: Int = 0 @@ -637,7 +637,7 @@ private func contactListNodeEntries(accountPeer: EnginePeer?, peers: [ContactLis if showSelf, let accountPeer { if let peer = topPeers.first(where: { $0.id == accountPeer.id }) { let header = ChatListSearchItemHeader(type: .text(strings.Premium_Gift_ContactSelection_ThisIsYou.uppercased(), AnyHashable(10)), theme: theme, strings: strings) - entries.append(.peer(index, .peer(peer: peer._asPeer(), isGlobal: false, participantCount: nil), nil, header, .none, theme, strings, dateTimeFormat, sortOrder, displayOrder, false, false, true, nil, false, strings.Premium_Gift_ContactSelection_BuySelf)) + entries.append(.peer(index, .peer(peer: peer._asPeer(), isGlobal: false, participantCount: nil), nil, header, .none, theme, strings, dateTimeFormat, sortOrder, displayOrder, false, false, true, nil, false, selfSubtitle)) existingPeerIds.insert(.peer(peer.id)) } } @@ -882,7 +882,7 @@ public enum ContactListPresentation { public enum TopPeers { case none case recent - case custom(showSelf: Bool, sections: [(title: String, peerIds: [EnginePeer.Id], hasActions: Bool)]) + case custom(showSelf: Bool, selfSubtitle: String?, sections: [(title: String, peerIds: [EnginePeer.Id], hasActions: Bool)]) } case orderedByPresence(options: [ContactListAdditionalOption]) @@ -1728,7 +1728,7 @@ public final class ContactListNode: ASDisplayNode { return .single([]) } } - case let .custom(showSelf, sections): + case let .custom(showSelf, _, sections): var peerIds: [EnginePeer.Id] = [] if showSelf { peerIds.append(context.account.peerId) diff --git a/submodules/DrawingUI/Sources/DrawingScreen.swift b/submodules/DrawingUI/Sources/DrawingScreen.swift index b98216aa6c..3da8876d9c 100644 --- a/submodules/DrawingUI/Sources/DrawingScreen.swift +++ b/submodules/DrawingUI/Sources/DrawingScreen.swift @@ -2926,6 +2926,10 @@ public class DrawingScreen: ViewController, TGPhotoDrawingInterfaceController, U } public func adapterContainerLayoutUpdatedSize(_ size: CGSize, intrinsicInsets: UIEdgeInsets, safeInsets: UIEdgeInsets, statusBarHeight: CGFloat, inputHeight: CGFloat, orientation: UIInterfaceOrientation, isRegular: Bool, animated: Bool) { + var intrinsicInsets = intrinsicInsets + if intrinsicInsets.top.isZero { + intrinsicInsets.top = statusBarHeight + } let layout = ContainerViewLayout( size: size, metrics: LayoutMetrics(widthClass: isRegular ? .regular : .compact, heightClass: isRegular ? .regular : .compact, orientation: nil), diff --git a/submodules/LegacyComponents/Sources/TGPhotoAvatarPreviewController.m b/submodules/LegacyComponents/Sources/TGPhotoAvatarPreviewController.m index cde8a7acbb..ed101da016 100644 --- a/submodules/LegacyComponents/Sources/TGPhotoAvatarPreviewController.m +++ b/submodules/LegacyComponents/Sources/TGPhotoAvatarPreviewController.m @@ -569,6 +569,7 @@ const CGFloat TGPhotoAvatarPreviewLandscapePanelSize = TGPhotoAvatarPreviewPanel break; } + _cancelButton.modernHighlight = false; [UIView animateWithDuration:0.2f animations:^ { _portraitToolsWrapperView.alpha = 0.0f; @@ -688,6 +689,9 @@ const CGFloat TGPhotoAvatarPreviewLandscapePanelSize = TGPhotoAvatarPreviewPanel [_cropView hideImageForCustomTransition]; [_cropView animateTransitionOutSwitching:false]; + _cancelButton.modernHighlight = false; + [_cancelButton.layer removeAllAnimations]; + _previewView.hidden = true; [UIView animateWithDuration:0.3f animations:^ { diff --git a/submodules/LegacyComponents/Sources/TGPhotoEditorController.m b/submodules/LegacyComponents/Sources/TGPhotoEditorController.m index 048e94677c..093e0cc636 100644 --- a/submodules/LegacyComponents/Sources/TGPhotoEditorController.m +++ b/submodules/LegacyComponents/Sources/TGPhotoEditorController.m @@ -1507,9 +1507,9 @@ doneButtonType = TGPhotoEditorDoneButtonDone; if (sideButtonsHiddenInCrop) { - [_portraitToolbarView setCancelDoneButtonsHidden:true animated:true]; - [_portraitToolbarView setCenterButtonsHidden:false animated:true]; - [_landscapeToolbarView setAllButtonsHidden:false animated:true]; + [_portraitToolbarView setCancelDoneButtonsHidden:true animated:false]; + [_portraitToolbarView setCenterButtonsHidden:false animated:false]; + [_landscapeToolbarView setAllButtonsHidden:false animated:false]; } else { [_portraitToolbarView setAllButtonsHidden:false animated:false]; [_landscapeToolbarView setAllButtonsHidden:false animated:false]; diff --git a/submodules/SettingsUI/Sources/Themes/ThemeSettingsController.swift b/submodules/SettingsUI/Sources/Themes/ThemeSettingsController.swift index 5a0973a866..7dfc024c33 100644 --- a/submodules/SettingsUI/Sources/Themes/ThemeSettingsController.swift +++ b/submodules/SettingsUI/Sources/Themes/ThemeSettingsController.swift @@ -514,7 +514,7 @@ public func themeSettingsController(context: AccountContext, focusOnItemTag: The }, openWallpaperSettings: { pushControllerImpl?(ThemeGridController(context: context)) }, openNameColorSettings: { - pushControllerImpl?(PeerNameColorScreen(context: context, subject: .account)) + pushControllerImpl?(UserAppearanceScreen(context: context)) }, selectAccentColor: { accentColor in selectAccentColorImpl?(accentColor) }, openAccentColorPicker: { themeReference, create in diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Payments/StarGifts.swift b/submodules/TelegramCore/Sources/TelegramEngine/Payments/StarGifts.swift index 58d2163932..08eccef4c6 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Payments/StarGifts.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Payments/StarGifts.swift @@ -1124,6 +1124,9 @@ private final class ProfileGiftsContextImpl { } if let index = self.filteredGifts.firstIndex(where: { $0.reference == reference }) { self.filteredGifts[index] = self.filteredGifts[index].withSavedToProfile(added) + if !self.filter.contains(.hidden) && !added { + self.filteredGifts.remove(at: index) + } } self.pushState() } @@ -1803,3 +1806,14 @@ func _internal_toggleStarGiftsNotifications(account: Account, peerId: EnginePeer |> ignoreValues } } + +public extension StarGift.UniqueGift { + var itemFile: TelegramMediaFile? { + for attribute in self.attributes { + if case let .model(_, file, _) = attribute { + return file + } + } + return nil + } +} diff --git a/submodules/TelegramStringFormatting/Sources/Birthday.swift b/submodules/TelegramStringFormatting/Sources/Birthday.swift new file mode 100644 index 0000000000..049ba67bff --- /dev/null +++ b/submodules/TelegramStringFormatting/Sources/Birthday.swift @@ -0,0 +1,18 @@ +import Foundation +import TelegramCore + +public func hasBirthdayToday(cachedData: CachedUserData) -> Bool { + if let birthday = cachedData.birthday { + return hasBirthdayToday(birthday: birthday) + + } + return false +} + +public func hasBirthdayToday(birthday: TelegramBirthday) -> Bool { + let today = Calendar.current.dateComponents(Set([.day, .month]), from: Date()) + if today.day == Int(birthday.day) && today.month == Int(birthday.month) { + return true + } + return false +} diff --git a/submodules/TelegramUI/Components/Chat/ChatChannelSubscriberInputPanelNode/Sources/ChatChannelSubscriberInputPanelNode.swift b/submodules/TelegramUI/Components/Chat/ChatChannelSubscriberInputPanelNode/Sources/ChatChannelSubscriberInputPanelNode.swift index d966bd7730..61964aa70b 100644 --- a/submodules/TelegramUI/Components/Chat/ChatChannelSubscriberInputPanelNode/Sources/ChatChannelSubscriberInputPanelNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatChannelSubscriberInputPanelNode/Sources/ChatChannelSubscriberInputPanelNode.swift @@ -315,7 +315,7 @@ public final class ChatChannelSubscriberInputPanelNode: ChatInputPanelNode { private var displayedGiftTooltip = false private func presentGiftTooltip() { - guard let context = self.context, !self.displayedGiftTooltip else { + guard let context = self.context, !self.displayedGiftTooltip, let parentController = self.interfaceInteraction?.chatController() else { return } self.displayedGiftTooltip = true @@ -332,7 +332,7 @@ public final class ChatChannelSubscriberInputPanelNode: ChatInputPanelNode { let _ = ApplicationSpecificNotice.incrementChannelSendGiftTooltip(accountManager: context.sharedContext.accountManager).start() Queue.mainQueue().after(0.4, { - let absoluteFrame = self.giftButton.view.convert(self.giftButton.bounds, to: nil) + let absoluteFrame = self.giftButton.view.convert(self.giftButton.bounds, to: parentController.view) let location = CGRect(origin: CGPoint(x: absoluteFrame.midX, y: absoluteFrame.minY + 11.0), size: CGSize()) let presentationData = context.sharedContext.currentPresentationData.with { $0 } diff --git a/submodules/TelegramUI/Components/ChatListTitleView/Sources/ChatListTitleView.swift b/submodules/TelegramUI/Components/ChatListTitleView/Sources/ChatListTitleView.swift index 7c8a20b0b8..0fc758a8b8 100644 --- a/submodules/TelegramUI/Components/ChatListTitleView/Sources/ChatListTitleView.swift +++ b/submodules/TelegramUI/Components/ChatListTitleView/Sources/ChatListTitleView.swift @@ -134,11 +134,15 @@ public final class ChatListTitleView: UIView, NavigationBarTitleView, Navigation if let peerStatus = title.peerStatus { let statusContent: EmojiStatusComponent.Content + var statusParticleColor: UIColor? switch peerStatus { case .premium: statusContent = .premium(color: self.theme.list.itemAccentColor) - case let .emoji(emoji): - statusContent = .animation(content: .customEmoji(fileId: emoji.fileId), size: CGSize(width: 22.0, height: 22.0), placeholderColor: self.theme.list.mediaPlaceholderColor, themeColor: self.theme.list.itemAccentColor, loopMode: .count(2)) + case let .emoji(emojiStatus): + statusContent = .animation(content: .customEmoji(fileId: emojiStatus.fileId), size: CGSize(width: 22.0, height: 22.0), placeholderColor: self.theme.list.mediaPlaceholderColor, themeColor: self.theme.list.itemAccentColor, loopMode: .count(2)) + if let color = emojiStatus.color { + statusParticleColor = UIColor(rgb: UInt32(bitPattern: color)) + } } var titleCredibilityIconTransition: ComponentTransition @@ -164,6 +168,7 @@ public final class ChatListTitleView: UIView, NavigationBarTitleView, Navigation animationCache: self.animationCache, animationRenderer: self.animationRenderer, content: statusContent, + particleColor: statusParticleColor, isVisibleForAnimations: true, action: { [weak self] in guard let strongSelf = self, let titleCredibilityIconView = strongSelf.titleCredibilityIconView else { diff --git a/submodules/TelegramUI/Components/ChatTitleView/Sources/ChatTitleView.swift b/submodules/TelegramUI/Components/ChatTitleView/Sources/ChatTitleView.swift index 49baa6bdf6..46d7132d47 100644 --- a/submodules/TelegramUI/Components/ChatTitleView/Sources/ChatTitleView.swift +++ b/submodules/TelegramUI/Components/ChatTitleView/Sources/ChatTitleView.swift @@ -151,7 +151,7 @@ private enum ChatTitleCredibilityIcon: Equatable { case scam case verified case premium - case emojiStatus(PeerEmojiStatus, Int32?) + case emojiStatus(PeerEmojiStatus) } public final class ChatTitleView: UIView, NavigationBarTitleView { @@ -275,7 +275,7 @@ public final class ChatTitleView: UIView, NavigationBarTitleView { } else if peer.isScam { titleCredibilityIcon = .scam } else if let emojiStatus = peer.emojiStatus, !premiumConfiguration.isPremiumDisabled { - titleStatusIcon = .emojiStatus(emojiStatus, emojiStatus.color) + titleStatusIcon = .emojiStatus(emojiStatus) } else if peer.isPremium && !premiumConfiguration.isPremiumDisabled { titleCredibilityIcon = .premium } @@ -284,7 +284,7 @@ public final class ChatTitleView: UIView, NavigationBarTitleView { titleCredibilityIcon = .verified } if let verificationIconFileId = peer.verificationIconFileId { - titleVerifiedIcon = .emojiStatus(PeerEmojiStatus(content: .emoji(fileId: verificationIconFileId), expirationDate: nil), nil) + titleVerifiedIcon = .emojiStatus(PeerEmojiStatus(content: .emoji(fileId: verificationIconFileId), expirationDate: nil)) } } } @@ -839,7 +839,7 @@ public final class ChatTitleView: UIView, NavigationBarTitleView { titleCredibilityContent = .text(color: self.theme.chat.message.incoming.scamColor, string: self.strings.Message_FakeAccount.uppercased()) case .scam: titleCredibilityContent = .text(color: self.theme.chat.message.incoming.scamColor, string: self.strings.Message_ScamAccount.uppercased()) - case let .emojiStatus(emojiStatus, _): + case let .emojiStatus(emojiStatus): titleCredibilityContent = .animation(content: .customEmoji(fileId: emojiStatus.fileId), size: CGSize(width: 32.0, height: 32.0), placeholderColor: self.theme.list.mediaPlaceholderColor, themeColor: self.theme.list.itemAccentColor, loopMode: .count(2)) } @@ -855,16 +855,16 @@ public final class ChatTitleView: UIView, NavigationBarTitleView { titleVerifiedContent = .text(color: self.theme.chat.message.incoming.scamColor, string: self.strings.Message_FakeAccount.uppercased()) case .scam: titleVerifiedContent = .text(color: self.theme.chat.message.incoming.scamColor, string: self.strings.Message_ScamAccount.uppercased()) - case let .emojiStatus(emojiStatus, _): + case let .emojiStatus(emojiStatus): titleVerifiedContent = .animation(content: .customEmoji(fileId: emojiStatus.fileId), size: CGSize(width: 32.0, height: 32.0), placeholderColor: self.theme.list.mediaPlaceholderColor, themeColor: self.theme.list.itemAccentColor, loopMode: .count(2)) } let titleStatusContent: EmojiStatusComponent.Content var titleStatusParticleColor: UIColor? switch self.titleStatusIcon { - case let .emojiStatus(emojiStatus, color): + case let .emojiStatus(emojiStatus): titleStatusContent = .animation(content: .customEmoji(fileId: emojiStatus.fileId), size: CGSize(width: 32.0, height: 32.0), placeholderColor: self.theme.list.mediaPlaceholderColor, themeColor: self.theme.list.itemAccentColor, loopMode: .count(2)) - if let color { + if let color = emojiStatus.color { titleStatusParticleColor = UIColor(rgb: UInt32(bitPattern: color)) } default: diff --git a/submodules/TelegramUI/Components/EmojiStatusSelectionComponent/Sources/EmojiStatusSelectionComponent.swift b/submodules/TelegramUI/Components/EmojiStatusSelectionComponent/Sources/EmojiStatusSelectionComponent.swift index 79bd0a020c..beed3cb9d5 100644 --- a/submodules/TelegramUI/Components/EmojiStatusSelectionComponent/Sources/EmojiStatusSelectionComponent.swift +++ b/submodules/TelegramUI/Components/EmojiStatusSelectionComponent/Sources/EmojiStatusSelectionComponent.swift @@ -1149,7 +1149,7 @@ public final class EmojiStatusSelectionController: ViewController { } } - if let previewItem = self.previewItem, let itemFile = previewItem.item.itemFile { + if let previewItem = self.previewItem, let itemFile = previewItem.item.displayFile { let previewScreenView: ComponentView var previewScreenTransition = transition if let current = self.previewScreenView { @@ -1190,58 +1190,66 @@ public final class EmojiStatusSelectionController: ViewController { return } - if let result = result, let previewItem = strongSelf.previewItem { - var emojiString: String? - if let itemFile = previewItem.item.itemFile { - attributeLoop: for attribute in itemFile.attributes { - switch attribute { - case let .CustomEmoji(_, _, alt, _): - emojiString = alt - break attributeLoop - default: - break - } + if let result, let previewItem = strongSelf.previewItem { + let expirationDate: Int32? = result.timestamp + if let itemGift = previewItem.item.itemGift { + let _ = (strongSelf.context.engine.accountData.setStarGiftStatus(starGift: itemGift, expirationDate: expirationDate) + |> deliverOnMainQueue).start() + + if let destinationView = strongSelf.controller?.destinationItemView() { + strongSelf.animateOutToStatus(item: previewItem.item, sourceLayer: result.sourceView.layer, customEffectFile: nil, destinationView: destinationView, fromBackground: true) } - } - - let context = strongSelf.context - let _ = (context.engine.stickers.availableReactions() - |> take(1) - |> mapToSignal { availableReactions -> Signal in - guard let emojiString = emojiString, let availableReactions = availableReactions else { - return .single(nil) - } - for reaction in availableReactions.reactions { - if case let .builtin(value) = reaction.value, value == emojiString { - if let aroundAnimation = reaction.aroundAnimation { - return context.account.postbox.mediaBox.resourceData(aroundAnimation.resource) - |> take(1) - |> map { data -> String? in - if data.complete { - return data.path - } else { - return nil - } - } - } else { - return .single(nil) + } else { + var emojiString: String? + if let itemFile = previewItem.item.itemFile { + attributeLoop: for attribute in itemFile.attributes { + switch attribute { + case let .CustomEmoji(_, _, alt, _): + emojiString = alt + break attributeLoop + default: + break } } } - return .single(nil) - } - |> deliverOnMainQueue).start(next: { filePath in - guard let strongSelf = self, let previewItem = strongSelf.previewItem, let destinationView = strongSelf.controller?.destinationItemView() else { - return + + let context = strongSelf.context + let _ = (context.engine.stickers.availableReactions() + |> take(1) + |> mapToSignal { availableReactions -> Signal in + guard let emojiString = emojiString, let availableReactions = availableReactions else { + return .single(nil) + } + for reaction in availableReactions.reactions { + if case let .builtin(value) = reaction.value, value == emojiString { + if let aroundAnimation = reaction.aroundAnimation { + return context.account.postbox.mediaBox.resourceData(aroundAnimation.resource) + |> take(1) + |> map { data -> String? in + if data.complete { + return data.path + } else { + return nil + } + } + } else { + return .single(nil) + } + } + } + return .single(nil) } - - let expirationDate: Int32? = result.timestamp - - let _ = (strongSelf.context.engine.accountData.setEmojiStatus(file: previewItem.item.itemFile, expirationDate: expirationDate) - |> deliverOnMainQueue).start() - - strongSelf.animateOutToStatus(item: previewItem.item, sourceLayer: result.sourceView.layer, customEffectFile: filePath, destinationView: destinationView, fromBackground: true) - }) + |> deliverOnMainQueue).start(next: { filePath in + guard let strongSelf = self, let previewItem = strongSelf.previewItem, let destinationView = strongSelf.controller?.destinationItemView() else { + return + } + + let _ = (strongSelf.context.engine.accountData.setEmojiStatus(file: previewItem.item.itemFile, expirationDate: expirationDate) + |> deliverOnMainQueue).start() + + strongSelf.animateOutToStatus(item: previewItem.item, sourceLayer: result.sourceView.layer, customEffectFile: filePath, destinationView: destinationView, fromBackground: true) + }) + } } else { strongSelf.dismissedPreviewItem = strongSelf.previewItem strongSelf.previewItem = nil @@ -1551,3 +1559,15 @@ private func generateParabollicMotionKeyframes(from sourcePoint: CGPoint, to tar return keyframes } + +extension EmojiPagerContentComponent.Item { + var displayFile: TelegramMediaFile? { + if let file = self.itemFile { + return file + } else if let gift = self.itemGift { + return gift.itemFile + } else { + return nil + } + } +} diff --git a/submodules/TelegramUI/Components/EntityKeyboard/Sources/EntityKeyboard.swift b/submodules/TelegramUI/Components/EntityKeyboard/Sources/EntityKeyboard.swift index ee7a06c1e0..fd0198c52d 100644 --- a/submodules/TelegramUI/Components/EntityKeyboard/Sources/EntityKeyboard.swift +++ b/submodules/TelegramUI/Components/EntityKeyboard/Sources/EntityKeyboard.swift @@ -572,14 +572,16 @@ public final class EntityKeyboardComponent: Component { for itemGroup in emojiContent.panelItemGroups { if !itemGroup.items.isEmpty { if let id = itemGroup.groupId.base as? String, id != "peerSpecific" { - if id == "recent" || id == "liked" { + if id == "recent" || id == "liked" || id == "collectible" { let iconMapping: [String: EntityKeyboardIconTopPanelComponent.Icon] = [ "recent": .recent, "liked": .liked, + "collectible": .collectible ] let titleMapping: [String: String] = [ "recent": component.strings.Stickers_Recent, "liked": "", + "collectible": "" ] if let icon = iconMapping[id], let title = titleMapping[id] { topEmojiItems.append(EntityKeyboardTopPanelComponent.Item( diff --git a/submodules/TelegramUI/Components/EntityKeyboard/Sources/EntityKeyboardTopPanelComponent.swift b/submodules/TelegramUI/Components/EntityKeyboard/Sources/EntityKeyboardTopPanelComponent.swift index 28dada2af5..59e9034eb2 100644 --- a/submodules/TelegramUI/Components/EntityKeyboard/Sources/EntityKeyboardTopPanelComponent.swift +++ b/submodules/TelegramUI/Components/EntityKeyboard/Sources/EntityKeyboardTopPanelComponent.swift @@ -277,6 +277,7 @@ final class EntityKeyboardIconTopPanelComponent: Component { case saved case premium case liked + case collectible } let icon: Icon @@ -363,6 +364,8 @@ final class EntityKeyboardIconTopPanelComponent: Component { image = UIImage(bundleImageName: "Chat/Input/Media/PanelSavedIcon") case .liked: image = UIImage(bundleImageName: "Chat/Input/Media/PanelHeartIcon")?.withRenderingMode(.alwaysTemplate) + case .collectible: + image = UIImage(bundleImageName: "Chat/Input/Media/PanelCollectibleIcon")?.withRenderingMode(.alwaysTemplate) case .premium: image = generateImage(CGSize(width: 44.0, height: 44.0), contextGenerator: { size, context in context.clear(CGRect(origin: CGPoint(), size: size)) diff --git a/submodules/TelegramUI/Components/Gifts/GiftItemComponent/Sources/GiftItemComponent.swift b/submodules/TelegramUI/Components/Gifts/GiftItemComponent/Sources/GiftItemComponent.swift index dd32680db0..7aedefdfc2 100644 --- a/submodules/TelegramUI/Components/Gifts/GiftItemComponent/Sources/GiftItemComponent.swift +++ b/submodules/TelegramUI/Components/Gifts/GiftItemComponent/Sources/GiftItemComponent.swift @@ -87,6 +87,7 @@ public final class GiftItemComponent: Component { case profile case thumbnail case preview + case grid } let context: AccountContext @@ -99,6 +100,7 @@ public final class GiftItemComponent: Component { let isLoading: Bool let isHidden: Bool let isSoldOut: Bool + let isSelected: Bool let mode: Mode public init( @@ -112,6 +114,7 @@ public final class GiftItemComponent: Component { isLoading: Bool = false, isHidden: Bool = false, isSoldOut: Bool = false, + isSelected: Bool = false, mode: Mode = .generic ) { self.context = context @@ -124,6 +127,7 @@ public final class GiftItemComponent: Component { self.isLoading = isLoading self.isHidden = isHidden self.isSoldOut = isSoldOut + self.isSelected = isSelected self.mode = mode } @@ -158,6 +162,9 @@ public final class GiftItemComponent: Component { if lhs.isSoldOut != rhs.isSoldOut { return false } + if lhs.isSelected != rhs.isSelected { + return false + } if lhs.mode != rhs.mode { return false } @@ -181,6 +188,7 @@ public final class GiftItemComponent: Component { private let ribbonText = ComponentView() private var animationLayer: InlineStickerItemLayer? + private var selectionLayer: SimpleShapeLayer? private var disposables = DisposableSet() private var fetchedFiles = Set() @@ -234,6 +242,10 @@ public final class GiftItemComponent: Component { size = CGSize(width: availableSize.width, height: availableSize.width) iconSize = CGSize(width: floor(size.width * 0.7), height: floor(size.width * 0.7)) cornerRadius = floor(availableSize.width * 0.2) + case .grid: + size = CGSize(width: availableSize.width, height: availableSize.width) + iconSize = CGSize(width: floor(size.width * 0.7), height: floor(size.width * 0.7)) + cornerRadius = 10.0 case .preview: size = availableSize iconSize = CGSize(width: floor(size.width * 0.6), height: floor(size.width * 0.6)) @@ -587,6 +599,43 @@ public final class GiftItemComponent: Component { } } + if case .grid = component.mode { + let lineWidth: CGFloat = 2.0 + let selectionFrame = CGRect(origin: .zero, size: size).insetBy(dx: 3.0, dy: 3.0) + + if component.isSelected { + let selectionLayer: SimpleShapeLayer + if let current = self.selectionLayer { + selectionLayer = current + } else { + selectionLayer = SimpleShapeLayer() + self.selectionLayer = selectionLayer + self.layer.addSublayer(selectionLayer) + + selectionLayer.fillColor = UIColor.clear.cgColor + selectionLayer.strokeColor = UIColor.white.cgColor + selectionLayer.lineWidth = lineWidth + selectionLayer.frame = selectionFrame + selectionLayer.path = CGPath(roundedRect: CGRect(origin: .zero, size: selectionFrame.size).insetBy(dx: lineWidth / 2.0, dy: lineWidth / 2.0), cornerWidth: 6.0, cornerHeight: 6.0, transform: nil) + + if !transition.animation.isImmediate { + let initialPath = CGPath(roundedRect: CGRect(origin: .zero, size: selectionFrame.size).insetBy(dx: 0.0, dy: 0.0), cornerWidth: 6.0, cornerHeight: 6.0, transform: nil) + selectionLayer.animate(from: initialPath, to: selectionLayer.path as AnyObject, keyPath: "path", timingFunction: CAMediaTimingFunctionName.easeInEaseOut.rawValue, duration: 0.2) + selectionLayer.animateShapeLineWidth(from: 0.0, to: lineWidth, duration: 0.2) + } + } + + } else if let selectionLayer = self.selectionLayer { + self.selectionLayer = nil + + let targetPath = CGPath(roundedRect: CGRect(origin: .zero, size: selectionFrame.size).insetBy(dx: 0.0, dy: 0.0), cornerWidth: 6.0, cornerHeight: 6.0, transform: nil) + selectionLayer.animate(from: selectionLayer.path, to: targetPath, keyPath: "path", timingFunction: CAMediaTimingFunctionName.easeInEaseOut.rawValue, duration: 0.2, removeOnCompletion: false) + selectionLayer.animateShapeLineWidth(from: selectionLayer.lineWidth, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { _ in + selectionLayer.removeFromSuperlayer() + }) + } + } + return size } } diff --git a/submodules/TelegramUI/Components/Gifts/GiftOptionsScreen/Sources/GiftOptionsScreen.swift b/submodules/TelegramUI/Components/Gifts/GiftOptionsScreen/Sources/GiftOptionsScreen.swift index 4202a5080f..8617860046 100644 --- a/submodules/TelegramUI/Components/Gifts/GiftOptionsScreen/Sources/GiftOptionsScreen.swift +++ b/submodules/TelegramUI/Components/Gifts/GiftOptionsScreen/Sources/GiftOptionsScreen.swift @@ -19,7 +19,6 @@ import TelegramStringFormatting import PlainButtonComponent import BlurredBackgroundComponent import PremiumStarComponent -import ConfettiEffect import TextFormat import GiftItemComponent import InAppPurchaseManager diff --git a/submodules/TelegramUI/Components/Gifts/GiftSetupScreen/BUILD b/submodules/TelegramUI/Components/Gifts/GiftSetupScreen/BUILD index 019908ec4f..910119fa75 100644 --- a/submodules/TelegramUI/Components/Gifts/GiftSetupScreen/BUILD +++ b/submodules/TelegramUI/Components/Gifts/GiftSetupScreen/BUILD @@ -47,6 +47,7 @@ swift_library( "//submodules/Components/BlurredBackgroundComponent", "//submodules/ProgressNavigationButtonNode", "//submodules/TelegramUI/Components/Gifts/GiftViewScreen", + "//submodules/ConfettiEffect", ], visibility = [ "//visibility:public", diff --git a/submodules/TelegramUI/Components/Gifts/GiftSetupScreen/Sources/ChatGiftPreviewItem.swift b/submodules/TelegramUI/Components/Gifts/GiftSetupScreen/Sources/ChatGiftPreviewItem.swift index 9e1e53a46f..bc0d5e8851 100644 --- a/submodules/TelegramUI/Components/Gifts/GiftSetupScreen/Sources/ChatGiftPreviewItem.swift +++ b/submodules/TelegramUI/Components/Gifts/GiftSetupScreen/Sources/ChatGiftPreviewItem.swift @@ -233,7 +233,7 @@ final class ChatGiftPreviewItemNode: ListViewItemNode { case let .starGift(gift): media = [ TelegramMediaAction( - action: .starGift(gift: .generic(gift), convertStars: gift.convertStars, text: item.text, entities: item.entities, nameHidden: false, savedToProfile: false, converted: false, upgraded: false, canUpgrade: true, upgradeStars: item.upgradeStars, isRefunded: false, upgradeMessageId: nil, peerId: nil, senderId: nil, savedId: nil) + action: .starGift(gift: .generic(gift), convertStars: gift.convertStars, text: item.text, entities: item.entities, nameHidden: false, savedToProfile: false, converted: false, upgraded: false, canUpgrade: gift.upgradeStars != nil, upgradeStars: item.upgradeStars, isRefunded: false, upgradeMessageId: nil, peerId: nil, senderId: nil, savedId: nil) ) ] } diff --git a/submodules/TelegramUI/Components/Gifts/GiftSetupScreen/Sources/GiftSetupScreen.swift b/submodules/TelegramUI/Components/Gifts/GiftSetupScreen/Sources/GiftSetupScreen.swift index ec6b77ffe8..92732d9e47 100644 --- a/submodules/TelegramUI/Components/Gifts/GiftSetupScreen/Sources/GiftSetupScreen.swift +++ b/submodules/TelegramUI/Components/Gifts/GiftSetupScreen/Sources/GiftSetupScreen.swift @@ -35,6 +35,7 @@ import ProgressNavigationButtonNode import Markdown import GiftViewScreen import UndoUI +import ConfettiEffect final class GiftSetupScreenComponent: Component { typealias EnvironmentType = ViewControllerComponentContainer.Environment @@ -377,6 +378,8 @@ final class GiftSetupScreenComponent: Component { action: { _ in return true } ) (navigationController.viewControllers.last as? ViewController)?.present(tooltipController, in: .current) + + navigationController.view.addSubview(ConfettiView(frame: navigationController.view.bounds)) } if let completion { diff --git a/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftViewScreen.swift b/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftViewScreen.swift index 627b737abb..08c2b6e8e5 100644 --- a/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftViewScreen.swift +++ b/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftViewScreen.swift @@ -47,6 +47,7 @@ private final class GiftViewSheetContent: CombinedComponent { let cancel: (Bool) -> Void let openPeer: (EnginePeer) -> Void let openAddress: (String) -> Void + let copyAddress: (String) -> Void let updateSavedToProfile: (Bool) -> Void let convertToStars: () -> Void let openStarsIntro: () -> Void @@ -66,6 +67,7 @@ private final class GiftViewSheetContent: CombinedComponent { cancel: @escaping (Bool) -> Void, openPeer: @escaping (EnginePeer) -> Void, openAddress: @escaping (String) -> Void, + copyAddress: @escaping (String) -> Void, updateSavedToProfile: @escaping (Bool) -> Void, convertToStars: @escaping () -> Void, openStarsIntro: @escaping () -> Void, @@ -84,6 +86,7 @@ private final class GiftViewSheetContent: CombinedComponent { self.cancel = cancel self.openPeer = openPeer self.openAddress = openAddress + self.copyAddress = copyAddress self.updateSavedToProfile = updateSavedToProfile self.convertToStars = convertToStars self.openStarsIntro = openStarsIntro @@ -447,6 +450,7 @@ private final class GiftViewSheetContent: CombinedComponent { var soldOut = false var nameHidden = false var upgraded = false + var exported = false var canUpgrade = false var upgradeStars: Int64? var uniqueGift: StarGift.UniqueGift? @@ -1082,6 +1086,8 @@ private final class GiftViewSheetContent: CombinedComponent { let markdownAttributes = MarkdownAttributes(body: MarkdownAttributeSet(font: textFont, textColor: textColor), bold: MarkdownAttributeSet(font: textFont, textColor: textColor), link: MarkdownAttributeSet(font: textFont, textColor: linkColor), linkAttribute: { contents in return (TelegramTextAttributes.URL, contents) }) + + descriptionText = descriptionText.replacingOccurrences(of: " >]", with: "\u{00A0}>]") let attributedString = parseMarkdownIntoAttributedString(descriptionText, attributes: markdownAttributes, textAlignment: .center).mutableCopy() as! NSMutableAttributedString if let range = attributedString.string.range(of: ">"), let chevronImage = state.cachedChevronImage?.0 { attributedString.addAttribute(.attachment, value: chevronImage, range: NSRange(range, in: attributedString.string)) @@ -1286,6 +1292,8 @@ private final class GiftViewSheetContent: CombinedComponent { ) )) case let .address(address): + exported = true + func formatAddress(_ str: String) -> String { var result = str let middleIndex = result.index(result.startIndex, offsetBy: str.count / 2) @@ -1302,8 +1310,7 @@ private final class GiftViewSheetContent: CombinedComponent { MultilineTextComponent(text: .plain(NSAttributedString(string: formatAddress(address), font: tableLargeMonospaceFont, textColor: tableLinkColor)), maximumNumberOfLines: 2, lineSpacing: 0.2) ), action: { - component.openAddress(address) - component.cancel(true) + component.copyAddress(address) } ) ) @@ -1403,6 +1410,8 @@ private final class GiftViewSheetContent: CombinedComponent { var canTransfer = true if let peer = state.peerMap[peerId], case let .channel(channel) = peer, !channel.flags.contains(.isCreator) { canTransfer = false + } else if subject.arguments?.transferStars == nil { + canTransfer = false } let buttonsCount = canTransfer ? 3 : 2 @@ -1871,13 +1880,17 @@ private final class GiftViewSheetContent: CombinedComponent { originY += table.size.height + 23.0 } - if incoming && !converted && !upgraded && !showUpgradePreview && !showWearPreview { + if ((incoming && !converted && !upgraded) || exported) && (!showUpgradePreview && !showWearPreview) { let linkColor = theme.actionSheet.controlAccentColor if state.cachedSmallChevronImage == nil || state.cachedSmallChevronImage?.1 !== environment.theme { state.cachedSmallChevronImage = (generateTintedImage(image: UIImage(bundleImageName: "Item List/InlineTextRightArrow"), color: linkColor)!, theme) } - let descriptionText: String - if savedToProfile { + var addressToOpen: String? + var descriptionText: String + if let uniqueGift, case let .address(address) = uniqueGift.owner { + addressToOpen = address + descriptionText = strings.Gift_View_TonGiftInfo + } else if savedToProfile { descriptionText = isChannelGift ? strings.Gift_View_DisplayedInfoHide_Channel : strings.Gift_View_DisplayedInfoHide } else if let upgradeStars, upgradeStars > 0 && !upgraded { descriptionText = isChannelGift ? strings.Gift_View_HiddenInfoShow_Channel : strings.Gift_View_HiddenInfoShow @@ -1894,6 +1907,8 @@ private final class GiftViewSheetContent: CombinedComponent { let markdownAttributes = MarkdownAttributes(body: MarkdownAttributeSet(font: textFont, textColor: textColor), bold: MarkdownAttributeSet(font: textFont, textColor: textColor), link: MarkdownAttributeSet(font: textFont, textColor: linkColor), linkAttribute: { contents in return (TelegramTextAttributes.URL, contents) }) + + descriptionText = descriptionText.replacingOccurrences(of: " >]", with: "\u{00A0}>]") let attributedString = parseMarkdownIntoAttributedString(descriptionText, attributes: markdownAttributes, textAlignment: .center).mutableCopy() as! NSMutableAttributedString if let range = attributedString.string.range(of: ">"), let chevronImage = state.cachedSmallChevronImage?.0 { attributedString.addAttribute(.attachment, value: chevronImage, range: NSRange(range, in: attributedString.string)) @@ -1917,10 +1932,15 @@ private final class GiftViewSheetContent: CombinedComponent { }, tapAction: { attributes, _ in if let _ = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.URL)] as? String { - component.updateSavedToProfile(!savedToProfile) - Queue.mainQueue().after(0.6, { - component.cancel(false) - }) + if let addressToOpen { + component.openAddress(addressToOpen) + component.cancel(true) + } else { + component.updateSavedToProfile(!savedToProfile) + Queue.mainQueue().after(0.6, { + component.cancel(false) + }) + } } } ), @@ -2201,6 +2221,7 @@ private final class GiftViewSheetComponent: CombinedComponent { let subject: GiftViewScreen.Subject let openPeer: (EnginePeer) -> Void let openAddress: (String) -> Void + let copyAddress: (String) -> Void let updateSavedToProfile: (Bool) -> Void let convertToStars: () -> Void let openStarsIntro: () -> Void @@ -2218,6 +2239,7 @@ private final class GiftViewSheetComponent: CombinedComponent { subject: GiftViewScreen.Subject, openPeer: @escaping (EnginePeer) -> Void, openAddress: @escaping (String) -> Void, + copyAddress: @escaping (String) -> Void, updateSavedToProfile: @escaping (Bool) -> Void, convertToStars: @escaping () -> Void, openStarsIntro: @escaping () -> Void, @@ -2234,6 +2256,7 @@ private final class GiftViewSheetComponent: CombinedComponent { self.subject = subject self.openPeer = openPeer self.openAddress = openAddress + self.copyAddress = copyAddress self.updateSavedToProfile = updateSavedToProfile self.convertToStars = convertToStars self.openStarsIntro = openStarsIntro @@ -2286,6 +2309,7 @@ private final class GiftViewSheetComponent: CombinedComponent { }, openPeer: context.component.openPeer, openAddress: context.component.openAddress, + copyAddress: context.component.copyAddress, updateSavedToProfile: context.component.updateSavedToProfile, convertToStars: context.component.convertToStars, openStarsIntro: context.component.openStarsIntro, @@ -2447,6 +2471,7 @@ public class GiftViewScreen: ViewControllerComponentContainer { var openPeerImpl: ((EnginePeer) -> Void)? var openAddressImpl: ((String) -> Void)? + var copyAddressImpl: ((String) -> Void)? var updateSavedToProfileImpl: ((Bool) -> Void)? var convertToStarsImpl: (() -> Void)? var openStarsIntroImpl: (() -> Void)? @@ -2470,6 +2495,9 @@ public class GiftViewScreen: ViewControllerComponentContainer { openAddress: { address in openAddressImpl?(address) }, + copyAddress: { address in + copyAddressImpl?(address) + }, updateSavedToProfile: { added in updateSavedToProfileImpl?(added) }, @@ -2537,6 +2565,18 @@ public class GiftViewScreen: ViewControllerComponentContainer { } } } + copyAddressImpl = { [weak self] address in + guard let self else { + return + } + UIPasteboard.general.string = address + + self.dismissAllTooltips() + + self.present(UndoOverlayController(presentationData: presentationData, content: .copy(text: presentationData.strings.Gift_View_CopiedAddress), elevatedLayout: false, position: .bottom, action: { _ in return true }), in: .current) + + HapticFeedback().tap() + } updateSavedToProfileImpl = { [weak self] added in guard let self, let arguments = self.subject.arguments, let reference = arguments.reference else { return @@ -2618,6 +2658,11 @@ public class GiftViewScreen: ViewControllerComponentContainer { let configuration = GiftConfiguration.with(appConfiguration: context.currentAppConfiguration.with { $0 }) let starsConvertMaxDate = arguments.date + configuration.convertToStarsPeriod + var isChannelGift = false + if case let .peer(peerId, _) = reference, peerId.namespace == Namespaces.Peer.CloudChannel { + isChannelGift = true + } + let currentTime = Int32(CFAbsoluteTimeGetCurrent() + kCFAbsoluteTimeIntervalSince1970) if currentTime > starsConvertMaxDate { let days: Int32 = Int32(ceil(Float(configuration.convertToStarsPeriod) / 86400.0)) @@ -2653,8 +2698,10 @@ public class GiftViewScreen: ViewControllerComponentContainer { if let navigationController { Queue.mainQueue().after(0.5) { - if let starsContext = context.starsContext { - navigationController.pushViewController(context.sharedContext.makeStarsTransactionsScreen(context: context, starsContext: starsContext), animated: true) + if !isChannelGift { + if let starsContext = context.starsContext { + navigationController.pushViewController(context.sharedContext.makeStarsTransactionsScreen(context: context, starsContext: starsContext), animated: true) + } } if let lastController = navigationController.viewControllers.last as? ViewController { diff --git a/submodules/TelegramUI/Components/MediaEditor/Sources/MediaEditorComposer.swift b/submodules/TelegramUI/Components/MediaEditor/Sources/MediaEditorComposer.swift index 64c61999bf..3991c44358 100644 --- a/submodules/TelegramUI/Components/MediaEditor/Sources/MediaEditorComposer.swift +++ b/submodules/TelegramUI/Components/MediaEditor/Sources/MediaEditorComposer.swift @@ -127,7 +127,7 @@ final class MediaEditorComposer { if values.isSticker { self.maskImage = roundedCornersMaskImage(size: CGSize(width: floor(1080.0 * 0.97), height: floor(1080.0 * 0.97))) } else if values.isAvatar { - self.maskImage = rectangleMaskImage(size: CGSize(width: floor(1080.0 * 0.97), height: floor(1080.0 * 0.97))) + self.maskImage = rectangleMaskImage(size: CGSize(width: 1080.0, height: 1080.0)) } if let drawing = values.drawing, let drawingImage = CIImage(image: drawing, options: [.colorSpace: self.colorSpace]) { @@ -227,7 +227,7 @@ public func makeEditorImageComposition(context: CIContext, postbox: Postbox, inp if values.isSticker { maskImage = roundedCornersMaskImage(size: CGSize(width: floor(1080.0 * 0.97), height: floor(1080.0 * 0.97))) } else if values.isAvatar { - maskImage = rectangleMaskImage(size: CGSize(width: floor(1080.0 * 0.97), height: floor(1080.0 * 0.97))) + maskImage = rectangleMaskImage(size: CGSize(width: 1080.0, height: 1080.0)) } else if let outputDimensions { maskImage = rectangleMaskImage(size: outputDimensions.aspectFitted(CGSize(width: 1080.0, height: 1080.0))) } @@ -299,10 +299,14 @@ private func makeEditorImageFrameComposition(context: CIContext, inputImage: CII } resultImage = resultImage.transformed(by: CGAffineTransform(translationX: dimensions.width / 2.0, y: dimensions.height / 2.0)) - if values.isSticker || values.isAvatar { + if values.isSticker { let minSize = min(dimensions.width, dimensions.height) let scaledSize = CGSize(width: floor(minSize * 0.97), height: floor(minSize * 0.97)) resultImage = resultImage.transformed(by: CGAffineTransform(translationX: -(dimensions.width - scaledSize.width) / 2.0, y: -(dimensions.height - scaledSize.height) / 2.0)).cropped(to: CGRect(origin: .zero, size: scaledSize)) + } else if values.isAvatar { + let minSize = min(dimensions.width, dimensions.height) + let scaledSize = CGSize(width: minSize, height: minSize) + resultImage = resultImage.transformed(by: CGAffineTransform(translationX: -(dimensions.width - scaledSize.width) / 2.0, y: -(dimensions.height - scaledSize.height) / 2.0)).cropped(to: CGRect(origin: .zero, size: scaledSize)) } else if values.isCover, let outputDimensions { let minSize = min(dimensions.width, dimensions.height) let scaledSize = outputDimensions.aspectFitted(CGSize(width: minSize, height: minSize)) diff --git a/submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaEditorScreen.swift b/submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaEditorScreen.swift index 3f6f0912ae..ebf5ed47fa 100644 --- a/submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaEditorScreen.swift +++ b/submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaEditorScreen.swift @@ -5950,7 +5950,7 @@ public final class MediaEditorScreenImpl: ViewController, MediaEditorScreen, UID if let stickerBackgroundView = self.stickerBackgroundView, let stickerOverlayLayer = self.stickerOverlayLayer, let stickerFrameLayer = self.stickerFrameLayer { let stickerFrameFraction: CGFloat switch controller.mode { - case .avatarEditor, .stickerEditor: + case .stickerEditor: stickerFrameFraction = 0.97 default: stickerFrameFraction = 1.0 diff --git a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoHeaderNode.swift b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoHeaderNode.swift index 3a9dfe2b57..f5f7f42c0b 100644 --- a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoHeaderNode.swift +++ b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoHeaderNode.swift @@ -813,6 +813,7 @@ final class PeerInfoHeaderNode: ASDisplayNode { do { self.currentCredibilityIcon = credibilityIcon + var emojiStatusSize: CGSize? var currentEmojiStatus: PeerEmojiStatus? let emojiRegularStatusContent: EmojiStatusComponent.Content let emojiExpandedStatusContent: EmojiStatusComponent.Content @@ -823,6 +824,7 @@ final class PeerInfoHeaderNode: ASDisplayNode { case .premium: emojiRegularStatusContent = .premium(color: navigationContentsAccentColor) emojiExpandedStatusContent = .premium(color: navigationContentsAccentColor) + emojiStatusSize = CGSize(width: 30.0, height: 30.0) case .verified: emojiRegularStatusContent = .verified(fillColor: presentationData.theme.list.itemCheckColors.fillColor, foregroundColor: presentationData.theme.list.itemCheckColors.foregroundColor, sizeType: .large) emojiExpandedStatusContent = .verified(fillColor: navigationContentsAccentColor, foregroundColor: .clear, sizeType: .large) @@ -845,6 +847,7 @@ final class PeerInfoHeaderNode: ASDisplayNode { animationCache: self.animationCache, animationRenderer: self.animationRenderer, content: emojiRegularStatusContent, + size: emojiStatusSize, isVisibleForAnimations: true, useSharedAnimation: true, action: { [weak self] in @@ -866,6 +869,7 @@ final class PeerInfoHeaderNode: ASDisplayNode { animationCache: self.animationCache, animationRenderer: self.animationRenderer, content: emojiExpandedStatusContent, + size: emojiStatusSize, isVisibleForAnimations: true, useSharedAnimation: true, action: { [weak self] in diff --git a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoPaneContainerNode.swift b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoPaneContainerNode.swift index fb066c210d..62a60640cd 100644 --- a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoPaneContainerNode.swift +++ b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoPaneContainerNode.swift @@ -1173,7 +1173,7 @@ final class PeerInfoPaneContainerNode: ASDisplayNode, ASGestureRecognizerDelegat tabsAlpha = 1.0 - tabsOffset / tabsHeight } tabsAlpha *= tabsAlpha - transition.updateFrame(node: self.tabsContainerNode, frame: CGRect(origin: CGPoint(x: 0.0, y: -tabsOffset), size: CGSize(width: size.width, height: tabsHeight))) + transition.updateFrame(node: self.tabsContainerNode, frame: CGRect(origin: CGPoint(x: sideInset, y: -tabsOffset), size: CGSize(width: size.width - sideInset * 2.0, height: tabsHeight))) transition.updateAlpha(node: self.tabsContainerNode, alpha: tabsAlpha) transition.updateFrame(node: self.separatorNode, frame: CGRect(origin: CGPoint(x: 0.0, y: -UIScreenPixel - tabsOffset), size: CGSize(width: size.width, height: UIScreenPixel))) @@ -1183,7 +1183,7 @@ final class PeerInfoPaneContainerNode: ASDisplayNode, ASGestureRecognizerDelegat transition.updateFrame(node: self.tabsSeparatorNode, frame: CGRect(origin: CGPoint(x: 0.0, y: tabsHeight - tabsOffset), size: CGSize(width: size.width, height: UIScreenPixel))) - self.tabsContainerNode.update(size: CGSize(width: size.width, height: tabsHeight), presentationData: presentationData, paneList: availablePanes.map { key in + self.tabsContainerNode.update(size: CGSize(width: size.width - sideInset * 2.0, height: tabsHeight), presentationData: presentationData, paneList: availablePanes.map { key in let title: String var icons: [TelegramMediaFile] = [] switch key { diff --git a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreen.swift b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreen.swift index bddb6cad7c..f765a2a9d4 100644 --- a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreen.swift +++ b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreen.swift @@ -8906,7 +8906,7 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro private func editingOpenNameColorSetup() { if self.peerId == self.context.account.peerId { - let controller = PeerNameColorScreen(context: self.context, updatedPresentationData: self.controller?.updatedPresentationData, subject: .account) + let controller = UserAppearanceScreen(context: self.context, updatedPresentationData: self.controller?.updatedPresentationData) self.controller?.push(controller) } else if let peer = self.data?.peer, peer is TelegramChannel { self.controller?.push(ChannelAppearanceScreen(context: self.context, updatedPresentationData: self.controller?.updatedPresentationData, peerId: self.peerId, boostStatus: self.boostStatus)) @@ -9815,16 +9815,23 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro let premiumGiftOptions = self.data?.premiumGiftOptions ?? [] let premiumOptions = premiumGiftOptions.filter { $0.users == 1 }.map { CachedPremiumGiftOption(months: $0.months, currency: $0.currency, amount: $0.amount, botUrl: "", storeProductId: $0.storeProductId) } + var hasBirthday = false + if let cachedUserData = self.data?.cachedData as? CachedUserData { + hasBirthday = hasBirthdayToday(cachedData: cachedUserData) + } + let controller = self.context.sharedContext.makeGiftOptionsController( context: self.context, peerId: self.peerId, premiumOptions: premiumOptions, - hasBirthday: false, + hasBirthday: hasBirthday, completion: { [weak self] in guard let self, let profileGiftsContext = self.data?.profileGiftsContext else { return } - profileGiftsContext.reload() + Queue.mainQueue().after(0.5) { + profileGiftsContext.reload() + } } ) self.controller?.push(controller) @@ -10916,7 +10923,7 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro guard let controller = self.controller else { return } - guard let data = self.data, let channel = data.peer as? TelegramChannel, channel.hasPermission(.sendSomething), let giftsContext = data.profileGiftsContext else { + guard let data = self.data, let channel = data.peer as? TelegramChannel, let giftsContext = data.profileGiftsContext else { return } @@ -10979,18 +10986,20 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro toggleFilter(.unique) }))) - items.append(.separator) - - items.append(.action(ContextMenuActionItem(text: strings.PeerInfo_Gifts_Displayed, icon: { theme in - return filter.contains(.displayed) ? generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Check"), color: theme.contextMenu.primaryColor) : nil - }, action: { _, f in - toggleFilter(.displayed) - }))) - items.append(.action(ContextMenuActionItem(text: strings.PeerInfo_Gifts_Hidden, icon: { theme in - return filter.contains(.hidden) ? generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Check"), color: theme.contextMenu.primaryColor) : nil - }, action: { _, f in - toggleFilter(.hidden) - }))) + if channel.hasPermission(.sendSomething) { + items.append(.separator) + + items.append(.action(ContextMenuActionItem(text: strings.PeerInfo_Gifts_Displayed, icon: { theme in + return filter.contains(.displayed) ? generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Check"), color: theme.contextMenu.primaryColor) : nil + }, action: { _, f in + toggleFilter(.displayed) + }))) + items.append(.action(ContextMenuActionItem(text: strings.PeerInfo_Gifts_Hidden, icon: { theme in + return filter.contains(.hidden) ? generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Check"), color: theme.contextMenu.primaryColor) : nil + }, action: { _, f in + toggleFilter(.hidden) + }))) + } return ContextController.Items(content: .list(items)) } @@ -12019,7 +12028,7 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro rightNavigationButtons.append(PeerInfoHeaderNavigationButtonSpec(key: .more, isForExpandedView: true)) } case .gifts: - if let data = self.data, let channel = data.peer as? TelegramChannel, channel.hasPermission(.sendSomething) { + if let data = self.data, let channel = data.peer as? TelegramChannel, case .broadcast = channel.info { rightNavigationButtons.append(PeerInfoHeaderNavigationButtonSpec(key: .sort, isForExpandedView: true)) } default: @@ -12294,14 +12303,8 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro } self.didPlayBirthdayAnimation = true - - var hasBirthdayToday = false - let today = Calendar.current.dateComponents(Set([.day, .month]), from: Date()) - if today.day == Int(birthday.day) && today.month == Int(birthday.month) { - hasBirthdayToday = true - } - if hasBirthdayToday { + if hasBirthdayToday(cachedData: cachedData) { Queue.mainQueue().after(0.3) { var birthdayItemFrame: CGRect? if let section = self.regularSections[InfoSection.peerInfo] { diff --git a/submodules/TelegramUI/Components/PeerInfo/PeerInfoVisualMediaPaneNode/Sources/PeerInfoGiftsPaneNode.swift b/submodules/TelegramUI/Components/PeerInfo/PeerInfoVisualMediaPaneNode/Sources/PeerInfoGiftsPaneNode.swift index e905b5b021..88df209521 100644 --- a/submodules/TelegramUI/Components/PeerInfo/PeerInfoVisualMediaPaneNode/Sources/PeerInfoGiftsPaneNode.swift +++ b/submodules/TelegramUI/Components/PeerInfo/PeerInfoVisualMediaPaneNode/Sources/PeerInfoGiftsPaneNode.swift @@ -9,6 +9,7 @@ import AccountContext import ContextUI import PhotoResources import TelegramUIPreferences +import TelegramStringFormatting import ItemListPeerItem import ItemListPeerActionItem import MergeLists @@ -48,6 +49,7 @@ public final class PeerInfoGiftsPaneNode: ASDisplayNode, PeerInfoPaneNode, UIScr private var panelButton: SolidRoundedButtonNode? private var panelCheck: ComponentView? + private let emptyResultsClippingView = UIView() private let emptyResultsAnimation = ComponentView() private let emptyResultsTitle = ComponentView() private let emptyResultsAction = ComponentView() @@ -93,7 +95,7 @@ public final class PeerInfoGiftsPaneNode: ASDisplayNode, PeerInfoPaneNode, UIScr self.addSubnode(self.backgroundNode) self.addSubnode(self.scrollNode) - + self.dataDisposable = (profileGifts.state |> deliverOnMainQueue).startStrict(next: { [weak self] state in guard let self else { @@ -125,6 +127,9 @@ public final class PeerInfoGiftsPaneNode: ASDisplayNode, PeerInfoPaneNode, UIScr self.scrollNode.view.contentInsetAdjustmentBehavior = .never self.scrollNode.view.delegate = self + + self.emptyResultsClippingView.clipsToBounds = true + self.scrollNode.view.addSubview(self.emptyResultsClippingView) } public func ensureMessageIsVisible(id: MessageId) { @@ -145,7 +150,7 @@ public final class PeerInfoGiftsPaneNode: ASDisplayNode, PeerInfoPaneNode, UIScr let optionSpacing: CGFloat = 10.0 let itemsSideInset = params.sideInset + 16.0 - let defaultItemsInRow = 3 + let defaultItemsInRow = params.size.width > params.size.height ? 5 : 3 let itemsInRow = max(1, min(starsProducts.count, defaultItemsInRow)) let defaultOptionWidth = (params.size.width - itemsSideInset * 2.0 - optionSpacing * CGFloat(defaultItemsInRow - 1)) / CGFloat(defaultItemsInRow) let optionWidth = (params.size.width - itemsSideInset * 2.0 - optionSpacing * CGFloat(itemsInRow - 1)) / CGFloat(itemsInRow) @@ -489,6 +494,11 @@ public final class PeerInfoGiftsPaneNode: ASDisplayNode, PeerInfoPaneNode, UIScr let emptyAnimationSpacing: CGFloat = 20.0 let emptyTextSpacing: CGFloat = 18.0 + self.emptyResultsClippingView.isHidden = false + + transition.setFrame(view: self.emptyResultsClippingView, frame: CGRect(origin: CGPoint(x: 0.0, y: 48.0), size: self.scrollNode.frame.size)) + transition.setBounds(view: self.emptyResultsClippingView, bounds: CGRect(origin: CGPoint(x: 0.0, y: 48.0), size: self.scrollNode.frame.size)) + let emptyResultsTitleSize = self.emptyResultsTitle.update( transition: .immediate, component: AnyComponent( @@ -517,7 +527,8 @@ public final class PeerInfoGiftsPaneNode: ASDisplayNode, PeerInfoPaneNode, UIScr return } self.profileGifts.updateFilter(.All) - } + }, + animateScale: false ) ), environment: {}, @@ -545,7 +556,7 @@ public final class PeerInfoGiftsPaneNode: ASDisplayNode, PeerInfoPaneNode, UIScr if view.superview == nil { view.alpha = 0.0 fadeTransition.setAlpha(view: view, alpha: 1.0) - self.scrollNode.view.addSubview(view) + self.emptyResultsClippingView.addSubview(view) view.playOnce() } view.bounds = CGRect(origin: .zero, size: emptyResultsAnimationFrame.size) @@ -555,7 +566,7 @@ public final class PeerInfoGiftsPaneNode: ASDisplayNode, PeerInfoPaneNode, UIScr if view.superview == nil { view.alpha = 0.0 fadeTransition.setAlpha(view: view, alpha: 1.0) - self.scrollNode.view.addSubview(view) + self.emptyResultsClippingView.addSubview(view) } view.bounds = CGRect(origin: .zero, size: emptyResultsTitleFrame.size) transition.setPosition(view: view, position: emptyResultsTitleFrame.center) @@ -564,7 +575,7 @@ public final class PeerInfoGiftsPaneNode: ASDisplayNode, PeerInfoPaneNode, UIScr if view.superview == nil { view.alpha = 0.0 fadeTransition.setAlpha(view: view, alpha: 1.0) - self.scrollNode.view.addSubview(view) + self.emptyResultsClippingView.addSubview(view) } view.bounds = CGRect(origin: .zero, size: emptyResultsActionFrame.size) transition.setPosition(view: view, position: emptyResultsActionFrame.center) @@ -572,6 +583,7 @@ public final class PeerInfoGiftsPaneNode: ASDisplayNode, PeerInfoPaneNode, UIScr } else { if let view = self.emptyResultsAnimation.view { fadeTransition.setAlpha(view: view, alpha: 0.0, completion: { _ in + self.emptyResultsClippingView.isHidden = true view.removeFromSuperview() }) } @@ -650,8 +662,21 @@ public final class PeerInfoGiftsPaneNode: ASDisplayNode, PeerInfoPaneNode, UIScr self.chatControllerInteraction.navigationController()?.pushViewController(controller) }) } else { - let controller = self.context.sharedContext.makeGiftOptionsController(context: self.context, peerId: self.peerId, premiumOptions: [], hasBirthday: false, completion: nil) - self.chatControllerInteraction.navigationController()?.pushViewController(controller) + let _ = (self.context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Birthday(id: self.peerId)) + |> deliverOnMainQueue).start(next: { birthday in + var hasBirthday = false + if let birthday { + hasBirthday = hasBirthdayToday(birthday: birthday) + } + let controller = self.context.sharedContext.makeGiftOptionsController( + context: self.context, + peerId: self.peerId, + premiumOptions: [], + hasBirthday: hasBirthday, + completion: nil + ) + self.chatControllerInteraction.navigationController()?.pushViewController(controller) + }) } } diff --git a/submodules/TelegramUI/Components/Settings/PeerNameColorScreen/BUILD b/submodules/TelegramUI/Components/Settings/PeerNameColorScreen/BUILD index 59f5675707..6b4a2af12c 100644 --- a/submodules/TelegramUI/Components/Settings/PeerNameColorScreen/BUILD +++ b/submodules/TelegramUI/Components/Settings/PeerNameColorScreen/BUILD @@ -52,6 +52,8 @@ swift_library( "//submodules/TelegramUI/Components/Chat/ChatMessageItemImpl", "//submodules/TelegramUI/Components/Settings/PeerNameColorItem", "//submodules/TelegramUI/Components/EmojiActionIconComponent", + "//submodules/TelegramUI/Components/TabSelectorComponent", + "//submodules/TelegramUI/Components/PlainButtonComponent", ], visibility = [ "//visibility:public", diff --git a/submodules/TelegramUI/Components/Settings/PeerNameColorScreen/Sources/ChannelAppearanceScreen.swift b/submodules/TelegramUI/Components/Settings/PeerNameColorScreen/Sources/ChannelAppearanceScreen.swift index 7aaca4ddeb..8213aa5d9d 100644 --- a/submodules/TelegramUI/Components/Settings/PeerNameColorScreen/Sources/ChannelAppearanceScreen.swift +++ b/submodules/TelegramUI/Components/Settings/PeerNameColorScreen/Sources/ChannelAppearanceScreen.swift @@ -1071,7 +1071,8 @@ final class ChannelAppearanceScreenComponent: Component { isGroup ? environment.strings.Conversation_StatusMembers(Int32($0)) : environment.strings.Conversation_StatusSubscribers(Int32($0)) }, files: self.cachedIconFiles, - nameDisplayOrder: presentationData.nameDisplayOrder + nameDisplayOrder: presentationData.nameDisplayOrder, + showBackground: false ), params: ListViewItemLayoutParams(width: availableSize.width, leftInset: 0.0, rightInset: 0.0, availableHeight: 10000.0, isStandalone: true) ))), diff --git a/submodules/TelegramUI/Components/Settings/PeerNameColorScreen/Sources/GiftListItemComponent.swift b/submodules/TelegramUI/Components/Settings/PeerNameColorScreen/Sources/GiftListItemComponent.swift new file mode 100644 index 0000000000..caf03a252b --- /dev/null +++ b/submodules/TelegramUI/Components/Settings/PeerNameColorScreen/Sources/GiftListItemComponent.swift @@ -0,0 +1,198 @@ +import Foundation +import UIKit +import Display +import ComponentFlow +import TelegramCore +import GiftItemComponent +import PlainButtonComponent +import TelegramPresentationData +import AccountContext + +final class GiftListItemComponent: Component { + let context: AccountContext + let theme: PresentationTheme + let gifts: [StarGift.UniqueGift] + let selectedId: Int64? + let selectionUpdated: (StarGift.UniqueGift) -> Void + let tag: AnyObject? + + init( + context: AccountContext, + theme: PresentationTheme, + gifts: [StarGift.UniqueGift], + selectedId: Int64?, + selectionUpdated: @escaping (StarGift.UniqueGift) -> Void, + tag: AnyObject? + ) { + self.context = context + self.theme = theme + self.gifts = gifts + self.selectedId = selectedId + self.selectionUpdated = selectionUpdated + self.tag = tag + } + + static func ==(lhs: GiftListItemComponent, rhs: GiftListItemComponent) -> Bool { + if lhs.theme !== rhs.theme { + return false + } + if lhs.gifts != rhs.gifts { + return false + } + if lhs.selectedId != rhs.selectedId { + return false + } + if lhs.tag !== rhs.tag { + return false + } + return true + } + + final class View: UIView, ComponentTaggedView { + public func matches(tag: Any) -> Bool { + if let component = self.component, let componentTag = component.tag { + let tag = tag as AnyObject + if componentTag === tag { + return true + } + } + return false + } + + private var giftItems: [AnyHashable: ComponentView] = [:] + + private var component: GiftListItemComponent? + private var state: EmptyComponentState? + + override public init(frame: CGRect) { + super.init(frame: frame) + } + + required public init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + private var visibleBounds: CGRect? + func updateVisibleBounds(_ bounds: CGRect) { + self.visibleBounds = bounds + self.state?.updated() + } + + func update(component: GiftListItemComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { + self.component = component + self.state = state + + let sideInset: CGFloat = 16.0 + let topInset: CGFloat = 13.0 + let spacing: CGFloat = 10.0 + let itemsInRow = 3 + let rowsCount = Int(ceil(CGFloat(component.gifts.count) / CGFloat(itemsInRow))) + + let itemWidth = floorToScreenPixels((availableSize.width - sideInset * 2.0 - spacing * CGFloat(itemsInRow - 1)) / CGFloat(itemsInRow)) + var validIds: [AnyHashable] = [] + var itemFrame = CGRect(origin: CGPoint(x: sideInset, y: topInset), size: CGSize(width: itemWidth, height: itemWidth)) + + let contentHeight = topInset * 2.0 + itemWidth * CGFloat(rowsCount) + spacing * CGFloat(rowsCount - 1) + + var index: Int32 = 0 + for gift in component.gifts { + var isVisible = false + if let visibleBounds = self.visibleBounds, visibleBounds.intersects(itemFrame) { + isVisible = true + } + if isVisible { + let id = gift.id + let itemId = AnyHashable(id) + validIds.append(itemId) + + var itemTransition = transition + let visibleItem: ComponentView + if let current = self.giftItems[itemId] { + visibleItem = current + } else { + visibleItem = ComponentView() + self.giftItems[itemId] = visibleItem + itemTransition = .immediate + } + + let _ = visibleItem.update( + transition: itemTransition, + component: AnyComponent( + PlainButtonComponent( + content: AnyComponent( + GiftItemComponent( + context: component.context, + theme: component.theme, + peer: nil, + subject: .uniqueGift(gift: gift), + ribbon: nil, + isHidden: false, + isSelected: gift.id == component.selectedId, + mode: .grid + ) + ), + effectAlignment: .center, + action: { [weak self] in + guard let self, let component = self.component else { + return + } + component.selectionUpdated(gift) + }, + animateAlpha: false + ) + ), + environment: {}, + containerSize: itemFrame.size + ) + if let itemView = visibleItem.view { + if itemView.superview == nil { + self.addSubview(itemView) + + if !transition.animation.isImmediate { + itemView.layer.animateScale(from: 0.01, to: 1.0, duration: 0.25) + itemView.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.25) + } + } + itemTransition.setFrame(view: itemView, frame: itemFrame) + } + } + itemFrame.origin.x += itemFrame.width + spacing + if itemFrame.maxX > availableSize.width { + itemFrame.origin.x = sideInset + itemFrame.origin.y += itemFrame.height + spacing + } + index += 1 + } + + var removeIds: [AnyHashable] = [] + for (id, item) in self.giftItems { + if !validIds.contains(id) { + removeIds.append(id) + if let itemView = item.view { + if !transition.animation.isImmediate { + itemView.layer.animateScale(from: 1.0, to: 0.01, duration: 0.25, removeOnCompletion: false) + itemView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.25, removeOnCompletion: false, completion: { _ in + itemView.removeFromSuperview() + }) + } else { + itemView.removeFromSuperview() + } + } + } + } + for id in removeIds { + self.giftItems.removeValue(forKey: id) + } + + return CGSize(width: availableSize.width, height: contentHeight) + } + } + + 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/Settings/PeerNameColorScreen/Sources/PeerNameColorChatPreviewItem.swift b/submodules/TelegramUI/Components/Settings/PeerNameColorScreen/Sources/PeerNameColorChatPreviewItem.swift index f06eaa11d2..5c5028f37b 100644 --- a/submodules/TelegramUI/Components/Settings/PeerNameColorScreen/Sources/PeerNameColorChatPreviewItem.swift +++ b/submodules/TelegramUI/Components/Settings/PeerNameColorScreen/Sources/PeerNameColorChatPreviewItem.swift @@ -308,7 +308,7 @@ final class PeerNameColorChatPreviewItemNode: ListViewItemNode { if let snapshot = strongSelf.view.snapshotView(afterScreenUpdates: false) { snapshot.frame = CGRect(origin: CGPoint(x: 0.0, y: -insets.top), size: snapshot.frame.size) strongSelf.view.addSubview(snapshot) - snapshot.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, delay: 0.25, removeOnCompletion: false, completion: { _ in + snapshot.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, delay: 0.0, removeOnCompletion: false, completion: { _ in snapshot.removeFromSuperview() }) } diff --git a/submodules/TelegramUI/Components/Settings/PeerNameColorScreen/Sources/PeerNameColorProfilePreviewItem.swift b/submodules/TelegramUI/Components/Settings/PeerNameColorScreen/Sources/PeerNameColorProfilePreviewItem.swift index 5b4358da3c..bcf4f6fb0c 100644 --- a/submodules/TelegramUI/Components/Settings/PeerNameColorScreen/Sources/PeerNameColorProfilePreviewItem.swift +++ b/submodules/TelegramUI/Components/Settings/PeerNameColorScreen/Sources/PeerNameColorProfilePreviewItem.swift @@ -29,8 +29,9 @@ final class PeerNameColorProfilePreviewItem: ListViewItem, ItemListItem, ListIte let subtitleString: String? let files: [Int64: TelegramMediaFile] let nameDisplayOrder: PresentationPersonNameOrder + let showBackground: Bool - init(context: AccountContext, theme: PresentationTheme, componentTheme: PresentationTheme, strings: PresentationStrings, topInset: CGFloat, sectionId: ItemListSectionId, peer: EnginePeer?, subtitleString: String? = nil, files: [Int64: TelegramMediaFile], nameDisplayOrder: PresentationPersonNameOrder) { + init(context: AccountContext, theme: PresentationTheme, componentTheme: PresentationTheme, strings: PresentationStrings, topInset: CGFloat, sectionId: ItemListSectionId, peer: EnginePeer?, subtitleString: String? = nil, files: [Int64: TelegramMediaFile], nameDisplayOrder: PresentationPersonNameOrder, showBackground: Bool) { self.context = context self.theme = theme self.componentTheme = componentTheme @@ -41,6 +42,7 @@ final class PeerNameColorProfilePreviewItem: ListViewItem, ItemListItem, ListIte self.subtitleString = subtitleString self.files = files self.nameDisplayOrder = nameDisplayOrder + self.showBackground = showBackground } func nodeConfiguredForParams(async: @escaping (@escaping () -> Void) -> Void, params: ListViewItemLayoutParams, synchronousLoads: Bool, previousItem: ListViewItem?, nextItem: ListViewItem?, completion: @escaping (ListViewItemNode, @escaping () -> (Signal?, (ListViewItemApply) -> Void)) -> Void) { @@ -102,7 +104,9 @@ final class PeerNameColorProfilePreviewItem: ListViewItem, ItemListItem, ListIte if lhs.nameDisplayOrder != rhs.nameDisplayOrder { return false } - + if lhs.showBackground != rhs.showBackground { + return false + } return true } } @@ -114,6 +118,7 @@ final class PeerNameColorProfilePreviewItemNode: ListViewItemNode { private let subtitle = ComponentView() private var icon: ComponentView? + private let backgroundNode: ASDisplayNode private let topStripeNode: ASDisplayNode private let bottomStripeNode: ASDisplayNode private let maskNode: ASImageNode @@ -121,6 +126,9 @@ final class PeerNameColorProfilePreviewItemNode: ListViewItemNode { private var item: PeerNameColorProfilePreviewItem? init() { + self.backgroundNode = ASDisplayNode() + self.backgroundNode.isLayerBacked = true + self.topStripeNode = ASDisplayNode() self.topStripeNode.isLayerBacked = true @@ -137,10 +145,7 @@ final class PeerNameColorProfilePreviewItemNode: ListViewItemNode { self.clipsToBounds = true self.isUserInteractionEnabled = false } - - deinit { - } - + func asyncLayout() -> (_ item: PeerNameColorProfilePreviewItem, _ params: ListViewItemLayoutParams, _ neighbors: ItemListNeighbors) -> (ListViewItemNodeLayout, (ListViewItemUpdateAnimation) -> Void) { return { [weak self] item, params, neighbors in let separatorHeight = UIScreenPixel @@ -158,15 +163,19 @@ final class PeerNameColorProfilePreviewItemNode: ListViewItemNode { guard let self else { return } - if let previousItem = self.item, (previousItem.peer?.profileColor != item.peer?.profileColor) || (previousItem.peer?.profileBackgroundEmojiId != item.peer?.profileBackgroundEmojiId) { + if let previousItem = self.item, (previousItem.peer?.nameColor != item.peer?.nameColor) || (previousItem.peer?.profileColor != item.peer?.profileColor) || (previousItem.peer?.profileBackgroundEmojiId != item.peer?.profileBackgroundEmojiId) { UIView.transition(with: self.view, duration: 0.2, options: UIView.AnimationOptions.transitionCrossDissolve, animations: { }) } self.item = item + self.backgroundNode.backgroundColor = item.theme.rootController.navigationBar.opaqueBackgroundColor self.topStripeNode.backgroundColor = item.theme.list.itemBlocksSeparatorColor self.bottomStripeNode.backgroundColor = item.theme.list.itemBlocksSeparatorColor + if self.backgroundNode.supernode == nil { + self.addSubnode(self.backgroundNode) + } if self.topStripeNode.supernode == nil { self.addSubnode(self.topStripeNode) } @@ -178,10 +187,19 @@ final class PeerNameColorProfilePreviewItemNode: ListViewItemNode { } if params.isStandalone { + let transition = ContainedViewLayoutTransition.animated(duration: 0.2, curve: .easeInOut) + transition.updateAlpha(node: self.backgroundNode, alpha: item.showBackground ? 1.0 : 0.0) + transition.updateAlpha(node: self.bottomStripeNode, alpha: item.showBackground ? 1.0 : 0.0) + + self.backgroundNode.isHidden = false self.topStripeNode.isHidden = true - self.bottomStripeNode.isHidden = true + self.bottomStripeNode.isHidden = false self.maskNode.isHidden = true + + self.bottomStripeNode.frame = CGRect(origin: CGPoint(x: 0.0, y: contentSize.height - separatorHeight), size: CGSize(width: layoutSize.width, height: separatorHeight)) } else { + self.backgroundNode.isHidden = true + let hasCorners = itemListHasRoundedBlockLayout(params) var hasTopCorners = false var hasBottomCorners = false @@ -219,11 +237,19 @@ final class PeerNameColorProfilePreviewItemNode: ListViewItemNode { let avatarSize: CGFloat = 104.0 let avatarFrame = CGRect(origin: CGPoint(x: floor((coverFrame.width - avatarSize) * 0.5), y: coverFrame.minY + item.topInset + 24.0), size: CGSize(width: avatarSize, height: avatarSize)) + let subject: PeerInfoCoverComponent.Subject? + if let status = item.peer?.emojiStatus, case .starGift = status.content { + subject = .status(status) + } else if let peer = item.peer { + subject = .peer(peer) + } else { + subject = nil + } let _ = self.background.update( transition: .immediate, component: AnyComponent(PeerInfoCoverComponent( context: item.context, - subject: item.peer.flatMap { .peer($0) }, + subject: subject, files: item.files, isDark: item.theme.overallDarkAppearance, avatarCenter: avatarFrame.center, @@ -238,7 +264,7 @@ final class PeerNameColorProfilePreviewItemNode: ListViewItemNode { if let backgroundView = self.background.view { if backgroundView.superview == nil { backgroundView.clipsToBounds = true - self.view.insertSubview(backgroundView, at: 0) + self.view.insertSubview(backgroundView, at: 1) } backgroundView.frame = coverFrame } @@ -296,7 +322,9 @@ final class PeerNameColorProfilePreviewItemNode: ListViewItemNode { } let statusColor: UIColor - if let peer = item.peer, peer.profileColor != nil { + if let status = item.peer?.emojiStatus, case .starGift = status.content { + statusColor = .white + } else if let peer = item.peer, peer.profileColor != nil { statusColor = .white } else { statusColor = item.theme.list.itemCheckColors.fillColor @@ -321,7 +349,13 @@ final class PeerNameColorProfilePreviewItemNode: ListViewItemNode { let backgroundColor: UIColor let titleColor: UIColor let subtitleColor: UIColor - if let peer = item.peer, let profileColor = peer.profileColor { + var particleColor: UIColor? + if let status = item.peer?.emojiStatus, case let .starGift(_, _, _, _, _, _, outerColor, _, _) = status.content { + titleColor = .white + backgroundColor = UIColor(rgb: UInt32(bitPattern: outerColor)) + subtitleColor = UIColor(white: 1.0, alpha: 0.6).blitOver(backgroundColor.withMultiplied(hue: 1.0, saturation: 2.2, brightness: 1.5), alpha: 1.0) + particleColor = .white + } else if let peer = item.peer, let profileColor = peer.profileColor { titleColor = .white backgroundColor = item.context.peerNameColors.getProfile(profileColor).main subtitleColor = UIColor(white: 1.0, alpha: 0.6).blitOver(backgroundColor.withMultiplied(hue: 1.0, saturation: 2.2, brightness: 1.5), alpha: 1.0) @@ -384,6 +418,7 @@ final class PeerNameColorProfilePreviewItemNode: ListViewItemNode { animationCache: item.context.animationCache, animationRenderer: item.context.animationRenderer, content: emojiStatusContent, + particleColor: particleColor, isVisibleForAnimations: true, action: nil )), @@ -424,6 +459,7 @@ final class PeerNameColorProfilePreviewItemNode: ListViewItemNode { } self.maskNode.frame = backgroundFrame.insetBy(dx: params.leftInset, dy: 0.0) + self.backgroundNode.frame = backgroundFrame }) } } diff --git a/submodules/TelegramUI/Components/Settings/PeerNameColorScreen/Sources/PeerNameColorScreen.swift b/submodules/TelegramUI/Components/Settings/PeerNameColorScreen/Sources/PeerNameColorScreen.swift deleted file mode 100644 index 42c3ea0025..0000000000 --- a/submodules/TelegramUI/Components/Settings/PeerNameColorScreen/Sources/PeerNameColorScreen.swift +++ /dev/null @@ -1,936 +0,0 @@ -import Foundation -import UIKit -import Display -import AsyncDisplayKit -import SwiftSignalKit -import Postbox -import TelegramCore -import TelegramPresentationData -import TelegramUIPreferences -import ItemListUI -import PresentationDataUtils -import AccountContext -import UndoUI -import EntityKeyboard -import PremiumUI -import PeerNameColorItem - -private final class PeerNameColorScreenArguments { - let context: AccountContext - let updateNameColor: (PeerNameColor?) -> Void - let updateBackgroundEmojiId: (Int64?, TelegramMediaFile?) -> Void - let resetColor: () -> Void - - init( - context: AccountContext, - updateNameColor: @escaping (PeerNameColor?) -> Void, - updateBackgroundEmojiId: @escaping (Int64?, TelegramMediaFile?) -> Void, - resetColor: @escaping () -> Void - ) { - self.context = context - self.updateNameColor = updateNameColor - self.updateBackgroundEmojiId = updateBackgroundEmojiId - self.resetColor = resetColor - } -} - -private enum PeerNameColorScreenSection: Int32 { - case nameColor - case backgroundEmoji -} - -private enum PeerNameColorScreenEntry: ItemListNodeEntry { - enum StableId: Hashable { - case colorHeader - case colorMessage - case colorProfile - case colorPicker - case removeColor - case colorDescription - case backgroundEmojiHeader - case backgroundEmoji - } - - case colorHeader(String) - case colorMessage(wallpaper: TelegramWallpaper, fontSize: PresentationFontSize, bubbleCorners: PresentationChatBubbleCorners, dateTimeFormat: PresentationDateTimeFormat, nameDisplayOrder: PresentationPersonNameOrder, items: [PeerNameColorChatPreviewItem.MessageItem]) - case colorProfile(peer: EnginePeer?, files: [Int64: TelegramMediaFile], nameDisplayOrder: PresentationPersonNameOrder) - case colorPicker(colors: PeerNameColors, currentColor: PeerNameColor?, isProfile: Bool) - case removeColor - case colorDescription(String) - case backgroundEmojiHeader(String, String?) - case backgroundEmoji(EmojiPagerContentComponent, UIColor, Bool, Bool) - - var section: ItemListSectionId { - switch self { - case .colorHeader, .colorMessage, .colorProfile, .colorPicker, .removeColor, .colorDescription: - return PeerNameColorScreenSection.nameColor.rawValue - case .backgroundEmojiHeader, .backgroundEmoji: - return PeerNameColorScreenSection.backgroundEmoji.rawValue - } - } - - var stableId: StableId { - switch self { - case .colorHeader: - return .colorHeader - case .colorMessage: - return .colorMessage - case .colorProfile: - return .colorProfile - case .colorPicker: - return .colorPicker - case .removeColor: - return.removeColor - case .colorDescription: - return .colorDescription - case .backgroundEmojiHeader: - return .backgroundEmojiHeader - case .backgroundEmoji: - return .backgroundEmoji - } - } - - var sortId: Int { - switch self { - case .colorHeader: - return 0 - case .colorMessage: - return 1 - case .colorProfile: - return 2 - case .colorPicker: - return 3 - case .removeColor: - return 4 - case .colorDescription: - return 5 - case .backgroundEmojiHeader: - return 6 - case .backgroundEmoji: - return 7 - } - } - - static func ==(lhs: PeerNameColorScreenEntry, rhs: PeerNameColorScreenEntry) -> Bool { - switch lhs { - case let .colorHeader(text): - if case .colorHeader(text) = rhs { - return true - } else { - return false - } - case let .colorMessage(lhsWallpaper, lhsFontSize, lhsBubbleCorners, lhsDateTimeFormat, lhsNameDisplayOrder, lhsItems): - if case let .colorMessage(rhsWallpaper, rhsFontSize, rhsBubbleCorners, rhsDateTimeFormat, rhsNameDisplayOrder, rhsItems) = rhs, lhsWallpaper == rhsWallpaper, lhsFontSize == rhsFontSize, lhsBubbleCorners == rhsBubbleCorners, lhsDateTimeFormat == rhsDateTimeFormat, lhsNameDisplayOrder == rhsNameDisplayOrder, lhsItems == rhsItems { - return true - } else { - return false - } - case let .colorProfile(lhsPeer, lhsFiles, lhsNameDisplayOrder): - if case let .colorProfile(rhsPeer, rhsFiles, rhsNameDisplayOrder) = rhs { - if lhsPeer != rhsPeer { - return false - } - if lhsFiles != rhsFiles { - return false - } - if lhsNameDisplayOrder != rhsNameDisplayOrder { - return false - } - return true - } else { - return false - } - case let .colorPicker(lhsColors, lhsCurrentColor, lhsIsProfile): - if case let .colorPicker(rhsColors, rhsCurrentColor, rhsIsProfile) = rhs, lhsColors == rhsColors, lhsCurrentColor == rhsCurrentColor, lhsIsProfile == rhsIsProfile { - return true - } else { - return false - } - case .removeColor: - if case .removeColor = rhs { - return true - } else { - return false - } - case let .colorDescription(text): - if case .colorDescription(text) = rhs { - return true - } else { - return false - } - case let .backgroundEmojiHeader(text, action): - if case .backgroundEmojiHeader(text, action) = rhs { - return true - } else { - return false - } - case let .backgroundEmoji(lhsEmojiContent, lhsBackgroundIconColor, lhsIsProfile, lhsHasRemoveButton): - if case let .backgroundEmoji(rhsEmojiContent, rhsBackgroundIconColor, rhsIsProfile, rhsHasRemoveButton) = rhs, lhsEmojiContent == rhsEmojiContent, lhsBackgroundIconColor == rhsBackgroundIconColor, lhsIsProfile == rhsIsProfile, lhsHasRemoveButton == rhsHasRemoveButton { - return true - } else { - return false - } - } - } - - static func <(lhs: PeerNameColorScreenEntry, rhs: PeerNameColorScreenEntry) -> Bool { - return lhs.sortId < rhs.sortId - } - - func item(presentationData: ItemListPresentationData, arguments: Any) -> ListViewItem { - let arguments = arguments as! PeerNameColorScreenArguments - switch self { - case let .colorHeader(text): - return ItemListSectionHeaderItem(presentationData: presentationData, text: text, sectionId: self.section) - case let .colorMessage(wallpaper, fontSize, chatBubbleCorners, dateTimeFormat, nameDisplayOrder, items): - return PeerNameColorChatPreviewItem( - context: arguments.context, - theme: presentationData.theme, - componentTheme: presentationData.theme, - strings: presentationData.strings, - sectionId: self.section, - fontSize: fontSize, - chatBubbleCorners: chatBubbleCorners, - wallpaper: wallpaper, - dateTimeFormat: dateTimeFormat, - nameDisplayOrder: nameDisplayOrder, - messageItems: items - ) - case let .colorProfile(peer, files, nameDisplayOrder): - return PeerNameColorProfilePreviewItem( - context: arguments.context, - theme: presentationData.theme, - componentTheme: presentationData.theme, - strings: presentationData.strings, - topInset: 0.0, - sectionId: self.section, - peer: peer, - files: files, - nameDisplayOrder: nameDisplayOrder - ) - case let .colorPicker(colors, currentColor, isProfile): - return PeerNameColorItem( - theme: presentationData.theme, - colors: colors, - mode: isProfile ? .profile : .name, - currentColor: currentColor, - updated: { color in - if let color { - arguments.updateNameColor(color) - } - }, - sectionId: self.section - ) - case .removeColor: - return ItemListActionItem(presentationData: presentationData, title: presentationData.strings.ProfileColorSetup_ResetAction, kind: .generic, alignment: .natural, sectionId: self.section, style: .blocks, action: { - arguments.resetColor() - }) - case let .colorDescription(text): - return ItemListTextItem(presentationData: presentationData, text: .plain(text), sectionId: self.section) - case let .backgroundEmojiHeader(text, action): - return ItemListSectionHeaderItem(presentationData: presentationData, text: text, actionText: action, action: action != nil ? { - arguments.updateBackgroundEmojiId(0, nil) - } : nil, sectionId: self.section) - case let .backgroundEmoji(emojiContent, backgroundIconColor, isProfileColor, hasRemoveButton): - return EmojiPickerItem(context: arguments.context, theme: presentationData.theme, strings: presentationData.strings, emojiContent: emojiContent, backgroundIconColor: backgroundIconColor, isProfileColor: isProfileColor, hasRemoveButton: hasRemoveButton, sectionId: self.section) - } - } -} - -private struct PeerNameColorScreenState: Equatable { - var updatedNameColor: PeerNameColor? - var updatedBackgroundEmojiId: Int64? - var inProgress: Bool = false - var needsBoosts: Bool = false - - var updatedProfileColor: PeerNameColor? - var hasUpdatedProfileColor: Bool = false - var updatedProfileBackgroundEmojiId: Int64? - var hasUpdatedProfileBackgroundEmojiId: Bool = false - - var selectedTabIndex: Int = 0 - - var files: [Int64: TelegramMediaFile] = [:] -} - -private func peerNameColorScreenEntries( - nameColors: PeerNameColors, - presentationData: PresentationData, - state: PeerNameColorScreenState, - peer: EnginePeer?, - isPremium: Bool, - emojiContent: EmojiPagerContentComponent? -) -> [PeerNameColorScreenEntry] { - var entries: [PeerNameColorScreenEntry] = [] - - if let peer { - let nameColor: PeerNameColor - if let updatedNameColor = state.updatedNameColor { - nameColor = updatedNameColor - } else if let peerNameColor = peer.nameColor { - nameColor = peerNameColor - } else { - nameColor = .blue - } - - let colors = nameColors.get(nameColor, dark: presentationData.theme.overallDarkAppearance) - - let backgroundEmojiId: Int64? - if let updatedBackgroundEmojiId = state.updatedBackgroundEmojiId { - if updatedBackgroundEmojiId == 0 { - backgroundEmojiId = nil - } else { - backgroundEmojiId = updatedBackgroundEmojiId - } - } else if let emojiId = peer.backgroundEmojiId { - backgroundEmojiId = emojiId - } else { - backgroundEmojiId = nil - } - - let profileColor: PeerNameColor? - if state.hasUpdatedProfileColor { - profileColor = state.updatedProfileColor - } else { - profileColor = peer.profileColor - } - var selectedProfileEmojiId: Int64? - if state.hasUpdatedProfileBackgroundEmojiId { - selectedProfileEmojiId = state.updatedProfileBackgroundEmojiId - } else { - selectedProfileEmojiId = peer.profileBackgroundEmojiId - } - let profileColors = profileColor.flatMap { profileColor in nameColors.getProfile(profileColor, dark: presentationData.theme.overallDarkAppearance) } - - let replyText: String - let messageText: String - if case .channel = peer { - replyText = presentationData.strings.NameColor_ChatPreview_ReplyText_Channel - messageText = presentationData.strings.NameColor_ChatPreview_MessageText_Channel - } else { - replyText = presentationData.strings.NameColor_ChatPreview_ReplyText_Account - messageText = presentationData.strings.NameColor_ChatPreview_MessageText_Account - } - let messageItem = PeerNameColorChatPreviewItem.MessageItem( - outgoing: false, - peerId: PeerId(namespace: peer.id.namespace, id: PeerId.Id._internalFromInt64Value(0)), - author: peer.compactDisplayTitle, - photo: peer.profileImageRepresentations, - nameColor: nameColor, - backgroundEmojiId: backgroundEmojiId, - reply: (peer.compactDisplayTitle, replyText, nameColor), - linkPreview: (presentationData.strings.NameColor_ChatPreview_LinkSite, presentationData.strings.NameColor_ChatPreview_LinkTitle, presentationData.strings.NameColor_ChatPreview_LinkText), - text: messageText - ) - if state.selectedTabIndex == 0 { - entries.append(.colorMessage( - wallpaper: presentationData.chatWallpaper, - fontSize: presentationData.chatFontSize, - bubbleCorners: presentationData.chatBubbleCorners, - dateTimeFormat: presentationData.dateTimeFormat, - nameDisplayOrder: presentationData.nameDisplayOrder, - items: [messageItem] - )) - } else { - var updatedPeer = peer - switch updatedPeer { - case let .user(user): - updatedPeer = .user(user.withUpdatedNameColor(nameColor).withUpdatedBackgroundEmojiId(backgroundEmojiId).withUpdatedProfileColor(profileColor).withUpdatedProfileBackgroundEmojiId(selectedProfileEmojiId)) - case let .channel(channel): - updatedPeer = .channel(channel.withUpdatedNameColor(nameColor).withUpdatedBackgroundEmojiId(backgroundEmojiId).withUpdatedProfileColor(profileColor).withUpdatedProfileBackgroundEmojiId(selectedProfileEmojiId)) - default: - break - } - var files: [Int64: TelegramMediaFile] = [:] - if let fileId = updatedPeer.profileBackgroundEmojiId, let file = state.files[fileId] { - files[fileId] = file - } - entries.append(.colorProfile( - peer: updatedPeer, - files: files, - nameDisplayOrder: presentationData.nameDisplayOrder - )) - } - if state.selectedTabIndex == 0 { - entries.append(.colorPicker( - colors: nameColors, - currentColor: nameColor, - isProfile: false - )) - } else { - entries.append(.colorPicker( - colors: nameColors, - currentColor: profileColor, - isProfile: true - )) - } - if state.selectedTabIndex == 1 && profileColor != nil { - entries.append(.removeColor) - } - - if state.selectedTabIndex == 0 { - if case .channel = peer { - entries.append(.colorDescription(presentationData.strings.NameColor_ChatPreview_Description_Channel)) - } else { - entries.append(.colorDescription(presentationData.strings.NameColor_ChatPreview_Description_Account)) - } - - if let emojiContent { - var selectedItems = Set() - if let backgroundEmojiId { - selectedItems.insert(MediaId(namespace: Namespaces.Media.CloudFile, id: backgroundEmojiId)) - } - let emojiContent = emojiContent.withSelectedItems(selectedItems).withCustomTintColor(colors.main) - - entries.append(.backgroundEmojiHeader(presentationData.strings.NameColor_BackgroundEmoji_Title, (backgroundEmojiId != nil && backgroundEmojiId != 0) ? presentationData.strings.NameColor_BackgroundEmoji_Remove : nil)) - entries.append(.backgroundEmoji(emojiContent, colors.main, false, false)) - } - } else { - if let emojiContent { - var selectedItems = Set() - if let selectedProfileEmojiId { - selectedItems.insert(MediaId(namespace: Namespaces.Media.CloudFile, id: selectedProfileEmojiId)) - } - let emojiContent = emojiContent.withSelectedItems(selectedItems).withCustomTintColor(profileColors?.main ?? presentationData.theme.list.itemSecondaryTextColor) - - entries.append(.backgroundEmojiHeader(presentationData.strings.ProfileColorSetup_IconSectionTitle, (selectedProfileEmojiId != nil && selectedProfileEmojiId != 0) ? presentationData.strings.NameColor_BackgroundEmoji_Remove : nil)) - entries.append(.backgroundEmoji(emojiContent, profileColors?.main ?? presentationData.theme.list.itemSecondaryTextColor, true, profileColor != nil)) - } else { - if case .channel = peer { - entries.append(.colorDescription(presentationData.strings.ProfileColorSetup_ChannelColorInfoLabel)) - } else { - entries.append(.colorDescription(presentationData.strings.ProfileColorSetup_AccountColorInfoLabel)) - } - } - } - } - - return entries -} - -public enum PeerNameColorScreenSubject { - case account - case channel(EnginePeer.Id) -} - -public func PeerNameColorScreen( - context: AccountContext, - updatedPresentationData: (initial: PresentationData, signal: Signal)? = nil, - subject: PeerNameColorScreenSubject -) -> ViewController { - let statePromise = ValuePromise(PeerNameColorScreenState(), ignoreRepeated: true) - let stateValue = Atomic(value: PeerNameColorScreenState()) - let updateState: ((PeerNameColorScreenState) -> PeerNameColorScreenState) -> Void = { f in - statePromise.set(stateValue.modify { f($0) }) - } - - let premiumConfiguration = PremiumConfiguration.with(appConfiguration: context.currentAppConfiguration.with { $0 }) - - var presentImpl: ((ViewController) -> Void)? - var pushImpl: ((ViewController) -> Void)? - var dismissImpl: (() -> Void)? - var attemptNavigationImpl: ((@escaping () -> Void) -> Bool)? - var applyChangesImpl: (() -> Void)? - - let actionsDisposable = DisposableSet() - - let arguments = PeerNameColorScreenArguments( - context: context, - updateNameColor: { color in - updateState { state in - var updatedState = state - - if state.selectedTabIndex == 0 { - if let color { - updatedState.updatedNameColor = color - } - } else { - updatedState.updatedProfileColor = color - updatedState.hasUpdatedProfileColor = true - } - return updatedState - } - }, - updateBackgroundEmojiId: { emojiId, file in - updateState { state in - var updatedState = state - if state.selectedTabIndex == 0 { - updatedState.updatedBackgroundEmojiId = emojiId - } else { - updatedState.hasUpdatedProfileBackgroundEmojiId = true - updatedState.updatedProfileBackgroundEmojiId = emojiId - } - if let file { - updatedState.files[file.fileId.id] = file - } - return updatedState - } - }, - resetColor: { - updateState { state in - var updatedState = state - - if state.selectedTabIndex == 1 { - updatedState.updatedProfileColor = nil - updatedState.hasUpdatedProfileColor = true - updatedState.updatedProfileBackgroundEmojiId = nil - updatedState.hasUpdatedProfileBackgroundEmojiId = true - } - return updatedState - } - } - ) - - let peerId: EnginePeer.Id - switch subject { - case .account: - peerId = context.account.peerId - case let .channel(channelId): - peerId = channelId - } - - let emojiContent = EmojiPagerContentComponent.emojiInputData( - context: context, - animationCache: context.animationCache, - animationRenderer: context.animationRenderer, - isStandalone: false, - subject: .backgroundIcon, - hasTrending: false, - topReactionItems: [], - areUnicodeEmojiEnabled: false, - areCustomEmojiEnabled: true, - chatPeerId: context.account.peerId, - selectedItems: Set(), - backgroundIconColor: nil - ) - /*let emojiContent: Signal = combineLatest( - context.sharedContext.presentationData - ) - |> mapToSignal { presentationData, state, peer -> Signal<(EmojiPagerContentComponent, EmojiPagerContentComponent), NoError> in - var selectedEmojiId: Int64? - if let updatedBackgroundEmojiId = state.updatedBackgroundEmojiId { - selectedEmojiId = updatedBackgroundEmojiId - } else { - selectedEmojiId = peer?.backgroundEmojiId - } - let nameColor: PeerNameColor - if let updatedNameColor = state.updatedNameColor { - nameColor = updatedNameColor - } else { - nameColor = (peer?.nameColor ?? .blue) - } - - var selectedProfileEmojiId: Int64? - if state.hasUpdatedProfileBackgroundEmojiId { - selectedProfileEmojiId = state.updatedProfileBackgroundEmojiId - } else { - selectedProfileEmojiId = peer?.profileBackgroundEmojiId - } - let profileColor: PeerNameColor? - if state.hasUpdatedProfileColor { - profileColor = state.updatedProfileColor - } else { - profileColor = peer?.profileColor - } - - let color = context.peerNameColors.get(nameColor, dark: presentationData.theme.overallDarkAppearance) - let profileColorValue: UIColor? = profileColor.flatMap { profileColor in context.peerNameColors.getProfile(profileColor, dark: presentationData.theme.overallDarkAppearance).main } - - let selectedItems: [EngineMedia.Id] - if let selectedEmojiId, selectedEmojiId != 0 { - selectedItems = [EngineMedia.Id(namespace: Namespaces.Media.CloudFile, id: selectedEmojiId)] - } else { - selectedItems = [] - } - - let selectedProfileItems: [EngineMedia.Id] - if let selectedProfileEmojiId, selectedProfileEmojiId != 0 { - selectedProfileItems = [EngineMedia.Id(namespace: Namespaces.Media.CloudFile, id: selectedProfileEmojiId)] - } else { - selectedProfileItems = [] - } - }*/ - - let presentationData = updatedPresentationData?.signal ?? context.sharedContext.presentationData - let signal = combineLatest(queue: .mainQueue(), - presentationData, - statePromise.get(), - context.engine.stickers.availableReactions(), - context.engine.data.subscribe(TelegramEngine.EngineData.Item.Peer.Peer(id: peerId)), - emojiContent - ) - |> deliverOnMainQueue - |> map { presentationData, state, availableReactions, peer, emojiContent -> (ItemListControllerState, (ItemListNodeState, Any)) in - let isPremium = peer?.isPremium ?? false - let buttonTitle: String - let isLocked: Bool - switch subject { - case .account: - isLocked = !isPremium - case .channel: - isLocked = false - } - let _ = isLocked - - let backgroundEmojiId: Int64 - if let updatedBackgroundEmojiId = state.updatedBackgroundEmojiId { - backgroundEmojiId = updatedBackgroundEmojiId - } else if let emojiId = peer?.backgroundEmojiId { - backgroundEmojiId = emojiId - } else { - backgroundEmojiId = 0 - } - if backgroundEmojiId != 0 { - buttonTitle = presentationData.strings.NameColor_ApplyColorAndBackgroundEmoji - } else { - buttonTitle = presentationData.strings.NameColor_ApplyColor - } - let _ = buttonTitle - - /*let footerItem = ApplyColorFooterItem( - theme: presentationData.theme, - title: buttonTitle, - locked: isLocked, - inProgress: state.inProgress, - action: { - if !isLocked { - applyChangesImpl?() - } else { - HapticFeedback().impact(.light) - let controller = UndoOverlayController( - presentationData: presentationData, - content: .premiumPaywall( - title: nil, - text: presentationData.strings.NameColor_TooltipPremium_Account, - customUndoText: nil, - timeout: nil, - linkAction: nil - ), - elevatedLayout: false, - action: { action in - if case .info = action { - let controller = context.sharedContext.makePremiumIntroController(context: context, source: .nameColor, forceDark: false, dismissed: nil) - pushImpl?(controller) - } - return true - } - ) - presentImpl?(controller) - } - } - )*/ - - emojiContent.inputInteractionHolder.inputInteraction = EmojiPagerContentComponent.InputInteraction( - performItemAction: { _, item, _, _, _, _ in - var selectedFileId: Int64? - var selectedFile: TelegramMediaFile? - if let fileId = item.itemFile?.fileId.id { - selectedFileId = fileId - selectedFile = item.itemFile - } else { - selectedFileId = 0 - } - arguments.updateBackgroundEmojiId(selectedFileId, selectedFile) - }, - deleteBackwards: { - }, - openStickerSettings: { - }, - openFeatured: { - }, - openSearch: { - }, - addGroupAction: { groupId, isPremiumLocked, _ in - guard let collectionId = groupId.base as? ItemCollectionId else { - return - } - - let viewKey = PostboxViewKey.orderedItemList(id: Namespaces.OrderedItemList.CloudFeaturedEmojiPacks) - let _ = (context.account.postbox.combinedView(keys: [viewKey]) - |> take(1) - |> deliverOnMainQueue).start(next: { views in - guard let view = views.views[viewKey] as? OrderedItemListView else { - return - } - for featuredEmojiPack in view.items.lazy.map({ $0.contents.get(FeaturedStickerPackItem.self)! }) { - if featuredEmojiPack.info.id == collectionId { - let _ = context.engine.stickers.addStickerPackInteractively(info: featuredEmojiPack.info, items: featuredEmojiPack.topItems).start() - - break - } - } - }) - }, - clearGroup: { _ in - }, - editAction: { _ in - }, - pushController: { c in - }, - presentController: { c in - }, - presentGlobalOverlayController: { c in - }, - navigationController: { - return nil - }, - requestUpdate: { _ in - }, - updateSearchQuery: { _ in - }, - updateScrollingToItemGroup: { - }, - onScroll: {}, - chatPeerId: nil, - peekBehavior: nil, - customLayout: nil, - externalBackground: nil, - externalExpansionView: nil, - customContentView: nil, - useOpaqueTheme: true, - hideBackground: false, - stateContext: nil, - addImage: nil - ) - - let entries = peerNameColorScreenEntries( - nameColors: context.peerNameColors, - presentationData: presentationData, - state: state, - peer: peer, - isPremium: isPremium, - emojiContent: emojiContent - ) - - let title: ItemListControllerTitle = .sectionControl([presentationData.strings.ProfileColorSetup_TitleName, presentationData.strings.ProfileColorSetup_TitleProfile], state.selectedTabIndex) - - let controllerState = ItemListControllerState( - presentationData: ItemListPresentationData(presentationData), - title: title, - leftNavigationButton: nil, - rightNavigationButton: ItemListNavigationButton(content: .text(presentationData.strings.Common_Done), style: .bold, enabled: true, action: { - if !isLocked { - applyChangesImpl?() - } else { - HapticFeedback().impact(.light) - let controller = UndoOverlayController( - presentationData: presentationData, - content: .premiumPaywall( - title: nil, - text: presentationData.strings.NameColor_TooltipPremium_Account, - customUndoText: nil, - timeout: nil, - linkAction: nil - ), - elevatedLayout: false, - action: { action in - if case .info = action { - var replaceImpl: ((ViewController) -> Void)? - let controller = context.sharedContext.makePremiumDemoController(context: context, subject: .colors, forceDark: false, action: { - let controller = context.sharedContext.makePremiumIntroController(context: context, source: .settings, forceDark: false, dismissed: nil) - replaceImpl?(controller) - }, dismissed: nil) - replaceImpl = { [weak controller] c in - controller?.replace(with: c) - } - pushImpl?(controller) - } - return true - } - ) - presentImpl?(controller) - } - }), - backNavigationButton: ItemListBackButton(title: presentationData.strings.Common_Back), - animateChanges: false - ) - let listState = ItemListNodeState( - presentationData: ItemListPresentationData(presentationData), - entries: entries, - style: .blocks, - footerItem: nil, - animateChanges: false - ) - - return (controllerState, (listState, arguments)) - } - |> afterDisposed { - actionsDisposable.dispose() - } - - let controller = ItemListController(context: context, state: signal) - controller.supportedOrientations = ViewControllerSupportedOrientations(regularSize: .all, compactSize: .portrait) - controller.titleControlValueChanged = { value in - updateState { state in - var state = state - state.selectedTabIndex = value - return state - } - } - presentImpl = { [weak controller] c in - guard let controller else { - return - } - if c is UndoOverlayController { - controller.present(c, in: .current) - } else { - controller.present(c, in: .window(.root)) - } - } - pushImpl = { [weak controller] c in - guard let controller else { - return - } - controller.push(c) - } - dismissImpl = { [weak controller] in - guard let controller else { - return - } - controller.dismiss() - } - controller.attemptNavigation = { f in - return attemptNavigationImpl?(f) ?? true - } - attemptNavigationImpl = { f in - if case .account = subject, !context.isPremium { - return true - } - let state = stateValue.with({ $0 }) - if case .channel = subject, state.needsBoosts { - return true - } - var hasChanges = false - if state.updatedNameColor != nil || state.updatedBackgroundEmojiId != nil { - hasChanges = true - } - if hasChanges { - let presentationData = context.sharedContext.currentPresentationData.with { $0 } - presentImpl?(standardTextAlertController(theme: AlertControllerTheme(presentationData: presentationData), title: presentationData.strings.NameColor_UnsavedChanges_Title, text: presentationData.strings.NameColor_UnsavedChanges_Text, actions: [ - TextAlertAction(type: .genericAction, title: presentationData.strings.NameColor_UnsavedChanges_Discard, action: { - f() - dismissImpl?() - }), - TextAlertAction(type: .defaultAction, title: presentationData.strings.NameColor_UnsavedChanges_Apply, action: { - applyChangesImpl?() - }) - ])) - return false - } else { - return true - } - } - applyChangesImpl = { [weak controller] in - let _ = (context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: peerId)) - |> deliverOnMainQueue).startStandalone(next: { peer in - guard let peer else { - return - } - let state = stateValue.with { $0 } - if state.updatedNameColor == nil && state.updatedBackgroundEmojiId == nil && !state.hasUpdatedProfileColor && !state.hasUpdatedProfileBackgroundEmojiId { - dismissImpl?() - return - } - - let presentationData = context.sharedContext.currentPresentationData.with { $0 } - - let nameColor = state.updatedNameColor ?? peer.nameColor - let backgroundEmojiId = state.updatedBackgroundEmojiId ?? peer.backgroundEmojiId - let colors = context.peerNameColors.get(nameColor ?? .blue, dark: presentationData.theme.overallDarkAppearance) - - let profileColor = state.hasUpdatedProfileColor ? state.updatedProfileColor : peer.profileColor - let profileBackgroundEmojiId = state.hasUpdatedProfileBackgroundEmojiId ? state.updatedProfileBackgroundEmojiId : peer.profileBackgroundEmojiId - - switch subject { - case .account: - let _ = context.engine.accountData.updateNameColorAndEmoji(nameColor: nameColor ?? .blue, backgroundEmojiId: backgroundEmojiId ?? 0, profileColor: profileColor, profileBackgroundEmojiId: profileBackgroundEmojiId ?? 0).startStandalone() - - if let navigationController = controller?.navigationController as? NavigationController { - Queue.mainQueue().after(0.25) { - if let lastController = navigationController.viewControllers.last as? ViewController { - var colorList: [PeerNameColors.Colors] = [] - if let nameColor { - colorList.append(context.peerNameColors.get(nameColor, dark: presentationData.theme.overallDarkAppearance)) - } - if let profileColor { - colorList.append(context.peerNameColors.getProfile(profileColor, dark: presentationData.theme.overallDarkAppearance, subject: .palette)) - } - - let colorImage = generateSettingsMenuPeerColorsLabelIcon(colors: colorList) - - let tipController = UndoOverlayController(presentationData: presentationData, content: .image(image: colorImage, title: nil, text: presentationData.strings.ProfileColorSetup_ToastAccountColorUpdated, round: false, undoText: nil), elevatedLayout: false, position: .bottom, animateInAsReplacement: false, action: { _ in return false }) - lastController.present(tipController, in: .window(.root)) - } - } - } - - dismissImpl?() - case let .channel(peerId): - updateState { state in - var updatedState = state - updatedState.inProgress = true - return updatedState - } - let _ = (context.engine.peers.updatePeerNameColorAndEmoji(peerId: peerId, nameColor: nameColor ?? .blue, backgroundEmojiId: backgroundEmojiId ?? 0, profileColor: profileColor, profileBackgroundEmojiId: profileBackgroundEmojiId ?? 0) - |> deliverOnMainQueue).startStandalone(next: { - }, error: { error in - if case .channelBoostRequired = error { - updateState { state in - var updatedState = state - updatedState.needsBoosts = true - return updatedState - } - - let _ = combineLatest( - queue: Queue.mainQueue(), - context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: peerId)), - context.engine.peers.getChannelBoostStatus(peerId: peerId) - ).startStandalone(next: { peer, status in - guard let peer, let status else { - return - } - - updateState { state in - var updatedState = state - updatedState.inProgress = false - return updatedState - } - - let link = status.url - let controller = PremiumLimitScreen(context: context, subject: .storiesChannelBoost(peer: peer, boostSubject: .nameColors(colors: .blue), isCurrent: true, level: Int32(status.level), currentLevelBoosts: Int32(status.currentLevelBoosts), nextLevelBoosts: status.nextLevelBoosts.flatMap(Int32.init), link: link, myBoostCount: 0, canBoostAgain: false), count: Int32(status.boosts), action: { - UIPasteboard.general.string = link - let presentationData = context.sharedContext.currentPresentationData.with { $0 } - presentImpl?(UndoOverlayController(presentationData: presentationData, content: .linkCopied(title: nil, text: presentationData.strings.ChannelBoost_BoostLinkCopied), elevatedLayout: false, position: .bottom, animateInAsReplacement: false, action: { _ in return false })) - return true - }, openStats: nil, openGift: premiumConfiguration.giveawayGiftsPurchaseAvailable ? { - let controller = createGiveawayController(context: context, peerId: peerId, subject: .generic) - pushImpl?(controller) - } : nil) - pushImpl?(controller) - - HapticFeedback().impact(.light) - }) - } else { - updateState { state in - var updatedState = state - updatedState.inProgress = false - return updatedState - } - } - }, completed: { - if let navigationController = controller?.navigationController as? NavigationController { - Queue.mainQueue().after(0.25) { - if let lastController = navigationController.viewControllers.last as? ViewController { - let tipController = UndoOverlayController(presentationData: presentationData, content: .image(image: generatePeerNameColorImage(nameColor: colors, isDark: presentationData.theme.overallDarkAppearance, bounds: CGSize(width: 32.0, height: 32.0), size: CGSize(width: 22.0, height: 22.0))!, title: nil, text: presentationData.strings.NameColor_ChannelColorUpdated, round: false, undoText: nil), elevatedLayout: false, position: .bottom, animateInAsReplacement: false, action: { _ in return false }) - lastController.present(tipController, in: .window(.root)) - } - } - } - - dismissImpl?() - }) - } - }) - } - return controller -} diff --git a/submodules/TelegramUI/Components/Settings/PeerNameColorScreen/Sources/UserApperanceScreen.swift b/submodules/TelegramUI/Components/Settings/PeerNameColorScreen/Sources/UserApperanceScreen.swift new file mode 100644 index 0000000000..872397bec6 --- /dev/null +++ b/submodules/TelegramUI/Components/Settings/PeerNameColorScreen/Sources/UserApperanceScreen.swift @@ -0,0 +1,1304 @@ +import Foundation +import UIKit +import Photos +import Display +import AsyncDisplayKit +import SwiftSignalKit +import Postbox +import TelegramCore +import TelegramPresentationData +import TelegramUIPreferences +import ItemListUI +import PresentationDataUtils +import AccountContext +import UndoUI +import EntityKeyboard +import PremiumUI +import ComponentFlow +import BundleIconComponent +import AnimatedTextComponent +import ViewControllerComponent +import ButtonComponent +import ListItemComponentAdaptor +import ListSectionComponent +import MultilineTextComponent +import ListActionItemComponent +import EmojiStatusSelectionComponent +import EmojiStatusComponent +import DynamicCornerRadiusView +import ComponentDisplayAdapters +import BundleIconComponent +import Markdown +import PeerNameColorItem +import EmojiActionIconComponent +import TabSelectorComponent +import WallpaperResources + +private let giftListTag = GenericComponentViewTag() + +final class UserAppearanceScreenComponent: Component { + typealias EnvironmentType = ViewControllerComponentContainer.Environment + + let context: AccountContext + + init( + context: AccountContext + ) { + self.context = context + } + + static func ==(lhs: UserAppearanceScreenComponent, rhs: UserAppearanceScreenComponent) -> Bool { + if lhs.context !== rhs.context { + return false + } + return true + } + + private final class ContentsData { + let peer: EnginePeer? + let gifts: [StarGift.UniqueGift] + + init( + peer: EnginePeer?, + gifts: [StarGift.UniqueGift] + ) { + self.peer = peer + self.gifts = gifts + } + + static func get(context: AccountContext) -> Signal { + return combineLatest( + context.engine.data.subscribe( + TelegramEngine.EngineData.Item.Peer.Peer(id: context.account.peerId) + ), + context.account.postbox.itemCollectionsView(orderedItemListCollectionIds: [Namespaces.OrderedItemList.CloudUniqueStarGifts], namespaces: [Namespaces.ItemCollection.CloudDice], aroundIndex: nil, count: 10000000) + ) + |> map { peer, view -> ContentsData in + var gifts: [StarGift.UniqueGift] = [] + for orderedView in view.orderedItemListsViews { + if orderedView.collectionId == Namespaces.OrderedItemList.CloudUniqueStarGifts { + for item in orderedView.items { + guard let item = item.contents.get(RecentStarGiftItem.self) else { + continue + } + gifts.append(item.starGift) + } + } + } + return ContentsData( + peer: peer, + gifts: gifts + ) + } + } + } + + private final class ScrollView: UIScrollView { + override func touchesShouldCancel(in view: UIView) -> Bool { + return true + } + } + + private struct ResolvedState { + struct Changes: OptionSet { + var rawValue: Int32 + + init(rawValue: Int32) { + self.rawValue = rawValue + } + + static let nameColor = Changes(rawValue: 1 << 0) + static let profileColor = Changes(rawValue: 1 << 1) + static let replyFileId = Changes(rawValue: 1 << 2) + static let backgroundFileId = Changes(rawValue: 1 << 3) + static let emojiStatus = Changes(rawValue: 1 << 4) + } + + var nameColor: PeerNameColor + var profileColor: PeerNameColor? + var replyFileId: Int64? + var backgroundFileId: Int64? + var emojiStatus: PeerEmojiStatus? + + var changes: Changes + + init( + nameColor: PeerNameColor, + profileColor: PeerNameColor?, + replyFileId: Int64?, + backgroundFileId: Int64?, + emojiStatus: PeerEmojiStatus?, + changes: Changes + ) { + self.nameColor = nameColor + self.profileColor = profileColor + self.replyFileId = replyFileId + self.backgroundFileId = backgroundFileId + self.emojiStatus = emojiStatus + self.changes = changes + } + } + + final class View: UIView, UIScrollViewDelegate { + private let topOverscrollLayer = SimpleLayer() + private let scrollView: ScrollView + private let actionButton = ComponentView() + private let bottomPanelBackgroundView: BlurredBackgroundView + private let bottomPanelSeparator: SimpleLayer + + private let backButton = PeerInfoHeaderNavigationButton() + + private let tabSelector = ComponentView() + + private let previewSection = ComponentView() + private let boostSection = ComponentView() + private let bannerSection = ComponentView() + private let replySection = ComponentView() + private let wallpaperSection = ComponentView() + private let resetColorSection = ComponentView() + private let giftsSection = ComponentView() + + private var isUpdating: Bool = false + + private var component: UserAppearanceScreenComponent? + private(set) weak var state: EmptyComponentState? + private var environment: EnvironmentType? + + let isReady = ValuePromise(false, ignoreRepeated: true) + private var contentsData: ContentsData? + private var contentsDataDisposable: Disposable? + + private var cachedIconFiles: [Int64: TelegramMediaFile] = [:] + + private var updatedPeerNameColor: PeerNameColor? + private var updatedPeerNameEmoji: Int64?? + + private var updatedPeerProfileColor: PeerNameColor?? + private var updatedPeerProfileEmoji: Int64?? + private var updatedPeerStatus: PeerEmojiStatus?? + + private var currentTheme: PresentationThemeReference? + private var resolvedCurrentTheme: (reference: PresentationThemeReference, isDark: Bool, theme: PresentationTheme, wallpaper: TelegramWallpaper?)? + private var resolvingCurrentTheme: (reference: PresentationThemeReference, isDark: Bool, disposable: Disposable)? + + private var isApplyingSettings: Bool = false + private var applyDisposable: Disposable? + + private weak var emojiStatusSelectionController: ViewController? + + override init(frame: CGRect) { + self.scrollView = ScrollView() + self.scrollView.showsVerticalScrollIndicator = false + self.scrollView.showsHorizontalScrollIndicator = false + self.scrollView.scrollsToTop = false + self.scrollView.delaysContentTouches = false + self.scrollView.canCancelContentTouches = true + self.scrollView.contentInsetAdjustmentBehavior = .never + if #available(iOS 13.0, *) { + self.scrollView.automaticallyAdjustsScrollIndicatorInsets = false + } + self.scrollView.alwaysBounceVertical = true + + self.bottomPanelBackgroundView = BlurredBackgroundView(color: .clear, enableBlur: true) + self.bottomPanelSeparator = SimpleLayer() + + super.init(frame: frame) + + self.scrollView.delegate = self + self.addSubview(self.scrollView) + + self.scrollView.layer.addSublayer(self.topOverscrollLayer) + + self.addSubview(self.bottomPanelBackgroundView) + self.layer.addSublayer(self.bottomPanelSeparator) + + self.backButton.action = { [weak self] _, _ in + if let self, let controller = self.environment?.controller() { + controller.navigationController?.popViewController(animated: true) + } + } + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + deinit { + self.contentsDataDisposable?.dispose() + self.applyDisposable?.dispose() + self.resolvingCurrentTheme?.disposable.dispose() + } + + func scrollToTop() { + self.scrollView.setContentOffset(CGPoint(), animated: true) + } + + func attemptNavigation(complete: @escaping () -> Void) -> Bool { + guard let component = self.component, let resolvedState = self.resolveState() else { + return true + } + if self.isApplyingSettings { + return false + } + + if !resolvedState.changes.isEmpty { + let presentationData = component.context.sharedContext.currentPresentationData.with { $0 } + self.environment?.controller()?.present(standardTextAlertController(theme: AlertControllerTheme(presentationData: presentationData), title: presentationData.strings.Channel_Appearance_UnsavedChangesAlertTitle, text: presentationData.strings.Channel_Appearance_UnsavedChangesAlertText, actions: [ + TextAlertAction(type: .genericAction, title: presentationData.strings.Channel_Appearance_UnsavedChangesAlertDiscard, action: { [weak self] in + guard let self else { + return + } + self.environment?.controller()?.dismiss() + }), + TextAlertAction(type: .defaultAction, title: presentationData.strings.Channel_Appearance_UnsavedChangesAlertApply, action: { [weak self] in + guard let self else { + return + } + self.applySettings() + }) + ]), in: .window(.root)) + + return false + } + + return true + } + + func scrollViewDidScroll(_ scrollView: UIScrollView) { + self.updateScrolling(transition: .immediate) + } + + var scrolledUp = true + private func updateScrolling(transition: ComponentTransition) { + let navigationAlphaDistance: CGFloat = 16.0 + let navigationAlpha: CGFloat = max(0.0, min(1.0, self.scrollView.contentOffset.y / navigationAlphaDistance)) + if let controller = self.environment?.controller(), let navigationBar = controller.navigationBar { + navigationBar.backgroundNode.alpha = 0.0 + navigationBar.stripeNode.alpha = 0.0 + } + + var scrolledUp = false + if navigationAlpha < 0.5 { + scrolledUp = true + } else if navigationAlpha > 0.5 { + scrolledUp = false + } + + if self.scrolledUp != scrolledUp { + self.scrolledUp = scrolledUp + if !self.isUpdating { + self.state?.updated() + } + } + + let bottomNavigationAlphaDistance: CGFloat = 16.0 + let bottomNavigationAlpha: CGFloat = max(0.0, min(1.0, (self.scrollView.contentSize.height - self.scrollView.bounds.maxY) / bottomNavigationAlphaDistance)) + + transition.setAlpha(view: self.bottomPanelBackgroundView, alpha: bottomNavigationAlpha) + transition.setAlpha(layer: self.bottomPanelSeparator, alpha: bottomNavigationAlpha) + + if let giftListView = self.giftsSection.findTaggedView(tag: giftListTag) as? GiftListItemComponent.View { + let rect = self.scrollView.convert(self.scrollView.bounds, to: giftListView) + let visibleRect = giftListView.bounds.intersection(rect) + giftListView.updateVisibleBounds(visibleRect) + } + } + + private func resolveState() -> ResolvedState? { + guard let contentsData = self.contentsData, let peer = contentsData.peer else { + return nil + } + + var changes: ResolvedState.Changes = [] + + let nameColor: PeerNameColor + if let updatedPeerNameColor = self.updatedPeerNameColor { + nameColor = updatedPeerNameColor + } else if let peerNameColor = peer.nameColor { + nameColor = peerNameColor + } else { + nameColor = .blue + } + if nameColor != peer.nameColor { + changes.insert(.nameColor) + } + + let profileColor: PeerNameColor? + if case let .some(value) = self.updatedPeerProfileColor { + profileColor = value + } else if let peerProfileColor = peer.profileColor { + profileColor = peerProfileColor + } else { + profileColor = nil + } + if profileColor != peer.profileColor { + changes.insert(.profileColor) + } + + let replyFileId: Int64? + if case let .some(value) = self.updatedPeerNameEmoji { + replyFileId = value + } else { + replyFileId = peer.backgroundEmojiId + } + if replyFileId != peer.backgroundEmojiId { + changes.insert(.replyFileId) + } + + let backgroundFileId: Int64? + if case let .some(value) = self.updatedPeerProfileEmoji { + backgroundFileId = value + } else { + backgroundFileId = peer.profileBackgroundEmojiId + } + if backgroundFileId != peer.profileBackgroundEmojiId { + changes.insert(.backgroundFileId) + } + + let emojiStatus: PeerEmojiStatus? + if case let .some(value) = self.updatedPeerStatus { + emojiStatus = value + } else { + emojiStatus = peer.emojiStatus + } + if emojiStatus != peer.emojiStatus { + changes.insert(.emojiStatus) + } + + return ResolvedState( + nameColor: nameColor, + profileColor: profileColor, + replyFileId: replyFileId, + backgroundFileId: backgroundFileId, + emojiStatus: emojiStatus, + changes: changes + ) + } + + private func applySettings() { + guard let component = self.component, let environment = self.environment, let resolvedState = self.resolveState() else { + return + } + if self.isApplyingSettings { + return + } + if resolvedState.changes.isEmpty { + self.environment?.controller()?.dismiss() + return + } else if !component.context.isPremium { + HapticFeedback().impact(.light) + + let toastController = UndoOverlayController( + presentationData: component.context.sharedContext.currentPresentationData.with { $0 }, + content: .premiumPaywall( + title: nil, + text: environment.strings.NameColor_TooltipPremium_Account, + customUndoText: nil, + timeout: nil, + linkAction: nil + ), + elevatedLayout: false, + action: { [weak environment] action in + if case .info = action { + var replaceImpl: ((ViewController) -> Void)? + let controller = component.context.sharedContext.makePremiumDemoController(context: component.context, subject: .colors, forceDark: false, action: { + let controller = component.context.sharedContext.makePremiumIntroController(context: component.context, source: .settings, forceDark: false, dismissed: nil) + replaceImpl?(controller) + }, dismissed: nil) + replaceImpl = { [weak controller] c in + controller?.replace(with: c) + } + environment?.controller()?.push(controller) + } + return true + } + ) + environment.controller()?.present(toastController, in: .current) + return + } + + self.isApplyingSettings = true + self.state?.updated(transition: .immediate) + + self.applyDisposable?.dispose() + + enum ApplyError { + case generic + } + + var signals: [Signal] = [] + if !resolvedState.changes.intersection([.nameColor, .replyFileId, .profileColor, .backgroundFileId]).isEmpty { + signals.append(component.context.engine.accountData.updateNameColorAndEmoji(nameColor: resolvedState.nameColor, backgroundEmojiId: resolvedState.replyFileId, profileColor: resolvedState.profileColor, profileBackgroundEmojiId: resolvedState.backgroundFileId) + |> ignoreValues + |> mapError { _ -> ApplyError in + return .generic + }) + } + if resolvedState.changes.contains(.emojiStatus) { + let signal: Signal + if let emojiStatus = resolvedState.emojiStatus { + switch emojiStatus.content { + case let .emoji(fileId): + if let file = self.cachedIconFiles[fileId] { + signal = component.context.engine.accountData.setEmojiStatus(file: file, expirationDate: emojiStatus.expirationDate) + } else { + signal = .complete() + } + case let .starGift(id, fileId, title, slug, patternFileId, innerColor, outerColor, patternColor, textColor): + let slugComponents = slug.components(separatedBy: "-") + if let file = self.cachedIconFiles[fileId], let patternFile = self.cachedIconFiles[patternFileId], let numberString = slugComponents.last, let number = Int32(numberString) { + let gift = StarGift.UniqueGift( + id: id, + title: title, + number: number, + slug: slug, + owner: .peerId(component.context.account.peerId), + attributes: [ + .model(name: "", file: file, rarity: 0), + .pattern(name: "", file: patternFile, rarity: 0), + .backdrop(name: "", innerColor: innerColor, outerColor: outerColor, patternColor: patternColor, textColor: textColor, rarity: 0) + ], + availability: StarGift.UniqueGift.Availability(issued: 0, total: 0) + ) + signal = component.context.engine.accountData.setStarGiftStatus(starGift: gift, expirationDate: emojiStatus.expirationDate) + } else { + signal = .complete() + } + } + } else { + signal = component.context.engine.accountData.setEmojiStatus(file: nil, expirationDate: nil) + } + signals.append(signal + |> castError(ApplyError.self)) + } + + self.applyDisposable = (combineLatest(signals) + |> deliverOnMainQueue).start(error: { [weak self] _ in + guard let self, let component = self.component else { + return + } + + let presentationData = component.context.sharedContext.currentPresentationData.with { $0 } + self.environment?.controller()?.present(standardTextAlertController(theme: AlertControllerTheme(presentationData: presentationData), title: nil, text: presentationData.strings.Login_UnknownError, actions: [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_OK, action: {})]), in: .window(.root)) + + self.isApplyingSettings = false + self.state?.updated(transition: .immediate) + }, completed: { [weak self] in + guard let self else { + return + } + + let presentationData = component.context.sharedContext.currentPresentationData.with { $0 } + let navigationController: NavigationController? = self.environment?.controller()?.navigationController as? NavigationController + + self.environment?.controller()?.dismiss() + + if let lastController = navigationController?.viewControllers.last as? ViewController { + let tipController = UndoOverlayController(presentationData: presentationData, content: .actionSucceeded(title: nil, text: presentationData.strings.ProfileColorSetup_ToastAccountColorUpdated, cancel: nil, destructive: false), elevatedLayout: false, position: .bottom, animateInAsReplacement: false, action: { _ in return false }) + lastController.present(tipController, in: .window(.root)) + } + }) + } + + private enum EmojiSetupSubject { + case reply + case profile + case status + } + + private var previousEmojiSetupTimestamp: Double? + private func openEmojiSetup(sourceView: UIView, currentFileId: Int64?, color: UIColor?, subject: EmojiSetupSubject) { + guard let component = self.component, let environment = self.environment else { + return + } + + let currentTimestamp = CACurrentMediaTime() + if let previousTimestamp = self.previousEmojiSetupTimestamp, currentTimestamp < previousTimestamp + 1.0 { + return + } + self.previousEmojiSetupTimestamp = currentTimestamp + + self.emojiStatusSelectionController?.dismiss() + var selectedItems = Set() + if let currentFileId { + selectedItems.insert(MediaId(namespace: Namespaces.Media.CloudFile, id: currentFileId)) + } + + let mappedSubject: EmojiPagerContentComponent.Subject + switch subject { + case .reply, .profile: + mappedSubject = .backgroundIcon + case .status: + mappedSubject = .channelStatus + } + + let mappedMode: EmojiStatusSelectionController.Mode + switch subject { + case .status: + mappedMode = .customStatusSelection(completion: { [weak self] result, timestamp in + guard let self else { + return + } + if let result { + self.cachedIconFiles[result.fileId.id] = result + } + + if let result { + self.updatedPeerStatus = PeerEmojiStatus(content: .emoji(fileId: result.fileId.id), expirationDate: timestamp) + } else { + self.updatedPeerStatus = .some(nil) + } + self.state?.updated(transition: .spring(duration: 0.4)) + }) + default: + mappedMode = .backgroundSelection(completion: { [weak self] result in + guard let self, let resolvedState = self.resolveState() else { + return + } + if let result { + self.cachedIconFiles[result.fileId.id] = result + } + switch subject { + case .reply: + if let result { + self.updatedPeerNameEmoji = result.fileId.id + } else { + self.updatedPeerNameEmoji = .some(nil) + } + case .profile: + if let result { + self.updatedPeerProfileEmoji = result.fileId.id + if case .starGift = resolvedState.emojiStatus?.content { + self.updatedPeerStatus = .some(nil) + } + } else { + self.updatedPeerProfileEmoji = .some(nil) + } + default: + break + } + self.state?.updated(transition: .spring(duration: 0.4)) + }) + } + + let controller = EmojiStatusSelectionController( + context: component.context, + mode: mappedMode, + sourceView: sourceView, + emojiContent: EmojiPagerContentComponent.emojiInputData( + context: component.context, + animationCache: component.context.animationCache, + animationRenderer: component.context.animationRenderer, + isStandalone: false, + subject: mappedSubject, + hasTrending: false, + topReactionItems: [], + areUnicodeEmojiEnabled: false, + areCustomEmojiEnabled: true, + chatPeerId: component.context.account.peerId, + selectedItems: selectedItems, + topStatusTitle: nil, + backgroundIconColor: color + ), + currentSelection: currentFileId, + color: color, + destinationItemView: { [weak sourceView] in + guard let sourceView else { + return nil + } + return sourceView + } + ) + self.emojiStatusSelectionController = controller + environment.controller()?.present(controller, in: .window(.root)) + } + + private var isGroup: Bool { + guard let contentsData = self.contentsData, let peer = contentsData.peer else { + return false + } + if case let .channel(channel) = peer, case .group = channel.info { + return true + } + return false + } + + func update(component: UserAppearanceScreenComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { + self.isUpdating = true + defer { + self.isUpdating = false + } + + let environment = environment[EnvironmentType.self].value + let themeUpdated = self.environment?.theme !== environment.theme + self.environment = environment + + self.component = component + self.state = state + + let presentationData = component.context.sharedContext.currentPresentationData.with { $0 } + if themeUpdated { + self.backgroundColor = environment.theme.list.blocksBackgroundColor + } + + if self.contentsDataDisposable == nil { + self.contentsDataDisposable = (ContentsData.get(context: component.context) + |> deliverOnMainQueue).start(next: { [weak self] contentsData in + guard let self else { + return + } + self.contentsData = contentsData + + if !self.isUpdating { + self.state?.updated(transition: .immediate) + } + self.isReady.set(true) + }) + } + + guard let contentsData = self.contentsData, var peer = contentsData.peer, let resolvedState = self.resolveState() else { + return availableSize + } + + if let currentTheme = self.currentTheme, (self.resolvedCurrentTheme?.reference != currentTheme || self.resolvedCurrentTheme?.isDark != environment.theme.overallDarkAppearance), (self.resolvingCurrentTheme?.reference != currentTheme || self.resolvingCurrentTheme?.isDark != environment.theme.overallDarkAppearance) { + self.resolvingCurrentTheme?.disposable.dispose() + + let disposable = MetaDisposable() + self.resolvingCurrentTheme = (currentTheme, environment.theme.overallDarkAppearance, disposable) + + var presentationTheme: PresentationTheme? + switch currentTheme { + case .builtin: + presentationTheme = makePresentationTheme(mediaBox: component.context.sharedContext.accountManager.mediaBox, themeReference: .builtin(environment.theme.overallDarkAppearance ? .night : .dayClassic)) + case let .cloud(cloudTheme): + presentationTheme = makePresentationTheme(cloudTheme: cloudTheme.theme, dark: environment.theme.overallDarkAppearance) + default: + presentationTheme = makePresentationTheme(mediaBox: component.context.sharedContext.accountManager.mediaBox, themeReference: currentTheme) + } + if let presentationTheme { + let resolvedWallpaper: Signal + if case let .file(file) = presentationTheme.chat.defaultWallpaper, file.id == 0 { + resolvedWallpaper = cachedWallpaper(account: component.context.account, slug: file.slug, settings: file.settings) + |> map { wallpaper -> TelegramWallpaper? in + return wallpaper?.wallpaper + } + } else { + resolvedWallpaper = .single(presentationTheme.chat.defaultWallpaper) + } + disposable.set((resolvedWallpaper + |> deliverOnMainQueue).startStrict(next: { [weak self] resolvedWallpaper in + guard let self, let environment = self.environment else { + return + } + self.resolvedCurrentTheme = (currentTheme, environment.theme.overallDarkAppearance, presentationTheme, resolvedWallpaper) + if !self.isUpdating { + self.state?.updated(transition: .immediate) + } + })) + } + } else if self.currentTheme == nil { + self.resolvingCurrentTheme?.disposable.dispose() + self.resolvingCurrentTheme = nil + self.resolvedCurrentTheme = nil + } + + if case let .user(user) = peer { + peer = .user(user + .withUpdatedNameColor(resolvedState.nameColor) + .withUpdatedProfileColor(resolvedState.profileColor) + .withUpdatedEmojiStatus(resolvedState.emojiStatus) + .withUpdatedBackgroundEmojiId(resolvedState.replyFileId) + .withUpdatedProfileBackgroundEmojiId(resolvedState.backgroundFileId) + ) + } + + let headerColor: UIColor + if let profileColor = resolvedState.profileColor { + let headerBackgroundColors = component.context.peerNameColors.getProfile(profileColor, dark: environment.theme.overallDarkAppearance, subject: .background) + headerColor = headerBackgroundColors.secondary ?? headerBackgroundColors.main + } else { + headerColor = .clear + } + self.topOverscrollLayer.backgroundColor = headerColor.cgColor + + let backSize = self.backButton.update(key: .back, presentationData: component.context.sharedContext.currentPresentationData.with { $0 }, height: 44.0) + + var hasHeaderColor = false + if resolvedState.profileColor != nil { + hasHeaderColor = true + } + if case .starGift = resolvedState.emojiStatus?.content { + hasHeaderColor = true + } + if let controller = self.environment?.controller() as? UserAppearanceScreen { + controller.statusBar.updateStatusBarStyle(hasHeaderColor ? .White : .Ignore, animated: true) + } + + self.backButton.updateContentsColor(backgroundColor: hasHeaderColor ? UIColor(white: 1.0, alpha: 0.1) : .clear, contentsColor: hasHeaderColor ? .white : environment.theme.rootController.navigationBar.accentTextColor, canBeExpanded: !hasHeaderColor, transition: .animated(duration: 0.2, curve: .easeInOut)) + self.backButton.frame = CGRect(origin: CGPoint(x: environment.safeInsets.left + 16.0, y: environment.navigationHeight - 44.0), size: backSize) + if self.backButton.view.superview == nil { + if let controller = self.environment?.controller(), let navigationBar = controller.navigationBar { + navigationBar.view.addSubview(self.backButton.view) + } + } + + let bottomContentInset: CGFloat = 24.0 + let bottomInset: CGFloat = 8.0 + let sideInset: CGFloat = 16.0 + environment.safeInsets.left + let sectionSpacing: CGFloat = 32.0 + + let listItemParams = ListViewItemLayoutParams(width: availableSize.width - sideInset * 2.0, leftInset: 0.0, rightInset: 0.0, availableHeight: 10000.0, isStandalone: true) + + var contentHeight: CGFloat = 0.0 + + let sectionTransition = transition + + let previewSectionSize = self.previewSection.update( + transition: sectionTransition, + component: AnyComponent(ListSectionComponent( + theme: environment.theme, + background: .none(clipped: false), + header: nil, + footer: nil, + items: [ + AnyComponentWithIdentity(id: 0, component: AnyComponent(ListItemComponentAdaptor( + itemGenerator: PeerNameColorProfilePreviewItem( + context: component.context, + theme: environment.theme, + componentTheme: environment.theme, + strings: environment.strings, + topInset: environment.statusBarHeight, + sectionId: 0, + peer: peer, + subtitleString: environment.strings.Presence_online, + files: self.cachedIconFiles, + nameDisplayOrder: presentationData.nameDisplayOrder, + showBackground: !self.scrolledUp + ), + params: ListViewItemLayoutParams(width: availableSize.width, leftInset: 0.0, rightInset: 0.0, availableHeight: 10000.0, isStandalone: true) + ))), + ], + displaySeparators: false, + extendsItemHighlightToSection: true + )), + environment: {}, + containerSize: CGSize(width: availableSize.width, height: 1000.0) + ) + let previewSectionFrame = CGRect(origin: CGPoint(x: 0.0, y: contentHeight), size: previewSectionSize) + if let previewSectionView = self.previewSection.view { + if previewSectionView.superview == nil { + self.addSubview(previewSectionView) + } + sectionTransition.setFrame(view: previewSectionView, frame: previewSectionFrame) + } + contentHeight += previewSectionSize.height + contentHeight += sectionSpacing - 15.0 + + var profileLogoContents: [AnyComponentWithIdentity] = [] + profileLogoContents.append(AnyComponentWithIdentity(id: 0, component: AnyComponent(MultilineTextComponent( + text: .plain(NSAttributedString( + string: environment.strings.NameColor_AddProfileIcons, + font: Font.regular(presentationData.listsFontSize.baseDisplaySize), + textColor: environment.theme.list.itemPrimaryTextColor + )), + maximumNumberOfLines: 0 + )))) + let bannerSectionSize = self.bannerSection.update( + transition: sectionTransition, + component: AnyComponent(ListSectionComponent( + theme: environment.theme, + background: .all, + header: nil, + footer: nil, + items: [ + AnyComponentWithIdentity(id: 1, component: AnyComponent(ListItemComponentAdaptor( + itemGenerator: PeerNameColorItem( + theme: environment.theme, + colors: component.context.peerNameColors, + mode: .profile, + currentColor: resolvedState.profileColor, + updated: { [weak self] value in + guard let self, let value, let resolvedState = self.resolveState() else { + return + } + self.updatedPeerProfileColor = value + if case .starGift = resolvedState.emojiStatus?.content { + self.updatedPeerStatus = .some(nil) + } + self.state?.updated(transition: .spring(duration: 0.4)) + }, + sectionId: 0 + ), + params: listItemParams + ))), + AnyComponentWithIdentity(id: 2, component: AnyComponent(ListActionItemComponent( + theme: environment.theme, + title: AnyComponent(HStack(profileLogoContents, spacing: 6.0)), + icon: ListActionItemComponent.Icon(component: AnyComponentWithIdentity(id: 0, component: AnyComponent(EmojiActionIconComponent( + context: component.context, + color: resolvedState.profileColor.flatMap { profileColor in + component.context.peerNameColors.getProfile(profileColor, dark: environment.theme.overallDarkAppearance, subject: .palette).main + } ?? environment.theme.list.itemAccentColor, + fileId: resolvedState.backgroundFileId, + file: resolvedState.backgroundFileId.flatMap { self.cachedIconFiles[$0] } + )))), + action: { [weak self] view in + guard let self, let resolvedState = self.resolveState(), let view = view as? ListActionItemComponent.View, let iconView = view.iconView else { + return + } + + self.openEmojiSetup(sourceView: iconView, currentFileId: resolvedState.backgroundFileId, color: resolvedState.profileColor.flatMap { + component.context.peerNameColors.getProfile($0, dark: environment.theme.overallDarkAppearance, subject: .palette).main + } ?? environment.theme.list.itemAccentColor, subject: .profile) + } + ))) + ], + displaySeparators: true, + extendsItemHighlightToSection: false + )), + environment: {}, + containerSize: CGSize(width: availableSize.width - sideInset * 2.0, height: 1000.0) + ) + let bannerSectionFrame = CGRect(origin: CGPoint(x: sideInset, y: contentHeight), size: bannerSectionSize) + if let bannerSectionView = self.bannerSection.view { + if bannerSectionView.superview == nil { + self.scrollView.addSubview(bannerSectionView) + } + sectionTransition.setFrame(view: bannerSectionView, frame: bannerSectionFrame) + } + contentHeight += bannerSectionSize.height + contentHeight += sectionSpacing + + let resetColorSectionSize = self.resetColorSection.update( + transition: sectionTransition, + component: AnyComponent(ListSectionComponent( + theme: environment.theme, + header: nil, + footer: nil, + items: [ + AnyComponentWithIdentity(id: 0, component: AnyComponent(ListActionItemComponent( + theme: environment.theme, + title: AnyComponent(MultilineTextComponent( + text: .plain(NSAttributedString( + string: environment.strings.Channel_Appearance_ResetProfileColor, + font: Font.regular(presentationData.listsFontSize.baseDisplaySize), + textColor: environment.theme.list.itemAccentColor + )), + maximumNumberOfLines: 0 + )), + icon: nil, + accessory: nil, + action: { [weak self] view in + guard let self, let resolvedState = self.resolveState() else { + return + } + + self.updatedPeerProfileColor = .some(nil) + self.updatedPeerProfileEmoji = .some(nil) + if case .starGift = resolvedState.emojiStatus?.content { + self.updatedPeerStatus = .some(nil) + } + self.state?.updated(transition: .spring(duration: 0.4)) + } + ))) + ], + displaySeparators: false, + extendsItemHighlightToSection: true + )), + environment: {}, + containerSize: CGSize(width: availableSize.width - sideInset * 2.0, height: 1000.0) + ) + + var displayResetProfileColor = resolvedState.profileColor != nil || resolvedState.backgroundFileId != nil + if case .starGift = resolvedState.emojiStatus?.content { + displayResetProfileColor = true + } + + let resetColorSectionFrame = CGRect(origin: CGPoint(x: sideInset, y: contentHeight), size: resetColorSectionSize) + if let resetColorSectionView = self.resetColorSection.view { + if resetColorSectionView.superview == nil { + self.scrollView.addSubview(resetColorSectionView) + } + sectionTransition.setPosition(view: resetColorSectionView, position: resetColorSectionFrame.center) + sectionTransition.setBounds(view: resetColorSectionView, bounds: CGRect(origin: CGPoint(), size: resetColorSectionFrame.size)) + sectionTransition.setScale(view: resetColorSectionView, scale: displayResetProfileColor ? 1.0 : 0.001) + sectionTransition.setAlpha(view: resetColorSectionView, alpha: displayResetProfileColor ? 1.0 : 0.0) + } + if displayResetProfileColor { + contentHeight += resetColorSectionSize.height + contentHeight += sectionSpacing + } + + var chatPreviewTheme: PresentationTheme = environment.theme + var chatPreviewWallpaper: TelegramWallpaper = presentationData.chatWallpaper + if let resolvedCurrentTheme = self.resolvedCurrentTheme { + chatPreviewTheme = resolvedCurrentTheme.theme + if let wallpaper = resolvedCurrentTheme.wallpaper { + chatPreviewWallpaper = wallpaper + } + } + + let messageItem = PeerNameColorChatPreviewItem.MessageItem( + outgoing: false, + peerId: EnginePeer.Id(namespace: peer.id.namespace, id: PeerId.Id._internalFromInt64Value(0)), + author: peer.compactDisplayTitle, + photo: peer.profileImageRepresentations, + nameColor: resolvedState.nameColor, + backgroundEmojiId: resolvedState.replyFileId, + reply: (peer.compactDisplayTitle, environment.strings.NameColor_ChatPreview_ReplyText_Account, resolvedState.nameColor), + linkPreview: (environment.strings.NameColor_ChatPreview_LinkSite, environment.strings.NameColor_ChatPreview_LinkTitle, environment.strings.NameColor_ChatPreview_LinkText), + text: environment.strings.NameColor_ChatPreview_MessageText_Account + ) + + var replyLogoContents: [AnyComponentWithIdentity] = [] + replyLogoContents.append(AnyComponentWithIdentity(id: 0, component: AnyComponent(MultilineTextComponent( + text: .plain(NSAttributedString( + string: environment.strings.NameColor_AddRepliesIcons, + font: Font.regular(presentationData.listsFontSize.baseDisplaySize), + textColor: environment.theme.list.itemPrimaryTextColor + )), + maximumNumberOfLines: 0 + )))) + let replySectionSize = self.replySection.update( + transition: sectionTransition, + component: AnyComponent(ListSectionComponent( + theme: environment.theme, + header: nil, + footer: AnyComponent(MultilineTextComponent( + text: .plain(NSAttributedString( + string: environment.strings.NameColor_ChatPreview_Description_Account, + font: Font.regular(presentationData.listsFontSize.itemListBaseHeaderFontSize), + textColor: environment.theme.list.freeTextColor + )), + maximumNumberOfLines: 0 + )), + items: [ + AnyComponentWithIdentity(id: 0, component: AnyComponent(ListItemComponentAdaptor( + itemGenerator: PeerNameColorChatPreviewItem( + context: component.context, + theme: chatPreviewTheme, + componentTheme: chatPreviewTheme, + strings: environment.strings, + sectionId: 0, + fontSize: presentationData.chatFontSize, + chatBubbleCorners: presentationData.chatBubbleCorners, + wallpaper: chatPreviewWallpaper, + dateTimeFormat: environment.dateTimeFormat, + nameDisplayOrder: presentationData.nameDisplayOrder, + messageItems: [messageItem] + ), + params: listItemParams + ))), + AnyComponentWithIdentity(id: 1, component: AnyComponent(ListItemComponentAdaptor( + itemGenerator: PeerNameColorItem( + theme: environment.theme, + colors: component.context.peerNameColors, + mode: .name, + currentColor: resolvedState.nameColor, + updated: { [weak self] value in + guard let self, let value else { + return + } + self.updatedPeerNameColor = value + self.state?.updated(transition: .spring(duration: 0.4)) + }, + sectionId: 0 + ), + params: listItemParams + ))), + AnyComponentWithIdentity(id: 2, component: AnyComponent(ListActionItemComponent( + theme: environment.theme, + title: AnyComponent(HStack(replyLogoContents, spacing: 6.0)), + icon: ListActionItemComponent.Icon(component: AnyComponentWithIdentity(id: 0, component: AnyComponent(EmojiActionIconComponent( + context: component.context, + color: component.context.peerNameColors.get(resolvedState.nameColor, dark: environment.theme.overallDarkAppearance).main, + fileId: resolvedState.replyFileId, + file: resolvedState.replyFileId.flatMap { self.cachedIconFiles[$0] } + )))), + action: { [weak self] view in + guard let self, let resolvedState = self.resolveState(), let view = view as? ListActionItemComponent.View, let iconView = view.iconView else { + return + } + + self.openEmojiSetup(sourceView: iconView, currentFileId: resolvedState.replyFileId, color: component.context.peerNameColors.get(resolvedState.nameColor, dark: environment.theme.overallDarkAppearance).main, subject: .reply) + } + ))) + ], + displaySeparators: true, + extendsItemHighlightToSection: false + )), + environment: {}, + containerSize: CGSize(width: availableSize.width - sideInset * 2.0, height: 1000.0) + ) + let replySectionFrame = CGRect(origin: CGPoint(x: sideInset, y: contentHeight), size: replySectionSize) + if let replySectionView = self.replySection.view { + if replySectionView.superview == nil { + self.scrollView.addSubview(replySectionView) + } + sectionTransition.setFrame(view: replySectionView, frame: replySectionFrame) + } + contentHeight += replySectionSize.height + contentHeight += sectionSpacing + + if !contentsData.gifts.isEmpty { + var selectedGiftId: Int64? + if let status = resolvedState.emojiStatus, case let .starGift(id, _, _, _, _, _, _, _, _) = status.content { + selectedGiftId = id + } + let giftsSectionSize = self.giftsSection.update( + transition: sectionTransition, + component: AnyComponent(ListSectionComponent( + theme: environment.theme, + header: AnyComponent(MultilineTextComponent( + text: .plain(NSAttributedString( + string: environment.strings.NameColor_GiftTitle, + font: Font.regular(presentationData.listsFontSize.itemListBaseHeaderFontSize), + textColor: environment.theme.list.freeTextColor + )), + maximumNumberOfLines: 0 + )), + footer: AnyComponent(MultilineTextComponent( + text: .plain(NSAttributedString( + string: environment.strings.NameColor_GiftInfo, + font: Font.regular(presentationData.listsFontSize.itemListBaseHeaderFontSize), + textColor: environment.theme.list.freeTextColor + )), + maximumNumberOfLines: 0 + )), + items: [ + AnyComponentWithIdentity(id: 0, component: AnyComponent( + GiftListItemComponent( + context: component.context, + theme: environment.theme, + gifts: contentsData.gifts, + selectedId: selectedGiftId, + selectionUpdated: { [weak self] gift in + guard let self else { + return + } + var fileId: Int64? + var patternFileId: Int64? + var innerColor: Int32? + var outerColor: Int32? + var patternColor: Int32? + var textColor: Int32? + for attribute in gift.attributes { + switch attribute { + case let .model(_, file, _): + fileId = file.fileId.id + self.cachedIconFiles[file.fileId.id] = file + case let .pattern(_, file, _): + patternFileId = file.fileId.id + self.cachedIconFiles[file.fileId.id] = file + case let .backdrop(_, innerColorValue, outerColorValue, patternColorValue, textColorValue, _): + innerColor = innerColorValue + outerColor = outerColorValue + patternColor = patternColorValue + textColor = textColorValue + default: + break + } + } + if let fileId, let patternFileId, let innerColor, let outerColor, let patternColor, let textColor { + self.updatedPeerProfileColor = .some(nil) + self.updatedPeerProfileEmoji = .some(nil) + self.updatedPeerStatus = .some(PeerEmojiStatus(content: .starGift(id: gift.id, fileId: fileId, title: gift.title, slug: gift.slug, patternFileId: patternFileId, innerColor: innerColor, outerColor: outerColor, patternColor: patternColor, textColor: textColor), expirationDate: nil)) + self.state?.updated(transition: .spring(duration: 0.4)) + } + }, + tag: giftListTag + ) + )), + ], + displaySeparators: false + )), + environment: {}, + containerSize: CGSize(width: availableSize.width - sideInset * 2.0, height: 10000.0) + ) + let giftsSectionFrame = CGRect(origin: CGPoint(x: sideInset, y: contentHeight), size: giftsSectionSize) + if let giftsSectionView = self.giftsSection.view { + if giftsSectionView.superview == nil { + self.scrollView.addSubview(giftsSectionView) + } + sectionTransition.setFrame(view: giftsSectionView, frame: giftsSectionFrame) + } + contentHeight += giftsSectionSize.height + contentHeight += sectionSpacing + } + + contentHeight += bottomContentInset + + var buttonTitle = environment.strings.Channel_Appearance_ApplyButton + if let emojiStatus = resolvedState.emojiStatus, case .starGift = emojiStatus.content, resolvedState.changes.contains(.emojiStatus) { + buttonTitle = environment.strings.NameColor_WearCollectible + } + + var buttonContents: [AnyComponentWithIdentity] = [] + buttonContents.append(AnyComponentWithIdentity(id: AnyHashable(buttonTitle), component: AnyComponent( + Text(text: buttonTitle, font: Font.semibold(17.0), color: environment.theme.list.itemCheckColors.foregroundColor) + ))) + + let buttonSize = self.actionButton.update( + transition: transition, + component: AnyComponent(ButtonComponent( + background: ButtonComponent.Background( + color: environment.theme.list.itemCheckColors.fillColor, + foreground: environment.theme.list.itemCheckColors.foregroundColor, + pressedColor: environment.theme.list.itemCheckColors.fillColor.withMultipliedAlpha(0.8) + ), + content: AnyComponentWithIdentity(id: AnyHashable(0 as Int), component: AnyComponent( + VStack(buttonContents, spacing: 3.0) + )), + isEnabled: true, + tintWhenDisabled: false, + displaysProgress: self.isApplyingSettings, + action: { [weak self] in + guard let self else { + return + } + self.applySettings() + } + )), + environment: {}, + containerSize: CGSize(width: availableSize.width - sideInset * 2.0, height: 50.0) + ) + contentHeight += buttonSize.height + + contentHeight += bottomInset + contentHeight += environment.safeInsets.bottom + + let buttonY = availableSize.height - bottomInset - environment.safeInsets.bottom - buttonSize.height + + let buttonFrame = CGRect(origin: CGPoint(x: sideInset, y: buttonY), size: buttonSize) + if let buttonView = self.actionButton.view { + if buttonView.superview == nil { + self.addSubview(buttonView) + } + transition.setFrame(view: buttonView, frame: buttonFrame) + transition.setAlpha(view: buttonView, alpha: 1.0) + } + + let bottomPanelFrame = CGRect(origin: CGPoint(x: 0.0, y: buttonY - 8.0), size: CGSize(width: availableSize.width, height: availableSize.height - buttonY + 8.0)) + transition.setFrame(view: self.bottomPanelBackgroundView, frame: bottomPanelFrame) + self.bottomPanelBackgroundView.updateColor(color: environment.theme.rootController.navigationBar.blurredBackgroundColor, transition: .immediate) + self.bottomPanelBackgroundView.update(size: bottomPanelFrame.size, transition: transition.containedViewLayoutTransition) + + self.bottomPanelSeparator.backgroundColor = environment.theme.rootController.navigationBar.separatorColor.cgColor + transition.setFrame(layer: self.bottomPanelSeparator, frame: CGRect(origin: CGPoint(x: bottomPanelFrame.minX, y: bottomPanelFrame.minY), size: CGSize(width: bottomPanelFrame.width, height: UIScreenPixel))) + + let previousBounds = self.scrollView.bounds + let contentSize = CGSize(width: availableSize.width, height: contentHeight) + if self.scrollView.frame != CGRect(origin: CGPoint(), size: availableSize) { + self.scrollView.frame = CGRect(origin: CGPoint(), size: availableSize) + } + if self.scrollView.contentSize != contentSize { + self.scrollView.contentSize = contentSize + } + let scrollInsets = UIEdgeInsets(top: environment.navigationHeight, left: 0.0, bottom: availableSize.height - bottomPanelFrame.minY, right: 0.0) + if self.scrollView.scrollIndicatorInsets != scrollInsets { + self.scrollView.scrollIndicatorInsets = scrollInsets + } + + if !previousBounds.isEmpty, !transition.animation.isImmediate { + let bounds = self.scrollView.bounds + if bounds.maxY != previousBounds.maxY { + let offsetY = previousBounds.maxY - bounds.maxY + transition.animateBoundsOrigin(view: self.scrollView, from: CGPoint(x: 0.0, y: offsetY), to: CGPoint(), additive: true) + } + } + + self.topOverscrollLayer.frame = CGRect(origin: CGPoint(x: 0.0, y: -3000.0), size: CGSize(width: availableSize.width, height: 3000.0)) + + self.updateScrolling(transition: transition) + + if let controller = environment.controller(), !controller.automaticallyControlPresentationContextLayout { + let layout = ContainerViewLayout( + size: availableSize, + metrics: environment.metrics, + deviceMetrics: environment.deviceMetrics, + intrinsicInsets: UIEdgeInsets(top: 0.0, left: 0.0, bottom: bottomPanelFrame.height, right: 0.0), + safeInsets: UIEdgeInsets(top: 0.0, left: environment.safeInsets.left, bottom: 0.0, right: environment.safeInsets.right), + additionalInsets: .zero, + statusBarHeight: environment.statusBarHeight, + inputHeight: nil, + inputHeightIsInteractivellyChanging: false, + inVoiceOver: false + ) + controller.presentationContext.containerLayoutUpdated(layout, transition: transition.containedViewLayoutTransition) + } + + return availableSize + } + } + + func makeView() -> View { + return View() + } + + 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) + } +} + +public class UserAppearanceScreen: ViewControllerComponentContainer { + private let context: AccountContext + + private var didSetReady: Bool = false + + public init( + context: AccountContext, + updatedPresentationData: (initial: PresentationData, signal: Signal)? = nil + ) { + self.context = context + + super.init(context: context, component: UserAppearanceScreenComponent( + context: context + ), navigationBarAppearance: .default, theme: .default, updatedPresentationData: updatedPresentationData) + + self.automaticallyControlPresentationContextLayout = false + + let presentationData = context.sharedContext.currentPresentationData.with { $0 } + self.title = "" + self.navigationItem.backBarButtonItem = UIBarButtonItem(title: presentationData.strings.Common_Back, style: .plain, target: nil, action: nil) + self.navigationItem.leftBarButtonItem = UIBarButtonItem(customView: UIView()) + + self.ready.set(.never()) + + self.scrollToTop = { [weak self] in + guard let self, let componentView = self.node.hostView.componentView as? UserAppearanceScreenComponent.View else { + return + } + componentView.scrollToTop() + } + + self.attemptNavigation = { [weak self] complete in + guard let self, let componentView = self.node.hostView.componentView as? UserAppearanceScreenComponent.View else { + return true + } + + return componentView.attemptNavigation(complete: complete) + } + } + + required public init(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + deinit { + } + + @objc private func cancelPressed() { + self.dismiss() + } + + override public func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) { + super.containerLayoutUpdated(layout, transition: transition) + + if let componentView = self.node.hostView.componentView as? UserAppearanceScreenComponent.View { + if !self.didSetReady { + self.didSetReady = true + self.ready.set(componentView.isReady.get()) + } + } + } +} diff --git a/submodules/TelegramUI/Images.xcassets/Chat/Input/Media/PanelCollectibleIcon.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Chat/Input/Media/PanelCollectibleIcon.imageset/Contents.json new file mode 100644 index 0000000000..0e3cc6777e --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Chat/Input/Media/PanelCollectibleIcon.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "diamond.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/submodules/TelegramUI/Images.xcassets/Chat/Input/Media/PanelCollectibleIcon.imageset/diamond.pdf b/submodules/TelegramUI/Images.xcassets/Chat/Input/Media/PanelCollectibleIcon.imageset/diamond.pdf new file mode 100644 index 0000000000..ee9e53aa18 Binary files /dev/null and b/submodules/TelegramUI/Images.xcassets/Chat/Input/Media/PanelCollectibleIcon.imageset/diamond.pdf differ diff --git a/submodules/TelegramUI/Sources/Chat/ChatControllerLoadDisplayNode.swift b/submodules/TelegramUI/Sources/Chat/ChatControllerLoadDisplayNode.swift index e4311f81ba..291ed90410 100644 --- a/submodules/TelegramUI/Sources/Chat/ChatControllerLoadDisplayNode.swift +++ b/submodules/TelegramUI/Sources/Chat/ChatControllerLoadDisplayNode.swift @@ -4339,7 +4339,22 @@ extension ChatControllerImpl { let _ = ApplicationSpecificNotice.incrementDismissedPremiumGiftSuggestion(accountManager: self.context.sharedContext.accountManager, peerId: peerId, timestamp: Int32(Date().timeIntervalSince1970)).startStandalone() } } else { - let controller = self.context.sharedContext.makeGiftOptionsController(context: self.context, peerId: peerId, premiumOptions: [], hasBirthday: false, completion: nil) + let controller = self.context.sharedContext.makeGiftOptionsController(context: self.context, peerId: peerId, premiumOptions: [], hasBirthday: false, completion: { [weak self] in + guard let self, let peer = self.presentationInterfaceState.renderedPeer?.peer else { + return + } + if let controller = self.context.sharedContext.makePeerInfoController( + context: self.context, + updatedPresentationData: nil, + peer: peer, + mode: .gifts, + avatarInitiallyExpanded: false, + fromChat: false, + requestsContext: nil + ) { + self.push(controller) + } + }) self.push(controller) } }, openPremiumRequiredForMessaging: { [weak self] in diff --git a/submodules/TelegramUI/Sources/ChatController.swift b/submodules/TelegramUI/Sources/ChatController.swift index a1ff060f8f..402877a551 100644 --- a/submodules/TelegramUI/Sources/ChatController.swift +++ b/submodules/TelegramUI/Sources/ChatController.swift @@ -4577,7 +4577,12 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G return } let premiumOptions = giftOptions.filter { $0.users == 1 }.map { CachedPremiumGiftOption(months: $0.months, currency: $0.currency, amount: $0.amount, botUrl: "", storeProductId: $0.storeProductId) } - let controller = self.context.sharedContext.makeGiftOptionsController(context: context, peerId: peerId, premiumOptions: premiumOptions, hasBirthday: false, completion: nil) + + var hasBirthday = false + if let cachedUserData = self.peerView?.cachedData as? CachedUserData { + hasBirthday = hasBirthdayToday(cachedData: cachedUserData) + } + let controller = self.context.sharedContext.makeGiftOptionsController(context: context, peerId: peerId, premiumOptions: premiumOptions, hasBirthday: hasBirthday, completion: nil) self.push(controller) }) }, requestMessageUpdate: { [weak self] id, scroll in diff --git a/submodules/TelegramUI/Sources/ContactMultiselectionControllerNode.swift b/submodules/TelegramUI/Sources/ContactMultiselectionControllerNode.swift index efb93aee9b..1019aa851c 100644 --- a/submodules/TelegramUI/Sources/ContactMultiselectionControllerNode.swift +++ b/submodules/TelegramUI/Sources/ContactMultiselectionControllerNode.swift @@ -226,7 +226,7 @@ final class ContactMultiselectionControllerNode: ASDisplayNode { sections.append((presentationData.strings.Premium_Gift_ContactSelection_BirthdayTomorrow, tomorrowPeers, hasActions)) } - displayTopPeers = .custom(showSelf: false, sections: sections) + displayTopPeers = .custom(showSelf: false, selfSubtitle: nil, sections: sections) } else { displayTopPeers = .recent } diff --git a/submodules/TelegramUI/Sources/ContactSelectionControllerNode.swift b/submodules/TelegramUI/Sources/ContactSelectionControllerNode.swift index 23e2c818ae..ebbfd6d3f3 100644 --- a/submodules/TelegramUI/Sources/ContactSelectionControllerNode.swift +++ b/submodules/TelegramUI/Sources/ContactSelectionControllerNode.swift @@ -67,7 +67,7 @@ final class ContactSelectionControllerNode: ASDisplayNode { var excludeSelf = true let displayTopPeers: ContactListPresentation.TopPeers - if case let .starsGifting(birthdays, hasActions, showSelf) = mode { + if case let .starsGifting(birthdays, hasActions, showSelf, selfSubtitle) = mode { if showSelf { excludeSelf = false } @@ -98,7 +98,7 @@ final class ContactSelectionControllerNode: ASDisplayNode { sections.append((presentationData.strings.Premium_Gift_ContactSelection_BirthdayTomorrow, tomorrowPeers, hasActions)) } - displayTopPeers = .custom(showSelf: showSelf, sections: sections) + displayTopPeers = .custom(showSelf: showSelf, selfSubtitle: selfSubtitle, sections: sections) } else { displayTopPeers = .recent } diff --git a/submodules/TelegramUI/Sources/OpenResolvedUrl.swift b/submodules/TelegramUI/Sources/OpenResolvedUrl.swift index ef517b9397..0bf8672f70 100644 --- a/submodules/TelegramUI/Sources/OpenResolvedUrl.swift +++ b/submodules/TelegramUI/Sources/OpenResolvedUrl.swift @@ -1220,7 +1220,14 @@ func openResolvedUrlImpl( updateExternalController(nil) } } - let controller = context.sharedContext.makeGiftViewScreen(context: context, gift: gift, shareStory: nil, dismissed: { + let controller = context.sharedContext.makeGiftViewScreen(context: context, gift: gift, shareStory: { [weak navigationController] uniqueGift in + Queue.mainQueue().after(0.15) { + if let lastController = navigationController?.viewControllers.last as? ViewController { + let controller = context.sharedContext.makeStorySharingScreen(context: context, subject: .gift(gift), parentController: lastController) + navigationController?.pushViewController(controller) + } + } + }, dismissed: { dismissedImpl?() }) navigationController?.pushViewController(controller) diff --git a/submodules/TelegramUI/Sources/SharedAccountContext.swift b/submodules/TelegramUI/Sources/SharedAccountContext.swift index 3eb2885af6..0bced54179 100644 --- a/submodules/TelegramUI/Sources/SharedAccountContext.swift +++ b/submodules/TelegramUI/Sources/SharedAccountContext.swift @@ -2352,7 +2352,7 @@ public final class SharedAccountContextImpl: SharedAccountContext { let presentationData = context.sharedContext.currentPresentationData.with { $0 } var presentBirthdayPickerImpl: (() -> Void)? - let starsMode: ContactSelectionControllerMode = .starsGifting(birthdays: birthdays, hasActions: false, showSelf: false) + let starsMode: ContactSelectionControllerMode = .starsGifting(birthdays: birthdays, hasActions: false, showSelf: false, selfSubtitle: nil) let contactOptions: Signal<[ContactListAdditionalOption], NoError> = context.engine.data.subscribe(TelegramEngine.EngineData.Item.Peer.Birthday(id: context.account.peerId)) |> map { birthday in @@ -2428,22 +2428,26 @@ public final class SharedAccountContextImpl: SharedAccountContext { var currentBirthdays: [EnginePeer.Id: TelegramBirthday]? if case let .starGiftTransfer(birthdays, _, _, _, _, showSelf) = source { - mode = .starsGifting(birthdays: birthdays, hasActions: false, showSelf: showSelf) + mode = .starsGifting(birthdays: birthdays, hasActions: false, showSelf: showSelf, selfSubtitle: presentationData.strings.Premium_Gift_ContactSelection_TransferSelf) currentBirthdays = birthdays } else if case let .chatList(birthdays) = source { - mode = .starsGifting(birthdays: birthdays, hasActions: true, showSelf: true) + mode = .starsGifting(birthdays: birthdays, hasActions: true, showSelf: true, selfSubtitle: presentationData.strings.Premium_Gift_ContactSelection_BuySelf) currentBirthdays = birthdays } else if case let .settings(birthdays) = source { - mode = .starsGifting(birthdays: birthdays, hasActions: true, showSelf: true) + mode = .starsGifting(birthdays: birthdays, hasActions: true, showSelf: true, selfSubtitle: presentationData.strings.Premium_Gift_ContactSelection_BuySelf) currentBirthdays = birthdays } else { - mode = .starsGifting(birthdays: nil, hasActions: true, showSelf: false) + mode = .starsGifting(birthdays: nil, hasActions: true, showSelf: false, selfSubtitle: nil) } var allowChannelsInSearch = false + var isChannelGift = false let contactOptions: Signal<[ContactListAdditionalOption], NoError> - if case let .starGiftTransfer(_, _, _, _, canExportDate, _) = source { + if case let .starGiftTransfer(_, reference, _, _, canExportDate, _) = source { allowChannelsInSearch = true + if case let .peer(peerId, _) = reference, peerId.namespace == Namespaces.Peer.CloudChannel { + isChannelGift = true + } var subtitle: String? if let canExportDate { let currentTime = Int32(CFAbsoluteTimeGetCurrent() + kCFAbsoluteTimeIntervalSince1970) @@ -2605,9 +2609,16 @@ public final class SharedAccountContextImpl: SharedAccountContext { case .requestPassword: let alertController = confirmGiftWithdrawalController(context: context, reference: reference, present: { [weak controller] c, a in controller?.present(c, in: .window(.root)) - }, completion: { url in + }, completion: { [weak controller] url in let presentationData = context.sharedContext.currentPresentationData.with { $0 } context.sharedContext.openExternalUrl(context: context, urlContext: .generic, url: url, forceExternal: true, presentationData: presentationData, navigationController: nil, dismissInput: {}) + + guard let controller, let navigationController = controller.navigationController as? NavigationController else { + return + } + var controllers = navigationController.viewControllers + controllers = controllers.filter { !($0 is ContactSelectionController) } + navigationController.setViewControllers(controllers, animated: true) }) controller?.present(alertController, in: .window(.root)) default: @@ -2646,20 +2657,23 @@ public final class SharedAccountContextImpl: SharedAccountContext { } var controllers = navigationController.viewControllers controllers = controllers.filter { !($0 is ContactSelectionController) } - var foundController = false - for controller in controllers.reversed() { - if let chatController = controller as? ChatController, case .peer(id: peer.id) = chatController.chatLocation { - chatController.hintPlayNextOutgoingGift() - foundController = true - break + + if !isChannelGift { + var foundController = false + for controller in controllers.reversed() { + if let chatController = controller as? ChatController, case .peer(id: peer.id) = chatController.chatLocation { + chatController.hintPlayNextOutgoingGift() + foundController = true + break + } } + if !foundController { + let chatController = context.sharedContext.makeChatController(context: context, chatLocation: .peer(id: peer.id), subject: nil, botStart: nil, mode: .standard(.default), params: nil) + chatController.hintPlayNextOutgoingGift() + controllers.append(chatController) + } + navigationController.setViewControllers(controllers, animated: true) } - if !foundController { - let chatController = context.sharedContext.makeChatController(context: context, chatLocation: .peer(id: peer.id), subject: nil, botStart: nil, mode: .standard(.default), params: nil) - chatController.hintPlayNextOutgoingGift() - controllers.append(chatController) - } - navigationController.setViewControllers(controllers, animated: true) Queue.mainQueue().after(0.3) { let tooltipController = UndoOverlayController(