Various improvements

This commit is contained in:
Ilya Laktyushin 2024-12-27 23:04:34 +04:00
parent e3ee1dde7e
commit 5d4213c4fc
41 changed files with 1193 additions and 545 deletions

View File

@ -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<Bool, NoError>
if reportDelivery, let messageId {
reportDeliverySignal = _internal_reportMessageDelivery(postbox: stateManager.postbox, network: stateManager.network, messageIds: [messageId], fromPushNotification: true)
|> mapToSignal { _ -> Signal<Bool, NoError> 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

Binary file not shown.

View File

@ -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";

View File

@ -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

View File

@ -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()

View File

@ -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

View File

@ -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))

View File

@ -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()

View File

@ -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

View File

@ -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<MessageId>
let reportMessageDelivery: Set<MessageId>
}
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<MessageId>
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<PeerId> = 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<MessageId> = 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<PeerId> = 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<MessageId> = Set(), reportMessageDelivery: Set<MessageId> = 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)
}
}

View File

@ -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

View File

@ -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<PeerId>()
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<MessageId>()
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
)
}

View File

@ -352,6 +352,7 @@ public final class AccountStateManager {
private let appliedMaxMessageIdDisposable = MetaDisposable()
private let appliedQtsPromise = Promise<Int32?>(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)
}

View File

@ -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
}
}

View File

@ -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")

View File

@ -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<Never, NoError> {
var signals: [Signal<Void, NoError>] = []
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<Void, NoError> {
return postbox.transaction { transaction -> Signal<Void, NoError> 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<Api.Bool, NoError> in
return .single(.boolFalse)
}
|> mapToSignal { _ in
return .complete()
}
}
|> switchToLatest
}

View File

@ -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<Api.payments.PaymentForm, BotPaymentFormRequestError> in
|> `catch` { error -> Signal<Api.payments.PaymentForm, BotPaymentFormRequestError> in
if error.errorDescription == "NO_PAYMENT_NEEDED" {
return .fail(.noPaymentNeeded)
}
return .fail(.generic)
}
|> mapToSignal { result -> Signal<BotPaymentForm, BotPaymentFormRequestError> 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)
}

View File

@ -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<BotPaymentForm?, TransferStarGiftError> 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<ProfileGiftsContext.State.StarGift, UpgradeStarGiftError> {
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<ProfileGiftsContext.State.StarGift, UpgradeStarGiftError> {
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
}
}

View File

@ -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)
}

View File

@ -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()))

View File

@ -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

View File

@ -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",

View File

@ -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)
}

View File

@ -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<Empty>, 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<Empty>, 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

View File

@ -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)
)
]
}

View File

@ -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,

View File

@ -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",
],

View File

@ -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

View File

@ -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<ProfileGiftsContext.State.StarGift, UpgradeStarGiftError>)
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<ProfileGiftsContext.State.StarGift, UpgradeStarGiftError>),
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<ProfileGiftsContext.State.StarGift, UpgradeStarGiftError>)
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<ProfileGiftsContext.State.StarGift, UpgradeStarGiftError>),
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<Empty>
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<ProfileGiftsContext.State.StarGift, UpgradeStarGiftError>)
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<ProfileGiftsContext.State.StarGift, UpgradeStarGiftError>),
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<ProfileGiftsContext.State.StarGift, UpgradeStarGiftError>)? = 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<ProfileGiftsContext.State.StarGift, UpgradeStarGiftError>)?
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() {

View File

@ -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

View File

@ -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)

View File

@ -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))

View File

@ -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<AvatarKeyboardInputData>()
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<MediaEditorScreenImpl.Subject?, NoError>
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<CameraScreenImpl.Result, NoError> {
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<AvatarKeyboardInputData>()
// 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<MediaEditorScreenImpl.Subject?, NoError>
// 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<CameraScreenImpl.Result, NoError> {
// 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
}

View File

@ -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)

View File

@ -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
}

View File

@ -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<Empty>
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<Empty>()
private let button = ComponentView<Empty>()
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<Empty>, 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<Empty>()
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<Empty>, 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<Empty>, transition: ComponentTransition) -> CGSize {
return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition)
}
}

View File

@ -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",

View File

@ -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<Empty>] = []
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

View File

@ -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

View File

@ -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)
}
}
})

View File

@ -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))
}