diff --git a/Telegram/NotificationService/Sources/NotificationService.swift b/Telegram/NotificationService/Sources/NotificationService.swift index 266ac1fa0d..6fca79a4e5 100644 --- a/Telegram/NotificationService/Sources/NotificationService.swift +++ b/Telegram/NotificationService/Sources/NotificationService.swift @@ -980,7 +980,7 @@ private final class NotificationServiceHandler { enum Action { case logout - case poll(peerId: PeerId, content: NotificationContent, messageId: MessageId?) + case poll(peerId: PeerId, content: NotificationContent, messageId: MessageId?, reportDelivery: Bool) case pollStories(peerId: PeerId, content: NotificationContent, storyId: Int32, isReaction: Bool) case deleteMessage([MessageId]) case readReactions([MessageId]) @@ -999,7 +999,7 @@ private final class NotificationServiceHandler { action = .logout case "MESSAGE_MUTED": if let peerId = peerId { - action = .poll(peerId: peerId, content: NotificationContent(isLockedMessage: nil), messageId: nil) + action = .poll(peerId: peerId, content: NotificationContent(isLockedMessage: nil), messageId: nil, reportDelivery: false) } case "MESSAGE_DELETED": if let peerId = peerId { @@ -1183,9 +1183,16 @@ private final class NotificationServiceHandler { action = .pollStories(peerId: peerId, content: content, storyId: storyId, isReaction: isReaction) } else { - action = .poll(peerId: peerId, content: content, messageId: messageIdValue) + var reportDelivery = false + if let reportDeliveryUntilDate = aps["report_delivery_until_date"] as? Int32, let messageId = messageIdValue { + let currentTime = Int32(CFAbsoluteTimeGetCurrent() + kCFAbsoluteTimeIntervalSince1970) + if reportDeliveryUntilDate > currentTime { + reportDelivery = true + } + } + action = .poll(peerId: peerId, content: content, messageId: messageIdValue, reportDelivery: reportDelivery) } - + updateCurrentContent(content) } } @@ -1245,7 +1252,7 @@ private final class NotificationServiceHandler { let content = NotificationContent(isLockedMessage: nil) updateCurrentContent(content) completed() - case let .poll(peerId, initialContent, messageId): + case let .poll(peerId, initialContent, messageId, reportDelivery): Logger.shared.log("NotificationService \(episode)", "Will poll") if let stateManager = strongSelf.stateManager { let shouldKeepConnection = stateManager.network.shouldKeepConnection @@ -1683,12 +1690,23 @@ private final class NotificationServiceHandler { pollWithUpdatedContent = pollSignal |> map { _ -> (NotificationContent, Media?) in } } + + let reportDeliverySignal: Signal + if reportDelivery, let messageId { + reportDeliverySignal = _internal_reportMessageDelivery(postbox: stateManager.postbox, network: stateManager.network, messageIds: [messageId], fromPushNotification: true) + |> mapToSignal { _ -> Signal in + return .single(true) + } + |> then(.single(true)) + } else { + reportDeliverySignal = .single(true) + } var updatedContent = initialContent var updatedMedia: Media? - strongSelf.pollDisposable.set(pollWithUpdatedContent.start(next: { content, media in - updatedContent = content - updatedMedia = media + strongSelf.pollDisposable.set(combineLatest(pollWithUpdatedContent, reportDeliverySignal).start(next: { contentAndMedia, _ in + updatedContent = contentAndMedia.0 + updatedMedia = contentAndMedia.1 }, completed: { pollCompletion(updatedContent, updatedMedia) })) @@ -1951,6 +1969,8 @@ private final class NotificationServiceHandler { } else { pollWithUpdatedContent = .complete() } + + var updatedContent = initialContent strongSelf.pollDisposable.set(pollWithUpdatedContent.start(next: { content in diff --git a/Telegram/Telegram-iOS/Resources/GiftUpgraded.tgs b/Telegram/Telegram-iOS/Resources/GiftUpgraded.tgs new file mode 100644 index 0000000000..aff88f5e7f Binary files /dev/null and b/Telegram/Telegram-iOS/Resources/GiftUpgraded.tgs differ diff --git a/Telegram/Telegram-iOS/en.lproj/Localizable.strings b/Telegram/Telegram-iOS/en.lproj/Localizable.strings index 02432016b8..13de52e05f 100644 --- a/Telegram/Telegram-iOS/en.lproj/Localizable.strings +++ b/Telegram/Telegram-iOS/en.lproj/Localizable.strings @@ -12299,6 +12299,7 @@ Sorry for the inconvenience."; "Stars.Intro.Transaction.ConvertedGift" = "Converted Gift"; "Stars.Intro.Transaction.Unsupported.Title" = "Unsupported"; "Stars.Intro.Transaction.Refund" = "Refund"; +"Stars.Intro.Transaction.GiftUpgrade" = "Gift Upgrade"; "Stars.Intro.PurchasedTitle" = "Stars Acquired"; "Stars.Intro.PurchasedText" = "**%@** added to your balance."; @@ -13503,9 +13504,9 @@ Sorry for the inconvenience."; "Gift.Unique.Availability" = "Availability"; "Gift.Unique.Issued" = "%@ issued"; "Gift.Unique.OriginalInfo" = "Gifted to %1$@ on %2$@."; -"Gift.Unique.OriginalInfoWithText" = "Gifted to %1$@ on %2$@ with comment \"%3$@\""; +"Gift.Unique.OriginalInfoWithText" = "Gifted to %1$@ on %2$@ with comment \"%3$@\"."; "Gift.Unique.OriginalInfoSender" = "Gifted by %1$@ to %2$@ on %3$@."; -"Gift.Unique.OriginalInfoSenderWithText" = "Gifted by %1$@ to %2$@ on %3$@ with comment \"%4$@\""; +"Gift.Unique.OriginalInfoSenderWithText" = "Gifted by %1$@ to %2$@ on %3$@ with comment \"%4$@\"."; "Gift.Unique.AttributeDescription" = "Only %@ of such collectibles have this attribute."; @@ -13533,6 +13534,7 @@ Sorry for the inconvenience."; "Gift.View.UpgradeForFree" = "Upgrade for Free"; "Gift.View.KeepUpgradeOrConvertDescription" = "You can keep this gift, upgrade it, or sell it for %@. [More About Stars >]()"; +"Gift.View.KeepOrUpgradeDescription" = "You can keep this gift or upgrade it."; "PeerInfo.VerificationInfo.Bot" = "This bot is verified as official by the representatives of Telegram."; "PeerInfo.VerificationInfo.Channel" = "This channel is verified as official by the representatives of Telegram."; @@ -13561,4 +13563,7 @@ Sorry for the inconvenience."; "Notification.StarGift.Subtitle.Refunded" = "This gift cannot be converted to Stars because the payment related to it was refunded."; "Notification.StarGift.Subtitle.Downgraded" = "This gift was downgraded because a request to refund the payment related to this gift was made, and the money was returned."; -"Gift.View.KeepOrUpgradeDescription" = "You can keep this gift or upgrade it."; +"Notification.StarGift.Subtitle.Other" = "%1$@ can keep this gift or upgrade it."; + +"Stars.Transaction.GiftFrom" = "Gift From"; +"Stars.Transaction.GiftUpgrade" = "Gift Upgrade"; diff --git a/submodules/AnimatedStickerNode/Sources/AnimatedStickerNode.swift b/submodules/AnimatedStickerNode/Sources/AnimatedStickerNode.swift index d9eb6947ce..f9ee14ae3b 100644 --- a/submodules/AnimatedStickerNode/Sources/AnimatedStickerNode.swift +++ b/submodules/AnimatedStickerNode/Sources/AnimatedStickerNode.swift @@ -218,7 +218,7 @@ public final class DefaultAnimatedStickerNodeImpl: ASDisplayNode, AnimatedSticke public var isPlaying: Bool = false private var currentLoopCount: Int = 0 private var canDisplayFirstFrame: Bool = false - private var playbackMode: AnimatedStickerPlaybackMode = .loop + public var playbackMode: AnimatedStickerPlaybackMode = .loop public var stopAtNearestLoop: Bool = false diff --git a/submodules/BotPaymentsUI/Sources/BotCheckoutControllerNode.swift b/submodules/BotPaymentsUI/Sources/BotCheckoutControllerNode.swift index cbf8994a0f..ae27df6f66 100644 --- a/submodules/BotPaymentsUI/Sources/BotCheckoutControllerNode.swift +++ b/submodules/BotPaymentsUI/Sources/BotCheckoutControllerNode.swift @@ -1527,7 +1527,7 @@ final class BotCheckoutControllerNode: ItemListControllerNode, PKPaymentAuthoriz } switch result { - case let .done(receiptMessageId, _): + case let .done(receiptMessageId, _, _): proceedWithCompletion(true, receiptMessageId) case let .externalVerificationRequired(url): strongSelf.updateActionButton() diff --git a/submodules/ContactsPeerItem/Sources/ContactsPeerItem.swift b/submodules/ContactsPeerItem/Sources/ContactsPeerItem.swift index 3bc9738206..0ac902e971 100644 --- a/submodules/ContactsPeerItem/Sources/ContactsPeerItem.swift +++ b/submodules/ContactsPeerItem/Sources/ContactsPeerItem.swift @@ -1352,6 +1352,7 @@ public class ContactsPeerItemNode: ItemListRevealOptionsItemNode { let _ = titleApply() var titleLeftOffset: CGFloat = 0.0 + var nextIconX: CGFloat = titleFrame.maxX if let verifiedIcon { let animationCache = item.context.animationCache let animationRenderer = item.context.animationRenderer @@ -1375,17 +1376,30 @@ public class ContactsPeerItemNode: ItemListRevealOptionsItemNode { emojiFileUpdated: nil ) 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( transition: .immediate, component: AnyComponent(verifiedIconComponent), environment: {}, - containerSize: CGSize(width: 16.0, height: 16.0) + containerSize: containerSize ) - transition.updateFrame(view: verifiedIconView, frame: CGRect(origin: CGPoint(x: titleFrame.minX, y: floorToScreenPixels(titleFrame.midY - iconSize.height / 2.0)), size: iconSize)) + transition.updateFrame(view: verifiedIconView, frame: CGRect(origin: CGPoint(x: iconOrigin, y: floorToScreenPixels(titleFrame.midY - iconSize.height / 2.0)), size: iconSize)) - titleLeftOffset += iconSize.width + 4.0 + if case .animation = verifiedIcon { + titleLeftOffset += iconSize.width + 4.0 + } else { + nextIconX += iconSize.width + } } else if let verifiedIconView = strongSelf.verifiedIconView { strongSelf.verifiedIconView = nil verifiedIconView.removeFromSuperview() @@ -1424,7 +1438,6 @@ public class ContactsPeerItemNode: ItemListRevealOptionsItemNode { } } - var nextIconX: CGFloat = titleFrame.maxX if let credibilityIcon { let animationCache = item.context.animationCache let animationRenderer = item.context.animationRenderer diff --git a/submodules/Display/Source/TextAlertController.swift b/submodules/Display/Source/TextAlertController.swift index 87cf1152c5..c8ddef511c 100644 --- a/submodules/Display/Source/TextAlertController.swift +++ b/submodules/Display/Source/TextAlertController.swift @@ -126,7 +126,7 @@ public final class TextAlertContentActionNode: HighlightableButtonNode { } let attributedString = NSMutableAttributedString(string: self.action.title, font: font, textColor: color, paragraphAlignment: .center) - if let range = attributedString.string.range(of: "⭐️") { + if let range = attributedString.string.range(of: "$") { attributedString.addAttribute(.attachment, value: UIImage(bundleImageName: "Item List/PremiumIcon")!, range: NSRange(range, in: attributedString.string)) attributedString.addAttribute(.foregroundColor, value: color, range: NSRange(range, in: attributedString.string)) attributedString.addAttribute(.baselineOffset, value: 1.0, range: NSRange(range, in: attributedString.string)) diff --git a/submodules/ItemListPeerItem/Sources/ItemListPeerItem.swift b/submodules/ItemListPeerItem/Sources/ItemListPeerItem.swift index 4a908a11b7..3882bae635 100644 --- a/submodules/ItemListPeerItem/Sources/ItemListPeerItem.swift +++ b/submodules/ItemListPeerItem/Sources/ItemListPeerItem.swift @@ -1436,6 +1436,7 @@ public class ItemListPeerItemNode: ItemListRevealOptionsItemNode, ItemListItemNo var titleFrame = CGRect(origin: CGPoint(x: leftInset + revealOffset + editingOffset, y: verticalInset + verticalOffset), size: titleLayout.size) var titleLeftOffset: CGFloat = 0.0 + var nextIconX: CGFloat = titleFrame.maxX if let verifiedIcon = verifiedIcon { let animationCache = item.context.animationCache let animationRenderer = item.context.animationRenderer @@ -1461,6 +1462,15 @@ 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), @@ -1468,9 +1478,13 @@ public class ItemListPeerItemNode: ItemListRevealOptionsItemNode, ItemListItemNo containerSize: CGSize(width: 20.0, height: 20.0) ) - transition.updateFrame(view: verifiedIconView, frame: CGRect(origin: CGPoint(x: titleFrame.maxX + 4.0, y: floorToScreenPixels(titleFrame.midY - iconSize.height / 2.0)), size: iconSize)) - - titleLeftOffset += iconSize.width + 4.0 + transition.updateFrame(view: verifiedIconView, frame: CGRect(origin: CGPoint(x: iconOrigin, y: floorToScreenPixels(titleFrame.midY - iconSize.height / 2.0)), size: iconSize)) + + if case .animation = verifiedIcon { + titleLeftOffset += iconSize.width + 4.0 + } else { + nextIconX += iconSize.width + } } else if let verifiedIconView = strongSelf.verifiedIconView { strongSelf.verifiedIconView = nil verifiedIconView.removeFromSuperview() @@ -1512,7 +1526,8 @@ public class ItemListPeerItemNode: ItemListRevealOptionsItemNode, ItemListItemNo containerSize: CGSize(width: 20.0, height: 20.0) ) - transition.updateFrame(view: credibilityIconView, frame: CGRect(origin: CGPoint(x: titleFrame.maxX + 4.0, y: floorToScreenPixels(titleFrame.midY - iconSize.height / 2.0)), size: iconSize)) + nextIconX += 4.0 + transition.updateFrame(view: credibilityIconView, frame: CGRect(origin: CGPoint(x: nextIconX, y: floorToScreenPixels(titleFrame.midY - iconSize.height / 2.0)), size: iconSize)) } else if let credibilityIconView = strongSelf.credibilityIconView { strongSelf.credibilityIconView = nil credibilityIconView.removeFromSuperview() diff --git a/submodules/StatisticsUI/Sources/StarsTransactionItem.swift b/submodules/StatisticsUI/Sources/StarsTransactionItem.swift index 07fc5652d8..9a9790fd9b 100644 --- a/submodules/StatisticsUI/Sources/StarsTransactionItem.swift +++ b/submodules/StatisticsUI/Sources/StarsTransactionItem.swift @@ -336,7 +336,7 @@ final class StarsTransactionItemNode: ListViewItemNode, ItemListItemNode { theme: item.presentationData.theme, title: AnyComponent(VStack(titleComponents, alignment: .left, spacing: 2.0)), contentInsets: UIEdgeInsets(top: 9.0, left: 0.0, bottom: 8.0, right: 0.0), - leftIcon: .custom(AnyComponentWithIdentity(id: "avatar", component: AnyComponent(StarsAvatarComponent(context: item.context, theme: item.presentationData.theme, peer: item.transaction.peer, photo: nil, media: [], backgroundColor: item.presentationData.theme.list.itemBlocksBackgroundColor))), false), + leftIcon: .custom(AnyComponentWithIdentity(id: "avatar", component: AnyComponent(StarsAvatarComponent(context: item.context, theme: item.presentationData.theme, peer: item.transaction.peer, photo: nil, media: [], uniqueGift: nil, backgroundColor: item.presentationData.theme.list.itemBlocksBackgroundColor))), false), icon: nil, accessory: .custom(ListActionItemComponent.CustomAccessory(component: AnyComponentWithIdentity(id: "label", component: AnyComponent(StarsLabelComponent(text: itemLabel))), insets: UIEdgeInsets(top: 0, left: 0, bottom: 0, right: 16.0))), action: { [weak self] _ in diff --git a/submodules/TelegramCore/Sources/Account/AccountIntermediateState.swift b/submodules/TelegramCore/Sources/Account/AccountIntermediateState.swift index 1b02ac704f..9973824ba0 100644 --- a/submodules/TelegramCore/Sources/Account/AccountIntermediateState.swift +++ b/submodules/TelegramCore/Sources/Account/AccountIntermediateState.swift @@ -132,6 +132,7 @@ enum AccountStateMutationOperation { case UpdateStarsBalance(peerId: PeerId, balance: Api.StarsAmount) case UpdateStarsRevenueStatus(peerId: PeerId, status: StarsRevenueStats.Balances) case UpdateStarsReactionsAreAnonymousByDefault(isAnonymous: Bool) + case ReportMessageDelivery([MessageId]) } struct HoleFromPreviousState { @@ -702,9 +703,13 @@ struct AccountMutableState { self.addOperation(.UpdateStarsReactionsAreAnonymousByDefault(isAnonymous: isAnonymous)) } + mutating func addReportMessageDelivery(messageIds: [MessageId]) { + self.addOperation(.ReportMessageDelivery(messageIds)) + } + mutating func addOperation(_ operation: AccountStateMutationOperation) { switch operation { - case .DeleteMessages, .DeleteMessagesWithGlobalIds, .EditMessage, .UpdateMessagePoll, .UpdateMessageReactions, .UpdateMedia, .ReadOutbox, .ReadGroupFeedInbox, .MergePeerPresences, .UpdateSecretChat, .AddSecretMessages, .ReadSecretOutbox, .AddPeerInputActivity, .UpdateCachedPeerData, .UpdatePinnedItemIds, .UpdatePinnedSavedItemIds, .UpdatePinnedTopic, .UpdatePinnedTopicOrder, .ReadMessageContents, .UpdateMessageImpressionCount, .UpdateMessageForwardsCount, .UpdateInstalledStickerPacks, .UpdateRecentGifs, .UpdateChatInputState, .UpdateCall, .AddCallSignalingData, .UpdateLangPack, .UpdateMinAvailableMessage, .UpdatePeerChatUnreadMark, .UpdateIsContact, .UpdatePeerChatInclusion, .UpdatePeersNearby, .UpdateTheme, .UpdateWallpaper, .SyncChatListFilters, .UpdateChatListFilterOrder, .UpdateChatListFilter, .UpdateReadThread, .UpdateGroupCallParticipants, .UpdateGroupCall, .UpdateMessagesPinned, .UpdateAutoremoveTimeout, .UpdateAttachMenuBots, .UpdateAudioTranscription, .UpdateConfig, .UpdateExtendedMedia, .ResetForumTopic, .UpdateStory, .UpdateReadStories, .UpdateStoryStealthMode, .UpdateStorySentReaction, .UpdateNewAuthorization, .UpdateRevenueBalances, .UpdateStarsBalance, .UpdateStarsRevenueStatus, .UpdateStarsReactionsAreAnonymousByDefault: + case .DeleteMessages, .DeleteMessagesWithGlobalIds, .EditMessage, .UpdateMessagePoll, .UpdateMessageReactions, .UpdateMedia, .ReadOutbox, .ReadGroupFeedInbox, .MergePeerPresences, .UpdateSecretChat, .AddSecretMessages, .ReadSecretOutbox, .AddPeerInputActivity, .UpdateCachedPeerData, .UpdatePinnedItemIds, .UpdatePinnedSavedItemIds, .UpdatePinnedTopic, .UpdatePinnedTopicOrder, .ReadMessageContents, .UpdateMessageImpressionCount, .UpdateMessageForwardsCount, .UpdateInstalledStickerPacks, .UpdateRecentGifs, .UpdateChatInputState, .UpdateCall, .AddCallSignalingData, .UpdateLangPack, .UpdateMinAvailableMessage, .UpdatePeerChatUnreadMark, .UpdateIsContact, .UpdatePeerChatInclusion, .UpdatePeersNearby, .UpdateTheme, .UpdateWallpaper, .SyncChatListFilters, .UpdateChatListFilterOrder, .UpdateChatListFilter, .UpdateReadThread, .UpdateGroupCallParticipants, .UpdateGroupCall, .UpdateMessagesPinned, .UpdateAutoremoveTimeout, .UpdateAttachMenuBots, .UpdateAudioTranscription, .UpdateConfig, .UpdateExtendedMedia, .ResetForumTopic, .UpdateStory, .UpdateReadStories, .UpdateStoryStealthMode, .UpdateStorySentReaction, .UpdateNewAuthorization, .UpdateRevenueBalances, .UpdateStarsBalance, .UpdateStarsRevenueStatus, .UpdateStarsReactionsAreAnonymousByDefault, .ReportMessageDelivery: break case let .AddMessages(messages, location): for message in messages { @@ -852,6 +857,7 @@ struct AccountReplayedFinalState { let updatedStarsBalance: [PeerId: StarsAmount] let updatedStarsRevenueStatus: [PeerId: StarsRevenueStats.Balances] let sentScheduledMessageIds: Set + let reportMessageDelivery: Set } struct AccountFinalStateEvents { @@ -882,12 +888,13 @@ struct AccountFinalStateEvents { let updatedRevenueBalances: [PeerId: RevenueStats.Balances] let updatedStarsBalance: [PeerId: StarsAmount] let updatedStarsRevenueStatus: [PeerId: StarsRevenueStats.Balances] + let reportMessageDelivery: Set var isEmpty: Bool { - return self.addedIncomingMessageIds.isEmpty && self.addedReactionEvents.isEmpty && self.wasScheduledMessageIds.isEmpty && self.deletedMessageIds.isEmpty && self.sentScheduledMessageIds.isEmpty && self.updatedTypingActivities.isEmpty && self.updatedWebpages.isEmpty && self.updatedCalls.isEmpty && self.addedCallSignalingData.isEmpty && self.updatedGroupCallParticipants.isEmpty && self.storyUpdates.isEmpty && self.updatedPeersNearby?.isEmpty ?? true && self.isContactUpdates.isEmpty && self.displayAlerts.isEmpty && self.dismissBotWebViews.isEmpty && self.delayNotificatonsUntil == nil && self.updatedMaxMessageId == nil && self.updatedQts == nil && self.externallyUpdatedPeerId.isEmpty && !authorizationListUpdated && self.updatedIncomingThreadReadStates.isEmpty && self.updatedOutgoingThreadReadStates.isEmpty && !self.updateConfig && !self.isPremiumUpdated && self.updatedRevenueBalances.isEmpty && self.updatedStarsBalance.isEmpty && self.updatedStarsRevenueStatus.isEmpty + return self.addedIncomingMessageIds.isEmpty && self.addedReactionEvents.isEmpty && self.wasScheduledMessageIds.isEmpty && self.deletedMessageIds.isEmpty && self.sentScheduledMessageIds.isEmpty && self.updatedTypingActivities.isEmpty && self.updatedWebpages.isEmpty && self.updatedCalls.isEmpty && self.addedCallSignalingData.isEmpty && self.updatedGroupCallParticipants.isEmpty && self.storyUpdates.isEmpty && self.updatedPeersNearby?.isEmpty ?? true && self.isContactUpdates.isEmpty && self.displayAlerts.isEmpty && self.dismissBotWebViews.isEmpty && self.delayNotificatonsUntil == nil && self.updatedMaxMessageId == nil && self.updatedQts == nil && self.externallyUpdatedPeerId.isEmpty && !authorizationListUpdated && self.updatedIncomingThreadReadStates.isEmpty && self.updatedOutgoingThreadReadStates.isEmpty && !self.updateConfig && !self.isPremiumUpdated && self.updatedRevenueBalances.isEmpty && self.updatedStarsBalance.isEmpty && self.updatedStarsRevenueStatus.isEmpty && self.reportMessageDelivery.isEmpty } - init(addedIncomingMessageIds: [MessageId] = [], addedReactionEvents: [(reactionAuthor: Peer, reaction: MessageReaction.Reaction, message: Message, timestamp: Int32)] = [], wasScheduledMessageIds: [MessageId] = [], deletedMessageIds: [DeletedMessageId] = [], updatedTypingActivities: [PeerActivitySpace: [PeerId: PeerInputActivity?]] = [:], updatedWebpages: [MediaId: TelegramMediaWebpage] = [:], updatedCalls: [Api.PhoneCall] = [], addedCallSignalingData: [(Int64, Data)] = [], updatedGroupCallParticipants: [(Int64, GroupCallParticipantsContext.Update)] = [], storyUpdates: [InternalStoryUpdate] = [], updatedPeersNearby: [PeerNearby]? = nil, isContactUpdates: [(PeerId, Bool)] = [], displayAlerts: [(text: String, isDropAuth: Bool)] = [], dismissBotWebViews: [Int64] = [], delayNotificatonsUntil: Int32? = nil, updatedMaxMessageId: Int32? = nil, updatedQts: Int32? = nil, externallyUpdatedPeerId: Set = Set(), authorizationListUpdated: Bool = false, updatedIncomingThreadReadStates: [MessageId: MessageId.Id] = [:], updatedOutgoingThreadReadStates: [MessageId: MessageId.Id] = [:], updateConfig: Bool = false, isPremiumUpdated: Bool = false, updatedRevenueBalances: [PeerId: RevenueStats.Balances] = [:], updatedStarsBalance: [PeerId: StarsAmount] = [:], updatedStarsRevenueStatus: [PeerId: StarsRevenueStats.Balances] = [:], sentScheduledMessageIds: Set = Set()) { + init(addedIncomingMessageIds: [MessageId] = [], addedReactionEvents: [(reactionAuthor: Peer, reaction: MessageReaction.Reaction, message: Message, timestamp: Int32)] = [], wasScheduledMessageIds: [MessageId] = [], deletedMessageIds: [DeletedMessageId] = [], updatedTypingActivities: [PeerActivitySpace: [PeerId: PeerInputActivity?]] = [:], updatedWebpages: [MediaId: TelegramMediaWebpage] = [:], updatedCalls: [Api.PhoneCall] = [], addedCallSignalingData: [(Int64, Data)] = [], updatedGroupCallParticipants: [(Int64, GroupCallParticipantsContext.Update)] = [], storyUpdates: [InternalStoryUpdate] = [], updatedPeersNearby: [PeerNearby]? = nil, isContactUpdates: [(PeerId, Bool)] = [], displayAlerts: [(text: String, isDropAuth: Bool)] = [], dismissBotWebViews: [Int64] = [], delayNotificatonsUntil: Int32? = nil, updatedMaxMessageId: Int32? = nil, updatedQts: Int32? = nil, externallyUpdatedPeerId: Set = Set(), authorizationListUpdated: Bool = false, updatedIncomingThreadReadStates: [MessageId: MessageId.Id] = [:], updatedOutgoingThreadReadStates: [MessageId: MessageId.Id] = [:], updateConfig: Bool = false, isPremiumUpdated: Bool = false, updatedRevenueBalances: [PeerId: RevenueStats.Balances] = [:], updatedStarsBalance: [PeerId: StarsAmount] = [:], updatedStarsRevenueStatus: [PeerId: StarsRevenueStats.Balances] = [:], sentScheduledMessageIds: Set = Set(), reportMessageDelivery: Set = Set()) { self.addedIncomingMessageIds = addedIncomingMessageIds self.addedReactionEvents = addedReactionEvents self.wasScheduledMessageIds = wasScheduledMessageIds @@ -915,6 +922,7 @@ struct AccountFinalStateEvents { self.updatedStarsBalance = updatedStarsBalance self.updatedStarsRevenueStatus = updatedStarsRevenueStatus self.sentScheduledMessageIds = sentScheduledMessageIds + self.reportMessageDelivery = reportMessageDelivery } init(state: AccountReplayedFinalState) { @@ -945,6 +953,7 @@ struct AccountFinalStateEvents { self.updatedStarsBalance = state.updatedStarsBalance self.updatedStarsRevenueStatus = state.updatedStarsRevenueStatus self.sentScheduledMessageIds = state.sentScheduledMessageIds + self.reportMessageDelivery = state.reportMessageDelivery } func union(with other: AccountFinalStateEvents) -> AccountFinalStateEvents { @@ -977,6 +986,9 @@ struct AccountFinalStateEvents { var sentScheduledMessageIds = self.sentScheduledMessageIds sentScheduledMessageIds.formUnion(other.sentScheduledMessageIds) - return AccountFinalStateEvents(addedIncomingMessageIds: self.addedIncomingMessageIds + other.addedIncomingMessageIds, addedReactionEvents: self.addedReactionEvents + other.addedReactionEvents, wasScheduledMessageIds: self.wasScheduledMessageIds + other.wasScheduledMessageIds, deletedMessageIds: self.deletedMessageIds + other.deletedMessageIds, updatedTypingActivities: self.updatedTypingActivities, updatedWebpages: self.updatedWebpages, updatedCalls: self.updatedCalls + other.updatedCalls, addedCallSignalingData: self.addedCallSignalingData + other.addedCallSignalingData, updatedGroupCallParticipants: self.updatedGroupCallParticipants + other.updatedGroupCallParticipants, storyUpdates: self.storyUpdates + other.storyUpdates, isContactUpdates: self.isContactUpdates + other.isContactUpdates, displayAlerts: self.displayAlerts + other.displayAlerts, dismissBotWebViews: self.dismissBotWebViews + other.dismissBotWebViews, delayNotificatonsUntil: delayNotificatonsUntil, updatedMaxMessageId: updatedMaxMessageId, updatedQts: updatedQts, externallyUpdatedPeerId: externallyUpdatedPeerId, authorizationListUpdated: authorizationListUpdated, updatedIncomingThreadReadStates: self.updatedIncomingThreadReadStates.merging(other.updatedIncomingThreadReadStates, uniquingKeysWith: { lhs, _ in lhs }), updateConfig: updateConfig, isPremiumUpdated: isPremiumUpdated, updatedRevenueBalances: self.updatedRevenueBalances.merging(other.updatedRevenueBalances, uniquingKeysWith: { lhs, _ in lhs }), updatedStarsBalance: self.updatedStarsBalance.merging(other.updatedStarsBalance, uniquingKeysWith: { lhs, _ in lhs }), updatedStarsRevenueStatus: self.updatedStarsRevenueStatus.merging(other.updatedStarsRevenueStatus, uniquingKeysWith: { lhs, _ in lhs }), sentScheduledMessageIds: sentScheduledMessageIds) + var reportMessageDelivery = self.reportMessageDelivery + reportMessageDelivery.formUnion(other.reportMessageDelivery) + + return AccountFinalStateEvents(addedIncomingMessageIds: self.addedIncomingMessageIds + other.addedIncomingMessageIds, addedReactionEvents: self.addedReactionEvents + other.addedReactionEvents, wasScheduledMessageIds: self.wasScheduledMessageIds + other.wasScheduledMessageIds, deletedMessageIds: self.deletedMessageIds + other.deletedMessageIds, updatedTypingActivities: self.updatedTypingActivities, updatedWebpages: self.updatedWebpages, updatedCalls: self.updatedCalls + other.updatedCalls, addedCallSignalingData: self.addedCallSignalingData + other.addedCallSignalingData, updatedGroupCallParticipants: self.updatedGroupCallParticipants + other.updatedGroupCallParticipants, storyUpdates: self.storyUpdates + other.storyUpdates, isContactUpdates: self.isContactUpdates + other.isContactUpdates, displayAlerts: self.displayAlerts + other.displayAlerts, dismissBotWebViews: self.dismissBotWebViews + other.dismissBotWebViews, delayNotificatonsUntil: delayNotificatonsUntil, updatedMaxMessageId: updatedMaxMessageId, updatedQts: updatedQts, externallyUpdatedPeerId: externallyUpdatedPeerId, authorizationListUpdated: authorizationListUpdated, updatedIncomingThreadReadStates: self.updatedIncomingThreadReadStates.merging(other.updatedIncomingThreadReadStates, uniquingKeysWith: { lhs, _ in lhs }), updateConfig: updateConfig, isPremiumUpdated: isPremiumUpdated, updatedRevenueBalances: self.updatedRevenueBalances.merging(other.updatedRevenueBalances, uniquingKeysWith: { lhs, _ in lhs }), updatedStarsBalance: self.updatedStarsBalance.merging(other.updatedStarsBalance, uniquingKeysWith: { lhs, _ in lhs }), updatedStarsRevenueStatus: self.updatedStarsRevenueStatus.merging(other.updatedStarsRevenueStatus, uniquingKeysWith: { lhs, _ in lhs }), sentScheduledMessageIds: sentScheduledMessageIds, reportMessageDelivery: reportMessageDelivery) } } diff --git a/submodules/TelegramCore/Sources/ApiUtils/TelegramMediaAction.swift b/submodules/TelegramCore/Sources/ApiUtils/TelegramMediaAction.swift index 65752ba302..ec8a8046fa 100644 --- a/submodules/TelegramCore/Sources/ApiUtils/TelegramMediaAction.swift +++ b/submodules/TelegramCore/Sources/ApiUtils/TelegramMediaAction.swift @@ -171,7 +171,7 @@ func telegramMediaActionFromApiAction(_ action: Api.MessageAction) -> TelegramMe return TelegramMediaAction(action: .paymentRefunded(peerId: peer.peerId, currency: currency, totalAmount: totalAmount, payload: payload?.makeData(), transactionId: transactionId)) case let .messageActionPrizeStars(flags, stars, transactionId, boostPeer, giveawayMsgId): return TelegramMediaAction(action: .prizeStars(amount: stars, isUnclaimed: (flags & (1 << 2)) != 0, boostPeerId: boostPeer.peerId, transactionId: transactionId, giveawayMessageId: MessageId(peerId: boostPeer.peerId, namespace: Namespaces.Message.Cloud, id: giveawayMsgId))) - case let .messageActionStarGift(flags, apiGift, message, convertStars, _, upgradeStars): + case let .messageActionStarGift(flags, apiGift, message, convertStars, upgradeMessageId, upgradeStars): let text: String? let entities: [MessageTextEntity]? switch message { @@ -185,7 +185,7 @@ func telegramMediaActionFromApiAction(_ action: Api.MessageAction) -> TelegramMe guard let gift = StarGift(apiStarGift: apiGift) else { return nil } - return TelegramMediaAction(action: .starGift(gift: gift, convertStars: convertStars, text: text, entities: entities, nameHidden: (flags & (1 << 0)) != 0, savedToProfile: (flags & (1 << 2)) != 0, converted: (flags & (1 << 3)) != 0, upgraded: (flags & (1 << 5)) != 0, canUpgrade: (flags & (1 << 10)) != 0, upgradeStars: upgradeStars, isRefunded: (flags & (1 << 9)) != 0)) + return TelegramMediaAction(action: .starGift(gift: gift, convertStars: convertStars, text: text, entities: entities, nameHidden: (flags & (1 << 0)) != 0, savedToProfile: (flags & (1 << 2)) != 0, converted: (flags & (1 << 3)) != 0, upgraded: (flags & (1 << 5)) != 0, canUpgrade: (flags & (1 << 10)) != 0, upgradeStars: upgradeStars, isRefunded: (flags & (1 << 9)) != 0, upgradeMessageId: upgradeMessageId)) case let .messageActionStarGiftUnique(flags, apiGift, canExportAt, transferStars): guard let gift = StarGift(apiStarGift: apiGift) else { return nil diff --git a/submodules/TelegramCore/Sources/State/AccountStateManagementUtils.swift b/submodules/TelegramCore/Sources/State/AccountStateManagementUtils.swift index e008aa32e1..b5d80567dc 100644 --- a/submodules/TelegramCore/Sources/State/AccountStateManagementUtils.swift +++ b/submodules/TelegramCore/Sources/State/AccountStateManagementUtils.swift @@ -721,6 +721,7 @@ func finalStateWithDifference(accountPeerId: PeerId, postbox: Postbox, network: updatedState.mergeChats(chats) updatedState.mergeUsers(users) + let currentTime = Int32(CFAbsoluteTimeGetCurrent() + kCFAbsoluteTimeIntervalSince1970) for message in messages { if let preCachedResources = message.preCachedResources { for (resource, data) in preCachedResources { @@ -738,6 +739,10 @@ func finalStateWithDifference(accountPeerId: PeerId, postbox: Postbox, network: } if let message = StoreMessage(apiMessage: message, accountPeerId: accountPeerId, peerIsForum: peerIsForum) { updatedState.addMessages([message], location: .UpperHistoryBlock) + + if let reportDeliveryAttribute = message.attributes.first(where: { $0 is ReportDeliveryMessageAttribute }) as? ReportDeliveryMessageAttribute, case let .Id(id) = message.id, reportDeliveryAttribute.untilDate > currentTime { + updatedState.addReportMessageDelivery(messageIds: [id]) + } } } @@ -901,6 +906,8 @@ private func finalStateWithUpdatesAndServerTime(accountPeerId: PeerId, postbox: } } + let currentTime = Int32(CFAbsoluteTimeGetCurrent() + kCFAbsoluteTimeIntervalSince1970) + var missingUpdatesFromChannels = Set() for update in sortedUpdates(updates) { @@ -1110,6 +1117,10 @@ private func finalStateWithUpdatesAndServerTime(accountPeerId: PeerId, postbox: } } updatedState.addMessages([message], location: .UpperHistoryBlock) + + if let reportDeliveryAttribute = message.attributes.first(where: { $0 is ReportDeliveryMessageAttribute }) as? ReportDeliveryMessageAttribute, case let .Id(id) = message.id, reportDeliveryAttribute.untilDate > currentTime { + updatedState.addReportMessageDelivery(messageIds: [id]) + } } case let .updateServiceNotification(flags, date, type, text, media, entities): let popup = (flags & (1 << 0)) != 0 @@ -3282,7 +3293,7 @@ private func optimizedOperations(_ operations: [AccountStateMutationOperation]) var currentAddQuickReplyMessages: OptimizeAddMessagesState? for operation in operations { switch operation { - case .DeleteMessages, .DeleteMessagesWithGlobalIds, .EditMessage, .UpdateMessagePoll, .UpdateMessageReactions, .UpdateMedia, .MergeApiChats, .MergeApiUsers, .MergePeerPresences, .UpdatePeer, .ReadInbox, .ReadOutbox, .ReadGroupFeedInbox, .ResetReadState, .ResetIncomingReadState, .UpdatePeerChatUnreadMark, .ResetMessageTagSummary, .UpdateNotificationSettings, .UpdateGlobalNotificationSettings, .UpdateSecretChat, .AddSecretMessages, .ReadSecretOutbox, .AddPeerInputActivity, .UpdateCachedPeerData, .UpdatePinnedItemIds, .UpdatePinnedSavedItemIds, .UpdatePinnedTopic, .UpdatePinnedTopicOrder, .ReadMessageContents, .UpdateMessageImpressionCount, .UpdateMessageForwardsCount, .UpdateInstalledStickerPacks, .UpdateRecentGifs, .UpdateChatInputState, .UpdateCall, .AddCallSignalingData, .UpdateLangPack, .UpdateMinAvailableMessage, .UpdateIsContact, .UpdatePeerChatInclusion, .UpdatePeersNearby, .UpdateTheme, .SyncChatListFilters, .UpdateChatListFilter, .UpdateChatListFilterOrder, .UpdateReadThread, .UpdateMessagesPinned, .UpdateGroupCallParticipants, .UpdateGroupCall, .UpdateAutoremoveTimeout, .UpdateAttachMenuBots, .UpdateAudioTranscription, .UpdateConfig, .UpdateExtendedMedia, .ResetForumTopic, .UpdateStory, .UpdateReadStories, .UpdateStoryStealthMode, .UpdateStorySentReaction, .UpdateNewAuthorization, .UpdateWallpaper, .UpdateRevenueBalances, .UpdateStarsBalance, .UpdateStarsRevenueStatus, .UpdateStarsReactionsAreAnonymousByDefault: + case .DeleteMessages, .DeleteMessagesWithGlobalIds, .EditMessage, .UpdateMessagePoll, .UpdateMessageReactions, .UpdateMedia, .MergeApiChats, .MergeApiUsers, .MergePeerPresences, .UpdatePeer, .ReadInbox, .ReadOutbox, .ReadGroupFeedInbox, .ResetReadState, .ResetIncomingReadState, .UpdatePeerChatUnreadMark, .ResetMessageTagSummary, .UpdateNotificationSettings, .UpdateGlobalNotificationSettings, .UpdateSecretChat, .AddSecretMessages, .ReadSecretOutbox, .AddPeerInputActivity, .UpdateCachedPeerData, .UpdatePinnedItemIds, .UpdatePinnedSavedItemIds, .UpdatePinnedTopic, .UpdatePinnedTopicOrder, .ReadMessageContents, .UpdateMessageImpressionCount, .UpdateMessageForwardsCount, .UpdateInstalledStickerPacks, .UpdateRecentGifs, .UpdateChatInputState, .UpdateCall, .AddCallSignalingData, .UpdateLangPack, .UpdateMinAvailableMessage, .UpdateIsContact, .UpdatePeerChatInclusion, .UpdatePeersNearby, .UpdateTheme, .SyncChatListFilters, .UpdateChatListFilter, .UpdateChatListFilterOrder, .UpdateReadThread, .UpdateMessagesPinned, .UpdateGroupCallParticipants, .UpdateGroupCall, .UpdateAutoremoveTimeout, .UpdateAttachMenuBots, .UpdateAudioTranscription, .UpdateConfig, .UpdateExtendedMedia, .ResetForumTopic, .UpdateStory, .UpdateReadStories, .UpdateStoryStealthMode, .UpdateStorySentReaction, .UpdateNewAuthorization, .UpdateWallpaper, .UpdateRevenueBalances, .UpdateStarsBalance, .UpdateStarsRevenueStatus, .UpdateStarsReactionsAreAnonymousByDefault, .ReportMessageDelivery: if let currentAddMessages = currentAddMessages, !currentAddMessages.messages.isEmpty { result.append(.AddMessages(currentAddMessages.messages, currentAddMessages.location)) } @@ -3421,6 +3432,7 @@ func replayFinalState( var updatedStarsBalance: [PeerId: StarsAmount] = [:] var updatedStarsRevenueStatus: [PeerId: StarsRevenueStats.Balances] = [:] var updatedStarsReactionsAreAnonymousByDefault: Bool? + var reportMessageDelivery = Set() var holesFromPreviousStateMessageIds: [MessageId] = [] var clearHolesFromPreviousStateForChannelMessagesWithPts: [PeerIdAndMessageNamespace: Int32] = [:] @@ -4855,6 +4867,8 @@ func replayFinalState( updatedStarsRevenueStatus[peerId] = status case let .UpdateStarsReactionsAreAnonymousByDefault(value): updatedStarsReactionsAreAnonymousByDefault = value + case let .ReportMessageDelivery(messageIds): + reportMessageDelivery = Set(messageIds) } } @@ -5376,6 +5390,7 @@ func replayFinalState( updatedRevenueBalances: updatedRevenueBalances, updatedStarsBalance: updatedStarsBalance, updatedStarsRevenueStatus: updatedStarsRevenueStatus, - sentScheduledMessageIds: finalState.state.sentScheduledMessageIds + sentScheduledMessageIds: finalState.state.sentScheduledMessageIds, + reportMessageDelivery: reportMessageDelivery ) } diff --git a/submodules/TelegramCore/Sources/State/AccountStateManager.swift b/submodules/TelegramCore/Sources/State/AccountStateManager.swift index d975eef92f..d6494b0923 100644 --- a/submodules/TelegramCore/Sources/State/AccountStateManager.swift +++ b/submodules/TelegramCore/Sources/State/AccountStateManager.swift @@ -352,6 +352,7 @@ public final class AccountStateManager { private let appliedMaxMessageIdDisposable = MetaDisposable() private let appliedQtsPromise = Promise(nil) private let appliedQtsDisposable = MetaDisposable() + private let reportMessageDeliveryDisposable = DisposableSet() let updateConfigRequested: (() -> Void)? let isPremiumUpdated: (() -> Void)? @@ -391,6 +392,7 @@ public final class AccountStateManager { self.operationDisposable.dispose() self.appliedMaxMessageIdDisposable.dispose() self.appliedQtsDisposable.dispose() + self.reportMessageDeliveryDisposable.dispose() } public func reset() { @@ -1130,6 +1132,9 @@ public final class AccountStateManager { if !events.sentScheduledMessageIds.isEmpty { strongSelf.sentScheduledMessageIdsPipe.putNext(events.sentScheduledMessageIds) } + if !events.reportMessageDelivery.isEmpty { + strongSelf.reportMessageDeliveryDisposable.add(_internal_reportMessageDelivery(postbox: strongSelf.postbox, network: strongSelf.network, messageIds: Array(events.reportMessageDelivery), fromPushNotification: false).start()) + } if !events.isContactUpdates.isEmpty { strongSelf.addIsContactUpdates(events.isContactUpdates) } diff --git a/submodules/TelegramCore/Sources/SyncCore/ReportDeliveryMessageAttribute.swift b/submodules/TelegramCore/Sources/SyncCore/ReportDeliveryMessageAttribute.swift index 7f9ce3a540..489a5014d9 100644 --- a/submodules/TelegramCore/Sources/SyncCore/ReportDeliveryMessageAttribute.swift +++ b/submodules/TelegramCore/Sources/SyncCore/ReportDeliveryMessageAttribute.swift @@ -4,24 +4,20 @@ import TelegramApi public final class ReportDeliveryMessageAttribute: Equatable, MessageAttribute { public let untilDate: Int32 - public let isReported: Bool public init(untilDate: Int32, isReported: Bool) { self.untilDate = untilDate - self.isReported = isReported } required public init(decoder: PostboxDecoder) { self.untilDate = decoder.decodeInt32ForKey("d", orElse: 0) - self.isReported = decoder.decodeBoolForKey("r", orElse: false) } public func encode(_ encoder: PostboxEncoder) { encoder.encodeInt32(self.untilDate, forKey: "d") - encoder.encodeBool(self.isReported, forKey: "r") } public static func ==(lhs: ReportDeliveryMessageAttribute, rhs: ReportDeliveryMessageAttribute) -> Bool { - return lhs.untilDate == rhs.untilDate && lhs.isReported == rhs.isReported + return lhs.untilDate == rhs.untilDate } } diff --git a/submodules/TelegramCore/Sources/SyncCore/SyncCore_TelegramMediaAction.swift b/submodules/TelegramCore/Sources/SyncCore/SyncCore_TelegramMediaAction.swift index bd1dde1685..a0dd0fbc3f 100644 --- a/submodules/TelegramCore/Sources/SyncCore/SyncCore_TelegramMediaAction.swift +++ b/submodules/TelegramCore/Sources/SyncCore/SyncCore_TelegramMediaAction.swift @@ -130,7 +130,7 @@ public enum TelegramMediaActionType: PostboxCoding, Equatable { case paymentRefunded(peerId: PeerId, currency: String, totalAmount: Int64, payload: Data?, transactionId: String) case giftStars(currency: String, amount: Int64, count: Int64, cryptoCurrency: String?, cryptoAmount: Int64?, transactionId: String?) case prizeStars(amount: Int64, isUnclaimed: Bool, boostPeerId: PeerId?, transactionId: String?, giveawayMessageId: MessageId?) - case starGift(gift: StarGift, convertStars: Int64?, text: String?, entities: [MessageTextEntity]?, nameHidden: Bool, savedToProfile: Bool, converted: Bool, upgraded: Bool, canUpgrade: Bool, upgradeStars: Int64?, isRefunded: Bool) + case starGift(gift: StarGift, convertStars: Int64?, text: String?, entities: [MessageTextEntity]?, nameHidden: Bool, savedToProfile: Bool, converted: Bool, upgraded: Bool, canUpgrade: Bool, upgradeStars: Int64?, isRefunded: Bool, upgradeMessageId: Int32?) case starGiftUnique(gift: StarGift, isUpgrade: Bool, isTransferred: Bool, savedToProfile: Bool, canExportDate: Int32?, transferStars: Int64?, isRefunded: Bool) public init(decoder: PostboxDecoder) { @@ -253,7 +253,7 @@ public enum TelegramMediaActionType: PostboxCoding, Equatable { } self = .prizeStars(amount: decoder.decodeInt64ForKey("amount", orElse: 0), isUnclaimed: decoder.decodeBoolForKey("unclaimed", orElse: false), boostPeerId: boostPeerId, transactionId: decoder.decodeOptionalStringForKey("transactionId"), giveawayMessageId: giveawayMessageId) case 44: - self = .starGift(gift: decoder.decodeObjectForKey("gift", decoder: { StarGift(decoder: $0) }) as! StarGift, convertStars: decoder.decodeOptionalInt64ForKey("convertStars"), text: decoder.decodeOptionalStringForKey("text"), entities: decoder.decodeOptionalObjectArrayWithDecoderForKey("entities"), nameHidden: decoder.decodeBoolForKey("nameHidden", orElse: false), savedToProfile: decoder.decodeBoolForKey("savedToProfile", orElse: false), converted: decoder.decodeBoolForKey("converted", orElse: false), upgraded: decoder.decodeBoolForKey("upgraded", orElse: false), canUpgrade: decoder.decodeBoolForKey("canUpgrade", orElse: false), upgradeStars: decoder.decodeOptionalInt64ForKey("upgradeStars"), isRefunded: decoder.decodeBoolForKey("isRefunded", orElse: false)) + self = .starGift(gift: decoder.decodeObjectForKey("gift", decoder: { StarGift(decoder: $0) }) as! StarGift, convertStars: decoder.decodeOptionalInt64ForKey("convertStars"), text: decoder.decodeOptionalStringForKey("text"), entities: decoder.decodeOptionalObjectArrayWithDecoderForKey("entities"), nameHidden: decoder.decodeBoolForKey("nameHidden", orElse: false), savedToProfile: decoder.decodeBoolForKey("savedToProfile", orElse: false), converted: decoder.decodeBoolForKey("converted", orElse: false), upgraded: decoder.decodeBoolForKey("upgraded", orElse: false), canUpgrade: decoder.decodeBoolForKey("canUpgrade", orElse: false), upgradeStars: decoder.decodeOptionalInt64ForKey("upgradeStars"), isRefunded: decoder.decodeBoolForKey("isRefunded", orElse: false), upgradeMessageId: decoder.decodeOptionalInt32ForKey("upgradeMessageId")) case 45: self = .starGiftUnique(gift: decoder.decodeObjectForKey("gift", decoder: { StarGift(decoder: $0) }) as! StarGift, isUpgrade: decoder.decodeBoolForKey("isUpgrade", orElse: false), isTransferred: decoder.decodeBoolForKey("isTransferred", orElse: false), savedToProfile: decoder.decodeBoolForKey("savedToProfile", orElse: false), canExportDate: decoder.decodeOptionalInt32ForKey("canExportDate"), transferStars: decoder.decodeOptionalInt64ForKey("transferStars"), isRefunded: decoder.decodeBoolForKey("isRefunded", orElse: false)) default: @@ -548,7 +548,7 @@ public enum TelegramMediaActionType: PostboxCoding, Equatable { } else { encoder.encodeNil(forKey: "giveawayMsgId") } - case let .starGift(gift, convertStars, text, entities, nameHidden, savedToProfile, converted, upgraded, canUpgrade, upgradeStars, isRefunded): + case let .starGift(gift, convertStars, text, entities, nameHidden, savedToProfile, converted, upgraded, canUpgrade, upgradeStars, isRefunded, upgradeMessageId): encoder.encodeInt32(44, forKey: "_rawValue") encoder.encodeObject(gift, forKey: "gift") if let convertStars { @@ -574,6 +574,11 @@ public enum TelegramMediaActionType: PostboxCoding, Equatable { encoder.encodeNil(forKey: "upgradeStars") } encoder.encodeBool(isRefunded, forKey: "isRefunded") + if let upgradeMessageId { + encoder.encodeInt32(upgradeMessageId, forKey: "upgradeMessageId") + } else { + encoder.encodeNil(forKey: "upgradeMessageId") + } case let .starGiftUnique(gift, isUpgrade, isTransferred, savedToProfile, canExportDate, transferStars, isRefunded): encoder.encodeInt32(45, forKey: "_rawValue") encoder.encodeObject(gift, forKey: "gift") diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Messages/ReportMessageDelivery.swift b/submodules/TelegramCore/Sources/TelegramEngine/Messages/ReportMessageDelivery.swift new file mode 100644 index 0000000000..a52477b9c1 --- /dev/null +++ b/submodules/TelegramCore/Sources/TelegramEngine/Messages/ReportMessageDelivery.swift @@ -0,0 +1,33 @@ +import Foundation +import Postbox +import SwiftSignalKit +import TelegramApi + +public func _internal_reportMessageDelivery(postbox: Postbox, network: Network, messageIds: [EngineMessage.Id], fromPushNotification: Bool) -> Signal { + var signals: [Signal] = [] + for (peerId, messageIds) in messagesIdsGroupedByPeerId(messageIds) { + signals.append(_internal_reportMessageDeliveryByPeerId(postbox: postbox, network: network, peerId: peerId, messageIds: messageIds, fromPushNotification: fromPushNotification)) + } + return combineLatest(signals) + |> ignoreValues +} + +private func _internal_reportMessageDeliveryByPeerId(postbox: Postbox, network: Network, peerId: EnginePeer.Id, messageIds: [EngineMessage.Id], fromPushNotification: Bool) -> Signal { + return postbox.transaction { transaction -> Signal in + guard let peer = transaction.getPeer(peerId), let inputPeer = apiInputPeer(peer) else { + return .complete() + } + var flags: Int32 = 0 + if fromPushNotification { + flags |= (1 << 0) + } + return network.request(Api.functions.messages.reportMessagesDelivery(flags: flags, peer: inputPeer, id: messageIds.map { $0.id })) + |> `catch` { error -> Signal in + return .single(.boolFalse) + } + |> mapToSignal { _ in + return .complete() + } + } + |> switchToLatest +} diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Payments/BotPaymentForm.swift b/submodules/TelegramCore/Sources/TelegramEngine/Payments/BotPaymentForm.swift index a64a77a28b..c5ae9f3fce 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Payments/BotPaymentForm.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Payments/BotPaymentForm.swift @@ -175,6 +175,7 @@ extension BotPaymentMethod { public enum BotPaymentFormRequestError { case generic case alreadyActive + case noPaymentNeeded } extension BotPaymentInvoice { @@ -457,7 +458,10 @@ func _internal_fetchBotPaymentForm(accountPeerId: PeerId, postbox: Postbox, netw } return network.request(Api.functions.payments.getPaymentForm(flags: flags, invoice: invoice, themeParams: serializedThemeParams)) - |> `catch` { _ -> Signal in + |> `catch` { error -> Signal in + if error.errorDescription == "NO_PAYMENT_NEEDED" { + return .fail(.noPaymentNeeded) + } return .fail(.generic) } |> mapToSignal { result -> Signal in @@ -622,7 +626,7 @@ public enum SendBotPaymentFormError { } public enum SendBotPaymentResult { - case done(receiptMessageId: MessageId?, subscriptionPeerId: PeerId?) + case done(receiptMessageId: MessageId?, subscriptionPeerId: PeerId?, uniqueStarGift: ProfileGiftsContext.State.StarGift?) case externalVerificationRequired(url: String) } @@ -671,12 +675,12 @@ func _internal_sendBotPaymentForm(account: Account, formId: Int64, source: BotPa case .starsChatSubscription: let chats = updates.chats.compactMap { parseTelegramGroupOrChannel(chat: $0) } if let first = chats.first { - return .done(receiptMessageId: nil, subscriptionPeerId: first.id) + return .done(receiptMessageId: nil, subscriptionPeerId: first.id, uniqueStarGift: nil) } default: break } - + for apiMessage in updates.messages { if let message = StoreMessage(apiMessage: apiMessage, accountPeerId: account.peerId, peerIsForum: false) { for media in message.media { @@ -721,7 +725,7 @@ func _internal_sendBotPaymentForm(account: Account, formId: Int64, source: BotPa } } } - return .done(receiptMessageId: receiptMessageId, subscriptionPeerId: nil) + return .done(receiptMessageId: receiptMessageId, subscriptionPeerId: nil, uniqueStarGift: nil) case let .paymentVerificationNeeded(url): return .externalVerificationRequired(url: url) } diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Payments/StarGifts.swift b/submodules/TelegramCore/Sources/TelegramEngine/Payments/StarGifts.swift index ecd058ecb1..1ddc78009f 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Payments/StarGifts.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Payments/StarGifts.swift @@ -689,15 +689,23 @@ func _internal_transferStarGift(account: Account, prepaid: Bool, messageId: Engi } else { let source: BotPaymentInvoiceSource = .starGiftTransfer(messageId: messageId, toPeerId: peerId) return _internal_fetchBotPaymentForm(accountPeerId: account.peerId, postbox: account.postbox, network: account.network, source: source, themeParams: nil) - |> mapError { _ -> TransferStarGiftError in - return .generic + |> map(Optional.init) + |> `catch` { error -> Signal in + if case .noPaymentNeeded = error { + return .single(nil) + } + return .fail(.generic) } |> mapToSignal { paymentForm in - return _internal_sendStarsPaymentForm(account: account, formId: paymentForm.id, source: source) - |> mapError { _ -> TransferStarGiftError in - return .generic + if let paymentForm { + return _internal_sendStarsPaymentForm(account: account, formId: paymentForm.id, source: source) + |> mapError { _ -> TransferStarGiftError in + return .generic + } + |> ignoreValues + } else { + return _internal_transferStarGift(account: account, prepaid: true, messageId: messageId, peerId: peerId) } - |> ignoreValues } } } @@ -714,8 +722,12 @@ func _internal_upgradeStarGift(account: Account, formId: Int64?, messageId: Engi |> mapError { _ -> UpgradeStarGiftError in return .generic } - |> mapToSignal { _ in - return .complete() + |> mapToSignal { result in + if case let .done(_, _, gift) = result, let gift { + return .single(gift) + } else { + return .complete() + } } } else { var flags: Int32 = 0 @@ -970,16 +982,30 @@ private final class ProfileGiftsContextImpl { self.pushState() } - func upgradeStarGift(formId: Int64?, messageId: EngineMessage.Id, keepOriginalInfo: Bool) { - self.actionDisposable.set( - _internal_upgradeStarGift(account: self.account, formId: formId, messageId: messageId, keepOriginalInfo: keepOriginalInfo).startStrict(next: { [weak self] result in - guard let self else { - return - } - let _ = self - }) - ) - self.pushState() + func upgradeStarGift(formId: Int64?, messageId: EngineMessage.Id, keepOriginalInfo: Bool) -> Signal { + return Signal { [weak self] subscriber in + guard let self else { + return EmptyDisposable + } + let disposable = MetaDisposable() + disposable.set( + _internal_upgradeStarGift(account: self.account, formId: formId, messageId: messageId, keepOriginalInfo: keepOriginalInfo).startStrict(next: { [weak self] result in + guard let self else { + return + } + if let index = self.gifts.firstIndex(where: { $0.messageId == messageId }) { + self.gifts[index] = result + self.pushState() + } + subscriber.putNext(result) + }, error: { error in + subscriber.putError(error) + }, completed: { + subscriber.putCompletion() + }) + ) + return disposable + } } private func pushState() { @@ -1186,9 +1212,19 @@ public final class ProfileGiftsContext { } } - public func upgradeStarGift(formId: Int64?, messageId: EngineMessage.Id, keepOriginalInfo: Bool) { - self.impl.with { impl in - impl.upgradeStarGift(formId: formId, messageId: messageId, keepOriginalInfo: keepOriginalInfo) + public func upgradeStarGift(formId: Int64?, messageId: EngineMessage.Id, keepOriginalInfo: Bool) -> Signal { + return Signal { subscriber in + let disposable = MetaDisposable() + self.impl.with { impl in + disposable.set(impl.upgradeStarGift(formId: formId, messageId: messageId, keepOriginalInfo: keepOriginalInfo).start(next: { value in + subscriber.putNext(value) + }, error: { error in + subscriber.putError(error) + }, completed: { + subscriber.putCompletion() + })) + } + return disposable } } diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Payments/Stars.swift b/submodules/TelegramCore/Sources/TelegramEngine/Payments/Stars.swift index ed4e4f3220..2fb2492280 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Payments/Stars.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Payments/Stars.swift @@ -1418,12 +1418,13 @@ func _internal_sendStarsPaymentForm(account: Account, formId: Int64, source: Bot case .starsChatSubscription: let chats = updates.chats.compactMap { parseTelegramGroupOrChannel(chat: $0) } if let first = chats.first { - return .done(receiptMessageId: nil, subscriptionPeerId: first.id) + return .done(receiptMessageId: nil, subscriptionPeerId: first.id, uniqueStarGift: nil) } default: break } var receiptMessageId: MessageId? + var resultGift: ProfileGiftsContext.State.StarGift? for apiMessage in updates.messages { if let message = StoreMessage(apiMessage: apiMessage, accountPeerId: account.peerId, peerIsForum: false) { for media in message.media { @@ -1463,12 +1464,28 @@ func _internal_sendStarsPaymentForm(account: Account, formId: Int64, source: Bot case .giftCode, .stars, .starsGift, .starsChatSubscription, .starGift, .starGiftUpgrade, .starGiftTransfer: receiptMessageId = nil } + } else if case let .starGiftUnique(gift, _, _, savedToProfile, canExportDate, transferStars, _) = action.action, case let .Id(messageId) = message.id { + resultGift = ProfileGiftsContext.State.StarGift( + gift: gift, + fromPeer: nil, + date: message.timestamp, + text: nil, + entities: nil, + messageId: messageId, + nameHidden: false, + savedToProfile: savedToProfile, + convertStars: nil, + canUpgrade: false, + canExportDate: canExportDate, + upgradeStars: nil, + transferStars: transferStars + ) } } } } } - return .done(receiptMessageId: receiptMessageId, subscriptionPeerId: nil) + return .done(receiptMessageId: receiptMessageId, subscriptionPeerId: nil, uniqueStarGift: resultGift) case let .paymentVerificationNeeded(url): return .externalVerificationRequired(url: url) } diff --git a/submodules/TelegramStringFormatting/Sources/ServiceMessageStrings.swift b/submodules/TelegramStringFormatting/Sources/ServiceMessageStrings.swift index d00b9ac2e9..35164045da 100644 --- a/submodules/TelegramStringFormatting/Sources/ServiceMessageStrings.swift +++ b/submodules/TelegramStringFormatting/Sources/ServiceMessageStrings.swift @@ -1066,7 +1066,7 @@ public func universalServiceMessageString(presentationData: (PresentationTheme, attributedString = mutableString case .prizeStars: attributedString = NSAttributedString(string: strings.Notification_StarsPrize, font: titleFont, textColor: primaryTextColor) - case let .starGift(gift, _, text, entities, _, _, _, _, _, upgradeStars, _): + case let .starGift(gift, _, text, entities, _, _, _, _, _, upgradeStars, _, _): if !forAdditionalServiceMessage { if let text { let mutableAttributedString = NSMutableAttributedString(attributedString: stringWithAppliedEntities(text, entities: entities ?? [], baseColor: primaryTextColor, linkColor: primaryTextColor, baseFont: titleFont, linkFont: titleBoldFont, boldFont: titleBoldFont, italicFont: titleFont, boldItalicFont: titleBoldFont, fixedFont: titleFont, blockQuoteFont: titleFont, underlineLinks: false, message: message._asMessage())) diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageGiftBubbleContentNode/Sources/ChatMessageGiftBubbleContentNode.swift b/submodules/TelegramUI/Components/Chat/ChatMessageGiftBubbleContentNode/Sources/ChatMessageGiftBubbleContentNode.swift index 40f72151cc..1007e3a39d 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessageGiftBubbleContentNode/Sources/ChatMessageGiftBubbleContentNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessageGiftBubbleContentNode/Sources/ChatMessageGiftBubbleContentNode.swift @@ -467,7 +467,7 @@ public class ChatMessageGiftBubbleContentNode: ChatMessageBubbleContentNode { buttonTitle = item.presentationData.strings.Notification_PremiumPrize_View hasServiceMessage = false } - case let .starGift(gift, convertStars, giftText, giftEntities, _, savedToProfile, converted, upgraded, _, upgradeStars, isRefunded): + case let .starGift(gift, convertStars, giftText, giftEntities, _, savedToProfile, converted, upgraded, _, upgradeStars, isRefunded, _): if case let .generic(gift) = gift { isStarGift = true let authorName = item.message.author.flatMap { EnginePeer($0) }?.compactDisplayTitle ?? "" @@ -488,7 +488,7 @@ public class ChatMessageGiftBubbleContentNode: ChatMessageBubbleContentNode { text = item.presentationData.strings.Notification_StarGift_Bot_Subtitle_Displaying } } else { - if let convertStars { + if let convertStars, convertStars > 0 { text = item.presentationData.strings.Notification_StarGift_Subtitle(item.presentationData.strings.Notification_StarGift_Subtitle_Stars(Int32(convertStars))).string } else { text = item.presentationData.strings.Notification_StarGift_Bot_Subtitle @@ -500,16 +500,20 @@ public class ChatMessageGiftBubbleContentNode: ChatMessageBubbleContentNode { peerName = EnginePeer(peer).compactDisplayTitle } if peerName.isEmpty { - if let convertStars { + if let convertStars, convertStars > 0 { text = item.presentationData.strings.Notification_StarGift_Subtitle(item.presentationData.strings.Notification_StarGift_Subtitle_Stars(Int32(convertStars))).string } else { text = item.presentationData.strings.Notification_StarGift_Bot_Subtitle } } else { - let formattedString = item.presentationData.strings.Notification_StarGift_Subtitle_Other(peerName, item.presentationData.strings.Notification_StarGift_Subtitle_Other_Stars(Int32(convertStars ?? 0))) - text = formattedString.string - if let starsRange = formattedString.ranges.last { - entities.append(MessageTextEntity(range: starsRange.range.lowerBound ..< starsRange.range.upperBound, type: .Bold)) + if let convertStars, convertStars > 0 { + let formattedString = item.presentationData.strings.Notification_StarGift_Subtitle_Other(peerName, item.presentationData.strings.Notification_StarGift_Subtitle_Other_Stars(Int32(convertStars))) + text = formattedString.string + if let starsRange = formattedString.ranges.last { + entities.append(MessageTextEntity(range: starsRange.range.lowerBound ..< starsRange.range.upperBound, type: .Bold)) + } + } else { + } } } @@ -524,17 +528,22 @@ public class ChatMessageGiftBubbleContentNode: ChatMessageBubbleContentNode { } ribbonTitle = item.presentationData.strings.Notification_StarGift_OneOf(availabilityString).string } - if incoming, let upgradeStars, upgradeStars > 0, !upgraded { + if incoming || item.presentationData.isPreview, let upgradeStars, upgradeStars > 0, !upgraded { buttonTitle = item.presentationData.strings.Notification_StarGift_Unpack buttonIcon = "Premium/GiftUnpack" } else { buttonTitle = item.presentationData.strings.Notification_StarGift_View } } - case let .starGiftUnique(gift, _, _, _, _, _, isRefunded): + case let .starGiftUnique(gift, isUpgrade, _, _, _, _, isRefunded): if case let .unique(uniqueGift) = gift { isStarGift = true - let authorName = item.message.author.flatMap { EnginePeer($0) }?.compactDisplayTitle ?? "" + 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 ?? "" + } 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 diff --git a/submodules/TelegramUI/Components/Gifts/GiftAnimationComponent/BUILD b/submodules/TelegramUI/Components/Gifts/GiftAnimationComponent/BUILD index 2fec2704be..518a36b234 100644 --- a/submodules/TelegramUI/Components/Gifts/GiftAnimationComponent/BUILD +++ b/submodules/TelegramUI/Components/Gifts/GiftAnimationComponent/BUILD @@ -24,6 +24,9 @@ swift_library( "//submodules/PresentationDataUtils", "//submodules/TextFormat", "//submodules/TelegramUI/Components/EmojiTextAttachmentView", + "//submodules/TelegramUI/Components/PeerInfo/PeerInfoCoverComponent", + "//submodules/AnimatedStickerNode", + "//submodules/TelegramAnimatedStickerNode", ], visibility = [ "//visibility:public", diff --git a/submodules/TelegramUI/Components/Gifts/GiftAnimationComponent/Sources/GiftAnimationComponent.swift b/submodules/TelegramUI/Components/Gifts/GiftAnimationComponent/Sources/GiftAnimationComponent.swift index c5d161e3de..d6ad0989b9 100644 --- a/submodules/TelegramUI/Components/Gifts/GiftAnimationComponent/Sources/GiftAnimationComponent.swift +++ b/submodules/TelegramUI/Components/Gifts/GiftAnimationComponent/Sources/GiftAnimationComponent.swift @@ -13,15 +13,21 @@ public final class GiftAnimationComponent: Component { let context: AccountContext let theme: PresentationTheme let file: TelegramMediaFile? + let still: Bool + let size: CGSize? public init( context: AccountContext, theme: PresentationTheme, - file: TelegramMediaFile? + file: TelegramMediaFile?, + still: Bool = false, + size: CGSize? = nil ) { self.context = context self.theme = theme self.file = file + self.still = still + self.size = size } public static func ==(lhs: GiftAnimationComponent, rhs: GiftAnimationComponent) -> Bool { @@ -34,6 +40,12 @@ public final class GiftAnimationComponent: Component { if lhs.file != rhs.file { return false } + if lhs.still != rhs.still { + return false + } + if lhs.size != rhs.size { + return false + } return true } @@ -61,7 +73,7 @@ public final class GiftAnimationComponent: Component { file: component.file ) - let iconSize = availableSize + let iconSize = component.size ?? availableSize if self.animationLayer == nil { let animationLayer = InlineStickerItemLayer( context: .account(component.context), @@ -71,12 +83,12 @@ public final class GiftAnimationComponent: Component { file: component.file, cache: component.context.animationCache, renderer: component.context.animationRenderer, - unique: true, + unique: !component.still, placeholderColor: component.theme.list.mediaPlaceholderColor, pointSize: CGSize(width: iconSize.width * 1.2, height: iconSize.height * 1.2), - loopCount: 1 + loopCount: component.still ? 0 : 1 ) - animationLayer.isVisibleForAnimations = true + animationLayer.isVisibleForAnimations = !component.still self.animationLayer = animationLayer self.layer.addSublayer(animationLayer) } diff --git a/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftCompositionComponent.swift b/submodules/TelegramUI/Components/Gifts/GiftAnimationComponent/Sources/GiftCompositionComponent.swift similarity index 82% rename from submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftCompositionComponent.swift rename to submodules/TelegramUI/Components/Gifts/GiftAnimationComponent/Sources/GiftCompositionComponent.swift index 8f084fc3f8..1efa05a41b 100644 --- a/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftCompositionComponent.swift +++ b/submodules/TelegramUI/Components/Gifts/GiftAnimationComponent/Sources/GiftCompositionComponent.swift @@ -13,7 +13,7 @@ import PeerInfoCoverComponent import AnimatedStickerNode import TelegramAnimatedStickerNode -final class GiftCompositionComponent: Component { +public final class GiftCompositionComponent: Component { public class ExternalState { public fileprivate(set) var previewPatternColor: UIColor? public init() { @@ -21,7 +21,7 @@ final class GiftCompositionComponent: Component { } } - enum Subject: Equatable { + public enum Subject: Equatable { case generic(TelegramMediaFile) case unique(StarGift.UniqueGift) case preview([StarGift.UniqueGift.Attribute]) @@ -30,15 +30,15 @@ final class GiftCompositionComponent: Component { let context: AccountContext let theme: PresentationTheme let subject: Subject - let externalState: ExternalState + let externalState: ExternalState? let requestUpdate: () -> Void - init( + public init( context: AccountContext, theme: PresentationTheme, subject: Subject, - externalState: ExternalState, - requestUpdate: @escaping () -> Void + externalState: ExternalState? = nil, + requestUpdate: @escaping () -> Void = {} ) { self.context = context self.theme = theme @@ -47,7 +47,7 @@ final class GiftCompositionComponent: Component { self.requestUpdate = requestUpdate } - static func ==(lhs: GiftCompositionComponent, rhs: GiftCompositionComponent) -> Bool { + public static func ==(lhs: GiftCompositionComponent, rhs: GiftCompositionComponent) -> Bool { if lhs.context !== rhs.context { return false } @@ -60,7 +60,7 @@ final class GiftCompositionComponent: Component { return true } - final class View: UIView { + public final class View: UIView { private var component: GiftCompositionComponent? private weak var componentState: EmptyComponentState? @@ -84,6 +84,8 @@ final class GiftCompositionComponent: Component { override init(frame: CGRect) { super.init(frame: frame) + + self.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.handleTap))) } required init?(coder: NSCoder) { @@ -94,7 +96,16 @@ final class GiftCompositionComponent: Component { self.disposables.dispose() } - func update(component: GiftCompositionComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { + @objc private func handleTap() { + guard let animationNode = animationNode as? DefaultAnimatedStickerNodeImpl else { + return + } + if case .once = animationNode.playbackMode, !animationNode.isPlaying { + animationNode.playOnce() + } + } + + public func update(component: GiftCompositionComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { let previousComponent = self.component self.component = component @@ -107,6 +118,7 @@ final class GiftCompositionComponent: Component { var patternFile: TelegramMediaFile? var files: [Int64: TelegramMediaFile] = [:] + var loop = false switch component.subject { case let .generic(file): animationFile = file @@ -142,6 +154,8 @@ final class GiftCompositionComponent: Component { self.previewTimer = nil } case let .preview(sampleAttributes): + loop = true + if self.previewModels.isEmpty { var models: [StarGift.UniqueGift.Attribute] = [] var patterns: [StarGift.UniqueGift.Attribute] = [] @@ -198,8 +212,21 @@ final class GiftCompositionComponent: Component { return } self.previewModelIndex = (self.previewModelIndex + 1) % Int32(self.previewModels.count) - self.previewPatternIndex = (self.previewPatternIndex + 1) % Int32(self.previewPatterns.count) - self.previewBackdropIndex = (self.previewBackdropIndex + 1) % Int32(self.previewBackdrops.count) + + let previousPatternIndex = self.previewPatternIndex + var randomPatternIndex = previousPatternIndex + while randomPatternIndex == previousPatternIndex { + randomPatternIndex = Int32.random(in: 0 ..< Int32(self.previewPatterns.count)) + } + self.previewPatternIndex = randomPatternIndex + + let previousBackdropIndex = self.previewBackdropIndex + var randomBackdropIndex = previousBackdropIndex + while randomBackdropIndex == previousBackdropIndex { + randomBackdropIndex = Int32.random(in: 0 ..< Int32(self.previewBackdrops.count)) + } + self.previewBackdropIndex = randomBackdropIndex + self.animatePreviewTransition = true self.componentState?.updated(transition: .easeInOut(duration: 0.25)) }, queue: Queue.mainQueue()) @@ -207,7 +234,7 @@ final class GiftCompositionComponent: Component { } } - component.externalState.previewPatternColor = secondBackgroundColor + component.externalState?.previewPatternColor = secondBackgroundColor var animateTransition = false if self.animatePreviewTransition { @@ -247,6 +274,7 @@ final class GiftCompositionComponent: Component { if backgroundView.superview == nil { backgroundTransition = .immediate backgroundView.clipsToBounds = true + backgroundView.isUserInteractionEnabled = false self.insertSubview(backgroundView, at: 0) backgroundView.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.25) @@ -259,7 +287,7 @@ final class GiftCompositionComponent: Component { }) } - let iconSize = CGSize(width: 128.0, height: 128.0) + let iconSize = CGSize(width: 136.0, height: 136.0) var startFromIndex: Int? if animateTransition, let disappearingAnimationNode = self.animationNode { @@ -274,17 +302,25 @@ final class GiftCompositionComponent: Component { let animationNode: AnimatedStickerNode if self.animationNode == nil { animationNode = DefaultAnimatedStickerNodeImpl() + animationNode.isUserInteractionEnabled = false self.animationNode = animationNode self.addSubview(animationNode.view) let pathPrefix = component.context.account.postbox.mediaBox.shortLivedResourceCachePathPrefix(file.resource.id) animationNode.setup(source: AnimatedStickerResourceSource(account: component.context.account, resource: file.resource, isVideo: file.isVideoSticker), width: Int(iconSize.width * 1.6), height: Int(iconSize.height * 1.6), playbackMode: .loop, mode: .direct(cachePathPrefix: pathPrefix)) - + if let startFromIndex { + if let animationNode = animationNode as? DefaultAnimatedStickerNodeImpl { + animationNode.playbackMode = loop ? .loop : .once + } animationNode.play(firstFrame: false, fromIndex: startFromIndex) } else { - animationNode.playLoop() + if loop { + animationNode.playLoop() + } else { + animationNode.playOnce() + } } animationNode.visibility = true animationNode.updateLayout(size: iconSize) @@ -295,7 +331,7 @@ 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: 25.0), size: iconSize)) + transition.setFrame(layer: animationNode.layer, frame: CGRect(origin: CGPoint(x: floorToScreenPixels((availableSize.width - iconSize.width) / 2.0), y: 20.0), size: iconSize)) } return availableSize diff --git a/submodules/TelegramUI/Components/Gifts/GiftSetupScreen/Sources/ChatGiftPreviewItem.swift b/submodules/TelegramUI/Components/Gifts/GiftSetupScreen/Sources/ChatGiftPreviewItem.swift index f7b3331e93..f1e0c80cce 100644 --- a/submodules/TelegramUI/Components/Gifts/GiftSetupScreen/Sources/ChatGiftPreviewItem.swift +++ b/submodules/TelegramUI/Components/Gifts/GiftSetupScreen/Sources/ChatGiftPreviewItem.swift @@ -227,7 +227,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 ? gift.upgradeStars : 0, isRefunded: false) + 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) ) ] } diff --git a/submodules/TelegramUI/Components/Gifts/GiftSetupScreen/Sources/GiftSetupScreen.swift b/submodules/TelegramUI/Components/Gifts/GiftSetupScreen/Sources/GiftSetupScreen.swift index e382678172..a5e6cf3e74 100644 --- a/submodules/TelegramUI/Components/Gifts/GiftSetupScreen/Sources/GiftSetupScreen.swift +++ b/submodules/TelegramUI/Components/Gifts/GiftSetupScreen/Sources/GiftSetupScreen.swift @@ -139,7 +139,7 @@ final class GiftSetupScreenComponent: Component { override init(frame: CGRect) { self.scrollView = ScrollView() - self.scrollView.showsVerticalScrollIndicator = true + self.scrollView.showsVerticalScrollIndicator = false self.scrollView.showsHorizontalScrollIndicator = false self.scrollView.scrollsToTop = false self.scrollView.delaysContentTouches = false @@ -253,6 +253,10 @@ final class GiftSetupScreenComponent: Component { |> deliverOnMainQueue).start(next: { [weak self] status in if let completion { completion() + + if let self, let controller = self.environment?.controller() { + controller.dismiss() + } } else { guard let self, case .purchased = status, let controller = self.environment?.controller(), let navigationController = controller.navigationController as? NavigationController else { return @@ -631,7 +635,6 @@ final class GiftSetupScreenComponent: Component { transition.setFrame(view: navigationTitleView, frame: navigationTitleFrame) } - let bottomContentInset: CGFloat = 24.0 let sideInset: CGFloat = 16.0 + environment.safeInsets.left let sectionSpacing: CGFloat = 24.0 @@ -842,8 +845,8 @@ final class GiftSetupScreenComponent: Component { body: MarkdownAttributeSet(font: Font.regular(13.0), textColor: environment.theme.list.freeTextColor), bold: MarkdownAttributeSet(font: Font.semibold(13.0), textColor: environment.theme.list.freeTextColor), link: MarkdownAttributeSet(font: Font.regular(13.0), textColor: environment.theme.list.itemAccentColor), - linkAttribute: { url in - return ("URL", url) + linkAttribute: { contents in + return (TelegramTextAttributes.URL, contents) })) let upgradeFooterText = NSMutableAttributedString(attributedString: parsedString) @@ -986,19 +989,19 @@ final class GiftSetupScreenComponent: Component { contentHeight += hideSectionSize.height } - contentHeight += bottomContentInset + contentHeight += 24.0 - let combinedBottomInset = max(inputHeight, environment.safeInsets.bottom) - contentHeight += combinedBottomInset - - if self.starImage == nil || self.starImage?.1 !== environment.theme { - self.starImage = (generateTintedImage(image: UIImage(bundleImageName: "Item List/PremiumIcon"), color: environment.theme.list.itemCheckColors.foregroundColor)!, environment.theme) - } - let buttonHeight: CGFloat = 50.0 let bottomPanelPadding: CGFloat = 12.0 let bottomInset: CGFloat = environment.safeInsets.bottom > 0.0 ? environment.safeInsets.bottom + 5.0 : bottomPanelPadding let bottomPanelHeight = bottomPanelPadding + buttonHeight + bottomInset + + let combinedBottomInset = max(inputHeight, environment.safeInsets.bottom) + contentHeight += max(bottomPanelHeight, combinedBottomInset) + + if self.starImage == nil || self.starImage?.1 !== environment.theme { + self.starImage = (generateTintedImage(image: UIImage(bundleImageName: "Item List/PremiumIcon"), color: environment.theme.list.itemCheckColors.foregroundColor)!, environment.theme) + } let bottomPanelSize = self.buttonBackground.update( transition: transition, diff --git a/submodules/TelegramUI/Components/Gifts/GiftViewScreen/BUILD b/submodules/TelegramUI/Components/Gifts/GiftViewScreen/BUILD index 713eeb0674..942da7286a 100644 --- a/submodules/TelegramUI/Components/Gifts/GiftViewScreen/BUILD +++ b/submodules/TelegramUI/Components/Gifts/GiftViewScreen/BUILD @@ -37,12 +37,9 @@ swift_library( "//submodules/TelegramUI/Components/Stars/StarsAvatarComponent", "//submodules/TelegramUI/Components/EmojiTextAttachmentView", "//submodules/TelegramUI/Components/Gifts/GiftAnimationComponent", - "//submodules/TelegramUI/Components/PeerInfo/PeerInfoCoverComponent", "//submodules/TelegramUI/Components/CheckComponent", "//submodules/UndoUI", "//submodules/ConfettiEffect", - "//submodules/AnimatedStickerNode:AnimatedStickerNode", - "//submodules/TelegramAnimatedStickerNode:TelegramAnimatedStickerNode", "//submodules/TooltipUI", "//submodules/TelegramUI/Components/Gifts/GiftItemComponent", ], diff --git a/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftTransferAlertController.swift b/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftTransferAlertController.swift index ae5f5e87ef..bbb0392c32 100644 --- a/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftTransferAlertController.swift +++ b/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftTransferAlertController.swift @@ -259,7 +259,7 @@ public func giftTransferAlertController(context: AccountContext, gift: StarGift. let buttonText: String if transferStars > 0 { text = strings.Gift_Transfer_Confirmation_Text("\(gift.title) #\(gift.number)", peer.displayTitle(strings: strings, displayOrder: presentationData.nameDisplayOrder), strings.Gift_Transfer_Confirmation_Text_Stars(Int32(transferStars))).string - buttonText = "\(strings.Gift_Transfer_Confirmation_Transfer) ⭐️\(transferStars)" + buttonText = "\(strings.Gift_Transfer_Confirmation_Transfer) $ \(transferStars)" } else { text = strings.Gift_Transfer_Confirmation_TextFree("\(gift.title) #\(gift.number)", peer.displayTitle(strings: strings, displayOrder: presentationData.nameDisplayOrder)).string buttonText = strings.Gift_Transfer_Confirmation_TransferFree diff --git a/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftViewScreen.swift b/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftViewScreen.swift index 75cbba5bbb..9c5f2e592d 100644 --- a/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftViewScreen.swift +++ b/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftViewScreen.swift @@ -27,6 +27,7 @@ import ConfettiEffect import PlainButtonComponent import CheckComponent import TooltipUI +import GiftAnimationComponent private let modelButtonTag = GenericComponentViewTag() private let backdropButtonTag = GenericComponentViewTag() @@ -45,6 +46,7 @@ private final class GiftViewSheetContent: CombinedComponent { let sendGift: (EnginePeer.Id) -> Void let openMyGifts: () -> Void let transferGift: () -> Void + let upgradeGift: ((Int64?, Bool) -> Signal) let showAttributeInfo: (Any, Float) -> Void let getController: () -> ViewController? @@ -59,6 +61,7 @@ private final class GiftViewSheetContent: CombinedComponent { sendGift: @escaping (EnginePeer.Id) -> Void, openMyGifts: @escaping () -> Void, transferGift: @escaping () -> Void, + upgradeGift: @escaping ((Int64?, Bool) -> Signal), showAttributeInfo: @escaping (Any, Float) -> Void, getController: @escaping () -> ViewController? ) { @@ -72,6 +75,7 @@ private final class GiftViewSheetContent: CombinedComponent { self.sendGift = sendGift self.openMyGifts = openMyGifts self.transferGift = transferGift + self.upgradeGift = upgradeGift self.showAttributeInfo = showAttributeInfo self.getController = getController } @@ -88,7 +92,8 @@ private final class GiftViewSheetContent: CombinedComponent { final class State: ComponentState { private let context: AccountContext - var subject: GiftViewScreen.Subject + private(set) var subject: GiftViewScreen.Subject + private let upgradeGift: ((Int64?, Bool) -> Signal) private let getController: () -> ViewController? private var disposable: Disposable? @@ -109,6 +114,7 @@ private final class GiftViewSheetContent: CombinedComponent { var inUpgradePreview = false var upgradeForm: BotPaymentForm? + var upgradeFormDisposable: Disposable? var upgradeDisposable: Disposable? var sampleGiftAttributes: [StarGift.UniqueGift.Attribute]? @@ -130,14 +136,24 @@ private final class GiftViewSheetContent: CombinedComponent { var upgradedMockBackgroundColor: UIColor = .white var upgradedMockIcon: TelegramMediaFile? - init(context: AccountContext, subject: GiftViewScreen.Subject, getController: @escaping () -> ViewController?) { + init( + context: AccountContext, + subject: GiftViewScreen.Subject, + upgradeGift: @escaping ((Int64?, Bool) -> Signal), + getController: @escaping () -> ViewController? + ) { self.context = context self.subject = subject + self.upgradeGift = upgradeGift self.getController = getController super.init() if let arguments = subject.arguments { + if let upgradeStars = arguments.upgradeStars, upgradeStars > 0 { + self.keepOriginalInfo = true + } + var peerIds: [EnginePeer.Id] = [arguments.peerId, context.account.peerId] if let fromPeerId = arguments.fromPeerId, !peerIds.contains(fromPeerId) { peerIds.append(fromPeerId) @@ -177,7 +193,7 @@ private final class GiftViewSheetContent: CombinedComponent { })) if arguments.upgradeStars == nil, let messageId = arguments.messageId { - self.upgradeDisposable = (context.engine.payments.fetchBotPaymentForm(source: .starGiftUpgrade(keepOriginalInfo: false, messageId: messageId), themeParams: nil) + self.upgradeFormDisposable = (context.engine.payments.fetchBotPaymentForm(source: .starGiftUpgrade(keepOriginalInfo: false, messageId: messageId), themeParams: nil) |> deliverOnMainQueue).start(next: { [weak self] paymentForm in guard let self else { return @@ -237,11 +253,12 @@ private final class GiftViewSheetContent: CombinedComponent { deinit { self.disposable?.dispose() self.sampleDisposable.dispose() + self.upgradeFormDisposable?.dispose() self.upgradeDisposable?.dispose() } func requestUpgradePreview() { - guard let _ = self.subject.arguments?.upgradeStars else { + guard let arguments = self.subject.arguments, arguments.canUpgrade || arguments.upgradeStars != nil else { return } self.context.starsContext?.load(force: false) @@ -251,7 +268,7 @@ private final class GiftViewSheetContent: CombinedComponent { } func commitUpgrade() { - guard let arguments = self.subject.arguments, let messageId = arguments.messageId, let starsContext = self.context.starsContext, let starsState = starsContext.currentState else { + guard let arguments = self.subject.arguments, let starsContext = self.context.starsContext, let starsState = starsContext.currentState else { return } let peerId = arguments.peerId @@ -259,7 +276,7 @@ private final class GiftViewSheetContent: CombinedComponent { self.inProgress = true self.updated() - let _ = (self.context.engine.payments.upgradeStarGift(formId: formId, messageId: messageId, keepOriginalInfo: self.keepOriginalInfo) + self.upgradeDisposable = (self.upgradeGift(formId, self.keepOriginalInfo) |> deliverOnMainQueue).start(next: { [weak self] result in guard let self, let controller = self.getController() as? GiftViewScreen else { return @@ -307,7 +324,7 @@ private final class GiftViewSheetContent: CombinedComponent { } func makeState() -> State { - return State(context: self.context, subject: self.subject, getController: self.getController) + return State(context: self.context, subject: self.subject, upgradeGift: self.upgradeGift, getController: self.getController) } static var body: Body { @@ -379,8 +396,6 @@ private final class GiftViewSheetContent: CombinedComponent { entities = arguments.entities limitTotal = gift.availability?.total convertStars = arguments.convertStars - incoming = arguments.incoming || arguments.peerId == component.context.account.peerId - savedToProfile = arguments.savedToProfile converted = arguments.converted giftId = gift.id date = arguments.date @@ -395,6 +410,8 @@ private final class GiftViewSheetContent: CombinedComponent { convertStars = nil uniqueGift = gift } + savedToProfile = arguments.savedToProfile + incoming = arguments.incoming || arguments.peerId == component.context.account.peerId nameHidden = arguments.nameHidden titleString = incoming ? strings.Gift_View_ReceivedTitle : strings.Gift_View_Title } else { @@ -748,8 +765,8 @@ private final class GiftViewSheetContent: CombinedComponent { // originY -= 12.0 // } - let linkColor = theme.actionSheet.controlAccentColor if !descriptionText.isEmpty { + let linkColor = theme.actionSheet.controlAccentColor if state.cachedChevronImage == nil || state.cachedChevronImage?.1 !== environment.theme { state.cachedChevronImage = (generateTintedImage(image: UIImage(bundleImageName: "Settings/TextArrowRight"), color: linkColor)!, theme) } @@ -767,7 +784,6 @@ private final class GiftViewSheetContent: CombinedComponent { textFont = soldOut ? Font.medium(15.0) : Font.regular(15.0) textColor = soldOut ? theme.list.itemDestructiveColor : theme.list.itemPrimaryTextColor } - let markdownAttributes = MarkdownAttributes(body: MarkdownAttributeSet(font: textFont, textColor: textColor), bold: MarkdownAttributeSet(font: textFont, textColor: textColor), link: MarkdownAttributeSet(font: textFont, textColor: linkColor), linkAttribute: { contents in return (TelegramTextAttributes.URL, contents) }) @@ -850,45 +866,65 @@ private final class GiftViewSheetContent: CombinedComponent { if !soldOut { if let uniqueGift { if let peer = state.peerMap[uniqueGift.ownerPeerId] { - let ownerComponent = AnyComponent( - HStack([ - AnyComponentWithIdentity( - id: AnyHashable(0), - component: AnyComponent(Button( - content: AnyComponent( - PeerCellComponent( + 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, - theme: theme, - strings: strings, - peer: peer - ) - ), - action: { - component.openPeer(peer) - Queue.mainQueue().after(1.0, { - component.cancel(false) - }) - } - )) + 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 + ) ), - 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) - ) + action: { + component.openPeer(peer) + Queue.mainQueue().after(1.0, { + component.cancel(false) + }) + } + )) + } tableItems.append(.init( id: "owner", title: strings.Gift_Unique_Owner, @@ -1042,13 +1078,22 @@ 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) - string.addAttribute(NSAttributedString.Key.foregroundColor, value: tableLinkColor, range: format.ranges[0].range) - + 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) + } else { + string.addAttribute(NSAttributedString.Key.foregroundColor, value: tableLinkColor, 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) - string.addAttribute(NSAttributedString.Key.foregroundColor, value: tableLinkColor, range: format.ranges[0].range) + 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) + } else { + string.addAttribute(NSAttributedString.Key.foregroundColor, value: tableLinkColor, range: format.ranges[0].range) + } value = string } @@ -1068,6 +1113,7 @@ private final class GiftViewSheetContent: CombinedComponent { animationRenderer: component.context.animationRenderer, placeholderColor: theme.list.mediaPlaceholderColor, text: .plain(value), + horizontalAlignment: .center, maximumNumberOfLines: 0, handleSpoilers: true ) @@ -1281,64 +1327,65 @@ private final class GiftViewSheetContent: CombinedComponent { .disappear(.default(alpha: true)) ) originY += table.size.height + 23.0 - - if incoming && !converted { - if state.cachedSmallChevronImage == nil || state.cachedSmallChevronImage?.1 !== environment.theme { - state.cachedSmallChevronImage = (generateTintedImage(image: UIImage(bundleImageName: "Item List/InlineTextRightArrow"), color: linkColor)!, theme) - } - let descriptionText: String - if savedToProfile { - descriptionText = strings.Gift_View_DisplayedInfoHide - } else if let upgradeStars, upgradeStars > 0 && !upgraded { - descriptionText = strings.Gift_View_HiddenInfoShow - } else { - descriptionText = strings.Gift_View_HiddenInfo - } - - let textFont = Font.regular(13.0) - let textColor = theme.list.itemSecondaryTextColor - let markdownAttributes = MarkdownAttributes(body: MarkdownAttributeSet(font: textFont, textColor: textColor), bold: MarkdownAttributeSet(font: textFont, textColor: textColor), link: MarkdownAttributeSet(font: textFont, textColor: linkColor), linkAttribute: { contents in - return (TelegramTextAttributes.URL, contents) - }) - let attributedString = parseMarkdownIntoAttributedString(descriptionText, attributes: markdownAttributes, textAlignment: .center).mutableCopy() as! NSMutableAttributedString - if let range = attributedString.string.range(of: ">"), let chevronImage = state.cachedSmallChevronImage?.0 { - attributedString.addAttribute(.attachment, value: chevronImage, range: NSRange(range, in: attributedString.string)) - } - - originY -= 5.0 - let additionalText = additionalText.update( - component: MultilineTextComponent( - text: .plain(attributedString), - horizontalAlignment: .center, - maximumNumberOfLines: 5, - lineSpacing: 0.2, - highlightColor: linkColor.withAlphaComponent(0.1), - highlightInset: UIEdgeInsets(top: 0.0, left: 0.0, bottom: 0.0, right: -8.0), - highlightAction: { attributes in - if let _ = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.URL)] { - return NSAttributedString.Key(rawValue: TelegramTextAttributes.URL) - } else { - return nil - } - }, - tapAction: { _, _ in - component.updateSavedToProfile(!savedToProfile) - Queue.mainQueue().after(1.0, { - component.cancel(false) - }) - } - ), - availableSize: CGSize(width: context.availableSize.width - sideInset * 2.0 - 60.0, height: CGFloat.greatestFiniteMagnitude), - transition: .immediate - ) - context.add(additionalText - .position(CGPoint(x: context.availableSize.width / 2.0, y: originY + additionalText.size.height / 2.0)) - .appear(.default(alpha: true)) - .disappear(.default(alpha: true)) - ) - originY += additionalText.size.height - originY += 16.0 + } + + if incoming && !converted && !upgraded && !showUpgradePreview { + let linkColor = theme.actionSheet.controlAccentColor + if state.cachedSmallChevronImage == nil || state.cachedSmallChevronImage?.1 !== environment.theme { + state.cachedSmallChevronImage = (generateTintedImage(image: UIImage(bundleImageName: "Item List/InlineTextRightArrow"), color: linkColor)!, theme) } + let descriptionText: String + if savedToProfile { + descriptionText = strings.Gift_View_DisplayedInfoHide + } else if let upgradeStars, upgradeStars > 0 && !upgraded { + descriptionText = strings.Gift_View_HiddenInfoShow + } else { + descriptionText = strings.Gift_View_HiddenInfo + } + + let textFont = Font.regular(13.0) + let textColor = theme.list.itemSecondaryTextColor + let markdownAttributes = MarkdownAttributes(body: MarkdownAttributeSet(font: textFont, textColor: textColor), bold: MarkdownAttributeSet(font: textFont, textColor: textColor), link: MarkdownAttributeSet(font: textFont, textColor: linkColor), linkAttribute: { contents in + return (TelegramTextAttributes.URL, contents) + }) + let attributedString = parseMarkdownIntoAttributedString(descriptionText, attributes: markdownAttributes, textAlignment: .center).mutableCopy() as! NSMutableAttributedString + if let range = attributedString.string.range(of: ">"), let chevronImage = state.cachedSmallChevronImage?.0 { + attributedString.addAttribute(.attachment, value: chevronImage, range: NSRange(range, in: attributedString.string)) + } + + originY -= 5.0 + let additionalText = additionalText.update( + component: MultilineTextComponent( + text: .plain(attributedString), + horizontalAlignment: .center, + maximumNumberOfLines: 5, + lineSpacing: 0.2, + highlightColor: linkColor.withAlphaComponent(0.1), + highlightInset: UIEdgeInsets(top: 0.0, left: 0.0, bottom: 0.0, right: -8.0), + highlightAction: { attributes in + if let _ = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.URL)] { + return NSAttributedString.Key(rawValue: TelegramTextAttributes.URL) + } else { + return nil + } + }, + tapAction: { _, _ in + component.updateSavedToProfile(!savedToProfile) + Queue.mainQueue().after(1.0, { + component.cancel(false) + }) + } + ), + availableSize: CGSize(width: context.availableSize.width - sideInset * 2.0 - 60.0, height: CGFloat.greatestFiniteMagnitude), + transition: .immediate + ) + context.add(additionalText + .position(CGPoint(x: context.availableSize.width / 2.0, y: originY + additionalText.size.height / 2.0)) + .appear(.default(alpha: true)) + .disappear(.default(alpha: true)) + ) + originY += additionalText.size.height + originY += 16.0 } let buttonChild: _UpdatedChildComponent @@ -1480,6 +1527,7 @@ private final class GiftViewSheetComponent: CombinedComponent { let sendGift: (EnginePeer.Id) -> Void let openMyGifts: () -> Void let transferGift: () -> Void + let upgradeGift: ((Int64?, Bool) -> Signal) let showAttributeInfo: (Any, Float) -> Void init( @@ -1492,6 +1540,7 @@ private final class GiftViewSheetComponent: CombinedComponent { sendGift: @escaping (EnginePeer.Id) -> Void, openMyGifts: @escaping () -> Void, transferGift: @escaping () -> Void, + upgradeGift: @escaping ((Int64?, Bool) -> Signal), showAttributeInfo: @escaping (Any, Float) -> Void ) { self.context = context @@ -1503,6 +1552,7 @@ private final class GiftViewSheetComponent: CombinedComponent { self.sendGift = sendGift self.openMyGifts = openMyGifts self.transferGift = transferGift + self.upgradeGift = upgradeGift self.showAttributeInfo = showAttributeInfo } @@ -1550,6 +1600,7 @@ private final class GiftViewSheetComponent: CombinedComponent { sendGift: context.component.sendGift, openMyGifts: context.component.openMyGifts, transferGift: context.component.transferGift, + upgradeGift: context.component.upgradeGift, showAttributeInfo: context.component.showAttributeInfo, getController: controller )), @@ -1629,20 +1680,20 @@ public class GiftViewScreen: ViewControllerComponentContainer { case let .message(message): if let action = message.media.first(where: { $0 is TelegramMediaAction }) as? TelegramMediaAction { switch action.action { - case let .starGift(gift, convertStars, text, entities, nameHidden, savedToProfile, converted, upgraded, canUpgrade, upgradeStars, _): + case let .starGift(gift, convertStars, text, entities, nameHidden, savedToProfile, converted, upgraded, canUpgrade, upgradeStars, _, _): return (message.id.peerId, message.author?.id, message.author?.compactDisplayTitle, message.id, message.flags.contains(.Incoming), gift, message.timestamp, convertStars, text, entities, nameHidden, savedToProfile, converted, upgraded, canUpgrade, upgradeStars, nil, nil) - case let .starGiftUnique(gift, _, _, savedToProfile, canExportDate, transferStars, _): - return (message.id.peerId, message.author?.id, message.author?.compactDisplayTitle, message.id, message.flags.contains(.Incoming), gift, message.timestamp, nil, nil, nil, false, savedToProfile, false, true, false, nil, transferStars, canExportDate) + case let .starGiftUnique(gift, isUpgrade, _, savedToProfile, canExportDate, transferStars, _): + var incoming = message.flags.contains(.Incoming) + if isUpgrade && message.author?.id != message.id.peerId { + incoming = true + } + return (message.id.peerId, message.author?.id, message.author?.compactDisplayTitle, message.id, incoming, gift, message.timestamp, nil, nil, nil, false, savedToProfile, false, false, false, nil, transferStars, canExportDate) default: return nil } } case let .profileGift(peerId, gift): - var upgraded = false - if case .unique = gift.gift { - upgraded = true - } - 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, upgraded, gift.canUpgrade, gift.upgradeStars, gift.transferStars, gift.canExportDate) + 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) case .soldOutGift: return nil case .upgradePreview: @@ -1663,7 +1714,9 @@ public class GiftViewScreen: ViewControllerComponentContainer { subject: GiftViewScreen.Subject, forceDark: Bool = false, updateSavedToProfile: ((Bool) -> Void)? = nil, - convertToStars: (() -> Void)? = nil + convertToStars: (() -> Void)? = nil, + transferGift: ((Bool, EnginePeer.Id) -> Void)? = nil, + upgradeGift: ((Int64?, Bool) -> Signal)? = nil ) { self.context = context self.subject = subject @@ -1676,6 +1729,7 @@ public class GiftViewScreen: ViewControllerComponentContainer { var openMyGiftsImpl: (() -> Void)? var transferGiftImpl: (() -> Void)? var showAttributeInfoImpl: ((Any, Float) -> Void)? + var upgradeGiftImpl: ((Int64?, Bool) -> Signal)? super.init( context: context, @@ -1703,6 +1757,9 @@ public class GiftViewScreen: ViewControllerComponentContainer { transferGift: { transferGiftImpl?() }, + upgradeGift: { formId, keepOriginalInfo in + return upgradeGiftImpl?(formId, keepOriginalInfo) ?? .complete() + }, showAttributeInfo: { tag, rarity in showAttributeInfoImpl?(tag, rarity) } @@ -1737,6 +1794,20 @@ public class GiftViewScreen: ViewControllerComponentContainer { guard let self, let arguments = self.subject.arguments, let messageId = arguments.messageId else { return } + + var animationFile: TelegramMediaFile? + switch arguments.gift { + case let .generic(gift): + animationFile = gift.file + case let .unique(gift): + for attribute in gift.attributes { + if case let .model(_, file, _) = attribute { + animationFile = file + break + } + } + } + if let updateSavedToProfile { updateSavedToProfile(added) } else { @@ -1749,10 +1820,10 @@ public class GiftViewScreen: ViewControllerComponentContainer { let text = added ? presentationData.strings.Gift_Displayed_NewText : presentationData.strings.Gift_Hidden_NewText if let navigationController = self.navigationController as? NavigationController { Queue.mainQueue().after(0.5) { - if let lastController = navigationController.viewControllers.last as? ViewController, case let .generic(gift) = arguments.gift { + if let lastController = navigationController.viewControllers.last as? ViewController, let animationFile { let resultController = UndoOverlayController( presentationData: presentationData, - content: .sticker(context: context, file: gift.file, loop: false, title: nil, text: text, undoText: updateSavedToProfile == nil ? presentationData.strings.Gift_Displayed_View : nil, customAction: nil), + content: .sticker(context: context, file: animationFile, loop: false, title: nil, text: text, undoText: updateSavedToProfile == nil ? presentationData.strings.Gift_Displayed_View : nil, customAction: nil), elevatedLayout: lastController is ChatController, action: { [weak navigationController] action in if case .undo = action, let navigationController { @@ -1792,7 +1863,6 @@ public class GiftViewScreen: ViewControllerComponentContainer { let starsConvertMaxDate = arguments.date + configuration.convertToStarsPeriod let currentTime = Int32(CFAbsoluteTimeGetCurrent() + kCFAbsoluteTimeIntervalSince1970) - if currentTime > starsConvertMaxDate { let days: Int32 = Int32(ceil(Float(configuration.convertToStarsPeriod) / 86400.0)) let controller = textAlertController( @@ -1857,6 +1927,7 @@ public class GiftViewScreen: ViewControllerComponentContainer { self.present(controller, in: .window(.root)) } } + openStarsIntroImpl = { [weak self] in guard let self else { return @@ -1864,6 +1935,7 @@ public class GiftViewScreen: ViewControllerComponentContainer { let introController = context.sharedContext.makeStarsIntroScreen(context: context) self.push(introController) } + sendGiftImpl = { [weak self] peerId in guard let self else { return @@ -1876,6 +1948,7 @@ public class GiftViewScreen: ViewControllerComponentContainer { self.push(controller) }) } + openMyGiftsImpl = { [weak self] in guard let self, let navigationController = self.navigationController as? NavigationController else { return @@ -1906,11 +1979,42 @@ public class GiftViewScreen: ViewControllerComponentContainer { let _ = (context.account.stateManager.contactBirthdays |> take(1) |> deliverOnMainQueue).start(next: { birthdays in - let controller = context.sharedContext.makePremiumGiftController(context: context, source: .starGiftTransfer(birthdays, messageId, gift, transferStars, arguments.canExportDate), completion: nil) + let controller = context.sharedContext.makePremiumGiftController(context: context, source: .starGiftTransfer(birthdays, messageId, gift, transferStars, arguments.canExportDate), completion: { peerIds in + guard let peerId = peerIds.first else { + return + } + if let transferGift { + transferGift(transferStars == 0, peerId) + } else { + let _ = (context.engine.payments.transferStarGift(prepaid: transferStars == 0, messageId: messageId, peerId: peerId) + |> deliverOnMainQueue).start() + } + Queue.mainQueue().after(1.0, { + if transferStars > 0 { + context.starsContext?.load(force: true) + } + }) + }) navigationController.pushViewController(controller) }) } + upgradeGiftImpl = { [weak self] formId, keepOriginalInfo in + guard let self, let arguments = self.subject.arguments, let messageId = arguments.messageId else { + return .complete() + } + if let upgradeGift { + return upgradeGift(formId, keepOriginalInfo) + } else { + return self.context.engine.payments.upgradeStarGift(formId: formId, messageId: messageId, keepOriginalInfo: keepOriginalInfo) + |> afterCompleted { + if formId != nil { + context.starsContext?.load(force: true) + } + } + } + } + showAttributeInfoImpl = { [weak self] tag, rarity in guard let self else { return @@ -1950,6 +2054,17 @@ public class GiftViewScreen: ViewControllerComponentContainer { fileprivate func animateSuccess() { self.navigationController?.view.addSubview(ConfettiView(frame: self.view.bounds)) + + let presentationData = self.context.sharedContext.currentPresentationData.with { $0 } + self.present(UndoOverlayController(presentationData: presentationData, content: .universal( + animation: "GiftUpgraded", + scale: 0.066, + colors: [:], + title: presentationData.strings.Gift_Upgrade_Succeed_Title, + text: presentationData.strings.Gift_Upgrade_Succeed_Text, + customUndoText: nil, + timeout: 4.0 + ), elevatedLayout: false, position: .bottom, action: { _ in return true }), in: .current) } public func dismissAnimated() { diff --git a/submodules/TelegramUI/Components/PeerInfo/PeerInfoCoverComponent/Sources/PeerInfoCoverComponent.swift b/submodules/TelegramUI/Components/PeerInfo/PeerInfoCoverComponent/Sources/PeerInfoCoverComponent.swift index c2870b3538..49c8b45f2e 100644 --- a/submodules/TelegramUI/Components/PeerInfo/PeerInfoCoverComponent/Sources/PeerInfoCoverComponent.swift +++ b/submodules/TelegramUI/Components/PeerInfo/PeerInfoCoverComponent/Sources/PeerInfoCoverComponent.swift @@ -351,10 +351,21 @@ public final class PeerInfoCoverComponent: Component { self.backgroundView.backgroundColor = secondaryBackgroundColor - self.backgroundGradientLayer.type = .axial - self.backgroundGradientLayer.startPoint = CGPoint(x: 0.5, y: 1.0) - self.backgroundGradientLayer.endPoint = CGPoint(x: 0.5, y: 0.0) - self.backgroundGradientLayer.colors = [backgroundColor.cgColor, secondaryBackgroundColor.cgColor] + 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) + } + self.backgroundGradientLayer.endPoint = CGPoint(x: 1.0, y: 1.0) + self.backgroundGradientLayer.type = .radial + self.backgroundGradientLayer.colors = [secondaryBackgroundColor.cgColor, backgroundColor.cgColor] + } else { + self.backgroundGradientLayer.startPoint = CGPoint(x: 0.5, y: 1.0) + self.backgroundGradientLayer.endPoint = CGPoint(x: 0.5, y: 0.0) + self.backgroundGradientLayer.type = .axial + self.backgroundGradientLayer.colors = [backgroundColor.cgColor, secondaryBackgroundColor.cgColor] + } self.backgroundGradientLayer.anchorPoint = CGPoint(x: 0.0, y: 1.0) let gradientHeight: CGFloat = component.defaultHeight @@ -431,7 +442,7 @@ public final class PeerInfoCoverComponent: Component { } else if availableSize.width < 150.0 { baseDistance *= 0.6 baseRowDistance *= 0.6 - baseItemSize *= 0.75 + baseItemSize *= 0.83 } var avatarBackgroundPatternLayerCount = 0 diff --git a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoHeaderNode.swift b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoHeaderNode.swift index 2ec2c10548..aa3cc094e2 100644 --- a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoHeaderNode.swift +++ b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoHeaderNode.swift @@ -1306,7 +1306,7 @@ final class PeerInfoHeaderNode: ASDisplayNode { var nextIconX: CGFloat = titleSize.width var nextExpandedIconX: CGFloat = titleExpandedSize.width - if let credibilityIconSize = self.credibilityIconSize, let titleExpandedCredibilityIconSize = self.titleExpandedCredibilityIconSize { + if let credibilityIconSize = self.credibilityIconSize, let titleExpandedCredibilityIconSize = self.titleExpandedCredibilityIconSize, credibilityIconSize.width > 0.0 { let offset = (credibilityIconSize.width + 4.0) / 2.0 let leftOffset: CGFloat = nextIconX + 4.0 @@ -1325,13 +1325,21 @@ final class PeerInfoHeaderNode: ASDisplayNode { nextExpandedIconX += 4.0 + titleExpandedCredibilityIconSize.width } - if let verifiedIconSize = self.verifiedIconSize, let titleExpandedVerifiedIconSize = self.titleExpandedVerifiedIconSize { - let offset = (verifiedIconSize.width + 4.0) / 2.0 - titleHorizontalOffset += offset - titleExpandedHorizontalOffset += offset - - let leftOffset: CGFloat = -verifiedIconSize.width - 4.0 - let leftExpandedOffset: CGFloat = -titleExpandedVerifiedIconSize.width - 4.0 + if let verifiedIconSize = self.verifiedIconSize, let titleExpandedVerifiedIconSize = self.titleExpandedVerifiedIconSize, verifiedIconSize.width > 0.0 { + let leftOffset: CGFloat + let leftExpandedOffset: CGFloat + if case .verified = verifiedIcon { + titleHorizontalOffset -= (verifiedIconSize.width + 4.0) / 2.0 + + leftOffset = nextIconX + 4.0 + leftExpandedOffset = nextExpandedIconX + 4.0 + } else { + titleHorizontalOffset += (verifiedIconSize.width + 4.0) / 2.0 + titleExpandedHorizontalOffset += titleExpandedVerifiedIconSize.width + + leftOffset = -verifiedIconSize.width - 4.0 + leftExpandedOffset = -titleExpandedVerifiedIconSize.width - 8.0 + } var collapsedTransitionOffset: CGFloat = 0.0 if let navigationTransition = self.navigationTransition { @@ -1340,6 +1348,11 @@ final class PeerInfoHeaderNode: ASDisplayNode { transition.updateFrame(view: self.titleVerifiedIconView, frame: CGRect(origin: CGPoint(x: leftOffset + collapsedTransitionOffset, y: floor((titleSize.height - verifiedIconSize.height) / 2.0)), size: verifiedIconSize)) transition.updateFrame(view: self.titleExpandedVerifiedIconView, frame: CGRect(origin: CGPoint(x: leftExpandedOffset, y: floor((titleExpandedSize.height - titleExpandedVerifiedIconSize.height) / 2.0) + 1.0), size: titleExpandedVerifiedIconSize)) + + if case .verified = verifiedIcon { + nextIconX += 4.0 + verifiedIconSize.width + nextExpandedIconX += 4.0 + titleExpandedVerifiedIconSize.width + } } var titleFrame: CGRect @@ -1759,7 +1772,9 @@ final class PeerInfoHeaderNode: ASDisplayNode { var titleFrame = titleFrame if !self.isAvatarExpanded { - titleFrame = titleFrame.offsetBy(dx: self.isAvatarExpanded ? titleExpandedHorizontalOffset : titleHorizontalOffset * titleScale, dy: 0.0) + titleFrame = titleFrame.offsetBy(dx: titleHorizontalOffset * titleScale, dy: 0.0) + } else { + titleFrame = titleFrame.offsetBy(dx: titleExpandedHorizontalOffset, dy: 0.0) } let titleCenter = CGPoint(x: transitionFraction * transitionSourceTitleFrame.midX + (1.0 - transitionFraction) * titleFrame.midX, y: transitionFraction * transitionSourceTitleFrame.midY + (1.0 - transitionFraction) * titleFrame.midY) diff --git a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreen.swift b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreen.swift index b2df007b98..51d7b0bbf1 100644 --- a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreen.swift +++ b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreen.swift @@ -3986,8 +3986,7 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro self?.controller?.updateProfilePhoto(image, mode: .generic) } galleryController.avatarVideoEditCompletion = { [weak self] image, asset, adjustments in - let _ = self - //self?.controller?.updateProfileVideo(image, asset: asset, adjustments: adjustments, mode: .generic) + self?.controller?.updateProfileVideo(image, asset: asset, adjustments: adjustments, mode: .generic) } galleryController.removedEntry = { [weak self] entry in if let item = PeerInfoAvatarListItem(entry: entry) { @@ -9605,7 +9604,7 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro } } - fileprivate func oldOpenAvatarForEditing(mode: PeerInfoAvatarEditingMode = .generic, fromGallery: Bool = false, completion: @escaping (UIImage?) -> Void = { _ in }) { + func openAvatarForEditing(mode: PeerInfoAvatarEditingMode = .generic, fromGallery: Bool = false, completion: @escaping (UIImage?) -> Void = { _ in }) { guard let peer = self.data?.peer, mode != .generic || canEditPeerInfo(context: self.context, peer: peer, chatLocation: self.chatLocation, threadData: self.data?.threadData) else { return } @@ -9694,21 +9693,6 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro let mixin = TGMediaAvatarMenuMixin(context: legacyController.context, parentController: emptyController, hasSearchButton: true, hasDeleteButton: hasDeleteButton, hasViewButton: false, personalPhoto: strongSelf.isSettings || strongSelf.isMyProfile, isVideo: currentIsVideo, saveEditedPhotos: false, saveCapturedMedia: false, signup: false, forum: isForum, title: title, isSuggesting: [.custom, .suggest].contains(mode))! mixin.stickersContext = LegacyPaintStickersContext(context: strongSelf.context) let _ = strongSelf.currentAvatarMixin.swap(mixin) -// mixin.requestSearchController = { [weak self, weak parentController] assetsController in -// guard let strongSelf = self else { -// return -// } -// let controller = WebSearchController(context: strongSelf.context, updatedPresentationData: strongSelf.controller?.updatedPresentationData, peer: peer, chatLocation: nil, configuration: searchBotsConfiguration, mode: .avatar(initialQuery: (strongSelf.isSettings || strongSelf.isMyProfile) ? nil : peer.compactDisplayTitle, completion: { [weak self] result in -// assetsController?.dismiss() -// self?.updateProfilePhoto(result, mode: mode) -// })) -// controller.navigationPresentation = .modal -// parentController?.push(controller) -// -// if fromGallery { -// completion(nil) -// } -// } var isFromEditor = false mixin.requestAvatarEditor = { [weak self, weak parentController] imageCompletion, videoCompletion in guard let strongSelf = self, let imageCompletion, let videoCompletion else { @@ -9755,18 +9739,18 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro } } } -// mixin.didFinishWithImage = { [weak self] image in -// if let image = image { -// completion(image) -// self?.updateProfilePhoto(image, mode: mode) -// } -// } -// mixin.didFinishWithVideo = { [weak self] image, asset, adjustments in -// if let image = image, let asset = asset { -// completion(image) -// self?.updateProfileVideo(image, asset: asset, adjustments: adjustments, mode: mode) -// } -// } + mixin.didFinishWithImage = { [weak self] image in + if let image = image { + completion(image) + self?.controller?.updateProfilePhoto(image, mode: mode) + } + } + mixin.didFinishWithVideo = { [weak self] image, asset, adjustments in + if let image = image, let asset = asset { + completion(image) + self?.controller?.updateProfileVideo(image, asset: asset, adjustments: adjustments, mode: mode) + } + } mixin.didFinishWithDelete = { guard let strongSelf = self else { return @@ -12766,6 +12750,10 @@ public final class PeerInfoScreenImpl: ViewController, PeerInfoScreen, KeyShortc proceed() } } + + func openAvatarForEditing(mode: PeerInfoAvatarEditingMode = .generic, fromGallery: Bool = false, completion: @escaping (UIImage?) -> Void = { _ in }) { + self.controllerNode.openAvatarForEditing(mode: mode, fromGallery: fromGallery, completion: completion) + } static func openPeer(context: AccountContext, peerId: PeerId, navigation: ChatControllerInteractionNavigateToPeer, navigationController: NavigationController) { let _ = (context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: peerId)) diff --git a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreenAvatarSetup.swift b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreenAvatarSetup.swift index 49826fe63e..3b2b7ad43e 100644 --- a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreenAvatarSetup.swift +++ b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreenAvatarSetup.swift @@ -15,166 +15,167 @@ import OverlayStatusController import UndoUI import PeerAvatarGalleryUI import PresentationDataUtils +import LegacyComponents extension PeerInfoScreenImpl { - func openAvatarForEditing(mode: PeerInfoAvatarEditingMode = .generic, fromGallery: Bool = false, completion: @escaping (UIImage?) -> Void = { _ in }) { - guard let data = self.controllerNode.data, let peer = data.peer, mode != .generic || canEditPeerInfo(context: self.context, peer: peer, chatLocation: self.chatLocation, threadData: data.threadData) else { - return - } - self.view.endEditing(true) - - let peerId = self.peerId - var isForum = false - if let peer = peer as? TelegramChannel, peer.flags.contains(.isForum) { - isForum = true - } - - var currentIsVideo = false - var emojiMarkup: TelegramMediaImage.EmojiMarkup? - let item = self.controllerNode.headerNode.avatarListNode.listContainerNode.currentItemNode?.item - if let item = item, case let .image(_, _, videoRepresentations, _, _, emojiMarkupValue) = item { - currentIsVideo = !videoRepresentations.isEmpty - emojiMarkup = emojiMarkupValue - } - - let _ = isForum - let _ = currentIsVideo - - let _ = (self.context.engine.data.get( - TelegramEngine.EngineData.Item.Peer.Peer(id: peerId) - ) - |> deliverOnMainQueue).startStandalone(next: { [weak self] peer in - guard let self, let peer else { - return - } - - let keyboardInputData = Promise() - keyboardInputData.set(AvatarEditorScreen.inputData(context: self.context, isGroup: peer.id.namespace != Namespaces.Peer.CloudUser)) - - var hasPhotos = false - if !peer.profileImageRepresentations.isEmpty { - hasPhotos = true - } - - var hasDeleteButton = false - if case .generic = mode { - hasDeleteButton = hasPhotos && !fromGallery - } else if case .custom = mode { - hasDeleteButton = peer.profileImageRepresentations.first?.isPersonal == true - } else if case .fallback = mode { - if let cachedData = data.cachedData as? CachedUserData, case let .known(photo) = cachedData.fallbackPhoto { - hasDeleteButton = photo != nil - } - } - - let _ = hasDeleteButton - - let parentController = (self.context.sharedContext.mainWindow?.viewController as? NavigationController)?.topViewController as? ViewController - - var dismissImpl: (() -> Void)? - let mainController = self.context.sharedContext.makeAvatarMediaPickerScreen(context: self.context, getSourceRect: { return nil }, canDelete: hasDeleteButton, performDelete: { [weak self] in - self?.openAvatarRemoval(mode: mode, peer: peer, item: item) - }, completion: { result, transitionView, transitionRect, transitionImage, fromCamera, transitionOut, cancelled in - let subject: Signal - if let asset = result as? PHAsset { - subject = .single(.asset(asset)) - } else if let image = result as? UIImage { - subject = .single(.image(image: image, dimensions: PixelDimensions(image.size), additionalImage: nil, additionalImagePosition: .bottomRight)) - } else if let result = result as? Signal { - subject = result - |> map { value -> MediaEditorScreenImpl.Subject? in - switch value { - case .pendingImage: - return nil - case let .image(image): - return .image(image: image.image, dimensions: PixelDimensions(image.image.size), additionalImage: nil, additionalImagePosition: .topLeft) - case let .video(video): - return .video(videoPath: video.videoPath, thumbnail: video.coverImage, mirror: video.mirror, additionalVideoPath: nil, additionalThumbnail: nil, dimensions: video.dimensions, duration: video.duration, videoPositionChanges: [], additionalVideoPosition: .topLeft) - default: - return nil - } - } - } else { - let peerType: AvatarEditorScreen.PeerType - if mode == .suggest { - peerType = .suggest - } else if case .legacyGroup = peer { - peerType = .group - } else if case let .channel(channel) = peer { - if case .group = channel.info { - peerType = channel.flags.contains(.isForum) ? .forum : .group - } else { - peerType = .channel - } - } else { - peerType = .user - } - let controller = AvatarEditorScreen(context: self.context, inputData: keyboardInputData.get(), peerType: peerType, markup: emojiMarkup) - //controller.imageCompletion = imageCompletion - //controller.videoCompletion = videoCompletion - parentController?.push(controller) - //isFromEditor = true - return - } - - let editorController = MediaEditorScreenImpl( - context: self.context, - mode: .avatarEditor, - subject: subject, - transitionIn: fromCamera ? .camera : transitionView.flatMap({ .gallery( - MediaEditorScreenImpl.TransitionIn.GalleryTransitionIn( - sourceView: $0, - sourceRect: transitionRect, - sourceImage: transitionImage - ) - ) }), - transitionOut: { finished, isNew in - if !finished, let transitionView { - return MediaEditorScreenImpl.TransitionOut( - destinationView: transitionView, - destinationRect: transitionView.bounds, - destinationCornerRadius: 0.0 - ) - } - return nil - }, completion: { [weak self] result, commit in - dismissImpl?() - - switch result.media { - case let .image(image, _): - self?.updateProfilePhoto(image, mode: mode) - commit({}) - case let .video(video, coverImage, values, _, _): - if let coverImage { - self?.updateProfileVideo(coverImage, asset: video, adjustments: values, mode: mode) - } - commit({}) - default: - break - } - } as (MediaEditorScreenImpl.Result, @escaping (@escaping () -> Void) -> Void) -> Void - ) - editorController.cancelled = { _ in - cancelled() - } - self.push(editorController) - }, dismissed: { - - }) - dismissImpl = { [weak mainController] in - if let mainController, let navigationController = mainController.navigationController { - var viewControllers = navigationController.viewControllers - viewControllers = viewControllers.filter { c in - return !(c is CameraScreen) && c !== mainController - } - navigationController.setViewControllers(viewControllers, animated: false) - } - } - mainController.navigationPresentation = .flatModal - mainController.supportedOrientations = ViewControllerSupportedOrientations(regularSize: .all, compactSize: .portrait) - self.push(mainController) - }) - } +// func newopenAvatarForEditing(mode: PeerInfoAvatarEditingMode = .generic, fromGallery: Bool = false, completion: @escaping (UIImage?) -> Void = { _ in }) { +// guard let data = self.controllerNode.data, let peer = data.peer, mode != .generic || canEditPeerInfo(context: self.context, peer: peer, chatLocation: self.chatLocation, threadData: data.threadData) else { +// return +// } +// self.view.endEditing(true) +// +// let peerId = self.peerId +// var isForum = false +// if let peer = peer as? TelegramChannel, peer.flags.contains(.isForum) { +// isForum = true +// } +// +// var currentIsVideo = false +// var emojiMarkup: TelegramMediaImage.EmojiMarkup? +// let item = self.controllerNode.headerNode.avatarListNode.listContainerNode.currentItemNode?.item +// if let item = item, case let .image(_, _, videoRepresentations, _, _, emojiMarkupValue) = item { +// currentIsVideo = !videoRepresentations.isEmpty +// emojiMarkup = emojiMarkupValue +// } +// +// let _ = isForum +// let _ = currentIsVideo +// +// let _ = (self.context.engine.data.get( +// TelegramEngine.EngineData.Item.Peer.Peer(id: peerId) +// ) +// |> deliverOnMainQueue).startStandalone(next: { [weak self] peer in +// guard let self, let peer else { +// return +// } +// +// let keyboardInputData = Promise() +// keyboardInputData.set(AvatarEditorScreen.inputData(context: self.context, isGroup: peer.id.namespace != Namespaces.Peer.CloudUser)) +// +// var hasPhotos = false +// if !peer.profileImageRepresentations.isEmpty { +// hasPhotos = true +// } +// +// var hasDeleteButton = false +// if case .generic = mode { +// hasDeleteButton = hasPhotos && !fromGallery +// } else if case .custom = mode { +// hasDeleteButton = peer.profileImageRepresentations.first?.isPersonal == true +// } else if case .fallback = mode { +// if let cachedData = data.cachedData as? CachedUserData, case let .known(photo) = cachedData.fallbackPhoto { +// hasDeleteButton = photo != nil +// } +// } +// +// let _ = hasDeleteButton +// +// let parentController = (self.context.sharedContext.mainWindow?.viewController as? NavigationController)?.topViewController as? ViewController +// +// var dismissImpl: (() -> Void)? +// let mainController = self.context.sharedContext.makeAvatarMediaPickerScreen(context: self.context, getSourceRect: { return nil }, canDelete: hasDeleteButton, performDelete: { [weak self] in +// self?.openAvatarRemoval(mode: mode, peer: peer, item: item) +// }, completion: { result, transitionView, transitionRect, transitionImage, fromCamera, transitionOut, cancelled in +// let subject: Signal +// if let asset = result as? PHAsset { +// subject = .single(.asset(asset)) +// } else if let image = result as? UIImage { +// subject = .single(.image(image: image, dimensions: PixelDimensions(image.size), additionalImage: nil, additionalImagePosition: .bottomRight)) +// } else if let result = result as? Signal { +// subject = result +// |> map { value -> MediaEditorScreenImpl.Subject? in +// switch value { +// case .pendingImage: +// return nil +// case let .image(image): +// return .image(image: image.image, dimensions: PixelDimensions(image.image.size), additionalImage: nil, additionalImagePosition: .topLeft) +// case let .video(video): +// return .video(videoPath: video.videoPath, thumbnail: video.coverImage, mirror: video.mirror, additionalVideoPath: nil, additionalThumbnail: nil, dimensions: video.dimensions, duration: video.duration, videoPositionChanges: [], additionalVideoPosition: .topLeft) +// default: +// return nil +// } +// } +// } else { +// let peerType: AvatarEditorScreen.PeerType +// if mode == .suggest { +// peerType = .suggest +// } else if case .legacyGroup = peer { +// peerType = .group +// } else if case let .channel(channel) = peer { +// if case .group = channel.info { +// peerType = channel.flags.contains(.isForum) ? .forum : .group +// } else { +// peerType = .channel +// } +// } else { +// peerType = .user +// } +// let controller = AvatarEditorScreen(context: self.context, inputData: keyboardInputData.get(), peerType: peerType, markup: emojiMarkup) +// //controller.imageCompletion = imageCompletion +// //controller.videoCompletion = videoCompletion +// parentController?.push(controller) +// //isFromEditor = true +// return +// } +// +// let editorController = MediaEditorScreenImpl( +// context: self.context, +// mode: .avatarEditor, +// subject: subject, +// transitionIn: fromCamera ? .camera : transitionView.flatMap({ .gallery( +// MediaEditorScreenImpl.TransitionIn.GalleryTransitionIn( +// sourceView: $0, +// sourceRect: transitionRect, +// sourceImage: transitionImage +// ) +// ) }), +// transitionOut: { finished, isNew in +// if !finished, let transitionView { +// return MediaEditorScreenImpl.TransitionOut( +// destinationView: transitionView, +// destinationRect: transitionView.bounds, +// destinationCornerRadius: 0.0 +// ) +// } +// return nil +// }, completion: { [weak self] result, commit in +// dismissImpl?() +// +// switch result.media { +// case let .image(image, _): +// self?.updateProfilePhoto(image, mode: mode) +// commit({}) +// case let .video(video, coverImage, values, _, _): +// if let coverImage { +// self?.updateProfileVideo(coverImage, asset: video, adjustments: values, mode: mode) +// } +// commit({}) +// default: +// break +// } +// } as (MediaEditorScreenImpl.Result, @escaping (@escaping () -> Void) -> Void) -> Void +// ) +// editorController.cancelled = { _ in +// cancelled() +// } +// self.push(editorController) +// }, dismissed: { +// +// }) +// dismissImpl = { [weak mainController] in +// if let mainController, let navigationController = mainController.navigationController { +// var viewControllers = navigationController.viewControllers +// viewControllers = viewControllers.filter { c in +// return !(c is CameraScreen) && c !== mainController +// } +// navigationController.setViewControllers(viewControllers, animated: false) +// } +// } +// mainController.navigationPresentation = .flatModal +// mainController.supportedOrientations = ViewControllerSupportedOrientations(regularSize: .all, compactSize: .portrait) +// self.push(mainController) +// }) +// } func openAvatarRemoval(mode: PeerInfoAvatarEditingMode, peer: EnginePeer? = nil, item: PeerInfoAvatarListItem? = nil, completion: @escaping () -> Void = {}) { let proceed = { [weak self] in @@ -368,7 +369,7 @@ extension PeerInfoScreenImpl { })) } - public func updateProfileVideo(_ image: UIImage, asset: Any?, adjustments: MediaEditorValues?, mode: PeerInfoAvatarEditingMode) { + public func updateProfileVideo(_ image: UIImage, asset: Any?, adjustments: TGVideoEditAdjustments?, mode: PeerInfoAvatarEditingMode) { guard let data = image.jpegData(compressionQuality: 0.6) else { return } diff --git a/submodules/TelegramUI/Components/PeerInfo/PeerInfoVisualMediaPaneNode/Sources/PeerInfoGiftsPaneNode.swift b/submodules/TelegramUI/Components/PeerInfo/PeerInfoVisualMediaPaneNode/Sources/PeerInfoGiftsPaneNode.swift index 5200e0b359..6ac674c843 100644 --- a/submodules/TelegramUI/Components/PeerInfo/PeerInfoVisualMediaPaneNode/Sources/PeerInfoGiftsPaneNode.swift +++ b/submodules/TelegramUI/Components/PeerInfo/PeerInfoVisualMediaPaneNode/Sources/PeerInfoGiftsPaneNode.swift @@ -234,6 +234,18 @@ public final class PeerInfoGiftsPaneNode: ASDisplayNode, PeerInfoPaneNode, UIScr return } self.profileGifts.convertStarGift(messageId: messageId) + }, + transferGift: { [weak self] prepaid, peerId in + guard let self, let messageId = product.messageId else { + return + } + self.profileGifts.transferStarGift(prepaid: prepaid, messageId: messageId, peerId: peerId) + }, + upgradeGift: { [weak self] formId, keepOriginalInfo in + guard let self, let messageId = product.messageId else { + return .never() + } + return self.profileGifts.upgradeStarGift(formId: formId, messageId: messageId, keepOriginalInfo: keepOriginalInfo) } ) self.parentController?.push(controller) diff --git a/submodules/TelegramUI/Components/Stars/StarsAvatarComponent/Sources/StarsAvatarComponent.swift b/submodules/TelegramUI/Components/Stars/StarsAvatarComponent/Sources/StarsAvatarComponent.swift index 976a5cde0a..ee90ffd29f 100644 --- a/submodules/TelegramUI/Components/Stars/StarsAvatarComponent/Sources/StarsAvatarComponent.swift +++ b/submodules/TelegramUI/Components/Stars/StarsAvatarComponent/Sources/StarsAvatarComponent.swift @@ -18,15 +18,26 @@ public final class StarsAvatarComponent: Component { let peer: StarsContext.State.Transaction.Peer? let photo: TelegramMediaWebFile? let media: [Media] + let uniqueGift: StarGift.UniqueGift? let backgroundColor: UIColor let size: CGSize? - public init(context: AccountContext, theme: PresentationTheme, peer: StarsContext.State.Transaction.Peer?, photo: TelegramMediaWebFile?, media: [Media], backgroundColor: UIColor, size: CGSize? = nil) { + public init( + context: AccountContext, + theme: PresentationTheme, + peer: StarsContext.State.Transaction.Peer?, + photo: TelegramMediaWebFile?, + media: [Media], + uniqueGift: StarGift.UniqueGift?, + backgroundColor: UIColor, + size: CGSize? = nil + ) { self.context = context self.theme = theme self.peer = peer self.photo = photo self.media = media + self.uniqueGift = uniqueGift self.backgroundColor = backgroundColor self.size = size } @@ -47,6 +58,9 @@ public final class StarsAvatarComponent: Component { if !areMediaArraysEqual(lhs.media, rhs.media) { return false } + if lhs.uniqueGift != rhs.uniqueGift { + return false + } if lhs.backgroundColor != rhs.backgroundColor { return false } diff --git a/submodules/TelegramUI/Components/Stars/StarsTransactionScreen/Sources/StarsTransactionScreen.swift b/submodules/TelegramUI/Components/Stars/StarsTransactionScreen/Sources/StarsTransactionScreen.swift index ff860f76e4..6eb28d7f2c 100644 --- a/submodules/TelegramUI/Components/Stars/StarsTransactionScreen/Sources/StarsTransactionScreen.swift +++ b/submodules/TelegramUI/Components/Stars/StarsTransactionScreen/Sources/StarsTransactionScreen.swift @@ -39,6 +39,7 @@ private final class StarsTransactionSheetContent: CombinedComponent { let openAppExamples: () -> Void let copyTransactionId: (String) -> Void let updateSubscription: () -> Void + let sendGift: (EnginePeer.Id) -> Void init( context: AccountContext, @@ -49,7 +50,8 @@ private final class StarsTransactionSheetContent: CombinedComponent { openMedia: @escaping ([Media], @escaping (Media) -> (ASDisplayNode, CGRect, () -> (UIView?, UIView?))?, @escaping (UIView) -> Void) -> Void, openAppExamples: @escaping () -> Void, copyTransactionId: @escaping (String) -> Void, - updateSubscription: @escaping () -> Void + updateSubscription: @escaping () -> Void, + sendGift: @escaping (EnginePeer.Id) -> Void ) { self.context = context self.subject = subject @@ -60,6 +62,7 @@ private final class StarsTransactionSheetContent: CombinedComponent { self.openAppExamples = openAppExamples self.copyTransactionId = copyTransactionId self.updateSubscription = updateSubscription + self.sendGift = sendGift } static func ==(lhs: StarsTransactionSheetContent, rhs: StarsTransactionSheetContent) -> Bool { @@ -80,6 +83,7 @@ private final class StarsTransactionSheetContent: CombinedComponent { var peerMap: [EnginePeer.Id: EnginePeer] = [:] var cachedCloseImage: (UIImage, PresentationTheme)? + var cachedOverlayCloseImage: UIImage? var cachedChevronImage: (UIImage, PresentationTheme)? var inProgress = false @@ -149,7 +153,7 @@ private final class StarsTransactionSheetContent: CombinedComponent { let title = Child(MultilineTextComponent.self) let star = Child(StarsImageComponent.self) let activeStar = Child(PremiumStarComponent.self) - let gift = Child(GiftAnimationComponent.self) + let gift = Child(GiftCompositionComponent.self) let amountBackground = Child(RoundedRectangle.self) let amount = Child(BalancedTextComponent.self) let amountStar = Child(BundleIconComponent.self) @@ -188,17 +192,14 @@ private final class StarsTransactionSheetContent: CombinedComponent { state.cachedCloseImage = (closeImage, theme) } - let closeButton = closeButton.update( - component: Button( - content: AnyComponent(Image(image: closeImage)), - action: { [weak component] in - component?.cancel(true) - } - ), - availableSize: CGSize(width: 30.0, height: 30.0), - transition: .immediate - ) - + let closeOverlayImage: UIImage + if let image = state.cachedOverlayCloseImage { + closeOverlayImage = image + } else { + closeOverlayImage = generateCloseButtonImage(backgroundColor: UIColor(rgb: 0xffffff, alpha: 0.1), foregroundColor: .white)! + state.cachedOverlayCloseImage = closeOverlayImage + } + let titleText: String let amountText: String var descriptionText: String @@ -219,7 +220,6 @@ private final class StarsTransactionSheetContent: CombinedComponent { var via: String? var messageId: EngineMessage.Id? var toPeer: EnginePeer? -// var toString: String? var transactionPeer: StarsContext.State.Transaction.Peer? var media: [AnyMediaReference] = [] var photo: TelegramMediaWebFile? @@ -234,7 +234,9 @@ private final class StarsTransactionSheetContent: CombinedComponent { var isReaction = false var giveawayMessageId: MessageId? var isBoost = false - var giftAnimation: TelegramMediaFile? + var giftAnimationSubject: GiftCompositionComponent.Subject? + var isGiftUpgrade = false + var giftAvailability: StarGift.Gift.Availability? var isRefProgram = false var delayedCloseOnOpenPeer = true @@ -250,7 +252,6 @@ private final class StarsTransactionSheetContent: CombinedComponent { count = StarsAmount(value: stars, nanos: 0) date = boost.date toPeer = state.peerMap[peerId] -// toString = strings.Stars_Transaction_Giveaway_Boost_Subscribers(boost.quantity) giveawayMessageId = boost.giveawayMessageId isBoost = true case let .importer(peer, pricing, importer, usdRate): @@ -368,9 +369,15 @@ private final class StarsTransactionSheetContent: CombinedComponent { toPeer = peer } transactionPeer = transaction.peer - if case let .generic(gift) = starGift { - giftAnimation = gift.file + + switch starGift { + case let .generic(gift): + giftAnimationSubject = .generic(gift.file) + giftAvailability = gift.availability + case let .unique(gift): + giftAnimationSubject = .unique(gift) } + isGiftUpgrade = transaction.flags.contains(.isStarGiftUpgrade) } else if let giveawayMessageIdValue = transaction.giveawayMessageId { titleText = strings.Stars_Transaction_Giveaway_Title descriptionText = "" @@ -583,6 +590,28 @@ private final class StarsTransactionSheetContent: CombinedComponent { descriptionText = modifiedString } + var closeButtonImage = closeImage + if case .unique = giftAnimationSubject { + closeButtonImage = closeOverlayImage + } + let closeButton = closeButton.update( + component: Button( + content: AnyComponent(Image(image: closeButtonImage)), + action: { [weak component] in + component?.cancel(true) + } + ), + availableSize: CGSize(width: 30.0, height: 30.0), + transition: .immediate + ) + + let headerTextColor: UIColor + if case .unique = giftAnimationSubject { + headerTextColor = .white + } else { + headerTextColor = theme.actionSheet.primaryTextColor + } + let absCount = StarsAmount(value: abs(count.value), nanos: abs(count.nanos)) let formattedAmount = presentationStringsFormattedNumber(absCount, dateTimeFormat.groupingSeparator) let countColor: UIColor @@ -601,18 +630,22 @@ private final class StarsTransactionSheetContent: CombinedComponent { countColor = theme.list.itemPrimaryTextColor } else if count < StarsAmount.zero { amountText = "- \(formattedAmount)" - countColor = theme.list.itemDestructiveColor + if case .unique = giftAnimationSubject { + countColor = .white + } else { + countColor = theme.list.itemDestructiveColor + } } else { amountText = "+ \(formattedAmount)" countColor = theme.list.itemDisclosureActions.constructive.fillColor } - + let title = title.update( component: MultilineTextComponent( text: .plain(NSAttributedString( string: titleText, font: Font.bold(25.0), - textColor: theme.actionSheet.primaryTextColor, + textColor: headerTextColor, paragraphAlignment: .center )), horizontalAlignment: .center, @@ -647,17 +680,25 @@ private final class StarsTransactionSheetContent: CombinedComponent { imageIcon = nil } + var starOriginY: CGFloat = 81.0 var starChild: _UpdatedChildComponent - if let giftAnimation { + if let giftAnimationSubject { + let animationHeight: CGFloat + if case .unique = giftAnimationSubject { + animationHeight = 240.0 + } else { + animationHeight = 210.0 + } starChild = gift.update( - component: GiftAnimationComponent( + component: GiftCompositionComponent( context: component.context, theme: theme, - file: giftAnimation + subject: giftAnimationSubject ), - availableSize: CGSize(width: 128.0, height: 128.0), + availableSize: CGSize(width: context.availableSize.width, height: animationHeight), transition: .immediate ) + starOriginY = animationHeight / 2.0 } else if isBoost { starChild = activeStar.update( component: PremiumStarComponent( @@ -721,6 +762,16 @@ private final class StarsTransactionSheetContent: CombinedComponent { let tableLinkColor = theme.list.itemAccentColor var tableItems: [TableComponent.Item] = [] + if isGiftUpgrade { + tableItems.append(.init( + id: "reason", + title: strings.Stars_Transaction_Giveaway_Reason, + component: AnyComponent( + MultilineTextComponent(text: .plain(NSAttributedString(string: strings.Stars_Transaction_GiftUpgrade, font: tableFont, textColor: tableTextColor))) + ) + )) + } + if isGift, toPeer == nil { tableItems.append(.init( id: "from", @@ -746,7 +797,9 @@ private final class StarsTransactionSheetContent: CombinedComponent { )) } else if let toPeer, !isRefProgram { let title: String - if isSubscription { + if isGiftUpgrade { + title = strings.Stars_Transaction_GiftFrom + } else if isSubscription { if isBotSubscription { title = strings.Stars_Transaction_Subscription_Bot } else if isBusinessSubscription { @@ -759,10 +812,56 @@ private final class StarsTransactionSheetContent: CombinedComponent { } else { title = count < StarsAmount.zero || countIsGeneric ? strings.Stars_Transaction_To : strings.Stars_Transaction_From } - tableItems.append(.init( - id: "to", - title: title, - component: AnyComponent( + + let toComponent: AnyComponent + if let _ = giftAnimationSubject, !toPeer.isDeleted && !isGiftUpgrade { + toComponent = AnyComponent( + HStack([ + AnyComponentWithIdentity( + id: AnyHashable(0), + component: AnyComponent(Button( + content: AnyComponent( + PeerCellComponent( + context: component.context, + theme: theme, + peer: toPeer + ) + ), + action: { + if delayedCloseOnOpenPeer { + component.openPeer(toPeer, false) + Queue.mainQueue().after(1.0, { + component.cancel(false) + }) + } else { + if let controller = controller() as? StarsTransactionScreen, let navigationController = controller.navigationController, let chatController = navigationController.viewControllers.first(where: { $0 is ChatController }) as? ChatController { + chatController.playShakeAnimation() + } + component.cancel(true) + } + } + )) + ), + AnyComponentWithIdentity( + id: AnyHashable(1), + component: AnyComponent(Button( + content: AnyComponent(ButtonContentComponent( + context: component.context, + text: strings.Gift_View_Send, + color: theme.list.itemAccentColor + )), + action: { + component.sendGift(toPeer.id) + Queue.mainQueue().after(1.0, { + component.cancel(false) + }) + } + )) + ) + ], spacing: 4.0) + ) + } else { + toComponent = AnyComponent( Button( content: AnyComponent( PeerCellComponent( @@ -786,6 +885,11 @@ private final class StarsTransactionSheetContent: CombinedComponent { } ) ) + } + tableItems.append(.init( + id: "to", + title: title, + component: toComponent )) if case let .subscription(subscription) = component.subject, let title = subscription.title { tableItems.append(.init( @@ -1001,6 +1105,7 @@ private final class StarsTransactionSheetContent: CombinedComponent { Button( content: AnyComponent( TransactionCellComponent( + backgroundColor: theme.actionSheet.opaqueItemBackgroundColor, textColor: tableTextColor, accentColor: tableLinkColor, transactionId: transactionId @@ -1048,6 +1153,17 @@ private final class StarsTransactionSheetContent: CombinedComponent { MultilineTextComponent(text: .plain(NSAttributedString(string: stringForMediumDate(timestamp: date, strings: strings, dateTimeFormat: dateTimeFormat), font: tableFont, textColor: tableTextColor))) ) )) + if let giftAvailability { + let remainsString = presentationStringsFormattedNumber(giftAvailability.remains, environment.dateTimeFormat.groupingSeparator) + let totalString = presentationStringsFormattedNumber(giftAvailability.total, environment.dateTimeFormat.groupingSeparator) + tableItems.append(.init( + id: "availability", + title: strings.Gift_View_Availability, + component: AnyComponent( + MultilineTextComponent(text: .plain(NSAttributedString(string: strings.Gift_View_Availability_NewOf("\(remainsString)", "\(totalString)").string, font: tableFont, textColor: tableTextColor))) + ) + )) + } if isSubscriber, let additionalDate { tableItems.append(.init( @@ -1103,15 +1219,17 @@ private final class StarsTransactionSheetContent: CombinedComponent { ) context.add(starChild - .position(CGPoint(x: context.availableSize.width / 2.0, y: 200.0 / 2.0 - 19.0)) + .position(CGPoint(x: context.availableSize.width / 2.0, y: starOriginY)) ) + var originY: CGFloat = 156.0 + if let _ = giftAnimationSubject { + originY += 18.0 + } context.add(title - .position(CGPoint(x: context.availableSize.width / 2.0, y: 31.0 + 125.0)) + .position(CGPoint(x: context.availableSize.width / 2.0, y: originY)) ) - - var originY: CGFloat = 0.0 - originY += 200.0 - 23.0 + originY += 21.0 var descriptionSize: CGSize = .zero if !descriptionText.isEmpty { @@ -1237,6 +1355,10 @@ private final class StarsTransactionSheetContent: CombinedComponent { context.add(amountStar .position(CGPoint(x: amountStarOriginX, y: amountOrigin + amountStar.size.height / 2.0 - UIScreenPixel + amountStarOffsetY)) ) + + if case .unique = giftAnimationSubject { + originY += 21.0 + } context.add(table .position(CGPoint(x: context.availableSize.width / 2.0, y: originY + table.size.height / 2.0)) @@ -1353,6 +1475,7 @@ private final class StarsTransactionSheetComponent: CombinedComponent { let openAppExamples: () -> Void let copyTransactionId: (String) -> Void let updateSubscription: () -> Void + let sendGift: (EnginePeer.Id) -> Void init( context: AccountContext, @@ -1362,7 +1485,8 @@ private final class StarsTransactionSheetComponent: CombinedComponent { openMedia: @escaping ([Media], @escaping (Media) -> (ASDisplayNode, CGRect, () -> (UIView?, UIView?))?, @escaping (UIView) -> Void) -> Void, openAppExamples: @escaping () -> Void, copyTransactionId: @escaping (String) -> Void, - updateSubscription: @escaping () -> Void + updateSubscription: @escaping () -> Void, + sendGift: @escaping (EnginePeer.Id) -> Void ) { self.context = context self.subject = subject @@ -1372,6 +1496,7 @@ private final class StarsTransactionSheetComponent: CombinedComponent { self.openAppExamples = openAppExamples self.copyTransactionId = copyTransactionId self.updateSubscription = updateSubscription + self.sendGift = sendGift } static func ==(lhs: StarsTransactionSheetComponent, rhs: StarsTransactionSheetComponent) -> Bool { @@ -1416,7 +1541,8 @@ private final class StarsTransactionSheetComponent: CombinedComponent { openMedia: context.component.openMedia, openAppExamples: context.component.openAppExamples, copyTransactionId: context.component.copyTransactionId, - updateSubscription: context.component.updateSubscription + updateSubscription: context.component.updateSubscription, + sendGift: context.component.sendGift )), backgroundColor: .color(environment.theme.actionSheet.opaqueItemBackgroundColor), followContentSizeChanges: true, @@ -1516,6 +1642,7 @@ public class StarsTransactionScreen: ViewControllerComponentContainer { var openAppExamplesImpl: (() -> Void)? var copyTransactionIdImpl: ((String) -> Void)? var updateSubscriptionImpl: (() -> Void)? + var sendGiftImpl: ((EnginePeer.Id) -> Void)? super.init( context: context, @@ -1539,6 +1666,9 @@ public class StarsTransactionScreen: ViewControllerComponentContainer { }, updateSubscription: { updateSubscriptionImpl?() + }, + sendGift: { peerId in + sendGiftImpl?(peerId) } ), navigationBarAppearance: .none, @@ -1690,6 +1820,19 @@ public class StarsTransactionScreen: ViewControllerComponentContainer { } } } + + sendGiftImpl = { [weak self] peerId in + guard let self else { + return + } + let _ = (context.engine.payments.premiumGiftCodeOptions(peerId: nil, onlyCached: true) + |> filter { !$0.isEmpty } + |> deliverOnMainQueue).start(next: { giftOptions in + let premiumOptions = giftOptions.filter { $0.users == 1 }.map { CachedPremiumGiftOption(months: $0.months, currency: $0.currency, amount: $0.amount, botUrl: "", storeProductId: $0.storeProductId) } + let controller = context.sharedContext.makeGiftOptionsController(context: context, peerId: peerId, premiumOptions: premiumOptions, hasBirthday: false) + self.push(controller) + }) + } } required public init(coder aDecoder: NSCoder) { @@ -2011,7 +2154,7 @@ private final class PeerCellComponent: Component { let avatarNaturalSize = self.avatar.update( transition: .immediate, component: AnyComponent( - StarsAvatarComponent(context: component.context, theme: component.theme, peer: peer, photo: nil, media: [], backgroundColor: .clear) + StarsAvatarComponent(context: component.context, theme: component.theme, peer: peer, photo: nil, media: [], uniqueGift: nil, backgroundColor: .clear) ), environment: {}, containerSize: CGSize(width: 40.0, height: 40.0) @@ -2063,18 +2206,23 @@ private final class PeerCellComponent: Component { } private final class TransactionCellComponent: Component { + let backgroundColor: UIColor let textColor: UIColor let accentColor: UIColor let transactionId: String - init(textColor: UIColor, accentColor: UIColor, transactionId: String) { + init(backgroundColor: UIColor, textColor: UIColor, accentColor: UIColor, transactionId: String) { + self.backgroundColor = backgroundColor self.textColor = textColor self.accentColor = accentColor self.transactionId = transactionId } static func ==(lhs: TransactionCellComponent, rhs: TransactionCellComponent) -> Bool { - if lhs.textColor !== rhs.textColor { + if lhs.backgroundColor != rhs.backgroundColor { + return false + } + if lhs.textColor != rhs.textColor { return false } if lhs.accentColor != rhs.accentColor { @@ -2089,12 +2237,17 @@ private final class TransactionCellComponent: Component { final class View: UIView { private let text = ComponentView() private let button = ComponentView() + private let gradientView = UIImageView() private var component: TransactionCellComponent? private weak var state: EmptyComponentState? override init(frame: CGRect) { super.init(frame: frame) + + self.layer.allowsGroupOpacity = true + + self.gradientView.image = generateGradientImage(size: CGSize(width: 40.0, height: 1.0), colors: [UIColor.white.withAlphaComponent(0.0), UIColor.white, UIColor.white], locations: [0.0, 0.65, 1.0], direction: .horizontal)?.withRenderingMode(.alwaysTemplate) } required init?(coder: NSCoder) { @@ -2104,55 +2257,49 @@ private final class TransactionCellComponent: Component { func update(component: TransactionCellComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { self.component = component self.state = state - - let spacing: CGFloat = 6.0 + + self.gradientView.tintColor = component.backgroundColor let buttonSize = self.button.update( transition: .immediate, component: AnyComponent( - BundleIconComponent(name: "Chat/Context Menu/Copy", tintColor: component.accentColor) + BundleIconComponent( + name: "Chat/Context Menu/Copy", + tintColor: component.accentColor + ) ), environment: {}, containerSize: CGSize(width: availableSize.width, height: availableSize.height) ) - - func brokenLine(_ string: String) -> String { - if string.count > 30 { - return string - } - let middleIndex = string.index(string.startIndex, offsetBy: string.count / 2) - var newString = string - newString.insert("\n", at: middleIndex) - return newString - } - - let text: String - if availableSize.width > 230.0 { - text = component.transactionId - } else { - text = brokenLine(component.transactionId) - } - + let textSize = self.text.update( transition: .immediate, component: AnyComponent( MultilineTextComponent( text: .plain(NSAttributedString( - string: text, + string: component.transactionId, font: Font.monospace(15.0), textColor: component.textColor, paragraphAlignment: .left )), - maximumNumberOfLines: 0, - lineSpacing: 0.2 + maximumNumberOfLines: 1 ) ), environment: {}, - containerSize: CGSize(width: availableSize.width - buttonSize.width - spacing, height: availableSize.height) + containerSize: CGSize(width: availableSize.width - buttonSize.width + 10.0, height: availableSize.height) ) let size = CGSize(width: availableSize.width, height: textSize.height) + let textFrame = CGRect(origin: CGPoint(x: 0.0, y: floorToScreenPixels((size.height - textSize.height) / 2.0) + 1.0), size: textSize) + if let textView = self.text.view { + if textView.superview == nil { + self.addSubview(textView) + self.addSubview(self.gradientView) + } + transition.setFrame(view: textView, frame: textFrame) + } + let buttonFrame = CGRect(origin: CGPoint(x: availableSize.width - buttonSize.width - 2.0, y: floorToScreenPixels((size.height - buttonSize.height) / 2.0)), size: buttonSize) if let buttonView = self.button.view { if buttonView.superview == nil { @@ -2161,13 +2308,7 @@ private final class TransactionCellComponent: Component { transition.setFrame(view: buttonView, frame: buttonFrame) } - let textFrame = CGRect(origin: CGPoint(x: 0.0, y: floorToScreenPixels((size.height - textSize.height) / 2.0) + 1.0), size: textSize) - if let textView = self.text.view { - if textView.superview == nil { - self.addSubview(textView) - } - transition.setFrame(view: textView, frame: textFrame) - } + self.gradientView.frame = CGRect(x: size.width - buttonSize.width - 32.0, y: 0.0, width: 40.0, height: size.height) return size } @@ -2202,3 +2343,92 @@ private func generateCloseButtonImage(backgroundColor: UIColor, foregroundColor: context.strokePath() }) } + +private final class ButtonContentComponent: Component { + let context: AccountContext + let text: String + let color: UIColor + + public init( + context: AccountContext, + text: String, + color: UIColor + ) { + self.context = context + self.text = text + self.color = color + } + + public static func ==(lhs: ButtonContentComponent, rhs: ButtonContentComponent) -> Bool { + if lhs.context !== rhs.context { + return false + } + if lhs.text != rhs.text { + return false + } + if lhs.color != rhs.color { + return false + } + return true + } + + public final class View: UIView { + private var component: ButtonContentComponent? + private weak var componentState: EmptyComponentState? + + private let backgroundLayer = SimpleLayer() + private let title = ComponentView() + + override init(frame: CGRect) { + super.init(frame: frame) + + self.layer.addSublayer(self.backgroundLayer) + self.backgroundLayer.masksToBounds = true + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + func update(component: ButtonContentComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { + self.component = component + self.componentState = state + + let attributedText = NSAttributedString(string: component.text, font: Font.regular(11.0), textColor: component.color) + let titleSize = self.title.update( + transition: transition, + component: AnyComponent( + MultilineTextComponent(text: .plain(attributedText)) + ), + environment: {}, + containerSize: availableSize + ) + + let padding: CGFloat = 6.0 + let size = CGSize(width: titleSize.width + padding * 2.0, height: 18.0) + + let titleFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((size.width - titleSize.width) / 2.0), y: floorToScreenPixels((size.height - titleSize.height) / 2.0)), size: titleSize) + if let titleView = self.title.view { + if titleView.superview == nil { + self.addSubview(titleView) + } + transition.setFrame(view: titleView, frame: titleFrame) + } + + let backgroundColor = component.color.withAlphaComponent(0.1) + self.backgroundLayer.backgroundColor = backgroundColor.cgColor + transition.setFrame(layer: self.backgroundLayer, frame: CGRect(origin: .zero, size: size)) + self.backgroundLayer.cornerRadius = size.height / 2.0 + + return size + } + } + + public func makeView() -> View { + return View(frame: CGRect()) + } + + public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { + return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) + } +} diff --git a/submodules/TelegramUI/Components/Stars/StarsTransactionsScreen/BUILD b/submodules/TelegramUI/Components/Stars/StarsTransactionsScreen/BUILD index e1d1eb0f04..393e97bd63 100644 --- a/submodules/TelegramUI/Components/Stars/StarsTransactionsScreen/BUILD +++ b/submodules/TelegramUI/Components/Stars/StarsTransactionsScreen/BUILD @@ -49,6 +49,7 @@ swift_library( "//submodules/TelegramUI/Components/Stars/StarsAvatarComponent", "//submodules/TelegramUI/Components/LottieComponent", "//submodules/TelegramUI/Components/LottieComponentResourceContent", + "//submodules/TelegramUI/Components/Gifts/GiftAnimationComponent", ], visibility = [ "//visibility:public", diff --git a/submodules/TelegramUI/Components/Stars/StarsTransactionsScreen/Sources/StarsTransactionsListPanelComponent.swift b/submodules/TelegramUI/Components/Stars/StarsTransactionsScreen/Sources/StarsTransactionsListPanelComponent.swift index b55d747d0a..f8f61409c3 100644 --- a/submodules/TelegramUI/Components/Stars/StarsTransactionsScreen/Sources/StarsTransactionsListPanelComponent.swift +++ b/submodules/TelegramUI/Components/Stars/StarsTransactionsScreen/Sources/StarsTransactionsListPanelComponent.swift @@ -16,7 +16,7 @@ import AvatarNode import BundleIconComponent import PhotoResources import StarsAvatarComponent -import LottieComponent +import GiftAnimationComponent private extension StarsContext.State.Transaction { var extendedId: String { @@ -300,13 +300,20 @@ final class StarsTransactionsListPanelComponent: Component { var itemDate: String var itemPeer = item.peer var itemFile: TelegramMediaFile? + var uniqueGift: StarGift.UniqueGift? switch item.peer { case let .peer(peer): if let starGift = item.starGift { - itemTitle = peer.displayTitle(strings: environment.strings, displayOrder: .firstLast) - itemSubtitle = item.count > StarsAmount.zero ? environment.strings.Stars_Intro_Transaction_ConvertedGift : environment.strings.Stars_Intro_Transaction_Gift - if case let .generic(gift) = starGift { - itemFile = gift.file + if item.flags.contains(.isStarGiftUpgrade), case let .unique(gift) = starGift { + itemTitle = "\(gift.title) #\(gift.number)" + itemSubtitle = environment.strings.Stars_Intro_Transaction_GiftUpgrade + uniqueGift = gift + } else { + itemTitle = peer.displayTitle(strings: environment.strings, displayOrder: .firstLast) + itemSubtitle = item.count > StarsAmount.zero ? environment.strings.Stars_Intro_Transaction_ConvertedGift : environment.strings.Stars_Intro_Transaction_Gift + if case let .generic(gift) = starGift { + itemFile = gift.file + } } } else if let _ = item.giveawayMessageId { itemTitle = peer.displayTitle(strings: environment.strings, displayOrder: .firstLast) @@ -394,7 +401,7 @@ final class StarsTransactionsListPanelComponent: Component { itemDate += " – \(environment.strings.Monetization_Transaction_Failed)" itemDateColor = environment.theme.list.itemDestructiveColor } - + var titleComponents: [AnyComponentWithIdentity] = [] titleComponents.append( AnyComponentWithIdentity(id: AnyHashable(0), component: AnyComponent(MultilineTextComponent( @@ -411,24 +418,23 @@ final class StarsTransactionsListPanelComponent: Component { if let itemFile { subtitleComponent = AnyComponent( HStack([ - AnyComponentWithIdentity(id: AnyHashable(0), component: AnyComponent(LottieComponent( - content: LottieComponent.ResourceContent( + AnyComponentWithIdentity(id: AnyHashable(0), component: AnyComponent( + GiftAnimationComponent( context: component.context, + theme: environment.theme, file: itemFile, - attemptSynchronously: false, - providesPlaceholder: true - ), - color: nil, - placeholderColor: environment.theme.list.mediaPlaceholderColor, - size: CGSize(width: 20.0, height: 20.0), - loop: false - ))), - AnyComponentWithIdentity(id: AnyHashable(1), component: AnyComponent(MultilineTextComponent( - text: .plain(NSAttributedString( - string: itemSubtitle, - font: Font.regular(fontBaseDisplaySize * 16.0 / 17.0), - textColor: environment.theme.list.itemPrimaryTextColor - )) + still: true, + size: CGSize(width: 20.0, height: 20.0) + ) + )), + AnyComponentWithIdentity(id: AnyHashable(1), component: AnyComponent( + MultilineTextComponent( + text: .plain(NSAttributedString( + string: itemSubtitle, + font: Font.regular(fontBaseDisplaySize * 16.0 / 17.0), + textColor: environment.theme.list.itemPrimaryTextColor + ) + ) ))) ], spacing: 2.0) ) @@ -463,7 +469,7 @@ final class StarsTransactionsListPanelComponent: Component { theme: environment.theme, title: AnyComponent(VStack(titleComponents, alignment: .left, spacing: 2.0)), contentInsets: UIEdgeInsets(top: 9.0, left: environment.containerInsets.left, bottom: 8.0, right: environment.containerInsets.right), - leftIcon: .custom(AnyComponentWithIdentity(id: "avatar", component: AnyComponent(StarsAvatarComponent(context: component.context, theme: environment.theme, peer: itemPeer, photo: item.photo, media: item.media, backgroundColor: environment.theme.list.plainBackgroundColor))), false), + leftIcon: .custom(AnyComponentWithIdentity(id: "avatar", component: AnyComponent(StarsAvatarComponent(context: component.context, theme: environment.theme, peer: itemPeer, photo: item.photo, media: item.media, uniqueGift: uniqueGift, backgroundColor: environment.theme.list.plainBackgroundColor))), false), icon: nil, accessory: .custom(ListActionItemComponent.CustomAccessory(component: AnyComponentWithIdentity(id: "label", component: AnyComponent(StarsLabelComponent(text: itemLabel))), insets: UIEdgeInsets(top: 0, left: 0, bottom: 0, right: 16.0))), action: { [weak self] _ in diff --git a/submodules/TelegramUI/Components/Stars/StarsTransactionsScreen/Sources/StarsTransactionsScreen.swift b/submodules/TelegramUI/Components/Stars/StarsTransactionsScreen/Sources/StarsTransactionsScreen.swift index 424777c77e..c3110b3a10 100644 --- a/submodules/TelegramUI/Components/Stars/StarsTransactionsScreen/Sources/StarsTransactionsScreen.swift +++ b/submodules/TelegramUI/Components/Stars/StarsTransactionsScreen/Sources/StarsTransactionsScreen.swift @@ -765,7 +765,7 @@ final class StarsTransactionsScreenComponent: Component { if let photo = subscription.photo { nameGroupComponent = AnyComponent( HStack([ - AnyComponentWithIdentity(id: AnyHashable(0), component: AnyComponent(StarsAvatarComponent(context: component.context, theme: environment.theme, peer: nil, photo: photo, media: [], backgroundColor: .clear, size: CGSize(width: 19.0, height: 19.0)))), + AnyComponentWithIdentity(id: AnyHashable(0), component: AnyComponent(StarsAvatarComponent(context: component.context, theme: environment.theme, peer: nil, photo: photo, media: [], uniqueGift: nil, backgroundColor: .clear, size: CGSize(width: 19.0, height: 19.0)))), AnyComponentWithIdentity(id: AnyHashable(1), component: nameComponent) ], spacing: 6.0) ) @@ -806,7 +806,7 @@ final class StarsTransactionsScreenComponent: Component { theme: environment.theme, title: AnyComponent(VStack(titleComponents, alignment: .left, spacing: 2.0)), contentInsets: UIEdgeInsets(top: 9.0, left: 0.0, bottom: 8.0, right: 0.0), - leftIcon: .custom(AnyComponentWithIdentity(id: "avatar", component: AnyComponent(StarsAvatarComponent(context: component.context, theme: environment.theme, peer: .peer(subscription.peer), photo: nil, media: [], backgroundColor: environment.theme.list.plainBackgroundColor))), false), + leftIcon: .custom(AnyComponentWithIdentity(id: "avatar", component: AnyComponent(StarsAvatarComponent(context: component.context, theme: environment.theme, peer: .peer(subscription.peer), photo: nil, media: [], uniqueGift: nil, backgroundColor: environment.theme.list.plainBackgroundColor))), false), icon: nil, accessory: .custom(ListActionItemComponent.CustomAccessory(component: labelComponent, insets: UIEdgeInsets(top: 0, left: 0, bottom: 0, right: 16.0))), action: { [weak self] _ in diff --git a/submodules/TelegramUI/Sources/ChatController.swift b/submodules/TelegramUI/Sources/ChatController.swift index 4f13cdf748..0737b4d93c 100644 --- a/submodules/TelegramUI/Sources/ChatController.swift +++ b/submodules/TelegramUI/Sources/ChatController.swift @@ -1228,8 +1228,8 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G } controller.videoCompletion = { [weak self] image, url, adjustments, commit in if let strongSelf = self { - if let rootController = strongSelf.effectiveNavigationController as? TelegramRootController, let _ = rootController.accountSettingsController as? PeerInfoScreenImpl { - //settingsController.updateProfileVideo(image, mode: .accept, asset: AVURLAsset(url: url), adjustments: adjustments) + if let rootController = strongSelf.effectiveNavigationController as? TelegramRootController, let settingsController = rootController.accountSettingsController as? PeerInfoScreenImpl { + settingsController.updateProfileVideo(image, asset: AVURLAsset(url: url), adjustments: adjustments, mode: .accept) commit() } } @@ -1263,8 +1263,8 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G } }, videoCompletion: { [weak self] image, url, adjustments in if let strongSelf = self { - if let rootController = strongSelf.effectiveNavigationController as? TelegramRootController, let _ = rootController.accountSettingsController as? PeerInfoScreenImpl { - //settingsController.updateProfileVideo(image, mode: .accept, asset: AVURLAsset(url: url), adjustments: adjustments) + if let rootController = strongSelf.effectiveNavigationController as? TelegramRootController, let settingsController = rootController.accountSettingsController as? PeerInfoScreenImpl { + settingsController.updateProfileVideo(image, asset: AVURLAsset(url: url), adjustments: adjustments, mode: .accept) } } }) diff --git a/submodules/TelegramUI/Sources/SharedAccountContext.swift b/submodules/TelegramUI/Sources/SharedAccountContext.swift index 457f6812d8..a18a74c999 100644 --- a/submodules/TelegramUI/Sources/SharedAccountContext.swift +++ b/submodules/TelegramUI/Sources/SharedAccountContext.swift @@ -2472,12 +2472,31 @@ public final class SharedAccountContextImpl: SharedAccountContext { } presentTransferAlertImpl = { [weak controller] peer in - guard let controller, case let .starGiftTransfer(_, messageId, gift, transferStars, _) = source else { + guard let controller, case let .starGiftTransfer(_, _, gift, transferStars, _) = source else { return } let alertController = giftTransferAlertController(context: context, gift: gift, peer: peer, transferStars: transferStars, commit: { [weak controller] in - controller?.dismiss() - let _ = context.engine.payments.transferStarGift(prepaid: transferStars == 0, messageId: messageId, peerId: peer.id).start() + completion?([peer.id]) + + guard let controller, let navigationController = controller.navigationController as? NavigationController else { + return + } + var controllers = navigationController.viewControllers + controllers = controllers.filter { !($0 is ContactSelectionController) } + var foundController = false + for controller in controllers.reversed() { + if let chatController = controller as? ChatController, case .peer(id: peer.id) = chatController.chatLocation { + chatController.hintPlayNextOutgoingGift() + foundController = true + break + } + } + if !foundController { + let chatController = context.sharedContext.makeChatController(context: context, chatLocation: .peer(id: peer.id), subject: nil, botStart: nil, mode: .standard(.default), params: nil) + chatController.hintPlayNextOutgoingGift() + controllers.append(chatController) + } + navigationController.setViewControllers(controllers, animated: true) }) controller.present(alertController, in: .window(.root)) }