diff --git a/Telegram/Telegram-iOS/en.lproj/Localizable.strings b/Telegram/Telegram-iOS/en.lproj/Localizable.strings index 93633ba923..44ca74299d 100644 --- a/Telegram/Telegram-iOS/en.lproj/Localizable.strings +++ b/Telegram/Telegram-iOS/en.lproj/Localizable.strings @@ -13054,6 +13054,7 @@ Sorry for the inconvenience."; "Gift.Options.Gift.Text" = "Give **%@** gifts that can be kept on the profile or converted to Stars. [What are Stars >]()"; "Gift.Options.Gift.Filter.AllGifts" = "All Gifts"; "Gift.Options.Gift.Filter.Limited" = "Limited"; +"Gift.Options.Gift.Filter.InStock" = "In Stock"; "Gift.Options.Gift.Limited" = "limited"; "Gift.Options.Gift.SoldOut" = "sold out"; "Gift.Options.SoldOut.Text" = "Sorry, this gift is sold out."; @@ -13087,6 +13088,7 @@ Sorry for the inconvenience."; "Gift.Send.HideMyName" = "Hide My Name"; "Gift.Send.HideMyName.Info" = "Hide my name and message from visitors to %1$@'s profile. %2$@ will still see your name and message."; "Gift.Send.Send" = "Send a Gift for"; +"Gift.Send.Buy" = "Buy a Gift for"; "Gift.Send.Limited" = "Limited"; "Gift.Send.Remains_1" = "%@ left"; "Gift.Send.Remains_any" = "%@ left"; @@ -13616,7 +13618,7 @@ Sorry for the inconvenience."; "BotVerification.Verify.Channel.Text" = "Do you want to verify this channel with your verification mark and description?"; "BotVerification.Verify.Group.Title" = "Verify Group"; "BotVerification.Verify.Group.Text" = "Do you want to verify this group with your verification mark and description?"; -"BotVerification.Verify.User.Title" = "Verify Bot"; +"BotVerification.Verify.User.Title" = "Verify User"; "BotVerification.Verify.User.Text" = "Do you want to verify this user with your verification mark and description?"; "BotVerification.Verify.Bot.Title" = "Verify Bot"; "BotVerification.Verify.Bot.Text" = "Do you want to verify this bot with your verification mark and description?"; @@ -13639,3 +13641,44 @@ Sorry for the inconvenience."; "Gift.View.Outgoing.NameHidden" = "Only %@ can see your name."; "Gift.View.Outgoing.NameAndMessageHidden" = "Only %@ can see your name and message."; + +"Conversation.ViewStarGift" = "VIEW COLLECTIBLE"; + +"ChatList.AddPhoto.Title" = "Add your photo! 📸"; +"ChatList.AddPhoto.Text" = "Help your friends spot you easily."; + +"Story.ViewGift" = "View Gift"; + +"Camera.OpenChat" = "Open Chat"; + +"Gift.Wear.Wear" = "Wear %@"; +"Gift.Wear.GetBenefits" = "and get these benefits:"; +"Gift.Wear.Badge.Title" = "Radiant Badge"; +"Gift.Wear.Badge.Text" = "The glittering icon of this item will be displayed next to your name."; +"Gift.Wear.Design.Title" = "Unqiue Profile Design"; +"Gift.Wear.Design.Text" = "Your profile page will get the color and the symbol of this item."; +"Gift.Wear.Proof.Title" = "Proof of Ownership"; +"Gift.Wear.Proof.Text" = "Tapping the icon of this item next to your name will show its info and owner."; +"Gift.Wear.Start" = "Start Wearing"; + +"Gift.View.Header.Transfer" = "transfer"; +"Gift.View.Header.Wear" = "wear"; +"Gift.View.Header.TakeOff" = "take off"; +"Gift.View.Header.Share" = "share"; + +"Conversation.AddToContactsLong" = "Add to Contacts"; + +"PeerInfo.PaneRecommendedBots" = "Similar Bots"; + +"SharedMedia.SimilarChannelCount_1" = "%@ channel"; +"SharedMedia.SimilarChannelCount_any" = "%@ channels"; + +"SharedMedia.SimilarBotCount_1" = "%@ bot"; +"SharedMedia.SimilarBotCount_any" = "%@ bots"; + +"PeerInfo.SimilarBots.ShowMore" = "Show More Bots"; +"PeerInfo.SimilarBots.ShowMoreInfo" = "Subscribe to [Telegram Premium]()\nto unlock up to **100** similar bots."; + +"Gift.View.Context.Share" = "Share"; +"Gift.View.Context.CopyLink" = "Copy Link"; +"Gift.View.Context.Transfer" = "Transfer"; diff --git a/submodules/AccountContext/Sources/AccountContext.swift b/submodules/AccountContext/Sources/AccountContext.swift index 0da1413dc6..7f0c5df683 100644 --- a/submodules/AccountContext/Sources/AccountContext.swift +++ b/submodules/AccountContext/Sources/AccountContext.swift @@ -318,6 +318,7 @@ public enum ResolvedUrl { case boost(peerId: PeerId?, status: ChannelBoostStatus?, myBoostStatus: MyBoostStatus?) case premiumGiftCode(slug: String) case premiumMultiGift(reference: String?) + case collectible(gift: StarGift.UniqueGift?) case messageLink(link: TelegramResolvedMessageLink?) } @@ -1100,10 +1101,15 @@ public protocol SharedAccountContext: AnyObject { func makeStarsGiftScreen(context: AccountContext, message: EngineMessage) -> ViewController func makeStarsGiveawayBoostScreen(context: AccountContext, peerId: EnginePeer.Id, boost: ChannelBoostersContext.State.Boost) -> ViewController func makeStarsIntroScreen(context: AccountContext) -> ViewController - func makeGiftViewScreen(context: AccountContext, message: EngineMessage) -> ViewController + func makeGiftViewScreen(context: AccountContext, message: EngineMessage, shareStory: (() -> Void)?) -> ViewController + func makeGiftViewScreen(context: AccountContext, gift: StarGift.UniqueGift, shareStory: (() -> Void)?) -> ViewController + + func makeStorySharingScreen(context: AccountContext, subject: StorySharingSubject, parentController: ViewController) -> ViewController func makeContentReportScreen(context: AccountContext, subject: ReportContentSubject, forceDark: Bool, present: @escaping (ViewController) -> Void, completion: @escaping () -> Void, requestSelectMessages: ((String, Data, String?) -> Void)?) + func makeShareController(context: AccountContext, subject: ShareControllerSubject, forceExternal: Bool, shareStory: (() -> Void)?, enqueued: (([PeerId], [Int64]) -> Void)?, actionCompleted: (() -> Void)?) -> ViewController + func makeMiniAppListScreenInitialData(context: AccountContext) -> Signal func makeMiniAppListScreen(context: AccountContext, initialData: MiniAppListScreenInitialData) -> ViewController @@ -1339,28 +1345,3 @@ public struct StickersSearchConfiguration { } } } - -public protocol ShareControllerAccountContext: AnyObject { - var accountId: AccountRecordId { get } - var accountPeerId: EnginePeer.Id { get } - var stateManager: AccountStateManager { get } - var engineData: TelegramEngine.EngineData { get } - var animationCache: AnimationCache { get } - var animationRenderer: MultiAnimationRenderer { get } - var contentSettings: ContentSettings { get } - var appConfiguration: AppConfiguration { get } - - func resolveInlineStickers(fileIds: [Int64]) -> Signal<[Int64: TelegramMediaFile], NoError> -} - -public protocol ShareControllerEnvironment: AnyObject { - var presentationData: PresentationData { get } - var updatedPresentationData: Signal { get } - var isMainApp: Bool { get } - var energyUsageSettings: EnergyUsageSettings { get } - - var mediaManager: MediaManager? { get } - - func setAccountUserInterfaceInUse(id: AccountRecordId) -> Disposable - func donateSendMessageIntent(account: ShareControllerAccountContext, peerIds: [EnginePeer.Id]) -} diff --git a/submodules/AccountContext/Sources/ShareController.swift b/submodules/AccountContext/Sources/ShareController.swift new file mode 100644 index 0000000000..3281342e1f --- /dev/null +++ b/submodules/AccountContext/Sources/ShareController.swift @@ -0,0 +1,60 @@ +import Foundation +import SwiftSignalKit +import Postbox +import TelegramCore +import TelegramPresentationData +import TelegramUIPreferences +import AnimationCache +import MultiAnimationRenderer + +public enum StorySharingSubject { + case messages([Message]) + case gift(StarGift.UniqueGift) +} + +public protocol ShareControllerAccountContext: AnyObject { + var accountId: AccountRecordId { get } + var accountPeerId: EnginePeer.Id { get } + var stateManager: AccountStateManager { get } + var engineData: TelegramEngine.EngineData { get } + var animationCache: AnimationCache { get } + var animationRenderer: MultiAnimationRenderer { get } + var contentSettings: ContentSettings { get } + var appConfiguration: AppConfiguration { get } + + func resolveInlineStickers(fileIds: [Int64]) -> Signal<[Int64: TelegramMediaFile], NoError> +} + +public protocol ShareControllerEnvironment: AnyObject { + var presentationData: PresentationData { get } + var updatedPresentationData: Signal { get } + var isMainApp: Bool { get } + var energyUsageSettings: EnergyUsageSettings { get } + + var mediaManager: MediaManager? { get } + + func setAccountUserInterfaceInUse(id: AccountRecordId) -> Disposable + func donateSendMessageIntent(account: ShareControllerAccountContext, peerIds: [EnginePeer.Id]) +} + +public enum ShareControllerExternalStatus { + case preparing(Bool) + case progress(Float) + case done +} + +public enum ShareControllerError { + case generic + case fileTooBig(Int64) +} + +public enum ShareControllerSubject { + case url(String) + case text(String) + case quote(text: String, url: String) + case messages([Message]) + case image([ImageRepresentationWithReference]) + case media(AnyMediaReference) + case mapMedia(TelegramMediaMap) + case fromExternal(([PeerId], [PeerId: Int64], String, ShareControllerAccountContext, Bool) -> Signal) +} diff --git a/submodules/ChatListUI/Sources/ChatListController.swift b/submodules/ChatListUI/Sources/ChatListController.swift index 2eb09c1bc1..90d16017c1 100644 --- a/submodules/ChatListUI/Sources/ChatListController.swift +++ b/submodules/ChatListUI/Sources/ChatListController.swift @@ -6586,7 +6586,13 @@ private final class ChatListLocationContext { if channel.flags.contains(.requestToJoin) { actionTitle = presentationData.strings.Group_ApplyToJoin } else { - actionTitle = presentationData.strings.Channel_JoinChannel + switch channel.info { + case .broadcast: + actionTitle = presentationData.strings.Channel_JoinChannel + case .group: + actionTitle = presentationData.strings.Group_JoinGroup + } + } toolbar = Toolbar(leftAction: nil, rightAction: nil, middleAction: ToolbarAction(title: actionTitle, isEnabled: true)) } diff --git a/submodules/ChatListUI/Sources/ChatListSearchListPaneNode.swift b/submodules/ChatListUI/Sources/ChatListSearchListPaneNode.swift index e0976a8e4a..7d12d87d6b 100644 --- a/submodules/ChatListUI/Sources/ChatListSearchListPaneNode.swift +++ b/submodules/ChatListUI/Sources/ChatListSearchListPaneNode.swift @@ -2865,6 +2865,7 @@ final class ChatListSearchListPaneNode: ASDisplayNode, ChatListSearchPaneNode { for renderedPeer in foundLocalPeers.peers { if renderedPeer.peerId == context.account.peerId, let peer = renderedPeer.peers[renderedPeer.peerId], filteredPeer(peer, EnginePeer(accountPeer)) { if !existingPeerIds.contains(peer.id) { + existingPeerIds.insert(peer.id) entries.append(.localPeer(peer, nil, nil, index, presentationData.theme, presentationData.strings, presentationData.nameSortOrder, presentationData.nameDisplayOrder, localExpandType, nil, false, true)) } break @@ -4402,14 +4403,24 @@ final class ChatListSearchListPaneNode: ASDisplayNode, ChatListSearchPaneNode { func scrollToTop() -> Bool { if !self.mediaNode.isHidden { return self.mediaNode.scrollToTop() - } - let offset = self.listNode.visibleContentOffset() - switch offset { - case let .known(value) where value <= CGFloat.ulpOfOne: - return false - default: - self.listNode.transaction(deleteIndices: [], insertIndicesAndItems: [], updateIndicesAndItems: [], options: [.Synchronous, .LowLatency], scrollToItem: ListViewScrollToItem(index: 0, position: .top(0.0), animated: true, curve: .Default(duration: nil), directionHint: .Up), updateSizeAndInsets: nil, stationaryItemRange: nil, updateOpaqueState: nil, completion: { _ in }) - return true + } else if !self.recentListNode.isHidden { + let offset = self.recentListNode.visibleContentOffset() + switch offset { + case let .known(value) where value <= CGFloat.ulpOfOne: + return false + default: + self.recentListNode.transaction(deleteIndices: [], insertIndicesAndItems: [], updateIndicesAndItems: [], options: [.Synchronous, .LowLatency], scrollToItem: ListViewScrollToItem(index: 0, position: .top(0.0), animated: true, curve: .Default(duration: nil), directionHint: .Up), updateSizeAndInsets: nil, stationaryItemRange: nil, updateOpaqueState: nil, completion: { _ in }) + return true + } + } else { + let offset = self.listNode.visibleContentOffset() + switch offset { + case let .known(value) where value <= CGFloat.ulpOfOne: + return false + default: + self.listNode.transaction(deleteIndices: [], insertIndicesAndItems: [], updateIndicesAndItems: [], options: [.Synchronous, .LowLatency], scrollToItem: ListViewScrollToItem(index: 0, position: .top(0.0), animated: true, curve: .Default(duration: nil), directionHint: .Up), updateSizeAndInsets: nil, stationaryItemRange: nil, updateOpaqueState: nil, completion: { _ in }) + return true + } } } diff --git a/submodules/ChatListUI/Sources/Node/ChatListHoleItem.swift b/submodules/ChatListUI/Sources/Node/ChatListHoleItem.swift index 4434f59e2f..1716677a87 100644 --- a/submodules/ChatListUI/Sources/Node/ChatListHoleItem.swift +++ b/submodules/ChatListUI/Sources/Node/ChatListHoleItem.swift @@ -191,6 +191,14 @@ class ChatListSearchEmptyFooterItemNode: ListViewItemNode { self.contentNode.frame = contentFrame } + override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { + let result = super.hitTest(point, with: event) + if let contentResult = self.contentNode.view.hitTest(self.view.convert(point, to: self.contentNode.view), with: event), contentResult === self.searchAllMessagesButton.view { + return contentResult + } + return result + } + func asyncLayout() -> (_ item: ChatListSearchEmptyFooterItem, _ params: ListViewItemLayoutParams) -> (ListViewItemNodeLayout, () -> Void) { let makeTitleNodeLayout = TextNode.asyncLayout(self.titleNode) let makeTextNodeLayout = TextNode.asyncLayout(self.textNode) diff --git a/submodules/ChatListUI/Sources/Node/ChatListItem.swift b/submodules/ChatListUI/Sources/Node/ChatListItem.swift index 9184eef095..58d1d4aa84 100644 --- a/submodules/ChatListUI/Sources/Node/ChatListItem.swift +++ b/submodules/ChatListUI/Sources/Node/ChatListItem.swift @@ -2145,6 +2145,7 @@ public class ChatListItemNode: ItemListRevealOptionsItemNode { var currentCredibilityIconContent: EmojiStatusComponent.Content? var currentVerifiedIconContent: EmojiStatusComponent.Content? var currentStatusIconContent: EmojiStatusComponent.Content? + var currentStatusIconParticleColor: UIColor? var currentSecretIconImage: UIImage? var currentForwardedIcon: UIImage? var currentStoryIcon: UIImage? @@ -3102,6 +3103,9 @@ public class ChatListItemNode: ItemListRevealOptionsItemNode { currentCredibilityIconContent = .text(color: item.presentationData.theme.chat.message.incoming.scamColor, string: item.presentationData.strings.Message_FakeAccount.uppercased()) } else if let emojiStatus = peer.emojiStatus, !premiumConfiguration.isPremiumDisabled { currentStatusIconContent = .animation(content: .customEmoji(fileId: emojiStatus.fileId), size: CGSize(width: 32.0, height: 32.0), placeholderColor: item.presentationData.theme.list.mediaPlaceholderColor, themeColor: item.presentationData.theme.list.itemAccentColor, loopMode: .count(2)) + if case let .starGift(_, _, _, _, _, innerColor, _, _, _) = emojiStatus.content { + currentStatusIconParticleColor = UIColor(rgb: UInt32(bitPattern: innerColor)) + } } else if peer.isPremium && !premiumConfiguration.isPremiumDisabled { currentCredibilityIconContent = .premium(color: item.presentationData.theme.list.itemAccentColor) } @@ -3130,6 +3134,9 @@ public class ChatListItemNode: ItemListRevealOptionsItemNode { currentCredibilityIconContent = .text(color: item.presentationData.theme.chat.message.incoming.scamColor, string: item.presentationData.strings.Message_FakeAccount.uppercased()) } else if let emojiStatus = peer.emojiStatus, !premiumConfiguration.isPremiumDisabled { currentStatusIconContent = .animation(content: .customEmoji(fileId: emojiStatus.fileId), size: CGSize(width: 32.0, height: 32.0), placeholderColor: item.presentationData.theme.list.mediaPlaceholderColor, themeColor: item.presentationData.theme.list.itemAccentColor, loopMode: .count(2)) + if case let .starGift(_, _, _, _, _, innerColor, _, _, _) = emojiStatus.content { + currentStatusIconParticleColor = UIColor(rgb: UInt32(bitPattern: innerColor)) + } } else if peer.isPremium && !premiumConfiguration.isPremiumDisabled { currentCredibilityIconContent = .premium(color: item.presentationData.theme.list.itemAccentColor) } @@ -3149,7 +3156,7 @@ public class ChatListItemNode: ItemListRevealOptionsItemNode { var titleLeftOffset: CGFloat = 0.0 if let currentVerifiedIconContent { if titleLeftOffset.isZero, case .animation = currentVerifiedIconContent { - titleLeftOffset += 20.0 + titleLeftOffset += 19.0 } if titleIconsWidth.isZero { @@ -4548,12 +4555,13 @@ public class ChatListItemNode: ItemListRevealOptionsItemNode { strongSelf.statusIconView = statusIconView strongSelf.mainContentContainerNode.view.addSubview(statusIconView) } - + let statusIconComponent = EmojiStatusComponent( context: item.context, animationCache: item.interaction.animationCache, animationRenderer: item.interaction.animationRenderer, content: currentStatusIconContent, + particleColor: currentStatusIconParticleColor, isVisibleForAnimations: strongSelf.visibilityStatus && item.context.sharedContext.energyUsageSettings.loopEmoji, action: nil ) @@ -4595,7 +4603,12 @@ public class ChatListItemNode: ItemListRevealOptionsItemNode { strongSelf.credibilityIconComponent = credibilityIconComponent let iconOrigin: CGFloat = nextTitleIconOrigin - let containerSize = CGSize(width: 20.0, height: 20.0) + let containerSize: CGSize + if case .verified = currentCredibilityIconContent { + containerSize = CGSize(width: 16.0, height: 16.0) + } else { + containerSize = CGSize(width: 20.0, height: 20.0) + } let iconSize = credibilityIconView.update( transition: .immediate, component: AnyComponent(credibilityIconComponent), diff --git a/submodules/ChatListUI/Sources/Node/ChatListNoticeItem.swift b/submodules/ChatListUI/Sources/Node/ChatListNoticeItem.swift index 65a0f6f330..b50d94405c 100644 --- a/submodules/ChatListUI/Sources/Node/ChatListNoticeItem.swift +++ b/submodules/ChatListUI/Sources/Node/ChatListNoticeItem.swift @@ -289,9 +289,8 @@ final class ChatListNoticeItemNode: ItemListRevealOptionsItemNode { titleString = attributedTitle textString = NSAttributedString(string: text, font: smallTextFont, textColor: item.theme.rootController.navigationBar.secondaryTextColor) case let .setupPhoto(accountPeer): - //TODO:localize - titleString = NSAttributedString(string: "Add your photo! 📸", font: titleFont, textColor: item.theme.rootController.navigationBar.primaryTextColor) - textString = NSAttributedString(string: "Help your friends spot you easily.", font: smallTextFont, textColor: item.theme.rootController.navigationBar.secondaryTextColor) + titleString = NSAttributedString(string: item.strings.ChatList_AddPhoto_Title, font: titleFont, textColor: item.theme.rootController.navigationBar.primaryTextColor) + textString = NSAttributedString(string: item.strings.ChatList_AddPhoto_Text, font: smallTextFont, textColor: item.theme.rootController.navigationBar.secondaryTextColor) avatarPeer = accountPeer } @@ -383,17 +382,12 @@ final class ChatListNoticeItemNode: ItemListRevealOptionsItemNode { strongSelf.arrowNode.frame = CGRect(origin: CGPoint(x: layout.size.width - sideInset - image.size.width + 8.0, y: floor((layout.size.height - image.size.height) / 2.0)), size: image.size) } - var hasCloseButton = false - if case .xmasPremiumGift = item.notice { - hasCloseButton = true - } else if case .setupBirthday = item.notice { - hasCloseButton = true - } else if case .birthdayPremiumGift = item.notice { - hasCloseButton = true - } else if case .premiumGrace = item.notice { - hasCloseButton = true - } else if case .starsSubscriptionLowBalance = item.notice { + let hasCloseButton: Bool + switch item.notice { + case .xmasPremiumGift, .setupBirthday, .birthdayPremiumGift, .premiumGrace, .starsSubscriptionLowBalance, .setupPhoto: hasCloseButton = true + default: + hasCloseButton = false } if let okButtonLayout, let cancelButtonLayout { diff --git a/submodules/Components/ReactionButtonListComponent/Sources/ReactionButtonListComponent.swift b/submodules/Components/ReactionButtonListComponent/Sources/ReactionButtonListComponent.swift index 9b450ce4ac..ce867a96d1 100644 --- a/submodules/Components/ReactionButtonListComponent/Sources/ReactionButtonListComponent.swift +++ b/submodules/Components/ReactionButtonListComponent/Sources/ReactionButtonListComponent.swift @@ -1225,7 +1225,9 @@ public final class ReactionButtonAsyncNode: ContextControllerSourceView { let tintColor: UIColor if layout.backgroundLayout.colors.isSelected { - if layout.spec.component.colors.selectedForeground != 0 { + if layout.spec.component.colors.selectedIconTintColor != 0 { + tintColor = UIColor(argb: layout.spec.component.colors.selectedIconTintColor) + } else if layout.spec.component.colors.selectedForeground != 0 { tintColor = UIColor(argb: layout.spec.component.colors.selectedForeground) } else { tintColor = .white @@ -1352,6 +1354,7 @@ public final class ReactionButtonComponent: Equatable { public var selectedBackground: UInt32 public var deselectedForeground: UInt32 public var selectedForeground: UInt32 + public var selectedIconTintColor: UInt32 public var deselectedStarsBackground: UInt32 public var selectedStarsBackground: UInt32 public var deselectedStarsForeground: UInt32 @@ -1367,6 +1370,7 @@ public final class ReactionButtonComponent: Equatable { selectedBackground: UInt32, deselectedForeground: UInt32, selectedForeground: UInt32, + selectedIconTintColor: UInt32, deselectedStarsBackground: UInt32, selectedStarsBackground: UInt32, deselectedStarsForeground: UInt32, @@ -1381,6 +1385,7 @@ public final class ReactionButtonComponent: Equatable { self.selectedBackground = selectedBackground self.deselectedForeground = deselectedForeground self.selectedForeground = selectedForeground + self.selectedIconTintColor = selectedIconTintColor self.deselectedStarsBackground = deselectedStarsBackground self.selectedStarsBackground = selectedStarsBackground self.deselectedStarsForeground = deselectedStarsForeground diff --git a/submodules/ContactListUI/Sources/ContactListNode.swift b/submodules/ContactListUI/Sources/ContactListNode.swift index 65fe2836a5..d15cd4ab70 100644 --- a/submodules/ContactListUI/Sources/ContactListNode.swift +++ b/submodules/ContactListUI/Sources/ContactListNode.swift @@ -221,25 +221,17 @@ private enum ContactListNodeEntry: Comparable, Identifiable { })] } - - var storyStats: (total: Int, unseen: Int, hasUnseenCloseFriends: Bool)? if let customSubtitle { status = .custom(string: NSAttributedString(string: customSubtitle), multiline: false, isActive: false, icon: nil) - } else if let storyData { - storyStats = (storyData.count, storyData.unseenCount, storyData.hasUnseenCloseFriends) - - let text: String - text = presentationData.strings.ChatList_ArchiveStoryCount(Int32(storyData.count)) - status = .custom(string: NSAttributedString(string: text), multiline: false, isActive: false, icon: nil) } - return ContactsPeerItem(presentationData: ItemListPresentationData(presentationData), sortOrder: nameSortOrder, displayOrder: nameDisplayOrder, context: context, peerMode: isSearch ? .generalSearch(isSavedMessages: false) : .peer, peer: itemPeer, status: status, requiresPremiumForMessaging: requiresPremiumForMessaging, enabled: enabled, selection: selection, selectionPosition: .left, editing: ContactsPeerItemEditing(editable: false, editing: false, revealed: false), additionalActions: additionalActions, index: nil, header: header, action: { _ in - interaction.openPeer(peer, .generic, nil, nil) - }, disabledAction: { _ in - if case let .peer(peer, _, _) = peer { - interaction.openDisabledPeer(EnginePeer(peer), requiresPremiumForMessaging ? .premiumRequired : .generic) - } - }, itemHighlighting: interaction.itemHighlighting, contextAction: itemContextAction, storyStats: storyStats, openStories: { peer, sourceNode in + return ContactsPeerItem(presentationData: ItemListPresentationData(presentationData), sortOrder: nameSortOrder, displayOrder: nameDisplayOrder, context: context, peerMode: isSearch ? .generalSearch(isSavedMessages: false) : .peer, peer: itemPeer, status: status, requiresPremiumForMessaging: requiresPremiumForMessaging, enabled: enabled, selection: selection, selectionPosition: .left, editing: ContactsPeerItemEditing(editable: false, editing: false, revealed: false), additionalActions: additionalActions, index: nil, header: header, action: { _ in + interaction.openPeer(peer, .generic, nil, nil) + }, disabledAction: { _ in + if case let .peer(peer, _, _) = peer { + interaction.openDisabledPeer(EnginePeer(peer), requiresPremiumForMessaging ? .premiumRequired : .generic) + } + }, itemHighlighting: interaction.itemHighlighting, contextAction: itemContextAction, storyStats: nil, openStories: { peer, sourceNode in if case let .peer(peerValue, _) = peer, let peerValue { interaction.openStories(peerValue, sourceNode) } @@ -581,15 +573,7 @@ private func contactListNodeEntries(accountPeer: EnginePeer?, peers: [ContactLis case let .custom(showSelf, sections): if !topPeers.isEmpty { var index: Int = 0 - - 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)) - existingPeerIds.insert(.peer(peer.id)) - } - } - + var sectionId: Int = 2 for (title, peerIds, hasActions) in sections { var allSelected = true @@ -647,6 +631,14 @@ private func contactListNodeEntries(accountPeer: EnginePeer?, peers: [ContactLis sectionId += 1 } + 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)) + existingPeerIds.insert(.peer(peer.id)) + } + } + var hasDeselectAll = !(selectionState?.selectedPeerIndices ?? [:]).isEmpty if !sections.isEmpty, let selectionState { var hasNonBirthdayPeers = false diff --git a/submodules/ContactsPeerItem/Sources/ContactsPeerItem.swift b/submodules/ContactsPeerItem/Sources/ContactsPeerItem.swift index e67e462239..035ef01b1c 100644 --- a/submodules/ContactsPeerItem/Sources/ContactsPeerItem.swift +++ b/submodules/ContactsPeerItem/Sources/ContactsPeerItem.swift @@ -794,7 +794,7 @@ public class ContactsPeerItemNode: ItemListRevealOptionsItemNode { var verifiedIcon: EmojiStatusComponent.Content? switch item.peer { case let .peer(peer, _): - if let peer = peer, (peer.id != item.context.account.peerId || item.peerMode == .memberList || item.aliasHandling == .treatSelfAsSaved) { + if let peer = peer, (peer.id != item.context.account.peerId || item.peerMode == .memberList || item.aliasHandling == .standard) { if peer.isScam { credibilityIcon = .text(color: item.presentationData.theme.chat.message.incoming.scamColor, string: item.presentationData.strings.Message_ScamAccount.uppercased()) } else if peer.isFake { @@ -1422,13 +1422,6 @@ public class ContactsPeerItemNode: ItemListRevealOptionsItemNode { ) strongSelf.verifiedIconComponent = verifiedIconComponent - let iconOrigin: CGFloat - if case .animation = verifiedIcon { - iconOrigin = titleFrame.minX - } else { - nextIconX += 4.0 - iconOrigin = nextIconX - } let containerSize = CGSize(width: 16.0, height: 16.0) let iconSize = verifiedIconView.update( @@ -1438,14 +1431,10 @@ public class ContactsPeerItemNode: ItemListRevealOptionsItemNode { containerSize: containerSize ) - transition.updateFrame(view: verifiedIconView, frame: CGRect(origin: CGPoint(x: iconOrigin, y: floorToScreenPixels(titleFrame.midY - iconSize.height / 2.0)), size: iconSize)) + transition.updateFrame(view: verifiedIconView, frame: CGRect(origin: CGPoint(x: titleFrame.minX, y: floorToScreenPixels(titleFrame.midY - iconSize.height / 2.0)), size: iconSize)) - if case .animation = verifiedIcon { - titleLeftOffset += iconSize.width + 4.0 - nextIconX += iconSize.width - } else { - nextIconX += iconSize.width - } + titleLeftOffset += iconSize.width + 4.0 + nextIconX += iconSize.width + 4.0 } else if let verifiedIconView = strongSelf.verifiedIconView { strongSelf.verifiedIconView = nil verifiedIconView.removeFromSuperview() diff --git a/submodules/ContextUI/Sources/ContextController.swift b/submodules/ContextUI/Sources/ContextController.swift index dbcab9d1ea..fd8b12acdd 100644 --- a/submodules/ContextUI/Sources/ContextController.swift +++ b/submodules/ContextUI/Sources/ContextController.swift @@ -2490,6 +2490,7 @@ public final class ContextController: ViewController, StandalonePresentableContr public var immediateItemsTransitionAnimation = false let workaroundUseLegacyImplementation: Bool let disableScreenshots: Bool + let hideReactionPanelTail: Bool public enum HandledTouchEvent { case ignore @@ -2505,7 +2506,7 @@ public final class ContextController: ViewController, StandalonePresentableContr public var getOverlayViews: (() -> [UIView])? - convenience public init(context: AccountContext? = nil, presentationData: PresentationData, source: ContextContentSource, items: Signal, recognizer: TapLongTapOrDoubleTapGestureRecognizer? = nil, gesture: ContextGesture? = nil, workaroundUseLegacyImplementation: Bool = false, disableScreenshots: Bool = false) { + convenience public init(context: AccountContext? = nil, presentationData: PresentationData, source: ContextContentSource, items: Signal, recognizer: TapLongTapOrDoubleTapGestureRecognizer? = nil, gesture: ContextGesture? = nil, workaroundUseLegacyImplementation: Bool = false, disableScreenshots: Bool = false, hideReactionPanelTail: Bool = false) { self.init( context: context, presentationData: presentationData, @@ -2521,7 +2522,8 @@ public final class ContextController: ViewController, StandalonePresentableContr recognizer: recognizer, gesture: gesture, workaroundUseLegacyImplementation: workaroundUseLegacyImplementation, - disableScreenshots: disableScreenshots + disableScreenshots: disableScreenshots, + hideReactionPanelTail: hideReactionPanelTail ) } @@ -2532,7 +2534,8 @@ public final class ContextController: ViewController, StandalonePresentableContr recognizer: TapLongTapOrDoubleTapGestureRecognizer? = nil, gesture: ContextGesture? = nil, workaroundUseLegacyImplementation: Bool = false, - disableScreenshots: Bool = false + disableScreenshots: Bool = false, + hideReactionPanelTail: Bool = false ) { self.context = context self.presentationData = presentationData @@ -2541,6 +2544,7 @@ public final class ContextController: ViewController, StandalonePresentableContr self.gesture = gesture self.workaroundUseLegacyImplementation = workaroundUseLegacyImplementation self.disableScreenshots = disableScreenshots + self.hideReactionPanelTail = hideReactionPanelTail super.init(navigationBarPresentationData: nil) diff --git a/submodules/ContextUI/Sources/ContextControllerExtractedPresentationNode.swift b/submodules/ContextUI/Sources/ContextControllerExtractedPresentationNode.swift index d229d6ddba..e5f05efef2 100644 --- a/submodules/ContextUI/Sources/ContextControllerExtractedPresentationNode.swift +++ b/submodules/ContextUI/Sources/ContextControllerExtractedPresentationNode.swift @@ -645,7 +645,7 @@ final class ContextControllerExtractedPresentationNode: ASDisplayNode, ContextCo var contentTopInset: CGFloat = topInset var removedReactionContextNode: ReactionContextNode? - if let reactionItems = self.actionsStackNode.topReactionItems, !reactionItems.reactionItems.isEmpty { + if let reactionItems = self.actionsStackNode.topReactionItems, !reactionItems.reactionItems.isEmpty, let controller = self.getController() as? ContextController { let reactionContextNode: ReactionContextNode if let current = self.reactionContextNode { reactionContextNode = current @@ -681,6 +681,7 @@ final class ContextControllerExtractedPresentationNode: ASDisplayNode, ContextCo strongSelf.requestUpdateOverlayWantsToBeBelowKeyboard(transition) } ) + reactionContextNode.displayTail = !controller.hideReactionPanelTail self.reactionContextNode = reactionContextNode self.addSubnode(reactionContextNode) diff --git a/submodules/DrawingUI/BUILD b/submodules/DrawingUI/BUILD index 9eb89f2f76..4e444e4071 100644 --- a/submodules/DrawingUI/BUILD +++ b/submodules/DrawingUI/BUILD @@ -98,7 +98,7 @@ swift_library( "//submodules/TelegramUI/Components/LottieComponent", "//submodules/TelegramUI/Components/LottieComponentResourceContent", "//submodules/ImageTransparency", - "//submodules/GalleryUI", + #"//submodules/GalleryUI", "//submodules/MediaPlayer:UniversalMediaPlayer", "//submodules/TelegramUniversalVideoContent", "//submodules/TelegramUI/Components/CameraButtonComponent", diff --git a/submodules/DrawingUI/Sources/DrawingEntitiesView.swift b/submodules/DrawingUI/Sources/DrawingEntitiesView.swift index e50f772e3a..7f65ce1f9d 100644 --- a/submodules/DrawingUI/Sources/DrawingEntitiesView.swift +++ b/submodules/DrawingUI/Sources/DrawingEntitiesView.swift @@ -810,6 +810,8 @@ public final class DrawingEntitiesView: UIView, TGPhotoDrawingEntitiesView { selectionView.handlePan(gestureRecognizer) } else if let stickerEntity = selectedEntityView.entity as? DrawingStickerEntity, case .message = stickerEntity.content { selectionView.handlePan(gestureRecognizer) + } else if let stickerEntity = selectedEntityView.entity as? DrawingStickerEntity, case .gift = stickerEntity.content { + selectionView.handlePan(gestureRecognizer) } else { var isTrappedInBin = false let scale = 100.0 / selectedEntityView.bounds.size.width diff --git a/submodules/DrawingUI/Sources/DrawingScreen.swift b/submodules/DrawingUI/Sources/DrawingScreen.swift index ca595aadc5..b98216aa6c 100644 --- a/submodules/DrawingUI/Sources/DrawingScreen.swift +++ b/submodules/DrawingUI/Sources/DrawingScreen.swift @@ -3096,6 +3096,8 @@ public final class DrawingToolsInteraction { isAdditional = isAdditionalValue } else if case .message = entity.content { isMessage = true + } else if case .gift = entity.content { + isMessage = true } } else if entityView.entity is DrawingLinkEntity { isLink = true diff --git a/submodules/DrawingUI/Sources/DrawingStickerEntityView.swift b/submodules/DrawingUI/Sources/DrawingStickerEntityView.swift index 99f3629b04..29f2e4ce2b 100644 --- a/submodules/DrawingUI/Sources/DrawingStickerEntityView.swift +++ b/submodules/DrawingUI/Sources/DrawingStickerEntityView.swift @@ -142,6 +142,8 @@ public class DrawingStickerEntityView: DrawingEntityView { return image } else if case .message = self.stickerEntity.content { return self.animatedImageView?.image + } else if case .gift = self.stickerEntity.content { + return self.animatedImageView?.image } else { return nil } @@ -167,7 +169,7 @@ public class DrawingStickerEntityView: DrawingEntityView { return file.dimensions?.cgSize ?? CGSize(width: 512.0, height: 512.0) case .dualVideoReference: return CGSize(width: 512.0, height: 512.0) - case let .message(_, size, _, _, _): + case let .message(_, size, _, _, _), let .gift(_, size): return size } } @@ -296,6 +298,43 @@ public class DrawingStickerEntityView: DrawingEntityView { if let file, let _ = mediaRect { self.setupWithVideo(file) } + } else if case let .gift(gift, _) = self.stickerEntity.content { + if let image = self.stickerEntity.renderImage { + self.setupWithImage(image, overlayImage: self.stickerEntity.overlayRenderImage) + } + + var file: TelegramMediaFile? + for attribute in gift.attributes { + if case let .model(_, fileValue, _) = attribute { + file = fileValue + break + } + } + guard let file, let dimensions = file.dimensions else { + return + } + if self.animationNode == nil { + let animationNode = DefaultAnimatedStickerNodeImpl() + animationNode.clipsToBounds = true + animationNode.autoplay = false + self.animationNode = animationNode + animationNode.started = { [weak self, weak animationNode] in + self?.imageNode.isHidden = true + + if let animationNode = animationNode { + let _ = (animationNode.status + |> take(1) + |> deliverOnMainQueue).start(next: { [weak self] status in + self?.started?(status.duration) + }) + } + } + self.addSubnode(self.imageNode) + self.addSubnode(animationNode) + } + self.imageNode.setSignal(chatMessageAnimatedSticker(postbox: self.context.account.postbox, userLocation: .other, file: file, small: false, size: dimensions.cgSize.aspectFitted(CGSize(width: 256.0, height: 256.0)))) + self.stickerFetchedDisposable.set(freeMediaFileResourceInteractiveFetched(account: self.context.account, userLocation: .other, fileReference: stickerPackFileReference(file), resource: file.resource).start()) + self.setNeedsLayout() } } @@ -418,7 +457,18 @@ public class DrawingStickerEntityView: DrawingEntityView { if self.isPlaying != isPlaying { self.isPlaying = isPlaying - if let file = self.file { + var file: TelegramMediaFile? + if let fileValue = self.file { + file = fileValue + } else if case let .gift(gift, _) = self.stickerEntity.content { + for attribute in gift.attributes { + if case let .model(_, fileValue, _) = attribute { + file = fileValue + break + } + } + } + if let file { if isPlaying && !self.didSetUpAnimationNode { self.didSetUpAnimationNode = true let dimensions = file.dimensions ?? PixelDimensions(width: 512, height: 512) @@ -596,15 +646,22 @@ public class DrawingStickerEntityView: DrawingEntityView { let imageSize = self.dimensions.aspectFitted(boundingSize) let imageFrame = CGRect(origin: CGPoint(x: floor((size.width - imageSize.width) / 2.0), y: (size.height - imageSize.height) / 2.0), size: imageSize) - - self.imageNode.asyncLayout()(TransformImageArguments(corners: ImageCorners(), imageSize: imageSize, boundingSize: imageSize, intrinsicInsets: UIEdgeInsets()))() - self.imageNode.frame = imageFrame + + var animationSize = CGSize(width: imageSize.width, height: imageSize.width) + var animationFrame = imageFrame + if case .gift = self.stickerEntity.content { + animationSize = CGSize(width: animationSize.width * 0.48, height: animationSize.height * 0.48) + animationFrame = CGRect(origin: CGPoint(x: floor((size.width - animationSize.width) / 2.0), y: 22.0), size: animationSize) + } + + self.imageNode.asyncLayout()(TransformImageArguments(corners: ImageCorners(), imageSize: animationSize, boundingSize: animationSize, intrinsicInsets: UIEdgeInsets()))() + self.imageNode.frame = animationFrame if let animationNode = self.animationNode { if self.isReaction { - animationNode.cornerRadius = floor(imageSize.width * 0.1) + animationNode.cornerRadius = floor(animationSize.width * 0.1) } - animationNode.frame = imageFrame - animationNode.updateLayout(size: imageSize) + animationNode.frame = animationFrame + animationNode.updateLayout(size: animationSize) if !self.didApplyVisibility { self.didApplyVisibility = true @@ -818,6 +875,35 @@ public class DrawingStickerEntityView: DrawingEntityView { return entities } + } else if case let .gift(gift, _) = self.stickerEntity.content { + var file: TelegramMediaFile? + for attribute in gift.attributes { + if case let .model(_, fileValue, _) = attribute { + file = fileValue + break + } + } + if let file, let animationNode = self.animationNode { + let stickerSize = self.bounds.size + let stickerPosition = self.stickerEntity.position + let videoSize = animationNode.frame.size + let scale = self.stickerEntity.scale + let rotation = self.stickerEntity.rotation + + let videoPosition = animationNode.position.offsetBy(dx: -stickerSize.width / 2.0, dy: -stickerSize.height / 2.0) + let videoScale = videoSize.width / stickerSize.width + + let videoEntity = DrawingStickerEntity(content: .file(.standalone(media: file), .sticker)) + videoEntity.referenceDrawingSize = self.stickerEntity.referenceDrawingSize + videoEntity.position = stickerPosition.offsetBy( + dx: (videoPosition.x * cos(rotation) - videoPosition.y * sin(rotation)) * scale, + dy: (videoPosition.y * cos(rotation) + videoPosition.x * sin(rotation)) * scale + ) + videoEntity.scale = scale * videoScale + videoEntity.rotation = rotation + + return [videoEntity] + } } return [] } @@ -1103,6 +1189,9 @@ final class DrawingStickerEntitySelectionView: DrawingEntitySelectionView { if case .message = entity.content { cornerRadius *= 2.1 count = 24 + } else if case .gift = entity.content { + cornerRadius *= 2.1 + count = 24 } else if case .image = entity.content { count = 24 } diff --git a/submodules/ItemListPeerItem/Sources/ItemListPeerItem.swift b/submodules/ItemListPeerItem/Sources/ItemListPeerItem.swift index 2ad809fcf2..6256672ee8 100644 --- a/submodules/ItemListPeerItem/Sources/ItemListPeerItem.swift +++ b/submodules/ItemListPeerItem/Sources/ItemListPeerItem.swift @@ -1438,7 +1438,7 @@ public class ItemListPeerItemNode: ItemListRevealOptionsItemNode, ItemListItemNo var titleLeftOffset: CGFloat = 0.0 var nextIconX: CGFloat = titleFrame.maxX - if let verifiedIcon = verifiedIcon { + if let verifiedIcon { let animationCache = item.context.animationCache let animationRenderer = item.context.animationRenderer @@ -1463,29 +1463,18 @@ public class ItemListPeerItemNode: ItemListRevealOptionsItemNode, ItemListItemNo emojiFileUpdated: nil ) strongSelf.verifiedIconComponent = verifiedIconComponent - - let iconOrigin: CGFloat - if case .animation = verifiedIcon { - iconOrigin = titleFrame.minX - } else { - nextIconX += 4.0 - iconOrigin = nextIconX - } - + let iconSize = verifiedIconView.update( transition: .immediate, component: AnyComponent(verifiedIconComponent), environment: {}, - containerSize: CGSize(width: 20.0, height: 20.0) + containerSize: CGSize(width: 16.0, height: 16.0) ) - transition.updateFrame(view: verifiedIconView, frame: CGRect(origin: CGPoint(x: iconOrigin, y: floorToScreenPixels(titleFrame.midY - iconSize.height / 2.0)), size: iconSize)) + transition.updateFrame(view: verifiedIconView, frame: CGRect(origin: CGPoint(x: titleFrame.minX, y: floorToScreenPixels(titleFrame.midY - iconSize.height / 2.0)), size: iconSize)) - if case .animation = verifiedIcon { - titleLeftOffset += iconSize.width + 4.0 - } else { - nextIconX += iconSize.width - } + titleLeftOffset += iconSize.width + 4.0 + nextIconX += iconSize.width + 4.0 } else if let verifiedIconView = strongSelf.verifiedIconView { strongSelf.verifiedIconView = nil verifiedIconView.removeFromSuperview() diff --git a/submodules/LegacyMediaPickerUI/Sources/LegacyPaintStickersContext.swift b/submodules/LegacyMediaPickerUI/Sources/LegacyPaintStickersContext.swift index 79d1597354..73f16210f2 100644 --- a/submodules/LegacyMediaPickerUI/Sources/LegacyPaintStickersContext.swift +++ b/submodules/LegacyMediaPickerUI/Sources/LegacyPaintStickersContext.swift @@ -147,7 +147,7 @@ private class LegacyPaintStickerEntity: LegacyPaintEntity { case let .image(image, _): self.file = nil self.imagePromise.set(.single(image)) - case .animatedImage, .video, .dualVideoReference, .message: + case .animatedImage, .video, .dualVideoReference, .message, .gift: self.file = nil } } diff --git a/submodules/ShareController/BUILD b/submodules/ShareController/BUILD index 096622d8c8..3f541667da 100644 --- a/submodules/ShareController/BUILD +++ b/submodules/ShareController/BUILD @@ -43,7 +43,9 @@ swift_library( "//submodules/Components/MultilineTextComponent", "//submodules/Components/BundleIconComponent", "//submodules/TelegramUI/Components/LottieComponent", - #"//submodules/TelegramUI/Components/MessageInputPanelComponent", + "//submodules/TelegramUI/Components/MessageInputPanelComponent", + "//submodules/TelegramUI/Components/ChatEntityKeyboardInputNode", + "//submodules/ChatPresentationInterfaceState", ], visibility = [ "//visibility:public", diff --git a/submodules/ShareController/Sources/ShareController.swift b/submodules/ShareController/Sources/ShareController.swift index 09447ffa43..27bb68504b 100644 --- a/submodules/ShareController/Sources/ShareController.swift +++ b/submodules/ShareController/Sources/ShareController.swift @@ -40,17 +40,6 @@ public enum ShareControllerPreferredAction { case custom(action: ShareControllerAction) } -public enum ShareControllerExternalStatus { - case preparing(Bool) - case progress(Float) - case done -} - -public enum ShareControllerError { - case generic - case fileTooBig(Int64) -} - public struct ShareControllerSegmentedValue { let title: String let subject: ShareControllerSubject @@ -65,17 +54,6 @@ public struct ShareControllerSegmentedValue { } } -public enum ShareControllerSubject { - case url(String) - case text(String) - case quote(text: String, url: String) - case messages([Message]) - case image([ImageRepresentationWithReference]) - case media(AnyMediaReference) - case mapMedia(TelegramMediaMap) - case fromExternal(([PeerId], [PeerId: Int64], String, ShareControllerAccountContext, Bool) -> Signal) -} - private enum ExternalShareItem { case text(String) case url(URL) @@ -686,6 +664,8 @@ public final class ShareController: ViewController { var fromPublicChannel = false if case let .messages(messages) = self.subject, let message = messages.first, let peer = message.peers[message.id.peerId] as? TelegramChannel, case .broadcast = peer.info { fromPublicChannel = true + } else if case let .url(link) = self.subject, link.contains("t.me/nft/") { + fromPublicChannel = true } self.displayNode = ShareControllerNode(controller: self, environment: self.environment, presentationData: self.presentationData, presetText: self.presetText, defaultAction: self.defaultAction, requestLayout: { [weak self] transition in diff --git a/submodules/StickerPackPreviewUI/BUILD b/submodules/StickerPackPreviewUI/BUILD index 00654a3f56..1d935d2ea6 100644 --- a/submodules/StickerPackPreviewUI/BUILD +++ b/submodules/StickerPackPreviewUI/BUILD @@ -18,7 +18,6 @@ swift_library( "//submodules/TelegramPresentationData:TelegramPresentationData", "//submodules/AccountContext:AccountContext", "//submodules/TelegramUIPreferences:TelegramUIPreferences", - "//submodules/ShareController:ShareController", "//submodules/StickerResources:StickerResources", "//submodules/AlertUI:AlertUI", "//submodules/PresentationDataUtils:PresentationDataUtils", diff --git a/submodules/StickerPackPreviewUI/Sources/StickerPackPreviewController.swift b/submodules/StickerPackPreviewUI/Sources/StickerPackPreviewController.swift index c788f58895..8acbcb2030 100644 --- a/submodules/StickerPackPreviewUI/Sources/StickerPackPreviewController.swift +++ b/submodules/StickerPackPreviewUI/Sources/StickerPackPreviewController.swift @@ -7,7 +7,6 @@ import TelegramCore import SwiftSignalKit import TelegramUIPreferences import AccountContext -import ShareController import StickerResources import AlertUI import PresentationDataUtils @@ -116,15 +115,20 @@ public final class StickerPackPreviewController: ViewController, StandalonePrese } if let stickerPackContentsValue = strongSelf.stickerPackContentsValue, case let .result(info, _, _) = stickerPackContentsValue, !info.shortName.isEmpty { - let shareController = ShareController(context: strongSelf.context, subject: .url("https://t.me/addstickers/\(info.shortName)"), externalShare: true) - let parentNavigationController = strongSelf.parentNavigationController - shareController.actionCompleted = { [weak parentNavigationController] in - if let parentNavigationController = parentNavigationController, let controller = parentNavigationController.topViewController as? ViewController { - let presentationData = strongSelf.context.sharedContext.currentPresentationData.with { $0 } - controller.present(UndoOverlayController(presentationData: presentationData, content: .linkCopied(title: nil, text: presentationData.strings.Conversation_LinkCopied), elevatedLayout: false, animateInAsReplacement: false, action: { _ in return false }), in: .window(.root)) + let shareController = strongSelf.context.sharedContext.makeShareController( + context: strongSelf.context, + subject: .url("https://t.me/addstickers/\(info.shortName)"), + forceExternal: true, + shareStory: nil, + enqueued: nil, + actionCompleted: { [weak parentNavigationController] in + if let parentNavigationController = parentNavigationController, let controller = parentNavigationController.topViewController as? ViewController { + let presentationData = strongSelf.context.sharedContext.currentPresentationData.with { $0 } + controller.present(UndoOverlayController(presentationData: presentationData, content: .linkCopied(title: nil, text: presentationData.strings.Conversation_LinkCopied), elevatedLayout: false, animateInAsReplacement: false, action: { _ in return false }), in: .window(.root)) + } } - } + ) strongSelf.present(shareController, in: .window(.root)) strongSelf.dismiss() } diff --git a/submodules/StickerPackPreviewUI/Sources/StickerPackPreviewGridItem.swift b/submodules/StickerPackPreviewUI/Sources/StickerPackPreviewGridItem.swift index e98d2366d8..475962b539 100644 --- a/submodules/StickerPackPreviewUI/Sources/StickerPackPreviewGridItem.swift +++ b/submodules/StickerPackPreviewUI/Sources/StickerPackPreviewGridItem.swift @@ -13,7 +13,6 @@ import TelegramPresentationData import ShimmerEffect import StickerPeekUI import TextFormat -import Accelerate final class StickerPackPreviewInteraction { var previewedItem: StickerPreviewPeekItem? @@ -536,100 +535,3 @@ final class StickerPackPreviewGridItemNode: GridItemNode { } } } - -private func getAverageColor(image: UIImage) -> UIColor? { - let blurredWidth = 16 - let blurredHeight = 16 - let blurredBytesPerRow = blurredWidth * 4 - guard let context = DrawingContext(size: CGSize(width: CGFloat(blurredWidth), height: CGFloat(blurredHeight)), scale: 1.0, opaque: true, bytesPerRow: blurredBytesPerRow) else { - return nil - } - - let size = CGSize(width: CGFloat(blurredWidth), height: CGFloat(blurredHeight)) - - if let cgImage = image.cgImage { - context.withFlippedContext { c in - c.setFillColor(UIColor.white.cgColor) - c.fill(CGRect(origin: CGPoint(), size: size)) - c.draw(cgImage, in: CGRect(origin: CGPoint(x: -size.width / 2.0, y: -size.height / 2.0), size: CGSize(width: size.width * 1.8, height: size.height * 1.8))) - } - } - - var destinationBuffer = vImage_Buffer() - destinationBuffer.width = UInt(blurredWidth) - destinationBuffer.height = UInt(blurredHeight) - destinationBuffer.data = context.bytes - destinationBuffer.rowBytes = context.bytesPerRow - - vImageBoxConvolve_ARGB8888(&destinationBuffer, - &destinationBuffer, - nil, - 0, 0, - UInt32(15), - UInt32(15), - nil, - vImage_Flags(kvImageTruncateKernel)) - - let divisor: Int32 = 0x1000 - - let rwgt: CGFloat = 0.3086 - let gwgt: CGFloat = 0.6094 - let bwgt: CGFloat = 0.0820 - - let adjustSaturation: CGFloat = 1.7 - - let a = (1.0 - adjustSaturation) * rwgt + adjustSaturation - let b = (1.0 - adjustSaturation) * rwgt - let c = (1.0 - adjustSaturation) * rwgt - let d = (1.0 - adjustSaturation) * gwgt - let e = (1.0 - adjustSaturation) * gwgt + adjustSaturation - let f = (1.0 - adjustSaturation) * gwgt - let g = (1.0 - adjustSaturation) * bwgt - let h = (1.0 - adjustSaturation) * bwgt - let i = (1.0 - adjustSaturation) * bwgt + adjustSaturation - - let satMatrix: [CGFloat] = [ - a, b, c, 0, - d, e, f, 0, - g, h, i, 0, - 0, 0, 0, 1 - ] - - var matrix: [Int16] = satMatrix.map { value in - return Int16(value * CGFloat(divisor)) - } - - vImageMatrixMultiply_ARGB8888(&destinationBuffer, &destinationBuffer, &matrix, divisor, nil, nil, vImage_Flags(kvImageDoNotTile)) - - context.withFlippedContext { c in - c.setFillColor(UIColor.white.withMultipliedAlpha(0.1).cgColor) - c.fill(CGRect(origin: CGPoint(), size: size)) - } - - var sumR: UInt64 = 0 - var sumG: UInt64 = 0 - var sumB: UInt64 = 0 - var sumA: UInt64 = 0 - - for y in 0 ..< blurredHeight { - let row = context.bytes.assumingMemoryBound(to: UInt8.self).advanced(by: y * blurredBytesPerRow) - for x in 0 ..< blurredWidth { - let pixel = row.advanced(by: x * 4) - sumB += UInt64(pixel.advanced(by: 0).pointee) - sumG += UInt64(pixel.advanced(by: 1).pointee) - sumR += UInt64(pixel.advanced(by: 2).pointee) - sumA += UInt64(pixel.advanced(by: 3).pointee) - } - } - sumR /= UInt64(blurredWidth * blurredHeight) - sumG /= UInt64(blurredWidth * blurredHeight) - sumB /= UInt64(blurredWidth * blurredHeight) - sumA /= UInt64(blurredWidth * blurredHeight) - sumA = 255 - - var color = UIColor(red: CGFloat(sumR) / 255.0, green: CGFloat(sumG) / 255.0, blue: CGFloat(sumB) / 255.0, alpha: CGFloat(sumA) / 255.0) - if color.lightness > 0.8 { - color = color.withMultipliedBrightnessBy(0.8) - } - return color -} diff --git a/submodules/StickerPackPreviewUI/Sources/StickerPackScreen.swift b/submodules/StickerPackPreviewUI/Sources/StickerPackScreen.swift index 0be8ae331a..ff097a6e60 100644 --- a/submodules/StickerPackPreviewUI/Sources/StickerPackScreen.swift +++ b/submodules/StickerPackPreviewUI/Sources/StickerPackScreen.swift @@ -13,7 +13,6 @@ import ShimmerEffect import ContextUI import MoreButtonNode import UndoUI -import ShareController import TextFormat import PremiumUI import OverlayStatusController @@ -306,18 +305,7 @@ private final class StickerPackContainer: ASDisplayNode { guard let self else { return } - if let mainPreviewIconView = self.mainPreviewIcon?.view { - mainPreviewIconView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { [weak self] _ in - guard let self else { - return - } - if let mainPreviewIconView = self.mainPreviewIcon?.view { - self.mainPreviewIcon = nil - mainPreviewIconView.removeFromSuperview() - } - }) - mainPreviewIconView.layer.animateScale(from: 1.0, to: 0.5, duration: 0.2, removeOnCompletion: false) - } + self.hideMainPreviewIcon() } self.gridNode.interactiveScrollingEnded = { [weak self] in @@ -643,6 +631,8 @@ private final class StickerPackContainer: ASDisplayNode { return nil }, present: { [weak self] content, sourceView, sourceRect in if let strongSelf = self { + strongSelf.hideMainPreviewIcon() + let controller = PeekController(presentationData: strongSelf.presentationData, content: content, sourceView: { return (sourceView, sourceRect) }) @@ -697,6 +687,21 @@ private final class StickerPackContainer: ASDisplayNode { self.gridNode.view.addGestureRecognizer(reorderingGestureRecognizer) } + private func hideMainPreviewIcon() { + if let mainPreviewIconView = self.mainPreviewIcon?.view { + mainPreviewIconView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { [weak self] _ in + guard let self else { + return + } + if let mainPreviewIconView = self.mainPreviewIcon?.view { + self.mainPreviewIcon = nil + mainPreviewIconView.removeFromSuperview() + } + }) + mainPreviewIconView.layer.animateScale(from: 1.0, to: 0.5, duration: 0.2, removeOnCompletion: false) + } + } + private var reorderFeedback: HapticFeedback? private var reorderNode: ReorderingItemNode? private var reorderInitialIndex: Int? @@ -1127,13 +1132,19 @@ private final class StickerPackContainer: ASDisplayNode { if let strongSelf = self { let parentNavigationController = strongSelf.controller?.parentNavigationController - let shareController = ShareController(context: strongSelf.context, subject: shareSubject) - shareController.actionCompleted = { [weak parentNavigationController] in - if let parentNavigationController = parentNavigationController, let controller = parentNavigationController.topViewController as? ViewController { - let presentationData = strongSelf.context.sharedContext.currentPresentationData.with { $0 } - controller.present(UndoOverlayController(presentationData: presentationData, content: .linkCopied(title: nil, text: presentationData.strings.Conversation_LinkCopied), elevatedLayout: false, animateInAsReplacement: false, action: { _ in return false }), in: .window(.root)) + let shareController = strongSelf.context.sharedContext.makeShareController( + context: strongSelf.context, + subject: shareSubject, + forceExternal: false, + shareStory: nil, + enqueued: nil, + actionCompleted: { [weak parentNavigationController] in + if let parentNavigationController = parentNavigationController, let controller = parentNavigationController.topViewController as? ViewController { + let presentationData = strongSelf.context.sharedContext.currentPresentationData.with { $0 } + controller.present(UndoOverlayController(presentationData: presentationData, content: .linkCopied(title: nil, text: presentationData.strings.Conversation_LinkCopied), elevatedLayout: false, animateInAsReplacement: false, action: { _ in return false }), in: .window(.root)) + } } - } + ) strongSelf.controller?.present(shareController, in: .window(.root)) } }))) @@ -2184,7 +2195,7 @@ private final class StickerPackContainer: ASDisplayNode { transition.updateFrame(node: self.backgroundNode, frame: backgroundFrame) if let previewIconFile = self.previewIconFile, let mainPreviewIcon = self.mainPreviewIcon { - let iconFitSize = CGSize(width: 90.0, height: 90.0) + let iconFitSize = CGSize(width: 120.0, height: 120.0) let iconSize = mainPreviewIcon.update( transition: .immediate, component: AnyComponent(EmojiStatusComponent( diff --git a/submodules/StickerResources/Sources/StickerResources.swift b/submodules/StickerResources/Sources/StickerResources.swift index b2ed2535d7..572611db40 100644 --- a/submodules/StickerResources/Sources/StickerResources.swift +++ b/submodules/StickerResources/Sources/StickerResources.swift @@ -8,6 +8,7 @@ import MediaResources import Tuples import ImageBlur import FastBlur +import Accelerate public func imageFromAJpeg(data: Data) -> (UIImage, UIImage)? { if let (colorData, alphaData) = data.withUnsafeBytes({ bytes -> (Data, Data)? in @@ -660,3 +661,100 @@ public func preloadedStickerPackThumbnail(account: Account, info: StickerPackCol return .single(true) } + +public func getAverageColor(image: UIImage) -> UIColor? { + let blurredWidth = 16 + let blurredHeight = 16 + let blurredBytesPerRow = blurredWidth * 4 + guard let context = DrawingContext(size: CGSize(width: CGFloat(blurredWidth), height: CGFloat(blurredHeight)), scale: 1.0, opaque: true, bytesPerRow: blurredBytesPerRow) else { + return nil + } + + let size = CGSize(width: CGFloat(blurredWidth), height: CGFloat(blurredHeight)) + + if let cgImage = image.cgImage { + context.withFlippedContext { c in + c.setFillColor(UIColor.white.cgColor) + c.fill(CGRect(origin: CGPoint(), size: size)) + c.draw(cgImage, in: CGRect(origin: CGPoint(x: -size.width / 2.0, y: -size.height / 2.0), size: CGSize(width: size.width * 1.8, height: size.height * 1.8))) + } + } + + var destinationBuffer = vImage_Buffer() + destinationBuffer.width = UInt(blurredWidth) + destinationBuffer.height = UInt(blurredHeight) + destinationBuffer.data = context.bytes + destinationBuffer.rowBytes = context.bytesPerRow + + vImageBoxConvolve_ARGB8888(&destinationBuffer, + &destinationBuffer, + nil, + 0, 0, + UInt32(15), + UInt32(15), + nil, + vImage_Flags(kvImageTruncateKernel)) + + let divisor: Int32 = 0x1000 + + let rwgt: CGFloat = 0.3086 + let gwgt: CGFloat = 0.6094 + let bwgt: CGFloat = 0.0820 + + let adjustSaturation: CGFloat = 1.7 + + let a = (1.0 - adjustSaturation) * rwgt + adjustSaturation + let b = (1.0 - adjustSaturation) * rwgt + let c = (1.0 - adjustSaturation) * rwgt + let d = (1.0 - adjustSaturation) * gwgt + let e = (1.0 - adjustSaturation) * gwgt + adjustSaturation + let f = (1.0 - adjustSaturation) * gwgt + let g = (1.0 - adjustSaturation) * bwgt + let h = (1.0 - adjustSaturation) * bwgt + let i = (1.0 - adjustSaturation) * bwgt + adjustSaturation + + let satMatrix: [CGFloat] = [ + a, b, c, 0, + d, e, f, 0, + g, h, i, 0, + 0, 0, 0, 1 + ] + + var matrix: [Int16] = satMatrix.map { value in + return Int16(value * CGFloat(divisor)) + } + + vImageMatrixMultiply_ARGB8888(&destinationBuffer, &destinationBuffer, &matrix, divisor, nil, nil, vImage_Flags(kvImageDoNotTile)) + + context.withFlippedContext { c in + c.setFillColor(UIColor.white.withMultipliedAlpha(0.1).cgColor) + c.fill(CGRect(origin: CGPoint(), size: size)) + } + + var sumR: UInt64 = 0 + var sumG: UInt64 = 0 + var sumB: UInt64 = 0 + var sumA: UInt64 = 0 + + for y in 0 ..< blurredHeight { + let row = context.bytes.assumingMemoryBound(to: UInt8.self).advanced(by: y * blurredBytesPerRow) + for x in 0 ..< blurredWidth { + let pixel = row.advanced(by: x * 4) + sumB += UInt64(pixel.advanced(by: 0).pointee) + sumG += UInt64(pixel.advanced(by: 1).pointee) + sumR += UInt64(pixel.advanced(by: 2).pointee) + sumA += UInt64(pixel.advanced(by: 3).pointee) + } + } + sumR /= UInt64(blurredWidth * blurredHeight) + sumG /= UInt64(blurredWidth * blurredHeight) + sumB /= UInt64(blurredWidth * blurredHeight) + sumA /= UInt64(blurredWidth * blurredHeight) + sumA = 255 + + var color = UIColor(red: CGFloat(sumR) / 255.0, green: CGFloat(sumG) / 255.0, blue: CGFloat(sumB) / 255.0, alpha: CGFloat(sumA) / 255.0) + if color.lightness > 0.8 { + color = color.withMultipliedBrightnessBy(0.8) + } + return color +} diff --git a/submodules/TelegramApi/Sources/Api0.swift b/submodules/TelegramApi/Sources/Api0.swift index b18395f5ec..39e076cb80 100644 --- a/submodules/TelegramApi/Sources/Api0.swift +++ b/submodules/TelegramApi/Sources/Api0.swift @@ -267,9 +267,10 @@ fileprivate let parsers: [Int32 : (BufferReader) -> Any?] = { dict[-1275374751] = { return Api.EmojiLanguage.parse_emojiLanguage($0) } dict[2048790993] = { return Api.EmojiList.parse_emojiList($0) } dict[1209970170] = { return Api.EmojiList.parse_emojiListNotModified($0) } - dict[-1835310691] = { return Api.EmojiStatus.parse_emojiStatus($0) } + dict[-402717046] = { return Api.EmojiStatus.parse_emojiStatus($0) } + dict[1904500795] = { return Api.EmojiStatus.parse_emojiStatusCollectible($0) } dict[769727150] = { return Api.EmojiStatus.parse_emojiStatusEmpty($0) } - dict[-97474361] = { return Api.EmojiStatus.parse_emojiStatusUntil($0) } + dict[118758847] = { return Api.EmojiStatus.parse_inputEmojiStatusCollectible($0) } dict[-1519029347] = { return Api.EmojiURL.parse_emojiURL($0) } dict[1643173063] = { return Api.EncryptedChat.parse_encryptedChat($0) } dict[505183301] = { return Api.EncryptedChat.parse_encryptedChatDiscarded($0) } @@ -388,7 +389,7 @@ fileprivate let parsers: [Int32 : (BufferReader) -> Any?] = { dict[1710230755] = { return Api.InputInvoice.parse_inputInvoiceStars($0) } dict[-122978821] = { return Api.InputMedia.parse_inputMediaContact($0) } dict[-428884101] = { return Api.InputMedia.parse_inputMediaDice($0) } - dict[860303448] = { return Api.InputMedia.parse_inputMediaDocument($0) } + dict[1946579745] = { return Api.InputMedia.parse_inputMediaDocument($0) } dict[-78455655] = { return Api.InputMedia.parse_inputMediaDocumentExternal($0) } dict[-1771768449] = { return Api.InputMedia.parse_inputMediaEmpty($0) } dict[-750828557] = { return Api.InputMedia.parse_inputMediaGame($0) } @@ -400,7 +401,7 @@ fileprivate let parsers: [Int32 : (BufferReader) -> Any?] = { dict[-440664550] = { return Api.InputMedia.parse_inputMediaPhotoExternal($0) } dict[261416433] = { return Api.InputMedia.parse_inputMediaPoll($0) } dict[-1979852936] = { return Api.InputMedia.parse_inputMediaStory($0) } - dict[1530447553] = { return Api.InputMedia.parse_inputMediaUploadedDocument($0) } + dict[-264125395] = { return Api.InputMedia.parse_inputMediaUploadedDocument($0) } dict[505969924] = { return Api.InputMedia.parse_inputMediaUploadedPhoto($0) } dict[-1052959727] = { return Api.InputMedia.parse_inputMediaVenue($0) } dict[-1038383031] = { return Api.InputMedia.parse_inputMediaWebPage($0) } @@ -534,6 +535,7 @@ fileprivate let parsers: [Int32 : (BufferReader) -> Any?] = { dict[-1300094593] = { return Api.MediaArea.parse_inputMediaAreaVenue($0) } dict[1996756655] = { return Api.MediaArea.parse_mediaAreaChannelPost($0) } dict[-891992787] = { return Api.MediaArea.parse_mediaAreaGeoPoint($0) } + dict[1468491885] = { return Api.MediaArea.parse_mediaAreaStarGift($0) } dict[340088945] = { return Api.MediaArea.parse_mediaAreaSuggestedReaction($0) } dict[926421125] = { return Api.MediaArea.parse_mediaAreaUrl($0) } dict[-1098720356] = { return Api.MediaArea.parse_mediaAreaVenue($0) } @@ -616,7 +618,7 @@ fileprivate let parsers: [Int32 : (BufferReader) -> Any?] = { dict[1313731771] = { return Api.MessageFwdHeader.parse_messageFwdHeader($0) } dict[1882335561] = { return Api.MessageMedia.parse_messageMediaContact($0) } dict[1065280907] = { return Api.MessageMedia.parse_messageMediaDice($0) } - dict[-581497899] = { return Api.MessageMedia.parse_messageMediaDocument($0) } + dict[1838230743] = { return Api.MessageMedia.parse_messageMediaDocument($0) } dict[1038967584] = { return Api.MessageMedia.parse_messageMediaEmpty($0) } dict[-38694904] = { return Api.MessageMedia.parse_messageMediaGame($0) } dict[1457575028] = { return Api.MessageMedia.parse_messageMediaGeo($0) } @@ -909,7 +911,7 @@ fileprivate let parsers: [Int32 : (BufferReader) -> Any?] = { dict[1301522832] = { return Api.SponsoredMessage.parse_sponsoredMessage($0) } dict[1124938064] = { return Api.SponsoredMessageReportOption.parse_sponsoredMessageReportOption($0) } dict[46953416] = { return Api.StarGift.parse_starGift($0) } - dict[1779697613] = { return Api.StarGift.parse_starGiftUnique($0) } + dict[880997154] = { return Api.StarGift.parse_starGiftUnique($0) } dict[-1809377438] = { return Api.StarGiftAttribute.parse_starGiftAttributeBackdrop($0) } dict[970559507] = { return Api.StarGiftAttribute.parse_starGiftAttributeModel($0) } dict[-1070837941] = { return Api.StarGiftAttribute.parse_starGiftAttributeOriginalDetails($0) } @@ -1156,6 +1158,7 @@ fileprivate let parsers: [Int32 : (BufferReader) -> Any?] = { dict[1355547603] = { return Api.WebPageAttribute.parse_webPageAttributeStickerSet($0) } dict[781501415] = { return Api.WebPageAttribute.parse_webPageAttributeStory($0) } dict[1421174295] = { return Api.WebPageAttribute.parse_webPageAttributeTheme($0) } + dict[-814781000] = { return Api.WebPageAttribute.parse_webPageAttributeUniqueStarGift($0) } dict[211046684] = { return Api.WebViewMessageSent.parse_webViewMessageSent($0) } dict[1294139288] = { return Api.WebViewResult.parse_webViewResultUrl($0) } dict[-1389486888] = { return Api.account.AuthorizationForm.parse_authorizationForm($0) } @@ -1361,6 +1364,7 @@ fileprivate let parsers: [Int32 : (BufferReader) -> Any?] = { dict[870003448] = { return Api.messages.TranslatedText.parse_translateResult($0) } dict[1218005070] = { return Api.messages.VotesList.parse_votesList($0) } dict[-44166467] = { return Api.messages.WebPage.parse_webPage($0) } + dict[-1254192351] = { return Api.messages.WebPagePreview.parse_webPagePreview($0) } dict[1042605427] = { return Api.payments.BankCardData.parse_bankCardData($0) } dict[675942550] = { return Api.payments.CheckedGiftCode.parse_checkedGiftCode($0) } dict[-1730811363] = { return Api.payments.ConnectedStarRefBots.parse_connectedStarRefBots($0) } @@ -1383,6 +1387,7 @@ fileprivate let parsers: [Int32 : (BufferReader) -> Any?] = { dict[497778871] = { return Api.payments.StarsRevenueWithdrawalUrl.parse_starsRevenueWithdrawalUrl($0) } dict[1822222573] = { return Api.payments.StarsStatus.parse_starsStatus($0) } dict[-1261053863] = { return Api.payments.SuggestedStarRefBots.parse_suggestedStarRefBots($0) } + dict[-895289845] = { return Api.payments.UniqueStarGift.parse_uniqueStarGift($0) } dict[1801827607] = { return Api.payments.UserStarGifts.parse_userStarGifts($0) } dict[-784000893] = { return Api.payments.ValidatedRequestedInfo.parse_validatedRequestedInfo($0) } dict[541839704] = { return Api.phone.ExportedGroupCallInvite.parse_exportedGroupCallInvite($0) } @@ -2440,6 +2445,8 @@ public extension Api { _1.serialize(buffer, boxed) case let _1 as Api.messages.WebPage: _1.serialize(buffer, boxed) + case let _1 as Api.messages.WebPagePreview: + _1.serialize(buffer, boxed) case let _1 as Api.payments.BankCardData: _1.serialize(buffer, boxed) case let _1 as Api.payments.CheckedGiftCode: @@ -2472,6 +2479,8 @@ public extension Api { _1.serialize(buffer, boxed) case let _1 as Api.payments.SuggestedStarRefBots: _1.serialize(buffer, boxed) + case let _1 as Api.payments.UniqueStarGift: + _1.serialize(buffer, boxed) case let _1 as Api.payments.UserStarGifts: _1.serialize(buffer, boxed) case let _1 as Api.payments.ValidatedRequestedInfo: diff --git a/submodules/TelegramApi/Sources/Api10.swift b/submodules/TelegramApi/Sources/Api10.swift index ecc50324a1..b5828fb64d 100644 --- a/submodules/TelegramApi/Sources/Api10.swift +++ b/submodules/TelegramApi/Sources/Api10.swift @@ -428,7 +428,7 @@ public extension Api { indirect enum InputMedia: TypeConstructorDescription { case inputMediaContact(phoneNumber: String, firstName: String, lastName: String, vcard: String) case inputMediaDice(emoticon: String) - case inputMediaDocument(flags: Int32, id: Api.InputDocument, ttlSeconds: Int32?, query: String?) + case inputMediaDocument(flags: Int32, id: Api.InputDocument, videoCover: Api.InputPhoto?, ttlSeconds: Int32?, query: String?) case inputMediaDocumentExternal(flags: Int32, url: String, ttlSeconds: Int32?) case inputMediaEmpty case inputMediaGame(id: Api.InputGame) @@ -440,7 +440,7 @@ public extension Api { case inputMediaPhotoExternal(flags: Int32, url: String, ttlSeconds: Int32?) case inputMediaPoll(flags: Int32, poll: Api.Poll, correctAnswers: [Buffer]?, solution: String?, solutionEntities: [Api.MessageEntity]?) case inputMediaStory(peer: Api.InputPeer, id: Int32) - case inputMediaUploadedDocument(flags: Int32, file: Api.InputFile, thumb: Api.InputFile?, mimeType: String, attributes: [Api.DocumentAttribute], stickers: [Api.InputDocument]?, ttlSeconds: Int32?) + case inputMediaUploadedDocument(flags: Int32, file: Api.InputFile, thumb: Api.InputFile?, mimeType: String, attributes: [Api.DocumentAttribute], stickers: [Api.InputDocument]?, videoCover: Api.InputPhoto?, ttlSeconds: Int32?) case inputMediaUploadedPhoto(flags: Int32, file: Api.InputFile, stickers: [Api.InputDocument]?, ttlSeconds: Int32?) case inputMediaVenue(geoPoint: Api.InputGeoPoint, title: String, address: String, provider: String, venueId: String, venueType: String) case inputMediaWebPage(flags: Int32, url: String) @@ -462,12 +462,13 @@ public extension Api { } serializeString(emoticon, buffer: buffer, boxed: false) break - case .inputMediaDocument(let flags, let id, let ttlSeconds, let query): + case .inputMediaDocument(let flags, let id, let videoCover, let ttlSeconds, let query): if boxed { - buffer.appendInt32(860303448) + buffer.appendInt32(1946579745) } serializeInt32(flags, buffer: buffer, boxed: false) id.serialize(buffer, true) + if Int(flags) & Int(1 << 3) != 0 {videoCover!.serialize(buffer, true)} if Int(flags) & Int(1 << 0) != 0 {serializeInt32(ttlSeconds!, buffer: buffer, boxed: false)} if Int(flags) & Int(1 << 1) != 0 {serializeString(query!, buffer: buffer, boxed: false)} break @@ -576,9 +577,9 @@ public extension Api { peer.serialize(buffer, true) serializeInt32(id, buffer: buffer, boxed: false) break - case .inputMediaUploadedDocument(let flags, let file, let thumb, let mimeType, let attributes, let stickers, let ttlSeconds): + case .inputMediaUploadedDocument(let flags, let file, let thumb, let mimeType, let attributes, let stickers, let videoCover, let ttlSeconds): if boxed { - buffer.appendInt32(1530447553) + buffer.appendInt32(-264125395) } serializeInt32(flags, buffer: buffer, boxed: false) file.serialize(buffer, true) @@ -594,6 +595,7 @@ public extension Api { for item in stickers! { item.serialize(buffer, true) }} + if Int(flags) & Int(1 << 6) != 0 {videoCover!.serialize(buffer, true)} if Int(flags) & Int(1 << 1) != 0 {serializeInt32(ttlSeconds!, buffer: buffer, boxed: false)} break case .inputMediaUploadedPhoto(let flags, let file, let stickers, let ttlSeconds): @@ -636,8 +638,8 @@ public extension Api { return ("inputMediaContact", [("phoneNumber", phoneNumber as Any), ("firstName", firstName as Any), ("lastName", lastName as Any), ("vcard", vcard as Any)]) case .inputMediaDice(let emoticon): return ("inputMediaDice", [("emoticon", emoticon as Any)]) - case .inputMediaDocument(let flags, let id, let ttlSeconds, let query): - return ("inputMediaDocument", [("flags", flags as Any), ("id", id as Any), ("ttlSeconds", ttlSeconds as Any), ("query", query as Any)]) + case .inputMediaDocument(let flags, let id, let videoCover, let ttlSeconds, let query): + return ("inputMediaDocument", [("flags", flags as Any), ("id", id as Any), ("videoCover", videoCover as Any), ("ttlSeconds", ttlSeconds as Any), ("query", query as Any)]) case .inputMediaDocumentExternal(let flags, let url, let ttlSeconds): return ("inputMediaDocumentExternal", [("flags", flags as Any), ("url", url as Any), ("ttlSeconds", ttlSeconds as Any)]) case .inputMediaEmpty: @@ -660,8 +662,8 @@ public extension Api { return ("inputMediaPoll", [("flags", flags as Any), ("poll", poll as Any), ("correctAnswers", correctAnswers as Any), ("solution", solution as Any), ("solutionEntities", solutionEntities as Any)]) case .inputMediaStory(let peer, let id): return ("inputMediaStory", [("peer", peer as Any), ("id", id as Any)]) - case .inputMediaUploadedDocument(let flags, let file, let thumb, let mimeType, let attributes, let stickers, let ttlSeconds): - return ("inputMediaUploadedDocument", [("flags", flags as Any), ("file", file as Any), ("thumb", thumb as Any), ("mimeType", mimeType as Any), ("attributes", attributes as Any), ("stickers", stickers as Any), ("ttlSeconds", ttlSeconds as Any)]) + case .inputMediaUploadedDocument(let flags, let file, let thumb, let mimeType, let attributes, let stickers, let videoCover, let ttlSeconds): + return ("inputMediaUploadedDocument", [("flags", flags as Any), ("file", file as Any), ("thumb", thumb as Any), ("mimeType", mimeType as Any), ("attributes", attributes as Any), ("stickers", stickers as Any), ("videoCover", videoCover as Any), ("ttlSeconds", ttlSeconds as Any)]) case .inputMediaUploadedPhoto(let flags, let file, let stickers, let ttlSeconds): return ("inputMediaUploadedPhoto", [("flags", flags as Any), ("file", file as Any), ("stickers", stickers as Any), ("ttlSeconds", ttlSeconds as Any)]) case .inputMediaVenue(let geoPoint, let title, let address, let provider, let venueId, let venueType): @@ -709,16 +711,21 @@ public extension Api { if let signature = reader.readInt32() { _2 = Api.parse(reader, signature: signature) as? Api.InputDocument } - var _3: Int32? - if Int(_1!) & Int(1 << 0) != 0 {_3 = reader.readInt32() } - var _4: String? - if Int(_1!) & Int(1 << 1) != 0 {_4 = parseString(reader) } + var _3: Api.InputPhoto? + if Int(_1!) & Int(1 << 3) != 0 {if let signature = reader.readInt32() { + _3 = Api.parse(reader, signature: signature) as? Api.InputPhoto + } } + var _4: Int32? + if Int(_1!) & Int(1 << 0) != 0 {_4 = reader.readInt32() } + var _5: String? + if Int(_1!) & Int(1 << 1) != 0 {_5 = parseString(reader) } let _c1 = _1 != nil let _c2 = _2 != nil - let _c3 = (Int(_1!) & Int(1 << 0) == 0) || _3 != nil - let _c4 = (Int(_1!) & Int(1 << 1) == 0) || _4 != nil - if _c1 && _c2 && _c3 && _c4 { - return Api.InputMedia.inputMediaDocument(flags: _1!, id: _2!, ttlSeconds: _3, query: _4) + let _c3 = (Int(_1!) & Int(1 << 3) == 0) || _3 != nil + let _c4 = (Int(_1!) & Int(1 << 0) == 0) || _4 != nil + let _c5 = (Int(_1!) & Int(1 << 1) == 0) || _5 != nil + if _c1 && _c2 && _c3 && _c4 && _c5 { + return Api.InputMedia.inputMediaDocument(flags: _1!, id: _2!, videoCover: _3, ttlSeconds: _4, query: _5) } else { return nil @@ -965,17 +972,22 @@ public extension Api { if Int(_1!) & Int(1 << 0) != 0 {if let _ = reader.readInt32() { _6 = Api.parseVector(reader, elementSignature: 0, elementType: Api.InputDocument.self) } } - var _7: Int32? - if Int(_1!) & Int(1 << 1) != 0 {_7 = reader.readInt32() } + var _7: Api.InputPhoto? + if Int(_1!) & Int(1 << 6) != 0 {if let signature = reader.readInt32() { + _7 = Api.parse(reader, signature: signature) as? Api.InputPhoto + } } + var _8: Int32? + if Int(_1!) & Int(1 << 1) != 0 {_8 = reader.readInt32() } let _c1 = _1 != nil let _c2 = _2 != nil let _c3 = (Int(_1!) & Int(1 << 2) == 0) || _3 != nil let _c4 = _4 != nil let _c5 = _5 != nil let _c6 = (Int(_1!) & Int(1 << 0) == 0) || _6 != nil - let _c7 = (Int(_1!) & Int(1 << 1) == 0) || _7 != nil - if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 && _c7 { - return Api.InputMedia.inputMediaUploadedDocument(flags: _1!, file: _2!, thumb: _3, mimeType: _4!, attributes: _5!, stickers: _6, ttlSeconds: _7) + let _c7 = (Int(_1!) & Int(1 << 6) == 0) || _7 != nil + let _c8 = (Int(_1!) & Int(1 << 1) == 0) || _8 != nil + if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 && _c7 && _c8 { + return Api.InputMedia.inputMediaUploadedDocument(flags: _1!, file: _2!, thumb: _3, mimeType: _4!, attributes: _5!, stickers: _6, videoCover: _7, ttlSeconds: _8) } else { return nil diff --git a/submodules/TelegramApi/Sources/Api14.swift b/submodules/TelegramApi/Sources/Api14.swift index aaba39c1cd..b1d5202b08 100644 --- a/submodules/TelegramApi/Sources/Api14.swift +++ b/submodules/TelegramApi/Sources/Api14.swift @@ -868,6 +868,7 @@ public extension Api { case inputMediaAreaVenue(coordinates: Api.MediaAreaCoordinates, queryId: Int64, resultId: String) case mediaAreaChannelPost(coordinates: Api.MediaAreaCoordinates, channelId: Int64, msgId: Int32) case mediaAreaGeoPoint(flags: Int32, coordinates: Api.MediaAreaCoordinates, geo: Api.GeoPoint, address: Api.GeoPointAddress?) + case mediaAreaStarGift(coordinates: Api.MediaAreaCoordinates, slug: String) case mediaAreaSuggestedReaction(flags: Int32, coordinates: Api.MediaAreaCoordinates, reaction: Api.Reaction) case mediaAreaUrl(coordinates: Api.MediaAreaCoordinates, url: String) case mediaAreaVenue(coordinates: Api.MediaAreaCoordinates, geo: Api.GeoPoint, title: String, address: String, provider: String, venueId: String, venueType: String) @@ -908,6 +909,13 @@ public extension Api { geo.serialize(buffer, true) if Int(flags) & Int(1 << 0) != 0 {address!.serialize(buffer, true)} break + case .mediaAreaStarGift(let coordinates, let slug): + if boxed { + buffer.appendInt32(1468491885) + } + coordinates.serialize(buffer, true) + serializeString(slug, buffer: buffer, boxed: false) + break case .mediaAreaSuggestedReaction(let flags, let coordinates, let reaction): if boxed { buffer.appendInt32(340088945) @@ -957,6 +965,8 @@ public extension Api { return ("mediaAreaChannelPost", [("coordinates", coordinates as Any), ("channelId", channelId as Any), ("msgId", msgId as Any)]) case .mediaAreaGeoPoint(let flags, let coordinates, let geo, let address): return ("mediaAreaGeoPoint", [("flags", flags as Any), ("coordinates", coordinates as Any), ("geo", geo as Any), ("address", address as Any)]) + case .mediaAreaStarGift(let coordinates, let slug): + return ("mediaAreaStarGift", [("coordinates", coordinates as Any), ("slug", slug as Any)]) case .mediaAreaSuggestedReaction(let flags, let coordinates, let reaction): return ("mediaAreaSuggestedReaction", [("flags", flags as Any), ("coordinates", coordinates as Any), ("reaction", reaction as Any)]) case .mediaAreaUrl(let coordinates, let url): @@ -1053,6 +1063,22 @@ public extension Api { return nil } } + public static func parse_mediaAreaStarGift(_ reader: BufferReader) -> MediaArea? { + var _1: Api.MediaAreaCoordinates? + if let signature = reader.readInt32() { + _1 = Api.parse(reader, signature: signature) as? Api.MediaAreaCoordinates + } + var _2: String? + _2 = parseString(reader) + let _c1 = _1 != nil + let _c2 = _2 != nil + if _c1 && _c2 { + return Api.MediaArea.mediaAreaStarGift(coordinates: _1!, slug: _2!) + } + else { + return nil + } + } public static func parse_mediaAreaSuggestedReaction(_ reader: BufferReader) -> MediaArea? { var _1: Int32? _1 = reader.readInt32() diff --git a/submodules/TelegramApi/Sources/Api16.swift b/submodules/TelegramApi/Sources/Api16.swift index 77e5198937..22fdd7dbed 100644 --- a/submodules/TelegramApi/Sources/Api16.swift +++ b/submodules/TelegramApi/Sources/Api16.swift @@ -710,7 +710,7 @@ public extension Api { indirect enum MessageMedia: TypeConstructorDescription { case messageMediaContact(phoneNumber: String, firstName: String, lastName: String, vcard: String, userId: Int64) case messageMediaDice(value: Int32, emoticon: String) - case messageMediaDocument(flags: Int32, document: Api.Document?, altDocuments: [Api.Document]?, ttlSeconds: Int32?) + case messageMediaDocument(flags: Int32, document: Api.Document?, altDocuments: [Api.Document]?, coverPhoto: Api.Photo?, ttlSeconds: Int32?) case messageMediaEmpty case messageMediaGame(game: Api.Game) case messageMediaGeo(geo: Api.GeoPoint) @@ -745,9 +745,9 @@ public extension Api { serializeInt32(value, buffer: buffer, boxed: false) serializeString(emoticon, buffer: buffer, boxed: false) break - case .messageMediaDocument(let flags, let document, let altDocuments, let ttlSeconds): + case .messageMediaDocument(let flags, let document, let altDocuments, let coverPhoto, let ttlSeconds): if boxed { - buffer.appendInt32(-581497899) + buffer.appendInt32(1838230743) } serializeInt32(flags, buffer: buffer, boxed: false) if Int(flags) & Int(1 << 0) != 0 {document!.serialize(buffer, true)} @@ -756,6 +756,7 @@ public extension Api { for item in altDocuments! { item.serialize(buffer, true) }} + if Int(flags) & Int(1 << 9) != 0 {coverPhoto!.serialize(buffer, true)} if Int(flags) & Int(1 << 2) != 0 {serializeInt32(ttlSeconds!, buffer: buffer, boxed: false)} break case .messageMediaEmpty: @@ -909,8 +910,8 @@ public extension Api { return ("messageMediaContact", [("phoneNumber", phoneNumber as Any), ("firstName", firstName as Any), ("lastName", lastName as Any), ("vcard", vcard as Any), ("userId", userId as Any)]) case .messageMediaDice(let value, let emoticon): return ("messageMediaDice", [("value", value as Any), ("emoticon", emoticon as Any)]) - case .messageMediaDocument(let flags, let document, let altDocuments, let ttlSeconds): - return ("messageMediaDocument", [("flags", flags as Any), ("document", document as Any), ("altDocuments", altDocuments as Any), ("ttlSeconds", ttlSeconds as Any)]) + case .messageMediaDocument(let flags, let document, let altDocuments, let coverPhoto, let ttlSeconds): + return ("messageMediaDocument", [("flags", flags as Any), ("document", document as Any), ("altDocuments", altDocuments as Any), ("coverPhoto", coverPhoto as Any), ("ttlSeconds", ttlSeconds as Any)]) case .messageMediaEmpty: return ("messageMediaEmpty", []) case .messageMediaGame(let game): @@ -990,14 +991,19 @@ public extension Api { if Int(_1!) & Int(1 << 5) != 0 {if let _ = reader.readInt32() { _3 = Api.parseVector(reader, elementSignature: 0, elementType: Api.Document.self) } } - var _4: Int32? - if Int(_1!) & Int(1 << 2) != 0 {_4 = reader.readInt32() } + var _4: Api.Photo? + if Int(_1!) & Int(1 << 9) != 0 {if let signature = reader.readInt32() { + _4 = Api.parse(reader, signature: signature) as? Api.Photo + } } + var _5: Int32? + if Int(_1!) & Int(1 << 2) != 0 {_5 = reader.readInt32() } let _c1 = _1 != nil let _c2 = (Int(_1!) & Int(1 << 0) == 0) || _2 != nil let _c3 = (Int(_1!) & Int(1 << 5) == 0) || _3 != nil - let _c4 = (Int(_1!) & Int(1 << 2) == 0) || _4 != nil - if _c1 && _c2 && _c3 && _c4 { - return Api.MessageMedia.messageMediaDocument(flags: _1!, document: _2, altDocuments: _3, ttlSeconds: _4) + let _c4 = (Int(_1!) & Int(1 << 9) == 0) || _4 != nil + let _c5 = (Int(_1!) & Int(1 << 2) == 0) || _5 != nil + if _c1 && _c2 && _c3 && _c4 && _c5 { + return Api.MessageMedia.messageMediaDocument(flags: _1!, document: _2, altDocuments: _3, coverPhoto: _4, ttlSeconds: _5) } else { return nil diff --git a/submodules/TelegramApi/Sources/Api24.swift b/submodules/TelegramApi/Sources/Api24.swift index 51a39f1c74..6a393c61c1 100644 --- a/submodules/TelegramApi/Sources/Api24.swift +++ b/submodules/TelegramApi/Sources/Api24.swift @@ -575,7 +575,7 @@ public extension Api { public extension Api { enum StarGift: TypeConstructorDescription { case starGift(flags: Int32, id: Int64, sticker: Api.Document, stars: Int64, availabilityRemains: Int32?, availabilityTotal: Int32?, convertStars: Int64, firstSaleDate: Int32?, lastSaleDate: Int32?, upgradeStars: Int64?) - case starGiftUnique(id: Int64, title: String, num: Int32, ownerId: Int64, attributes: [Api.StarGiftAttribute], availabilityIssued: Int32, availabilityTotal: Int32) + case starGiftUnique(flags: Int32, id: Int64, title: String, slug: String, num: Int32, ownerId: Int64?, ownerName: String?, attributes: [Api.StarGiftAttribute], availabilityIssued: Int32, availabilityTotal: Int32) public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { switch self { @@ -594,14 +594,17 @@ public extension Api { if Int(flags) & Int(1 << 1) != 0 {serializeInt32(lastSaleDate!, buffer: buffer, boxed: false)} if Int(flags) & Int(1 << 3) != 0 {serializeInt64(upgradeStars!, buffer: buffer, boxed: false)} break - case .starGiftUnique(let id, let title, let num, let ownerId, let attributes, let availabilityIssued, let availabilityTotal): + case .starGiftUnique(let flags, let id, let title, let slug, let num, let ownerId, let ownerName, let attributes, let availabilityIssued, let availabilityTotal): if boxed { - buffer.appendInt32(1779697613) + buffer.appendInt32(880997154) } + serializeInt32(flags, buffer: buffer, boxed: false) serializeInt64(id, buffer: buffer, boxed: false) serializeString(title, buffer: buffer, boxed: false) + serializeString(slug, buffer: buffer, boxed: false) serializeInt32(num, buffer: buffer, boxed: false) - serializeInt64(ownerId, buffer: buffer, boxed: false) + if Int(flags) & Int(1 << 0) != 0 {serializeInt64(ownerId!, buffer: buffer, boxed: false)} + if Int(flags) & Int(1 << 1) != 0 {serializeString(ownerName!, buffer: buffer, boxed: false)} buffer.appendInt32(481674261) buffer.appendInt32(Int32(attributes.count)) for item in attributes { @@ -617,8 +620,8 @@ public extension Api { switch self { case .starGift(let flags, let id, let sticker, let stars, let availabilityRemains, let availabilityTotal, let convertStars, let firstSaleDate, let lastSaleDate, let upgradeStars): return ("starGift", [("flags", flags as Any), ("id", id as Any), ("sticker", sticker as Any), ("stars", stars as Any), ("availabilityRemains", availabilityRemains as Any), ("availabilityTotal", availabilityTotal as Any), ("convertStars", convertStars as Any), ("firstSaleDate", firstSaleDate as Any), ("lastSaleDate", lastSaleDate as Any), ("upgradeStars", upgradeStars as Any)]) - case .starGiftUnique(let id, let title, let num, let ownerId, let attributes, let availabilityIssued, let availabilityTotal): - return ("starGiftUnique", [("id", id as Any), ("title", title as Any), ("num", num as Any), ("ownerId", ownerId as Any), ("attributes", attributes as Any), ("availabilityIssued", availabilityIssued as Any), ("availabilityTotal", availabilityTotal as Any)]) + case .starGiftUnique(let flags, let id, let title, let slug, let num, let ownerId, let ownerName, let attributes, let availabilityIssued, let availabilityTotal): + return ("starGiftUnique", [("flags", flags as Any), ("id", id as Any), ("title", title as Any), ("slug", slug as Any), ("num", num as Any), ("ownerId", ownerId as Any), ("ownerName", ownerName as Any), ("attributes", attributes as Any), ("availabilityIssued", availabilityIssued as Any), ("availabilityTotal", availabilityTotal as Any)]) } } @@ -663,31 +666,40 @@ public extension Api { } } public static func parse_starGiftUnique(_ reader: BufferReader) -> StarGift? { - var _1: Int64? - _1 = reader.readInt64() - var _2: String? - _2 = parseString(reader) - var _3: Int32? - _3 = reader.readInt32() - var _4: Int64? - _4 = reader.readInt64() - var _5: [Api.StarGiftAttribute]? + var _1: Int32? + _1 = reader.readInt32() + var _2: Int64? + _2 = reader.readInt64() + var _3: String? + _3 = parseString(reader) + var _4: String? + _4 = parseString(reader) + var _5: Int32? + _5 = reader.readInt32() + var _6: Int64? + if Int(_1!) & Int(1 << 0) != 0 {_6 = reader.readInt64() } + var _7: String? + if Int(_1!) & Int(1 << 1) != 0 {_7 = parseString(reader) } + var _8: [Api.StarGiftAttribute]? if let _ = reader.readInt32() { - _5 = Api.parseVector(reader, elementSignature: 0, elementType: Api.StarGiftAttribute.self) + _8 = Api.parseVector(reader, elementSignature: 0, elementType: Api.StarGiftAttribute.self) } - var _6: Int32? - _6 = reader.readInt32() - var _7: Int32? - _7 = reader.readInt32() + var _9: Int32? + _9 = reader.readInt32() + var _10: Int32? + _10 = reader.readInt32() let _c1 = _1 != nil let _c2 = _2 != nil let _c3 = _3 != nil let _c4 = _4 != nil let _c5 = _5 != nil - let _c6 = _6 != nil - let _c7 = _7 != nil - if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 && _c7 { - return Api.StarGift.starGiftUnique(id: _1!, title: _2!, num: _3!, ownerId: _4!, attributes: _5!, availabilityIssued: _6!, availabilityTotal: _7!) + let _c6 = (Int(_1!) & Int(1 << 0) == 0) || _6 != nil + let _c7 = (Int(_1!) & Int(1 << 1) == 0) || _7 != nil + let _c8 = _8 != nil + let _c9 = _9 != nil + let _c10 = _10 != nil + if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 && _c7 && _c8 && _c9 && _c10 { + return Api.StarGift.starGiftUnique(flags: _1!, id: _2!, title: _3!, slug: _4!, num: _5!, ownerId: _6, ownerName: _7, attributes: _8!, availabilityIssued: _9!, availabilityTotal: _10!) } else { return nil diff --git a/submodules/TelegramApi/Sources/Api28.swift b/submodules/TelegramApi/Sources/Api28.swift index 8034ebaa39..3f9b6d17be 100644 --- a/submodules/TelegramApi/Sources/Api28.swift +++ b/submodules/TelegramApi/Sources/Api28.swift @@ -207,6 +207,7 @@ public extension Api { case webPageAttributeStickerSet(flags: Int32, stickers: [Api.Document]) case webPageAttributeStory(flags: Int32, peer: Api.Peer, id: Int32, story: Api.StoryItem?) case webPageAttributeTheme(flags: Int32, documents: [Api.Document]?, settings: Api.ThemeSettings?) + case webPageAttributeUniqueStarGift(gift: Api.StarGift) public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { switch self { @@ -242,6 +243,12 @@ public extension Api { }} if Int(flags) & Int(1 << 1) != 0 {settings!.serialize(buffer, true)} break + case .webPageAttributeUniqueStarGift(let gift): + if boxed { + buffer.appendInt32(-814781000) + } + gift.serialize(buffer, true) + break } } @@ -253,6 +260,8 @@ public extension Api { return ("webPageAttributeStory", [("flags", flags as Any), ("peer", peer as Any), ("id", id as Any), ("story", story as Any)]) case .webPageAttributeTheme(let flags, let documents, let settings): return ("webPageAttributeTheme", [("flags", flags as Any), ("documents", documents as Any), ("settings", settings as Any)]) + case .webPageAttributeUniqueStarGift(let gift): + return ("webPageAttributeUniqueStarGift", [("gift", gift as Any)]) } } @@ -317,6 +326,19 @@ public extension Api { return nil } } + public static func parse_webPageAttributeUniqueStarGift(_ reader: BufferReader) -> WebPageAttribute? { + var _1: Api.StarGift? + if let signature = reader.readInt32() { + _1 = Api.parse(reader, signature: signature) as? Api.StarGift + } + let _c1 = _1 != nil + if _c1 { + return Api.WebPageAttribute.webPageAttributeUniqueStarGift(gift: _1!) + } + else { + return nil + } + } } } diff --git a/submodules/TelegramApi/Sources/Api34.swift b/submodules/TelegramApi/Sources/Api34.swift index 7906a60dfa..b82d6421c4 100644 --- a/submodules/TelegramApi/Sources/Api34.swift +++ b/submodules/TelegramApi/Sources/Api34.swift @@ -744,6 +744,54 @@ public extension Api.messages { } } +public extension Api.messages { + indirect enum WebPagePreview: TypeConstructorDescription { + case webPagePreview(media: Api.MessageMedia, users: [Api.User]) + + public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { + switch self { + case .webPagePreview(let media, let users): + if boxed { + buffer.appendInt32(-1254192351) + } + media.serialize(buffer, true) + buffer.appendInt32(481674261) + buffer.appendInt32(Int32(users.count)) + for item in users { + item.serialize(buffer, true) + } + break + } + } + + public func descriptionFields() -> (String, [(String, Any)]) { + switch self { + case .webPagePreview(let media, let users): + return ("webPagePreview", [("media", media as Any), ("users", users as Any)]) + } + } + + public static func parse_webPagePreview(_ reader: BufferReader) -> WebPagePreview? { + var _1: Api.MessageMedia? + if let signature = reader.readInt32() { + _1 = Api.parse(reader, signature: signature) as? Api.MessageMedia + } + var _2: [Api.User]? + if let _ = reader.readInt32() { + _2 = Api.parseVector(reader, elementSignature: 0, elementType: Api.User.self) + } + let _c1 = _1 != nil + let _c2 = _2 != nil + if _c1 && _c2 { + return Api.messages.WebPagePreview.webPagePreview(media: _1!, users: _2!) + } + else { + return nil + } + } + + } +} public extension Api.payments { enum BankCardData: TypeConstructorDescription { case bankCardData(title: String, openUrls: [Api.BankCardOpenUrl]) @@ -1538,45 +1586,3 @@ public extension Api.payments { } } -public extension Api.payments { - enum StarGiftUpgradePreview: TypeConstructorDescription { - case starGiftUpgradePreview(sampleAttributes: [Api.StarGiftAttribute]) - - public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { - switch self { - case .starGiftUpgradePreview(let sampleAttributes): - if boxed { - buffer.appendInt32(377215243) - } - buffer.appendInt32(481674261) - buffer.appendInt32(Int32(sampleAttributes.count)) - for item in sampleAttributes { - item.serialize(buffer, true) - } - break - } - } - - public func descriptionFields() -> (String, [(String, Any)]) { - switch self { - case .starGiftUpgradePreview(let sampleAttributes): - return ("starGiftUpgradePreview", [("sampleAttributes", sampleAttributes as Any)]) - } - } - - public static func parse_starGiftUpgradePreview(_ reader: BufferReader) -> StarGiftUpgradePreview? { - var _1: [Api.StarGiftAttribute]? - if let _ = reader.readInt32() { - _1 = Api.parseVector(reader, elementSignature: 0, elementType: Api.StarGiftAttribute.self) - } - let _c1 = _1 != nil - if _c1 { - return Api.payments.StarGiftUpgradePreview.starGiftUpgradePreview(sampleAttributes: _1!) - } - else { - return nil - } - } - - } -} diff --git a/submodules/TelegramApi/Sources/Api35.swift b/submodules/TelegramApi/Sources/Api35.swift index a37ca076a3..c6df005355 100644 --- a/submodules/TelegramApi/Sources/Api35.swift +++ b/submodules/TelegramApi/Sources/Api35.swift @@ -1,3 +1,45 @@ +public extension Api.payments { + enum StarGiftUpgradePreview: TypeConstructorDescription { + case starGiftUpgradePreview(sampleAttributes: [Api.StarGiftAttribute]) + + public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { + switch self { + case .starGiftUpgradePreview(let sampleAttributes): + if boxed { + buffer.appendInt32(377215243) + } + buffer.appendInt32(481674261) + buffer.appendInt32(Int32(sampleAttributes.count)) + for item in sampleAttributes { + item.serialize(buffer, true) + } + break + } + } + + public func descriptionFields() -> (String, [(String, Any)]) { + switch self { + case .starGiftUpgradePreview(let sampleAttributes): + return ("starGiftUpgradePreview", [("sampleAttributes", sampleAttributes as Any)]) + } + } + + public static func parse_starGiftUpgradePreview(_ reader: BufferReader) -> StarGiftUpgradePreview? { + var _1: [Api.StarGiftAttribute]? + if let _ = reader.readInt32() { + _1 = Api.parseVector(reader, elementSignature: 0, elementType: Api.StarGiftAttribute.self) + } + let _c1 = _1 != nil + if _c1 { + return Api.payments.StarGiftUpgradePreview.starGiftUpgradePreview(sampleAttributes: _1!) + } + else { + return nil + } + } + + } +} public extension Api.payments { enum StarGifts: TypeConstructorDescription { case starGifts(hash: Int32, gifts: [Api.StarGift]) @@ -334,6 +376,54 @@ public extension Api.payments { } } +public extension Api.payments { + enum UniqueStarGift: TypeConstructorDescription { + case uniqueStarGift(gift: Api.StarGift, users: [Api.User]) + + public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { + switch self { + case .uniqueStarGift(let gift, let users): + if boxed { + buffer.appendInt32(-895289845) + } + gift.serialize(buffer, true) + buffer.appendInt32(481674261) + buffer.appendInt32(Int32(users.count)) + for item in users { + item.serialize(buffer, true) + } + break + } + } + + public func descriptionFields() -> (String, [(String, Any)]) { + switch self { + case .uniqueStarGift(let gift, let users): + return ("uniqueStarGift", [("gift", gift as Any), ("users", users as Any)]) + } + } + + public static func parse_uniqueStarGift(_ reader: BufferReader) -> UniqueStarGift? { + var _1: Api.StarGift? + if let signature = reader.readInt32() { + _1 = Api.parse(reader, signature: signature) as? Api.StarGift + } + var _2: [Api.User]? + if let _ = reader.readInt32() { + _2 = Api.parseVector(reader, elementSignature: 0, elementType: Api.User.self) + } + let _c1 = _1 != nil + let _c2 = _2 != nil + if _c1 && _c2 { + return Api.payments.UniqueStarGift.uniqueStarGift(gift: _1!, users: _2!) + } + else { + return nil + } + } + + } +} public extension Api.payments { enum UserStarGifts: TypeConstructorDescription { case userStarGifts(flags: Int32, count: Int32, gifts: [Api.UserStarGift], nextOffset: String?, users: [Api.User]) @@ -1776,121 +1866,3 @@ public extension Api.stats { } } -public extension Api.stats { - enum PublicForwards: TypeConstructorDescription { - case publicForwards(flags: Int32, count: Int32, forwards: [Api.PublicForward], nextOffset: String?, chats: [Api.Chat], users: [Api.User]) - - public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { - switch self { - case .publicForwards(let flags, let count, let forwards, let nextOffset, let chats, let users): - if boxed { - buffer.appendInt32(-1828487648) - } - serializeInt32(flags, buffer: buffer, boxed: false) - serializeInt32(count, buffer: buffer, boxed: false) - buffer.appendInt32(481674261) - buffer.appendInt32(Int32(forwards.count)) - for item in forwards { - item.serialize(buffer, true) - } - if Int(flags) & Int(1 << 0) != 0 {serializeString(nextOffset!, buffer: buffer, boxed: false)} - buffer.appendInt32(481674261) - buffer.appendInt32(Int32(chats.count)) - for item in chats { - item.serialize(buffer, true) - } - buffer.appendInt32(481674261) - buffer.appendInt32(Int32(users.count)) - for item in users { - item.serialize(buffer, true) - } - break - } - } - - public func descriptionFields() -> (String, [(String, Any)]) { - switch self { - case .publicForwards(let flags, let count, let forwards, let nextOffset, let chats, let users): - return ("publicForwards", [("flags", flags as Any), ("count", count as Any), ("forwards", forwards as Any), ("nextOffset", nextOffset as Any), ("chats", chats as Any), ("users", users as Any)]) - } - } - - public static func parse_publicForwards(_ reader: BufferReader) -> PublicForwards? { - var _1: Int32? - _1 = reader.readInt32() - var _2: Int32? - _2 = reader.readInt32() - var _3: [Api.PublicForward]? - if let _ = reader.readInt32() { - _3 = Api.parseVector(reader, elementSignature: 0, elementType: Api.PublicForward.self) - } - var _4: String? - if Int(_1!) & Int(1 << 0) != 0 {_4 = parseString(reader) } - var _5: [Api.Chat]? - if let _ = reader.readInt32() { - _5 = Api.parseVector(reader, elementSignature: 0, elementType: Api.Chat.self) - } - var _6: [Api.User]? - if let _ = reader.readInt32() { - _6 = Api.parseVector(reader, elementSignature: 0, elementType: Api.User.self) - } - let _c1 = _1 != nil - let _c2 = _2 != nil - let _c3 = _3 != nil - let _c4 = (Int(_1!) & Int(1 << 0) == 0) || _4 != nil - let _c5 = _5 != nil - let _c6 = _6 != nil - if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 { - return Api.stats.PublicForwards.publicForwards(flags: _1!, count: _2!, forwards: _3!, nextOffset: _4, chats: _5!, users: _6!) - } - else { - return nil - } - } - - } -} -public extension Api.stats { - enum StoryStats: TypeConstructorDescription { - case storyStats(viewsGraph: Api.StatsGraph, reactionsByEmotionGraph: Api.StatsGraph) - - public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { - switch self { - case .storyStats(let viewsGraph, let reactionsByEmotionGraph): - if boxed { - buffer.appendInt32(1355613820) - } - viewsGraph.serialize(buffer, true) - reactionsByEmotionGraph.serialize(buffer, true) - break - } - } - - public func descriptionFields() -> (String, [(String, Any)]) { - switch self { - case .storyStats(let viewsGraph, let reactionsByEmotionGraph): - return ("storyStats", [("viewsGraph", viewsGraph as Any), ("reactionsByEmotionGraph", reactionsByEmotionGraph as Any)]) - } - } - - public static func parse_storyStats(_ reader: BufferReader) -> StoryStats? { - var _1: Api.StatsGraph? - if let signature = reader.readInt32() { - _1 = Api.parse(reader, signature: signature) as? Api.StatsGraph - } - var _2: Api.StatsGraph? - if let signature = reader.readInt32() { - _2 = Api.parse(reader, signature: signature) as? Api.StatsGraph - } - let _c1 = _1 != nil - let _c2 = _2 != nil - if _c1 && _c2 { - return Api.stats.StoryStats.storyStats(viewsGraph: _1!, reactionsByEmotionGraph: _2!) - } - else { - return nil - } - } - - } -} diff --git a/submodules/TelegramApi/Sources/Api36.swift b/submodules/TelegramApi/Sources/Api36.swift index fd5e3e5fa8..3d6117bab4 100644 --- a/submodules/TelegramApi/Sources/Api36.swift +++ b/submodules/TelegramApi/Sources/Api36.swift @@ -1,3 +1,121 @@ +public extension Api.stats { + enum PublicForwards: TypeConstructorDescription { + case publicForwards(flags: Int32, count: Int32, forwards: [Api.PublicForward], nextOffset: String?, chats: [Api.Chat], users: [Api.User]) + + public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { + switch self { + case .publicForwards(let flags, let count, let forwards, let nextOffset, let chats, let users): + if boxed { + buffer.appendInt32(-1828487648) + } + serializeInt32(flags, buffer: buffer, boxed: false) + serializeInt32(count, buffer: buffer, boxed: false) + buffer.appendInt32(481674261) + buffer.appendInt32(Int32(forwards.count)) + for item in forwards { + item.serialize(buffer, true) + } + if Int(flags) & Int(1 << 0) != 0 {serializeString(nextOffset!, buffer: buffer, boxed: false)} + buffer.appendInt32(481674261) + buffer.appendInt32(Int32(chats.count)) + for item in chats { + item.serialize(buffer, true) + } + buffer.appendInt32(481674261) + buffer.appendInt32(Int32(users.count)) + for item in users { + item.serialize(buffer, true) + } + break + } + } + + public func descriptionFields() -> (String, [(String, Any)]) { + switch self { + case .publicForwards(let flags, let count, let forwards, let nextOffset, let chats, let users): + return ("publicForwards", [("flags", flags as Any), ("count", count as Any), ("forwards", forwards as Any), ("nextOffset", nextOffset as Any), ("chats", chats as Any), ("users", users as Any)]) + } + } + + public static func parse_publicForwards(_ reader: BufferReader) -> PublicForwards? { + var _1: Int32? + _1 = reader.readInt32() + var _2: Int32? + _2 = reader.readInt32() + var _3: [Api.PublicForward]? + if let _ = reader.readInt32() { + _3 = Api.parseVector(reader, elementSignature: 0, elementType: Api.PublicForward.self) + } + var _4: String? + if Int(_1!) & Int(1 << 0) != 0 {_4 = parseString(reader) } + var _5: [Api.Chat]? + if let _ = reader.readInt32() { + _5 = Api.parseVector(reader, elementSignature: 0, elementType: Api.Chat.self) + } + var _6: [Api.User]? + if let _ = reader.readInt32() { + _6 = Api.parseVector(reader, elementSignature: 0, elementType: Api.User.self) + } + let _c1 = _1 != nil + let _c2 = _2 != nil + let _c3 = _3 != nil + let _c4 = (Int(_1!) & Int(1 << 0) == 0) || _4 != nil + let _c5 = _5 != nil + let _c6 = _6 != nil + if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 { + return Api.stats.PublicForwards.publicForwards(flags: _1!, count: _2!, forwards: _3!, nextOffset: _4, chats: _5!, users: _6!) + } + else { + return nil + } + } + + } +} +public extension Api.stats { + enum StoryStats: TypeConstructorDescription { + case storyStats(viewsGraph: Api.StatsGraph, reactionsByEmotionGraph: Api.StatsGraph) + + public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { + switch self { + case .storyStats(let viewsGraph, let reactionsByEmotionGraph): + if boxed { + buffer.appendInt32(1355613820) + } + viewsGraph.serialize(buffer, true) + reactionsByEmotionGraph.serialize(buffer, true) + break + } + } + + public func descriptionFields() -> (String, [(String, Any)]) { + switch self { + case .storyStats(let viewsGraph, let reactionsByEmotionGraph): + return ("storyStats", [("viewsGraph", viewsGraph as Any), ("reactionsByEmotionGraph", reactionsByEmotionGraph as Any)]) + } + } + + public static func parse_storyStats(_ reader: BufferReader) -> StoryStats? { + var _1: Api.StatsGraph? + if let signature = reader.readInt32() { + _1 = Api.parse(reader, signature: signature) as? Api.StatsGraph + } + var _2: Api.StatsGraph? + if let signature = reader.readInt32() { + _2 = Api.parse(reader, signature: signature) as? Api.StatsGraph + } + let _c1 = _1 != nil + let _c2 = _2 != nil + if _c1 && _c2 { + return Api.stats.StoryStats.storyStats(viewsGraph: _1!, reactionsByEmotionGraph: _2!) + } + else { + return nil + } + } + + } +} public extension Api.stickers { enum SuggestedShortName: TypeConstructorDescription { case suggestedShortName(shortName: String) @@ -1276,57 +1394,3 @@ public extension Api.upload { } } -public extension Api.upload { - enum WebFile: TypeConstructorDescription { - case webFile(size: Int32, mimeType: String, fileType: Api.storage.FileType, mtime: Int32, bytes: Buffer) - - public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { - switch self { - case .webFile(let size, let mimeType, let fileType, let mtime, let bytes): - if boxed { - buffer.appendInt32(568808380) - } - serializeInt32(size, buffer: buffer, boxed: false) - serializeString(mimeType, buffer: buffer, boxed: false) - fileType.serialize(buffer, true) - serializeInt32(mtime, buffer: buffer, boxed: false) - serializeBytes(bytes, buffer: buffer, boxed: false) - break - } - } - - public func descriptionFields() -> (String, [(String, Any)]) { - switch self { - case .webFile(let size, let mimeType, let fileType, let mtime, let bytes): - return ("webFile", [("size", size as Any), ("mimeType", mimeType as Any), ("fileType", fileType as Any), ("mtime", mtime as Any), ("bytes", bytes as Any)]) - } - } - - public static func parse_webFile(_ reader: BufferReader) -> WebFile? { - var _1: Int32? - _1 = reader.readInt32() - var _2: String? - _2 = parseString(reader) - var _3: Api.storage.FileType? - if let signature = reader.readInt32() { - _3 = Api.parse(reader, signature: signature) as? Api.storage.FileType - } - var _4: Int32? - _4 = reader.readInt32() - var _5: Buffer? - _5 = parseBytes(reader) - let _c1 = _1 != nil - let _c2 = _2 != nil - let _c3 = _3 != nil - let _c4 = _4 != nil - let _c5 = _5 != nil - if _c1 && _c2 && _c3 && _c4 && _c5 { - return Api.upload.WebFile.webFile(size: _1!, mimeType: _2!, fileType: _3!, mtime: _4!, bytes: _5!) - } - else { - return nil - } - } - - } -} diff --git a/submodules/TelegramApi/Sources/Api37.swift b/submodules/TelegramApi/Sources/Api37.swift index a5d3434758..04e18d918c 100644 --- a/submodules/TelegramApi/Sources/Api37.swift +++ b/submodules/TelegramApi/Sources/Api37.swift @@ -1,3 +1,57 @@ +public extension Api.upload { + enum WebFile: TypeConstructorDescription { + case webFile(size: Int32, mimeType: String, fileType: Api.storage.FileType, mtime: Int32, bytes: Buffer) + + public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { + switch self { + case .webFile(let size, let mimeType, let fileType, let mtime, let bytes): + if boxed { + buffer.appendInt32(568808380) + } + serializeInt32(size, buffer: buffer, boxed: false) + serializeString(mimeType, buffer: buffer, boxed: false) + fileType.serialize(buffer, true) + serializeInt32(mtime, buffer: buffer, boxed: false) + serializeBytes(bytes, buffer: buffer, boxed: false) + break + } + } + + public func descriptionFields() -> (String, [(String, Any)]) { + switch self { + case .webFile(let size, let mimeType, let fileType, let mtime, let bytes): + return ("webFile", [("size", size as Any), ("mimeType", mimeType as Any), ("fileType", fileType as Any), ("mtime", mtime as Any), ("bytes", bytes as Any)]) + } + } + + public static func parse_webFile(_ reader: BufferReader) -> WebFile? { + var _1: Int32? + _1 = reader.readInt32() + var _2: String? + _2 = parseString(reader) + var _3: Api.storage.FileType? + if let signature = reader.readInt32() { + _3 = Api.parse(reader, signature: signature) as? Api.storage.FileType + } + var _4: Int32? + _4 = reader.readInt32() + var _5: Buffer? + _5 = parseBytes(reader) + let _c1 = _1 != nil + let _c2 = _2 != nil + let _c3 = _3 != nil + let _c4 = _4 != nil + let _c5 = _5 != nil + if _c1 && _c2 && _c3 && _c4 && _c5 { + return Api.upload.WebFile.webFile(size: _1!, mimeType: _2!, fileType: _3!, mtime: _4!, bytes: _5!) + } + else { + return nil + } + } + + } +} public extension Api.users { enum UserFull: TypeConstructorDescription { case userFull(fullUser: Api.UserFull, chats: [Api.Chat], users: [Api.User]) diff --git a/submodules/TelegramApi/Sources/Api38.swift b/submodules/TelegramApi/Sources/Api38.swift index 9df97e6f8b..3a8468e0ac 100644 --- a/submodules/TelegramApi/Sources/Api38.swift +++ b/submodules/TelegramApi/Sources/Api38.swift @@ -464,6 +464,21 @@ public extension Api.functions.account { }) } } +public extension Api.functions.account { + static func getCollectibleEmojiStatuses(hash: Int64) -> (FunctionDescription, Buffer, DeserializeFunctionResponse) { + let buffer = Buffer() + buffer.appendInt32(779830595) + serializeInt64(hash, buffer: buffer, boxed: false) + return (FunctionDescription(name: "account.getCollectibleEmojiStatuses", parameters: [("hash", String(describing: hash))]), buffer, DeserializeFunctionResponse { (buffer: Buffer) -> Api.account.EmojiStatuses? in + let reader = BufferReader(buffer) + var result: Api.account.EmojiStatuses? + if let signature = reader.readInt32() { + result = Api.parse(reader, signature: signature) as? Api.account.EmojiStatuses + } + return result + }) + } +} public extension Api.functions.account { static func getConnectedBots() -> (FunctionDescription, Buffer, DeserializeFunctionResponse) { let buffer = Buffer() @@ -2383,12 +2398,11 @@ public extension Api.functions.bots { } } public extension Api.functions.bots { - static func getBotRecommendations(flags: Int32, bot: Api.InputUser) -> (FunctionDescription, Buffer, DeserializeFunctionResponse) { + static func getBotRecommendations(bot: Api.InputUser) -> (FunctionDescription, Buffer, DeserializeFunctionResponse) { let buffer = Buffer() - buffer.appendInt32(676707937) - serializeInt32(flags, buffer: buffer, boxed: false) + buffer.appendInt32(-1581840363) bot.serialize(buffer, true) - return (FunctionDescription(name: "bots.getBotRecommendations", parameters: [("flags", String(describing: flags)), ("bot", String(describing: bot))]), buffer, DeserializeFunctionResponse { (buffer: Buffer) -> Api.users.Users? in + return (FunctionDescription(name: "bots.getBotRecommendations", parameters: [("bot", String(describing: bot))]), buffer, DeserializeFunctionResponse { (buffer: Buffer) -> Api.users.Users? in let reader = BufferReader(buffer) var result: Api.users.Users? if let signature = reader.readInt32() { @@ -7063,9 +7077,9 @@ public extension Api.functions.messages { } } public extension Api.functions.messages { - static func getWebPagePreview(flags: Int32, message: String, entities: [Api.MessageEntity]?) -> (FunctionDescription, Buffer, DeserializeFunctionResponse) { + static func getWebPagePreview(flags: Int32, message: String, entities: [Api.MessageEntity]?) -> (FunctionDescription, Buffer, DeserializeFunctionResponse) { let buffer = Buffer() - buffer.appendInt32(-1956073268) + buffer.appendInt32(1460498287) serializeInt32(flags, buffer: buffer, boxed: false) serializeString(message, buffer: buffer, boxed: false) if Int(flags) & Int(1 << 3) != 0 {buffer.appendInt32(481674261) @@ -7073,11 +7087,11 @@ public extension Api.functions.messages { for item in entities! { item.serialize(buffer, true) }} - return (FunctionDescription(name: "messages.getWebPagePreview", parameters: [("flags", String(describing: flags)), ("message", String(describing: message)), ("entities", String(describing: entities))]), buffer, DeserializeFunctionResponse { (buffer: Buffer) -> Api.MessageMedia? in + return (FunctionDescription(name: "messages.getWebPagePreview", parameters: [("flags", String(describing: flags)), ("message", String(describing: message)), ("entities", String(describing: entities))]), buffer, DeserializeFunctionResponse { (buffer: Buffer) -> Api.messages.WebPagePreview? in let reader = BufferReader(buffer) - var result: Api.MessageMedia? + var result: Api.messages.WebPagePreview? if let signature = reader.readInt32() { - result = Api.parse(reader, signature: signature) as? Api.MessageMedia + result = Api.parse(reader, signature: signature) as? Api.messages.WebPagePreview } return result }) @@ -9479,6 +9493,21 @@ public extension Api.functions.payments { }) } } +public extension Api.functions.payments { + static func getUniqueStarGift(slug: String) -> (FunctionDescription, Buffer, DeserializeFunctionResponse) { + let buffer = Buffer() + buffer.appendInt32(-1583919758) + serializeString(slug, buffer: buffer, boxed: false) + return (FunctionDescription(name: "payments.getUniqueStarGift", parameters: [("slug", String(describing: slug))]), buffer, DeserializeFunctionResponse { (buffer: Buffer) -> Api.payments.UniqueStarGift? in + let reader = BufferReader(buffer) + var result: Api.payments.UniqueStarGift? + if let signature = reader.readInt32() { + result = Api.parse(reader, signature: signature) as? Api.payments.UniqueStarGift + } + return result + }) + } +} public extension Api.functions.payments { static func getUserStarGift(msgId: [Int32]) -> (FunctionDescription, Buffer, DeserializeFunctionResponse) { let buffer = Buffer() diff --git a/submodules/TelegramApi/Sources/Api6.swift b/submodules/TelegramApi/Sources/Api6.swift index 90cfa4cde6..68e237c7af 100644 --- a/submodules/TelegramApi/Sources/Api6.swift +++ b/submodules/TelegramApi/Sources/Api6.swift @@ -796,17 +796,36 @@ public extension Api { } public extension Api { enum EmojiStatus: TypeConstructorDescription { - case emojiStatus(documentId: Int64) + case emojiStatus(flags: Int32, documentId: Int64, until: Int32?) + case emojiStatusCollectible(flags: Int32, collectibleId: Int64, documentId: Int64, title: String, slug: String, patternDocumentId: Int64, centerColor: Int32, edgeColor: Int32, patternColor: Int32, textColor: Int32, until: Int32?) case emojiStatusEmpty - case emojiStatusUntil(documentId: Int64, until: Int32) + case inputEmojiStatusCollectible(flags: Int32, collectibleId: Int64, until: Int32?) public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { switch self { - case .emojiStatus(let documentId): + case .emojiStatus(let flags, let documentId, let until): if boxed { - buffer.appendInt32(-1835310691) + buffer.appendInt32(-402717046) } + serializeInt32(flags, buffer: buffer, boxed: false) serializeInt64(documentId, buffer: buffer, boxed: false) + if Int(flags) & Int(1 << 0) != 0 {serializeInt32(until!, buffer: buffer, boxed: false)} + break + case .emojiStatusCollectible(let flags, let collectibleId, let documentId, let title, let slug, let patternDocumentId, let centerColor, let edgeColor, let patternColor, let textColor, let until): + if boxed { + buffer.appendInt32(1904500795) + } + serializeInt32(flags, buffer: buffer, boxed: false) + serializeInt64(collectibleId, buffer: buffer, boxed: false) + serializeInt64(documentId, buffer: buffer, boxed: false) + serializeString(title, buffer: buffer, boxed: false) + serializeString(slug, buffer: buffer, boxed: false) + serializeInt64(patternDocumentId, buffer: buffer, boxed: false) + serializeInt32(centerColor, buffer: buffer, boxed: false) + serializeInt32(edgeColor, buffer: buffer, boxed: false) + serializeInt32(patternColor, buffer: buffer, boxed: false) + serializeInt32(textColor, buffer: buffer, boxed: false) + if Int(flags) & Int(1 << 0) != 0 {serializeInt32(until!, buffer: buffer, boxed: false)} break case .emojiStatusEmpty: if boxed { @@ -814,33 +833,83 @@ public extension Api { } break - case .emojiStatusUntil(let documentId, let until): + case .inputEmojiStatusCollectible(let flags, let collectibleId, let until): if boxed { - buffer.appendInt32(-97474361) + buffer.appendInt32(118758847) } - serializeInt64(documentId, buffer: buffer, boxed: false) - serializeInt32(until, buffer: buffer, boxed: false) + serializeInt32(flags, buffer: buffer, boxed: false) + serializeInt64(collectibleId, buffer: buffer, boxed: false) + if Int(flags) & Int(1 << 0) != 0 {serializeInt32(until!, buffer: buffer, boxed: false)} break } } public func descriptionFields() -> (String, [(String, Any)]) { switch self { - case .emojiStatus(let documentId): - return ("emojiStatus", [("documentId", documentId as Any)]) + case .emojiStatus(let flags, let documentId, let until): + return ("emojiStatus", [("flags", flags as Any), ("documentId", documentId as Any), ("until", until as Any)]) + case .emojiStatusCollectible(let flags, let collectibleId, let documentId, let title, let slug, let patternDocumentId, let centerColor, let edgeColor, let patternColor, let textColor, let until): + return ("emojiStatusCollectible", [("flags", flags as Any), ("collectibleId", collectibleId as Any), ("documentId", documentId as Any), ("title", title as Any), ("slug", slug as Any), ("patternDocumentId", patternDocumentId as Any), ("centerColor", centerColor as Any), ("edgeColor", edgeColor as Any), ("patternColor", patternColor as Any), ("textColor", textColor as Any), ("until", until as Any)]) case .emojiStatusEmpty: return ("emojiStatusEmpty", []) - case .emojiStatusUntil(let documentId, let until): - return ("emojiStatusUntil", [("documentId", documentId as Any), ("until", until as Any)]) + case .inputEmojiStatusCollectible(let flags, let collectibleId, let until): + return ("inputEmojiStatusCollectible", [("flags", flags as Any), ("collectibleId", collectibleId as Any), ("until", until as Any)]) } } public static func parse_emojiStatus(_ reader: BufferReader) -> EmojiStatus? { - var _1: Int64? - _1 = reader.readInt64() + var _1: Int32? + _1 = reader.readInt32() + var _2: Int64? + _2 = reader.readInt64() + var _3: Int32? + if Int(_1!) & Int(1 << 0) != 0 {_3 = reader.readInt32() } let _c1 = _1 != nil - if _c1 { - return Api.EmojiStatus.emojiStatus(documentId: _1!) + let _c2 = _2 != nil + let _c3 = (Int(_1!) & Int(1 << 0) == 0) || _3 != nil + if _c1 && _c2 && _c3 { + return Api.EmojiStatus.emojiStatus(flags: _1!, documentId: _2!, until: _3) + } + else { + return nil + } + } + public static func parse_emojiStatusCollectible(_ reader: BufferReader) -> EmojiStatus? { + var _1: Int32? + _1 = reader.readInt32() + var _2: Int64? + _2 = reader.readInt64() + var _3: Int64? + _3 = reader.readInt64() + var _4: String? + _4 = parseString(reader) + var _5: String? + _5 = parseString(reader) + var _6: Int64? + _6 = reader.readInt64() + var _7: Int32? + _7 = reader.readInt32() + var _8: Int32? + _8 = reader.readInt32() + var _9: Int32? + _9 = reader.readInt32() + var _10: Int32? + _10 = reader.readInt32() + var _11: Int32? + if Int(_1!) & Int(1 << 0) != 0 {_11 = reader.readInt32() } + let _c1 = _1 != nil + let _c2 = _2 != nil + let _c3 = _3 != nil + let _c4 = _4 != nil + let _c5 = _5 != nil + let _c6 = _6 != nil + let _c7 = _7 != nil + let _c8 = _8 != nil + let _c9 = _9 != nil + let _c10 = _10 != nil + let _c11 = (Int(_1!) & Int(1 << 0) == 0) || _11 != nil + if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 && _c7 && _c8 && _c9 && _c10 && _c11 { + return Api.EmojiStatus.emojiStatusCollectible(flags: _1!, collectibleId: _2!, documentId: _3!, title: _4!, slug: _5!, patternDocumentId: _6!, centerColor: _7!, edgeColor: _8!, patternColor: _9!, textColor: _10!, until: _11) } else { return nil @@ -849,15 +918,18 @@ public extension Api { public static func parse_emojiStatusEmpty(_ reader: BufferReader) -> EmojiStatus? { return Api.EmojiStatus.emojiStatusEmpty } - public static func parse_emojiStatusUntil(_ reader: BufferReader) -> EmojiStatus? { - var _1: Int64? - _1 = reader.readInt64() - var _2: Int32? - _2 = reader.readInt32() + public static func parse_inputEmojiStatusCollectible(_ reader: BufferReader) -> EmojiStatus? { + var _1: Int32? + _1 = reader.readInt32() + var _2: Int64? + _2 = reader.readInt64() + var _3: Int32? + if Int(_1!) & Int(1 << 0) != 0 {_3 = reader.readInt32() } let _c1 = _1 != nil let _c2 = _2 != nil - if _c1 && _c2 { - return Api.EmojiStatus.emojiStatusUntil(documentId: _1!, until: _2!) + let _c3 = (Int(_1!) & Int(1 << 0) == 0) || _3 != nil + if _c1 && _c2 && _c3 { + return Api.EmojiStatus.inputEmojiStatusCollectible(flags: _1!, collectibleId: _2!, until: _3) } else { return nil diff --git a/submodules/TelegramCore/Sources/ApiUtils/StoreMessage_Telegram.swift b/submodules/TelegramCore/Sources/ApiUtils/StoreMessage_Telegram.swift index d4ea6fa3a3..89aea20e89 100644 --- a/submodules/TelegramCore/Sources/ApiUtils/StoreMessage_Telegram.swift +++ b/submodules/TelegramCore/Sources/ApiUtils/StoreMessage_Telegram.swift @@ -350,7 +350,8 @@ func textMediaAndExpirationTimerFromApiMedia(_ media: Api.MessageMedia?, _ peerI case let .messageMediaGeoLive(_, geo, heading, period, proximityNotificationRadius): let mediaMap = telegramMediaMapFromApiGeoPoint(geo, title: nil, address: nil, provider: nil, venueId: nil, venueType: nil, liveBroadcastingTimeout: period, liveProximityNotificationRadius: proximityNotificationRadius, heading: heading) return (mediaMap, nil, nil, nil, nil) - case let .messageMediaDocument(flags, document, altDocuments, ttlSeconds): + case let .messageMediaDocument(flags, document, altDocuments, coverPhoto, ttlSeconds): + let _ = coverPhoto if let document = document { if let mediaFile = telegramMediaFileFromApiDocument(document, altDocuments: altDocuments) { return (mediaFile, ttlSeconds, (flags & (1 << 3)) != 0, (flags & (1 << 4)) != 0, nil) @@ -542,6 +543,8 @@ func mediaAreaFromApiMediaArea(_ mediaArea: Api.MediaArea) -> MediaArea? { return .channelMessage(coordinates: coodinatesFromApiMediaAreaCoordinates(coordinates), messageId: EngineMessage.Id(peerId: PeerId(namespace: Namespaces.Peer.CloudChannel, id: PeerId.Id._internalFromInt64Value(channelId)), namespace: Namespaces.Message.Cloud, id: messageId)) case let .mediaAreaWeather(coordinates, emoji, temperatureC, color): return .weather(coordinates: coodinatesFromApiMediaAreaCoordinates(coordinates), emoji: emoji, temperature: temperatureC, color: color) + case let .mediaAreaStarGift(coordinates, slug): + return .starGift(coordinates: coodinatesFromApiMediaAreaCoordinates(coordinates), slug: slug) } } @@ -596,6 +599,8 @@ func apiMediaAreasFromMediaAreas(_ mediaAreas: [MediaArea], transaction: Transac apiMediaAreas.append(.mediaAreaUrl(coordinates: inputCoordinates, url: url)) case let .weather(_, emoji, temperature, color): apiMediaAreas.append(.mediaAreaWeather(coordinates: inputCoordinates, emoji: emoji, temperatureC: temperature, color: color)) + case let .starGift(_, slug): + apiMediaAreas.append(.mediaAreaStarGift(coordinates: inputCoordinates, slug: slug)) } } return apiMediaAreas diff --git a/submodules/TelegramCore/Sources/ApiUtils/TelegramMediaWebpage.swift b/submodules/TelegramCore/Sources/ApiUtils/TelegramMediaWebpage.swift index a27d6773f8..3fc7a03776 100644 --- a/submodules/TelegramCore/Sources/ApiUtils/TelegramMediaWebpage.swift +++ b/submodules/TelegramCore/Sources/ApiUtils/TelegramMediaWebpage.swift @@ -23,6 +23,11 @@ func telegramMediaWebpageAttributeFromApiWebpageAttribute(_ attribute: Api.WebPa var files: [TelegramMediaFile] = [] files = stickers.compactMap { telegramMediaFileFromApiDocument($0, altDocuments: []) } return .stickerPack(TelegramMediaWebpageStickerPackAttribute(flags: flags, files: files)) + case let .webPageAttributeUniqueStarGift(gift): + if let starGift = StarGift(apiStarGift: gift) { + return .starGift(TelegramMediaWebpageStarGiftAttribute(gift: starGift)) + } + return nil case .webPageAttributeStory: return nil } diff --git a/submodules/TelegramCore/Sources/PendingMessages/PendingMessageUploadedContent.swift b/submodules/TelegramCore/Sources/PendingMessages/PendingMessageUploadedContent.swift index 8346d55cb0..58ee46b27e 100644 --- a/submodules/TelegramCore/Sources/PendingMessages/PendingMessageUploadedContent.swift +++ b/submodules/TelegramCore/Sources/PendingMessages/PendingMessageUploadedContent.swift @@ -229,7 +229,7 @@ func mediaContentToUpload(accountPeerId: PeerId, network: Network, postbox: Post } |> mapToSignal { validatedResource -> Signal in if let validatedResource = validatedResource.updatedResource as? TelegramCloudMediaResourceWithFileReference, let reference = validatedResource.fileReference { - return .single(.content(PendingMessageUploadedContentAndReuploadInfo(content: .media(Api.InputMedia.inputMediaDocument(flags: 0, id: Api.InputDocument.inputDocument(id: resource.fileId, accessHash: resource.accessHash, fileReference: Buffer(data: reference)), ttlSeconds: nil, query: nil), text), reuploadInfo: nil, cacheReferenceKey: nil))) + return .single(.content(PendingMessageUploadedContentAndReuploadInfo(content: .media(Api.InputMedia.inputMediaDocument(flags: 0, id: Api.InputDocument.inputDocument(id: resource.fileId, accessHash: resource.accessHash, fileReference: Buffer(data: reference)), videoCover: nil, ttlSeconds: nil, query: nil), text), reuploadInfo: nil, cacheReferenceKey: nil))) } else { return .fail(.generic) } @@ -246,7 +246,7 @@ func mediaContentToUpload(accountPeerId: PeerId, network: Network, postbox: Post } } - return .single(.content(PendingMessageUploadedContentAndReuploadInfo(content: .media(Api.InputMedia.inputMediaDocument(flags: flags, id: Api.InputDocument.inputDocument(id: resource.fileId, accessHash: resource.accessHash, fileReference: Buffer(data: resource.fileReference ?? Data())), ttlSeconds: nil, query: emojiSearchQuery), text), reuploadInfo: nil, cacheReferenceKey: nil))) + return .single(.content(PendingMessageUploadedContentAndReuploadInfo(content: .media(Api.InputMedia.inputMediaDocument(flags: flags, id: Api.InputDocument.inputDocument(id: resource.fileId, accessHash: resource.accessHash, fileReference: Buffer(data: resource.fileReference ?? Data())), videoCover: nil, ttlSeconds: nil, query: emojiSearchQuery), text), reuploadInfo: nil, cacheReferenceKey: nil))) } } else { return uploadedMediaFileContent(network: network, postbox: postbox, auxiliaryMethods: auxiliaryMethods, transformOutgoingMessageMedia: transformOutgoingMessageMedia, messageMediaPreuploadManager: messageMediaPreuploadManager, forceReupload: forceReupload, isGrouped: isGrouped, isPaid: false, passFetchProgress: passFetchProgress, forceNoBigParts: forceNoBigParts, peerId: peerId, messageId: messageId, text: text, attributes: attributes, autoremoveMessageAttribute: autoremoveMessageAttribute, autoclearMessageAttribute: autoclearMessageAttribute, file: file) @@ -828,7 +828,7 @@ private func uploadedMediaFileContent(network: Network, postbox: Postbox, auxili return .single(.progress(PendingMessageUploadedContentProgress(progress: 1.0))) |> then( - .single(.content(PendingMessageUploadedContentAndReuploadInfo(content: .media(Api.InputMedia.inputMediaDocument(flags: flags, id: Api.InputDocument.inputDocument(id: resource.fileId, accessHash: resource.accessHash, fileReference: Buffer(data: fileReference)), ttlSeconds: ttlSeconds, query: nil), text), reuploadInfo: nil, cacheReferenceKey: nil))) + .single(.content(PendingMessageUploadedContentAndReuploadInfo(content: .media(Api.InputMedia.inputMediaDocument(flags: flags, id: Api.InputDocument.inputDocument(id: resource.fileId, accessHash: resource.accessHash, fileReference: Buffer(data: fileReference)), videoCover: nil, ttlSeconds: ttlSeconds, query: nil), text), reuploadInfo: nil, cacheReferenceKey: nil))) ) } referenceKey = key @@ -1049,11 +1049,11 @@ private func uploadedMediaFileContent(network: Network, postbox: Postbox, auxili } if ttlSeconds != nil { - return .single(.content(PendingMessageUploadedContentAndReuploadInfo(content: .media(.inputMediaUploadedDocument(flags: flags, file: inputFile, thumb: thumbnailFile, mimeType: file.mimeType, attributes: inputDocumentAttributesFromFileAttributes(file.attributes), stickers: stickers, ttlSeconds: ttlSeconds), text), reuploadInfo: nil, cacheReferenceKey: referenceKey))) + return .single(.content(PendingMessageUploadedContentAndReuploadInfo(content: .media(.inputMediaUploadedDocument(flags: flags, file: inputFile, thumb: thumbnailFile, mimeType: file.mimeType, attributes: inputDocumentAttributesFromFileAttributes(file.attributes), stickers: stickers, videoCover: nil, ttlSeconds: ttlSeconds), text), reuploadInfo: nil, cacheReferenceKey: referenceKey))) } if !isGrouped { - let resultInfo = PendingMessageUploadedContentAndReuploadInfo(content: .media(.inputMediaUploadedDocument(flags: flags, file: inputFile, thumb: thumbnailFile, mimeType: file.mimeType, attributes: inputDocumentAttributesFromFileAttributes(file.attributes), stickers: stickers, ttlSeconds: ttlSeconds), text), reuploadInfo: nil, cacheReferenceKey: referenceKey) + let resultInfo = PendingMessageUploadedContentAndReuploadInfo(content: .media(.inputMediaUploadedDocument(flags: flags, file: inputFile, thumb: thumbnailFile, mimeType: file.mimeType, attributes: inputDocumentAttributesFromFileAttributes(file.attributes), stickers: stickers, videoCover: nil, ttlSeconds: ttlSeconds), text), reuploadInfo: nil, cacheReferenceKey: referenceKey) return .single(.content(resultInfo)) } @@ -1064,11 +1064,11 @@ private func uploadedMediaFileContent(network: Network, postbox: Postbox, auxili |> mapError { _ -> PendingMessageUploadError in } |> mapToSignal { inputPeer -> Signal in if let inputPeer = inputPeer { - return network.request(Api.functions.messages.uploadMedia(flags: 0, businessConnectionId: nil, peer: inputPeer, media: .inputMediaUploadedDocument(flags: flags, file: inputFile, thumb: thumbnailFile, mimeType: file.mimeType, attributes: inputDocumentAttributesFromFileAttributes(file.attributes), stickers: stickers, ttlSeconds: ttlSeconds))) + return network.request(Api.functions.messages.uploadMedia(flags: 0, businessConnectionId: nil, peer: inputPeer, media: .inputMediaUploadedDocument(flags: flags, file: inputFile, thumb: thumbnailFile, mimeType: file.mimeType, attributes: inputDocumentAttributesFromFileAttributes(file.attributes), stickers: stickers, videoCover: nil, ttlSeconds: ttlSeconds))) |> mapError { _ -> PendingMessageUploadError in return .generic } |> mapToSignal { result -> Signal in switch result { - case let .messageMediaDocument(_, document, altDocuments, _): + case let .messageMediaDocument(_, document, altDocuments, _, _): if let document = document, let mediaFile = telegramMediaFileFromApiDocument(document, altDocuments: altDocuments), let resource = mediaFile.resource as? CloudDocumentMediaResource, let fileReference = resource.fileReference { var flags: Int32 = 0 var ttlSeconds: Int32? @@ -1080,7 +1080,7 @@ private func uploadedMediaFileContent(network: Network, postbox: Postbox, auxili flags |= (1 << 2) } - let result: PendingMessageUploadedContentResult = .content(PendingMessageUploadedContentAndReuploadInfo(content: .media(.inputMediaDocument(flags: flags, id: .inputDocument(id: resource.fileId, accessHash: resource.accessHash, fileReference: Buffer(data: fileReference)), ttlSeconds: ttlSeconds, query: nil), text), reuploadInfo: nil, cacheReferenceKey: nil)) + let result: PendingMessageUploadedContentResult = .content(PendingMessageUploadedContentAndReuploadInfo(content: .media(.inputMediaDocument(flags: flags, id: .inputDocument(id: resource.fileId, accessHash: resource.accessHash, fileReference: Buffer(data: fileReference)), videoCover: nil, ttlSeconds: ttlSeconds, query: nil), text), reuploadInfo: nil, cacheReferenceKey: nil)) if let _ = ttlSeconds { return .single(result) } else { diff --git a/submodules/TelegramCore/Sources/PendingMessages/StandaloneSendMessage.swift b/submodules/TelegramCore/Sources/PendingMessages/StandaloneSendMessage.swift index 2daf73906d..064f8d9991 100644 --- a/submodules/TelegramCore/Sources/PendingMessages/StandaloneSendMessage.swift +++ b/submodules/TelegramCore/Sources/PendingMessages/StandaloneSendMessage.swift @@ -702,7 +702,7 @@ private func uploadedFile(account: Account, data: Data, mimeType: String, attrib |> map { next -> UploadMediaEvent in switch next { case let .inputFile(inputFile): - return .result(Api.InputMedia.inputMediaUploadedDocument(flags: 0, file: inputFile, thumb: nil, mimeType: mimeType, attributes: inputDocumentAttributesFromFileAttributes(attributes), stickers: nil, ttlSeconds: nil)) + return .result(Api.InputMedia.inputMediaUploadedDocument(flags: 0, file: inputFile, thumb: nil, mimeType: mimeType, attributes: inputDocumentAttributesFromFileAttributes(attributes), stickers: nil, videoCover: nil, ttlSeconds: nil)) case .inputSecretFile: preconditionFailure() case let .progress(progress): diff --git a/submodules/TelegramCore/Sources/PendingMessages/StandaloneUploadedMedia.swift b/submodules/TelegramCore/Sources/PendingMessages/StandaloneUploadedMedia.swift index 2f99126412..2d195b87a2 100644 --- a/submodules/TelegramCore/Sources/PendingMessages/StandaloneUploadedMedia.swift +++ b/submodules/TelegramCore/Sources/PendingMessages/StandaloneUploadedMedia.swift @@ -158,11 +158,11 @@ public func standaloneUploadedFile(postbox: Postbox, network: Network, peerId: P if let _ = thumbnailFile { flags |= 1 << 2 } - return network.request(Api.functions.messages.uploadMedia(flags: 0, businessConnectionId: nil, peer: inputPeer, media: Api.InputMedia.inputMediaUploadedDocument(flags: flags, file: inputFile, thumb: thumbnailFile, mimeType: mimeType, attributes: inputDocumentAttributesFromFileAttributes(attributes), stickers: nil, ttlSeconds: nil))) + return network.request(Api.functions.messages.uploadMedia(flags: 0, businessConnectionId: nil, peer: inputPeer, media: Api.InputMedia.inputMediaUploadedDocument(flags: flags, file: inputFile, thumb: thumbnailFile, mimeType: mimeType, attributes: inputDocumentAttributesFromFileAttributes(attributes), stickers: nil, videoCover: nil, ttlSeconds: nil))) |> mapError { _ -> StandaloneUploadMediaError in return .generic } |> mapToSignal { media -> Signal in switch media { - case let .messageMediaDocument(_, document, altDocuments, _): + case let .messageMediaDocument(_, document, altDocuments, _, _): if let document = document { if let mediaFile = telegramMediaFileFromApiDocument(document, altDocuments: altDocuments) { return .single(.result(.media(.standalone(media: mediaFile)))) diff --git a/submodules/TelegramCore/Sources/State/AccountTaskManager.swift b/submodules/TelegramCore/Sources/State/AccountTaskManager.swift index bcc226f1d8..ba23451233 100644 --- a/submodules/TelegramCore/Sources/State/AccountTaskManager.swift +++ b/submodules/TelegramCore/Sources/State/AccountTaskManager.swift @@ -108,6 +108,7 @@ final class AccountTaskManager { tasks.add(managedRecentStatusEmoji(postbox: self.stateManager.postbox, network: self.stateManager.network).start()) tasks.add(managedFeaturedStatusEmoji(postbox: self.stateManager.postbox, network: self.stateManager.network).start()) tasks.add(managedFeaturedChannelStatusEmoji(postbox: self.stateManager.postbox, network: self.stateManager.network).start()) + tasks.add(managedUniqueStarGifts(accountPeerId: self.accountPeerId, postbox: self.stateManager.postbox, network: self.stateManager.network).start()) tasks.add(managedProfilePhotoEmoji(postbox: self.stateManager.postbox, network: self.stateManager.network).start()) tasks.add(managedGroupPhotoEmoji(postbox: self.stateManager.postbox, network: self.stateManager.network).start()) tasks.add(managedBackgroundIconEmoji(postbox: self.stateManager.postbox, network: self.stateManager.network).start()) diff --git a/submodules/TelegramCore/Sources/State/ManagedRecentStickers.swift b/submodules/TelegramCore/Sources/State/ManagedRecentStickers.swift index 8974f82500..5a06921c6b 100644 --- a/submodules/TelegramCore/Sources/State/ManagedRecentStickers.swift +++ b/submodules/TelegramCore/Sources/State/ManagedRecentStickers.swift @@ -213,11 +213,11 @@ func managedRecentStatusEmoji(postbox: Postbox, network: Network) -> Signal map { files -> [OrderedItemListEntry] in var items: [OrderedItemListEntry] = [] for status in parsedStatuses { - guard let file = files[status.fileId] else { + guard let fileId = status.emojiFileId, let file = files[fileId] else { continue } if let entry = CodableEntry(RecentMediaItem(file)) { @@ -243,11 +243,11 @@ func managedFeaturedStatusEmoji(postbox: Postbox, network: Network) -> Signal map { files -> [OrderedItemListEntry] in var items: [OrderedItemListEntry] = [] for status in parsedStatuses { - guard let file = files[status.fileId] else { + guard let fileId = status.emojiFileId, let file = files[fileId] else { continue } if let entry = CodableEntry(RecentMediaItem(file)) { @@ -273,11 +273,11 @@ func managedFeaturedChannelStatusEmoji(postbox: Postbox, network: Network) -> Si case let .emojiStatuses(_, statuses): let parsedStatuses = statuses.compactMap(PeerEmojiStatus.init(apiStatus:)) - return _internal_resolveInlineStickers(postbox: postbox, network: network, fileIds: parsedStatuses.map(\.fileId)) + return _internal_resolveInlineStickers(postbox: postbox, network: network, fileIds: parsedStatuses.compactMap(\.emojiFileId)) |> map { files -> [OrderedItemListEntry] in var items: [OrderedItemListEntry] = [] for status in parsedStatuses { - guard let file = files[status.fileId] else { + guard let fileId = status.emojiFileId, let file = files[fileId] else { continue } if let entry = CodableEntry(RecentMediaItem(file)) { @@ -292,6 +292,55 @@ func managedFeaturedChannelStatusEmoji(postbox: Postbox, network: Network) -> Si return (poll |> then(.complete() |> suspendAwareDelay(3.0 * 60.0 * 60.0, queue: Queue.concurrentDefaultQueue()))) |> restart } +func managedUniqueStarGifts(accountPeerId: PeerId, postbox: Postbox, network: Network) -> Signal { + let poll = managedRecentMedia(postbox: postbox, network: network, collectionId: Namespaces.OrderedItemList.CloudUniqueStarGifts, extractItemId: { RecentStarGiftItemId($0).id }, reverseHashOrder: false, forceFetch: false, fetch: { hash in + return network.request(Api.functions.account.getCollectibleEmojiStatuses(hash: hash)) + |> retryRequest + |> mapToSignal { result -> Signal<[OrderedItemListEntry]?, NoError> in + switch result { + case .emojiStatusesNotModified: + return .single(nil) + case let .emojiStatuses(_, statuses): + let parsedStatuses = statuses.compactMap(PeerEmojiStatus.init(apiStatus:)) + + return _internal_resolveInlineStickers(postbox: postbox, network: network, fileIds: parsedStatuses.flatMap(\.associatedFileIds)) + |> map { files -> [OrderedItemListEntry] in + var items: [OrderedItemListEntry] = [] + for status in parsedStatuses { + switch status.content { + case let .starGift(id, fileId, title, slug, patternFileId, innerColor, outerColor, patternColor, textColor): + let slugComponents = slug.components(separatedBy: "-") + if let file = files[fileId], let patternFile = files[patternFileId], let numberString = slugComponents.last, let number = Int32(numberString) { + let gift = StarGift.UniqueGift( + id: id, + title: title, + number: number, + slug: slug, + owner: .peerId(accountPeerId), + 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) + ) + if let entry = CodableEntry(RecentStarGiftItem(gift)) { + items.append(OrderedItemListEntry(id: RecentStarGiftItemId(id).rawValue, contents: entry)) + } + } + default: + break + } + } + return items + } + } + } + }) + return (poll |> then(.complete() |> suspendAwareDelay(1.0 * 60.0 * 60.0, queue: Queue.concurrentDefaultQueue()))) |> restart +} + + func managedProfilePhotoEmoji(postbox: Postbox, network: Network) -> Signal { let poll = managedRecentMedia(postbox: postbox, network: network, collectionId: Namespaces.OrderedItemList.CloudFeaturedProfilePhotoEmoji, extractItemId: { RecentMediaItemId($0).mediaId.id }, reverseHashOrder: false, forceFetch: false, fetch: { hash in return network.request(Api.functions.account.getDefaultProfilePhotoEmojis(hash: hash)) diff --git a/submodules/TelegramCore/Sources/State/Serialization.swift b/submodules/TelegramCore/Sources/State/Serialization.swift index cbbbced361..40f0173b41 100644 --- a/submodules/TelegramCore/Sources/State/Serialization.swift +++ b/submodules/TelegramCore/Sources/State/Serialization.swift @@ -210,7 +210,7 @@ public class BoxedMessage: NSObject { public class Serialization: NSObject, MTSerialization { public func currentLayer() -> UInt { - return 196 + return 198 } public func parseMessage(_ data: Data!) -> Any! { diff --git a/submodules/TelegramCore/Sources/State/UpdatesApiUtils.swift b/submodules/TelegramCore/Sources/State/UpdatesApiUtils.swift index 47ef304788..bd2b2f20f0 100644 --- a/submodules/TelegramCore/Sources/State/UpdatesApiUtils.swift +++ b/submodules/TelegramCore/Sources/State/UpdatesApiUtils.swift @@ -52,7 +52,7 @@ extension Api.MessageMedia { } else { return nil } - case let .messageMediaDocument(_, document, _, _): + case let .messageMediaDocument(_, document, _, _, _): if let document = document { return collectPreCachedResources(for: document) } @@ -626,14 +626,14 @@ extension Api.EncryptedMessage { extension Api.InputMedia { func withUpdatedStickers(_ stickers: [Api.InputDocument]?) -> Api.InputMedia { switch self { - case let .inputMediaUploadedDocument(flags, file, thumb, mimeType, attributes, _, ttlSeconds): + case let .inputMediaUploadedDocument(flags, file, thumb, mimeType, attributes, _, videoCover, ttlSeconds): var flags = flags var attributes = attributes if let _ = stickers { flags |= (1 << 0) attributes.append(.documentAttributeHasStickers) } - return .inputMediaUploadedDocument(flags: flags, file: file, thumb: thumb, mimeType: mimeType, attributes: attributes, stickers: stickers, ttlSeconds: ttlSeconds) + return .inputMediaUploadedDocument(flags: flags, file: file, thumb: thumb, mimeType: mimeType, attributes: attributes, stickers: stickers, videoCover: videoCover, ttlSeconds: ttlSeconds) case let .inputMediaUploadedPhoto(flags, file, _, ttlSeconds): var flags = flags if let _ = stickers { diff --git a/submodules/TelegramCore/Sources/SyncCore/SyncCore_CachedUserData.swift b/submodules/TelegramCore/Sources/SyncCore/SyncCore_CachedUserData.swift index f91c7088ea..c1d130f18b 100644 --- a/submodules/TelegramCore/Sources/SyncCore/SyncCore_CachedUserData.swift +++ b/submodules/TelegramCore/Sources/SyncCore/SyncCore_CachedUserData.swift @@ -260,11 +260,62 @@ public enum PeerNameColor: Hashable { } public struct PeerEmojiStatus: Equatable, Codable { - public var fileId: Int64 + public enum Content: Equatable, Codable { + private enum CodingKeys: String, CodingKey { + case discriminator + case fileId + case id + case title + case slug + case patternFileId + case innerColor + case outerColor + case patternColor + case textColor + } + + case emoji(fileId: Int64) + case starGift(id: Int64, fileId: Int64, title: String, slug: String, patternFileId: Int64, innerColor: Int32, outerColor: Int32, patternColor: Int32, textColor: Int32) + + public init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + + switch try container.decode(Int32.self, forKey: .discriminator) { + case 0: + self = .emoji(fileId: try container.decode(Int64.self, forKey: .fileId)) + case 1: + self = .starGift(id: try container.decode(Int64.self, forKey: .id), fileId: try container.decode(Int64.self, forKey: .fileId), title: try container.decode(String.self, forKey: .title), slug: try container.decode(String.self, forKey: .slug), patternFileId: try container.decode(Int64.self, forKey: .patternFileId), innerColor: try container.decode(Int32.self, forKey: .innerColor), outerColor: try container.decode(Int32.self, forKey: .outerColor), patternColor: try container.decode(Int32.self, forKey: .patternColor), textColor: try container.decode(Int32.self, forKey: .textColor)) + default: + throw DecodingError.dataCorrupted(DecodingError.Context(codingPath: decoder.codingPath, debugDescription: "content")) + } + } + + public func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + + switch self { + case let .emoji(fileId): + try container.encode(0 as Int32, forKey: .discriminator) + try container.encode(fileId, forKey: .fileId) + case let .starGift(id, fileId, title, slug, patternFileId, innerColor, outerColor, patternColor, textColor): + try container.encode(1 as Int32, forKey: .discriminator) + try container.encode(id, forKey: .id) + try container.encode(fileId, forKey: .fileId) + try container.encode(title, forKey: .title) + try container.encode(slug, forKey: .slug) + try container.encode(patternFileId, forKey: .patternFileId) + try container.encode(innerColor, forKey: .innerColor) + try container.encode(outerColor, forKey: .outerColor) + try container.encode(patternColor, forKey: .patternColor) + try container.encode(textColor, forKey: .textColor) + } + } + } + public var content: Content public var expirationDate: Int32? - public init(fileId: Int64, expirationDate: Int32?) { - self.fileId = fileId + public init(content: Content, expirationDate: Int32?) { + self.content = content self.expirationDate = expirationDate } } @@ -272,15 +323,52 @@ public struct PeerEmojiStatus: Equatable, Codable { extension PeerEmojiStatus { init?(apiStatus: Api.EmojiStatus) { switch apiStatus { - case let .emojiStatus(documentId): - self.init(fileId: documentId, expirationDate: nil) - case let .emojiStatusUntil(documentId, until): - self.init(fileId: documentId, expirationDate: until) - case .emojiStatusEmpty: + case let .emojiStatus(_, documentId, until): + self.init(content: .emoji(fileId: documentId), expirationDate: until) + case let .emojiStatusCollectible(_, collectibleId, documentId, title, slug, patternDocumentId, centerColor, edgeColor, patternColor, textColor, until): + self.init(content: .starGift(id: collectibleId, fileId: documentId, title: title, slug: slug, patternFileId: patternDocumentId, innerColor: centerColor, outerColor: edgeColor, patternColor: patternColor, textColor: textColor), expirationDate: until) + case .emojiStatusEmpty, .inputEmojiStatusCollectible: return nil } } } +extension PeerEmojiStatus { + var emojiFileId: Int64? { + switch self.content { + case let .emoji(fileId): + return fileId + default: + return nil + } + } + + var associatedFileIds: [Int64] { + switch self.content { + case let .emoji(fileId): + return [fileId] + case let .starGift(_, fileId, _, _, patternFileId, _, _, _, _): + return [fileId, patternFileId] + } + } + + public var fileId: Int64 { + switch self.content { + case let .emoji(fileId): + return fileId + case let .starGift(_, fileId, _, _, _, _, _, _, _): + return fileId + } + } + + public var color: Int32? { + switch self.content { + case .emoji: + return nil + case let .starGift(_, _, _, _, _, innerColor, _, _, _): + return innerColor + } + } +} public struct CachedUserFlags: OptionSet { public var rawValue: Int32 diff --git a/submodules/TelegramCore/Sources/SyncCore/SyncCore_Namespaces.swift b/submodules/TelegramCore/Sources/SyncCore/SyncCore_Namespaces.swift index ba7b2a4dd4..2ce6c8d17a 100644 --- a/submodules/TelegramCore/Sources/SyncCore/SyncCore_Namespaces.swift +++ b/submodules/TelegramCore/Sources/SyncCore/SyncCore_Namespaces.swift @@ -95,6 +95,7 @@ public struct Namespaces { public static let CloudFeaturedChannelStatusEmoji: Int32 = 27 public static let CloudDisabledChannelStatusEmoji: Int32 = 28 public static let CloudDefaultTagReactions: Int32 = 29 + public static let CloudUniqueStarGifts: Int32 = 30 } public struct CachedItemCollection { @@ -138,6 +139,7 @@ public struct Namespaces { public static let starsReactionDefaultToPrivate: Int8 = 41 public static let cachedPremiumGiftCodeOptions: Int8 = 42 public static let cachedProfileGifts: Int8 = 43 + public static let recommendedBots: Int8 = 44 } public struct UnorderedItemList { diff --git a/submodules/TelegramCore/Sources/SyncCore/SyncCore_RecentMediaItem.swift b/submodules/TelegramCore/Sources/SyncCore/SyncCore_RecentMediaItem.swift index c8a0ad4f8c..9bdd6e3078 100644 --- a/submodules/TelegramCore/Sources/SyncCore/SyncCore_RecentMediaItem.swift +++ b/submodules/TelegramCore/Sources/SyncCore/SyncCore_RecentMediaItem.swift @@ -284,3 +284,47 @@ public final class RecentReactionItem: Codable, Equatable { return lhs.content == rhs.content } } + +public struct RecentStarGiftItemId { + public let rawValue: MemoryBuffer + public let id: Int64 + + public init(_ rawValue: MemoryBuffer) { + self.rawValue = rawValue + assert(rawValue.length == 8) + var id: Int64 = 0 + memcpy(&id, rawValue.memory, 8) + self.id = id + } + + public init(_ id: Int64) { + var id = id + self.id = id + self.rawValue = MemoryBuffer(memory: malloc(8)!, capacity: 8, length: 8, freeWhenDone: true) + memcpy(self.rawValue.memory, &id, 8) + } +} + +public final class RecentStarGiftItem: Codable, Equatable { + public let starGift: StarGift.UniqueGift + + public init(_ starGift: StarGift.UniqueGift) { + self.starGift = starGift + } + + public init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: StringCodingKey.self) + + self.starGift = try container.decode(StarGift.UniqueGift.self, forKey: "g") + } + + public func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: StringCodingKey.self) + + try container.encode(self.starGift, forKey: "g") + } + + public static func ==(lhs: RecentStarGiftItem, rhs: RecentStarGiftItem) -> Bool { + return lhs.starGift == rhs.starGift + } +} diff --git a/submodules/TelegramCore/Sources/SyncCore/SyncCore_TelegramChannel.swift b/submodules/TelegramCore/Sources/SyncCore/SyncCore_TelegramChannel.swift index d5450424a5..fc635a43f0 100644 --- a/submodules/TelegramCore/Sources/SyncCore/SyncCore_TelegramChannel.swift +++ b/submodules/TelegramCore/Sources/SyncCore/SyncCore_TelegramChannel.swift @@ -186,22 +186,26 @@ public final class TelegramChannel: Peer, Equatable { } public var associatedMediaIds: [MediaId]? { - if let emojiStatus = self.emojiStatus, let backgroundEmojiId = self.backgroundEmojiId { - return [ - MediaId(namespace: Namespaces.Media.CloudFile, id: emojiStatus.fileId), - MediaId(namespace: Namespaces.Media.CloudFile, id: backgroundEmojiId) - ] - } else if let emojiStatus = self.emojiStatus { - return [ - MediaId(namespace: Namespaces.Media.CloudFile, id: emojiStatus.fileId) - ] - } else if let backgroundEmojiId = self.backgroundEmojiId { - return [ - MediaId(namespace: Namespaces.Media.CloudFile, id: backgroundEmojiId) - ] - } else { + var mediaIds: [MediaId] = [] + if let emojiStatus = self.emojiStatus { + switch emojiStatus.content { + case let .emoji(fileId): + mediaIds.append(MediaId(namespace: Namespaces.Media.CloudFile, id: fileId)) + case let .starGift(_, fileId, _, _, patternFileId, _, _, _, _): + mediaIds.append(MediaId(namespace: Namespaces.Media.CloudFile, id: fileId)) + mediaIds.append(MediaId(namespace: Namespaces.Media.CloudFile, id: patternFileId)) + } + } + if let backgroundEmojiId = self.backgroundEmojiId { + mediaIds.append(MediaId(namespace: Namespaces.Media.CloudFile, id: backgroundEmojiId)) + } + if let profileBackgroundEmojiId = self.profileBackgroundEmojiId { + mediaIds.append(MediaId(namespace: Namespaces.Media.CloudFile, id: profileBackgroundEmojiId)) + } + guard !mediaIds.isEmpty else { return nil } + return mediaIds } public let associatedPeerId: PeerId? = nil diff --git a/submodules/TelegramCore/Sources/SyncCore/SyncCore_TelegramMediaWebpage.swift b/submodules/TelegramCore/Sources/SyncCore/SyncCore_TelegramMediaWebpage.swift index de0cecfa5f..7d792980d9 100644 --- a/submodules/TelegramCore/Sources/SyncCore/SyncCore_TelegramMediaWebpage.swift +++ b/submodules/TelegramCore/Sources/SyncCore/SyncCore_TelegramMediaWebpage.swift @@ -4,12 +4,14 @@ private enum TelegramMediaWebpageAttributeTypes: Int32 { case unsupported case theme case stickerPack + case starGift } public enum TelegramMediaWebpageAttribute: PostboxCoding, Equatable { case unsupported case theme(TelegraMediaWebpageThemeAttribute) case stickerPack(TelegramMediaWebpageStickerPackAttribute) + case starGift(TelegramMediaWebpageStarGiftAttribute) public init(decoder: PostboxDecoder) { switch decoder.decodeInt32ForKey("r", orElse: 0) { @@ -17,6 +19,8 @@ public enum TelegramMediaWebpageAttribute: PostboxCoding, Equatable { self = .theme(decoder.decodeObjectForKey("a", decoder: { TelegraMediaWebpageThemeAttribute(decoder: $0) }) as! TelegraMediaWebpageThemeAttribute) case TelegramMediaWebpageAttributeTypes.stickerPack.rawValue: self = .stickerPack(decoder.decodeObjectForKey("a", decoder: { TelegramMediaWebpageStickerPackAttribute(decoder: $0) }) as! TelegramMediaWebpageStickerPackAttribute) + case TelegramMediaWebpageAttributeTypes.starGift.rawValue: + self = .starGift(decoder.decodeObjectForKey("a", decoder: { TelegramMediaWebpageStarGiftAttribute(decoder: $0) }) as! TelegramMediaWebpageStarGiftAttribute) default: self = .unsupported } @@ -32,6 +36,9 @@ public enum TelegramMediaWebpageAttribute: PostboxCoding, Equatable { case let .stickerPack(attribute): encoder.encodeInt32(TelegramMediaWebpageAttributeTypes.stickerPack.rawValue, forKey: "r") encoder.encodeObject(attribute, forKey: "a") + case let .starGift(attribute): + encoder.encodeInt32(TelegramMediaWebpageAttributeTypes.starGift.rawValue, forKey: "r") + encoder.encodeObject(attribute, forKey: "a") } } } @@ -127,6 +134,29 @@ public final class TelegramMediaWebpageStickerPackAttribute: PostboxCoding, Equa } } +public final class TelegramMediaWebpageStarGiftAttribute: PostboxCoding, Equatable { + public static func == (lhs: TelegramMediaWebpageStarGiftAttribute, rhs: TelegramMediaWebpageStarGiftAttribute) -> Bool { + if lhs.gift != rhs.gift { + return false + } + return true + } + + public let gift: StarGift + + public init(gift: StarGift) { + self.gift = gift + } + + public init(decoder: PostboxDecoder) { + self.gift = decoder.decodeObjectForKey("gift", decoder: { StarGift(decoder: $0) }) as! StarGift + } + + public func encode(_ encoder: PostboxEncoder) { + encoder.encodeObject(self.gift, forKey: "gift") + } +} + public final class TelegramMediaWebpageLoadedContent: PostboxCoding, Equatable { public let url: String public let displayUrl: String diff --git a/submodules/TelegramCore/Sources/SyncCore/SyncCore_TelegramUser.swift b/submodules/TelegramCore/Sources/SyncCore/SyncCore_TelegramUser.swift index ed06240571..4ffd92962b 100644 --- a/submodules/TelegramCore/Sources/SyncCore/SyncCore_TelegramUser.swift +++ b/submodules/TelegramCore/Sources/SyncCore/SyncCore_TelegramUser.swift @@ -174,22 +174,26 @@ public final class TelegramUser: Peer, Equatable { } public var associatedMediaIds: [MediaId]? { - if let emojiStatus = self.emojiStatus, let backgroundEmojiId = self.backgroundEmojiId { - return [ - MediaId(namespace: Namespaces.Media.CloudFile, id: emojiStatus.fileId), - MediaId(namespace: Namespaces.Media.CloudFile, id: backgroundEmojiId) - ] - } else if let emojiStatus = self.emojiStatus { - return [ - MediaId(namespace: Namespaces.Media.CloudFile, id: emojiStatus.fileId) - ] - } else if let backgroundEmojiId = self.backgroundEmojiId { - return [ - MediaId(namespace: Namespaces.Media.CloudFile, id: backgroundEmojiId) - ] - } else { + var mediaIds: [MediaId] = [] + if let emojiStatus = self.emojiStatus { + switch emojiStatus.content { + case let .emoji(fileId): + mediaIds.append(MediaId(namespace: Namespaces.Media.CloudFile, id: fileId)) + case let .starGift(_, fileId, _, _, patternFileId, _, _, _, _): + mediaIds.append(MediaId(namespace: Namespaces.Media.CloudFile, id: fileId)) + mediaIds.append(MediaId(namespace: Namespaces.Media.CloudFile, id: patternFileId)) + } + } + if let backgroundEmojiId = self.backgroundEmojiId { + mediaIds.append(MediaId(namespace: Namespaces.Media.CloudFile, id: backgroundEmojiId)) + } + if let profileBackgroundEmojiId = self.profileBackgroundEmojiId { + mediaIds.append(MediaId(namespace: Namespaces.Media.CloudFile, id: profileBackgroundEmojiId)) + } + guard !mediaIds.isEmpty else { return nil } + return mediaIds } public let associatedPeerId: PeerId? = nil diff --git a/submodules/TelegramCore/Sources/TelegramEngine/AccountData/TelegramEngineAccountData.swift b/submodules/TelegramCore/Sources/TelegramEngine/AccountData/TelegramEngineAccountData.swift index 02aa66b673..61fd06f150 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/AccountData/TelegramEngineAccountData.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/AccountData/TelegramEngineAccountData.swift @@ -75,15 +75,75 @@ public extension TelegramEngine { return _internal_removeAccountPhoto(account: self.account, reference: reference, fallback: true) } + public func setStarGiftStatus(starGift: StarGift.UniqueGift, expirationDate: Int32?) -> Signal { + let peerId = self.account.peerId + + var flags: Int32 = 0 + if let _ = expirationDate { + flags |= (1 << 0) + } + var file: TelegramMediaFile? + var patternFile: TelegramMediaFile? + var innerColor: Int32? + var outerColor: Int32? + var patternColor: Int32? + var textColor: Int32? + for attribute in starGift.attributes { + switch attribute { + case let .model(_, fileValue, _): + file = fileValue + case let .pattern(_, patternFileValue, _): + patternFile = patternFileValue + case let .backdrop(_, innerColorValue, outerColorValue, patternColorValue, textColorValue, _): + innerColor = innerColorValue + outerColor = outerColorValue + patternColor = patternColorValue + textColor = textColorValue + default: + break + } + } + let apiEmojiStatus: Api.EmojiStatus + var emojiStatus: PeerEmojiStatus? + if let file, let patternFile, let innerColor, let outerColor, let patternColor, let textColor { + apiEmojiStatus = .inputEmojiStatusCollectible(flags: flags, collectibleId: starGift.id, until: expirationDate) + emojiStatus = PeerEmojiStatus(content: .starGift(id: starGift.id, fileId: file.fileId.id, title: starGift.title, slug: starGift.slug, patternFileId: patternFile.fileId.id, innerColor: innerColor, outerColor: outerColor, patternColor: patternColor, textColor: textColor), expirationDate: expirationDate) + } else { + apiEmojiStatus = .emojiStatusEmpty + } + + let remoteApply = self.account.network.request(Api.functions.account.updateEmojiStatus(emojiStatus: apiEmojiStatus)) + |> `catch` { _ -> Signal in + return .single(.boolFalse) + } + |> ignoreValues + + return self.account.postbox.transaction { transaction -> Void in + if let file, let patternFile { + transaction.storeMediaIfNotPresent(media: file) + transaction.storeMediaIfNotPresent(media: patternFile) + } + if let peer = transaction.getPeer(peerId) as? TelegramUser { + updatePeersCustom(transaction: transaction, peers: [ + peer.withUpdatedEmojiStatus(emojiStatus) + ], update: { _, updated in + updated + }) + } + } + |> ignoreValues + |> then(remoteApply) + } + public func setEmojiStatus(file: TelegramMediaFile?, expirationDate: Int32?) -> Signal { let peerId = self.account.peerId let remoteApply = self.account.network.request(Api.functions.account.updateEmojiStatus(emojiStatus: file.flatMap({ file in - if let expirationDate = expirationDate { - return Api.EmojiStatus.emojiStatusUntil(documentId: file.fileId.id, until: expirationDate) - } else { - return Api.EmojiStatus.emojiStatus(documentId: file.fileId.id) + var flags: Int32 = 0 + if let _ = expirationDate { + flags |= (1 << 0) } + return Api.EmojiStatus.emojiStatus(flags: flags, documentId: file.fileId.id, until: expirationDate) }) ?? Api.EmojiStatus.emojiStatusEmpty)) |> `catch` { _ -> Signal in return .single(.boolFalse) @@ -101,7 +161,7 @@ public extension TelegramEngine { } if let peer = transaction.getPeer(peerId) as? TelegramUser { - updatePeersCustom(transaction: transaction, peers: [peer.withUpdatedEmojiStatus(file.flatMap({ PeerEmojiStatus(fileId: $0.fileId.id, expirationDate: expirationDate) }))], update: { _, updated in + updatePeersCustom(transaction: transaction, peers: [peer.withUpdatedEmojiStatus(file.flatMap({ PeerEmojiStatus(content: .emoji(fileId: $0.fileId.id), expirationDate: expirationDate) }))], update: { _, updated in updated }) } diff --git a/submodules/TelegramCore/Sources/TelegramEngine/HistoryImport/TelegramEngineHistoryImport.swift b/submodules/TelegramCore/Sources/TelegramEngine/HistoryImport/TelegramEngineHistoryImport.swift index 8d48f8789e..9a044ea5aa 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/HistoryImport/TelegramEngineHistoryImport.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/HistoryImport/TelegramEngineHistoryImport.swift @@ -153,7 +153,7 @@ public extension TelegramEngine { default: break } - inputMedia = .inputMediaUploadedDocument(flags: 0, file: inputFile, thumb: nil, mimeType: resolvedMimeType, attributes: attributes, stickers: nil, ttlSeconds: nil) + inputMedia = .inputMediaUploadedDocument(flags: 0, file: inputFile, thumb: nil, mimeType: resolvedMimeType, attributes: attributes, stickers: nil, videoCover: nil, ttlSeconds: nil) } case let .progress(value): return .single(value) diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Messages/MediaArea.swift b/submodules/TelegramCore/Sources/TelegramEngine/Messages/MediaArea.swift index dec49da75a..eb6ebed9d1 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Messages/MediaArea.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Messages/MediaArea.swift @@ -152,6 +152,7 @@ public enum MediaArea: Codable, Equatable { case channelMessage(coordinates: Coordinates, messageId: EngineMessage.Id) case link(coordinates: Coordinates, url: String) case weather(coordinates: Coordinates, emoji: String, temperature: Double, color: Int32) + case starGift(coordinates: Coordinates, slug: String) public struct ReactionFlags: OptionSet { public var rawValue: Int32 @@ -174,6 +175,7 @@ public enum MediaArea: Codable, Equatable { case channelMessage case link case weather + case starGift } public enum DecodingError: Error { @@ -210,6 +212,10 @@ public enum MediaArea: Codable, Equatable { let temperature = try container.decode(Double.self, forKey: .temperature) let color = try container.decodeIfPresent(Int32.self, forKey: .color) ?? 0 self = .weather(coordinates: coordinates, emoji: emoji, temperature: temperature, color: color) + case .starGift: + let coordinates = try container.decode(MediaArea.Coordinates.self, forKey: .coordinates) + let slug = try container.decode(String.self, forKey: .value) + self = .starGift(coordinates: coordinates, slug: slug) } } @@ -240,6 +246,10 @@ public enum MediaArea: Codable, Equatable { try container.encode(emoji, forKey: .value) try container.encode(temperature, forKey: .temperature) try container.encode(color, forKey: .color) + case let .starGift(coordinates, slug): + try container.encode(MediaAreaType.starGift.rawValue, forKey: .type) + try container.encode(coordinates, forKey: .coordinates) + try container.encode(slug, forKey: .value) } } } @@ -257,6 +267,8 @@ public extension MediaArea { return coordinates case let .weather(coordinates, _, _, _): return coordinates + case let .starGift(coordinates, _): + return coordinates } } } diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Messages/Stories.swift b/submodules/TelegramCore/Sources/TelegramEngine/Messages/Stories.swift index 2336fd44b7..b498a4667d 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Messages/Stories.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Messages/Stories.swift @@ -1405,7 +1405,7 @@ func _internal_deleteBotPreviews(account: Account, peerId: PeerId, language: Str inputMedia.append(.inputMediaPhoto(flags: 0, id: .inputPhoto(id: resource.photoId, accessHash: resource.accessHash, fileReference: Buffer(data: resource.fileReference)), ttlSeconds: nil)) inputMedia.append(Api.InputMedia.inputMediaPhoto(flags: 0, id: Api.InputPhoto.inputPhoto(id: resource.photoId, accessHash: resource.accessHash, fileReference: Buffer(data: resource.fileReference)), ttlSeconds: nil)) } else if let file = item as? TelegramMediaFile, let resource = file.resource as? CloudDocumentMediaResource { - inputMedia.append(.inputMediaDocument(flags: 0, id: .inputDocument(id: resource.fileId, accessHash: resource.accessHash, fileReference: Buffer(data: resource.fileReference ?? Data())), ttlSeconds: nil, query: nil)) + inputMedia.append(.inputMediaDocument(flags: 0, id: .inputDocument(id: resource.fileId, accessHash: resource.accessHash, fileReference: Buffer(data: resource.fileReference ?? Data())), videoCover: nil, ttlSeconds: nil, query: nil)) } } if language == nil { @@ -1463,7 +1463,7 @@ func _internal_deleteBotPreviewsLanguage(account: Account, peerId: PeerId, langu inputMedia.append(.inputMediaPhoto(flags: 0, id: .inputPhoto(id: resource.photoId, accessHash: resource.accessHash, fileReference: Buffer(data: resource.fileReference)), ttlSeconds: nil)) inputMedia.append(Api.InputMedia.inputMediaPhoto(flags: 0, id: Api.InputPhoto.inputPhoto(id: resource.photoId, accessHash: resource.accessHash, fileReference: Buffer(data: resource.fileReference)), ttlSeconds: nil)) } else if let file = item as? TelegramMediaFile, let resource = file.resource as? CloudDocumentMediaResource { - inputMedia.append(.inputMediaDocument(flags: 0, id: .inputDocument(id: resource.fileId, accessHash: resource.accessHash, fileReference: Buffer(data: resource.fileReference ?? Data())), ttlSeconds: nil, query: nil)) + inputMedia.append(.inputMediaDocument(flags: 0, id: .inputDocument(id: resource.fileId, accessHash: resource.accessHash, fileReference: Buffer(data: resource.fileReference ?? Data())), videoCover: nil, ttlSeconds: nil, query: nil)) } } transaction.updatePeerCachedData(peerIds: Set([peerId]), update: { _, current -> CachedPeerData? in @@ -1528,7 +1528,7 @@ func _internal_editStory(account: Account, peerId: PeerId, id: Int32, media: Eng if let result = result, case let .content(uploadedContent) = result, case let .media(media, _) = uploadedContent.content { inputMedia = media } else if case let .existing(media) = media, let file = media as? TelegramMediaFile, let resource = file.resource as? CloudDocumentMediaResource { - inputMedia = .inputMediaUploadedDocument(flags: 0, file: .inputFileStoryDocument(id: .inputDocument(id: resource.fileId, accessHash: resource.accessHash, fileReference: Buffer(data: resource.fileReference))), thumb: nil, mimeType: file.mimeType, attributes: inputDocumentAttributesFromFileAttributes(file.attributes), stickers: nil, ttlSeconds: nil) + inputMedia = .inputMediaUploadedDocument(flags: 0, file: .inputFileStoryDocument(id: .inputDocument(id: resource.fileId, accessHash: resource.accessHash, fileReference: Buffer(data: resource.fileReference))), thumb: nil, mimeType: file.mimeType, attributes: inputDocumentAttributesFromFileAttributes(file.attributes), stickers: nil, videoCover: nil, ttlSeconds: nil) updatingCoverTime = true } else { inputMedia = nil @@ -2099,7 +2099,7 @@ extension Stories.StoredItem { var parsedAlternativeMedia: [Media] = [] switch media { - case let .messageMediaDocument(_, _, altDocuments, _): + case let .messageMediaDocument(_, _, altDocuments, _, _): if let altDocuments { parsedAlternativeMedia = altDocuments.compactMap { telegramMediaFileFromApiDocument($0, altDocuments: []) } } diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Messages/StoryListContext.swift b/submodules/TelegramCore/Sources/TelegramEngine/Messages/StoryListContext.swift index b6e2c1fd7f..9502d36aea 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Messages/StoryListContext.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Messages/StoryListContext.swift @@ -2615,7 +2615,7 @@ public final class BotPreviewStoryListContext: StoryListContext { inputMedia.append(.inputMediaPhoto(flags: 0, id: .inputPhoto(id: resource.photoId, accessHash: resource.accessHash, fileReference: Buffer(data: resource.fileReference)), ttlSeconds: nil)) inputMedia.append(Api.InputMedia.inputMediaPhoto(flags: 0, id: Api.InputPhoto.inputPhoto(id: resource.photoId, accessHash: resource.accessHash, fileReference: Buffer(data: resource.fileReference)), ttlSeconds: nil)) } else if let file = item as? TelegramMediaFile, let resource = file.resource as? CloudDocumentMediaResource { - inputMedia.append(.inputMediaDocument(flags: 0, id: .inputDocument(id: resource.fileId, accessHash: resource.accessHash, fileReference: Buffer(data: resource.fileReference ?? Data())), ttlSeconds: nil, query: nil)) + inputMedia.append(.inputMediaDocument(flags: 0, id: .inputDocument(id: resource.fileId, accessHash: resource.accessHash, fileReference: Buffer(data: resource.fileReference ?? Data())), videoCover: nil, ttlSeconds: nil, query: nil)) } } diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Payments/StarGifts.swift b/submodules/TelegramCore/Sources/TelegramEngine/Payments/StarGifts.swift index 1ddc78009f..bfeb70e88f 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Payments/StarGifts.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Payments/StarGifts.swift @@ -204,7 +204,9 @@ public enum StarGift: Equatable, Codable, PostboxCoding { case id case title case number + case slug case ownerPeerId + case ownerName case attributes case availability } @@ -428,6 +430,11 @@ public enum StarGift: Equatable, Codable, PostboxCoding { encoder.encodeInt32(self.total, forKey: CodingKeys.total.rawValue) } } + + public enum Owner: Equatable { + case peerId(EnginePeer.Id) + case name(String) + } public enum DecodingError: Error { case generic @@ -436,15 +443,17 @@ public enum StarGift: Equatable, Codable, PostboxCoding { public let id: Int64 public let title: String public let number: Int32 - public let ownerPeerId: EnginePeer.Id + public let slug: String + public let owner: Owner public let attributes: [Attribute] public let availability: Availability - public init(id: Int64, title: String, number: Int32, ownerPeerId: EnginePeer.Id, attributes: [Attribute], availability: Availability) { + public init(id: Int64, title: String, number: Int32, slug: String, owner: Owner, attributes: [Attribute], availability: Availability) { self.id = id self.title = title self.number = number - self.ownerPeerId = ownerPeerId + self.slug = slug + self.owner = owner self.attributes = attributes self.availability = availability } @@ -454,7 +463,14 @@ public enum StarGift: Equatable, Codable, PostboxCoding { self.id = try container.decode(Int64.self, forKey: .id) self.title = try container.decode(String.self, forKey: .title) self.number = try container.decode(Int32.self, forKey: .number) - self.ownerPeerId = EnginePeer.Id(namespace: Namespaces.Peer.CloudUser, id: PeerId.Id._internalFromInt64Value(try container.decode(Int64.self, forKey: .ownerPeerId))) + self.slug = try container.decodeIfPresent(String.self, forKey: .slug) ?? "" + if let ownerId = try container.decodeIfPresent(Int64.self, forKey: .ownerPeerId) { + self.owner = .peerId(EnginePeer.Id(namespace: Namespaces.Peer.CloudUser, id: PeerId.Id._internalFromInt64Value(ownerId))) + } else if let ownerName = try container.decodeIfPresent(String.self, forKey: .ownerName) { + self.owner = .name(ownerName) + } else { + self.owner = .name("Unknown") + } self.attributes = try container.decode([UniqueGift.Attribute].self, forKey: .attributes) self.availability = try container.decode(UniqueGift.Availability.self, forKey: .availability) } @@ -463,7 +479,14 @@ public enum StarGift: Equatable, Codable, PostboxCoding { self.id = decoder.decodeInt64ForKey(CodingKeys.id.rawValue, orElse: 0) self.title = decoder.decodeStringForKey(CodingKeys.title.rawValue, orElse: "") self.number = decoder.decodeInt32ForKey(CodingKeys.number.rawValue, orElse: 0) - self.ownerPeerId = EnginePeer.Id(namespace: Namespaces.Peer.CloudUser, id: PeerId.Id._internalFromInt64Value(decoder.decodeInt64ForKey(CodingKeys.ownerPeerId.rawValue, orElse: 0))) + self.slug = decoder.decodeStringForKey(CodingKeys.slug.rawValue, orElse: "") + if let ownerId = decoder.decodeOptionalInt64ForKey(CodingKeys.ownerPeerId.rawValue) { + self.owner = .peerId(EnginePeer.Id(namespace: Namespaces.Peer.CloudUser, id: PeerId.Id._internalFromInt64Value(ownerId))) + } else if let ownerName = decoder.decodeOptionalStringForKey(CodingKeys.ownerName.rawValue) { + self.owner = .name(ownerName) + } else { + self.owner = .name("Unknown") + } self.attributes = (try? decoder.decodeObjectArrayWithCustomDecoderForKey(CodingKeys.attributes.rawValue, decoder: { UniqueGift.Attribute(decoder: $0) })) ?? [] self.availability = decoder.decodeObjectForKey(CodingKeys.availability.rawValue, decoder: { UniqueGift.Availability(decoder: $0) }) as! UniqueGift.Availability } @@ -473,7 +496,13 @@ public enum StarGift: Equatable, Codable, PostboxCoding { try container.encode(self.id, forKey: .id) try container.encode(self.title, forKey: .title) try container.encode(self.number, forKey: .number) - try container.encode(self.ownerPeerId.id._internalGetInt64Value(), forKey: .ownerPeerId) + try container.encode(self.slug, forKey: .slug) + switch self.owner { + case let .peerId(peerId): + try container.encode(peerId.id._internalGetInt64Value(), forKey: .ownerPeerId) + case let .name(name): + try container.encode(name, forKey: .ownerName) + } try container.encode(self.attributes, forKey: .attributes) try container.encode(self.availability, forKey: .availability) } @@ -482,7 +511,13 @@ public enum StarGift: Equatable, Codable, PostboxCoding { encoder.encodeInt64(self.id, forKey: CodingKeys.id.rawValue) encoder.encodeString(self.title, forKey: CodingKeys.title.rawValue) encoder.encodeInt32(self.number, forKey: CodingKeys.number.rawValue) - encoder.encodeInt64(self.ownerPeerId.id._internalGetInt64Value(), forKey: CodingKeys.ownerPeerId.rawValue) + encoder.encodeString(self.slug, forKey: CodingKeys.slug.rawValue) + switch self.owner { + case let .peerId(peerId): + encoder.encodeInt64(peerId.id._internalGetInt64Value(), forKey: CodingKeys.ownerPeerId.rawValue) + case let .name(name): + encoder.encodeString(name, forKey: CodingKeys.ownerName.rawValue) + } encoder.encodeObjectArray(self.attributes, forKey: CodingKeys.attributes.rawValue) encoder.encodeObject(self.availability, forKey: CodingKeys.availability.rawValue) } @@ -570,8 +605,16 @@ extension StarGift { return nil } self = .generic(StarGift.Gift(id: id, file: file, price: stars, convertStars: convertStars, availability: availability, soldOut: soldOut, flags: flags, upgradeStars: upgradeStars)) - case let .starGiftUnique(id, title, num, ownerId, attributes, availabilityIssued, availabilityTotal): - self = .unique(StarGift.UniqueGift(id: id, title: title, number: num, ownerPeerId: EnginePeer.Id(namespace: Namespaces.Peer.CloudUser, id: PeerId.Id._internalFromInt64Value(ownerId)), attributes: attributes.compactMap { UniqueGift.Attribute(apiAttribute: $0) }, availability: UniqueGift.Availability(issued: availabilityIssued, total: availabilityTotal))) + case let .starGiftUnique(_, id, title, slug, num, ownerId, ownerName, attributes, availabilityIssued, availabilityTotal): + let owner: StarGift.UniqueGift.Owner + if let ownerId { + owner = .peerId(EnginePeer.Id(namespace: Namespaces.Peer.CloudUser, id: PeerId.Id._internalFromInt64Value(ownerId))) + } else if let ownerName { + owner = .name(ownerName) + } else { + return nil + } + self = .unique(StarGift.UniqueGift(id: id, title: title, number: num, slug: slug, owner: owner, attributes: attributes.compactMap { UniqueGift.Attribute(apiAttribute: $0) }, availability: UniqueGift.Availability(issued: availabilityIssued, total: availabilityTotal))) } } } @@ -1311,3 +1354,29 @@ extension StarGift.UniqueGift.Attribute { } } } + + +func _internal_getUniqueStarGift(account: Account, slug: String) -> Signal { + return account.network.request(Api.functions.payments.getUniqueStarGift(slug: slug)) + |> map(Optional.init) + |> `catch` { _ -> Signal in + return .single(nil) + } + |> mapToSignal { result -> Signal in + if let result = result { + switch result { + case let .uniqueStarGift(gift, users): + return account.postbox.transaction { transaction in + let parsedPeers = AccumulatedPeers(users: users) + updatePeers(transaction: transaction, accountPeerId: account.peerId, peers: parsedPeers) + guard case let .unique(uniqueGift) = StarGift(apiStarGift: gift) else { + return nil + } + return uniqueGift + } + } + } else { + return .single(nil) + } + } +} diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Payments/TelegramEnginePayments.swift b/submodules/TelegramCore/Sources/TelegramEngine/Payments/TelegramEnginePayments.swift index 283a831b99..6ab372a3d2 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Payments/TelegramEnginePayments.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Payments/TelegramEnginePayments.swift @@ -132,5 +132,9 @@ public extension TelegramEngine { public func starGiftUpgradePreview(giftId: Int64) -> Signal<[StarGift.UniqueGift.Attribute], NoError> { return _internal_starGiftUpgradePreview(account: self.account, giftId: giftId) } + + public func getUniqueStarGift(slug: String) -> Signal { + return _internal_getUniqueStarGift(account: self.account, slug: slug) + } } } diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Peers/BotRecomendation.swift b/submodules/TelegramCore/Sources/TelegramEngine/Peers/BotRecomendation.swift new file mode 100644 index 0000000000..2f7d3681cb --- /dev/null +++ b/submodules/TelegramCore/Sources/TelegramEngine/Peers/BotRecomendation.swift @@ -0,0 +1,130 @@ +import Foundation +import Postbox +import SwiftSignalKit +import TelegramApi +import MtProtoKit + +final class CachedRecommendedBots: Codable { + public let peerIds: [EnginePeer.Id] + public let count: Int32 + public let timestamp: Int32? + + public init(peerIds: [EnginePeer.Id], count: Int32, timestamp: Int32?) { + self.peerIds = peerIds + self.count = count + self.timestamp = timestamp + } + + public init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: StringCodingKey.self) + + self.peerIds = try container.decode([Int64].self, forKey: "l").map(EnginePeer.Id.init) + self.count = try container.decodeIfPresent(Int32.self, forKey: "c") ?? 0 + self.timestamp = try container.decodeIfPresent(Int32.self, forKey: "ts") + } + + public func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: StringCodingKey.self) + + try container.encode(self.peerIds.map { $0.toInt64() }, forKey: "l") + try container.encode(self.count, forKey: "c") + try container.encodeIfPresent(self.timestamp, forKey: "ts") + } +} + +private func entryId(peerId: EnginePeer.Id?) -> ItemCacheEntryId { + let cacheKey = ValueBoxKey(length: 8) + if let peerId { + cacheKey.setInt64(0, value: peerId.toInt64()) + } else { + cacheKey.setInt64(0, value: 0) + } + return ItemCacheEntryId(collectionId: Namespaces.CachedItemCollection.recommendedBots, key: cacheKey) +} + +func _internal_requestRecommendedBots(account: Account, peerId: EnginePeer.Id, forceUpdate: Bool) -> Signal { + return account.postbox.transaction { transaction -> (Peer?, Bool) in + guard let user = transaction.getPeer(peerId) as? TelegramUser, let _ = user.botInfo else { + return (nil, false) + } + if let entry = transaction.retrieveItemCacheEntry(id: entryId(peerId: peerId))?.get(CachedRecommendedBots.self), !entry.peerIds.isEmpty && !forceUpdate { + return (nil, false) + } else { + return (user, true) + } + } + |> mapToSignal { user, shouldUpdate in + guard shouldUpdate, let user, let inputUser = apiInputUser(user) else { + return .complete() + } + return account.network.request(Api.functions.bots.getBotRecommendations(bot: inputUser)) + |> retryRequest + |> mapToSignal { result -> Signal in + return account.postbox.transaction { transaction -> [EnginePeer] in + let users: [Api.User] + let parsedPeers: AccumulatedPeers + var count: Int32 + switch result { + case let .users(apiUsers): + users = apiUsers + count = Int32(apiUsers.count) + case let .usersSlice(apiCount, apiUsers): + users = apiUsers + count = apiCount + } + parsedPeers = AccumulatedPeers(users: users) + updatePeers(transaction: transaction, accountPeerId: account.peerId, peers: parsedPeers) + + let peers = users.map { EnginePeer(TelegramUser(user: $0)) } + if let entry = CodableEntry(CachedRecommendedBots(peerIds: peers.map(\.id), count: count, timestamp: Int32(Date().timeIntervalSince1970))) { + transaction.putItemCacheEntry(id: entryId(peerId: peerId), entry: entry) + } + return peers + } + |> ignoreValues + } + } +} + +public struct RecommendedBots: Equatable { + public var bots: [EnginePeer] + public var count: Int32 + + public init(bots: [EnginePeer], count: Int32) { + self.bots = bots + self.count = count + } +} + +func _internal_recommendedBotPeerIds(account: Account, peerId: EnginePeer.Id) -> Signal<[EnginePeer.Id]?, NoError> { + let key = PostboxViewKey.cachedItem(entryId(peerId: peerId)) + return account.postbox.combinedView(keys: [key]) + |> mapToSignal { views -> Signal<[EnginePeer.Id]?, NoError> in + guard let cachedBots = (views.views[key] as? CachedItemView)?.value?.get(CachedRecommendedBots.self), !cachedBots.peerIds.isEmpty else { + return .single(nil) + } + return .single(cachedBots.peerIds) + } +} + +func _internal_recommendedBots(account: Account, peerId: EnginePeer.Id) -> Signal { + let key = PostboxViewKey.cachedItem(entryId(peerId: peerId)) + return account.postbox.combinedView(keys: [key]) + |> mapToSignal { views -> Signal in + guard let cachedBots = (views.views[key] as? CachedItemView)?.value?.get(CachedRecommendedBots.self) else { + return .single(nil) + } + if cachedBots.peerIds.isEmpty { + return .single(nil) + } + return account.postbox.transaction { transaction -> RecommendedBots? in + var bots: [EnginePeer] = [] + for peerId in cachedBots.peerIds { + if let peer = transaction.getPeer(peerId) { + bots.append(EnginePeer(peer)) + } + } + return RecommendedBots(bots: bots, count: cachedBots.count) + } + } +} diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Peers/TelegramEnginePeers.swift b/submodules/TelegramCore/Sources/TelegramEngine/Peers/TelegramEnginePeers.swift index b716f47008..986ec158e2 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Peers/TelegramEnginePeers.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Peers/TelegramEnginePeers.swift @@ -1454,7 +1454,15 @@ public extension TelegramEngine { public func requestRecommendedAppsIfNeeded() -> Signal { return _internal_requestRecommendedApps(account: self.account, forceUpdate: false) } + + public func recommendedBots(peerId: EnginePeer.Id) -> Signal { + return _internal_recommendedBots(account: self.account, peerId: peerId) + } + public func requestRecommendedBots(peerId: EnginePeer.Id, forceUpdate: Bool = false) -> Signal { + return _internal_requestRecommendedBots(account: self.account, peerId: peerId, forceUpdate: forceUpdate) + } + public func isPremiumRequiredToContact(_ peerIds: [EnginePeer.Id]) -> Signal<[EnginePeer.Id], NoError> { return _internal_updateIsPremiumRequiredToContact(account: self.account, peerIds: peerIds) } diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Peers/UpdatePeerInfo.swift b/submodules/TelegramCore/Sources/TelegramEngine/Peers/UpdatePeerInfo.swift index c4ebfb1334..bbb1851742 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Peers/UpdatePeerInfo.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Peers/UpdatePeerInfo.swift @@ -274,7 +274,7 @@ public enum UpdatePeerEmojiStatusError { func _internal_updatePeerEmojiStatus(account: Account, peerId: PeerId, fileId: Int64?, expirationDate: Int32?) -> Signal { return account.postbox.transaction { transaction -> Api.InputChannel? in let updatedStatus = fileId.flatMap { - PeerEmojiStatus(fileId: $0, expirationDate: expirationDate) + PeerEmojiStatus(content: .emoji(fileId: $0), expirationDate: expirationDate) } if let peer = transaction.getPeer(peerId) as? TelegramChannel { updatePeersCustom(transaction: transaction, peers: [peer.withUpdatedEmojiStatus(updatedStatus)], update: { _, updated in updated }) @@ -289,11 +289,11 @@ func _internal_updatePeerEmojiStatus(account: Account, peerId: PeerId, fileId: I } let mappedStatus: Api.EmojiStatus if let fileId = fileId { - if let expirationDate = expirationDate { - mappedStatus = .emojiStatusUntil(documentId: fileId, until: expirationDate) - } else { - mappedStatus = .emojiStatus(documentId: fileId) + var flags: Int32 = 0 + if let _ = expirationDate { + flags |= (1 << 0) } + mappedStatus = .emojiStatus(flags: flags, documentId: fileId, until: expirationDate) } else { mappedStatus = .emojiStatusEmpty } diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Stickers/ImportStickers.swift b/submodules/TelegramCore/Sources/TelegramEngine/Stickers/ImportStickers.swift index f8a8e133b1..e8b8a2ef60 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Stickers/ImportStickers.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Stickers/ImportStickers.swift @@ -83,11 +83,11 @@ func _internal_uploadSticker(account: Account, peer: Peer, resource: MediaResour attributes.append(.documentAttributeVideo(flags: 0, duration: duration, w: dimensions.width, h: dimensions.height, preloadPrefixSize: nil, videoStartTs: nil, videoCodec: nil)) } attributes.append(.documentAttributeImageSize(w: dimensions.width, h: dimensions.height)) - return account.network.request(Api.functions.messages.uploadMedia(flags: 0, businessConnectionId: nil, peer: inputPeer, media: Api.InputMedia.inputMediaUploadedDocument(flags: flags, file: file, thumb: thumbnailFile, mimeType: mimeType, attributes: attributes, stickers: nil, ttlSeconds: nil))) + return account.network.request(Api.functions.messages.uploadMedia(flags: 0, businessConnectionId: nil, peer: inputPeer, media: Api.InputMedia.inputMediaUploadedDocument(flags: flags, file: file, thumb: thumbnailFile, mimeType: mimeType, attributes: attributes, stickers: nil, videoCover: nil, ttlSeconds: nil))) |> mapError { _ -> UploadStickerError in return .generic } |> mapToSignal { media -> Signal in switch media { - case let .messageMediaDocument(_, document, altDocuments, _): + case let .messageMediaDocument(_, document, altDocuments, _, _): if let document = document, let file = telegramMediaFileFromApiDocument(document, altDocuments: altDocuments), let uploadedResource = file.resource as? CloudDocumentMediaResource { account.postbox.mediaBox.copyResourceData(from: resource.id, to: uploadedResource.id, synchronous: true) if let thumbnail, let previewRepresentation = file.previewRepresentations.first(where: { $0.dimensions == PixelDimensions(width: 320, height: 320) }) { diff --git a/submodules/TelegramCore/Sources/WebpagePreview.swift b/submodules/TelegramCore/Sources/WebpagePreview.swift index d1c6064370..1fa282410b 100644 --- a/submodules/TelegramCore/Sources/WebpagePreview.swift +++ b/submodules/TelegramCore/Sources/WebpagePreview.swift @@ -155,8 +155,8 @@ public func webpagePreviewWithProgress(account: Account, urls: [String], webpage } return account.network.requestWithAdditionalInfo(Api.functions.messages.getWebPagePreview(flags: 0, message: urls.joined(separator: " "), entities: nil), info: .progress) - |> `catch` { _ -> Signal, NoError> in - return .single(.result(.messageMediaEmpty)) + |> `catch` { _ -> Signal, NoError> in + return .single(.result(.webPagePreview(media: .messageMediaEmpty, users: []))) } |> mapToSignal { result -> Signal in switch result { @@ -169,36 +169,44 @@ public func webpagePreviewWithProgress(account: Account, urls: [String], webpage return .complete() } case let .result(result): - if let preCachedResources = result.preCachedResources { + if case let .webPagePreview(result, _) = result, let preCachedResources = result.preCachedResources { for (resource, data) in preCachedResources { account.postbox.mediaBox.storeResourceData(resource.id, data: data) } } switch result { - case let .messageMediaWebPage(flags, webpage): - let _ = flags - if let media = telegramMediaWebpageFromApiWebpage(webpage), let url = media.content.url { - if case .Loaded = media.content { - return .single(.result(WebpagePreviewResult.Result(webpage: media, sourceUrl: url))) - } else { - return .single(.result(WebpagePreviewResult.Result(webpage: media, sourceUrl: url))) - |> then( - account.stateManager.updatedWebpage(media.webpageId) - |> take(1) - |> map { next -> WebpagePreviewWithProgressResult in - if let url = next.content.url { - return .result(WebpagePreviewResult.Result(webpage: next, sourceUrl: url)) - } else { - return .result(nil) - } + case let .webPagePreview(media, users): + switch media { + case let .messageMediaWebPage(_, webpage): + return account.postbox.transaction { transaction -> Signal in + let peers = AccumulatedPeers(users: users) + updatePeers(transaction: transaction, accountPeerId: account.peerId, peers: peers) + + if let media = telegramMediaWebpageFromApiWebpage(webpage), let url = media.content.url { + if case .Loaded = media.content { + return .single(.result(WebpagePreviewResult.Result(webpage: media, sourceUrl: url))) + } else { + return .single(.result(WebpagePreviewResult.Result(webpage: media, sourceUrl: url))) + |> then( + account.stateManager.updatedWebpage(media.webpageId) + |> take(1) + |> map { next -> WebpagePreviewWithProgressResult in + if let url = next.content.url { + return .result(WebpagePreviewResult.Result(webpage: next, sourceUrl: url)) + } else { + return .result(nil) + } + } + ) } - ) + } else { + return .single(.result(nil)) + } } - } else { + |> switchToLatest + default: return .single(.result(nil)) } - default: - return .single(.result(nil)) } } } diff --git a/submodules/TelegramPresentationData/Sources/NumericFormat.swift b/submodules/TelegramPresentationData/Sources/NumericFormat.swift index b3f03bea71..cc556add44 100644 --- a/submodules/TelegramPresentationData/Sources/NumericFormat.swift +++ b/submodules/TelegramPresentationData/Sources/NumericFormat.swift @@ -2,17 +2,17 @@ import Foundation import PresentationStrings import TelegramCore -public func compactNumericCountString(_ count: Int, decimalSeparator: String = ".") -> String { +public func compactNumericCountString(_ count: Int, decimalSeparator: String = ".", showDecimalPart: Bool = true) -> String { if count >= 1000 * 1000 { let remainder = (count % (1000 * 1000)) / (1000 * 100) - if remainder != 0 { + if remainder != 0 && showDecimalPart { return "\(count / (1000 * 1000))\(decimalSeparator)\(remainder)M" } else { return "\(count / (1000 * 1000))M" } } else if count >= 1000 { let remainder = (count % (1000)) / (100) - if remainder != 0 { + if remainder != 0 && showDecimalPart { return "\(count / 1000)\(decimalSeparator)\(remainder)K" } else { return "\(count / 1000)K" diff --git a/submodules/TelegramUI/Components/ButtonComponent/Sources/ButtonComponent.swift b/submodules/TelegramUI/Components/ButtonComponent/Sources/ButtonComponent.swift index 606d218025..4ae316e7b1 100644 --- a/submodules/TelegramUI/Components/ButtonComponent/Sources/ButtonComponent.swift +++ b/submodules/TelegramUI/Components/ButtonComponent/Sources/ButtonComponent.swift @@ -357,6 +357,16 @@ public final class ButtonComponent: Component { self.cornerRadius = cornerRadius self.isShimmering = isShimmering } + + public func withIsShimmering(_ isShimmering: Bool) -> Background { + return Background( + color: self.color, + foreground: self.foreground, + pressedColor: self.pressedColor, + cornerRadius: self.cornerRadius, + isShimmering: isShimmering + ) + } } public let background: Background diff --git a/submodules/TelegramUI/Components/CameraScreen/Sources/CameraCodeResultComponent.swift b/submodules/TelegramUI/Components/CameraScreen/Sources/CameraCodeResultComponent.swift index a3b2b29885..11f953f24d 100644 --- a/submodules/TelegramUI/Components/CameraScreen/Sources/CameraCodeResultComponent.swift +++ b/submodules/TelegramUI/Components/CameraScreen/Sources/CameraCodeResultComponent.swift @@ -189,8 +189,7 @@ final class CameraCodeResultComponent: Component { view.frame = CGRect(origin: CGPoint(x: 54.0, y: 9.0), size: titleSize) } - //TODO:localize - let subtitleString = NSMutableAttributedString(string: "Open Chat >", font: Font.regular(15.0), textColor: UIColor(rgb: 0xffffff, alpha: 0.7)) + let subtitleString = NSMutableAttributedString(string: "\(presentationData.strings.Camera_OpenChat) >", font: Font.regular(15.0), textColor: UIColor(rgb: 0xffffff, alpha: 0.7)) if let range = subtitleString.string.range(of: ">"), let arrowImage = UIImage(bundleImageName: "Item List/InlineTextRightArrow") { subtitleString.addAttribute(.attachment, value: arrowImage, range: NSRange(range, in: subtitleString.string)) subtitleString.addAttribute(.baselineOffset, value: 1.0, range: NSRange(range, in: subtitleString.string)) diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageAttachedContentNode/Sources/ChatMessageAttachedContentNode.swift b/submodules/TelegramUI/Components/Chat/ChatMessageAttachedContentNode/Sources/ChatMessageAttachedContentNode.swift index 21b8b81a5d..798e63e8f5 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessageAttachedContentNode/Sources/ChatMessageAttachedContentNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessageAttachedContentNode/Sources/ChatMessageAttachedContentNode.swift @@ -315,7 +315,7 @@ public final class ChatMessageAttachedContentNode: ASDisplayNode { var mediaAndFlags = mediaAndFlags if let mediaAndFlagsValue = mediaAndFlags { - if mediaAndFlagsValue.0.first is TelegramMediaStory || mediaAndFlagsValue.0.first is WallpaperPreviewMedia { + if mediaAndFlagsValue.0.first is TelegramMediaStory || mediaAndFlagsValue.0.first is WallpaperPreviewMedia || mediaAndFlagsValue.0.first is UniqueGiftPreviewMedia { var flags = mediaAndFlagsValue.1 flags.remove(.preferMediaInline) mediaAndFlags = (mediaAndFlagsValue.0, flags) @@ -374,13 +374,15 @@ public final class ChatMessageAttachedContentNode: ASDisplayNode { contentMediaAspectFilled = true } } - } else if let _ = media as? TelegramMediaImage { + } else if media is TelegramMediaImage { contentMediaValue = media - } else if let _ = media as? TelegramMediaWebFile { + } else if media is TelegramMediaWebFile { contentMediaValue = media - } else if let _ = media as? WallpaperPreviewMedia { + } else if media is WallpaperPreviewMedia { contentMediaValue = media - } else if let _ = media as? TelegramMediaStory { + } else if media is TelegramMediaStory { + contentMediaValue = media + } else if media is UniqueGiftPreviewMedia { contentMediaValue = media } } diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageDateAndStatusNode/Sources/ChatMessageDateAndStatusNode.swift b/submodules/TelegramUI/Components/Chat/ChatMessageDateAndStatusNode/Sources/ChatMessageDateAndStatusNode.swift index f5510b8649..c1212c0a0e 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessageDateAndStatusNode/Sources/ChatMessageDateAndStatusNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessageDateAndStatusNode/Sources/ChatMessageDateAndStatusNode.swift @@ -356,6 +356,7 @@ public class ChatMessageDateAndStatusNode: ASDisplayNode { selectedBackground: themeColors.reactionActiveBackground.argb, deselectedForeground: themeColors.reactionInactiveForeground.argb, selectedForeground: themeColors.reactionActiveForeground.argb, + selectedIconTintColor: 0, deselectedStarsBackground: themeColors.reactionStarsInactiveBackground.argb, selectedStarsBackground: themeColors.reactionStarsActiveBackground.argb, deselectedStarsForeground: themeColors.reactionStarsInactiveForeground.argb, @@ -374,6 +375,7 @@ public class ChatMessageDateAndStatusNode: ASDisplayNode { selectedBackground: themeColors.reactionActiveBackground.argb, deselectedForeground: themeColors.reactionInactiveForeground.argb, selectedForeground: themeColors.reactionActiveForeground.argb, + selectedIconTintColor: 0, deselectedStarsBackground: themeColors.reactionStarsInactiveBackground.argb, selectedStarsBackground: themeColors.reactionStarsActiveBackground.argb, deselectedStarsForeground: themeColors.reactionStarsInactiveForeground.argb, diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageGiftBubbleContentNode/Sources/ChatMessageGiftBubbleContentNode.swift b/submodules/TelegramUI/Components/Chat/ChatMessageGiftBubbleContentNode/Sources/ChatMessageGiftBubbleContentNode.swift index 90b5fd5d0d..11569668ea 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessageGiftBubbleContentNode/Sources/ChatMessageGiftBubbleContentNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessageGiftBubbleContentNode/Sources/ChatMessageGiftBubbleContentNode.swift @@ -59,6 +59,7 @@ public class ChatMessageGiftBubbleContentNode: ChatMessageBubbleContentNode { private var shimmerEffectNode: ShimmerEffectForegroundNode? private let buttonNode: HighlightTrackingButtonNode private let buttonStarsNode: PremiumStarsNode + private let buttonContentNode: ASDisplayNode private let buttonTitleNode: TextNode private var buttonIconNode: DefaultAnimatedStickerNodeImpl? @@ -162,8 +163,10 @@ public class ChatMessageGiftBubbleContentNode: ChatMessageBubbleContentNode { self.buttonStarsNode = PremiumStarsNode() + self.buttonContentNode = ASDisplayNode() + self.buttonContentNode.isUserInteractionEnabled = false + self.buttonTitleNode = TextNode() - self.buttonTitleNode.isUserInteractionEnabled = false self.buttonTitleNode.displaysAsynchronously = false self.ribbonBackgroundNode = ASImageNode() @@ -190,7 +193,9 @@ public class ChatMessageGiftBubbleContentNode: ChatMessageBubbleContentNode { self.addSubnode(self.buttonNode) self.buttonNode.addSubnode(self.buttonStarsNode) - self.buttonNode.addSubnode(self.buttonTitleNode) + self.buttonNode.addSubnode(self.buttonContentNode) + + self.buttonContentNode.addSubnode(self.buttonTitleNode) self.addSubnode(self.ribbonBackgroundNode) self.addSubnode(self.ribbonTextNode) @@ -361,7 +366,6 @@ public class ChatMessageGiftBubbleContentNode: ChatMessageBubbleContentNode { var buttonTitle = item.presentationData.strings.Notification_PremiumGift_View var buttonIcon: String? var ribbonTitle = "" - var hasServiceMessage = true var textSpacing: CGFloat = 0.0 var isStarGift = false @@ -376,6 +380,9 @@ public class ChatMessageGiftBubbleContentNode: ChatMessageBubbleContentNode { var uniquePatternColor: UIColor? var uniquePatternFile: TelegramMediaFile? + let isStoryEntity = item.message.id.id == -1 + var hasServiceMessage = !isStoryEntity + for media in item.message.media { if let action = media as? TelegramMediaAction { switch action.action { @@ -546,15 +553,19 @@ public class ChatMessageGiftBubbleContentNode: ChatMessageBubbleContentNode { if case let .unique(uniqueGift) = gift { isStarGift = true let authorName: String - if isUpgrade && item.message.author?.id == item.context.account.peerId { - authorName = item.message.peers[item.message.id.peerId].flatMap { EnginePeer($0) }?.compactDisplayTitle ?? "" + if isUpgrade { + if item.message.author?.id == item.context.account.peerId { + authorName = item.message.peers[item.message.id.peerId].flatMap { EnginePeer($0) }?.compactDisplayTitle ?? "" + } else { + authorName = item.associatedData.accountPeer?.compactDisplayTitle ?? "" + } } else { authorName = item.message.author.flatMap { EnginePeer($0) }?.compactDisplayTitle ?? "" } - title = item.presentationData.strings.Notification_StarGift_Title(authorName).string - text = "**\(uniqueGift.title) #\(uniqueGift.number)**" - ribbonTitle = item.presentationData.strings.Notification_StarGift_Gift - buttonTitle = item.presentationData.strings.Notification_StarGift_View + title = isStoryEntity ? uniqueGift.title : item.presentationData.strings.Notification_StarGift_Title(authorName).string + text = isStoryEntity ? "**Collectible #\(uniqueGift.number)**" : "**\(uniqueGift.title) #\(uniqueGift.number)**" + ribbonTitle = isStoryEntity ? "" : item.presentationData.strings.Notification_StarGift_Gift + buttonTitle = isStoryEntity ? "" : item.presentationData.strings.Notification_StarGift_View modelTitle = item.presentationData.strings.Notification_StarGift_Model backdropTitle = item.presentationData.strings.Notification_StarGift_Backdrop symbolTitle = item.presentationData.strings.Notification_StarGift_Symbol @@ -688,6 +699,8 @@ public class ChatMessageGiftBubbleContentNode: ChatMessageBubbleContentNode { if !buttonTitle.isEmpty { giftSize.height += 48.0 + } else if isStoryEntity { + giftSize.height += 12.0 } var labelRects = labelLayout.linesRects() @@ -745,9 +758,9 @@ public class ChatMessageGiftBubbleContentNode: ChatMessageBubbleContentNode { } } - let overlayColor = item.presentationData.theme.theme.overallDarkAppearance ? UIColor(rgb: 0xffffff, alpha: 0.12) : UIColor(rgb: 0x000000, alpha: 0.12) + let overlayColor = item.presentationData.theme.theme.overallDarkAppearance && uniquePatternFile == nil ? UIColor(rgb: 0xffffff, alpha: 0.12) : UIColor(rgb: 0x000000, alpha: 0.12) - let imageFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((backgroundSize.width - giftSize.width) / 2.0), y: hasServiceMessage ? labelLayout.size.height + 16.0 : 0.0), size: giftSize) + let imageFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((backgroundSize.width - giftSize.width) / 2.0), y: hasServiceMessage ? labelLayout.size.height + 12.0 : 0.0), size: giftSize) let mediaBackgroundFrame = imageFrame.insetBy(dx: -2.0, dy: -2.0) var iconSize = CGSize(width: 160.0, height: 160.0) @@ -758,11 +771,12 @@ public class ChatMessageGiftBubbleContentNode: ChatMessageBubbleContentNode { } let animationFrame = CGRect(origin: CGPoint(x: mediaBackgroundFrame.minX + floorToScreenPixels((mediaBackgroundFrame.width - iconSize.width) / 2.0), y: mediaBackgroundFrame.minY - 16.0 + iconOffset), size: iconSize) strongSelf.animationNode.frame = animationFrame + strongSelf.animationNode.isHidden = isStoryEntity strongSelf.buttonNode.isHidden = buttonTitle.isEmpty strongSelf.buttonTitleNode.isHidden = buttonTitle.isEmpty - if strongSelf.item == nil { + if strongSelf.item == nil && !isStoryEntity { strongSelf.animationNode.started = { [weak self] in if let strongSelf = self { let current = CACurrentMediaTime() @@ -874,63 +888,78 @@ public class ChatMessageGiftBubbleContentNode: ChatMessageBubbleContentNode { strongSelf.dustNode = nil } - var middleX = mediaBackgroundFrame.width / 2.0 - if let (modelValueLayout, _) = modelValueLayoutAndApply, let (backdropValueLayout, _) = backdropValueLayoutAndApply, let (symbolValueLayout, _) = symbolValueLayoutAndApply { - let maxWidth = max(modelValueLayout.size.width, max(backdropValueLayout.size.width, symbolValueLayout.size.width)) - middleX = min(mediaBackgroundFrame.width - maxWidth - 16.0, middleX) + let attributeSpacing: CGFloat = 6.0 + let attributeVerticalSpacing: CGFloat = 22.0 + var attributeMidpoints: [CGFloat] = [] + + func appendAttributeMidpoint(titleLayout: TextNodeLayout?, valueLayout: TextNodeLayout?) { + if let titleLayout, let valueLayout { + let totalWidth = titleLayout.size.width + attributeSpacing + valueLayout.size.width + let titleOffset = titleLayout.size.width + attributeSpacing / 2.0 + let midpoint = (mediaBackgroundFrame.width - totalWidth) / 2.0 + titleOffset + attributeMidpoints.append(midpoint) + } } - - let titleMaxX: CGFloat = mediaBackgroundFrame.minX + middleX - 2.0 - let valueMinX: CGFloat = mediaBackgroundFrame.minX + middleX + 3.0 + appendAttributeMidpoint(titleLayout: modelTitleLayoutAndApply?.0, valueLayout: modelValueLayoutAndApply?.0) + appendAttributeMidpoint(titleLayout: backdropTitleLayoutAndApply?.0, valueLayout: backdropValueLayoutAndApply?.0) + appendAttributeMidpoint(titleLayout: symbolTitleLayoutAndApply?.0, valueLayout: symbolValueLayoutAndApply?.0) + + let middleX = attributeMidpoints.isEmpty ? mediaBackgroundFrame.width / 2.0 : attributeMidpoints.reduce(0, +) / CGFloat(attributeMidpoints.count) + + let titleMaxX: CGFloat = mediaBackgroundFrame.minX + middleX - attributeSpacing / 2.0 + let valueMinX: CGFloat = mediaBackgroundFrame.minX + middleX + attributeSpacing / 2.0 - if let (modelTitleLayout, modelTitleApply) = modelTitleLayoutAndApply { - if strongSelf.modelTitleTextNode.supernode == nil { - strongSelf.addSubnode(strongSelf.modelTitleTextNode) + func positionAttributeNodes( + titleTextNode: TextNode, + valueTextNode: TextNode, + titleLayoutAndApply: (TextNodeLayout, () -> TextNode)?, + valueLayoutAndApply: (TextNodeLayout, () -> TextNode)?, + yOffset: CGFloat + ) { + if let (titleLayout, titleApply) = titleLayoutAndApply { + if titleTextNode.supernode == nil { + strongSelf.addSubnode(titleTextNode) + } + let _ = titleApply() + titleTextNode.frame = CGRect( + origin: CGPoint(x: titleMaxX - titleLayout.size.width, y: clippingTextFrame.maxY + yOffset), + size: titleLayout.size + ) + } + if let (valueLayout, valueApply) = valueLayoutAndApply { + if valueTextNode.supernode == nil { + strongSelf.addSubnode(valueTextNode) + } + let _ = valueApply() + valueTextNode.frame = CGRect( + origin: CGPoint(x: valueMinX, y: clippingTextFrame.maxY + yOffset), + size: valueLayout.size + ) } - let _ = modelTitleApply() - strongSelf.modelTitleTextNode.frame = CGRect(origin: CGPoint(x: titleMaxX - modelTitleLayout.size.width, y: clippingTextFrame.maxY + 10.0), size: modelTitleLayout.size) - } - - if let (modelValueLayout, modelValueApply) = modelValueLayoutAndApply { - if strongSelf.modelValueTextNode.supernode == nil { - strongSelf.addSubnode(strongSelf.modelValueTextNode) - } - let _ = modelValueApply() - strongSelf.modelValueTextNode.frame = CGRect(origin: CGPoint(x: valueMinX, y: clippingTextFrame.maxY + 10.0), size: modelValueLayout.size) - } - - if let (backdropTitleLayout, backdropTitleApply) = backdropTitleLayoutAndApply { - if strongSelf.backdropTitleTextNode.supernode == nil { - strongSelf.addSubnode(strongSelf.backdropTitleTextNode) - } - let _ = backdropTitleApply() - strongSelf.backdropTitleTextNode.frame = CGRect(origin: CGPoint(x: titleMaxX - backdropTitleLayout.size.width, y: clippingTextFrame.maxY + 32.0), size: backdropTitleLayout.size) - } - - if let (backdropValueLayout, backdropValueApply) = backdropValueLayoutAndApply { - if strongSelf.backdropValueTextNode.supernode == nil { - strongSelf.addSubnode(strongSelf.backdropValueTextNode) - } - let _ = backdropValueApply() - strongSelf.backdropValueTextNode.frame = CGRect(origin: CGPoint(x: valueMinX, y: clippingTextFrame.maxY + 32.0), size: backdropValueLayout.size) - } - - if let (symbolTitleLayout, symbolTitleApply) = symbolTitleLayoutAndApply { - if strongSelf.symbolTitleTextNode.supernode == nil { - strongSelf.addSubnode(strongSelf.symbolTitleTextNode) - } - let _ = symbolTitleApply() - strongSelf.symbolTitleTextNode.frame = CGRect(origin: CGPoint(x: titleMaxX - symbolTitleLayout.size.width, y: clippingTextFrame.maxY + 54.0), size: symbolTitleLayout.size) - } - - if let (symbolValueLayout, symbolValueApply) = symbolValueLayoutAndApply { - if strongSelf.symbolValueTextNode.supernode == nil { - strongSelf.addSubnode(strongSelf.symbolValueTextNode) - } - let _ = symbolValueApply() - strongSelf.symbolValueTextNode.frame = CGRect(origin: CGPoint(x: valueMinX, y: clippingTextFrame.maxY + 54.0), size: symbolValueLayout.size) } + positionAttributeNodes( + titleTextNode: strongSelf.modelTitleTextNode, + valueTextNode: strongSelf.modelValueTextNode, + titleLayoutAndApply: modelTitleLayoutAndApply, + valueLayoutAndApply: modelValueLayoutAndApply, + yOffset: 10.0 + ) + positionAttributeNodes( + titleTextNode: strongSelf.backdropTitleTextNode, + valueTextNode: strongSelf.backdropValueTextNode, + titleLayoutAndApply: backdropTitleLayoutAndApply, + valueLayoutAndApply: backdropValueLayoutAndApply, + yOffset: 10.0 + attributeVerticalSpacing + ) + positionAttributeNodes( + titleTextNode: strongSelf.symbolTitleTextNode, + valueTextNode: strongSelf.symbolValueTextNode, + titleLayoutAndApply: symbolTitleLayoutAndApply, + valueLayoutAndApply: symbolValueLayoutAndApply, + yOffset: 10.0 + attributeVerticalSpacing * 2 + ) + var buttonSize = CGSize(width: buttonTitleLayout.size.width + 38.0, height: 34.0) var buttonOriginY = clippingTextFrame.maxY + 10.0 if modelTitleLayoutAndApply != nil { @@ -945,9 +974,21 @@ public class ChatMessageGiftBubbleContentNode: ChatMessageBubbleContentNode { if let current = strongSelf.buttonIconNode { buttonIconNode = current } else { + if animation.isAnimated { + if let snapshotView = strongSelf.buttonContentNode.view.snapshotView(afterScreenUpdates: false) { + snapshotView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { _ in + snapshotView.removeFromSuperview() + }) + snapshotView.layer.animateScale(from: 1.0, to: 0.1, duration: 0.2, removeOnCompletion: false) + strongSelf.buttonNode.view.addSubview(snapshotView) + } + strongSelf.buttonContentNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) + strongSelf.buttonContentNode.layer.animateScale(from: 0.1, to: 1.0, duration: 0.2) + } + buttonIconNode = DefaultAnimatedStickerNodeImpl() buttonIconNode.setup(source: AnimatedStickerNodeLocalFileSource(name: buttonIcon), width: 60, height: 60, playbackMode: .loop, mode: .direct(cachePathPrefix: nil)) - strongSelf.buttonNode.addSubnode(buttonIconNode) + strongSelf.buttonContentNode.addSubnode(buttonIconNode) strongSelf.buttonIconNode = buttonIconNode buttonIconNode.playLoop() } @@ -956,10 +997,26 @@ public class ChatMessageGiftBubbleContentNode: ChatMessageBubbleContentNode { buttonIconNode.updateLayout(size: iconSize) buttonIconNode.visibility = strongSelf.visibilityStatus == true buttonIconNode.dynamicColor = primaryTextColor + } else if let buttonIconNode = strongSelf.buttonIconNode { + if animation.isAnimated { + if let snapshotView = strongSelf.buttonContentNode.view.snapshotView(afterScreenUpdates: false) { + snapshotView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { _ in + snapshotView.removeFromSuperview() + }) + snapshotView.layer.animateScale(from: 1.0, to: 0.1, duration: 0.2, removeOnCompletion: false) + strongSelf.buttonNode.view.addSubview(snapshotView) + } + strongSelf.buttonContentNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) + strongSelf.buttonContentNode.layer.animateScale(from: 0.1, to: 1.0, duration: 0.2) + } + + strongSelf.buttonIconNode = nil + buttonIconNode.removeFromSupernode() } animation.animator.updateFrame(layer: strongSelf.buttonNode.layer, frame: CGRect(origin: CGPoint(x: mediaBackgroundFrame.minX + floorToScreenPixels((mediaBackgroundFrame.width - buttonSize.width) / 2.0), y: buttonOriginY), size: buttonSize), completion: nil) strongSelf.buttonStarsNode.frame = CGRect(origin: .zero, size: buttonSize) + strongSelf.buttonContentNode.frame = CGRect(origin: .zero, size: buttonSize) if ribbonTextLayout.size.width > 0.0 { if strongSelf.ribbonBackgroundNode.image == nil { @@ -1036,7 +1093,7 @@ public class ChatMessageGiftBubbleContentNode: ChatMessageBubbleContentNode { subject: .custom(uniqueBackgroundColor, uniqueSecondBackgroundColor, uniquePatternColor, uniquePatternFile.fileId.id), files: files, isDark: false, - avatarCenter: CGPoint(x: patternSize.width / 2.0, y: 106.0), + avatarCenter: CGPoint(x: patternSize.width / 2.0, y: 104.0), avatarScale: 1.0, defaultHeight: patternSize.height, avatarTransitionFraction: 0.0, diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageInteractiveMediaNode/BUILD b/submodules/TelegramUI/Components/Chat/ChatMessageInteractiveMediaNode/BUILD index 0803f2a11f..660c84d84c 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessageInteractiveMediaNode/BUILD +++ b/submodules/TelegramUI/Components/Chat/ChatMessageInteractiveMediaNode/BUILD @@ -14,6 +14,7 @@ swift_library( "//submodules/Postbox", "//submodules/SSignalKit/SwiftSignalKit", "//submodules/Display", + "//submodules/ComponentFlow", "//submodules/TelegramCore", "//submodules/TelegramPresentationData", "//submodules/TelegramUIPreferences", @@ -40,6 +41,7 @@ swift_library( "//submodules/TelegramUI/Components/Chat/ChatMessageItemCommon", "//submodules/TelegramUI/Components/WallpaperPreviewMedia", "//submodules/TelegramUI/Components/TextNodeWithEntities", + "//submodules/TelegramUI/Components/Gifts/GiftItemComponent", "//submodules/Utils/RangeSet", ], visibility = [ diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageInteractiveMediaNode/Sources/ChatMessageInteractiveMediaNode.swift b/submodules/TelegramUI/Components/Chat/ChatMessageInteractiveMediaNode/Sources/ChatMessageInteractiveMediaNode.swift index 26da378aa8..bcdf9bc7e1 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessageInteractiveMediaNode/Sources/ChatMessageInteractiveMediaNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessageInteractiveMediaNode/Sources/ChatMessageInteractiveMediaNode.swift @@ -4,6 +4,7 @@ import AsyncDisplayKit import Postbox import SwiftSignalKit import Display +import ComponentFlow import TelegramCore import TelegramPresentationData import TelegramUIPreferences @@ -31,6 +32,7 @@ import ChatMessageItemCommon import WallpaperPreviewMedia import TextNodeWithEntities import RangeSet +import GiftItemComponent private struct FetchControls { let fetch: (Bool) -> Void @@ -433,6 +435,7 @@ public final class ChatMessageInteractiveMediaNode: ASDisplayNode, GalleryItemTr private var videoNode: UniversalVideoNode? private var videoContent: UniversalVideoContent? private var animatedStickerNode: AnimatedStickerNode? + private var giftView: ComponentView? private var statusNode: RadialStatusNode? public var videoNodeDecoration: ChatBubbleVideoDecoration? public var decoration: UniversalVideoDecoration? { @@ -833,6 +836,7 @@ public final class ChatMessageInteractiveMediaNode: ASDisplayNode, GalleryItemTr var maxDimensions = layoutConstants.image.maxDimensions var maxHeight = layoutConstants.image.maxDimensions.height var isStory = false + var isGift = false let _ = isStory @@ -907,6 +911,9 @@ public final class ChatMessageInteractiveMediaNode: ASDisplayNode, GalleryItemTr case .color, .gradient, .emoticon: unboundSize = CGSize(width: 128.0, height: 128.0) } + } else if let _ = media as? UniqueGiftPreviewMedia { + isGift = true + unboundSize = CGSize(width: 200.0, height: 200.0) } else { var extendedMedia: TelegramExtendedMedia? if let invoice = media as? TelegramMediaInvoice, let selectedMedia = invoice.extendedMedia { @@ -960,7 +967,7 @@ public final class ChatMessageInteractiveMediaNode: ASDisplayNode, GalleryItemTr switch sizeCalculation { case let .constrained(constrainedSize): - if isSticker { + if isSticker || isGift { nativeSize = unboundSize.aspectFittedOrSmaller(constrainedSize) } else { var constrainedSize = constrainedSize @@ -1691,6 +1698,8 @@ public final class ChatMessageInteractiveMediaNode: ASDisplayNode, GalleryItemTr case .themeSettings, .color, .gradient, .image, .emoticon: updatedStatusSignal = .single((.Local, nil)) } + } else if let _ = media as? UniqueGiftPreviewMedia { + updatedStatusSignal = .single((.Local, nil)) } } @@ -1887,7 +1896,6 @@ public final class ChatMessageInteractiveMediaNode: ASDisplayNode, GalleryItemTr } } - if message.attributes.contains(where: { $0 is MediaSpoilerMessageAttribute }), strongSelf.extendedMediaOverlayNode == nil { strongSelf.internallyVisible = false } @@ -1924,6 +1932,40 @@ public final class ChatMessageInteractiveMediaNode: ASDisplayNode, GalleryItemTr animatedStickerNode.updateLayout(size: imageFrame.size) } + if let giftPreview = media as? UniqueGiftPreviewMedia, let gift = giftPreview.content { + let giftView: ComponentView + if let current = strongSelf.giftView { + giftView = current + } else { + giftView = ComponentView() + strongSelf.giftView = giftView + } + + let _ = giftView.update( + transition: .immediate, + component: AnyComponent( + GiftItemComponent( + context: context, + theme: presentationData.theme.theme, + subject: .uniqueGift(gift: gift), + mode: .preview + ) + ), + environment: {}, + containerSize: imageFrame.size + ) + + if let giftView = giftView.view { + if giftView.superview == nil { + strongSelf.pinchContainerNode.contentNode.view.addSubview(giftView) + } + giftView.frame = imageFrame + } + } else if let giftView = strongSelf.giftView { + strongSelf.giftView = nil + giftView.view?.removeFromSuperview() + } + if let updateImageSignal = updateImageSignal { strongSelf.imageNode.captureProtected = message.isCopyProtected() || isExtendedMedia strongSelf.imageNode.setSignal(updateImageSignal(synchronousLoads, false), attemptSynchronously: synchronousLoads) diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageReactionsFooterContentNode/Sources/ChatMessageReactionsFooterContentNode.swift b/submodules/TelegramUI/Components/Chat/ChatMessageReactionsFooterContentNode/Sources/ChatMessageReactionsFooterContentNode.swift index 38f55f1297..a01d02ed29 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessageReactionsFooterContentNode/Sources/ChatMessageReactionsFooterContentNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessageReactionsFooterContentNode/Sources/ChatMessageReactionsFooterContentNode.swift @@ -78,6 +78,7 @@ public final class MessageReactionButtonsNode: ASDisplayNode { selectedBackground: themeColors.reactionActiveBackground.argb, deselectedForeground: themeColors.reactionInactiveForeground.argb, selectedForeground: themeColors.reactionActiveForeground.argb, + selectedIconTintColor: 0, deselectedStarsBackground: themeColors.reactionStarsInactiveBackground.argb, selectedStarsBackground: themeColors.reactionStarsActiveBackground.argb, deselectedStarsForeground: themeColors.reactionStarsInactiveForeground.argb, @@ -95,6 +96,7 @@ public final class MessageReactionButtonsNode: ASDisplayNode { selectedBackground: themeColors.reactionActiveBackground.argb, deselectedForeground: themeColors.reactionInactiveForeground.argb, selectedForeground: themeColors.reactionActiveForeground.argb, + selectedIconTintColor: 0, deselectedStarsBackground: themeColors.reactionStarsInactiveBackground.argb, selectedStarsBackground: themeColors.reactionStarsActiveBackground.argb, deselectedStarsForeground: themeColors.reactionStarsInactiveForeground.argb, @@ -117,6 +119,7 @@ public final class MessageReactionButtonsNode: ASDisplayNode { selectedBackground: themeColors.reactionActiveBackground.argb, deselectedForeground: themeColors.reactionInactiveForeground.argb, selectedForeground: themeColors.reactionActiveForeground.argb, + selectedIconTintColor: presentationData.theme.theme.overallDarkAppearance ? 0 : presentationData.theme.theme.chat.message.incoming.accentTextColor.argb, deselectedStarsBackground: selectReactionFillStaticColor(theme: presentationData.theme.theme, wallpaper: presentationData.theme.wallpaper, isStars: true).argb, selectedStarsBackground: themeColors.reactionStarsActiveBackground.argb, deselectedStarsForeground: themeColors.reactionStarsInactiveForeground.argb, diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageWebpageBubbleContentNode/Sources/ChatMessageWebpageBubbleContentNode.swift b/submodules/TelegramUI/Components/Chat/ChatMessageWebpageBubbleContentNode/Sources/ChatMessageWebpageBubbleContentNode.swift index ba57a1641a..ab53dd68ed 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessageWebpageBubbleContentNode/Sources/ChatMessageWebpageBubbleContentNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessageWebpageBubbleContentNode/Sources/ChatMessageWebpageBubbleContentNode.swift @@ -380,6 +380,14 @@ public final class ChatMessageWebpageBubbleContentNode: ChatMessageBubbleContent let media = WallpaperPreviewMedia(content: .themeSettings(settings)) mediaAndFlags = ([media], ChatMessageAttachedContentNodeMediaFlags()) } + } else if type == "telegram_nft" { + for attribute in webpage.attributes { + if case let .starGift(gift) = attribute, case let .unique(uniqueGift) = gift.gift { + let media = UniqueGiftPreviewMedia(content: uniqueGift) + mediaAndFlags = ([media], []) + break + } + } } } @@ -457,6 +465,10 @@ public final class ChatMessageWebpageBubbleContentNode: ChatMessageBubbleContent } } actionTitle = isEmoji ? item.presentationData.strings.Conversation_ViewEmojis : item.presentationData.strings.Conversation_ViewStickers + case "telegram_nft": + actionTitle = item.presentationData.strings.Conversation_ViewStarGift + text = nil + entities = nil default: break } diff --git a/submodules/TelegramUI/Components/Chat/ChatRecentActionsController/Sources/ChatRecentActionsControllerNode.swift b/submodules/TelegramUI/Components/Chat/ChatRecentActionsController/Sources/ChatRecentActionsControllerNode.swift index a87dbf9b27..ba5c035ffe 100644 --- a/submodules/TelegramUI/Components/Chat/ChatRecentActionsController/Sources/ChatRecentActionsControllerNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatRecentActionsController/Sources/ChatRecentActionsControllerNode.swift @@ -1381,6 +1381,8 @@ final class ChatRecentActionsControllerNode: ViewControllerTracingNode { break case .premiumMultiGift: break + case .collectible: + break case .messageLink: break } diff --git a/submodules/TelegramUI/Components/ChatTitleView/Sources/ChatTitleView.swift b/submodules/TelegramUI/Components/ChatTitleView/Sources/ChatTitleView.swift index 00f6f152a7..b9fad025fe 100644 --- a/submodules/TelegramUI/Components/ChatTitleView/Sources/ChatTitleView.swift +++ b/submodules/TelegramUI/Components/ChatTitleView/Sources/ChatTitleView.swift @@ -284,7 +284,7 @@ public final class ChatTitleView: UIView, NavigationBarTitleView { titleCredibilityIcon = .verified } if let verificationIconFileId = peer.verificationIconFileId { - titleVerifiedIcon = .emojiStatus(PeerEmojiStatus(fileId: verificationIconFileId, expirationDate: nil)) + titleVerifiedIcon = .emojiStatus(PeerEmojiStatus(content: .emoji(fileId: verificationIconFileId), expirationDate: nil)) } } } @@ -950,13 +950,12 @@ public final class ChatTitleView: UIView, NavigationBarTitleView { titleTransition = .immediate } - let iconSpacing: CGFloat = 2.0 let titleSideInset: CGFloat = 6.0 var titleFrame: CGRect if size.height > 40.0 { var titleInsets: UIEdgeInsets = .zero if case .emojiStatus = self.titleVerifiedIcon, verifiedIconWidth > 0.0 { - titleInsets.left = verifiedIconWidth + iconSpacing + titleInsets.left = verifiedIconWidth } var titleSize = self.titleTextNode.updateLayout(size: CGSize(width: clearBounds.width - leftIconWidth - credibilityIconWidth - verifiedIconWidth - statusIconWidth - rightIconWidth - titleSideInset * 2.0, height: size.height), insets: titleInsets, animated: titleTransition.isAnimated) @@ -998,18 +997,7 @@ public final class ChatTitleView: UIView, NavigationBarTitleView { var nextIconX: CGFloat = titleFrame.width - var verifiedIconX: CGFloat - if case .emojiStatus = self.titleVerifiedIcon { - verifiedIconX = 0.0 - } else { - verifiedIconX = nextIconX - titleVerifiedSize.width - } - - self.titleVerifiedIconView.frame = CGRect(origin: CGPoint(x: verifiedIconX, y: floor((titleFrame.height - titleVerifiedSize.height) / 2.0)), size: titleVerifiedSize) - if case .emojiStatus = self.titleVerifiedIcon { - } else { - nextIconX -= titleVerifiedSize.width - } + self.titleVerifiedIconView.frame = CGRect(origin: CGPoint(x: 0.0, y: floor((titleFrame.height - titleVerifiedSize.height) / 2.0)), size: titleVerifiedSize) self.titleCredibilityIconView.frame = CGRect(origin: CGPoint(x: nextIconX - titleCredibilitySize.width, y: floor((titleFrame.height - titleCredibilitySize.height) / 2.0)), size: titleCredibilitySize) nextIconX -= titleCredibilitySize.width diff --git a/submodules/TelegramUI/Components/EmojiStatusComponent/Sources/EmojiStatusComponent.swift b/submodules/TelegramUI/Components/EmojiStatusComponent/Sources/EmojiStatusComponent.swift index b8a2912289..c5d3a8ab92 100644 --- a/submodules/TelegramUI/Components/EmojiStatusComponent/Sources/EmojiStatusComponent.swift +++ b/submodules/TelegramUI/Components/EmojiStatusComponent/Sources/EmojiStatusComponent.swift @@ -59,22 +59,26 @@ public final class EmojiStatusComponent: Component { public let animationCache: AnimationCache public let animationRenderer: MultiAnimationRenderer public let content: Content + public let particleColor: UIColor? public let size: CGSize? public let isVisibleForAnimations: Bool public let useSharedAnimation: Bool public let action: (() -> Void)? public let emojiFileUpdated: ((TelegramMediaFile?) -> Void)? + public let tag: AnyObject? public convenience init( context: AccountContext, animationCache: AnimationCache, animationRenderer: MultiAnimationRenderer, content: Content, + particleColor: UIColor? = nil, size: CGSize? = nil, isVisibleForAnimations: Bool, useSharedAnimation: Bool = false, action: (() -> Void)?, - emojiFileUpdated: ((TelegramMediaFile?) -> Void)? = nil + emojiFileUpdated: ((TelegramMediaFile?) -> Void)? = nil, + tag: AnyObject? = nil ) { self.init( postbox: context.account.postbox, @@ -85,11 +89,13 @@ public final class EmojiStatusComponent: Component { animationCache: animationCache, animationRenderer: animationRenderer, content: content, + particleColor: particleColor, size: size, isVisibleForAnimations: isVisibleForAnimations, useSharedAnimation: useSharedAnimation, action: action, - emojiFileUpdated: emojiFileUpdated + emojiFileUpdated: emojiFileUpdated, + tag: tag ) } @@ -100,11 +106,13 @@ public final class EmojiStatusComponent: Component { animationCache: AnimationCache, animationRenderer: MultiAnimationRenderer, content: Content, + particleColor: UIColor? = nil, size: CGSize? = nil, isVisibleForAnimations: Bool, useSharedAnimation: Bool = false, action: (() -> Void)?, - emojiFileUpdated: ((TelegramMediaFile?) -> Void)? = nil + emojiFileUpdated: ((TelegramMediaFile?) -> Void)? = nil, + tag: AnyObject? = nil ) { self.postbox = postbox self.energyUsageSettings = energyUsageSettings @@ -112,11 +120,13 @@ public final class EmojiStatusComponent: Component { self.animationCache = animationCache self.animationRenderer = animationRenderer self.content = content + self.particleColor = particleColor self.size = size self.isVisibleForAnimations = isVisibleForAnimations self.useSharedAnimation = useSharedAnimation self.action = action self.emojiFileUpdated = emojiFileUpdated + self.tag = tag } public func withVisibleForAnimations(_ isVisibleForAnimations: Bool) -> EmojiStatusComponent { @@ -127,11 +137,13 @@ public final class EmojiStatusComponent: Component { animationCache: self.animationCache, animationRenderer: self.animationRenderer, content: self.content, + particleColor: self.particleColor, size: self.size, isVisibleForAnimations: isVisibleForAnimations, useSharedAnimation: self.useSharedAnimation, action: self.action, - emojiFileUpdated: self.emojiFileUpdated + emojiFileUpdated: self.emojiFileUpdated, + tag: self.tag ) } @@ -151,6 +163,9 @@ public final class EmojiStatusComponent: Component { if lhs.content != rhs.content { return false } + if lhs.particleColor != rhs.particleColor { + return false + } if lhs.size != rhs.size { return false } @@ -160,10 +175,23 @@ public final class EmojiStatusComponent: Component { if lhs.useSharedAnimation != rhs.useSharedAnimation { return false } + if lhs.tag !== rhs.tag { + return false + } return true } - public final class View: UIView { + public 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 final class AnimationFileProperties { let path: String let coloredComposition: Animation? @@ -195,6 +223,7 @@ public final class EmojiStatusComponent: Component { private weak var state: EmptyComponentState? private var component: EmojiStatusComponent? + private var starsLayer: StarsEffectLayer? private var iconView: UIImageView? private var animationLayer: InlineStickerItemLayer? private var lottieAnimationView: AnimationView? @@ -255,6 +284,24 @@ public final class EmojiStatusComponent: Component { self.isUserInteractionEnabled = component.action != nil + if let particleColor = component.particleColor { + let starsLayer: StarsEffectLayer + if let current = self.starsLayer { + starsLayer = current + } else { + starsLayer = StarsEffectLayer() + self.layer.insertSublayer(starsLayer, at: 0) + self.starsLayer = starsLayer + } + let side = floor(availableSize.width * 1.25) + let starsFrame = CGSize(width: side, height: side).centered(in: CGRect(origin: .zero, size: availableSize)) + starsLayer.frame = starsFrame + starsLayer.update(color: particleColor, size: starsFrame.size) + } else if let starsLayer = self.starsLayer { + self.starsLayer = nil + starsLayer.removeFromSuperlayer() + } + //let previousContent = self.component?.content if self.component?.content != component.content { switch component.content { @@ -662,3 +709,57 @@ public func topicIconColors(for color: Int32) -> ([UInt32], [UInt32]) { return topicColors[color] ?? ([0x6FB9F0, 0x0261E4], [0x026CB5, 0x064BB7]) } + +public final class StarsEffectLayer: SimpleLayer { + private let emitterLayer = CAEmitterLayer() + + public override init() { + super.init() + + self.addSublayer(self.emitterLayer) + } + + override init(layer: Any) { + super.init(layer: layer) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + private func setup(color: UIColor, size: CGSize) { + let emitter = CAEmitterCell() + emitter.name = "emitter" + emitter.contents = UIImage(bundleImageName: "Premium/Stars/Particle")?.cgImage + emitter.birthRate = 8.0 + emitter.lifetime = 2.0 + emitter.velocity = 0.1 + emitter.scale = (size.width / 32.0) * 0.12 + emitter.scaleRange = 0.02 + emitter.alphaRange = 0.1 + emitter.emissionRange = .pi * 2.0 + + let staticColors: [Any] = [ + color.withAlphaComponent(0.0).cgColor, + color.withAlphaComponent(0.58).cgColor, + color.withAlphaComponent(0.58).cgColor, + color.withAlphaComponent(0.0).cgColor + ] + let staticColorBehavior = CAEmitterCell.createEmitterBehavior(type: "colorOverLife") + staticColorBehavior.setValue(staticColors, forKey: "colors") + emitter.setValue([staticColorBehavior], forKey: "emitterBehaviors") + self.emitterLayer.emitterCells = [emitter] + } + + public func update(color: UIColor, size: CGSize) { + if self.emitterLayer.emitterCells == nil { + self.setup(color: color, size: size) + } + self.emitterLayer.seed = UInt32.random(in: .min ..< .max) + self.emitterLayer.emitterShape = .circle + self.emitterLayer.emitterSize = size + self.emitterLayer.emitterMode = .surface + self.emitterLayer.frame = CGRect(origin: .zero, size: size) + self.emitterLayer.emitterPosition = CGPoint(x: size.width / 2.0, y: size.height / 2.0) + } +} diff --git a/submodules/TelegramUI/Components/EmojiStatusSelectionComponent/Sources/EmojiStatusSelectionComponent.swift b/submodules/TelegramUI/Components/EmojiStatusSelectionComponent/Sources/EmojiStatusSelectionComponent.swift index 9b437cf096..d08905b75b 100644 --- a/submodules/TelegramUI/Components/EmojiStatusSelectionComponent/Sources/EmojiStatusSelectionComponent.swift +++ b/submodules/TelegramUI/Components/EmojiStatusSelectionComponent/Sources/EmojiStatusSelectionComponent.swift @@ -1336,8 +1336,13 @@ public final class EmojiStatusSelectionController: ViewController { switch controller.mode { case .statusSelection: - let _ = (self.context.engine.accountData.setEmojiStatus(file: item?.itemFile, expirationDate: nil) - |> deliverOnMainQueue).start() + if let gift = item?.itemGift { + let _ = (self.context.engine.accountData.setStarGiftStatus(starGift: gift, expirationDate: nil) + |> deliverOnMainQueue).start() + } else { + let _ = (self.context.engine.accountData.setEmojiStatus(file: item?.itemFile, expirationDate: nil) + |> deliverOnMainQueue).start() + } case let .backgroundSelection(completion): completion(item?.itemFile) case let .customStatusSelection(completion): diff --git a/submodules/TelegramUI/Components/EntityKeyboard/Sources/EmojiKeyboardItemLayer.swift b/submodules/TelegramUI/Components/EntityKeyboard/Sources/EmojiKeyboardItemLayer.swift index 2932b20a14..43ff78156c 100644 --- a/submodules/TelegramUI/Components/EntityKeyboard/Sources/EmojiKeyboardItemLayer.swift +++ b/submodules/TelegramUI/Components/EntityKeyboard/Sources/EmojiKeyboardItemLayer.swift @@ -53,6 +53,7 @@ public final class EmojiKeyboardItemLayer: MultiAnimationRenderTarget { private var iconLayer: SimpleLayer? private var tintIconLayer: SimpleLayer? + private(set) var underlyingContentLayer: SimpleLayer? private(set) var tintContentLayer: SimpleLayer? private var badge: Badge? @@ -93,6 +94,9 @@ public final class EmojiKeyboardItemLayer: MultiAnimationRenderTarget { if let mirrorLayer = self.tintContentLayer { mirrorLayer.position = value } + if let mirrorLayer = self.underlyingContentLayer { + mirrorLayer.position = value + } super.position = value } } @@ -104,6 +108,9 @@ public final class EmojiKeyboardItemLayer: MultiAnimationRenderTarget { if let mirrorLayer = self.tintContentLayer { mirrorLayer.bounds = value } + if let mirrorLayer = self.underlyingContentLayer { + mirrorLayer.bounds = value + } super.bounds = value } } @@ -112,7 +119,9 @@ public final class EmojiKeyboardItemLayer: MultiAnimationRenderTarget { if let mirrorLayer = self.tintContentLayer { mirrorLayer.add(animation, forKey: key) } - + if let mirrorLayer = self.underlyingContentLayer { + mirrorLayer.add(animation, forKey: key) + } super.add(animation, forKey: key) } @@ -120,7 +129,9 @@ public final class EmojiKeyboardItemLayer: MultiAnimationRenderTarget { if let mirrorLayer = self.tintContentLayer { mirrorLayer.removeAllAnimations() } - + if let mirrorLayer = self.underlyingContentLayer { + mirrorLayer.removeAllAnimations() + } super.removeAllAnimations() } @@ -128,7 +139,9 @@ public final class EmojiKeyboardItemLayer: MultiAnimationRenderTarget { if let mirrorLayer = self.tintContentLayer { mirrorLayer.removeAnimation(forKey: forKey) } - + if let mirrorLayer = self.underlyingContentLayer { + mirrorLayer.removeAnimation(forKey: forKey) + } super.removeAnimation(forKey: forKey) } @@ -233,6 +246,16 @@ public final class EmojiKeyboardItemLayer: MultiAnimationRenderTarget { } }) } + + if let particleColor = animationData.particleColor { + let underlyingContentLayer = SimpleLayer() + self.underlyingContentLayer = underlyingContentLayer + + let starsLayer = StarsEffectLayer() + starsLayer.frame = CGRect(origin: CGPoint(x: -3.0, y: -3.0), size: CGSize(width: 42.0, height: 42.0)) + starsLayer.update(color: particleColor, size: CGSize(width: 42.0, height: 42.0)) + underlyingContentLayer.addSublayer(starsLayer) + } case let .staticEmoji(staticEmoji): let image = generateImage(pointSize, opaque: false, scale: min(UIScreenScale, 3.0), rotatedContext: { size, context in context.clear(CGRect(origin: CGPoint(), size: size)) diff --git a/submodules/TelegramUI/Components/EntityKeyboard/Sources/EmojiPagerContentComponent.swift b/submodules/TelegramUI/Components/EntityKeyboard/Sources/EmojiPagerContentComponent.swift index 6fd0351253..c18b142b68 100644 --- a/submodules/TelegramUI/Components/EntityKeyboard/Sources/EmojiPagerContentComponent.swift +++ b/submodules/TelegramUI/Components/EntityKeyboard/Sources/EmojiPagerContentComponent.swift @@ -40,6 +40,7 @@ public final class EntityKeyboardAnimationData: Equatable { public enum Id: Hashable { case file(MediaId) case stickerPackThumbnail(ItemCollectionId) + case gift(String) } public enum ItemType { @@ -66,8 +67,9 @@ public final class EntityKeyboardAnimationData: Equatable { public let immediateThumbnailData: Data? public let isReaction: Bool public let isTemplate: Bool + public let particleColor: UIColor? - public init(id: Id, type: ItemType, resource: MediaResourceReference, dimensions: CGSize, immediateThumbnailData: Data?, isReaction: Bool, isTemplate: Bool) { + public init(id: Id, type: ItemType, resource: MediaResourceReference, dimensions: CGSize, immediateThumbnailData: Data?, isReaction: Bool, isTemplate: Bool, particleColor: UIColor? = nil) { self.id = id self.type = type self.resource = resource @@ -75,6 +77,7 @@ public final class EntityKeyboardAnimationData: Equatable { self.immediateThumbnailData = immediateThumbnailData self.isReaction = isReaction self.isTemplate = isTemplate + self.particleColor = particleColor } public convenience init(file: TelegramMediaFile, isReaction: Bool = false, partialReference: PartialMediaReference? = nil) { @@ -97,6 +100,25 @@ public final class EntityKeyboardAnimationData: Equatable { self.init(id: .file(file.fileId), type: type, resource: resourceReference, dimensions: file.dimensions?.cgSize ?? CGSize(width: 512.0, height: 512.0), immediateThumbnailData: file.immediateThumbnailData, isReaction: isReaction, isTemplate: isTemplate) } + public convenience init?(gift: StarGift.UniqueGift) { + var file: TelegramMediaFile? + var color: UIColor? + for attribute in gift.attributes { + if case let .model(_, fileValue, _) = attribute { + file = fileValue + } else if case let .backdrop(_, innerColor, outerColor, _, _, _) = attribute { + color = UIColor(rgb: UInt32(bitPattern: innerColor)) + let _ = outerColor + } + } + if let file, let color { + let resourceReference: MediaResourceReference = .standalone(resource: file.resource) + self.init(id: .gift(gift.slug), type: .lottie, resource: resourceReference, dimensions: file.dimensions?.cgSize ?? CGSize(width: 512.0, height: 512.0), immediateThumbnailData: file.immediateThumbnailData, isReaction: false, isTemplate: false, particleColor: color) + } else { + return nil + } + } + public static func ==(lhs: EntityKeyboardAnimationData, rhs: EntityKeyboardAnimationData) -> Bool { if lhs === rhs { return true @@ -335,6 +357,7 @@ public final class EmojiPagerContentComponent: Component { case animation(EntityKeyboardAnimationData.Id) case staticEmoji(String) case icon(Icon) + case starGift(String) } public enum Icon: Equatable, Hashable { @@ -379,6 +402,7 @@ public final class EmojiPagerContentComponent: Component { public let animationData: EntityKeyboardAnimationData? public let content: ItemContent public let itemFile: TelegramMediaFile? + public let itemGift: StarGift.UniqueGift? public let subgroupId: Int32? public let icon: Icon public let tintMode: TintMode @@ -387,6 +411,7 @@ public final class EmojiPagerContentComponent: Component { animationData: EntityKeyboardAnimationData?, content: ItemContent, itemFile: TelegramMediaFile?, + itemGift: StarGift.UniqueGift? = nil, subgroupId: Int32?, icon: Icon, tintMode: TintMode @@ -394,6 +419,7 @@ public final class EmojiPagerContentComponent: Component { self.animationData = animationData self.content = content self.itemFile = itemFile + self.itemGift = itemGift self.subgroupId = subgroupId self.icon = icon self.tintMode = tintMode @@ -412,6 +438,9 @@ public final class EmojiPagerContentComponent: Component { if lhs.itemFile?.fileId != rhs.itemFile?.fileId { return false } + if lhs.itemGift?.id != rhs.itemGift?.id { + return false + } if lhs.subgroupId != rhs.subgroupId { return false } @@ -3461,6 +3490,9 @@ public final class EmojiPagerContentComponent: Component { ) self.visibleItemLayers[itemId] = itemLayer + if let underlyingContentLayer = itemLayer.underlyingContentLayer { + self.scrollView.layer.addSublayer(underlyingContentLayer) + } self.scrollView.layer.addSublayer(itemLayer) if let tintContentLayer = itemLayer.tintContentLayer { self.mirrorContentScrollView.layer.addSublayer(tintContentLayer) @@ -3692,6 +3724,7 @@ public final class EmojiPagerContentComponent: Component { itemLayer.opacity = 0.0 itemLayer.animateScale(from: 1.0, to: 0.01, duration: 0.16) itemLayer.animateAlpha(from: 1.0, to: 0.0, duration: 0.16, completion: { [weak itemLayer] _ in + itemLayer?.underlyingContentLayer?.removeFromSuperlayer() itemLayer?.tintContentLayer?.removeFromSuperlayer() itemLayer?.removeFromSuperlayer() }) @@ -3712,6 +3745,7 @@ public final class EmojiPagerContentComponent: Component { } } else if let position = updatedItemPositions?[.item(id: id)], transitionHintInstalledGroupId != id.groupId { transition.setPosition(layer: itemLayer, position: position, completion: { [weak itemLayer] _ in + itemLayer?.underlyingContentLayer?.removeFromSuperlayer() itemLayer?.tintContentLayer?.removeFromSuperlayer() itemLayer?.removeFromSuperlayer() }) @@ -3726,6 +3760,7 @@ public final class EmojiPagerContentComponent: Component { itemLayer.opacity = 0.0 itemLayer.animateScale(from: 1.0, to: 0.01, duration: 0.2) itemLayer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, completion: { [weak itemLayer] _ in + itemLayer?.underlyingContentLayer?.removeFromSuperlayer() itemLayer?.tintContentLayer?.removeFromSuperlayer() itemLayer?.removeFromSuperlayer() }) @@ -3746,6 +3781,7 @@ public final class EmojiPagerContentComponent: Component { } } } else { + itemLayer.underlyingContentLayer?.removeFromSuperlayer() itemLayer.tintContentLayer?.removeFromSuperlayer() itemLayer.removeFromSuperlayer() diff --git a/submodules/TelegramUI/Components/EntityKeyboard/Sources/EmojiPagerContentSignals.swift b/submodules/TelegramUI/Components/EntityKeyboard/Sources/EmojiPagerContentSignals.swift index 512b446dcc..cb7e670d8e 100644 --- a/submodules/TelegramUI/Components/EntityKeyboard/Sources/EmojiPagerContentSignals.swift +++ b/submodules/TelegramUI/Components/EntityKeyboard/Sources/EmojiPagerContentSignals.swift @@ -126,6 +126,7 @@ public extension EmojiPagerContentComponent { if case .status = subject { orderedItemListCollectionIds.append(Namespaces.OrderedItemList.CloudFeaturedStatusEmoji) orderedItemListCollectionIds.append(Namespaces.OrderedItemList.CloudRecentStatusEmoji) + orderedItemListCollectionIds.append(Namespaces.OrderedItemList.CloudUniqueStarGifts) iconStatusEmoji = context.engine.stickers.loadedStickerPack(reference: .iconStatusEmoji, forceActualized: false) |> map { result -> [TelegramMediaFile] in @@ -343,8 +344,11 @@ public extension EmojiPagerContentComponent { var featuredAvatarEmoji: OrderedItemListView? var featuredBackgroundIconEmoji: OrderedItemListView? var defaultTagReactions: OrderedItemListView? + var uniqueGifts: OrderedItemListView? for orderedView in view.orderedItemListsViews { - if orderedView.collectionId == Namespaces.OrderedItemList.LocalRecentEmoji { + if orderedView.collectionId == Namespaces.OrderedItemList.CloudUniqueStarGifts { + uniqueGifts = orderedView + } else if orderedView.collectionId == Namespaces.OrderedItemList.LocalRecentEmoji { recentEmoji = orderedView } else if orderedView.collectionId == Namespaces.OrderedItemList.CloudFeaturedStatusEmoji { featuredStatusEmoji = orderedView @@ -608,6 +612,38 @@ public extension EmojiPagerContentComponent { } } } + + if let uniqueGifts, !uniqueGifts.items.isEmpty { + //TODO:localize + let groupId = "collectible" + let groupIndex: Int + if let current = itemGroupIndexById[groupId] { + groupIndex = current + } else { + groupIndex = itemGroups.count + itemGroupIndexById[groupId] = groupIndex + itemGroups.append(ItemGroup(supergroupId: groupId, id: groupId, title: "COLLECTIBLES".uppercased(), subtitle: nil, badge: nil, isPremiumLocked: false, isFeatured: false, collapsedLineCount: 2, isClearable: false, headerItem: nil, items: [])) + } + + for item in uniqueGifts.items { + guard let item = item.contents.get(RecentStarGiftItem.self) else { + continue + } + guard let animationData = EntityKeyboardAnimationData(gift: item.starGift) else { + continue + } + let resultItem = EmojiPagerContentComponent.Item( + animationData: animationData, + content: .animation(animationData), + itemFile: nil, + itemGift: item.starGift, + subgroupId: nil, + icon: .none, + tintMode: .none + ) + itemGroups[groupIndex].items.append(resultItem) + } + } } else if case .channelStatus = subject { let resultItem = EmojiPagerContentComponent.Item( animationData: nil, diff --git a/submodules/TelegramUI/Components/Gifts/GiftAnimationComponent/BUILD b/submodules/TelegramUI/Components/Gifts/GiftAnimationComponent/BUILD index 518a36b234..ab8dc02d74 100644 --- a/submodules/TelegramUI/Components/Gifts/GiftAnimationComponent/BUILD +++ b/submodules/TelegramUI/Components/Gifts/GiftAnimationComponent/BUILD @@ -25,6 +25,7 @@ swift_library( "//submodules/TextFormat", "//submodules/TelegramUI/Components/EmojiTextAttachmentView", "//submodules/TelegramUI/Components/PeerInfo/PeerInfoCoverComponent", + "//submodules/TelegramUI/Components/EmojiStatusComponent", "//submodules/AnimatedStickerNode", "//submodules/TelegramAnimatedStickerNode", ], diff --git a/submodules/TelegramUI/Components/Gifts/GiftAnimationComponent/Sources/GiftCompositionComponent.swift b/submodules/TelegramUI/Components/Gifts/GiftAnimationComponent/Sources/GiftCompositionComponent.swift index 2c157867b2..dff5e42cb3 100644 --- a/submodules/TelegramUI/Components/Gifts/GiftAnimationComponent/Sources/GiftCompositionComponent.swift +++ b/submodules/TelegramUI/Components/Gifts/GiftAnimationComponent/Sources/GiftCompositionComponent.swift @@ -12,6 +12,7 @@ import TextFormat import PeerInfoCoverComponent import AnimatedStickerNode import TelegramAnimatedStickerNode +import EmojiStatusComponent public final class GiftCompositionComponent: Component { public class ExternalState { @@ -30,6 +31,9 @@ public final class GiftCompositionComponent: Component { let context: AccountContext let theme: PresentationTheme let subject: Subject + let animationOffset: CGPoint? + let animationScale: CGFloat? + let displayAnimationStars: Bool let externalState: ExternalState? let requestUpdate: () -> Void @@ -37,12 +41,18 @@ public final class GiftCompositionComponent: Component { context: AccountContext, theme: PresentationTheme, subject: Subject, + animationOffset: CGPoint? = nil, + animationScale: CGFloat? = nil, + displayAnimationStars: Bool = false, externalState: ExternalState? = nil, requestUpdate: @escaping () -> Void = {} ) { self.context = context self.theme = theme self.subject = subject + self.animationOffset = animationOffset + self.animationScale = animationScale + self.displayAnimationStars = displayAnimationStars self.externalState = externalState self.requestUpdate = requestUpdate } @@ -57,6 +67,15 @@ public final class GiftCompositionComponent: Component { if lhs.subject != rhs.subject { return false } + if lhs.animationOffset != rhs.animationOffset { + return false + } + if lhs.animationScale != rhs.animationScale { + return false + } + if lhs.displayAnimationStars != rhs.displayAnimationStars { + return false + } return true } @@ -64,6 +83,8 @@ public final class GiftCompositionComponent: Component { private var component: GiftCompositionComponent? private weak var componentState: EmptyComponentState? + private var starsLayer: StarsEffectLayer? + private let background = ComponentView() private var animationNode: AnimatedStickerNode? @@ -256,6 +277,12 @@ public final class GiftCompositionComponent: Component { if animateTransition, let backgroundView = self.background.view as? PeerInfoCoverComponent.View { backgroundView.animateTransition() } + + var avatarCenter = CGPoint(x: availableSize.width / 2.0, y: 104.0) + if let _ = component.animationScale { + avatarCenter = CGPoint(x: avatarCenter.x, y: 67.0) + } + let _ = self.background.update( transition: backgroundTransition, component: AnyComponent(PeerInfoCoverComponent( @@ -263,9 +290,10 @@ public final class GiftCompositionComponent: Component { subject: .custom(backgroundColor, secondBackgroundColor, patternColor, patternFile?.fileId.id), files: files, isDark: false, - avatarCenter: CGPoint(x: availableSize.width / 2.0, y: 104.0), + avatarCenter: avatarCenter, avatarScale: 1.0, - defaultHeight: availableSize.height, + defaultHeight: 300.0, + gradientOnTop: true, avatarTransitionFraction: 0.0, patternTransitionFraction: 0.0 )), @@ -292,17 +320,20 @@ public final class GiftCompositionComponent: Component { let iconSize = CGSize(width: 136.0, height: 136.0) var startFromIndex: Int? + var animationTransition = transition if animateTransition, let disappearingAnimationNode = self.animationNode { self.animationNode = nil startFromIndex = disappearingAnimationNode.currentFrameIndex disappearingAnimationNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.25, removeOnCompletion: false, completion: { _ in disappearingAnimationNode.view.removeFromSuperview() }) + animationTransition = .immediate } if let file = animationFile { let animationNode: AnimatedStickerNode if self.animationNode == nil { + animationTransition = .immediate animationNode = DefaultAnimatedStickerNodeImpl() animationNode.isUserInteractionEnabled = false self.animationNode = animationNode @@ -333,9 +364,41 @@ public final class GiftCompositionComponent: Component { } } if let animationNode = self.animationNode { - transition.setFrame(layer: animationNode.layer, frame: CGRect(origin: CGPoint(x: floorToScreenPixels((availableSize.width - iconSize.width) / 2.0), y: 20.0), size: iconSize)) + let offset = component.animationOffset ?? .zero + var size = CGSize(width: iconSize.width, height: iconSize.height) + if let scale = component.animationScale { + size = CGSize(width: size.width * scale, height: size.height * scale) + } + let animationFrame = CGRect(origin: CGPoint(x: availableSize.width / 2.0 + offset.x - size.width / 2.0, y: 88.0 + offset.y - size.height / 2.0), size: size) + animationNode.layer.bounds = CGRect(origin: .zero, size: iconSize) + animationTransition.setPosition(layer: animationNode.layer, position: animationFrame.center) + animationTransition.setScale(layer: animationNode.layer, scale: size.width / iconSize.width) + + if component.displayAnimationStars { + var starsTransition = transition + let starsLayer: StarsEffectLayer + if let current = self.starsLayer { + starsLayer = current + } else { + starsTransition = .immediate + starsLayer = StarsEffectLayer() + self.layer.insertSublayer(starsLayer, below: animationNode.layer) + self.starsLayer = starsLayer + starsLayer.animateAlpha(from: 0.0, to: 1.0, duration: 0.25) + } + let starsSize = CGSize(width: 36.0, height: 36.0) + starsLayer.update(color: .white, size: starsSize) + starsLayer.bounds = CGRect(origin: .zero, size: starsSize) + starsTransition.setPosition(layer: starsLayer, position: animationFrame.center) + } else if let starsLayer = self.starsLayer { + self.starsLayer = nil + transition.setPosition(layer: starsLayer, position: animationFrame.center) + starsLayer.animateAlpha(from: 1.0, to: 0.0, duration: 0.25, removeOnCompletion: false, completion: { _ in + starsLayer.removeFromSuperlayer() + }) + } } - + return availableSize } } @@ -348,3 +411,56 @@ public final class GiftCompositionComponent: Component { return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) } } + +private final class StarsEffectLayer: SimpleLayer { + private let emitterLayer = CAEmitterLayer() + + override init() { + super.init() + + self.addSublayer(self.emitterLayer) + } + + override init(layer: Any) { + super.init(layer: layer) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + private func setup(color: UIColor) { + let emitter = CAEmitterCell() + emitter.name = "emitter" + emitter.contents = UIImage(bundleImageName: "Premium/Stars/Particle")?.cgImage + emitter.birthRate = 8.0 + emitter.lifetime = 2.0 + emitter.velocity = 0.1 + emitter.scale = 0.12 + emitter.scaleRange = 0.02 + emitter.alphaRange = 0.1 + emitter.emissionRange = .pi * 2.0 + + let staticColors: [Any] = [ + color.withAlphaComponent(0.0).cgColor, + color.withAlphaComponent(0.55).cgColor, + color.withAlphaComponent(0.55).cgColor, + color.withAlphaComponent(0.0).cgColor + ] + let staticColorBehavior = CAEmitterCell.createEmitterBehavior(type: "colorOverLife") + staticColorBehavior.setValue(staticColors, forKey: "colors") + emitter.setValue([staticColorBehavior], forKey: "emitterBehaviors") + self.emitterLayer.emitterCells = [emitter] + } + + func update(color: UIColor, size: CGSize) { + if self.emitterLayer.emitterCells == nil { + self.setup(color: color) + } + self.emitterLayer.emitterShape = .circle + self.emitterLayer.emitterSize = size + self.emitterLayer.emitterMode = .surface + self.emitterLayer.frame = CGRect(origin: .zero, size: size) + self.emitterLayer.emitterPosition = CGPoint(x: size.width / 2.0, y: size.height / 2.0) + } +} diff --git a/submodules/TelegramUI/Components/Gifts/GiftItemComponent/Sources/GiftItemComponent.swift b/submodules/TelegramUI/Components/Gifts/GiftItemComponent/Sources/GiftItemComponent.swift index 95712eb17f..9ef28bae36 100644 --- a/submodules/TelegramUI/Components/Gifts/GiftItemComponent/Sources/GiftItemComponent.swift +++ b/submodules/TelegramUI/Components/Gifts/GiftItemComponent/Sources/GiftItemComponent.swift @@ -80,6 +80,7 @@ public final class GiftItemComponent: Component { case generic case profile case thumbnail + case preview } let context: AccountContext @@ -97,7 +98,7 @@ public final class GiftItemComponent: Component { public init( context: AccountContext, theme: PresentationTheme, - peer: GiftItemComponent.Peer?, + peer: GiftItemComponent.Peer? = nil, subject: GiftItemComponent.Subject, title: String? = nil, subtitle: String? = nil, @@ -220,13 +221,17 @@ public final class GiftItemComponent: Component { iconSize = CGSize(width: 88.0, height: 88.0) cornerRadius = 10.0 case .profile: - size = CGSize(width: availableSize.width, height: availableSize.width) + size = availableSize iconSize = CGSize(width: 88.0, height: 88.0) cornerRadius = 10.0 case .thumbnail: 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 .preview: + size = availableSize + iconSize = CGSize(width: floor(size.width * 0.6), height: floor(size.width * 0.6)) + cornerRadius = 4.0 } self.backgroundLayer.cornerRadius = cornerRadius @@ -431,10 +436,9 @@ public final class GiftItemComponent: Component { } price = priceValue case .uniqueGift: - //TODO:localize buttonColor = UIColor.white starsColor = UIColor.white - price = "Unique" + price = "" } let buttonSize = self.button.update( @@ -460,11 +464,17 @@ public final class GiftItemComponent: Component { } if let ribbon = component.ribbon { + let ribbonFontSize: CGFloat + if case .profile = component.mode { + ribbonFontSize = 9.0 + } else { + ribbonFontSize = 10.0 + } let ribbonTextSize = self.ribbonText.update( transition: transition, component: AnyComponent( MultilineTextComponent( - text: .plain(NSAttributedString(string: ribbon.text, font: Font.semibold(10.0), textColor: .white)), + text: .plain(NSAttributedString(string: ribbon.text, font: Font.semibold(ribbonFontSize), textColor: .white)), horizontalAlignment: .center ) ), @@ -510,7 +520,7 @@ public final class GiftItemComponent: Component { switch peer { case let .peer(peer): - avatarNode.setPeerV2(context: component.context, theme: component.theme, peer: peer, displayDimensions: CGSize(width: 20.0, height: 20.0)) + avatarNode.setPeer(context: component.context, theme: component.theme, peer: peer, displayDimensions: CGSize(width: 20.0, height: 20.0)) case .anonymous: avatarNode.setPeer(context: component.context, theme: component.theme, peer: nil, overrideImage: .anonymousSavedMessagesIcon(isColored: true)) } diff --git a/submodules/TelegramUI/Components/Gifts/GiftOptionsScreen/Sources/GiftOptionsScreen.swift b/submodules/TelegramUI/Components/Gifts/GiftOptionsScreen/Sources/GiftOptionsScreen.swift index 1dbdc4b095..aa61c9ecb8 100644 --- a/submodules/TelegramUI/Components/Gifts/GiftOptionsScreen/Sources/GiftOptionsScreen.swift +++ b/submodules/TelegramUI/Components/Gifts/GiftOptionsScreen/Sources/GiftOptionsScreen.swift @@ -79,6 +79,7 @@ final class GiftOptionsScreenComponent: Component { public enum StarsFilter: Equatable { case all case limited + case inStock case stars(Int64) init(rawValue: Int64) { @@ -87,6 +88,8 @@ final class GiftOptionsScreenComponent: Component { self = .all case -1: self = .limited + case -2: + self = .inStock default: self = .stars(rawValue) } @@ -98,6 +101,8 @@ final class GiftOptionsScreenComponent: Component { return 0 case .limited: return -1 + case .inStock: + return -2 case let .stars(stars): return stars } @@ -159,6 +164,10 @@ final class GiftOptionsScreenComponent: Component { if $0.availability != nil { return true } + case .inStock: + if $0.availability == nil || $0.availability!.remains > 0 { + return true + } case let .stars(stars): if $0.price == stars { return true @@ -901,6 +910,11 @@ final class GiftOptionsScreenComponent: Component { title: strings.Gift_Options_Gift_Filter_Limited )) } + + tabSelectorItems.append(TabSelectorComponent.Item( + id: AnyHashable(StarsFilter.inStock.rawValue), + title: strings.Gift_Options_Gift_Filter_InStock + )) let starsAmounts = Array(starsAmountsSet).sorted() for amount in starsAmounts { diff --git a/submodules/TelegramUI/Components/Gifts/GiftSetupScreen/Sources/ChatGiftPreviewItem.swift b/submodules/TelegramUI/Components/Gifts/GiftSetupScreen/Sources/ChatGiftPreviewItem.swift index fbb41da69f..f9059e1cb8 100644 --- a/submodules/TelegramUI/Components/Gifts/GiftSetupScreen/Sources/ChatGiftPreviewItem.swift +++ b/submodules/TelegramUI/Components/Gifts/GiftSetupScreen/Sources/ChatGiftPreviewItem.swift @@ -34,7 +34,7 @@ final class ChatGiftPreviewItem: ListViewItem, ItemListItem, ListItemComponentAd let isSelf: Bool let text: String let entities: [MessageTextEntity] - let includeUpgrade: Bool + let upgradeStars: Int64? init( context: AccountContext, @@ -52,7 +52,7 @@ final class ChatGiftPreviewItem: ListViewItem, ItemListItem, ListItemComponentAd isSelf: Bool, text: String, entities: [MessageTextEntity], - includeUpgrade: Bool + upgradeStars: Int64? ) { self.context = context self.theme = theme @@ -69,7 +69,7 @@ final class ChatGiftPreviewItem: ListViewItem, ItemListItem, ListItemComponentAd self.isSelf = isSelf self.text = text self.entities = entities - self.includeUpgrade = includeUpgrade + self.upgradeStars = upgradeStars } func nodeConfiguredForParams(async: @escaping (@escaping () -> Void) -> Void, params: ListViewItemLayoutParams, synchronousLoads: Bool, previousItem: ListViewItem?, nextItem: ListViewItem?, completion: @escaping (ListViewItemNode, @escaping () -> (Signal?, (ListViewItemApply) -> Void)) -> Void) { @@ -146,7 +146,7 @@ final class ChatGiftPreviewItem: ListViewItem, ItemListItem, ListItemComponentAd if lhs.entities != rhs.entities { return false } - if lhs.includeUpgrade != rhs.includeUpgrade { + if lhs.upgradeStars != rhs.upgradeStars { return false } return true @@ -234,7 +234,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.includeUpgrade ? 1 : nil, isRefunded: false, upgradeMessageId: nil) + 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) ) ] } @@ -250,7 +250,7 @@ final class ChatGiftPreviewItemNode: ListViewItemNode { let itemNode = messageNodes[i] items[i].updateNode(async: { $0() }, node: { return itemNode - }, params: params, previousItem: i == 0 ? nil : items[i - 1], nextItem: i == (items.count - 1) ? nil : items[i + 1], animation: .None, completion: { (layout, apply) in + }, params: params, previousItem: i == 0 ? nil : items[i - 1], nextItem: i == (items.count - 1) ? nil : items[i + 1], animation: .System(duration: 0.2, transition: ControlledTransition(duration: 0.2, curve: .spring, interactive: false)), completion: { (layout, apply) in let nodeFrame = CGRect(origin: itemNode.frame.origin, size: CGSize(width: layout.size.width, height: layout.size.height)) itemNode.contentSize = layout.contentSize diff --git a/submodules/TelegramUI/Components/Gifts/GiftSetupScreen/Sources/GiftSetupScreen.swift b/submodules/TelegramUI/Components/Gifts/GiftSetupScreen/Sources/GiftSetupScreen.swift index b3db1ba090..4ef057c292 100644 --- a/submodules/TelegramUI/Components/Gifts/GiftSetupScreen/Sources/GiftSetupScreen.swift +++ b/submodules/TelegramUI/Components/Gifts/GiftSetupScreen/Sources/GiftSetupScreen.swift @@ -416,11 +416,27 @@ final class GiftSetupScreenComponent: Component { starsContext: starsContext, options: options ?? [], purpose: .starGift(peerId: component.peerId, requiredStars: starGift.price), - completion: { [weak starsContext] stars in - starsContext?.add(balance: StarsAmount(value: stars, nanos: 0)) - Queue.mainQueue().after(2.0) { - proceed() + completion: { [weak self, weak starsContext] stars in + guard let self, let starsContext else { + return } + self.inProgress = true + self.state?.updated() + + starsContext.add(balance: StarsAmount(value: stars, nanos: 0)) + + let _ = (starsContext.state + |> take(until: { value in + if let value { + if !value.flags.contains(.isPendingBalance) { + return SignalTakeAction(passthrough: true, complete: true) + } + } + return SignalTakeAction(passthrough: false, complete: false) + }) + |> deliverOnMainQueue).start(next: { _ in + proceed() + }) } ) controller.push(purchaseController) @@ -464,8 +480,13 @@ final class GiftSetupScreenComponent: Component { } let peerName = self.peerMap[component.peerId]?.compactDisplayTitle ?? "" + let isSelfGift = component.peerId == component.context.account.peerId if self.component == nil { + if isSelfGift { + self.hideName = true + } + let _ = (component.context.engine.data.get( TelegramEngine.EngineData.Item.Peer.Peer(id: component.peerId), TelegramEngine.EngineData.Item.Peer.Peer(id: component.context.account.peerId) @@ -615,9 +636,7 @@ final class GiftSetupScreenComponent: Component { } let presentationData = component.context.sharedContext.currentPresentationData.with { $0 } - - let isSelfGift = component.peerId == component.context.account.peerId - + let navigationTitleSize = self.navigationTitle.update( transition: transition, component: AnyComponent(MultilineTextComponent( @@ -793,6 +812,7 @@ final class GiftSetupScreenComponent: Component { let listItemParams = ListViewItemLayoutParams(width: availableSize.width - sideInset * 2.0, leftInset: 0.0, rightInset: 0.0, availableHeight: 10000.0, isStandalone: true) if let accountPeer = self.peerMap[component.context.account.peerId] { + var upgradeStars: Int64? let subject: ChatGiftPreviewItem.Subject switch component.subject { case let .premium(product): @@ -800,6 +820,7 @@ final class GiftSetupScreenComponent: Component { subject = .premium(months: product.months, amount: amount, currency: currency) case let .starGift(gift): subject = .starGift(gift: gift) + upgradeStars = gift.upgradeStars } let introContentSize = self.introContent.update( @@ -822,7 +843,7 @@ final class GiftSetupScreenComponent: Component { isSelf: component.peerId == component.context.account.peerId, text: self.textInputState.text.string, entities: generateChatInputTextEntities(self.textInputState.text), - includeUpgrade: self.includeUpgrade + upgradeStars: self.includeUpgrade ? upgradeStars : nil ), params: listItemParams ) @@ -1037,7 +1058,8 @@ final class GiftSetupScreenComponent: Component { finalPrice += upgradePrice } let amountString = presentationStringsFormattedNumber(Int32(finalPrice), presentationData.dateTimeFormat.groupingSeparator) - buttonString = "\(environment.strings.Gift_Send_Send) # \(amountString)" + let buttonTitle = isSelfGift ? environment.strings.Gift_Send_Buy : environment.strings.Gift_Send_Send + buttonString = "\(buttonTitle) # \(amountString)" if let availability = starGift.availability, availability.remains == 0 { buttonIsEnabled = false } diff --git a/submodules/TelegramUI/Components/Gifts/GiftViewScreen/BUILD b/submodules/TelegramUI/Components/Gifts/GiftViewScreen/BUILD index 942da7286a..4eec76a393 100644 --- a/submodules/TelegramUI/Components/Gifts/GiftViewScreen/BUILD +++ b/submodules/TelegramUI/Components/Gifts/GiftViewScreen/BUILD @@ -42,6 +42,8 @@ swift_library( "//submodules/ConfettiEffect", "//submodules/TooltipUI", "//submodules/TelegramUI/Components/Gifts/GiftItemComponent", + "//submodules/MoreButtonNode", + "//submodules/TelegramUI/Components/EmojiStatusComponent", ], visibility = [ "//visibility:public", diff --git a/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/ButtonsComponent.swift b/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/ButtonsComponent.swift new file mode 100644 index 0000000000..09e7712a60 --- /dev/null +++ b/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/ButtonsComponent.swift @@ -0,0 +1,143 @@ +import Foundation +import UIKit +import Display +import AsyncDisplayKit +import ComponentFlow +import BundleIconComponent +import MultilineTextComponent +import MoreButtonNode +import AccountContext +import TelegramPresentationData + +final class ButtonsComponent: Component { + let theme: PresentationTheme + let isOverlay: Bool + let showMoreButton: Bool + let closePressed: () -> Void + let morePressed: (ASDisplayNode, ContextGesture?) -> Void + + init( + theme: PresentationTheme, + isOverlay: Bool, + showMoreButton: Bool, + closePressed: @escaping () -> Void, + morePressed: @escaping (ASDisplayNode, ContextGesture?) -> Void + ) { + self.theme = theme + self.isOverlay = isOverlay + self.showMoreButton = showMoreButton + self.closePressed = closePressed + self.morePressed = morePressed + } + + static func ==(lhs: ButtonsComponent, rhs: ButtonsComponent) -> Bool { + return lhs.theme === rhs.theme && lhs.isOverlay == rhs.isOverlay && lhs.showMoreButton == rhs.showMoreButton + } + + final class View: UIView { + private let backgroundView = UIView() + + private let closeButton = HighlightTrackingButton() + private let closeIcon = UIImageView() + private let moreNode = MoreButtonNode(theme: defaultPresentationTheme, size: CGSize(width: 36.0, height: 36.0), encircled: false) + + private var component: ButtonsComponent? + private weak var state: EmptyComponentState? + + override init(frame: CGRect) { + super.init(frame: frame) + + self.backgroundView.clipsToBounds = true + self.addSubview(self.backgroundView) + + self.closeIcon.image = generateCloseButtonImage() + self.moreNode.updateColor(.white, transition: .immediate) + + self.backgroundView.addSubview(self.moreNode.view) + self.backgroundView.addSubview(self.closeButton) + self.backgroundView.addSubview(self.closeIcon) + + self.closeButton.highligthedChanged = { [weak self] highlighted in + guard let self else { + return + } + if highlighted { + self.closeIcon.layer.removeAnimation(forKey: "opacity") + self.closeIcon.alpha = 0.6 + } else { + self.closeIcon.alpha = 1.0 + self.closeIcon.layer.animateAlpha(from: 0.6, to: 1.0, duration: 0.2) + } + } + self.closeButton.addTarget(self, action: #selector(self.closePressed), for: .touchUpInside) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + @objc private func closePressed() { + guard let component = self.component else { + return + } + component.closePressed() + } + + func update(component: ButtonsComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { + self.component = component + self.state = state + + let backgroundSize = CGSize(width: component.showMoreButton ? 70.0 : 30.0, height: 30.0) + self.backgroundView.layer.cornerRadius = backgroundSize.height / 2.0 + + let backgroundColor: UIColor = component.isOverlay ? UIColor(rgb: 0xffffff, alpha: 0.1) : UIColor(rgb: 0x808084, alpha: 0.1) + let foregroundColor: UIColor = component.isOverlay ? .white : component.theme.actionSheet.inputClearButtonColor + transition.setBackgroundColor(view: self.backgroundView, color: backgroundColor) + transition.setTintColor(view: self.closeIcon, color: foregroundColor) + + let backgroundFrame = CGRect(origin: .zero, size: backgroundSize) + transition.setFrame(view: self.backgroundView, frame: backgroundFrame) + + transition.setFrame(view: self.moreNode.view, frame: CGRect(origin: CGPoint(x: -7.0, y: -4.0), size: CGSize(width: 36.0, height: 36.0))) + transition.setAlpha(view: self.moreNode.view, alpha: component.showMoreButton ? 1.0 : 0.0) + self.moreNode.action = { [weak self] node, gesture in + guard let self, let component = self.component else { + return + } + component.morePressed(node, gesture) + } + + let closeFrame = CGRect(origin: CGPoint(x: backgroundSize.width - 30.0 - (component.showMoreButton ? 3.0 : 0.0), y: 0.0), size: CGSize(width: 30.0, height: 30.0)) + transition.setFrame(view: self.closeIcon, frame: closeFrame) + transition.setFrame(view: self.closeButton, frame: closeFrame) + + return backgroundSize + } + } + + 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) + } +} + +private func generateCloseButtonImage() -> UIImage? { + return generateImage(CGSize(width: 30.0, height: 30.0), contextGenerator: { size, context in + context.clear(CGRect(origin: CGPoint(), size: size)) + + context.setLineWidth(2.0) + context.setLineCap(.round) + context.setStrokeColor(UIColor.white.cgColor) + + context.move(to: CGPoint(x: 10.0, y: 10.0)) + context.addLine(to: CGPoint(x: 20.0, y: 20.0)) + context.strokePath() + + context.move(to: CGPoint(x: 20.0, y: 10.0)) + context.addLine(to: CGPoint(x: 10.0, y: 20.0)) + context.strokePath() + })?.withRenderingMode(.alwaysTemplate) +} diff --git a/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftViewScreen.swift b/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftViewScreen.swift index 4cc54532b3..bac312a9b3 100644 --- a/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftViewScreen.swift +++ b/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftViewScreen.swift @@ -22,6 +22,7 @@ import TextFormat import TelegramStringFormatting import StarsAvatarComponent import EmojiTextAttachmentView +import EmojiStatusComponent import UndoUI import ConfettiEffect import PlainButtonComponent @@ -29,10 +30,12 @@ import CheckComponent import TooltipUI import GiftAnimationComponent import LottieComponent +import ContextUI private let modelButtonTag = GenericComponentViewTag() private let backdropButtonTag = GenericComponentViewTag() private let symbolButtonTag = GenericComponentViewTag() +private let statusTag = GenericComponentViewTag() private final class GiftViewSheetContent: CombinedComponent { typealias EnvironmentType = ViewControllerComponentContainer.Environment @@ -48,8 +51,10 @@ private final class GiftViewSheetContent: CombinedComponent { let openMyGifts: () -> Void let transferGift: () -> Void let upgradeGift: ((Int64?, Bool) -> Signal) - let showAttributeInfo: (Any, Float) -> Void + let shareGift: () -> Void + let showAttributeInfo: (Any, String) -> Void let viewUpgraded: (EngineMessage.Id) -> Void + let openMore: (ASDisplayNode, ContextGesture?) -> Void let getController: () -> ViewController? init( @@ -64,8 +69,10 @@ private final class GiftViewSheetContent: CombinedComponent { openMyGifts: @escaping () -> Void, transferGift: @escaping () -> Void, upgradeGift: @escaping ((Int64?, Bool) -> Signal), - showAttributeInfo: @escaping (Any, Float) -> Void, + shareGift: @escaping () -> Void, + showAttributeInfo: @escaping (Any, String) -> Void, viewUpgraded: @escaping (EngineMessage.Id) -> Void, + openMore: @escaping (ASDisplayNode, ContextGesture?) -> Void, getController: @escaping () -> ViewController? ) { self.context = context @@ -79,8 +86,10 @@ private final class GiftViewSheetContent: CombinedComponent { self.openMyGifts = openMyGifts self.transferGift = transferGift self.upgradeGift = upgradeGift + self.shareGift = shareGift self.showAttributeInfo = showAttributeInfo self.viewUpgraded = viewUpgraded + self.openMore = openMore self.getController = getController } @@ -108,8 +117,6 @@ private final class GiftViewSheetContent: CombinedComponent { var cachedCircleImage: UIImage? var cachedStarImage: (UIImage, PresentationTheme)? - var cachedCloseImage: (UIImage, PresentationTheme)? - var cachedOverlayCloseImage: UIImage? var cachedChevronImage: (UIImage, PresentationTheme)? var cachedSmallChevronImage: (UIImage, PresentationTheme)? @@ -121,6 +128,10 @@ private final class GiftViewSheetContent: CombinedComponent { var upgradeFormDisposable: Disposable? var upgradeDisposable: Disposable? + var inWearPreview = false + var pendingWear = false + var pendingTakeOff = false + var sampleGiftAttributes: [StarGift.UniqueGift.Attribute]? let sampleDisposable = DisposableSet() @@ -133,13 +144,7 @@ private final class GiftViewSheetContent: CombinedComponent { } } private let optionsPromise = ValuePromise<[StarsTopUpOption]?>(nil) - - var mockFiles: [TelegramMediaFile] = [] - var mockIconFiles: [TelegramMediaFile] = [] - var upgradedMockId: Int = 0 - var upgradedMockBackgroundColor: UIColor = .white - var upgradedMockIcon: TelegramMediaFile? - + init( context: AccountContext, subject: GiftViewScreen.Subject, @@ -154,16 +159,21 @@ private final class GiftViewSheetContent: CombinedComponent { super.init() if let arguments = subject.arguments { - if let upgradeStars = arguments.upgradeStars, upgradeStars > 0 { + if let upgradeStars = arguments.upgradeStars, upgradeStars > 0, !arguments.nameHidden { self.keepOriginalInfo = true } - var peerIds: [EnginePeer.Id] = [arguments.peerId, context.account.peerId] + var peerIds: [EnginePeer.Id] = [context.account.peerId] + if let peerId = arguments.peerId { + peerIds.append(peerId) + } if let fromPeerId = arguments.fromPeerId, !peerIds.contains(fromPeerId) { peerIds.append(fromPeerId) } if case let .unique(gift) = arguments.gift { - peerIds.append(gift.ownerPeerId) + if case let .peerId(peerId) = gift.owner { + peerIds.append(peerId) + } for attribute in gift.attributes { if case let .originalInfo(senderPeerId, recipientPeerId, _, _, _) = attribute { if let senderPeerId { @@ -271,11 +281,16 @@ private final class GiftViewSheetContent: CombinedComponent { self.updated(transition: .spring(duration: 0.4)) } + func requestWearPreview() { + self.inWearPreview = true + self.updated(transition: .spring(duration: 0.4)) + } + func commitUpgrade() { - guard let arguments = self.subject.arguments, let starsContext = self.context.starsContext, let starsState = starsContext.currentState else { + guard let arguments = self.subject.arguments, let peerId = arguments.peerId, let starsContext = self.context.starsContext, let starsState = starsContext.currentState else { return } - let peerId = arguments.peerId + let proceed: (Int64?) -> Void = { formId in self.inProgress = true self.updated() @@ -332,10 +347,22 @@ private final class GiftViewSheetContent: CombinedComponent { } static var body: Body { - let closeButton = Child(Button.self) + let buttons = Child(ButtonsComponent.self) let animation = Child(GiftCompositionComponent.self) let title = Child(MultilineTextComponent.self) let description = Child(MultilineTextComponent.self) + + let transferButton = Child(PlainButtonComponent.self) + let wearButton = Child(PlainButtonComponent.self) + let shareButton = Child(PlainButtonComponent.self) + + let wearAvatar = Child(AvatarComponent.self) + let wearPeerName = Child(MultilineTextComponent.self) + let wearPeerStatus = Child(MultilineTextComponent.self) + let wearTitle = Child(MultilineTextComponent.self) + let wearDescription = Child(MultilineTextComponent.self) + let wearPerks = Child(List.self) + let hiddenText = Child(MultilineTextComponent.self) let table = Child(TableComponent.self) let additionalText = Child(MultilineTextComponent.self) @@ -436,22 +463,7 @@ private final class GiftViewSheetContent: CombinedComponent { convertStars = nil titleString = "" } - - let closeImage: UIImage - let closeOverlayImage: UIImage - if let (image, theme) = state.cachedCloseImage, theme === environment.theme { - closeImage = image - } else { - closeImage = generateCloseButtonImage(backgroundColor: UIColor(rgb: 0x808084, alpha: 0.1), foregroundColor: theme.actionSheet.inputClearButtonColor)! - state.cachedCloseImage = (closeImage, theme) - } - if let image = state.cachedOverlayCloseImage { - closeOverlayImage = image - } else { - closeOverlayImage = generateCloseButtonImage(backgroundColor: UIColor(rgb: 0xffffff, alpha: 0.1), foregroundColor: .white)! - state.cachedOverlayCloseImage = closeOverlayImage - } - + var showUpgradePreview = false if state.inUpgradePreview, let _ = state.sampleGiftAttributes { showUpgradePreview = true @@ -460,66 +472,275 @@ private final class GiftViewSheetContent: CombinedComponent { } let cancel = component.cancel - let closeButton = closeButton.update( - component: Button( - content: AnyComponent(Image(image: showUpgradePreview || uniqueGift != nil ? closeOverlayImage : closeImage)), - action: { [weak state] in + let buttons = buttons.update( + component: ButtonsComponent( + theme: theme, + isOverlay: showUpgradePreview || uniqueGift != nil, + showMoreButton: uniqueGift != nil && !state.inWearPreview, + closePressed: { [weak state] in guard let state else { return } - if state.inUpgradePreview { + if state.inWearPreview { + state.inWearPreview = false + state.updated(transition: .spring(duration: 0.4)) + } else if state.inUpgradePreview { state.inUpgradePreview = false state.updated(transition: .spring(duration: 0.4)) } else { cancel(true) } + }, + morePressed: { node, gesture in + component.openMore(node, gesture) } ), availableSize: CGSize(width: 30.0, height: 30.0), - transition: .immediate + transition: context.transition ) var originY: CGFloat = 0.0 - - let animationHeight: CGFloat - let animationSubject: GiftCompositionComponent.Subject? + + let headerHeight: CGFloat + let headerSubject: GiftCompositionComponent.Subject? if let uniqueGift { - animationHeight = 240.0 - animationSubject = .unique(uniqueGift) + if state.inWearPreview { + headerHeight = 200.0 + } else if case let .peerId(peerId) = uniqueGift.owner, peerId == component.context.account.peerId { + headerHeight = 314.0 + } else { + headerHeight = 240.0 + } + headerSubject = .unique(uniqueGift) } else if state.inUpgradePreview, let attributes = state.sampleGiftAttributes { - animationHeight = 258.0 - animationSubject = .preview(attributes) + headerHeight = 258.0 + headerSubject = .preview(attributes) } else if case let .upgradePreview(attributes, _) = component.subject { - animationHeight = 258.0 - animationSubject = .preview(attributes) + headerHeight = 258.0 + headerSubject = .preview(attributes) } else if let animationFile { - animationHeight = 210.0 - animationSubject = .generic(animationFile) + headerHeight = 210.0 + headerSubject = .generic(animationFile) } else { - animationHeight = 210.0 - animationSubject = nil + headerHeight = 210.0 + headerSubject = nil } - if let animationSubject { + + var wearPeerNameChild: _UpdatedChildComponent? + if state.inWearPreview, let uniqueGift { + var peerName = "" + if let accountPeer = state.peerMap[component.context.account.peerId] { + peerName = accountPeer.displayTitle(strings: strings, displayOrder: nameDisplayOrder) + } + wearPeerNameChild = wearPeerName.update( + component: MultilineTextComponent( + text: .plain(NSAttributedString( + string: peerName, + font: Font.bold(28.0), + textColor: .white, + paragraphAlignment: .center + )), + horizontalAlignment: .center, + maximumNumberOfLines: 1 + ), + availableSize: CGSize(width: context.availableSize.width - sideInset * 2.0 - 60.0, height: CGFloat.greatestFiniteMagnitude), + transition: .immediate + ) + + let wearTitle = wearTitle.update( + component: MultilineTextComponent( + text: .plain(NSAttributedString( + string: strings.Gift_Wear_Wear("\(uniqueGift.title) #\(uniqueGift.number)").string, + font: Font.bold(24.0), + textColor: theme.actionSheet.primaryTextColor, + paragraphAlignment: .center + )), + horizontalAlignment: .center, + maximumNumberOfLines: 1 + ), + availableSize: CGSize(width: context.availableSize.width - sideInset * 2.0 - 60.0, height: CGFloat.greatestFiniteMagnitude), + transition: .immediate + ) + let wearDescription = wearDescription.update( + component: MultilineTextComponent( + text: .plain(NSAttributedString( + string: strings.Gift_Wear_GetBenefits, + font: Font.regular(15.0), + textColor: theme.actionSheet.primaryTextColor, + paragraphAlignment: .center + )), + horizontalAlignment: .center, + maximumNumberOfLines: 1, + lineSpacing: 0.2 + ), + availableSize: CGSize(width: context.availableSize.width - sideInset * 2.0 - 50.0, height: CGFloat.greatestFiniteMagnitude), + transition: .immediate + ) + + var titleOriginY = headerHeight + 18.0 + context.add(wearTitle + .position(CGPoint(x: context.availableSize.width / 2.0, y: titleOriginY + wearTitle.size.height)) + .appear(.default(alpha: true)) + .disappear(.default(alpha: true)) + ) + titleOriginY += wearTitle.size.height + titleOriginY += 18.0 + + context.add(wearDescription + .position(CGPoint(x: context.availableSize.width / 2.0, y: titleOriginY + wearDescription.size.height)) + .appear(.default(alpha: true)) + .disappear(.default(alpha: true)) + ) + } + + var animationOffset: CGPoint? + var animationScale: CGFloat? + if let wearPeerNameChild { + animationOffset = CGPoint(x: wearPeerNameChild.size.width / 2.0 + 20.0 - 12.0, y: 56.0) + animationScale = 0.19 + } + + if let headerSubject { let animation = animation.update( component: GiftCompositionComponent( context: component.context, theme: environment.theme, - subject: animationSubject, + subject: headerSubject, + animationOffset: animationOffset, + animationScale: animationScale, + displayAnimationStars: state.inWearPreview, externalState: giftCompositionExternalState, requestUpdate: { [weak state] in state?.updated() } ), - availableSize: CGSize(width: context.availableSize.width, height: animationHeight), - transition: .immediate + availableSize: CGSize(width: context.availableSize.width, height: headerHeight), + transition: context.transition ) context.add(animation - .position(CGPoint(x: context.availableSize.width / 2.0, y: animationHeight / 2.0)) + .position(CGPoint(x: context.availableSize.width / 2.0, y: headerHeight / 2.0)) ) } - originY += animationHeight - - if showUpgradePreview { + originY += headerHeight + + let vibrantColor: UIColor + if let previewPatternColor = giftCompositionExternalState.previewPatternColor { + vibrantColor = previewPatternColor.withMultiplied(hue: 1.0, saturation: 1.02, brightness: 1.25).mixedWith(UIColor.white, alpha: 0.3) + } else { + vibrantColor = UIColor.white.withAlphaComponent(0.6) + } + + if let wearPeerNameChild, let accountPeer = state.peerMap[component.context.account.peerId] { + let wearAvatar = wearAvatar.update( + component: AvatarComponent( + context: component.context, + theme: theme, + peer: accountPeer + ), + environment: {}, + availableSize: CGSize(width: 100.0, height: 100.0), + transition: context.transition + ) + context.add(wearAvatar + .position(CGPoint(x: context.availableSize.width / 2.0, y: 67.0)) + .appear(.default(scale: true, alpha: true)) + .disappear(.default(scale: true, alpha: true)) + ) + + let wearPeerStatus = wearPeerStatus.update( + component: MultilineTextComponent( + text: .plain(NSAttributedString( + string: strings.Presence_online, + font: Font.regular(17.0), + textColor: vibrantColor, + paragraphAlignment: .center + )), + horizontalAlignment: .center, + maximumNumberOfLines: 5, + lineSpacing: 0.2 + ), + availableSize: CGSize(width: context.availableSize.width - sideInset * 2.0 - 50.0, height: CGFloat.greatestFiniteMagnitude), + transition: .immediate + ) + + context.add(wearPeerNameChild + .position(CGPoint(x: context.availableSize.width / 2.0 - 12.0, y: 144.0)) + .appear(.default(alpha: true)) + .disappear(.default(alpha: true)) + ) + context.add(wearPeerStatus + .position(CGPoint(x: context.availableSize.width / 2.0, y: 174.0)) + .appear(.default(alpha: true)) + .disappear(.default(alpha: true)) + ) + originY += 18.0 + originY += 28.0 + originY += 18.0 + originY += 20.0 + originY += 24.0 + + let textColor = theme.actionSheet.primaryTextColor + let secondaryTextColor = theme.actionSheet.secondaryTextColor + let linkColor = theme.actionSheet.controlAccentColor + + var items: [AnyComponentWithIdentity] = [] + items.append( + AnyComponentWithIdentity( + id: "badge", + component: AnyComponent(ParagraphComponent( + title: strings.Gift_Wear_Badge_Title, + titleColor: textColor, + text: strings.Gift_Wear_Badge_Text, + textColor: secondaryTextColor, + accentColor: linkColor, + iconName: "Premium/Collectible/Badge", + iconColor: linkColor + )) + ) + ) + items.append( + AnyComponentWithIdentity( + id: "design", + component: AnyComponent(ParagraphComponent( + title: strings.Gift_Wear_Design_Title, + titleColor: textColor, + text: strings.Gift_Wear_Design_Text, + textColor: secondaryTextColor, + accentColor: linkColor, + iconName: "Premium/BoostPerk/CoverColor", + iconColor: linkColor + )) + ) + ) + items.append( + AnyComponentWithIdentity( + id: "proof", + component: AnyComponent(ParagraphComponent( + title: strings.Gift_Wear_Proof_Title, + titleColor: textColor, + text: strings.Gift_Wear_Proof_Text, + textColor: secondaryTextColor, + accentColor: linkColor, + iconName: "Premium/Collectible/Proof", + iconColor: linkColor + )) + ) + ) + + let perksSideInset = sideInset + 16.0 + let wearPerks = wearPerks.update( + component: List(items), + availableSize: CGSize(width: context.availableSize.width - perksSideInset * 2.0, height: 10000.0), + transition: context.transition + ) + context.add(wearPerks + .position(CGPoint(x: context.availableSize.width / 2.0, y: originY + wearPerks.size.height / 2.0)) + .appear(.default(alpha: true)) + .disappear(.default(alpha: true)) + ) + originY += wearPerks.size.height + originY += 16.0 + } else if showUpgradePreview { let title: String let description: String let uniqueText: String @@ -553,18 +774,12 @@ private final class GiftViewSheetContent: CombinedComponent { availableSize: CGSize(width: context.availableSize.width - sideInset * 2.0 - 60.0, height: CGFloat.greatestFiniteMagnitude), transition: .immediate ) - let descriptionColor: UIColor - if let previewPatternColor = giftCompositionExternalState.previewPatternColor { - descriptionColor = previewPatternColor.withMultiplied(hue: 1.0, saturation: 1.02, brightness: 1.25).mixedWith(UIColor.white, alpha: 0.3) - } else { - descriptionColor = UIColor.white.withAlphaComponent(0.6) - } let upgradeDescription = upgradeDescription.update( component: BalancedTextComponent( text: .plain(NSAttributedString( string: description, font: Font.regular(13.0), - textColor: descriptionColor, + textColor: vibrantColor, paragraphAlignment: .center )), horizontalAlignment: .center, @@ -640,7 +855,7 @@ private final class GiftViewSheetContent: CombinedComponent { ) ) - let perksSideInset = sideInset + 12.0 + let perksSideInset = sideInset + 16.0 let upgradePerks = upgradePerks.update( component: List(items), availableSize: CGSize(width: context.availableSize.width - perksSideInset * 2.0, height: 10000.0), @@ -777,11 +992,6 @@ private final class GiftViewSheetContent: CombinedComponent { .disappear(.default(alpha: true)) ) -// originY += 32.0 -// if soldOut { -// originY -= 12.0 -// } - if !descriptionText.isEmpty { let linkColor = theme.actionSheet.controlAccentColor if state.cachedChevronImage == nil || state.cachedChevronImage?.1 !== environment.theme { @@ -792,11 +1002,7 @@ private final class GiftViewSheetContent: CombinedComponent { let textColor: UIColor if let _ = uniqueGift { textFont = Font.regular(13.0) - if let previewPatternColor = giftCompositionExternalState.previewPatternColor { - textColor = previewPatternColor.withMultiplied(hue: 1.0, saturation: 1.02, brightness: 1.25).mixedWith(UIColor.white, alpha: 0.3) - } else { - textColor = UIColor.white.withAlphaComponent(0.6) - } + textColor = vibrantColor } else { textFont = soldOut ? Font.medium(15.0) : Font.regular(15.0) textColor = soldOut ? theme.list.itemDestructiveColor : theme.list.itemPrimaryTextColor @@ -845,7 +1051,7 @@ private final class GiftViewSheetContent: CombinedComponent { } } } else { - originY += 21.0 + originY += 9.0 } if nameHidden && uniqueGift == nil { @@ -855,28 +1061,30 @@ private final class GiftViewSheetContent: CombinedComponent { let hiddenDescription: String if incoming { hiddenDescription = text != nil ? strings.Gift_View_NameAndMessageHidden : strings.Gift_View_NameHidden - } else if let peerId = subject.arguments?.peerId, let peer = state.peerMap[peerId] { + } else if let peerId = subject.arguments?.peerId, let peer = state.peerMap[peerId], subject.arguments?.fromPeerId != nil { hiddenDescription = text != nil ? strings.Gift_View_Outgoing_NameAndMessageHidden(peer.compactDisplayTitle).string : strings.Gift_View_Outgoing_NameHidden(peer.compactDisplayTitle).string } else { hiddenDescription = "" } - - let hiddenText = hiddenText.update( - component: MultilineTextComponent( - text: .plain(NSAttributedString(string: hiddenDescription, font: textFont, textColor: textColor)), - horizontalAlignment: .center, - maximumNumberOfLines: 2, - lineSpacing: 0.2 - ), - availableSize: CGSize(width: context.availableSize.width - sideInset * 2.0 - 60.0, height: CGFloat.greatestFiniteMagnitude), - transition: .immediate - ) - context.add(hiddenText - .position(CGPoint(x: context.availableSize.width / 2.0, y: originY)) - ) - - originY += hiddenText.size.height - originY += 11.0 + + if !hiddenDescription.isEmpty { + let hiddenText = hiddenText.update( + component: MultilineTextComponent( + text: .plain(NSAttributedString(string: hiddenDescription, font: textFont, textColor: textColor)), + horizontalAlignment: .center, + maximumNumberOfLines: 2, + lineSpacing: 0.2 + ), + availableSize: CGSize(width: context.availableSize.width - sideInset * 2.0 - 60.0, height: CGFloat.greatestFiniteMagnitude), + transition: .immediate + ) + context.add(hiddenText + .position(CGPoint(x: context.availableSize.width / 2.0, y: originY)) + ) + + originY += hiddenText.size.height + originY += 11.0 + } } let tableFont = Font.regular(15.0) @@ -889,72 +1097,116 @@ private final class GiftViewSheetContent: CombinedComponent { let tableLinkColor = theme.list.itemAccentColor var tableItems: [TableComponent.Item] = [] + var isWearing = state.pendingWear + if !soldOut { if let uniqueGift { - if let peer = state.peerMap[uniqueGift.ownerPeerId] { - let ownerComponent: AnyComponent - if let _ = subject.arguments?.transferStars { - ownerComponent = AnyComponent( - HStack([ - AnyComponentWithIdentity( - id: AnyHashable(0), - component: AnyComponent(Button( - content: AnyComponent( - PeerCellComponent( - context: component.context, - theme: theme, - strings: strings, - peer: peer - ) - ), - action: { - component.openPeer(peer) - Queue.mainQueue().after(1.0, { - component.cancel(false) - }) - } - )) - ), - AnyComponentWithIdentity( - id: AnyHashable(1), - component: AnyComponent(Button( - content: AnyComponent(ButtonContentComponent( - context: component.context, - text: strings.Gift_Unique_Transfer, - color: theme.list.itemAccentColor - )), - action: { - component.transferGift() - Queue.mainQueue().after(1.0, { - component.cancel(false) - }) - } - )) - ) - ], spacing: 4.0) - ) - } else { - ownerComponent = AnyComponent(Button( - content: AnyComponent( - PeerCellComponent( - context: component.context, - theme: theme, - strings: strings, - peer: peer - ) - ), - action: { - component.openPeer(peer) - Queue.mainQueue().after(1.0, { - component.cancel(false) - }) + switch uniqueGift.owner { + case let .peerId(peerId): + if let peer = state.peerMap[peerId] { + let ownerComponent: AnyComponent + if peer.id == component.context.account.peerId, peer.isPremium { + let animationContent: EmojiStatusComponent.Content + var color: UIColor? + if state.pendingWear { + var fileId: Int64? + for attribute in uniqueGift.attributes { + if case let .model(_, file, _) = attribute { + fileId = file.fileId.id + } + if case let .backdrop(_, innerColor, _, _, _, _) = attribute { + color = UIColor(rgb: UInt32(bitPattern: innerColor)) + } + } + if let fileId { + animationContent = .animation(content: .customEmoji(fileId: fileId), size: CGSize(width: 18.0, height: 18.0), placeholderColor: theme.list.mediaPlaceholderColor, themeColor: tableLinkColor, loopMode: .count(2)) + } else { + animationContent = .premium(color: tableLinkColor) + } + } else if let emojiStatus = peer.emojiStatus, !state.pendingTakeOff { + animationContent = .animation(content: .customEmoji(fileId: emojiStatus.fileId), size: CGSize(width: 18.0, height: 18.0), placeholderColor: theme.list.mediaPlaceholderColor, themeColor: tableLinkColor, loopMode: .count(2)) + if case let .starGift(id, _, _, _, _, innerColor, _, _, _) = emojiStatus.content { + color = UIColor(rgb: UInt32(bitPattern: innerColor)) + if id == uniqueGift.id { + isWearing = true + state.pendingWear = false + } + } + } else { + animationContent = .premium(color: tableLinkColor) + state.pendingTakeOff = false } + + ownerComponent = AnyComponent( + HStack([ + AnyComponentWithIdentity( + id: AnyHashable(0), + component: AnyComponent(Button( + content: AnyComponent( + PeerCellComponent( + context: component.context, + theme: theme, + strings: strings, + peer: peer + ) + ), + action: { + component.openPeer(peer) + Queue.mainQueue().after(1.0, { + component.cancel(false) + }) + } + )) + ), + AnyComponentWithIdentity( + id: AnyHashable(1), + component: AnyComponent(EmojiStatusComponent( + context: component.context, + animationCache: component.context.animationCache, + animationRenderer: component.context.animationRenderer, + content: animationContent, + particleColor: color, + size: CGSize(width: 18.0, height: 18.0), + isVisibleForAnimations: true, + action: { + + }, + tag: statusTag + )) + ) + ], spacing: 2.0) + ) + } else { + ownerComponent = AnyComponent(Button( + content: AnyComponent( + PeerCellComponent( + context: component.context, + theme: theme, + strings: strings, + peer: peer + ) + ), + action: { + component.openPeer(peer) + Queue.mainQueue().after(1.0, { + component.cancel(false) + }) + } + )) + } + tableItems.append(.init( + id: "owner", + title: strings.Gift_Unique_Owner, + component: ownerComponent )) } + case let .name(name): tableItems.append(.init( - id: "owner", + id: "anon_owner", title: strings.Gift_Unique_Owner, - component: ownerComponent + component: AnyComponent( + MultilineTextComponent(text: .plain(NSAttributedString(string: name, font: tableFont, textColor: tableTextColor))) + ) )) } } else if let peerId = subject.arguments?.fromPeerId, let peer = state.peerMap[peerId] { @@ -1047,6 +1299,93 @@ private final class GiftViewSheetContent: CombinedComponent { } if let uniqueGift { + if case let .peerId(peerId) = uniqueGift.owner, peerId == component.context.account.peerId { + let buttonSpacing: CGFloat = 10.0 + let buttonWidth = floor(context.availableSize.width - sideInset * 2.0 - buttonSpacing * 2.0) / 3.0 + let buttonHeight: CGFloat = 58.0 + + let transferButton = transferButton.update( + component: PlainButtonComponent( + content: AnyComponent( + HeaderButtonComponent( + title: strings.Gift_View_Header_Transfer, + iconName: "Premium/Collectible/Transfer" + ) + ), + effectAlignment: .center, + action: { + component.transferGift() + Queue.mainQueue().after(1.0, { + component.cancel(false) + }) + } + ), + environment: {}, + availableSize: CGSize(width: buttonWidth, height: buttonHeight), + transition: context.transition + ) + context.add(transferButton + .position(CGPoint(x: sideInset + buttonWidth / 2.0, y: headerHeight - buttonHeight / 2.0 - 16.0)) + ) + + let controller = environment.controller + let wearButton = wearButton.update( + component: PlainButtonComponent( + content: AnyComponent( + HeaderButtonComponent( + title: isWearing ? strings.Gift_View_Header_TakeOff : strings.Gift_View_Header_Wear, + iconName: isWearing ? "Premium/Collectible/Unwear" : "Premium/Collectible/Wear" + ) + ), + effectAlignment: .center, + action: { [weak state] in + if let state { + if isWearing { + state.pendingTakeOff = true + state.pendingWear = false + state.updated(transition: .spring(duration: 0.4)) + + component.showAttributeInfo(statusTag, "You took off \(uniqueGift.title) #\(uniqueGift.number)") + } else { + if let controller = controller() as? GiftViewScreen { + controller.dismissAllTooltips() + } + + state.requestWearPreview() + } + } + } + ), + environment: {}, + availableSize: CGSize(width: buttonWidth, height: buttonHeight), + transition: context.transition + ) + context.add(wearButton + .position(CGPoint(x: context.availableSize.width / 2.0, y: headerHeight - buttonHeight / 2.0 - 16.0)) + ) + + let shareButton = shareButton.update( + component: PlainButtonComponent( + content: AnyComponent( + HeaderButtonComponent( + title: strings.Gift_View_Header_Share, + iconName: "Premium/Collectible/Share" + ) + ), + effectAlignment: .center, + action: { + component.shareGift() + } + ), + environment: {}, + availableSize: CGSize(width: buttonWidth, height: buttonHeight), + transition: context.transition + ) + context.add(shareButton + .position(CGPoint(x: context.availableSize.width - sideInset - buttonWidth / 2.0, y: headerHeight - buttonHeight / 2.0 - 16.0)) + ) + } + let showAttributeInfo = component.showAttributeInfo let order: [StarGift.UniqueGift.Attribute.AttributeType] = [ @@ -1108,21 +1447,27 @@ private final class GiftViewSheetContent: CombinedComponent { let format = senderName != nil ? strings.Gift_Unique_OriginalInfoSenderWithText(senderName!, recipientName, dateString, "") : strings.Gift_Unique_OriginalInfoWithText(recipientName, dateString, "") let string = NSMutableAttributedString(string: format.string, font: tableFont, textColor: tableTextColor) string.replaceCharacters(in: format.ranges[format.ranges.count - 1].range, with: attributedText) - if let _ = senderName { - string.addAttribute(NSAttributedString.Key.foregroundColor, value: tableLinkColor, range: format.ranges[0].range) - string.addAttribute(NSAttributedString.Key.foregroundColor, value: tableLinkColor, range: format.ranges[1].range) + if let senderPeerId { + string.addAttribute(.foregroundColor, value: tableLinkColor, range: format.ranges[0].range) + string.addAttribute(NSAttributedString.Key(rawValue: TelegramTextAttributes.PeerMention), value: TelegramPeerMention(peerId: senderPeerId, mention: ""), range: format.ranges[0].range) + string.addAttribute(.foregroundColor, value: tableLinkColor, range: format.ranges[1].range) + string.addAttribute(NSAttributedString.Key(rawValue: TelegramTextAttributes.PeerMention), value: TelegramPeerMention(peerId: recipientPeerId, mention: ""), range: format.ranges[1].range) } else { - string.addAttribute(NSAttributedString.Key.foregroundColor, value: tableLinkColor, range: format.ranges[0].range) + string.addAttribute(.foregroundColor, value: tableLinkColor, range: format.ranges[0].range) + string.addAttribute(NSAttributedString.Key(rawValue: TelegramTextAttributes.PeerMention), value: TelegramPeerMention(peerId: recipientPeerId, mention: ""), range: format.ranges[0].range) } value = string } else { let format = senderName != nil ? strings.Gift_Unique_OriginalInfoSender(senderName!, recipientName, dateString) : strings.Gift_Unique_OriginalInfo(recipientName, dateString) let string = NSMutableAttributedString(string: format.string, font: tableFont, textColor: tableTextColor) - if let _ = senderName { - string.addAttribute(NSAttributedString.Key.foregroundColor, value: tableLinkColor, range: format.ranges[0].range) - string.addAttribute(NSAttributedString.Key.foregroundColor, value: tableLinkColor, range: format.ranges[1].range) + if let senderPeerId { + string.addAttribute(.foregroundColor, value: tableLinkColor, range: format.ranges[0].range) + string.addAttribute(NSAttributedString.Key(rawValue: TelegramTextAttributes.PeerMention), value: TelegramPeerMention(peerId: senderPeerId, mention: ""), range: format.ranges[0].range) + string.addAttribute(.foregroundColor, value: tableLinkColor, range: format.ranges[1].range) + string.addAttribute(NSAttributedString.Key(rawValue: TelegramTextAttributes.PeerMention), value: TelegramPeerMention(peerId: recipientPeerId, mention: ""), range: format.ranges[1].range) } else { - string.addAttribute(NSAttributedString.Key.foregroundColor, value: tableLinkColor, range: format.ranges[0].range) + string.addAttribute(.foregroundColor, value: tableLinkColor, range: format.ranges[0].range) + string.addAttribute(NSAttributedString.Key(rawValue: TelegramTextAttributes.PeerMention), value: TelegramPeerMention(peerId: recipientPeerId, mention: ""), range: format.ranges[0].range) } value = string @@ -1146,7 +1491,26 @@ private final class GiftViewSheetContent: CombinedComponent { horizontalAlignment: .center, maximumNumberOfLines: 0, insets: id == "originalInfo" ? UIEdgeInsets(top: 2.0, left: 0.0, bottom: 2.0, right: 0.0) : .zero, - handleSpoilers: true + highlightColor: tableLinkColor.withAlphaComponent(0.1), + handleSpoilers: true, + highlightAction: { attributes in + if let _ = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.PeerMention)] { + return NSAttributedString.Key(rawValue: TelegramTextAttributes.PeerMention) + } else { + return nil + } + }, + tapAction: { [weak state] attributes, _ in + guard let state else { + return + } + if let mention = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.PeerMention)] as? TelegramPeerMention, let peer = state.peerMap[mention.peerId] { + component.openPeer(peer) + Queue.mainQueue().after(1.0, { + component.cancel(false) + }) + } + } ) ) ) @@ -1161,7 +1525,7 @@ private final class GiftViewSheetContent: CombinedComponent { color: theme.list.itemAccentColor )), action: { - showAttributeInfo(tag, percentage) + showAttributeInfo(tag, strings.Gift_Unique_AttributeDescription(formatPercentage(percentage)).string) } ).tagged(tag)) )) @@ -1178,11 +1542,13 @@ private final class GiftViewSheetContent: CombinedComponent { } } + let issuedString = presentationStringsFormattedNumber(uniqueGift.availability.issued, environment.dateTimeFormat.groupingSeparator) + let totalString = presentationStringsFormattedNumber(uniqueGift.availability.total, environment.dateTimeFormat.groupingSeparator) tableItems.insert(.init( id: "availability", title: strings.Gift_Unique_Availability, component: AnyComponent( - MultilineTextComponent(text: .plain(NSAttributedString(string: strings.Gift_Unique_Issued("\(uniqueGift.availability.issued)/\(uniqueGift.availability.total)").string, font: tableFont, textColor: tableTextColor))) + MultilineTextComponent(text: .plain(NSAttributedString(string: strings.Gift_Unique_Issued("\(issuedString)/\(totalString)").string, font: tableFont, textColor: tableTextColor))) ) ), at: hasOriginalInfo ? tableItems.count - 1 : tableItems.count) } else { @@ -1361,7 +1727,7 @@ private final class GiftViewSheetContent: CombinedComponent { originY += table.size.height + 23.0 } - if incoming && !converted && !upgraded && !showUpgradePreview { + if incoming && !converted && !upgraded && !showUpgradePreview && !state.inWearPreview { 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) @@ -1420,8 +1786,41 @@ private final class GiftViewSheetContent: CombinedComponent { originY += 16.0 } + let buttonSize = CGSize(width: context.availableSize.width - sideInset * 2.0, height: 50.0) + let buttonBackground = ButtonComponent.Background( + color: theme.list.itemCheckColors.fillColor, + foreground: theme.list.itemCheckColors.foregroundColor, + pressedColor: theme.list.itemCheckColors.fillColor.withMultipliedAlpha(0.9) + ) let buttonChild: _UpdatedChildComponent - if state.inUpgradePreview { + if state.inWearPreview, let uniqueGift { + buttonChild = button.update( + component: ButtonComponent( + background: buttonBackground, + content: AnyComponentWithIdentity( + id: AnyHashable("wear"), + component: AnyComponent(MultilineTextComponent(text: .plain(NSAttributedString(string: strings.Gift_Wear_Start, font: Font.semibold(17.0), textColor: theme.list.itemCheckColors.foregroundColor, paragraphAlignment: .center)))) + ), + isEnabled: true, + displaysProgress: false, + action: { [weak state] in + if let state { + state.pendingWear = true + state.pendingTakeOff = false + state.inWearPreview = false + state.updated(transition: .spring(duration: 0.4)) + + let _ = component.context.engine.accountData.setStarGiftStatus(starGift: uniqueGift, expirationDate: nil).start() + + Queue.mainQueue().after(0.2) { + component.showAttributeInfo(statusTag, "You put on \(uniqueGift.title) #\(uniqueGift.number)") + } + } + }), + availableSize: buttonSize, + transition: context.transition + ) + } else if state.inUpgradePreview { if state.cachedStarImage == nil || state.cachedStarImage?.1 !== theme { state.cachedStarImage = (generateTintedImage(image: UIImage(bundleImageName: "Item List/PremiumIcon"), color: theme.list.itemCheckColors.foregroundColor)!, theme) } @@ -1438,12 +1837,7 @@ private final class GiftViewSheetContent: CombinedComponent { } buttonChild = button.update( component: ButtonComponent( - background: ButtonComponent.Background( - color: theme.list.itemCheckColors.fillColor, - foreground: theme.list.itemCheckColors.foregroundColor, - pressedColor: theme.list.itemCheckColors.fillColor.withMultipliedAlpha(0.9), - cornerRadius: 10.0 - ), + background: buttonBackground, content: AnyComponentWithIdentity( id: AnyHashable("upgrade"), component: AnyComponent(MultilineTextComponent(text: .plain(buttonAttributedString))) @@ -1453,7 +1847,7 @@ private final class GiftViewSheetContent: CombinedComponent { action: { [weak state] in state?.commitUpgrade() }), - availableSize: CGSize(width: context.availableSize.width - sideInset * 2.0, height: 50.0), + availableSize: buttonSize, transition: context.transition ) } else if upgraded, let upgradeMessageIdId = subject.arguments?.upgradeMessageId, let originalMessageId = subject.arguments?.messageId { @@ -1461,12 +1855,7 @@ private final class GiftViewSheetContent: CombinedComponent { let buttonTitle = strings.Gift_View_ViewUpgraded buttonChild = button.update( component: ButtonComponent( - background: ButtonComponent.Background( - color: theme.list.itemCheckColors.fillColor, - foreground: theme.list.itemCheckColors.foregroundColor, - pressedColor: theme.list.itemCheckColors.fillColor.withMultipliedAlpha(0.9), - cornerRadius: 10.0 - ), + background: buttonBackground, content: AnyComponentWithIdentity( id: AnyHashable("button"), component: AnyComponent(MultilineTextComponent(text: .plain(NSAttributedString(string: buttonTitle, font: Font.semibold(17.0), textColor: theme.list.itemCheckColors.foregroundColor, paragraphAlignment: .center)))) @@ -1477,20 +1866,14 @@ private final class GiftViewSheetContent: CombinedComponent { component.cancel(true) component.viewUpgraded(upgradeMessageId) }), - availableSize: CGSize(width: context.availableSize.width - sideInset * 2.0, height: 50.0), + availableSize: buttonSize, transition: context.transition ) } else if incoming && !converted && !upgraded, let upgradeStars, upgradeStars > 0 { let buttonTitle = strings.Gift_View_UpgradeForFree buttonChild = button.update( component: ButtonComponent( - background: ButtonComponent.Background( - color: theme.list.itemCheckColors.fillColor, - foreground: theme.list.itemCheckColors.foregroundColor, - pressedColor: theme.list.itemCheckColors.fillColor.withMultipliedAlpha(0.9), - cornerRadius: 10.0, - isShimmering: true - ), + background: buttonBackground.withIsShimmering(true), content: AnyComponentWithIdentity( id: AnyHashable("freeUpgrade"), component: AnyComponent(HStack([ @@ -1514,19 +1897,14 @@ private final class GiftViewSheetContent: CombinedComponent { state?.requestUpgradePreview() } ), - availableSize: CGSize(width: context.availableSize.width - sideInset * 2.0, height: 50.0), + availableSize: buttonSize, transition: context.transition ) } else if incoming && !converted && !savedToProfile { let buttonTitle = savedToProfile ? strings.Gift_View_Hide : strings.Gift_View_Display buttonChild = button.update( component: ButtonComponent( - background: ButtonComponent.Background( - color: theme.list.itemCheckColors.fillColor, - foreground: theme.list.itemCheckColors.foregroundColor, - pressedColor: theme.list.itemCheckColors.fillColor.withMultipliedAlpha(0.9), - cornerRadius: 10.0 - ), + background: buttonBackground, content: AnyComponentWithIdentity( id: AnyHashable("button"), component: AnyComponent(MultilineTextComponent(text: .plain(NSAttributedString(string: buttonTitle, font: Font.semibold(17.0), textColor: theme.list.itemCheckColors.foregroundColor, paragraphAlignment: .center)))) @@ -1536,18 +1914,13 @@ private final class GiftViewSheetContent: CombinedComponent { action: { component.updateSavedToProfile(!savedToProfile) }), - availableSize: CGSize(width: context.availableSize.width - sideInset * 2.0, height: 50.0), + availableSize: buttonSize, transition: context.transition ) } else { buttonChild = button.update( component: ButtonComponent( - background: ButtonComponent.Background( - color: theme.list.itemCheckColors.fillColor, - foreground: theme.list.itemCheckColors.foregroundColor, - pressedColor: theme.list.itemCheckColors.fillColor.withMultipliedAlpha(0.9), - cornerRadius: 10.0 - ), + background: buttonBackground, content: AnyComponentWithIdentity( id: AnyHashable("ok"), component: AnyComponent(MultilineTextComponent(text: .plain(NSAttributedString(string: strings.Common_OK, font: Font.semibold(17.0), textColor: theme.list.itemCheckColors.foregroundColor, paragraphAlignment: .center)))) @@ -1557,7 +1930,7 @@ private final class GiftViewSheetContent: CombinedComponent { action: { component.cancel(true) }), - availableSize: CGSize(width: context.availableSize.width - sideInset * 2.0, height: 50.0), + availableSize: buttonSize, transition: context.transition ) } @@ -1569,8 +1942,8 @@ private final class GiftViewSheetContent: CombinedComponent { originY += buttonChild.size.height originY += 7.0 - context.add(closeButton - .position(CGPoint(x: context.availableSize.width - environment.safeInsets.left - closeButton.size.width, y: 28.0)) + context.add(buttons + .position(CGPoint(x: context.availableSize.width - environment.safeInsets.left - 16.0 - buttons.size.width / 2.0, y: 28.0)) ) let contentSize = CGSize(width: context.availableSize.width, height: originY + 5.0 + environment.safeInsets.bottom) @@ -1593,8 +1966,10 @@ private final class GiftViewSheetComponent: CombinedComponent { let openMyGifts: () -> Void let transferGift: () -> Void let upgradeGift: ((Int64?, Bool) -> Signal) + let shareGift: () -> Void let viewUpgraded: (EngineMessage.Id) -> Void - let showAttributeInfo: (Any, Float) -> Void + let openMore: (ASDisplayNode, ContextGesture?) -> Void + let showAttributeInfo: (Any, String) -> Void init( context: AccountContext, @@ -1607,8 +1982,10 @@ private final class GiftViewSheetComponent: CombinedComponent { openMyGifts: @escaping () -> Void, transferGift: @escaping () -> Void, upgradeGift: @escaping ((Int64?, Bool) -> Signal), + shareGift: @escaping () -> Void, viewUpgraded: @escaping (EngineMessage.Id) -> Void, - showAttributeInfo: @escaping (Any, Float) -> Void + openMore: @escaping (ASDisplayNode, ContextGesture?) -> Void, + showAttributeInfo: @escaping (Any, String) -> Void ) { self.context = context self.subject = subject @@ -1620,7 +1997,9 @@ private final class GiftViewSheetComponent: CombinedComponent { self.openMyGifts = openMyGifts self.transferGift = transferGift self.upgradeGift = upgradeGift + self.shareGift = shareGift self.viewUpgraded = viewUpgraded + self.openMore = openMore self.showAttributeInfo = showAttributeInfo } @@ -1669,8 +2048,10 @@ private final class GiftViewSheetComponent: CombinedComponent { openMyGifts: context.component.openMyGifts, transferGift: context.component.transferGift, upgradeGift: context.component.upgradeGift, + shareGift: context.component.shareGift, showAttributeInfo: context.component.showAttributeInfo, viewUpgraded: context.component.viewUpgraded, + openMore: context.component.openMore, getController: controller )), backgroundColor: .color(environment.theme.actionSheet.opaqueItemBackgroundColor), @@ -1740,11 +2121,12 @@ private final class GiftViewSheetComponent: CombinedComponent { public class GiftViewScreen: ViewControllerComponentContainer { public enum Subject: Equatable { case message(EngineMessage) + case uniqueGift(StarGift.UniqueGift) case profileGift(EnginePeer.Id, ProfileGiftsContext.State.StarGift) case soldOutGift(StarGift.Gift) case upgradePreview([StarGift.UniqueGift.Attribute], String) - var arguments: (peerId: EnginePeer.Id, fromPeerId: EnginePeer.Id?, fromPeerName: String?, messageId: EngineMessage.Id?, incoming: Bool, gift: StarGift, date: Int32, convertStars: Int64?, text: String?, entities: [MessageTextEntity]?, nameHidden: Bool, savedToProfile: Bool, converted: Bool, upgraded: Bool, canUpgrade: Bool, upgradeStars: Int64?, transferStars: Int64?, canExportDate: Int32?, upgradeMessageId: Int32?)? { + var arguments: (peerId: EnginePeer.Id?, fromPeerId: EnginePeer.Id?, fromPeerName: String?, messageId: EngineMessage.Id?, incoming: Bool, gift: StarGift, date: Int32, convertStars: Int64?, text: String?, entities: [MessageTextEntity]?, nameHidden: Bool, savedToProfile: Bool, converted: Bool, upgraded: Bool, canUpgrade: Bool, upgradeStars: Int64?, transferStars: Int64?, canExportDate: Int32?, upgradeMessageId: Int32?)? { switch self { case let .message(message): if let action = message.media.first(where: { $0 is TelegramMediaAction }) as? TelegramMediaAction { @@ -1769,6 +2151,8 @@ public class GiftViewScreen: ViewControllerComponentContainer { return nil } } + case let .uniqueGift(gift): + return (nil, nil, nil, nil, false, .unique(gift), 0, nil, nil, nil, false, false, false, false, false, nil, nil, nil, nil) case let .profileGift(peerId, gift): return (peerId, gift.fromPeer?.id, gift.fromPeer?.compactDisplayTitle, gift.messageId, false, gift.gift, gift.date, gift.convertStars, gift.text, gift.entities, gift.nameHidden, gift.savedToProfile, false, false, gift.canUpgrade, gift.upgradeStars, gift.transferStars, gift.canExportDate, nil) case .soldOutGift: @@ -1793,7 +2177,8 @@ public class GiftViewScreen: ViewControllerComponentContainer { updateSavedToProfile: ((EngineMessage.Id, Bool) -> Void)? = nil, convertToStars: (() -> Void)? = nil, transferGift: ((Bool, EnginePeer.Id) -> Void)? = nil, - upgradeGift: ((Int64?, Bool) -> Signal)? = nil + upgradeGift: ((Int64?, Bool) -> Signal)? = nil, + shareStory: (() -> Void)? = nil ) { self.context = context self.subject = subject @@ -1805,8 +2190,10 @@ public class GiftViewScreen: ViewControllerComponentContainer { var sendGiftImpl: ((EnginePeer.Id) -> Void)? var openMyGiftsImpl: (() -> Void)? var transferGiftImpl: (() -> Void)? - var showAttributeInfoImpl: ((Any, Float) -> Void)? var upgradeGiftImpl: ((Int64?, Bool) -> Signal)? + var shareGiftImpl: (() -> Void)? + var openMoreImpl: ((ASDisplayNode, ContextGesture?) -> Void)? + var showAttributeInfoImpl: ((Any, String) -> Void)? var viewUpgradedImpl: ((EngineMessage.Id) -> Void)? super.init( @@ -1838,11 +2225,17 @@ public class GiftViewScreen: ViewControllerComponentContainer { upgradeGift: { formId, keepOriginalInfo in return upgradeGiftImpl?(formId, keepOriginalInfo) ?? .complete() }, + shareGift: { + shareGiftImpl?() + }, viewUpgraded: { messageId in viewUpgradedImpl?(messageId) }, - showAttributeInfo: { tag, rarity in - showAttributeInfoImpl?(tag, rarity) + openMore: { node, gesture in + openMoreImpl?(node, gesture) + }, + showAttributeInfo: { tag, text in + showAttributeInfoImpl?(tag, text) } ), navigationBarAppearance: .none, @@ -2096,6 +2489,74 @@ public class GiftViewScreen: ViewControllerComponentContainer { } } + shareGiftImpl = { [weak self] in + guard let self, let arguments = self.subject.arguments, case let .unique(gift) = arguments.gift else { + return + } + let link = "https://t.me/nft/\(gift.slug)" + let shareController = context.sharedContext.makeShareController( + context: context, + subject: .url(link), + forceExternal: false, + shareStory: shareStory, + enqueued: { peerIds, _ in + let _ = (context.engine.data.get( + EngineDataList( + peerIds.map(TelegramEngine.EngineData.Item.Peer.Peer.init) + ) + ) + |> deliverOnMainQueue).startStandalone(next: { [weak self] peerList in + let peers = peerList.compactMap { $0 } + let presentationData = context.sharedContext.currentPresentationData.with { $0 } + let text: String + var savedMessages = false + if peerIds.count == 1, let peerId = peerIds.first, peerId == context.account.peerId { + text = presentationData.strings.Conversation_ForwardTooltip_SavedMessages_One + savedMessages = true + } else { + if peers.count == 1, let peer = peers.first { + var peerName = peer.id == context.account.peerId ? presentationData.strings.DialogList_SavedMessages : peer.displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder) + peerName = peerName.replacingOccurrences(of: "**", with: "") + text = presentationData.strings.Conversation_ForwardTooltip_Chat_One(peerName).string + } else if peers.count == 2, let firstPeer = peers.first, let secondPeer = peers.last { + var firstPeerName = firstPeer.id == context.account.peerId ? presentationData.strings.DialogList_SavedMessages : firstPeer.displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder) + firstPeerName = firstPeerName.replacingOccurrences(of: "**", with: "") + var secondPeerName = secondPeer.id == context.account.peerId ? presentationData.strings.DialogList_SavedMessages : secondPeer.displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder) + secondPeerName = secondPeerName.replacingOccurrences(of: "**", with: "") + text = presentationData.strings.Conversation_ForwardTooltip_TwoChats_One(firstPeerName, secondPeerName).string + } else if let peer = peers.first { + var peerName = peer.displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder) + peerName = peerName.replacingOccurrences(of: "**", with: "") + text = presentationData.strings.Conversation_ForwardTooltip_ManyChats_One(peerName, "\(peers.count - 1)").string + } else { + text = "" + } + } + + self?.present(UndoOverlayController(presentationData: presentationData, content: .forward(savedMessages: savedMessages, text: text), elevatedLayout: false, animateInAsReplacement: false, action: { action in + if savedMessages, action == .info { + let _ = (context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: context.account.peerId)) + |> deliverOnMainQueue).start(next: { peer in + guard let peer else { + return + } + openPeerImpl?(peer) + Queue.mainQueue().after(1.0) { + self?.dismiss(animated: false, completion: nil) + } + }) + } + return false + }, additionalView: nil), in: .current) + }) + }, + actionCompleted: { [weak self] in + self?.present(UndoOverlayController(presentationData: presentationData, content: .linkCopied(title: nil, text: presentationData.strings.Conversation_LinkCopied), elevatedLayout: false, animateInAsReplacement: false, action: { _ in return false }), in: .current) + } + ) + self.present(shareController, in: .window(.root)) + } + viewUpgradedImpl = { [weak self] messageId in guard let self, let navigationController = self.navigationController as? NavigationController else { return @@ -2111,7 +2572,7 @@ public class GiftViewScreen: ViewControllerComponentContainer { }) } - showAttributeInfoImpl = { [weak self] tag, rarity in + showAttributeInfoImpl = { [weak self] tag, text in guard let self else { return } @@ -2122,11 +2583,56 @@ public class GiftViewScreen: ViewControllerComponentContainer { } let location = CGRect(origin: CGPoint(x: absoluteLocation.x, y: absoluteLocation.y - 12.0), size: CGSize()) - let controller = TooltipScreen(account: self.context.account, sharedContext: self.context.sharedContext, text: .plain(text: presentationData.strings.Gift_Unique_AttributeDescription(formatPercentage(rarity)).string), location: .point(location, .bottom), displayDuration: .default, inset: 16.0, shouldDismissOnTouch: { _, _ in + let controller = TooltipScreen(account: self.context.account, sharedContext: self.context.sharedContext, text: .plain(text: text), style: .wide, location: .point(location, .bottom), displayDuration: .default, inset: 16.0, shouldDismissOnTouch: { _, _ in return .ignore }) self.present(controller, in: .current) } + + openMoreImpl = { [weak self] node, gesture in + guard let self, let arguments = self.subject.arguments, case let .unique(gift) = arguments.gift else { + return + } + + let presentationData = context.sharedContext.currentPresentationData.with { $0 } + let link = "https://t.me/nft/\(gift.slug)" + + var items: [ContextMenuItem] = [] + items.append(.action(ContextMenuActionItem(text: presentationData.strings.Gift_View_Context_CopyLink, icon: { theme in + return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Link"), color: theme.contextMenu.primaryColor) + }, action: { [weak self] c, _ in + c?.dismiss(completion: nil) + + guard let self else { + return + } + + UIPasteboard.general.string = link + + self.present(UndoOverlayController(presentationData: presentationData, content: .linkCopied(title: nil, text: presentationData.strings.Conversation_LinkCopied), elevatedLayout: false, animateInAsReplacement: false, action: { _ in return false }), in: .current) + }))) + + items.append(.action(ContextMenuActionItem(text: presentationData.strings.Gift_View_Context_Share, icon: { theme in + return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Forward"), color: theme.contextMenu.primaryColor) + }, action: { c, _ in + c?.dismiss(completion: nil) + + shareGiftImpl?() + }))) + + if let _ = arguments.transferStars { + items.append(.action(ContextMenuActionItem(text: presentationData.strings.Gift_View_Context_Transfer, icon: { theme in + return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Replace"), color: theme.contextMenu.primaryColor) + }, action: { c, _ in + c?.dismiss(completion: nil) + + transferGiftImpl?() + }))) + } + + let contextController = ContextController(presentationData: presentationData, source: .reference(GiftViewContextReferenceContentSource(controller: self, sourceNode: node)), items: .single(ContextController.Items(content: .list(items))), gesture: gesture) + self.presentInGlobalOverlay(contextController) + } } required public init(coder aDecoder: NSCoder) { @@ -2274,6 +2780,7 @@ private final class TableComponent: CombinedComponent { let backgroundColor = context.component.theme.actionSheet.opaqueItemBackgroundColor let borderColor = backgroundColor.mixedWith(context.component.theme.list.itemBlocksSeparatorColor, alpha: 0.6) + let secondaryBackgroundColor = context.component.theme.overallDarkAppearance ? context.component.theme.list.itemModalBlocksBackgroundColor : context.component.theme.list.itemInputField.backgroundColor var leftColumnWidth: CGFloat = 0.0 @@ -2364,7 +2871,7 @@ private final class TableComponent: CombinedComponent { if hasLastBackground { let lastRowHeight = rowHeights[i - 1] ?? 0 let lastBackground = lastBackground.update( - component: Rectangle(color: context.component.theme.list.itemInputField.backgroundColor), + component: Rectangle(color: secondaryBackgroundColor), availableSize: CGSize(width: context.availableSize.width, height: lastRowHeight), transition: context.transition ) @@ -2375,7 +2882,7 @@ private final class TableComponent: CombinedComponent { } let leftColumnBackground = leftColumnBackground.update( - component: Rectangle(color: context.component.theme.list.itemInputField.backgroundColor), + component: Rectangle(color: secondaryBackgroundColor), availableSize: CGSize(width: leftColumnWidth, height: innerTotalHeight), transition: context.transition ) @@ -2573,27 +3080,6 @@ private final class PeerCellComponent: Component { } } -private func generateCloseButtonImage(backgroundColor: UIColor, foregroundColor: UIColor) -> UIImage? { - return generateImage(CGSize(width: 30.0, height: 30.0), contextGenerator: { size, context in - context.clear(CGRect(origin: CGPoint(), size: size)) - - context.setFillColor(backgroundColor.cgColor) - context.fillEllipse(in: CGRect(origin: CGPoint(), size: size)) - - context.setLineWidth(2.0) - context.setLineCap(.round) - context.setStrokeColor(foregroundColor.cgColor) - - context.move(to: CGPoint(x: 10.0, y: 10.0)) - context.addLine(to: CGPoint(x: 20.0, y: 20.0)) - context.strokePath() - - context.move(to: CGPoint(x: 20.0, y: 10.0)) - context.addLine(to: CGPoint(x: 10.0, y: 20.0)) - context.strokePath() - }) -} - private final class ButtonContentComponent: Component { let context: AccountContext let text: String @@ -2890,3 +3376,161 @@ private final class ParagraphComponent: CombinedComponent { } } } + +private final class GiftViewContextReferenceContentSource: ContextReferenceContentSource { + private let controller: ViewController + private let sourceNode: ASDisplayNode + + init(controller: ViewController, sourceNode: ASDisplayNode) { + self.controller = controller + self.sourceNode = sourceNode + } + + func transitionInfo() -> ContextControllerReferenceViewInfo? { + return ContextControllerReferenceViewInfo(referenceView: self.sourceNode.view, contentAreaInScreenSpace: UIScreen.main.bounds) + } +} + +private final class HeaderButtonComponent: CombinedComponent { + let title: String + let iconName: String + + public init( + title: String, + iconName: String + ) { + self.title = title + self.iconName = iconName + } + + static func ==(lhs: HeaderButtonComponent, rhs: HeaderButtonComponent) -> Bool { + if lhs.title != rhs.title { + return false + } + if lhs.iconName != rhs.iconName { + return false + } + return true + } + + static var body: Body { + let background = Child(RoundedRectangle.self) + let title = Child(MultilineTextComponent.self) + let icon = Child(BundleIconComponent.self) + + return { context in + let component = context.component + + let background = background.update( + component: RoundedRectangle( + color: UIColor.white.withAlphaComponent(0.16), + cornerRadius: 10.0 + ), + availableSize: context.availableSize, + transition: .immediate + ) + context.add(background + .position(CGPoint(x: context.availableSize.width / 2.0, y: context.availableSize.height / 2.0)) + ) + + let icon = icon.update( + component: BundleIconComponent( + name: component.iconName, + tintColor: UIColor.white + ), + availableSize: context.availableSize, + transition: .immediate + ) + context.add(icon + .position(CGPoint(x: context.availableSize.width / 2.0, y: 22.0)) + ) + + let title = title.update( + component: MultilineTextComponent( + text: .plain(NSAttributedString( + string: component.title, + font: Font.regular(11.0), + textColor: UIColor.white, + paragraphAlignment: .natural + )), + horizontalAlignment: .center, + maximumNumberOfLines: 1 + ), + availableSize: CGSize(width: context.availableSize.width - 16.0, height: context.availableSize.height), + transition: .immediate + ) + context.add(title + .position(CGPoint(x: context.availableSize.width / 2.0, y: 42.0)) + ) + + return context.availableSize + } + } +} + +private final class AvatarComponent: Component { + let context: AccountContext + let theme: PresentationTheme + let peer: EnginePeer + + init(context: AccountContext, theme: PresentationTheme, peer: EnginePeer) { + self.context = context + self.theme = theme + self.peer = peer + } + + static func ==(lhs: AvatarComponent, rhs: AvatarComponent) -> Bool { + if lhs.context !== rhs.context { + return false + } + if lhs.theme !== rhs.theme { + return false + } + if lhs.peer != rhs.peer { + return false + } + return true + } + + final class View: UIView { + private let avatarNode: AvatarNode + + private var component: AvatarComponent? + private weak var state: EmptyComponentState? + + override init(frame: CGRect) { + self.avatarNode = AvatarNode(font: avatarPlaceholderFont(size: 26.0)) + + super.init(frame: frame) + + self.addSubnode(self.avatarNode) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + func update(component: AvatarComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { + self.component = component + self.state = state + + self.avatarNode.frame = CGRect(origin: .zero, size: availableSize) + self.avatarNode.setPeer( + context: component.context, + theme: component.theme, + peer: component.peer, + synchronousLoad: true + ) + + return availableSize + } + } + + 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/MediaEditor/Sources/Drawing/CodableDrawingEntity.swift b/submodules/TelegramUI/Components/MediaEditor/Sources/Drawing/CodableDrawingEntity.swift index 811350c9c9..241661242e 100644 --- a/submodules/TelegramUI/Components/MediaEditor/Sources/Drawing/CodableDrawingEntity.swift +++ b/submodules/TelegramUI/Components/MediaEditor/Sources/Drawing/CodableDrawingEntity.swift @@ -180,6 +180,11 @@ public enum CodableDrawingEntity: Equatable { coordinates: coordinates, messageId: messageId ) + } else if case let .gift(gift, _) = entity.content { + return .starGift( + coordinates: coordinates, + slug: gift.slug + ) } else { return nil } diff --git a/submodules/TelegramUI/Components/MediaEditor/Sources/Drawing/DrawingStickerEntity.swift b/submodules/TelegramUI/Components/MediaEditor/Sources/Drawing/DrawingStickerEntity.swift index fa8979408a..13ce46aedd 100644 --- a/submodules/TelegramUI/Components/MediaEditor/Sources/Drawing/DrawingStickerEntity.swift +++ b/submodules/TelegramUI/Components/MediaEditor/Sources/Drawing/DrawingStickerEntity.swift @@ -39,6 +39,7 @@ public final class DrawingStickerEntity: DrawingEntity, Codable { case video(TelegramMediaFile) case dualVideoReference(Bool) case message([MessageId], CGSize, TelegramMediaFile?, CGRect?, CGFloat?) + case gift(StarGift.UniqueGift, CGSize) public static func == (lhs: Content, rhs: Content) -> Bool { switch lhs { @@ -78,6 +79,12 @@ public final class DrawingStickerEntity: DrawingEntity, Codable { } else { return false } + case let .gift(lhsGift, lhsSize): + if case let .gift(rhsGift, rhsSize) = rhs { + return lhsGift == rhsGift && lhsSize == rhsSize + } else { + return false + } } } } @@ -98,6 +105,9 @@ public final class DrawingStickerEntity: DrawingEntity, Codable { case messageSize case messageMediaRect case messageMediaCornerRadius + + case gift + case referenceDrawingSize case position case scale @@ -120,6 +130,8 @@ public final class DrawingStickerEntity: DrawingEntity, Codable { self.scale = max(0.59, min(1.77, self.scale)) } else if case .message = self.content { self.scale = max(2.5, self.scale) + } else if case .gift = self.content { + self.scale = max(2.5, self.scale) } } } @@ -164,6 +176,8 @@ public final class DrawingStickerEntity: DrawingEntity, Codable { dimensions = CGSize(width: 512.0, height: 512.0) case let .message(_, size, _, _, _): dimensions = size + case let .gift(_, size): + dimensions = size } let boundingSize = CGSize(width: size, height: size) @@ -191,7 +205,7 @@ public final class DrawingStickerEntity: DrawingEntity, Codable { return true case .dualVideoReference: return true - case .message: + case .message, .gift: return !(self.renderSubEntities ?? []).isEmpty } } @@ -202,7 +216,7 @@ public final class DrawingStickerEntity: DrawingEntity, Codable { return imageType == .rectangle case .video: return true - case .message: + case .message, .gift: return true default: return false @@ -232,7 +246,10 @@ public final class DrawingStickerEntity: DrawingEntity, Codable { public init(from decoder: Decoder) throws { let container = try decoder.container(keyedBy: CodingKeys.self) self.uuid = try container.decode(UUID.self, forKey: .uuid) - if let messageIds = try container.decodeIfPresent([MessageId].self, forKey: .messageIds) { + if let gift = try container.decodeIfPresent(StarGift.UniqueGift.self, forKey: .gift) { + let size = try container.decodeIfPresent(CGSize.self, forKey: .messageSize) ?? .zero + self.content = .gift(gift, size) + } else if let messageIds = try container.decodeIfPresent([MessageId].self, forKey: .messageIds) { let size = try container.decodeIfPresent(CGSize.self, forKey: .messageSize) ?? .zero let file = try container.decodeIfPresent(TelegramMediaFile.self, forKey: .messageFile) let mediaRect = try container.decodeIfPresent(CGRect.self, forKey: .messageMediaRect) @@ -343,6 +360,9 @@ public final class DrawingStickerEntity: DrawingEntity, Codable { try container.encodeIfPresent(file, forKey: .messageFile) try container.encodeIfPresent(mediaRect, forKey: .messageMediaRect) try container.encodeIfPresent(mediaCornerRadius, forKey: .messageMediaCornerRadius) + case let .gift(gift, size): + try container.encode(gift, forKey: .gift) + try container.encode(size, forKey: .messageSize) } try container.encode(self.referenceDrawingSize, forKey: .referenceDrawingSize) try container.encode(self.position, forKey: .position) diff --git a/submodules/TelegramUI/Components/MediaEditor/Sources/DrawingMessageRenderer.swift b/submodules/TelegramUI/Components/MediaEditor/Sources/DrawingMessageRenderer.swift index 482d271297..05f3c0c9dc 100644 --- a/submodules/TelegramUI/Components/MediaEditor/Sources/DrawingMessageRenderer.swift +++ b/submodules/TelegramUI/Components/MediaEditor/Sources/DrawingMessageRenderer.swift @@ -87,17 +87,29 @@ public final class DrawingMessageRenderer { private let isNight: Bool private let isOverlay: Bool private let isLink: Bool + private let isGift: Bool + private let wallpaperColor: UIColor? private let messagesContainerNode: ASDisplayNode private var avatarHeaderNode: ListViewItemHeaderNode? private var messageNodes: [ListViewItemNode]? - init(context: AccountContext, messages: [Message], isNight: Bool = false, isOverlay: Bool = false, isLink: Bool = false) { + init( + context: AccountContext, + messages: [Message], + isNight: Bool = false, + isOverlay: Bool = false, + isLink: Bool = false, + isGift: Bool = false, + wallpaperColor: UIColor? = nil + ) { self.context = context self.messages = messages self.isNight = isNight self.isOverlay = isOverlay self.isLink = isLink + self.isGift = isGift + self.wallpaperColor = wallpaperColor self.messagesContainerNode = ASDisplayNode() self.messagesContainerNode.clipsToBounds = true @@ -160,14 +172,19 @@ public final class DrawingMessageRenderer { } } } + + var borderColor: UIColor? + if self.isGift && !self.isOverlay, let wallpaperColor = self.wallpaperColor { + borderColor = wallpaperColor.withMultiplied(hue: 1.0, saturation: 1.5, brightness: self.isNight ? 1.6 : 0.7).withAlphaComponent(0.6) + } - self.generate(size: size) { image in + self.generate(size: size, borderColor: borderColor) { image in completion(size, image, mediaRect) } }) } - private func generate(size: CGSize, completion: @escaping (UIImage) -> Void) { + private func generate(size: CGSize, borderColor: UIColor? = nil, completion: @escaping (UIImage) -> Void) { UIGraphicsBeginImageContextWithOptions(size, false, 3.0) self.view.drawHierarchy(in: CGRect(origin: CGPoint(), size: size), afterScreenUpdates: true) let img = UIGraphicsGetImageFromCurrentImageContext() @@ -175,6 +192,11 @@ public final class DrawingMessageRenderer { let finalImage = generateImage(CGSize(width: size.width * 3.0, height: size.height * 3.0), contextGenerator: { size, context in context.clear(CGRect(origin: .zero, size: size)) + if let borderColor { + context.addPath(CGPath(roundedRect: CGRect(origin: CGPoint(x: 6.0, y: 12.0), size: CGSize(width: size.width - 6.0, height: size.height - 13.0)), cornerWidth: 70.0, cornerHeight: 70.0, transform: nil)) + context.setFillColor(borderColor.cgColor) + context.fillPath() + } if let cgImage = img?.cgImage { context.draw(cgImage, in: CGRect(origin: .zero, size: size), byTiling: false) } @@ -198,7 +220,13 @@ public final class DrawingMessageRenderer { let avatarHeaderItem: ListViewItemHeader? if let author = self.messages.first?.author { - avatarHeaderItem = self.context.sharedContext.makeChatMessageAvatarHeaderItem(context: self.context, timestamp: self.messages.first?.timestamp ?? 0, peer: self.messages.first!.peers[author.id]!, message: self.messages.first!, theme: theme, strings: presentationData.strings, wallpaper: presentationData.chatWallpaper, fontSize: presentationData.chatFontSize, chatBubbleCorners: chatBubbleCorners, dateTimeFormat: presentationData.dateTimeFormat, nameOrder: presentationData.nameDisplayOrder) + let avatarPeer: Peer + if let peer = self.messages.first!.peers[author.id] { + avatarPeer = peer + } else { + avatarPeer = author + } + avatarHeaderItem = self.context.sharedContext.makeChatMessageAvatarHeaderItem(context: self.context, timestamp: self.messages.first?.timestamp ?? 0, peer: avatarPeer, message: self.messages.first!, theme: theme, strings: presentationData.strings, wallpaper: presentationData.chatWallpaper, fontSize: presentationData.chatFontSize, chatBubbleCorners: chatBubbleCorners, dateTimeFormat: presentationData.dateTimeFormat, nameOrder: presentationData.nameDisplayOrder) } else { avatarHeaderItem = nil } @@ -208,6 +236,8 @@ public final class DrawingMessageRenderer { var leftInset: CGFloat = 37.0 if self.isLink { leftInset = -6.0 + } else if self.isGift { + leftInset = -50.0 } let containerWidth = layout.size.width - inset * 2.0 let params = ListViewItemLayoutParams(width: containerWidth, leftInset: layout.safeInsets.left, rightInset: layout.safeInsets.right, availableHeight: layout.size.height) @@ -329,13 +359,21 @@ public final class DrawingMessageRenderer { private let nightContainerNode: ContainerNode private let overlayContainerNode: ContainerNode - public init(context: AccountContext, messages: [Message], parentView: UIView, isLink: Bool = false) { + public init( + context: AccountContext, + messages: [Message], + parentView: UIView, + isLink: Bool = false, + isGift: Bool = false, + wallpaperDayColor: UIColor? = nil, + wallpaperNightColor: UIColor? = nil + ) { self.context = context self.messages = messages - self.dayContainerNode = ContainerNode(context: context, messages: messages, isLink: isLink) - self.nightContainerNode = ContainerNode(context: context, messages: messages, isNight: true, isLink: isLink) - self.overlayContainerNode = ContainerNode(context: context, messages: messages, isOverlay: true, isLink: isLink) + self.dayContainerNode = ContainerNode(context: context, messages: messages, isLink: isLink, isGift: isGift, wallpaperColor: wallpaperDayColor) + self.nightContainerNode = ContainerNode(context: context, messages: messages, isNight: true, isLink: isLink, isGift: isGift, wallpaperColor: wallpaperNightColor) + self.overlayContainerNode = ContainerNode(context: context, messages: messages, isOverlay: true, isLink: isLink, isGift: isGift, wallpaperColor: nil) parentView.addSubview(self.dayContainerNode.view) parentView.addSubview(self.nightContainerNode.view) diff --git a/submodules/TelegramUI/Components/MediaEditor/Sources/MediaEditor.swift b/submodules/TelegramUI/Components/MediaEditor/Sources/MediaEditor.swift index 38d839bb1c..e4dc09a8c1 100644 --- a/submodules/TelegramUI/Components/MediaEditor/Sources/MediaEditor.swift +++ b/submodules/TelegramUI/Components/MediaEditor/Sources/MediaEditor.swift @@ -168,6 +168,7 @@ public final class MediaEditor { case asset(PHAsset) case draft(MediaEditorDraft) case message(MessageId) + case gift(StarGift.UniqueGift) case sticker(TelegramMediaFile) var dimensions: PixelDimensions { @@ -178,7 +179,7 @@ public final class MediaEditor { return PixelDimensions(width: Int32(asset.pixelWidth), height: Int32(asset.pixelHeight)) case let .draft(draft): return draft.dimensions - case .message, .sticker, .videoCollage: + case .message, .gift, .sticker, .videoCollage: return PixelDimensions(width: 1080, height: 1920) } } @@ -307,7 +308,15 @@ public final class MediaEditor { return self.renderer.finalRenderedImage(mirror: mirror) } - private var wallpapers: ((day: UIImage, night: UIImage?))? + private var wallpapersValue: ((day: UIImage, night: UIImage?))? { + didSet { + self.wallpapersPromise.set(.single(self.wallpapersValue)) + } + } + private let wallpapersPromise = Promise<(day: UIImage, night: UIImage?)?>() + public var wallpapers: Signal<((day: UIImage, night: UIImage?))?, NoError> { + return self.wallpapersPromise.get() + } private struct PlaybackState: Equatable { let duration: Double @@ -817,7 +826,7 @@ public final class MediaEditor { player = self.makePlayer(asset: asset) } } - return getChatWallpaperImage(context: self.context, messageId: messageId) + return getChatWallpaperImage(context: self.context, peerId: messageId.peerId) |> map { _, image, nightImage in return TextureSourceResult( image: image, @@ -828,6 +837,17 @@ public final class MediaEditor { ) } } + case .gift: + textureSource = getChatWallpaperImage(context: self.context, peerId: self.context.account.peerId) + |> map { _, image, nightImage in + return TextureSourceResult( + image: image, + nightImage: nightImage, + player: nil, + playerIsReference: true, + gradientColors: GradientColors(top: .black, bottom: .black) + ) + } case let .sticker(file): let entity = MediaEditorComposerStickerEntity( postbox: self.context.account.postbox, @@ -859,10 +879,13 @@ public final class MediaEditor { let textureSource = UniversalTextureSource(renderTarget: renderTarget) - if case .message = self.self.subject { + switch self.subject { + case .message, .gift: if let image = textureSourceResult.image { - self.wallpapers = (image, textureSourceResult.nightImage ?? image) + self.wallpapersValue = (image, textureSourceResult.nightImage ?? image) } + default: + break } self.player = textureSourceResult.player @@ -1228,7 +1251,7 @@ public final class MediaEditor { return values.withUpdatedNightTheme(nightTheme) } - guard let (dayImage, nightImage) = self.wallpapers, let nightImage else { + guard let (dayImage, nightImage) = self.wallpapersValue, let nightImage else { return } diff --git a/submodules/TelegramUI/Components/MediaEditor/Sources/MediaEditorComposerEntity.swift b/submodules/TelegramUI/Components/MediaEditor/Sources/MediaEditorComposerEntity.swift index 4a8440725b..e8f3787677 100644 --- a/submodules/TelegramUI/Components/MediaEditor/Sources/MediaEditorComposerEntity.swift +++ b/submodules/TelegramUI/Components/MediaEditor/Sources/MediaEditorComposerEntity.swift @@ -94,7 +94,7 @@ func composerEntitiesForDrawingEntity(postbox: Postbox, textScale: CGFloat, enti content = .video(file) case .dualVideoReference: return [] - case .message: + case .message, .gift: if let renderImage = entity.renderImage, let image = CIImage(image: renderImage, options: [.colorSpace: colorSpace]) { var entities: [MediaEditorComposerEntity] = [] entities.append(MediaEditorComposerStaticEntity(image: image, position: entity.position, scale: entity.scale, rotation: entity.rotation, baseSize: entity.baseSize, mirrored: false)) diff --git a/submodules/TelegramUI/Components/MediaEditor/Sources/MediaEditorUtils.swift b/submodules/TelegramUI/Components/MediaEditor/Sources/MediaEditorUtils.swift index 6277737481..dd77fbaf44 100644 --- a/submodules/TelegramUI/Components/MediaEditor/Sources/MediaEditorUtils.swift +++ b/submodules/TelegramUI/Components/MediaEditor/Sources/MediaEditorUtils.swift @@ -135,7 +135,7 @@ func getTextureImage(device: MTLDevice, texture: MTLTexture, mirror: Bool = fals return UIImage(cgImage: cgImage) } -public func getChatWallpaperImage(context: AccountContext, messageId: EngineMessage.Id) -> Signal<(CGSize, UIImage?, UIImage?), NoError> { +public func getChatWallpaperImage(context: AccountContext, peerId: EnginePeer.Id) -> Signal<(CGSize, UIImage?, UIImage?), NoError> { let themeSettings = context.sharedContext.accountManager.sharedData(keys: [ApplicationSpecificSharedDataKeys.presentationThemeSettings]) |> map { sharedData -> PresentationThemeSettings in let themeSettings: PresentationThemeSettings @@ -148,7 +148,7 @@ public func getChatWallpaperImage(context: AccountContext, messageId: EngineMess } let peerWallpaper = context.account.postbox.transaction { transaction -> TelegramWallpaper? in - return (transaction.getPeerCachedData(peerId: messageId.peerId) as? CachedChannelData)?.wallpaper + return (transaction.getPeerCachedData(peerId: peerId) as? CachedChannelData)?.wallpaper } return combineLatest(themeSettings, peerWallpaper) diff --git a/submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaEditorDrafts.swift b/submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaEditorDrafts.swift index 1cba0fc422..29bc6f6f06 100644 --- a/submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaEditorDrafts.swift +++ b/submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaEditorDrafts.swift @@ -203,7 +203,7 @@ extension MediaEditorScreenImpl { } else if let image = UIImage(contentsOfFile: draft.fullPath(engine: context.engine)) { innerSaveDraft(media: .image(image: image, dimensions: draft.dimensions)) } - case .message: + case .message, .gift: if let pixel = generateSingleColorImage(size: CGSize(width: 1, height: 1), color: .black) { innerSaveDraft(media: .image(image: pixel, dimensions: PixelDimensions(width: 1080, height: 1920))) } diff --git a/submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaEditorScreen.swift b/submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaEditorScreen.swift index fe43aa0481..cff7729ebe 100644 --- a/submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaEditorScreen.swift +++ b/submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaEditorScreen.swift @@ -433,7 +433,6 @@ final class MediaEditorScreenComponent: Component { } }, dismissTextInput: { - }, insertText: { [weak self] text in if let self { @@ -1661,7 +1660,17 @@ final class MediaEditorScreenComponent: Component { var topButtonOffsetX: CGFloat = 0.0 var topButtonOffsetY: CGFloat = 0.0 - if let subject = controller.node.subject, case .message = subject { + var hasDayNightSelection = false + if let subject = controller.node.subject { + switch subject { + case .message, .gift: + hasDayNightSelection = true + default: + break + } + } + + if hasDayNightSelection { let isNightTheme = mediaEditor?.values.nightTheme == true let dayNightContentComponent: AnyComponentWithIdentity @@ -3145,6 +3154,8 @@ public final class MediaEditorScreenImpl: ViewController, MediaEditorScreen, UID isSavingAvailable = true case .message: isSavingAvailable = true + case .gift: + isSavingAvailable = true default: isSavingAvailable = false } @@ -3237,8 +3248,13 @@ public final class MediaEditorScreenImpl: ViewController, MediaEditorScreen, UID mediaEditor.seek(initialVideoPosition, andPlay: true) } } - if case .message = subject, self.context.sharedContext.currentPresentationData.with({$0}).autoNightModeTriggered { - mediaEditor.setNightTheme(true) + if self.context.sharedContext.currentPresentationData.with({$0}).autoNightModeTriggered { + switch subject { + case .message, .gift: + mediaEditor.setNightTheme(true) + default: + break + } } mediaEditor.valuesUpdated = { [weak self] values in if let self, let controller = self.controller, values.gradientColors != nil, controller.previousSavedValues != values { @@ -3284,28 +3300,33 @@ public final class MediaEditorScreenImpl: ViewController, MediaEditorScreen, UID self.stickerMaskDrawingView?.clearWithEmptyColor() } - if case .message = effectiveSubject { - } else { + switch effectiveSubject { + case .message, .gift: + break + default: self.readyValue.set(.single(true)) } - if case let .image(_, _, additionalImage, position) = effectiveSubject, let additionalImage { - let image = generateImage(CGSize(width: additionalImage.size.width, height: additionalImage.size.width), contextGenerator: { size, context in - let bounds = CGRect(origin: .zero, size: size) - context.clear(bounds) - context.addEllipse(in: bounds) - context.clip() - - if let cgImage = additionalImage.cgImage { - context.draw(cgImage, in: CGRect(origin: CGPoint(x: (size.width - additionalImage.size.width) / 2.0, y: (size.height - additionalImage.size.height) / 2.0), size: additionalImage.size)) - } - }, scale: 1.0) - let imageEntity = DrawingStickerEntity(content: .image(image ?? additionalImage, .dualPhoto)) - imageEntity.referenceDrawingSize = storyDimensions - imageEntity.scale = 1.625 - imageEntity.position = position.getPosition(storyDimensions) - self.entitiesView.add(imageEntity, announce: false) - } else if case let .video(_, _, mirror, additionalVideoPath, _, _, _, changes, position) = effectiveSubject { + switch effectiveSubject { + case let .image(_, _, additionalImage, position): + if let additionalImage { + let image = generateImage(CGSize(width: additionalImage.size.width, height: additionalImage.size.width), contextGenerator: { size, context in + let bounds = CGRect(origin: .zero, size: size) + context.clear(bounds) + context.addEllipse(in: bounds) + context.clip() + + if let cgImage = additionalImage.cgImage { + context.draw(cgImage, in: CGRect(origin: CGPoint(x: (size.width - additionalImage.size.width) / 2.0, y: (size.height - additionalImage.size.height) / 2.0), size: additionalImage.size)) + } + }, scale: 1.0) + let imageEntity = DrawingStickerEntity(content: .image(image ?? additionalImage, .dualPhoto)) + imageEntity.referenceDrawingSize = storyDimensions + imageEntity.scale = 1.625 + imageEntity.position = position.getPosition(storyDimensions) + self.entitiesView.add(imageEntity, announce: false) + } + case let .video(_, _, mirror, additionalVideoPath, _, _, _, changes, position): mediaEditor.setVideoIsMirrored(mirror) if let additionalVideoPath { let videoEntity = DrawingStickerEntity(content: .dualVideoReference(false)) @@ -3324,24 +3345,41 @@ public final class MediaEditorScreenImpl: ViewController, MediaEditorScreen, UID } } } - } else if case let .videoCollage(items) = effectiveSubject { + case let .videoCollage(items): mediaEditor.setupCollage(items.map { $0.editorItem }) - } else if case let .message(messageIds) = effectiveSubject { + case let .sticker(_, emoji): + controller.stickerSelectedEmoji = emoji + case .message, .gift: + var isGift = false + let messages: Signal<[Message], NoError> + if case let .message(messageIds) = effectiveSubject { + messages = self.context.engine.data.get( + EngineDataMap(messageIds.map(TelegramEngine.EngineData.Item.Messages.Message.init(id:))) + ) + |> map { result in + var messages: [Message] = [] + for id in messageIds { + if let maybeMessage = result[id], let message = maybeMessage { + messages.append(message._asMessage()) + } + } + return messages + } + } else if case let .gift(gift) = effectiveSubject { + isGift = true + let media: [Media] = [TelegramMediaAction(action: .starGiftUnique(gift: .unique(gift), isUpgrade: false, isTransferred: false, savedToProfile: false, canExportDate: nil, transferStars: nil, isRefunded: false))] + let message = Message(stableId: 0, stableVersion: 0, id: MessageId(peerId: self.context.account.peerId, namespace: Namespaces.Message.Cloud, id: -1), globallyUniqueId: nil, groupingKey: nil, groupInfo: nil, threadId: nil, timestamp: 0, flags: [], tags: [], globalTags: [], localTags: [], customTags: [], forwardInfo: nil, author: nil, text: "", attributes: [], media: media, peers: SimpleDictionary(), associatedMessages: SimpleDictionary(), associatedMessageIds: [], associatedMedia: [:], associatedThreadInfo: nil, associatedStories: [:]) + messages = .single([message]) + } else { + fatalError() + } + let isNightTheme = mediaEditor.values.nightTheme - let _ = ((self.context.engine.data.get( - EngineDataMap(messageIds.map(TelegramEngine.EngineData.Item.Messages.Message.init(id:))) - )) - |> deliverOnMainQueue).start(next: { [weak self] result in + let _ = (messages + |> deliverOnMainQueue).start(next: { [weak self] messages in guard let self else { return } - var messages: [Message] = [] - for id in messageIds { - if let maybeMessage = result[id], let message = maybeMessage { - messages.append(message._asMessage()) - } - } - var messageFile: TelegramMediaFile? if let maybeFile = messages.first?.media.first(where: { $0 is TelegramMediaFile }) as? TelegramMediaFile, maybeFile.isVideo, let _ = self.context.account.postbox.mediaBox.completedResourcePath(maybeFile.resource, pathExtension: nil) { messageFile = maybeFile @@ -3350,46 +3388,89 @@ public final class MediaEditorScreenImpl: ViewController, MediaEditorScreen, UID messageFile = nil } - let renderer = DrawingMessageRenderer(context: self.context, messages: messages, parentView: self.view) - renderer.render(completion: { result in - if case .draft = subject, let existingEntityView = self.entitiesView.getView(where: { entityView in - if let stickerEntityView = entityView as? DrawingStickerEntityView, case .message = (stickerEntityView.entity as! DrawingStickerEntity).content { - return true - } else { - return false - } - }) as? DrawingStickerEntityView { - existingEntityView.isNightTheme = isNightTheme - let messageEntity = existingEntityView.entity as! DrawingStickerEntity - messageEntity.renderImage = result.dayImage - messageEntity.secondaryRenderImage = result.nightImage - messageEntity.overlayRenderImage = result.overlayImage - existingEntityView.update(animated: false) - } else { - let messageEntity = DrawingStickerEntity(content: .message(messageIds, result.size, messageFile, result.mediaFrame?.rect, result.mediaFrame?.cornerRadius)) - messageEntity.renderImage = result.dayImage - messageEntity.secondaryRenderImage = result.nightImage - messageEntity.overlayRenderImage = result.overlayImage - messageEntity.referenceDrawingSize = storyDimensions - messageEntity.position = CGPoint(x: storyDimensions.width / 2.0 - 54.0, y: storyDimensions.height / 2.0) - - let fraction = max(result.size.width, result.size.height) / 353.0 - messageEntity.scale = min(6.0, 3.3 * fraction) - - if let entityView = self.entitiesView.add(messageEntity, announce: false) as? DrawingStickerEntityView { - if isNightTheme { - entityView.isNightTheme = true + let wallpaperColors: Signal<(UIColor?, UIColor?), NoError> + if let subject = self.subject, case .gift = subject { + wallpaperColors = self.mediaEditorPromise.get() + |> mapToSignal { mediaEditor in + if let mediaEditor { + return mediaEditor.wallpapers + |> filter { + $0 != nil + } + |> take(1) + |> map { result in + if let (dayImage, nightImage) = result { + return (getAverageColor(image: dayImage), nightImage.flatMap { getAverageColor(image: $0) }) + } + return (nil, nil) } } + return .complete() } - - self.readyValue.set(.single(true)) + + } else { + wallpaperColors = .single((nil, nil)) + } + + let _ = (wallpaperColors + |> deliverOnMainQueue).start(next: { [weak self] wallpaperColors in + guard let self else { + return + } + let renderer = DrawingMessageRenderer(context: self.context, messages: messages, parentView: self.view, isGift: isGift, wallpaperDayColor: wallpaperColors.0, wallpaperNightColor: wallpaperColors.1) + renderer.render(completion: { result in + if case .draft = subject, let existingEntityView = self.entitiesView.getView(where: { entityView in + if let stickerEntityView = entityView as? DrawingStickerEntityView, case .message = (stickerEntityView.entity as! DrawingStickerEntity).content { + return true + } else { + return false + } + }) as? DrawingStickerEntityView { + existingEntityView.isNightTheme = isNightTheme + let messageEntity = existingEntityView.entity as! DrawingStickerEntity + messageEntity.renderImage = result.dayImage + messageEntity.secondaryRenderImage = result.nightImage + messageEntity.overlayRenderImage = result.overlayImage + existingEntityView.update(animated: false) + } else { + var content: DrawingStickerEntity.Content + var position: CGPoint + switch effectiveSubject { + case let .message(messageIds): + content = .message(messageIds, result.size, messageFile, result.mediaFrame?.rect, result.mediaFrame?.cornerRadius) + position = CGPoint(x: storyDimensions.width / 2.0 - 54.0, y: storyDimensions.height / 2.0) + case let .gift(gift): + content = .gift(gift, result.size) + position = CGPoint(x: storyDimensions.width / 2.0, y: storyDimensions.height / 2.0) + default: + fatalError() + } + + let messageEntity = DrawingStickerEntity(content: content) + messageEntity.renderImage = result.dayImage + messageEntity.secondaryRenderImage = result.nightImage + messageEntity.overlayRenderImage = result.overlayImage + messageEntity.referenceDrawingSize = storyDimensions + messageEntity.position = position + + let fraction = max(result.size.width, result.size.height) / 353.0 + messageEntity.scale = min(6.0, 3.3 * fraction) + + if let entityView = self.entitiesView.add(messageEntity, announce: false) as? DrawingStickerEntityView { + if isNightTheme { + entityView.isNightTheme = true + } + } + } + + self.readyValue.set(.single(true)) + }) }) }) - } else if case let .sticker(_, emoji) = effectiveSubject { - controller.stickerSelectedEmoji = emoji + default: + break } - + self.gradientColorsDisposable = mediaEditor.gradientColors.start(next: { [weak self] colors in if let self, let colors { let gradientImage = generateGradientImage(size: CGSize(width: 5.0, height: 640.0), colors: colors.array, locations: [0.0, 1.0]) @@ -3749,10 +3830,12 @@ public final class MediaEditorScreenImpl: ViewController, MediaEditorScreen, UID } private var canEnhance: Bool { - if case .message = self.subject { + switch self.subject { + case .message, .gift: return false + default: + return true } - return true } private var enhanceInitialTranslation: Float? @@ -3856,8 +3939,13 @@ public final class MediaEditorScreenImpl: ViewController, MediaEditorScreen, UID guard !self.isCollageTimelineOpen else { return } - if gestureRecognizer.numberOfTouches == 2, let subject = self.subject, case .message = subject, !self.entitiesView.hasSelection { - return + if gestureRecognizer.numberOfTouches == 2, let subject = self.subject, !self.entitiesView.hasSelection { + switch subject { + case .message, .gift: + return + default: + break + } } let currentTimestamp = CACurrentMediaTime() if let previousPanTimestamp = self.previousPanTimestamp, currentTimestamp - previousPanTimestamp < 0.016, case .changed = gestureRecognizer.state { @@ -3871,8 +3959,13 @@ public final class MediaEditorScreenImpl: ViewController, MediaEditorScreen, UID guard !self.isCollageTimelineOpen else { return } - if gestureRecognizer.numberOfTouches == 2, let subject = self.subject, case .message = subject, !self.entitiesView.hasSelection { - return + if gestureRecognizer.numberOfTouches == 2, let subject = self.subject, !self.entitiesView.hasSelection { + switch subject { + case .message, .gift: + return + default: + break + } } let currentTimestamp = CACurrentMediaTime() if let previousPinchTimestamp = self.previousPinchTimestamp, currentTimestamp - previousPinchTimestamp < 0.016, case .changed = gestureRecognizer.state { @@ -3886,8 +3979,13 @@ public final class MediaEditorScreenImpl: ViewController, MediaEditorScreen, UID guard !self.isCollageTimelineOpen else { return } - if gestureRecognizer.numberOfTouches == 2, let subject = self.subject, case .message = subject, !self.entitiesView.hasSelection { - return + if gestureRecognizer.numberOfTouches == 2, let subject = self.subject, !self.entitiesView.hasSelection { + switch subject { + case .message, .gift: + return + default: + break + } } let currentTimestamp = CACurrentMediaTime() if let previousRotateTimestamp = self.previousRotateTimestamp, currentTimestamp - previousRotateTimestamp < 0.016, case .changed = gestureRecognizer.state { @@ -4072,7 +4170,7 @@ public final class MediaEditorScreenImpl: ViewController, MediaEditorScreen, UID var animateIn = false if let subject { switch subject { - case .empty, .message, .sticker, .image: + case .empty, .message, .gift, .sticker, .image: animateIn = true default: break @@ -5924,6 +6022,7 @@ public final class MediaEditorScreenImpl: ViewController, MediaEditorScreen, UID case asset(PHAsset) case draft(MediaEditorDraft, Int64?) case message([MessageId]) + case gift(StarGift.UniqueGift) case sticker(TelegramMediaFile, [String]) var dimensions: PixelDimensions { @@ -5936,7 +6035,7 @@ public final class MediaEditorScreenImpl: ViewController, MediaEditorScreen, UID return PixelDimensions(width: Int32(asset.pixelWidth), height: Int32(asset.pixelHeight)) case let .draft(draft, _): return draft.dimensions - case .message, .sticker, .videoCollage: + case .message, .gift, .sticker, .videoCollage: return PixelDimensions(width: 1080, height: 1920) } } @@ -5960,6 +6059,8 @@ public final class MediaEditorScreenImpl: ViewController, MediaEditorScreen, UID return .draft(draft) case let .message(messageIds): return .message(messageIds.first!) + case let .gift(gift): + return .gift(gift) case let .sticker(sticker, _): return .sticker(sticker) } @@ -5985,6 +6086,8 @@ public final class MediaEditorScreenImpl: ViewController, MediaEditorScreen, UID return draft.isVideo case .message: return false + case .gift: + return false case .sticker: return false } @@ -6189,9 +6292,12 @@ public final class MediaEditorScreenImpl: ViewController, MediaEditorScreen, UID if self.isEditingStory { needsAudioSession = true } - if case .message = subject { + switch subject { + case .message, .gift: needsAudioSession = true checkPostingAvailability = true + default: + break } if needsAudioSession { self.audioSessionDisposable = self.context.sharedContext.mediaManager.audioSession.push(audioSessionType: .record(speaker: false, video: true, withOthers: true), activate: { _ in @@ -7195,9 +7301,16 @@ public final class MediaEditorScreenImpl: ViewController, MediaEditorScreen, UID firstFrame = .single((UIImage(), nil)) } } - case let .message(messages): + case .message, .gift: + let peerId: EnginePeer.Id + if case let .message(messageIds) = subject { + peerId = messageIds.first!.peerId + } else { + peerId = self.context.account.peerId + } + let isNightTheme = mediaEditor.values.nightTheme - let wallpaper = getChatWallpaperImage(context: self.context, messageId: messages.first!) + let wallpaper = getChatWallpaperImage(context: self.context, peerId: peerId) |> map { _, image, nightImage -> UIImage? in if isNightTheme { return nightImage ?? image @@ -8055,7 +8168,18 @@ public final class MediaEditorScreenImpl: ViewController, MediaEditorScreen, UID } case let .message(messages): let isNightTheme = mediaEditor.values.nightTheme - exportSubject = getChatWallpaperImage(context: self.context, messageId: messages.first!) + exportSubject = getChatWallpaperImage(context: self.context, peerId: messages.first!.peerId) + |> mapToSignal { _, image, nightImage -> Signal in + if isNightTheme { + let effectiveImage = nightImage ?? image + return effectiveImage.flatMap({ .single(.image(image: $0)) }) ?? .complete() + } else { + return image.flatMap({ .single(.image(image: $0)) }) ?? .complete() + } + } + case .gift: + let isNightTheme = mediaEditor.values.nightTheme + exportSubject = getChatWallpaperImage(context: self.context, peerId: self.context.account.peerId) |> mapToSignal { _, image, nightImage -> Signal in if isNightTheme { let effectiveImage = nightImage ?? image diff --git a/submodules/TelegramUI/Components/PeerInfo/PeerInfoCoverComponent/Sources/PeerInfoCoverComponent.swift b/submodules/TelegramUI/Components/PeerInfo/PeerInfoCoverComponent/Sources/PeerInfoCoverComponent.swift index 49c8b45f2e..1a2253e243 100644 --- a/submodules/TelegramUI/Components/PeerInfo/PeerInfoCoverComponent/Sources/PeerInfoCoverComponent.swift +++ b/submodules/TelegramUI/Components/PeerInfo/PeerInfoCoverComponent/Sources/PeerInfoCoverComponent.swift @@ -102,6 +102,7 @@ public final class PeerInfoCoverComponent: Component { public let avatarCenter: CGPoint public let avatarScale: CGFloat public let defaultHeight: CGFloat + public let gradientOnTop: Bool public let avatarTransitionFraction: CGFloat public let patternTransitionFraction: CGFloat @@ -113,6 +114,7 @@ public final class PeerInfoCoverComponent: Component { avatarCenter: CGPoint, avatarScale: CGFloat, defaultHeight: CGFloat, + gradientOnTop: Bool = false, avatarTransitionFraction: CGFloat, patternTransitionFraction: CGFloat ) { @@ -123,6 +125,7 @@ public final class PeerInfoCoverComponent: Component { self.avatarCenter = avatarCenter self.avatarScale = avatarScale self.defaultHeight = defaultHeight + self.gradientOnTop = gradientOnTop self.avatarTransitionFraction = avatarTransitionFraction self.patternTransitionFraction = patternTransitionFraction } @@ -149,6 +152,9 @@ public final class PeerInfoCoverComponent: Component { if lhs.defaultHeight != rhs.defaultHeight { return false } + if lhs.gradientOnTop != rhs.gradientOnTop { + return false + } if lhs.avatarTransitionFraction != rhs.avatarTransitionFraction { return false } @@ -166,6 +172,7 @@ public final class PeerInfoCoverComponent: Component { private let avatarBackgroundGradientLayer: SimpleGradientLayer private let backgroundPatternContainer: UIView + private var currentSize: CGSize? private var component: PeerInfoCoverComponent? private var state: EmptyComponentState? @@ -180,6 +187,7 @@ public final class PeerInfoCoverComponent: Component { self.backgroundGradientLayer = SimpleGradientLayer() self.avatarBackgroundGradientLayer = SimpleGradientLayer() + self.avatarBackgroundGradientLayer.opacity = 0.0 let baseAvatarGradientAlpha: CGFloat = 0.4 let numSteps = 6 self.avatarBackgroundGradientLayer.colors = (0 ..< numSteps).map { i in @@ -221,13 +229,41 @@ public final class PeerInfoCoverComponent: Component { self.patternImageDisposable?.dispose() } + public func willAnimateIn() { + for layer in self.avatarPatternContentLayers { + layer.opacity = 0.0 + } + } + + public func animateIn() { + guard let _ = self.currentSize, let component = self.component else { + return + } + + for layer in self.avatarPatternContentLayers { + layer.opacity = 1.0 + layer.animatePosition( + from: component.avatarCenter, + to: layer.position, + duration: 0.4, + timingFunction: kCAMediaTimingFunctionSpring + ) + } + } + public func animateTransition() { if let gradientSnapshotLayer = self.backgroundGradientLayer.snapshotContentTree() { - gradientSnapshotLayer.frame = self.backgroundGradientLayer.frame - self.layer.insertSublayer(gradientSnapshotLayer, above: self.backgroundGradientLayer) - gradientSnapshotLayer.animateAlpha(from: 1.0, to: 0.0, duration: 0.25, removeOnCompletion: false, completion: { _ in - gradientSnapshotLayer.removeFromSuperlayer() + let backgroundSnapshotLayer = SimpleLayer() + backgroundSnapshotLayer.allowsGroupOpacity = true + backgroundSnapshotLayer.backgroundColor = self.backgroundView.backgroundColor?.cgColor + backgroundSnapshotLayer.frame = self.backgroundView.frame + self.layer.insertSublayer(backgroundSnapshotLayer, above: self.backgroundGradientLayer) + + backgroundSnapshotLayer.animateAlpha(from: 1.0, to: 0.0, duration: 0.25, removeOnCompletion: false, completion: { _ in + backgroundSnapshotLayer.removeFromSuperlayer() }) + gradientSnapshotLayer.frame = self.backgroundGradientLayer.convert(self.backgroundGradientLayer.bounds, to: self.backgroundView.layer) + backgroundSnapshotLayer.addSublayer(gradientSnapshotLayer) } for layer in self.avatarPatternContentLayers { if let _ = layer.contents, let snapshot = layer.snapshotContentTree() { @@ -295,6 +331,11 @@ public final class PeerInfoCoverComponent: Component { func update(component: PeerInfoCoverComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { let previousComponent = self.component self.component = component + self.currentSize = availableSize + + if case .custom = component.subject { + self.layer.allowsGroupOpacity = true + } if previousComponent?.subject?.fileId != component.subject?.fileId { if let fileId = component.subject?.fileId, fileId != 0 { @@ -349,18 +390,18 @@ public final class PeerInfoCoverComponent: Component { secondaryBackgroundColor = .clear } - self.backgroundView.backgroundColor = secondaryBackgroundColor - + let gradientWidth: CGFloat + let gradientHeight: CGFloat = component.defaultHeight if case .custom = component.subject { - if availableSize.width < availableSize.height { - self.backgroundGradientLayer.startPoint = CGPoint(x: 0.5, y: 0.25) - } else { - self.backgroundGradientLayer.startPoint = CGPoint(x: 0.5, y: 0.5) - } + gradientWidth = gradientHeight + self.backgroundView.backgroundColor = backgroundColor + self.backgroundGradientLayer.startPoint = CGPoint(x: 0.5, y: component.avatarCenter.y / gradientHeight) self.backgroundGradientLayer.endPoint = CGPoint(x: 1.0, y: 1.0) self.backgroundGradientLayer.type = .radial self.backgroundGradientLayer.colors = [secondaryBackgroundColor.cgColor, backgroundColor.cgColor] } else { + gradientWidth = availableSize.width + self.backgroundView.backgroundColor = secondaryBackgroundColor self.backgroundGradientLayer.startPoint = CGPoint(x: 0.5, y: 1.0) self.backgroundGradientLayer.endPoint = CGPoint(x: 0.5, y: 0.0) self.backgroundGradientLayer.type = .axial @@ -368,8 +409,7 @@ public final class PeerInfoCoverComponent: Component { } self.backgroundGradientLayer.anchorPoint = CGPoint(x: 0.0, y: 1.0) - let gradientHeight: CGFloat = component.defaultHeight - let backgroundGradientFrame = CGRect(origin: CGPoint(x: 0.0, y: availableSize.height - gradientHeight), size: CGSize(width: availableSize.width, height: gradientHeight)) + let backgroundGradientFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((availableSize.width - gradientWidth) / 2.0), y: component.gradientOnTop ? 0.0 : availableSize.height - gradientHeight), size: CGSize(width: gradientWidth, height: gradientHeight)) if !transition.animation.isImmediate { let previousPosition = self.backgroundGradientLayer.position let updatedPosition = CGPoint(x: backgroundGradientFrame.minX, y: backgroundGradientFrame.maxY) @@ -426,11 +466,7 @@ public final class PeerInfoCoverComponent: Component { let backgroundPatternContainerFrame = CGRect(origin: CGPoint(x: 0.0, y: availableSize.height), size: CGSize(width: availableSize.width, height: 0.0)) transition.containedViewLayoutTransition.updateFrameAdditive(view: self.backgroundPatternContainer, frame: backgroundPatternContainerFrame) -// if component.peer?.id == component.context.account.peerId { -// transition.setAlpha(view: self.backgroundPatternContainer, alpha: 0.0) -// } else { - transition.setAlpha(view: self.backgroundPatternContainer, alpha: component.patternTransitionFraction) -// } + transition.setAlpha(view: self.backgroundPatternContainer, alpha: component.patternTransitionFraction) var baseDistance: CGFloat = 72.0 var baseRowDistance: CGFloat = 28.0 diff --git a/submodules/TelegramUI/Components/PeerInfo/PeerInfoPaneNode/Sources/PeerInfoPaneNode.swift b/submodules/TelegramUI/Components/PeerInfo/PeerInfoPaneNode/Sources/PeerInfoPaneNode.swift index 785ce6231f..d07d0edd5d 100644 --- a/submodules/TelegramUI/Components/PeerInfo/PeerInfoPaneNode/Sources/PeerInfoPaneNode.swift +++ b/submodules/TelegramUI/Components/PeerInfo/PeerInfoPaneNode/Sources/PeerInfoPaneNode.swift @@ -20,7 +20,8 @@ public enum PeerInfoPaneKey: Int32 { case links case gifs case groupsInCommon - case recommended + case similarChannels + case similarBots } public struct PeerInfoStatusData: Equatable { diff --git a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/Panes/PeerInfoRecommendedChannelsPane.swift b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/Panes/PeerInfoRecommendedPeersPane.swift similarity index 73% rename from submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/Panes/PeerInfoRecommendedChannelsPane.swift rename to submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/Panes/PeerInfoRecommendedPeersPane.swift index 2dd9c7b988..716cb16119 100644 --- a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/Panes/PeerInfoRecommendedChannelsPane.swift +++ b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/Panes/PeerInfoRecommendedPeersPane.swift @@ -20,29 +20,29 @@ import Markdown import SolidRoundedButtonNode import PeerInfoPaneNode -private struct RecommendedChannelsListTransaction { +private struct RecommendedPeersListTransaction { let deletions: [ListViewDeleteItem] let insertions: [ListViewInsertItem] let updates: [ListViewUpdateItem] let animated: Bool } -private enum RecommendedChannelsListEntryStableId: Hashable { +private enum RecommendedPeersListEntryStableId: Hashable { case addMember case peer(PeerId) } -private enum RecommendedChannelsListEntry: Comparable, Identifiable { +private enum RecommendedPeersListEntry: Comparable, Identifiable { case peer(theme: PresentationTheme, index: Int, peer: EnginePeer, subscribers: Int32) - var stableId: RecommendedChannelsListEntryStableId { + var stableId: RecommendedPeersListEntryStableId { switch self { case let .peer(_, _, peer, _): return .peer(peer.id) } } - static func ==(lhs: RecommendedChannelsListEntry, rhs: RecommendedChannelsListEntry) -> Bool { + static func ==(lhs: RecommendedPeersListEntry, rhs: RecommendedPeersListEntry) -> Bool { switch lhs { case let .peer(lhsTheme, lhsIndex, lhsPeer, lhsSubscribers): if case let .peer(rhsTheme, rhsIndex, rhsPeer, rhsSubscribers) = rhs, lhsTheme === rhsTheme, lhsIndex == rhsIndex, lhsPeer == rhsPeer, lhsSubscribers == rhsSubscribers { @@ -53,7 +53,7 @@ private enum RecommendedChannelsListEntry: Comparable, Identifiable { } } - static func <(lhs: RecommendedChannelsListEntry, rhs: RecommendedChannelsListEntry) -> Bool { + static func <(lhs: RecommendedPeersListEntry, rhs: RecommendedPeersListEntry) -> Bool { switch lhs { case let .peer(_, lhsIndex, _, _): switch rhs { @@ -66,8 +66,15 @@ private enum RecommendedChannelsListEntry: Comparable, Identifiable { func item(context: AccountContext, presentationData: PresentationData, action: @escaping (EnginePeer) -> Void, openPeerContextAction: @escaping (Peer, ASDisplayNode, ContextGesture?) -> Void) -> ListViewItem { switch self { case let .peer(_, _, peer, subscribers): - let subtitle = presentationData.strings.Conversation_StatusSubscribers(subscribers) - return ItemListPeerItem(presentationData: ItemListPresentationData(presentationData), dateTimeFormat: presentationData.dateTimeFormat, nameDisplayOrder: presentationData.nameDisplayOrder, context: context, peer: peer, presence: nil, text: .text(subtitle, .secondary), label: .none, editing: ItemListPeerItemEditing(editable: false, editing: false, revealed: false), switchValue: nil, enabled: true, selectable: true, sectionId: 0, action: { + let text: ItemListPeerItemText + if subscribers > 0 { + text = .text(presentationData.strings.Conversation_StatusSubscribers(subscribers), .secondary) + } else if let addressName = peer.addressName { + text = .text("@\(addressName)", .secondary) + } else { + text = .none + } + return ItemListPeerItem(presentationData: ItemListPresentationData(presentationData), dateTimeFormat: presentationData.dateTimeFormat, nameDisplayOrder: presentationData.nameDisplayOrder, context: context, peer: peer, presence: nil, text: text, label: .none, editing: ItemListPeerItemEditing(editable: false, editing: false, revealed: false), switchValue: nil, enabled: true, selectable: true, sectionId: 0, action: { action(peer) }, setPeerIdWithRevealedOptions: { _, _ in }, removePeer: { _ in @@ -78,19 +85,29 @@ private enum RecommendedChannelsListEntry: Comparable, Identifiable { } } -private func preparedTransition(from fromEntries: [RecommendedChannelsListEntry], to toEntries: [RecommendedChannelsListEntry], context: AccountContext, presentationData: PresentationData, action: @escaping (EnginePeer) -> Void, openPeerContextAction: @escaping (Peer, ASDisplayNode, ContextGesture?) -> Void) -> RecommendedChannelsListTransaction { +private func preparedTransition(from fromEntries: [RecommendedPeersListEntry], to toEntries: [RecommendedPeersListEntry], context: AccountContext, presentationData: PresentationData, action: @escaping (EnginePeer) -> Void, openPeerContextAction: @escaping (Peer, ASDisplayNode, ContextGesture?) -> Void) -> RecommendedPeersListTransaction { let (deleteIndices, indicesAndItems, updateIndices) = mergeListsStableWithUpdates(leftList: fromEntries, rightList: toEntries) let deletions = deleteIndices.map { ListViewDeleteItem(index: $0, directionHint: nil) } let insertions = indicesAndItems.map { ListViewInsertItem(index: $0.0, previousIndex: $0.2, item: $0.1.item(context: context, presentationData: presentationData, action: action, openPeerContextAction: openPeerContextAction), directionHint: nil) } let updates = updateIndices.map { ListViewUpdateItem(index: $0.0, previousIndex: $0.2, item: $0.1.item(context: context, presentationData: presentationData, action: action, openPeerContextAction: openPeerContextAction), directionHint: nil) } - return RecommendedChannelsListTransaction(deletions: deletions, insertions: insertions, updates: updates, animated: toEntries.count < fromEntries.count) + return RecommendedPeersListTransaction(deletions: deletions, insertions: insertions, updates: updates, animated: toEntries.count < fromEntries.count) } -private let channelsLimit: Int32 = 8 +private protocol RecommendedPeers { + +} -final class PeerInfoRecommendedChannelsPaneNode: ASDisplayNode, PeerInfoPaneNode { +extension RecommendedChannels: RecommendedPeers { + +} + +extension RecommendedBots: RecommendedPeers { + +} + +final class PeerInfoRecommendedPeersPaneNode: ASDisplayNode, PeerInfoPaneNode { private let context: AccountContext private let chatControllerInteraction: ChatControllerInteraction private let openPeerContextAction: (Bool, Peer, ASDisplayNode, ContextGesture?) -> Void @@ -98,9 +115,9 @@ final class PeerInfoRecommendedChannelsPaneNode: ASDisplayNode, PeerInfoPaneNode weak var parentController: ViewController? private let listNode: ListView - private var currentEntries: [RecommendedChannelsListEntry] = [] - private var currentState: (RecommendedChannels?, Bool)? - private var enqueuedTransactions: [RecommendedChannelsListTransaction] = [] + private var currentEntries: [RecommendedPeersListEntry] = [] + private var enqueuedTransactions: [RecommendedPeersListTransaction] = [] + private var currentState: (RecommendedPeers?, Bool)? private var unlockBackground: UIImageView? private var unlockText: ComponentView? @@ -145,32 +162,35 @@ final class PeerInfoRecommendedChannelsPaneNode: ASDisplayNode, PeerInfoPaneNode self.listNode.preloadPages = true self.addSubnode(self.listNode) + let signal: Signal + if peerId.namespace == Namespaces.Peer.CloudUser { + signal = context.engine.peers.recommendedBots(peerId: peerId) + |> map { + $0 as RecommendedPeers? + } + } else { + signal = context.engine.peers.recommendedChannels(peerId: peerId) + |> map { + $0 as RecommendedPeers? + } + } + self.disposable = (combineLatest(queue: .mainQueue(), self.presentationDataPromise.get(), - context.engine.peers.recommendedChannels(peerId: peerId), + signal, context.engine.data.subscribe(TelegramEngine.EngineData.Item.Peer.Peer(id: context.account.peerId)) |> map { peer -> Bool in return peer?.isPremium ?? false } ) - |> deliverOnMainQueue).startStrict(next: { [weak self] presentationData, recommendedChannels, isPremium in - guard let strongSelf = self else { + |> deliverOnMainQueue).startStrict(next: { [weak self] presentationData, recommendedPeers, isPremium in + guard let self else { return } - strongSelf.currentState = (recommendedChannels, isPremium) - strongSelf.updateState(recommendedChannels: recommendedChannels, isPremium: isPremium, presentationData: presentationData) + self.currentState = (recommendedPeers, isPremium) + self.updateState(recommendedPeers: recommendedPeers, isPremium: isPremium, presentationData: presentationData) }) - - self.statusPromise.set(context.engine.data.subscribe( - TelegramEngine.EngineData.Item.Peer.ParticipantCount(id: peerId) - ) - |> map { count -> PeerInfoStatusData? in - if let count { - return PeerInfoStatusData(text: presentationData.strings.Conversation_StatusSubscribers(Int32(count)), isActivity: true, key: .recommended) - } - return nil - }) - + self.listNode.visibleBottomContentOffsetChanged = { [weak self] offset in if let self { self.layoutUnlockPanel(transition: .animated(duration: 0.4, curve: .spring)) @@ -215,8 +235,8 @@ final class PeerInfoRecommendedChannelsPaneNode: ASDisplayNode, PeerInfoPaneNode self.listNode.scrollEnabled = !isScrollingLockedAtTop - if isFirstLayout, let (recommendedChannels, isPremium) = self.currentState { - self.updateState(recommendedChannels: recommendedChannels, isPremium: isPremium, presentationData: presentationData) + if isFirstLayout, let (recommendedPeers, isPremium) = self.currentState { + self.updateState(recommendedPeers: recommendedPeers, isPremium: isPremium, presentationData: presentationData) } } @@ -225,8 +245,16 @@ final class PeerInfoRecommendedChannelsPaneNode: ASDisplayNode, PeerInfoPaneNode self.chatControllerInteraction.navigationController()?.pushViewController(controller) } + private func updateState(recommendedPeers: RecommendedPeers?, isPremium: Bool, presentationData: PresentationData) { + if let recommendedChannels = recommendedPeers as? RecommendedChannels { + self.updateState(recommendedChannels: recommendedChannels, isPremium: isPremium, presentationData: presentationData) + } else if let recommendedBots = recommendedPeers as? RecommendedBots { + self.updateState(recommendedBots: recommendedBots, isPremium: isPremium, presentationData: presentationData) + } + } + private func updateState(recommendedChannels: RecommendedChannels?, isPremium: Bool, presentationData: PresentationData) { - var entries: [RecommendedChannelsListEntry] = [] + var entries: [RecommendedPeersListEntry] = [] if let channels = recommendedChannels?.channels { for channel in channels { @@ -243,6 +271,42 @@ final class PeerInfoRecommendedChannelsPaneNode: ASDisplayNode, PeerInfoPaneNode self.currentEntries = entries self.enqueuedTransactions.append(transaction) self.dequeueTransaction() + + if let recommendedChannels { + self.statusPromise.set(.single( + PeerInfoStatusData(text: presentationData.strings.SharedMedia_SimilarChannelCount(recommendedChannels.count), isActivity: true, key: .similarChannels) + )) + } + } + + private func updateState(recommendedBots: RecommendedBots?, isPremium: Bool, presentationData: PresentationData) { + var entries: [RecommendedPeersListEntry] = [] + + if let bots = recommendedBots?.bots { + for bot in bots { + var subscriberCount: Int32 = 0 + if case let .user(user) = bot { + subscriberCount = user.subscriberCount ?? 0 + } + entries.append(.peer(theme: presentationData.theme, index: entries.count, peer: bot, subscribers: subscriberCount)) + } + } + + let transaction = preparedTransition(from: self.currentEntries, to: entries, context: self.context, presentationData: presentationData, action: { [weak self] peer in + self?.chatControllerInteraction.openPeer(peer, .info(nil), nil, .default) + }, openPeerContextAction: { [weak self] peer, node, gesture in + self?.openPeerContextAction(true, peer, node, gesture) + }) + + self.currentEntries = entries + self.enqueuedTransactions.append(transaction) + self.dequeueTransaction() + + if let recommendedBots { + self.statusPromise.set(.single( + PeerInfoStatusData(text: presentationData.strings.SharedMedia_SimilarBotCount(recommendedBots.count), isActivity: true, key: .similarBots) + )) + } } private func layoutUnlockPanel(transition: ContainedViewLayoutTransition) { @@ -278,6 +342,11 @@ final class PeerInfoRecommendedChannelsPaneNode: ASDisplayNode, PeerInfoPaneNode self.view.addSubview(unlockBackground) self.unlockBackground = unlockBackground } + + var isBots = false + if let (state, _) = self.currentState, state is RecommendedBots { + isBots = true + } if let current = self.unlockButton { unlockButton = current @@ -289,7 +358,7 @@ final class PeerInfoRecommendedChannelsPaneNode: ASDisplayNode, PeerInfoPaneNode unlockButton.animationLoopTime = 2.5 unlockButton.animation = "premium_unlock" unlockButton.iconPosition = .right - unlockButton.title = presentationData.strings.Channel_SimilarChannels_ShowMore + unlockButton.title = isBots ? presentationData.strings.PeerInfo_SimilarBots_ShowMore : presentationData.strings.Channel_SimilarChannels_ShowMore unlockButton.pressed = { [weak self] in self?.unlockPressed() @@ -320,7 +389,7 @@ final class PeerInfoRecommendedChannelsPaneNode: ASDisplayNode, PeerInfoPaneNode transition: .immediate, component: AnyComponent( MultilineTextComponent( - text: .markdown(text: presentationData.strings.Channel_SimilarChannels_ShowMoreInfo, attributes: markdownAttributes), + text: .markdown(text: isBots ? presentationData.strings.PeerInfo_SimilarBots_ShowMoreInfo : presentationData.strings.Channel_SimilarChannels_ShowMoreInfo, attributes: markdownAttributes), horizontalAlignment: .center, maximumNumberOfLines: 0, lineSpacing: 0.2 diff --git a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoData.swift b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoData.swift index b3c854765b..09a0fb2e32 100644 --- a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoData.swift +++ b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoData.swift @@ -1008,6 +1008,13 @@ func peerInfoScreenData(context: AccountContext, peerId: PeerId, strings: Presen groupsInCommon = nil } + let recommendedBots: Signal + if case .bot = kind { + recommendedBots = context.engine.peers.recommendedBots(peerId: userPeerId) + } else { + recommendedBots = .single(nil) + } + let premiumGiftOptions: Signal<[PremiumGiftCodeOption], NoError> let profileGiftsContext: ProfileGiftsContext? if case .user = kind { @@ -1309,6 +1316,7 @@ func peerInfoScreenData(context: AccountContext, peerId: PeerId, strings: Presen status, hasStories, hasStoryArchive, + recommendedBots, accountIsPremium, savedMessagesPeer, hasSavedMessagesChats, @@ -1322,7 +1330,7 @@ func peerInfoScreenData(context: AccountContext, peerId: PeerId, strings: Presen premiumGiftOptions, webAppPermissions ) - |> map { peerView, availablePanes, globalNotificationSettings, encryptionKeyFingerprint, status, hasStories, hasStoryArchive, accountIsPremium, savedMessagesPeer, hasSavedMessagesChats, hasSavedMessages, hasSavedMessageTags, hasBotPreviewItems, personalChannel, privacySettings, starsRevenueContextAndState, revenueContextAndState, premiumGiftOptions, webAppPermissions -> PeerInfoScreenData in + |> map { peerView, availablePanes, globalNotificationSettings, encryptionKeyFingerprint, status, hasStories, hasStoryArchive, recommendedBots, accountIsPremium, savedMessagesPeer, hasSavedMessagesChats, hasSavedMessages, hasSavedMessageTags, hasBotPreviewItems, personalChannel, privacySettings, starsRevenueContextAndState, revenueContextAndState, premiumGiftOptions, webAppPermissions -> PeerInfoScreenData in var availablePanes = availablePanes if isMyProfile { availablePanes?.insert(.stories, at: 0) @@ -1373,6 +1381,10 @@ func peerInfoScreenData(context: AccountContext, peerId: PeerId, strings: Presen availablePanes?.insert(.botPreview, at: 0) } } + + if let recommendedBots, recommendedBots.count > 0 { + availablePanes?.append(.similarBots) + } } else { availablePanes = nil } @@ -1574,7 +1586,7 @@ func peerInfoScreenData(context: AccountContext, peerId: PeerId, strings: Presen availablePanes?.insert(.stories, at: 0) } if let recommendedChannels, !recommendedChannels.channels.isEmpty { - availablePanes?.append(.recommended) + availablePanes?.append(.similarChannels) } if case .peer = chatLocation { diff --git a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoHeaderNode.swift b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoHeaderNode.swift index 10aaec713f..162eac20a0 100644 --- a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoHeaderNode.swift +++ b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoHeaderNode.swift @@ -166,6 +166,7 @@ final class PeerInfoHeaderNode: ASDisplayNode { var displayPremiumIntro: ((UIView, PeerEmojiStatus?, Signal<(TelegramMediaFile, LoadedStickerPack)?, NoError>, Bool) -> Void)? var displayStatusPremiumIntro: (() -> Void)? + var displayUniqueGiftInfo: ((UIView, String) -> Void)? var navigateToForum: (() -> Void)? @@ -408,6 +409,16 @@ final class PeerInfoHeaderNode: ASDisplayNode { self.displayPremiumIntro?(self.isAvatarExpanded ? self.titleExpandedCredibilityIconView : self.titleCredibilityIconView, nil, .never(), self.isAvatarExpanded) } + func invokeDisplayGiftInfo() { + guard case let .emojiStatus(status) = self.currentStatusIcon, case let .starGift(_, _, title, slug, _, _, _, _, _) = status.content else { + return + } + let slugComponents = slug.components(separatedBy: "-") + if let numberString = slugComponents.last { + self.displayUniqueGiftInfo?(self.isAvatarExpanded ? self.titleExpandedStatusIconView : self.titleStatusIconView, "\(title) #\(numberString)") + } + } + func initiateAvatarExpansion(gallery: Bool, first: Bool) { if let peer = self.peer, peer.profileImageRepresentations.isEmpty && gallery { self.requestOpenAvatarForEditing?(false) @@ -554,7 +565,7 @@ final class PeerInfoHeaderNode: ASDisplayNode { credibilityIcon = .verified } if let verificationIconFileId = peer.verificationIconFileId { - verifiedIcon = .emojiStatus(PeerEmojiStatus(fileId: verificationIconFileId, expirationDate: nil)) + verifiedIcon = .emojiStatus(PeerEmojiStatus(content: .emoji(fileId: verificationIconFileId), expirationDate: nil)) } } @@ -805,7 +816,7 @@ final class PeerInfoHeaderNode: ASDisplayNode { emojiRegularStatusContent = .animation(content: .customEmoji(fileId: emojiStatus.fileId), size: CGSize(width: 80.0, height: 80.0), placeholderColor: presentationData.theme.list.mediaPlaceholderColor, themeColor: navigationContentsAccentColor, loopMode: .forever) emojiExpandedStatusContent = .animation(content: .customEmoji(fileId: emojiStatus.fileId), size: CGSize(width: 80.0, height: 80.0), placeholderColor: navigationContentsAccentColor, themeColor: navigationContentsAccentColor, loopMode: .forever) } - + let iconSize = self.titleCredibilityIconView.update( transition: ComponentTransition(navigationTransition), component: AnyComponent(EmojiStatusComponent( @@ -857,6 +868,8 @@ final class PeerInfoHeaderNode: ASDisplayNode { self.currentStatusIcon = statusIcon var currentEmojiStatus: PeerEmojiStatus? + var particleColor: UIColor? + let emojiRegularStatusContent: EmojiStatusComponent.Content let emojiExpandedStatusContent: EmojiStatusComponent.Content switch statusIcon { @@ -864,11 +877,15 @@ final class PeerInfoHeaderNode: ASDisplayNode { currentEmojiStatus = emojiStatus emojiRegularStatusContent = .animation(content: .customEmoji(fileId: emojiStatus.fileId), size: CGSize(width: 80.0, height: 80.0), placeholderColor: presentationData.theme.list.mediaPlaceholderColor, themeColor: navigationContentsAccentColor, loopMode: .forever) emojiExpandedStatusContent = .animation(content: .customEmoji(fileId: emojiStatus.fileId), size: CGSize(width: 80.0, height: 80.0), placeholderColor: navigationContentsAccentColor, themeColor: navigationContentsAccentColor, loopMode: .forever) + + if let _ = emojiStatus.color { + particleColor = UIColor.white + } default: emojiRegularStatusContent = .none emojiExpandedStatusContent = .none } - + let iconSize = self.titleStatusIconView.update( transition: ComponentTransition(navigationTransition), component: AnyComponent(EmojiStatusComponent( @@ -876,13 +893,18 @@ final class PeerInfoHeaderNode: ASDisplayNode { animationCache: self.animationCache, animationRenderer: self.animationRenderer, content: emojiRegularStatusContent, + particleColor: particleColor, isVisibleForAnimations: true, useSharedAnimation: true, action: { [weak self] in guard let strongSelf = self else { return } - strongSelf.displayPremiumIntro?(strongSelf.titleStatusIconView, currentEmojiStatus, strongSelf.emojiStatusFileAndPackTitle.get(), false) + if let _ = particleColor { + strongSelf.invokeDisplayGiftInfo() + } else { + strongSelf.displayPremiumIntro?(strongSelf.titleStatusIconView, currentEmojiStatus, strongSelf.emojiStatusFileAndPackTitle.get(), false) + } }, emojiFileUpdated: { [weak self] emojiFile in guard let strongSelf = self else { @@ -932,13 +954,18 @@ final class PeerInfoHeaderNode: ASDisplayNode { animationCache: self.animationCache, animationRenderer: self.animationRenderer, content: emojiExpandedStatusContent, + particleColor: particleColor, isVisibleForAnimations: true, useSharedAnimation: true, action: { [weak self] in guard let strongSelf = self else { return } - strongSelf.displayPremiumIntro?(strongSelf.titleExpandedStatusIconView, currentEmojiStatus, strongSelf.emojiStatusFileAndPackTitle.get(), true) + if let _ = particleColor { + strongSelf.invokeDisplayGiftInfo() + } else { + strongSelf.displayPremiumIntro?(strongSelf.titleExpandedStatusIconView, currentEmojiStatus, strongSelf.emojiStatusFileAndPackTitle.get(), true) + } } )), environment: {}, @@ -1429,10 +1456,10 @@ final class PeerInfoHeaderNode: ASDisplayNode { leftExpandedOffset = nextExpandedIconX + 4.0 } else { titleHorizontalOffset += (verifiedIconSize.width + 4.0) / 2.0 - titleExpandedHorizontalOffset += titleExpandedVerifiedIconSize.width + titleExpandedHorizontalOffset += titleExpandedVerifiedIconSize.width - 2.0 leftOffset = -verifiedIconSize.width - 4.0 - leftExpandedOffset = -titleExpandedVerifiedIconSize.width - 8.0 + leftExpandedOffset = -titleExpandedVerifiedIconSize.width - 4.0 } var collapsedTransitionOffset: CGFloat = 0.0 @@ -2217,11 +2244,22 @@ final class PeerInfoHeaderNode: ASDisplayNode { transition.updateFrame(view: self.backgroundBannerView, frame: bannerFrame) } + let backgroundCoverSubject: PeerInfoCoverComponent.Subject? + var backgroundCoverAnimateIn = false + if let status = peer?.emojiStatus, case let .starGift(_, _, _, _, patternFileId, innerColor, outerColor, patternColor, _) = status.content { + backgroundCoverSubject = .custom(UIColor(rgb: UInt32(bitPattern: innerColor)), UIColor(rgb: UInt32(bitPattern: outerColor)), UIColor(rgb: UInt32(bitPattern: patternColor)), patternFileId) + backgroundCoverAnimateIn = true + } else if let peer { + backgroundCoverSubject = .peer(EnginePeer(peer)) + } else { + backgroundCoverSubject = nil + } + let backgroundCoverSize = self.backgroundCover.update( transition: ComponentTransition(transition), component: AnyComponent(PeerInfoCoverComponent( context: self.context, - subject: peer.flatMap { .peer(EnginePeer($0)) }, + subject: backgroundCoverSubject, files: [:], isDark: presentationData.theme.overallDarkAppearance, avatarCenter: apparentAvatarFrame.center, @@ -2233,9 +2271,25 @@ final class PeerInfoHeaderNode: ASDisplayNode { environment: {}, containerSize: CGSize(width: width + bannerInset * 2.0, height: apparentBackgroundHeight + bannerInset) ) - if let backgroundCoverView = self.backgroundCover.view { + if let backgroundCoverView = self.backgroundCover.view as? PeerInfoCoverComponent.View { if backgroundCoverView.superview == nil { self.backgroundBannerView.addSubview(backgroundCoverView) + + if backgroundCoverAnimateIn { + if !self.isAvatarExpanded { + backgroundCoverView.willAnimateIn() + Queue.mainQueue().after(0.2) { + backgroundCoverView.animateIn() + } + Queue.mainQueue().after(0.44) { + self.invokeDisplayGiftInfo() + } + } else { + Queue.mainQueue().after(0.44) { + self.invokeDisplayGiftInfo() + } + } + } } if additive { transition.updateFrameAdditive(view: backgroundCoverView, frame: CGRect(origin: CGPoint(x: -3.0, y: bannerFrame.height - backgroundCoverSize.height - bannerInset), size: backgroundCoverSize)) diff --git a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoPaneContainerNode.swift b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoPaneContainerNode.swift index 940bc2c5b9..bbc655fc54 100644 --- a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoPaneContainerNode.swift +++ b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoPaneContainerNode.swift @@ -545,8 +545,8 @@ private final class PeerInfoPendingPane { } else { preconditionFailure() } - case .recommended: - paneNode = PeerInfoRecommendedChannelsPaneNode(context: context, peerId: peerId, chatControllerInteraction: chatControllerInteraction, openPeerContextAction: openPeerContextAction) + case .similarChannels, .similarBots: + paneNode = PeerInfoRecommendedPeersPaneNode(context: context, peerId: peerId, chatControllerInteraction: chatControllerInteraction, openPeerContextAction: openPeerContextAction) case .savedMessagesChats: paneNode = PeerInfoChatListPaneNode(context: context, navigationController: chatControllerInteraction.navigationController) case .savedMessages: @@ -1201,8 +1201,10 @@ final class PeerInfoPaneContainerNode: ASDisplayNode, ASGestureRecognizerDelegat title = presentationData.strings.PeerInfo_PaneGroups case .members: title = presentationData.strings.PeerInfo_PaneMembers - case .recommended: + case .similarChannels: title = presentationData.strings.PeerInfo_PaneRecommended + case .similarBots: + title = presentationData.strings.PeerInfo_PaneRecommendedBots case .savedMessagesChats: title = presentationData.strings.DialogList_TabTitle case .savedMessages: diff --git a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreen.swift b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreen.swift index ee8d55af0f..5731d12fc8 100644 --- a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreen.swift +++ b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreen.swift @@ -4630,6 +4630,27 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro }) } + self.headerNode.displayUniqueGiftInfo = { [weak self] sourceView, text in + guard let self, let controller = self.controller else { + return + } + let sourceRect = sourceView.convert(sourceView.bounds, to: controller.view) + let tooltipController = TooltipScreen( + context: self.context, + account: self.context.account, + sharedContext: self.context.sharedContext, + text: .attributedString(text: NSAttributedString(string: text, font: Font.semibold(11.0), textColor: .white)), + style: .customBlur(UIColor(rgb: self.headerNode.isAvatarExpanded ? 0x000000 : 0x92c8de, alpha: 0.65), -4.0), + arrowStyle: .small, + location: .point(sourceRect, .bottom), + cornerRadius: 10.0, + shouldDismissOnTouch: { _, _ in + return .dismiss(consume: false) + } + ) + controller.present(tooltipController, in: .current) + } + self.headerNode.displayStatusPremiumIntro = { [weak self] in guard let self else { return @@ -4793,6 +4814,10 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro }) } + if peerId.namespace == Namespaces.Peer.CloudUser { + let _ = context.engine.peers.requestRecommendedBots(peerId: peerId, forceUpdate: true).startStandalone() + } + if peerId.namespace == Namespaces.Peer.CloudChannel || peerId.namespace == Namespaces.Peer.CloudUser { self.storiesReady.set(false) let expiringStoryList = PeerExpiringStoryListContext(account: context.account, peerId: peerId) @@ -12651,7 +12676,7 @@ public final class PeerInfoScreenImpl: ViewController, PeerInfoScreen, KeyShortc override public func loadDisplayNode() { var initialPaneKey: PeerInfoPaneKey? if self.switchToRecommendedChannels { - initialPaneKey = .recommended + initialPaneKey = .similarChannels } else if self.switchToGifts { initialPaneKey = .gifts } diff --git a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreenAvatarSetup.swift b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreenAvatarSetup.swift index db286e2590..b1bf6aacfc 100644 --- a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreenAvatarSetup.swift +++ b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreenAvatarSetup.swift @@ -16,6 +16,7 @@ import UndoUI import PeerAvatarGalleryUI import PresentationDataUtils import LegacyComponents +import LegacyMediaPickerUI extension PeerInfoScreenImpl { func newopenAvatarForEditing(mode: PeerInfoAvatarEditingMode = .generic, fromGallery: Bool = false, completion: @escaping (UIImage?) -> Void = { _ in }, completedWithUploadingImage: @escaping (UIImage, Signal) -> UIView? = { _, _ in nil }) { @@ -419,226 +420,226 @@ extension PeerInfoScreenImpl { let photoResource = LocalFileMediaResource(fileId: Int64.random(in: Int64.min ... Int64.max)) self.context.account.postbox.mediaBox.storeResourceData(photoResource.id, data: data) -// let representation = TelegramMediaImageRepresentation(dimensions: PixelDimensions(width: 640, height: 640), resource: photoResource, progressiveSizes: [], immediateThumbnailData: nil, hasVideo: false, isPersonal: mode == .custom ? true : false) -// -// var markup: UploadPeerPhotoMarkup? = nil -// if let fileId = adjustments?.documentId, let backgroundColors = adjustments?.colors as? [Int32], fileId != 0 { -// if let packId = adjustments?.stickerPackId, let accessHash = adjustments?.stickerPackAccessHash, packId != 0 { -// markup = .sticker(packReference: .id(id: packId, accessHash: accessHash), fileId: fileId, backgroundColors: backgroundColors) -// } else { -// markup = .emoji(fileId: fileId, backgroundColors: backgroundColors) -// } -// } -// -// var uploadVideo = true -// if let _ = markup { -// if let data = self.context.currentAppConfiguration.with({ $0 }).data, let uploadVideoValue = data["upload_markup_video"] as? Bool, uploadVideoValue { -// uploadVideo = true -// } else { -// uploadVideo = false -// } -// } -// -// if [.suggest, .fallback].contains(mode) { -// } else { -// self.controllerNode.state = self.controllerNode.state.withUpdatingAvatar(.image(representation)) -// if !uploadVideo { -// self.controllerNode.state = self.controllerNode.state.withAvatarUploadProgress(.indefinite) -// } -// } -// -// if let (layout, navigationHeight) = self.controllerNode.validLayout { -// self.controllerNode.containerLayoutUpdated(layout: layout, navigationHeight: navigationHeight, transition: mode == .custom ? .animated(duration: 0.2, curve: .easeInOut) : .immediate, additive: false) -// } -// self.controllerNode.headerNode.ignoreCollapse = false -// -// var videoStartTimestamp: Double? = nil -// if let adjustments = adjustments, adjustments.videoStartValue > 0.0 { -// videoStartTimestamp = adjustments.videoStartValue - adjustments.trimStartValue -// } -// -// let account = self.context.account -// let context = self.context -// -// let videoResource: Signal -// if uploadVideo { -// videoResource = Signal { [weak self] subscriber in -// let entityRenderer: LegacyPaintEntityRenderer? = adjustments.flatMap { adjustments in -// if let paintingData = adjustments.paintingData, paintingData.hasAnimation { -// return LegacyPaintEntityRenderer(postbox: account.postbox, adjustments: adjustments) -// } else { -// return nil -// } -// } -// -// let tempFile = EngineTempBox.shared.tempFile(fileName: "video.mp4") -// let uploadInterface = LegacyLiveUploadInterface(context: context) -// let signal: SSignal -// if let url = asset as? URL, url.absoluteString.hasSuffix(".jpg"), let data = try? Data(contentsOf: url, options: [.mappedRead]), let image = UIImage(data: data), let entityRenderer = entityRenderer { -// let durationSignal: SSignal = SSignal(generator: { subscriber in -// let disposable = (entityRenderer.duration()).start(next: { duration in -// subscriber.putNext(duration) -// subscriber.putCompletion() -// }) -// -// return SBlockDisposable(block: { -// disposable.dispose() -// }) -// }) -// signal = durationSignal.map(toSignal: { duration -> SSignal in -// if let duration = duration as? Double { -// return TGMediaVideoConverter.renderUIImage(image, duration: duration, adjustments: adjustments, path: tempFile.path, watcher: nil, entityRenderer: entityRenderer)! -// } else { -// return SSignal.single(nil) -// } -// }) -// } else if let asset = asset as? AVAsset { -// signal = TGMediaVideoConverter.convert(asset, adjustments: adjustments, path: tempFile.path, watcher: uploadInterface, entityRenderer: entityRenderer)! -// } else { -// signal = SSignal.complete() -// } -// -// let signalDisposable = signal.start(next: { next in -// if let result = next as? TGMediaVideoConversionResult { -// if let image = result.coverImage, let data = image.jpegData(compressionQuality: 0.7) { -// account.postbox.mediaBox.storeResourceData(photoResource.id, data: data) -// } -// -// if let timestamp = videoStartTimestamp { -// videoStartTimestamp = max(0.0, min(timestamp, result.duration - 0.05)) -// } -// -// var value = stat() -// if stat(result.fileURL.path, &value) == 0 { -// if let data = try? Data(contentsOf: result.fileURL) { -// let resource: TelegramMediaResource -// if let liveUploadData = result.liveUploadData as? LegacyLiveUploadInterfaceResult { -// resource = LocalFileMediaResource(fileId: liveUploadData.id) -// } else { -// resource = LocalFileMediaResource(fileId: Int64.random(in: Int64.min ... Int64.max)) -// } -// account.postbox.mediaBox.storeResourceData(resource.id, data: data, synchronous: true) -// subscriber.putNext(resource) -// -// EngineTempBox.shared.dispose(tempFile) -// } -// } -// subscriber.putCompletion() -// } else if let strongSelf = self, let progress = next as? NSNumber { -// Queue.mainQueue().async { -// strongSelf.state = strongSelf.state.withAvatarUploadProgress(.value(CGFloat(progress.floatValue * 0.45))) -// if let (layout, navigationHeight) = strongSelf.validLayout { -// strongSelf.containerLayoutUpdated(layout: layout, navigationHeight: navigationHeight, transition: .immediate, additive: false) -// } -// } -// } -// }, error: { _ in -// }, completed: nil) -// -// let disposable = ActionDisposable { -// signalDisposable?.dispose() -// } -// -// return ActionDisposable { -// disposable.dispose() -// } -// } -// } else { -// videoResource = .single(nil) -// } -// -// var dismissStatus: (() -> Void)? -// if [.suggest, .fallback, .accept].contains(mode) { -// let statusController = OverlayStatusController(theme: self.presentationData.theme, type: .loading(cancelled: { [weak self] in -// self?.controllerNode.updateAvatarDisposable.set(nil) -// dismissStatus?() -// })) -// dismissStatus = { [weak statusController] in -// statusController?.dismiss() -// } -// if let topController = self.navigationController?.topViewController as? ViewController { -// topController.presentInGlobalOverlay(statusController) -// } else if let topController = self.parentController?.topViewController as? ViewController { -// topController.presentInGlobalOverlay(statusController) -// } else { -// self.presentInGlobalOverlay(statusController) -// } -// } -// -// let peerId = self.peerId -// let isSettings = self.isSettings -// let isMyProfile = self.isMyProfile -// self.controllerNode.updateAvatarDisposable.set((videoResource -// |> mapToSignal { videoResource -> Signal in -// if isSettings || isMyProfile { -// if case .fallback = mode { -// return context.engine.accountData.updateFallbackPhoto(resource: photoResource, videoResource: videoResource, videoStartTimestamp: videoStartTimestamp, markup: markup, mapResourceToAvatarSizes: { resource, representations in -// return mapResourceToAvatarSizes(postbox: account.postbox, resource: resource, representations: representations) -// }) -// } else { -// return context.engine.accountData.updateAccountPhoto(resource: photoResource, videoResource: videoResource, videoStartTimestamp: videoStartTimestamp, markup: markup, mapResourceToAvatarSizes: { resource, representations in -// return mapResourceToAvatarSizes(postbox: account.postbox, resource: resource, representations: representations) -// }) -// } -// } else if case .custom = mode { -// return context.engine.contacts.updateContactPhoto(peerId: peerId, resource: photoResource, videoResource: videoResource, videoStartTimestamp: videoStartTimestamp, markup: markup, mode: .custom, mapResourceToAvatarSizes: { resource, representations in -// return mapResourceToAvatarSizes(postbox: account.postbox, resource: resource, representations: representations) -// }) -// } else if case .suggest = mode { -// return context.engine.contacts.updateContactPhoto(peerId: peerId, resource: photoResource, videoResource: videoResource, videoStartTimestamp: videoStartTimestamp, markup: markup, mode: .suggest, mapResourceToAvatarSizes: { resource, representations in -// return mapResourceToAvatarSizes(postbox: account.postbox, resource: resource, representations: representations) -// }) -// } else { -// return context.engine.peers.updatePeerPhoto(peerId: peerId, photo: context.engine.peers.uploadedPeerPhoto(resource: photoResource), video: videoResource.flatMap { context.engine.peers.uploadedPeerVideo(resource: $0) |> map(Optional.init) }, videoStartTimestamp: videoStartTimestamp, markup: markup, mapResourceToAvatarSizes: { resource, representations in -// return mapResourceToAvatarSizes(postbox: account.postbox, resource: resource, representations: representations) -// }) -// } -// } -// |> deliverOnMainQueue).startStrict(next: { [weak self] result in -// guard let strongSelf = self else { -// return -// } -// switch result { -// case .complete: -// strongSelf.controllerNode.state = strongSelf.controllerNode.state.withUpdatingAvatar(nil).withAvatarUploadProgress(nil) -// case let .progress(value): -// strongSelf.controllerNode.state = strongSelf.controllerNode.state.withAvatarUploadProgress(.value(CGFloat(0.45 + value * 0.55))) -// } -// if let (layout, navigationHeight) = strongSelf.controllerNode.validLayout { -// strongSelf.controllerNode.containerLayoutUpdated(layout: layout, navigationHeight: navigationHeight, transition: .immediate, additive: false) -// } -// -// if case .complete = result { -// dismissStatus?() -// -// let _ = (strongSelf.context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: strongSelf.peerId)) -// |> deliverOnMainQueue).startStandalone(next: { [weak self] peer in -// if let strongSelf = self, let peer { -// switch mode { -// case .fallback: -// (strongSelf.parentController?.topViewController as? ViewController)?.present(UndoOverlayController(presentationData: strongSelf.presentationData, content: .image(image: image, title: nil, text: strongSelf.presentationData.strings.Privacy_ProfilePhoto_PublicVideoSuccess, round: true, undoText: nil), elevatedLayout: false, animateInAsReplacement: true, action: { _ in return false }), in: .current) -// case .custom: -// strongSelf.present(UndoOverlayController(presentationData: strongSelf.presentationData, content: .invitedToVoiceChat(context: strongSelf.context, peer: peer, title: nil, text: strongSelf.presentationData.strings.UserInfo_SetCustomPhoto_SuccessVideoText(peer.compactDisplayTitle).string, action: nil, duration: 5), elevatedLayout: false, animateInAsReplacement: true, action: { _ in return false }), in: .current) -// -// let _ = (strongSelf.context.peerChannelMemberCategoriesContextsManager.profilePhotos(postbox: strongSelf.context.account.postbox, network: strongSelf.context.account.network, peerId: strongSelf.peerId, fetch: peerInfoProfilePhotos(context: strongSelf.context, peerId: strongSelf.peerId)) |> ignoreValues).startStandalone() -// case .suggest: -// if let navigationController = (strongSelf.navigationController as? NavigationController) { -// strongSelf.context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, context: strongSelf.context, chatLocation: .peer(peer), keepStack: .default, completion: { _ in -// })) -// } -// case .accept: -// (strongSelf.parentController?.topViewController as? ViewController)?.present(UndoOverlayController(presentationData: strongSelf.presentationData, content: .image(image: image, title: strongSelf.presentationData.strings.Conversation_SuggestedVideoSuccess, text: strongSelf.presentationData.strings.Conversation_SuggestedVideoSuccessText, round: true, undoText: nil), elevatedLayout: false, animateInAsReplacement: true, action: { [weak self] action in -// if case .info = action { -// self?.parentController?.openSettings() -// } -// return false -// }), in: .current) -// default: -// break -// } -// } -// }) -// } -// })) + let representation = TelegramMediaImageRepresentation(dimensions: PixelDimensions(width: 640, height: 640), resource: photoResource, progressiveSizes: [], immediateThumbnailData: nil, hasVideo: false, isPersonal: mode == .custom ? true : false) + + var markup: UploadPeerPhotoMarkup? = nil + if let fileId = adjustments?.documentId, let backgroundColors = adjustments?.colors as? [Int32], fileId != 0 { + if let packId = adjustments?.stickerPackId, let accessHash = adjustments?.stickerPackAccessHash, packId != 0 { + markup = .sticker(packReference: .id(id: packId, accessHash: accessHash), fileId: fileId, backgroundColors: backgroundColors) + } else { + markup = .emoji(fileId: fileId, backgroundColors: backgroundColors) + } + } + + var uploadVideo = true + if let _ = markup { + if let data = self.context.currentAppConfiguration.with({ $0 }).data, let uploadVideoValue = data["upload_markup_video"] as? Bool, uploadVideoValue { + uploadVideo = true + } else { + uploadVideo = false + } + } + + if [.suggest, .fallback].contains(mode) { + } else { + self.controllerNode.state = self.controllerNode.state.withUpdatingAvatar(.image(representation)) + if !uploadVideo { + self.controllerNode.state = self.controllerNode.state.withAvatarUploadProgress(.indefinite) + } + } + + if let (layout, navigationHeight) = self.controllerNode.validLayout { + self.controllerNode.containerLayoutUpdated(layout: layout, navigationHeight: navigationHeight, transition: mode == .custom ? .animated(duration: 0.2, curve: .easeInOut) : .immediate, additive: false) + } + self.controllerNode.headerNode.ignoreCollapse = false + + var videoStartTimestamp: Double? = nil + if let adjustments = adjustments, adjustments.videoStartValue > 0.0 { + videoStartTimestamp = adjustments.videoStartValue - adjustments.trimStartValue + } + + let account = self.context.account + let context = self.context + + let videoResource: Signal + if uploadVideo { + videoResource = Signal { [weak self] subscriber in + let entityRenderer: LegacyPaintEntityRenderer? = adjustments.flatMap { adjustments in + if let paintingData = adjustments.paintingData, paintingData.hasAnimation { + return LegacyPaintEntityRenderer(postbox: account.postbox, adjustments: adjustments) + } else { + return nil + } + } + + let tempFile = EngineTempBox.shared.tempFile(fileName: "video.mp4") + let uploadInterface = LegacyLiveUploadInterface(context: context) + let signal: SSignal + if let url = asset as? URL, url.absoluteString.hasSuffix(".jpg"), let data = try? Data(contentsOf: url, options: [.mappedRead]), let image = UIImage(data: data), let entityRenderer = entityRenderer { + let durationSignal: SSignal = SSignal(generator: { subscriber in + let disposable = (entityRenderer.duration()).start(next: { duration in + subscriber.putNext(duration) + subscriber.putCompletion() + }) + + return SBlockDisposable(block: { + disposable.dispose() + }) + }) + signal = durationSignal.map(toSignal: { duration -> SSignal in + if let duration = duration as? Double { + return TGMediaVideoConverter.renderUIImage(image, duration: duration, adjustments: adjustments, path: tempFile.path, watcher: nil, entityRenderer: entityRenderer)! + } else { + return SSignal.single(nil) + } + }) + } else if let asset = asset as? AVAsset { + signal = TGMediaVideoConverter.convert(asset, adjustments: adjustments, path: tempFile.path, watcher: uploadInterface, entityRenderer: entityRenderer)! + } else { + signal = SSignal.complete() + } + + let signalDisposable = signal.start(next: { next in + if let result = next as? TGMediaVideoConversionResult { + if let image = result.coverImage, let data = image.jpegData(compressionQuality: 0.7) { + account.postbox.mediaBox.storeResourceData(photoResource.id, data: data) + } + + if let timestamp = videoStartTimestamp { + videoStartTimestamp = max(0.0, min(timestamp, result.duration - 0.05)) + } + + var value = stat() + if stat(result.fileURL.path, &value) == 0 { + if let data = try? Data(contentsOf: result.fileURL) { + let resource: TelegramMediaResource + if let liveUploadData = result.liveUploadData as? LegacyLiveUploadInterfaceResult { + resource = LocalFileMediaResource(fileId: liveUploadData.id) + } else { + resource = LocalFileMediaResource(fileId: Int64.random(in: Int64.min ... Int64.max)) + } + account.postbox.mediaBox.storeResourceData(resource.id, data: data, synchronous: true) + subscriber.putNext(resource) + + EngineTempBox.shared.dispose(tempFile) + } + } + subscriber.putCompletion() + } else if let strongSelf = self, let progress = next as? NSNumber { + Queue.mainQueue().async { + strongSelf.controllerNode.state = strongSelf.controllerNode.state.withAvatarUploadProgress(.value(CGFloat(progress.floatValue * 0.45))) + if let (layout, navigationHeight) = strongSelf.controllerNode.validLayout { + strongSelf.controllerNode.containerLayoutUpdated(layout: layout, navigationHeight: navigationHeight, transition: .immediate, additive: false) + } + } + } + }, error: { _ in + }, completed: nil) + + let disposable = ActionDisposable { + signalDisposable?.dispose() + } + + return ActionDisposable { + disposable.dispose() + } + } + } else { + videoResource = .single(nil) + } + + var dismissStatus: (() -> Void)? + if [.suggest, .fallback, .accept].contains(mode) { + let statusController = OverlayStatusController(theme: self.presentationData.theme, type: .loading(cancelled: { [weak self] in + self?.controllerNode.updateAvatarDisposable.set(nil) + dismissStatus?() + })) + dismissStatus = { [weak statusController] in + statusController?.dismiss() + } + if let topController = self.navigationController?.topViewController as? ViewController { + topController.presentInGlobalOverlay(statusController) + } else if let topController = self.parentController?.topViewController as? ViewController { + topController.presentInGlobalOverlay(statusController) + } else { + self.presentInGlobalOverlay(statusController) + } + } + + let peerId = self.peerId + let isSettings = self.isSettings + let isMyProfile = self.isMyProfile + self.controllerNode.updateAvatarDisposable.set((videoResource + |> mapToSignal { videoResource -> Signal in + if isSettings || isMyProfile { + if case .fallback = mode { + return context.engine.accountData.updateFallbackPhoto(resource: photoResource, videoResource: videoResource, videoStartTimestamp: videoStartTimestamp, markup: markup, mapResourceToAvatarSizes: { resource, representations in + return mapResourceToAvatarSizes(postbox: account.postbox, resource: resource, representations: representations) + }) + } else { + return context.engine.accountData.updateAccountPhoto(resource: photoResource, videoResource: videoResource, videoStartTimestamp: videoStartTimestamp, markup: markup, mapResourceToAvatarSizes: { resource, representations in + return mapResourceToAvatarSizes(postbox: account.postbox, resource: resource, representations: representations) + }) + } + } else if case .custom = mode { + return context.engine.contacts.updateContactPhoto(peerId: peerId, resource: photoResource, videoResource: videoResource, videoStartTimestamp: videoStartTimestamp, markup: markup, mode: .custom, mapResourceToAvatarSizes: { resource, representations in + return mapResourceToAvatarSizes(postbox: account.postbox, resource: resource, representations: representations) + }) + } else if case .suggest = mode { + return context.engine.contacts.updateContactPhoto(peerId: peerId, resource: photoResource, videoResource: videoResource, videoStartTimestamp: videoStartTimestamp, markup: markup, mode: .suggest, mapResourceToAvatarSizes: { resource, representations in + return mapResourceToAvatarSizes(postbox: account.postbox, resource: resource, representations: representations) + }) + } else { + return context.engine.peers.updatePeerPhoto(peerId: peerId, photo: context.engine.peers.uploadedPeerPhoto(resource: photoResource), video: videoResource.flatMap { context.engine.peers.uploadedPeerVideo(resource: $0) |> map(Optional.init) }, videoStartTimestamp: videoStartTimestamp, markup: markup, mapResourceToAvatarSizes: { resource, representations in + return mapResourceToAvatarSizes(postbox: account.postbox, resource: resource, representations: representations) + }) + } + } + |> deliverOnMainQueue).startStrict(next: { [weak self] result in + guard let strongSelf = self else { + return + } + switch result { + case .complete: + strongSelf.controllerNode.state = strongSelf.controllerNode.state.withUpdatingAvatar(nil).withAvatarUploadProgress(nil) + case let .progress(value): + strongSelf.controllerNode.state = strongSelf.controllerNode.state.withAvatarUploadProgress(.value(CGFloat(0.45 + value * 0.55))) + } + if let (layout, navigationHeight) = strongSelf.controllerNode.validLayout { + strongSelf.controllerNode.containerLayoutUpdated(layout: layout, navigationHeight: navigationHeight, transition: .immediate, additive: false) + } + + if case .complete = result { + dismissStatus?() + + let _ = (strongSelf.context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: strongSelf.peerId)) + |> deliverOnMainQueue).startStandalone(next: { [weak self] peer in + if let strongSelf = self, let peer { + switch mode { + case .fallback: + (strongSelf.parentController?.topViewController as? ViewController)?.present(UndoOverlayController(presentationData: strongSelf.presentationData, content: .image(image: image, title: nil, text: strongSelf.presentationData.strings.Privacy_ProfilePhoto_PublicVideoSuccess, round: true, undoText: nil), elevatedLayout: false, animateInAsReplacement: true, action: { _ in return false }), in: .current) + case .custom: + strongSelf.present(UndoOverlayController(presentationData: strongSelf.presentationData, content: .invitedToVoiceChat(context: strongSelf.context, peer: peer, title: nil, text: strongSelf.presentationData.strings.UserInfo_SetCustomPhoto_SuccessVideoText(peer.compactDisplayTitle).string, action: nil, duration: 5), elevatedLayout: false, animateInAsReplacement: true, action: { _ in return false }), in: .current) + + let _ = (strongSelf.context.peerChannelMemberCategoriesContextsManager.profilePhotos(postbox: strongSelf.context.account.postbox, network: strongSelf.context.account.network, peerId: strongSelf.peerId, fetch: peerInfoProfilePhotos(context: strongSelf.context, peerId: strongSelf.peerId)) |> ignoreValues).startStandalone() + case .suggest: + if let navigationController = (strongSelf.navigationController as? NavigationController) { + strongSelf.context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, context: strongSelf.context, chatLocation: .peer(peer), keepStack: .default, completion: { _ in + })) + } + case .accept: + (strongSelf.parentController?.topViewController as? ViewController)?.present(UndoOverlayController(presentationData: strongSelf.presentationData, content: .image(image: image, title: strongSelf.presentationData.strings.Conversation_SuggestedVideoSuccess, text: strongSelf.presentationData.strings.Conversation_SuggestedVideoSuccessText, round: true, undoText: nil), elevatedLayout: false, animateInAsReplacement: true, action: { [weak self] action in + if case .info = action { + self?.parentController?.openSettings() + } + return false + }), in: .current) + default: + break + } + } + }) + } + })) } } diff --git a/submodules/TelegramUI/Components/PeerInfo/PeerInfoVisualMediaPaneNode/Sources/PeerInfoGiftsPaneNode.swift b/submodules/TelegramUI/Components/PeerInfo/PeerInfoVisualMediaPaneNode/Sources/PeerInfoGiftsPaneNode.swift index a781386708..1a9ba0f76a 100644 --- a/submodules/TelegramUI/Components/PeerInfo/PeerInfoVisualMediaPaneNode/Sources/PeerInfoGiftsPaneNode.swift +++ b/submodules/TelegramUI/Components/PeerInfo/PeerInfoVisualMediaPaneNode/Sources/PeerInfoGiftsPaneNode.swift @@ -130,10 +130,12 @@ public final class PeerInfoGiftsPaneNode: ASDisplayNode, PeerInfoPaneNode, UIScr let optionSpacing: CGFloat = 10.0 let sideInset = params.sideInset + 16.0 - let itemsInRow = max(1, min(starsProducts.count, 3)) + let defaultItemsInRow = 3 + let itemsInRow = max(1, min(starsProducts.count, defaultItemsInRow)) + let defaultOptionWidth = (params.size.width - sideInset * 2.0 - optionSpacing * CGFloat(defaultItemsInRow - 1)) / CGFloat(defaultItemsInRow) let optionWidth = (params.size.width - sideInset * 2.0 - optionSpacing * CGFloat(itemsInRow - 1)) / CGFloat(itemsInRow) - let starsOptionSize = CGSize(width: optionWidth, height: optionWidth) + let starsOptionSize = CGSize(width: optionWidth, height: defaultOptionWidth) let visibleBounds = self.scrollNode.bounds.insetBy(dx: 0.0, dy: -10.0) @@ -176,12 +178,12 @@ public final class PeerInfoGiftsPaneNode: ASDisplayNode, PeerInfoPaneNode, UIScr switch product.gift { case let .generic(gift): if let availability = gift.availability { - ribbonText = params.presentationData.strings.PeerInfo_Gifts_OneOf(compactNumericCountString(Int(availability.total))).string + ribbonText = params.presentationData.strings.PeerInfo_Gifts_OneOf(compactNumericCountString(Int(availability.total), decimalSeparator: params.presentationData.dateTimeFormat.decimalSeparator)).string } else { ribbonText = nil } case let .unique(gift): - ribbonText = params.presentationData.strings.PeerInfo_Gifts_OneOf(compactNumericCountString(Int(gift.availability.total))).string + ribbonText = params.presentationData.strings.PeerInfo_Gifts_OneOf(compactNumericCountString(Int(gift.availability.total), decimalSeparator: params.presentationData.dateTimeFormat.decimalSeparator)).string for attribute in gift.attributes { if case let .backdrop(_, innerColor, outerColor, _, _, _) = attribute { ribbonColor = .custom(outerColor, innerColor) @@ -247,6 +249,15 @@ public final class PeerInfoGiftsPaneNode: ASDisplayNode, PeerInfoPaneNode, UIScr return .never() } return self.profileGifts.upgradeStarGift(formId: formId, messageId: messageId, keepOriginalInfo: keepOriginalInfo) + }, + shareStory: { [weak self] in + guard let self, case let .unique(uniqueGift) = product.gift, let parentController = self.parentController else { + return + } + Queue.mainQueue().after(0.15) { + let controller = self.context.sharedContext.makeStorySharingScreen(context: self.context, subject: .gift(uniqueGift), parentController: parentController) + parentController.push(controller) + } } ) self.parentController?.push(controller) diff --git a/submodules/TelegramUI/Components/PeerSelectionController/Sources/PeerSelectionControllerNode.swift b/submodules/TelegramUI/Components/PeerSelectionController/Sources/PeerSelectionControllerNode.swift index 6f189e3089..e04ae5ff85 100644 --- a/submodules/TelegramUI/Components/PeerSelectionController/Sources/PeerSelectionControllerNode.swift +++ b/submodules/TelegramUI/Components/PeerSelectionController/Sources/PeerSelectionControllerNode.swift @@ -1274,6 +1274,9 @@ final class PeerSelectionControllerNode: ASDisplayNode { if updated { strongSelf.textInputPanelNode?.updateSendButtonEnabled(count > 0, animated: true) strongSelf.requestDeactivateSearch?() + if let (layout, navigationBarHeight, actualNavigationBarHeight) = strongSelf.containerLayout { + strongSelf.containerLayoutUpdated(layout, navigationBarHeight: navigationBarHeight, actualNavigationBarHeight: actualNavigationBarHeight, transition: .immediate) + } } else if let requestOpenPeerFromSearch = strongSelf.requestOpenPeerFromSearch { requestOpenPeerFromSearch(peer, threadId) } diff --git a/submodules/TelegramUI/Components/Settings/PeerNameColorScreen/Sources/ChannelAppearanceScreen.swift b/submodules/TelegramUI/Components/Settings/PeerNameColorScreen/Sources/ChannelAppearanceScreen.swift index 412be719b1..39d7b106c6 100644 --- a/submodules/TelegramUI/Components/Settings/PeerNameColorScreen/Sources/ChannelAppearanceScreen.swift +++ b/submodules/TelegramUI/Components/Settings/PeerNameColorScreen/Sources/ChannelAppearanceScreen.swift @@ -727,7 +727,7 @@ final class ChannelAppearanceScreenComponent: Component { } if let result { - self.updatedPeerStatus = PeerEmojiStatus(fileId: result.fileId.id, expirationDate: timestamp) + self.updatedPeerStatus = PeerEmojiStatus(content: .emoji(fileId: result.fileId.id), expirationDate: timestamp) } else { self.updatedPeerStatus = .some(nil) } diff --git a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerViewSendMessage.swift b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerViewSendMessage.swift index a175b7bbcb..b491063a4d 100644 --- a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerViewSendMessage.swift +++ b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerViewSendMessage.swift @@ -3342,14 +3342,14 @@ final class StoryItemSetContainerSendMessage { useGesturePosition = true let action = { [weak self, weak view, weak controller] in let _ = ((context.engine.messages.getMessagesLoadIfNecessary([messageId], strategy: .cloud(skipLocal: true)) - |> mapToSignal { result -> Signal in + |> mapToSignal { result -> Signal in if case let .result(messages) = result { return .single(messages.first) } else { return .complete() } }) - |> deliverOnMainQueue).startStandalone(next: { [weak self, weak view, weak controller] message in + |> deliverOnMainQueue).startStandalone(next: { [weak self, weak view, weak controller] message in guard let self, let view else { return } @@ -3365,7 +3365,7 @@ final class StoryItemSetContainerSendMessage { switch error { case .privateChannel: let _ = (context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: messageId.peerId)) - |> deliverOnMainQueue).startStandalone(next: { [weak self, weak view] peer in + |> deliverOnMainQueue).startStandalone(next: { [weak self, weak view] peer in guard let self, let view else { return } @@ -3416,6 +3416,31 @@ final class StoryItemSetContainerSendMessage { })) case .weather: return + case let .starGift(_, slug): + useGesturePosition = true + let action = { + let _ = openUserGeneratedUrl(context: component.context, peerId: nil, url: "https://t.me/nft/\(slug)", concealed: false, skipUrlAuth: false, skipConcealedAlert: false, forceDark: true, present: { [weak controller] c in + controller?.present(c, in: .window(.root)) + }, openResolved: { [weak self, weak view] resolved in + guard let self, let view else { + return + } + self.openResolved(view: view, result: resolved, forceExternal: false, concealed: false) + }, alertDisplayUpdated: { [weak self, weak view] alertController in + guard let self, let view else { + return + } + self.statusController = alertController + view.updateIsProgressPaused() + }) + } + if immediate { + action() + return + } + actions.append(ContextMenuAction(content: .textWithIcon(title: updatedPresentationData.initial.strings.Story_ViewGift, icon: generateTintedImage(image: UIImage(bundleImageName: "Settings/TextArrowRight"), color: .white)), action: { + action() + })) } self.selectedMediaArea = mediaArea diff --git a/submodules/TelegramUI/Components/WallpaperPreviewMedia/Sources/WallpaperPreviewMedia.swift b/submodules/TelegramUI/Components/WallpaperPreviewMedia/Sources/WallpaperPreviewMedia.swift index 7db183dbf5..647bd5f3e5 100644 --- a/submodules/TelegramUI/Components/WallpaperPreviewMedia/Sources/WallpaperPreviewMedia.swift +++ b/submodules/TelegramUI/Components/WallpaperPreviewMedia/Sources/WallpaperPreviewMedia.swift @@ -72,3 +72,39 @@ public extension WallpaperPreviewMedia { } } } + +public final class UniqueGiftPreviewMedia: Media { + public var id: MediaId? { + return nil + } + public let peerIds: [PeerId] = [] + + public let content: StarGift.UniqueGift? + + public init(content: StarGift.UniqueGift) { + self.content = content + } + + public init(decoder: PostboxDecoder) { + self.content = nil + } + + public func encode(_ encoder: PostboxEncoder) { + } + + public func isEqual(to other: Media) -> Bool { + guard let other = other as? UniqueGiftPreviewMedia else { + return false + } + + if self.content != other.content { + return false + } + + return true + } + + public func isSemanticallyEqual(to other: Media) -> Bool { + return self.isEqual(to: other) + } +} diff --git a/submodules/TelegramUI/Images.xcassets/Premium/Collectible/Badge.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Premium/Collectible/Badge.imageset/Contents.json new file mode 100644 index 0000000000..f6de9f866a --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Premium/Collectible/Badge.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "nft_30.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/submodules/TelegramUI/Images.xcassets/Premium/Collectible/Badge.imageset/nft_30.pdf b/submodules/TelegramUI/Images.xcassets/Premium/Collectible/Badge.imageset/nft_30.pdf new file mode 100644 index 0000000000..a0dc08c6f2 Binary files /dev/null and b/submodules/TelegramUI/Images.xcassets/Premium/Collectible/Badge.imageset/nft_30.pdf differ diff --git a/submodules/TelegramUI/Images.xcassets/Premium/Collectible/Proof.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Premium/Collectible/Proof.imageset/Contents.json new file mode 100644 index 0000000000..9085cfbf7f --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Premium/Collectible/Proof.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "badge_30.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/submodules/TelegramUI/Images.xcassets/Premium/Collectible/Proof.imageset/badge_30.pdf b/submodules/TelegramUI/Images.xcassets/Premium/Collectible/Proof.imageset/badge_30.pdf new file mode 100644 index 0000000000..54ce7132d8 Binary files /dev/null and b/submodules/TelegramUI/Images.xcassets/Premium/Collectible/Proof.imageset/badge_30.pdf differ diff --git a/submodules/TelegramUI/Images.xcassets/Premium/Collectible/Share.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Premium/Collectible/Share.imageset/Contents.json new file mode 100644 index 0000000000..20dff6d937 --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Premium/Collectible/Share.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "share_30.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/submodules/TelegramUI/Images.xcassets/Premium/Collectible/Share.imageset/share_30.pdf b/submodules/TelegramUI/Images.xcassets/Premium/Collectible/Share.imageset/share_30.pdf new file mode 100644 index 0000000000..179fe9efeb Binary files /dev/null and b/submodules/TelegramUI/Images.xcassets/Premium/Collectible/Share.imageset/share_30.pdf differ diff --git a/submodules/TelegramUI/Images.xcassets/Premium/Collectible/Transfer.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Premium/Collectible/Transfer.imageset/Contents.json new file mode 100644 index 0000000000..cb6acaef48 --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Premium/Collectible/Transfer.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "transfer_30.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/submodules/TelegramUI/Images.xcassets/Premium/Collectible/Transfer.imageset/transfer_30.pdf b/submodules/TelegramUI/Images.xcassets/Premium/Collectible/Transfer.imageset/transfer_30.pdf new file mode 100644 index 0000000000..46e40d11a4 Binary files /dev/null and b/submodules/TelegramUI/Images.xcassets/Premium/Collectible/Transfer.imageset/transfer_30.pdf differ diff --git a/submodules/TelegramUI/Images.xcassets/Premium/Collectible/Unwear.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Premium/Collectible/Unwear.imageset/Contents.json new file mode 100644 index 0000000000..bb86e1900a --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Premium/Collectible/Unwear.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "wearoff_30.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/submodules/TelegramUI/Images.xcassets/Premium/Collectible/Unwear.imageset/wearoff_30.pdf b/submodules/TelegramUI/Images.xcassets/Premium/Collectible/Unwear.imageset/wearoff_30.pdf new file mode 100644 index 0000000000..f40382dabd Binary files /dev/null and b/submodules/TelegramUI/Images.xcassets/Premium/Collectible/Unwear.imageset/wearoff_30.pdf differ diff --git a/submodules/TelegramUI/Images.xcassets/Premium/Collectible/Wear.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Premium/Collectible/Wear.imageset/Contents.json new file mode 100644 index 0000000000..c3eef99c2e --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Premium/Collectible/Wear.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "wear_30.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/submodules/TelegramUI/Images.xcassets/Premium/Collectible/Wear.imageset/wear_30.pdf b/submodules/TelegramUI/Images.xcassets/Premium/Collectible/Wear.imageset/wear_30.pdf new file mode 100644 index 0000000000..ec2544a255 Binary files /dev/null and b/submodules/TelegramUI/Images.xcassets/Premium/Collectible/Wear.imageset/wear_30.pdf differ diff --git a/submodules/TelegramUI/Resources/Animations/GiftUpgrade.json b/submodules/TelegramUI/Resources/Animations/GiftUpgrade.json index 2ec9f56482..44aa6bc3bf 100644 --- a/submodules/TelegramUI/Resources/Animations/GiftUpgrade.json +++ b/submodules/TelegramUI/Resources/Animations/GiftUpgrade.json @@ -1 +1 @@ -{"v":"5.12.1","fr":60,"ip":0,"op":91,"w":90,"h":90,"nm":"upgrade","ddd":0,"assets":[{"id":"comp_0","nm":"arrows","fr":60,"layers":[{"ddd":0,"ind":1,"ty":4,"nm":"Vector 203","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"s":true,"x":{"a":0,"k":45,"ix":3},"y":{"a":1,"k":[{"i":{"x":[0.34],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":10,"s":[32.25]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.66],"y":[0]},"t":20,"s":[27.25]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":30,"s":[34.25]},{"t":40,"s":[32.25]}],"ix":4}},"a":{"a":0,"k":[0,0,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0]],"v":[[4,1.75],[0,-1.75],[-4,1.75]],"c":false},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[0,0,0,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":1.33,"ix":5},"lc":2,"lj":2,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[300,300],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Vector 203","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":1800,"st":0,"ct":1,"bm":0},{"ddd":0,"ind":2,"ty":4,"nm":"Vector 202","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"s":true,"x":{"a":0,"k":45,"ix":3},"y":{"a":1,"k":[{"i":{"x":[0.34],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":5,"s":[44.25]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.66],"y":[0]},"t":15,"s":[39.25]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":25,"s":[46.25]},{"t":35,"s":[44.25]}],"ix":4}},"a":{"a":0,"k":[0,0,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0]],"v":[[4,1.75],[0,-1.75],[-4,1.75]],"c":false},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[0,0,0,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":1.33,"ix":5},"lc":2,"lj":2,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[300,300],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Vector 202","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":1800,"st":0,"ct":1,"bm":0},{"ddd":0,"ind":3,"ty":4,"nm":"Vector 201","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"s":true,"x":{"a":0,"k":45,"ix":3},"y":{"a":1,"k":[{"i":{"x":[0.34],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":0,"s":[56.25]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.66],"y":[0]},"t":10,"s":[51.25]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":20,"s":[58.25]},{"t":30,"s":[56.25]}],"ix":4}},"a":{"a":0,"k":[0,0,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0]],"v":[[4,1.75],[0,-1.75],[-4,1.75]],"c":false},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[0,0,0,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":1.33,"ix":5},"lc":2,"lj":2,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[300,300],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Vector 201","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":1800,"st":0,"ct":1,"bm":0}]}],"layers":[{"ddd":0,"ind":1,"ty":0,"nm":"arrows","td":1,"refId":"comp_0","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[45,45,0],"ix":2,"l":2},"a":{"a":0,"k":[45,45,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"w":90,"h":90,"ip":0,"op":1800,"st":0,"bm":0},{"ddd":0,"ind":2,"ty":4,"nm":"Rectangle 329","tt":2,"tp":1,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"s":true,"x":{"a":0,"k":45,"ix":3},"y":{"a":1,"k":[{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":0,"s":[45]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":20,"s":[40.5]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":30,"s":[47.5]},{"t":40,"s":[45]}],"ix":4}},"a":{"a":0,"k":[0,0,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ty":"rc","d":1,"s":{"a":1,"k":[{"i":{"x":[0.667,0.667],"y":[1,1]},"o":{"x":[0.333,0.333],"y":[0,0]},"t":0,"s":[20,20]},{"i":{"x":[0.667,0.667],"y":[1,1]},"o":{"x":[0.333,0.333],"y":[0,0]},"t":20,"s":[19,22]},{"i":{"x":[0.667,0.667],"y":[1,1]},"o":{"x":[0.333,0.333],"y":[0,0]},"t":30,"s":[21,19]},{"t":40,"s":[20,20]}],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"r":{"a":0,"k":30,"ix":4},"nm":"Rectangle Path 1","mn":"ADBE Vector Shape - Rect","hd":false},{"ty":"fl","c":{"a":0,"k":[1,1,1,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[300,300],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Rectangle 329","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":1800,"st":0,"ct":1,"bm":0}],"markers":[],"props":{}} \ No newline at end of file +{"v":"5.12.1","fr":60,"ip":0,"op":30,"w":90,"h":90,"nm":"upgrade 2","ddd":0,"assets":[{"id":"comp_0","nm":"arrows 2","fr":60,"layers":[{"ddd":0,"ind":1,"ty":3,"nm":"Vector 209","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"s":true,"x":{"a":0,"k":45,"ix":3},"y":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":0,"s":[15]},{"t":30,"s":[0.75]}],"ix":4}},"a":{"a":0,"k":[0,0,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"ip":0,"op":1800,"st":0,"ct":1,"bm":0},{"ddd":0,"ind":3,"ty":4,"nm":"Vector 208","parent":1,"sr":1,"ks":{"o":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":6,"s":[100]},{"t":26,"s":[0]}],"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"s":true,"x":{"a":0,"k":0,"ix":3},"y":{"a":0,"k":14.5,"ix":4}},"a":{"a":0,"k":[0,0,0],"ix":1,"l":2},"s":{"a":1,"k":[{"i":{"x":[0.833,0.833,0.833],"y":[0.833,0.833,0.833]},"o":{"x":[0.167,0.167,0.167],"y":[0.167,0.167,0.167]},"t":6,"s":[100,100,100]},{"t":26,"s":[50,50,100]}],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0]],"v":[[4,1.75],[0,-1.75],[-4,1.75]],"c":false},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[0,0,0,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":1.33,"ix":5},"lc":2,"lj":2,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[300,300],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Vector 202","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":1800,"st":0,"ct":1,"bm":0},{"ddd":0,"ind":5,"ty":4,"nm":"Vector 202","parent":1,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"s":true,"x":{"a":0,"k":0,"ix":3},"y":{"a":0,"k":43.5,"ix":4}},"a":{"a":0,"k":[0,0,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0]],"v":[[4,1.75],[0,-1.75],[-4,1.75]],"c":false},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[0,0,0,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":1.33,"ix":5},"lc":2,"lj":2,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[300,300],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Vector 202","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":1800,"st":0,"ct":1,"bm":0},{"ddd":0,"ind":6,"ty":4,"nm":"Vector 207","parent":1,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"s":true,"x":{"a":0,"k":0,"ix":3},"y":{"a":0,"k":29,"ix":4}},"a":{"a":0,"k":[0,0,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0]],"v":[[4,1.75],[0,-1.75],[-4,1.75]],"c":false},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[0,0,0,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":1.33,"ix":5},"lc":2,"lj":2,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[300,300],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Vector 201","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":1800,"st":0,"ct":1,"bm":0},{"ddd":0,"ind":7,"ty":4,"nm":"Vector 204","parent":1,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"s":true,"x":{"a":0,"k":0,"ix":3},"y":{"a":0,"k":87,"ix":4}},"a":{"a":0,"k":[0,0,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0]],"v":[[4,1.75],[0,-1.75],[-4,1.75]],"c":false},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[0,0,0,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":1.33,"ix":5},"lc":2,"lj":2,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[300,300],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Vector 201","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":1800,"st":0,"ct":1,"bm":0},{"ddd":0,"ind":8,"ty":4,"nm":"Vector 201","parent":1,"sr":1,"ks":{"o":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":6,"s":[0]},{"t":26,"s":[100]}],"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"s":true,"x":{"a":0,"k":0,"ix":3},"y":{"a":0,"k":58,"ix":4}},"a":{"a":0,"k":[0,0,0],"ix":1,"l":2},"s":{"a":1,"k":[{"i":{"x":[0.833,0.833,0.833],"y":[0.833,0.833,0.833]},"o":{"x":[0.167,0.167,0.167],"y":[0.167,0.167,0.167]},"t":6,"s":[30,30,100]},{"t":26,"s":[100,100,100]}],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0]],"v":[[4,1.75],[0,-1.75],[-4,1.75]],"c":false},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[0,0,0,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":1.33,"ix":5},"lc":2,"lj":2,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[300,300],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Vector 201","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":1800,"st":0,"ct":1,"bm":0}]}],"layers":[{"ddd":0,"ind":1,"ty":0,"nm":"arrows 2","td":1,"refId":"comp_0","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[45,45,0],"ix":2,"l":2},"a":{"a":0,"k":[45,45,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"w":90,"h":90,"ip":0,"op":31,"st":0,"bm":0},{"ddd":0,"ind":2,"ty":4,"nm":"Rectangle 329","tt":2,"tp":1,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"s":true,"x":{"a":0,"k":45,"ix":3},"y":{"a":0,"k":45,"ix":4}},"a":{"a":0,"k":[0,0,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ty":"rc","d":1,"s":{"a":0,"k":[20,20],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"r":{"a":0,"k":30,"ix":4},"nm":"Rectangle Path 1","mn":"ADBE Vector Shape - Rect","hd":false},{"ty":"fl","c":{"a":0,"k":[1,1,1,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[300,300],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Rectangle 329","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":1800,"st":0,"ct":1,"bm":0}],"markers":[],"props":{}} \ No newline at end of file diff --git a/submodules/TelegramUI/Sources/Chat/ChatControllerOpenMessageContextMenu.swift b/submodules/TelegramUI/Sources/Chat/ChatControllerOpenMessageContextMenu.swift index fa76f3a702..f869e5fce7 100644 --- a/submodules/TelegramUI/Sources/Chat/ChatControllerOpenMessageContextMenu.swift +++ b/submodules/TelegramUI/Sources/Chat/ChatControllerOpenMessageContextMenu.swift @@ -301,8 +301,20 @@ extension ChatControllerImpl { self.canReadHistory.set(false) + var hideReactionPanelTail = false + for media in message.media { + if let action = media as? TelegramMediaAction { + switch action.action { + case .phoneCall: + break + default: + hideReactionPanelTail = true + } + } + } + let isSecret = self.presentationInterfaceState.copyProtectionEnabled || self.chatLocation.peerId?.namespace == Namespaces.Peer.SecretChat - let controller = ContextController(presentationData: self.presentationData, source: source, items: actionsSignal, recognizer: recognizer, gesture: gesture, disableScreenshots: isSecret) + let controller = ContextController(presentationData: self.presentationData, source: source, items: actionsSignal, recognizer: recognizer, gesture: gesture, disableScreenshots: isSecret, hideReactionPanelTail: hideReactionPanelTail) controller.dismissed = { [weak self] in self?.canReadHistory.set(true) } diff --git a/submodules/TelegramUI/Sources/Chat/ChatControllerOpenStorySharing.swift b/submodules/TelegramUI/Sources/Chat/ChatControllerOpenStorySharing.swift deleted file mode 100644 index 91548dcfb2..0000000000 --- a/submodules/TelegramUI/Sources/Chat/ChatControllerOpenStorySharing.swift +++ /dev/null @@ -1,97 +0,0 @@ -import Foundation -import UIKit -import Postbox -import SwiftSignalKit -import Display -import AsyncDisplayKit -import TelegramCore -import SafariServices -import MobileCoreServices -import Intents -import LegacyComponents -import TelegramPresentationData -import TelegramUIPreferences -import DeviceAccess -import TextFormat -import TelegramBaseController -import AccountContext -import TelegramStringFormatting -import PresentationDataUtils -import UndoUI -import PeerInfoUI -import AppBundle -import LocalizedPeerData -import ChatInterfaceState -import ChatControllerInteraction -import StoryContainerScreen -import SaveToCameraRoll -import MediaEditorScreen - -extension ChatControllerImpl { - func openStorySharing(messages: [Message]) { - let context = self.context - let subject: Signal = .single(.message(messages.map { $0.id })) - - let externalState = MediaEditorTransitionOutExternalState( - storyTarget: nil, - isForcedTarget: false, - isPeerArchived: false, - transitionOut: nil - ) - - let controller = MediaEditorScreenImpl( - context: context, - mode: .storyEditor, - subject: subject, - transitionIn: nil, - transitionOut: { _, _ in - return nil - }, - completion: { [weak self] result, commit in - guard let self else { - return - } - let targetPeerId: EnginePeer.Id - let target: Stories.PendingTarget - if let sendAsPeerId = result.options.sendAsPeerId { - target = .peer(sendAsPeerId) - targetPeerId = sendAsPeerId - } else { - target = .myStories - targetPeerId = self.context.account.peerId - } - externalState.storyTarget = target - - if let rootController = context.sharedContext.mainWindow?.viewController as? TelegramRootControllerInterface { - rootController.proceedWithStoryUpload(target: target, result: result, existingMedia: nil, forwardInfo: nil, externalState: externalState, commit: commit) - } - - let _ = (self.context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: targetPeerId)) - |> deliverOnMainQueue).start(next: { [weak self] peer in - guard let self, let peer else { - return - } - let text: String - if case .channel = peer { - text = self.presentationData.strings.Story_MessageReposted_Channel(peer.compactDisplayTitle).string - } else { - text = self.presentationData.strings.Story_MessageReposted_Personal - } - Queue.mainQueue().after(0.25) { - self.present(UndoOverlayController( - presentationData: self.presentationData, - content: .forward(savedMessages: false, text: text), - elevatedLayout: false, - action: { _ in return false } - ), in: .current) - - Queue.mainQueue().after(0.1) { - self.chatDisplayNode.hapticFeedback.success() - } - } - }) - } - ) - self.push(controller) - } -} diff --git a/submodules/TelegramUI/Sources/ChatController.swift b/submodules/TelegramUI/Sources/ChatController.swift index 125f2760ed..9b69437200 100644 --- a/submodules/TelegramUI/Sources/ChatController.swift +++ b/submodules/TelegramUI/Sources/ChatController.swift @@ -1198,7 +1198,14 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G strongSelf.push(controller) return true case .starGift, .starGiftUnique: - let controller = strongSelf.context.sharedContext.makeGiftViewScreen(context: strongSelf.context, message: EngineMessage(message)) + let controller = strongSelf.context.sharedContext.makeGiftViewScreen(context: strongSelf.context, message: EngineMessage(message), shareStory: { [weak self] in + if let self, case let .starGiftUnique(gift, _, _, _, _, _, _) = action.action, case let .unique(uniqueGift) = gift { + Queue.mainQueue().after(0.15) { + let controller = self.context.sharedContext.makeStorySharingScreen(context: self.context, subject: .gift(uniqueGift), parentController: self) + self.push(controller) + } + } + }) strongSelf.push(controller) return true case .giftStars: @@ -3206,7 +3213,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G } strongSelf.selectPollOptionFeedback?.success() - strongSelf.chatDisplayNode.animateQuizCorrectOptionSelected() + strongSelf.chatDisplayNode.playConfettiAnimation() } else { var found = false strongSelf.chatDisplayNode.historyNode.forEachVisibleItemNode { itemNode in @@ -3608,7 +3615,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G }, displayDiceTooltip: { [weak self] dice in self?.displayDiceTooltip(dice: dice) }, animateDiceSuccess: { [weak self] haptic, confetti in - guard let strongSelf = self else { + guard let strongSelf = self, strongSelf.isNodeLoaded else { return } if strongSelf.selectPollOptionFeedback == nil { @@ -3618,7 +3625,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G strongSelf.selectPollOptionFeedback?.success() } if confetti { - strongSelf.chatDisplayNode.animateQuizCorrectOptionSelected() + strongSelf.chatDisplayNode.playConfettiAnimation() } }, displayPremiumStickerTooltip: { [weak self] file, message in self?.displayPremiumStickerTooltip(file: file, message: message) diff --git a/submodules/TelegramUI/Sources/ChatControllerNode.swift b/submodules/TelegramUI/Sources/ChatControllerNode.swift index 531a03d4aa..a2cdfdb1c9 100644 --- a/submodules/TelegramUI/Sources/ChatControllerNode.swift +++ b/submodules/TelegramUI/Sources/ChatControllerNode.swift @@ -4297,7 +4297,17 @@ class ChatControllerNode: ASDisplayNode, ASScrollViewDelegate { } } - func animateQuizCorrectOptionSelected() { + private var previousConfettiAnimationTimestamp: Double? + func playConfettiAnimation() { + guard self.view.bounds.width > 0.0 else { + return + } + let currentTime = CACurrentMediaTime() + if let previousConfettiAnimationTimestamp = self.previousConfettiAnimationTimestamp, abs(currentTime - previousConfettiAnimationTimestamp) < 0.1 { + return + } + self.previousConfettiAnimationTimestamp = currentTime + self.view.insertSubview(ConfettiView(frame: self.view.bounds), aboveSubview: self.historyNode.view) } diff --git a/submodules/TelegramUI/Sources/ChatControllerOpenMessageReactionContextMenu.swift b/submodules/TelegramUI/Sources/ChatControllerOpenMessageReactionContextMenu.swift index ee04acb0df..3bf0cf4150 100644 --- a/submodules/TelegramUI/Sources/ChatControllerOpenMessageReactionContextMenu.swift +++ b/submodules/TelegramUI/Sources/ChatControllerOpenMessageReactionContextMenu.swift @@ -471,7 +471,7 @@ extension ChatControllerImpl { } if isBecomingTop { - self.chatDisplayNode.animateQuizCorrectOptionSelected() + self.chatDisplayNode.playConfettiAnimation() } if let itemNode, let targetView = itemNode.targetReactionView(value: .stars), self.context.sharedContext.energyUsageSettings.fullTranslucency { diff --git a/submodules/TelegramUI/Sources/ChatControllerOpenMessageShareMenu.swift b/submodules/TelegramUI/Sources/ChatControllerOpenMessageShareMenu.swift index 619940c612..e284e26d67 100644 --- a/submodules/TelegramUI/Sources/ChatControllerOpenMessageShareMenu.swift +++ b/submodules/TelegramUI/Sources/ChatControllerOpenMessageShareMenu.swift @@ -134,7 +134,8 @@ extension ChatControllerImpl { return } Queue.mainQueue().after(0.15) { - self.openStorySharing(messages: messages) + let controller = self.context.sharedContext.makeStorySharingScreen(context: self.context, subject: .messages(messages), parentController: self) + self.push(controller) } } } diff --git a/submodules/TelegramUI/Sources/ChatHistoryListNode.swift b/submodules/TelegramUI/Sources/ChatHistoryListNode.swift index 6021c78c6e..01e1ae29e2 100644 --- a/submodules/TelegramUI/Sources/ChatHistoryListNode.swift +++ b/submodules/TelegramUI/Sources/ChatHistoryListNode.swift @@ -2905,12 +2905,6 @@ public final class ChatHistoryListNodeImpl: ListView, ChatHistoryNode, ChatHisto for entry in historyView.filteredEntries { switch entry { case let .MessageEntry(message, _, _, _, _, _): - var hasAction = false - for media in message.media { - if let _ = media as? TelegramMediaAction { - hasAction = true - } - } if let _ = message.inlineBotAttribute { if let visibleBusinessBotMessageIdValue = visibleBusinessBotMessageId { if visibleBusinessBotMessageIdValue < message.id { @@ -2920,22 +2914,14 @@ public final class ChatHistoryListNodeImpl: ListView, ChatHistoryNode, ChatHisto visibleBusinessBotMessageId = message.id } } - if !hasAction { - switch message.id.peerId.namespace { - case Namespaces.Peer.CloudGroup, Namespaces.Peer.CloudChannel: - messageIdsWithPossibleReactions.append(message.id) - default: - break - } + switch message.id.peerId.namespace { + case Namespaces.Peer.CloudGroup, Namespaces.Peer.CloudChannel: + messageIdsWithPossibleReactions.append(message.id) + default: + break } case let .MessageGroupEntry(_, messages, _): for (message, _, _, _, _) in messages { - var hasAction = false - for media in message.media { - if let _ = media as? TelegramMediaAction { - hasAction = true - } - } if let _ = message.inlineBotAttribute { if let visibleBusinessBotMessageIdValue = visibleBusinessBotMessageId { if visibleBusinessBotMessageIdValue < message.id { @@ -2945,13 +2931,11 @@ public final class ChatHistoryListNodeImpl: ListView, ChatHistoryNode, ChatHisto visibleBusinessBotMessageId = message.id } } - if !hasAction { - switch message.id.peerId.namespace { - case Namespaces.Peer.CloudGroup, Namespaces.Peer.CloudChannel: - messageIdsWithPossibleReactions.append(message.id) - default: - break - } + switch message.id.peerId.namespace { + case Namespaces.Peer.CloudGroup, Namespaces.Peer.CloudChannel: + messageIdsWithPossibleReactions.append(message.id) + default: + break } } default: diff --git a/submodules/TelegramUI/Sources/ChatReportPeerTitlePanelNode.swift b/submodules/TelegramUI/Sources/ChatReportPeerTitlePanelNode.swift index 5f758642c5..615db1d433 100644 --- a/submodules/TelegramUI/Sources/ChatReportPeerTitlePanelNode.swift +++ b/submodules/TelegramUI/Sources/ChatReportPeerTitlePanelNode.swift @@ -17,7 +17,7 @@ import AccountContext private enum ChatReportPeerTitleButton: Equatable { case block - case addContact(String?) + case addContact(String?, Bool) case shareMyPhoneNumber case reportSpam case reportUserSpam @@ -30,11 +30,15 @@ private enum ChatReportPeerTitleButton: Equatable { switch self { case .block: return strings.Conversation_BlockUser - case let .addContact(name): + case let .addContact(name, long): if let name = name { return strings.Conversation_AddNameToContacts(name).string } else { - return strings.Conversation_AddToContacts + if long { + return strings.Conversation_AddToContactsLong + } else { + return strings.Conversation_AddToContacts + } } case .shareMyPhoneNumber: return strings.Conversation_ShareMyPhoneNumber @@ -76,9 +80,9 @@ private func peerButtons(_ state: ChatPresentationInterfaceState) -> [ChatReport } } if buttons.isEmpty, let phone = peer.phone, !phone.isEmpty { - buttons.append(.addContact(EnginePeer(peer).compactDisplayTitle)) + buttons.append(.addContact(EnginePeer(peer).compactDisplayTitle, buttons.isEmpty)) } else { - buttons.append(.addContact(nil)) + buttons.append(.addContact(nil, buttons.isEmpty)) } } else { if peerStatusSettings.contains(.canBlock) || peerStatusSettings.contains(.canReport) { diff --git a/submodules/TelegramUI/Sources/ChatVerifiedPeerTitlePanelNode.swift b/submodules/TelegramUI/Sources/ChatVerifiedPeerTitlePanelNode.swift index 7f2935dbe8..80ec66650f 100644 --- a/submodules/TelegramUI/Sources/ChatVerifiedPeerTitlePanelNode.swift +++ b/submodules/TelegramUI/Sources/ChatVerifiedPeerTitlePanelNode.swift @@ -84,7 +84,7 @@ final class ChatVerifiedPeerTitlePanelNode: ChatTitleAccessoryPanelNode { let _ = ApplicationSpecificNotice.setDisplayedPeerVerification(accountManager: self.context.sharedContext.accountManager, peerId: peer.id).start() } - let emojiStatus = PeerEmojiStatus(fileId: verification.iconFileId, expirationDate: nil) + let emojiStatus = PeerEmojiStatus(content: .emoji(fileId: verification.iconFileId), expirationDate: nil) let emojiStatusTextNode = self.emojiStatusTextNode let description = verification.description diff --git a/submodules/TelegramUI/Sources/OpenResolvedUrl.swift b/submodules/TelegramUI/Sources/OpenResolvedUrl.swift index 033d37cbfc..37d9bd76f3 100644 --- a/submodules/TelegramUI/Sources/OpenResolvedUrl.swift +++ b/submodules/TelegramUI/Sources/OpenResolvedUrl.swift @@ -1211,6 +1211,13 @@ func openResolvedUrlImpl( present(textAlertController(context: context, updatedPresentationData: updatedPresentationData, title: nil, text: presentationData.strings.Login_UnknownError, actions: [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_OK, action: {})]), nil) } }) + case let .collectible(gift): + if let gift { + let controller = context.sharedContext.makeGiftViewScreen(context: context, gift: gift, shareStory: nil) + navigationController?.pushViewController(controller) + } else { + present(textAlertController(context: context, updatedPresentationData: updatedPresentationData, title: nil, text: presentationData.strings.Login_UnknownError, actions: [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_OK, action: {})]), nil) + } case let .messageLink(link): if let link { if let navigationController = navigationController { diff --git a/submodules/TelegramUI/Sources/OpenUrl.swift b/submodules/TelegramUI/Sources/OpenUrl.swift index ed63ab80c3..71230c9e5a 100644 --- a/submodules/TelegramUI/Sources/OpenUrl.swift +++ b/submodules/TelegramUI/Sources/OpenUrl.swift @@ -655,6 +655,22 @@ func openExternalUrlImpl(context: AccountContext, urlContext: OpenURLContext, ur convertedUrl = "https://t.me/addtheme/\(parameter)" } } + } else if parsedUrl.host == "nft" { + if let components = URLComponents(string: "/?" + query) { + var slug: String? + if let queryItems = components.queryItems { + for queryItem in queryItems { + if let value = queryItem.value { + if queryItem.name == "slug" { + slug = value + } + } + } + } + if let slug { + convertedUrl = "https://t.me/nft/\(slug)" + } + } } else if parsedUrl.host == "privatepost" { if let components = URLComponents(string: "/?" + query) { var channelId: Int64? diff --git a/submodules/TelegramUI/Sources/SharedAccountContext.swift b/submodules/TelegramUI/Sources/SharedAccountContext.swift index a79b34bd93..b16eeaf750 100644 --- a/submodules/TelegramUI/Sources/SharedAccountContext.swift +++ b/submodules/TelegramUI/Sources/SharedAccountContext.swift @@ -76,6 +76,7 @@ import StarsIntroScreen import ContentReportScreen import AffiliateProgramSetupScreen import GalleryUI +import ShareController private final class AccountUserInterfaceInUseContext { let subscribers = Bag<(Bool) -> Void>() @@ -2922,8 +2923,81 @@ public final class SharedAccountContextImpl: SharedAccountContext { return StarsIntroScreen(context: context) } - public func makeGiftViewScreen(context: AccountContext, message: EngineMessage) -> ViewController { - return GiftViewScreen(context: context, subject: .message(message)) + public func makeGiftViewScreen(context: AccountContext, message: EngineMessage, shareStory: (() -> Void)?) -> ViewController { + return GiftViewScreen(context: context, subject: .message(message), shareStory: shareStory) + } + + public func makeGiftViewScreen(context: AccountContext, gift: StarGift.UniqueGift, shareStory: (() -> Void)?) -> ViewController { + return GiftViewScreen(context: context, subject: .uniqueGift(gift), shareStory: shareStory) + } + + public func makeStorySharingScreen(context: AccountContext, subject: StorySharingSubject, parentController: ViewController) -> ViewController { + let editorSubject: Signal + switch subject { + case let .messages(messages): + editorSubject = .single(.message(messages.map { $0.id })) + case let .gift(gift): + editorSubject = .single(.gift(gift)) + } + + let externalState = MediaEditorTransitionOutExternalState( + storyTarget: nil, + isForcedTarget: false, + isPeerArchived: false, + transitionOut: nil + ) + let presentationData = context.sharedContext.currentPresentationData.with { $0 } + let controller = MediaEditorScreenImpl( + context: context, + mode: .storyEditor, + subject: editorSubject, + transitionIn: nil, + transitionOut: { _, _ in + return nil + }, + completion: { [weak parentController] result, commit in + let targetPeerId: EnginePeer.Id + let target: Stories.PendingTarget + if let sendAsPeerId = result.options.sendAsPeerId { + target = .peer(sendAsPeerId) + targetPeerId = sendAsPeerId + } else { + target = .myStories + targetPeerId = context.account.peerId + } + externalState.storyTarget = target + + if let rootController = context.sharedContext.mainWindow?.viewController as? TelegramRootControllerInterface { + rootController.proceedWithStoryUpload(target: target, result: result, existingMedia: nil, forwardInfo: nil, externalState: externalState, commit: commit) + } + + let _ = (context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: targetPeerId)) + |> deliverOnMainQueue).start(next: { peer in + guard let peer else { + return + } + let text: String + if case .channel = peer { + text = presentationData.strings.Story_MessageReposted_Channel(peer.compactDisplayTitle).string + } else { + text = presentationData.strings.Story_MessageReposted_Personal + } + Queue.mainQueue().after(0.25) { + parentController?.present(UndoOverlayController( + presentationData: presentationData, + content: .forward(savedMessages: false, text: text), + elevatedLayout: false, + action: { _ in return false } + ), in: .current) + + Queue.mainQueue().after(0.1) { + HapticFeedback().success() + } + } + }) + } + ) + return controller } public func makeContentReportScreen(context: AccountContext, subject: ReportContentSubject, forceDark: Bool, present: @escaping (ViewController) -> Void, completion: @escaping () -> Void, requestSelectMessages: ((String, Data, String?) -> Void)?) { @@ -2935,6 +3009,14 @@ public final class SharedAccountContextImpl: SharedAccountContext { }) } + public func makeShareController(context: AccountContext, subject: ShareControllerSubject, forceExternal: Bool, shareStory: (() -> Void)?, enqueued: (([PeerId], [Int64]) -> Void)?, actionCompleted: (() -> Void)?) -> ViewController { + let controller = ShareController(context: context, subject: subject, externalShare: forceExternal) + controller.shareStory = shareStory + controller.enqueued = enqueued + controller.actionCompleted = actionCompleted + return controller + } + public func makeMiniAppListScreenInitialData(context: AccountContext) -> Signal { return MiniAppListScreen.initialData(context: context) } diff --git a/submodules/TooltipUI/Sources/TooltipScreen.swift b/submodules/TooltipUI/Sources/TooltipScreen.swift index 58efffaa42..cc1f641314 100644 --- a/submodules/TooltipUI/Sources/TooltipScreen.swift +++ b/submodules/TooltipUI/Sources/TooltipScreen.swift @@ -600,7 +600,7 @@ private final class TooltipScreenNode: ViewControllerTracingNode { } } - let textSize: CGSize + var textSize: CGSize var isTextWithEntities = false switch self.text { @@ -630,6 +630,9 @@ private final class TooltipScreenNode: ViewControllerTracingNode { environment: {}, containerSize: CGSize(width: containerWidth - contentInset * 2.0 - animationSize.width - animationSpacing - buttonInset, height: 1000000.0) ) + if case let .customBlur(_, inset) = self.tooltipStyle, inset < 0.0 { + textSize.height -= 3.0 + } } else { textSize = self.textView.update( transition: .immediate, diff --git a/submodules/UrlHandling/Sources/UrlHandling.swift b/submodules/UrlHandling/Sources/UrlHandling.swift index baf37d9f2b..a0b251be7a 100644 --- a/submodules/UrlHandling/Sources/UrlHandling.swift +++ b/submodules/UrlHandling/Sources/UrlHandling.swift @@ -105,6 +105,7 @@ public enum ParsedInternalUrl { case chatFolder(slug: String) case premiumGiftCode(slug: String) case messageLink(slug: String) + case collectible(slug: String) case externalUrl(url: String) } @@ -523,6 +524,8 @@ public func parseInternalUrl(sharedContext: SharedAccountContext, context: Accou return .wallpaper(parameter) } else if pathComponents[0] == "addtheme" { return .theme(pathComponents[1]) + } else if pathComponents[0] == "nft" { + return .collectible(slug: pathComponents[1]) } else if pathComponents[0] == "addlist" || pathComponents[0] == "folder" || pathComponents[0] == "list" { return .chatFolder(slug: pathComponents[1]) } else if pathComponents[0] == "boost", pathComponents.count == 2 { @@ -1086,6 +1089,11 @@ private func resolveInternalUrl(context: AccountContext, url: ParsedInternalUrl) } case let .premiumGiftCode(slug): return .single(.result(.premiumGiftCode(slug: slug))) + case let .collectible(slug): + return .single(.progress) |> then(context.engine.payments.getUniqueStarGift(slug: slug) + |> map { gift -> ResolveInternalUrlResult in + return .result(.collectible(gift: gift)) + }) case let .messageLink(slug): return .single(.progress) |> then(context.engine.peers.resolveMessageLink(slug: slug) diff --git a/submodules/WebUI/Sources/WebAppMessagePreviewScreen.swift b/submodules/WebUI/Sources/WebAppMessagePreviewScreen.swift index 5769326ba9..da8e19073d 100644 --- a/submodules/WebUI/Sources/WebAppMessagePreviewScreen.swift +++ b/submodules/WebUI/Sources/WebAppMessagePreviewScreen.swift @@ -324,7 +324,8 @@ private final class WebAppMessagePreviewSheetComponent: CombinedComponent { preparedMessage: context.component.preparedMessage, dismiss: { animateOut.invoke(Action { _ in - if let controller = controller() { + if let controller = controller() as? WebAppMessagePreviewScreen { + controller.completeWithResult(false) controller.dismiss(completion: nil) } }) diff --git a/versions.json b/versions.json index 77784a5a49..ab99182b79 100644 --- a/versions.json +++ b/versions.json @@ -1,5 +1,5 @@ { - "app": "11.6", + "app": "11.6.1", "xcode": "16.0", "bazel": "7.3.1:981f82a470bad1349322b6f51c9c6ffa0aa291dab1014fac411543c12e661dff", "macos": "15.0"