Merge commit '84a17115fa6082750c991bde783485fd4d92daf0'

# Conflicts:
#	submodules/TelegramUI/Components/MessageInputPanelComponent/Sources/MessageInputPanelComponent.swift
This commit is contained in:
Isaac 2025-02-25 14:49:43 +00:00
commit 034480ba44
157 changed files with 4748 additions and 1605 deletions
.gitignore
Telegram/Telegram-iOS/en.lproj
submodules
AccountContext/Sources
AttachmentTextInputPanelNode
AttachmentUI/Sources
ChatSendMessageActionUI/Sources
Components/MultilineTextWithEntitiesComponent/Sources
ContactsPeerItem/Sources
GalleryUI/Sources/Items
GameUI/Sources
InviteLinksUI/Sources
ItemListUI/Sources/Items
LegacyComponents
LegacyMediaPickerUI
MediaPickerUI/Sources
PeerInfoUI/Sources
PremiumUI/Sources
SelectablePeerNode/Sources
SettingsUI/Sources/Privacy and Security
ShareController
ShareItems/Sources
StatisticsUI/Sources
TelegramApi/Sources
TelegramCore
TelegramNotices/Sources
TelegramPresentationData/Sources/Resources
TelegramStringFormatting/Sources
TelegramUI
BUILD
Components
Ads/AdsInfoScreen/Sources
AvatarBackground/Sources
AvatarEditorScreen
Chat
ChatEmptyNode/Sources
ChatHistoryEntry/Sources
ChatInputTextNode/Sources
ChatMessageActionBubbleContentNode/Sources
ChatMessageActionButtonsNode/Sources
ChatMessageBubbleContentNode
ChatMessageBubbleItemNode/Sources
ChatMessageInstantVideoBubbleContentNode/Sources
ChatMessageInstantVideoItemNode/Sources
ChatMessageInteractiveMediaNode/Sources
ChatMessageItemImpl/Sources
ChatMessagePaymentAlertController
ChatSendAudioMessageContextPreview/Sources
ChatUserInfoItem

3
.gitignore vendored

@ -68,4 +68,5 @@ build-input/*
submodules/OpusBinding/SharedHeaders/*
submodules/FFMpegBinding/SharedHeaders/*
submodules/OpenSSLEncryptionProvider/SharedHeaders/*
buildServer.json
submodules/TelegramCore/FlatSerialization/Sources/*
buildServer.json

@ -13819,12 +13819,12 @@ Sorry for the inconvenience.";
"GroupInfo.Permissions.ChargeForMessages" = "Charge for Messages";
"GroupInfo.Permissions.ChargeForMessagesInfo" = "If you turn this on, regular members of the group will have to pay Stars to send messages.";
"GroupInfo.Permissions.MessagePrice" = "SET YOUR PRICE PER MESSAGE";
"GroupInfo.Permissions.MessagePriceInfo" = "Your group will receive 85% of the selected fee (%1$@) for each incoming message.";
"GroupInfo.Permissions.MessagePriceInfo" = "Your group will receive %1$@% of the selected fee (%2$@) for each incoming message.";
"Privacy.Messages.ChargeForMessages" = "Charge for Messages";
"Privacy.Messages.ChargeForMessagesInfo" = "Charge a fee for messages from people outide your contacts or those you haven't messaged first.";
"Privacy.Messages.MessagePrice" = "SET YOUR PRICE PER MESSAGE";
"Privacy.Messages.MessagePriceInfo" = "Your will receive 85% of the selected fee (%1$@) for each incoming message.";
"Privacy.Messages.MessagePriceInfo" = "Your will receive %1$@% of the selected fee (%2$@) for each incoming message.";
"Privacy.Messages.RemoveFeeHeader" = "EXCEPTIONS";
"Privacy.Messages.RemoveFee" = "Remove Fee";
@ -13839,9 +13839,15 @@ Sorry for the inconvenience.";
"Notification.PaidMessage.Stars_1" = "%@ Star";
"Notification.PaidMessage.Stars_any" = "%@ Stars";
"Notification.PaidMessage.Messages_1" = "%@ message";
"Notification.PaidMessage.Messages_any" = "%@ messages";
"Notification.PaidMessage" = "%1$@ paid %2$@ to send a message";
"Notification.PaidMessageYou" = "You paid %1$@ to send a message";
"Notification.PaidMessageMany" = "%1$@ paid %2$@ to send %3$@";
"Notification.PaidMessageYouMany" = "You paid %1$@ to send %2$@";
"Stars.Transfer.Terms" = "By purchasing you agree to the [Terms of Service]().";
"Stars.Transfer.Terms_URL" = "https://telegram.org/tos/stars";
@ -13850,10 +13856,31 @@ Sorry for the inconvenience.";
"Settings.Privacy.Messages.ValuePaid" = "Paid";
"Stars.Transaction.PaidMessage_1" = "Fee for %@ Message";
"Stars.Transaction.PaidMessage_anu" = "Fee for %@ Messages";
"Stars.Transaction.PaidMessage_any" = "Fee for %@ Messages";
"Stars.Transaction.PaidMessage.Text" = "You receive **%@%** of the price that you charge for each incoming message. [Change Fee >]()";
"Stars.Transaction.Paid" = "Paid";
"Stars.Intro.Transaction.PaidMessage_1" = "Fee for %@ Message";
"Stars.Intro.Transaction.PaidMessage_any" = "Fee for %@ Messages";
"Stars.Purchase.SendMessageInfo" = "Buy Stars to send a message to **%@**.";
"Stars.Purchase.SendGroupMessageInfo" = "Buy Stars to send a message in **%@**.";
"Gift.Options.Gift.Transfer" = "Transfer";
"Gift.Options.Gift.Filter.MyGifts" = "My Gifts";
"Gift.Options.Premium.OrStars" = "or %@";
"Gift.Send.PayWithStars" = "Pay with %@";
"Gift.Send.PayWithStars.Info" = "Your balance is **%@**. [Get More Stars >]()";
"Chat.PanelCustomStatusShortInfo" = "%@ is a mark for [Premium subscribers >]()";
"Chat.InputTextPaidMessagePlaceholder" = "Message for %@";
"Privacy.Messages.Stars_1" = "%@ Star";
"Privacy.Messages.Stars_any" = "%@ Stars";
"Privacy.Messages.Unlock" = "Unlock with Telegram Premium";
"Premium.PaidMessages" = "Paid Messages";
"Premium.PaidMessagesInfo" = "Charge a fee for messages from non-contacts or new senders.";
"Premium.PaidMessages.Proceed" = "About Telegram Premium";

@ -1116,6 +1116,8 @@ public protocol SharedAccountContext: AnyObject {
func makeMiniAppListScreenInitialData(context: AccountContext) -> Signal<MiniAppListScreenInitialData, NoError>
func makeMiniAppListScreen(context: AccountContext, initialData: MiniAppListScreenInitialData) -> ViewController
func makeIncomingMessagePrivacyScreen(context: AccountContext, value: GlobalPrivacySettings.NonContactChatsPrivacy, exceptions: SelectivePrivacySettings, update: @escaping (GlobalPrivacySettings.NonContactChatsPrivacy) -> Void) -> ViewController
func openWebApp(context: AccountContext, parentController: ViewController, updatedPresentationData: (initial: PresentationData, signal: Signal<PresentationData, NoError>)?, botPeer: EnginePeer, chatPeer: EnginePeer?, threadId: Int64?, buttonText: String, url: String, simple: Bool, source: ChatOpenWebViewSource, skipTermsOfService: Bool, payload: String?)
func makeAffiliateProgramSetupScreenInitialData(context: AccountContext, peerId: EnginePeer.Id, mode: AffiliateProgramSetupScreenMode) -> Signal<AffiliateProgramSetupScreenInitialData, NoError>
@ -1351,20 +1353,50 @@ public struct StickersSearchConfiguration {
public struct StarsSubscriptionConfiguration {
static var defaultValue: StarsSubscriptionConfiguration {
return StarsSubscriptionConfiguration(maxFee: 2500, usdWithdrawRate: 1200)
return StarsSubscriptionConfiguration(
maxFee: 2500,
usdWithdrawRate: 1200,
paidMessageMaxAmount: 10000,
paidMessageCommissionPermille: 850,
paidMessagesAvailable: false
)
}
public let maxFee: Int64
public let usdWithdrawRate: Int64
public let paidMessageMaxAmount: Int64
public let paidMessageCommissionPermille: Int32
public let paidMessagesAvailable: Bool
public let maxFee: Int64?
public let usdWithdrawRate: Int64?
fileprivate init(maxFee: Int64?, usdWithdrawRate: Int64?) {
fileprivate init(
maxFee: Int64,
usdWithdrawRate: Int64,
paidMessageMaxAmount: Int64,
paidMessageCommissionPermille: Int32,
paidMessagesAvailable: Bool
) {
self.maxFee = maxFee
self.usdWithdrawRate = usdWithdrawRate
self.paidMessageMaxAmount = paidMessageMaxAmount
self.paidMessageCommissionPermille = paidMessageCommissionPermille
self.paidMessagesAvailable = paidMessagesAvailable
}
public static func with(appConfiguration: AppConfiguration) -> StarsSubscriptionConfiguration {
if let data = appConfiguration.data, let value = data["stars_subscription_amount_max"] as? Double, let usdRate = data["stars_usd_withdraw_rate_x1000"] as? Double {
return StarsSubscriptionConfiguration(maxFee: Int64(value), usdWithdrawRate: Int64(usdRate))
if let data = appConfiguration.data {
let maxFee = (data["stars_subscription_amount_max"] as? Double).flatMap(Int64.init) ?? StarsSubscriptionConfiguration.defaultValue.maxFee
let usdWithdrawRate = (data["stars_usd_withdraw_rate_x1000"] as? Double).flatMap(Int64.init) ?? StarsSubscriptionConfiguration.defaultValue.usdWithdrawRate
let paidMessageMaxAmount = (data["stars_paid_message_amount_max"] as? Double).flatMap(Int64.init) ?? StarsSubscriptionConfiguration.defaultValue.paidMessageMaxAmount
let paidMessageCommissionPermille = (data["stars_paid_message_commission_permille"] as? Double).flatMap(Int32.init) ?? StarsSubscriptionConfiguration.defaultValue.paidMessageCommissionPermille
let paidMessagesAvailable = (data["stars_paid_messages_available"] as? Bool) ?? StarsSubscriptionConfiguration.defaultValue.paidMessagesAvailable
return StarsSubscriptionConfiguration(
maxFee: maxFee,
usdWithdrawRate: usdWithdrawRate,
paidMessageMaxAmount: paidMessageMaxAmount,
paidMessageCommissionPermille: paidMessageCommissionPermille,
paidMessagesAvailable: paidMessagesAvailable
)
} else {
return .defaultValue
}

@ -42,6 +42,7 @@ public enum PremiumIntroSource {
case folderTags
case animatedEmoji
case messageEffects
case paidMessages
}
public enum PremiumGiftSource: Equatable {
@ -79,6 +80,7 @@ public enum PremiumDemoSubject {
case folderTags
case business
case messageEffects
case paidMessages
case businessLocation
case businessHours
@ -134,6 +136,7 @@ public enum StarsPurchasePurpose: Equatable {
case unlockMedia(requiredStars: Int64)
case starGift(peerId: EnginePeer.Id, requiredStars: Int64)
case upgradeStarGift(requiredStars: Int64)
case sendMessage(peerId: EnginePeer.Id, requiredStars: Int64)
}
public struct PremiumConfiguration {

@ -76,5 +76,5 @@ public enum ShareControllerSubject {
case image([ImageRepresentationWithReference])
case media(AnyMediaReference, MediaParameters?)
case mapMedia(TelegramMediaMap)
case fromExternal(([PeerId], [PeerId: Int64], String, ShareControllerAccountContext, Bool) -> Signal<ShareControllerExternalStatus, ShareControllerError>)
case fromExternal(Int, ([PeerId], [PeerId: Int64], [PeerId: StarsAmount], String, ShareControllerAccountContext, Bool) -> Signal<ShareControllerExternalStatus, ShareControllerError>)
}

@ -35,6 +35,7 @@ swift_library(
"//submodules/TelegramUI/Components/MultiAnimationRenderer:MultiAnimationRenderer",
"//submodules/TelegramUI/Components/TextNodeWithEntities:TextNodeWithEntities",
"//submodules/TelegramUI/Components/Chat/ChatInputTextNode",
"//submodules/AnimatedCountLabelNode",
],
visibility = [
"//visibility:public",

@ -8,6 +8,7 @@ import ContextUI
import ChatPresentationInterfaceState
import ComponentFlow
import AccountContext
import AnimatedCountLabelNode
final class AttachmentTextInputActionButtonsNode: ASDisplayNode, ChatSendMessageActionSheetControllerSourceSendButtonNode {
private let strings: PresentationStrings
@ -17,7 +18,7 @@ final class AttachmentTextInputActionButtonsNode: ASDisplayNode, ChatSendMessage
let sendButton: HighlightTrackingButtonNode
var sendButtonHasApplyIcon = false
var animatingSendButton = false
let textNode: ImmediateTextNode
let textNode: ImmediateAnimatedCountLabelNode
private var theme: PresentationTheme
@ -46,8 +47,7 @@ final class AttachmentTextInputActionButtonsNode: ASDisplayNode, ChatSendMessage
self.backgroundNode.clipsToBounds = true
self.sendButton = HighlightTrackingButtonNode(pointerStyle: nil)
self.textNode = ImmediateTextNode()
self.textNode.attributedText = NSAttributedString(string: self.strings.MediaPicker_Send, font: Font.semibold(17.0), textColor: theme.chat.inputPanel.actionControlForegroundColor)
self.textNode = ImmediateAnimatedCountLabelNode()
self.textNode.isUserInteractionEnabled = false
super.init()
@ -104,8 +104,6 @@ final class AttachmentTextInputActionButtonsNode: ASDisplayNode, ChatSendMessage
func updateTheme(theme: PresentationTheme, wallpaper: TelegramWallpaper) {
self.backgroundNode.backgroundColor = theme.chat.inputPanel.actionControlFillColor
self.textNode.attributedText = NSAttributedString(string: self.strings.MediaPicker_Send, font: Font.semibold(17.0), textColor: theme.chat.inputPanel.actionControlForegroundColor)
}
private var absoluteRect: (CGRect, CGSize)?
@ -113,20 +111,44 @@ final class AttachmentTextInputActionButtonsNode: ASDisplayNode, ChatSendMessage
self.absoluteRect = (rect, containerSize)
}
func updateLayout(size: CGSize, transition: ContainedViewLayoutTransition, minimized: Bool, interfaceState: ChatPresentationInterfaceState) -> CGSize {
func updateLayout(size: CGSize, transition: ContainedViewLayoutTransition, minimized: Bool, text: String, interfaceState: ChatPresentationInterfaceState) -> CGSize {
self.validLayout = size
let width: CGFloat
let textSize = self.textNode.updateLayout(CGSize(width: 100.0, height: 100.0))
var titleOffset: CGFloat = 0.0
var segments: [AnimatedCountLabelNode.Segment] = []
var buttonInset: CGFloat = 18.0
if text.hasPrefix("⭐️") {
let font = Font.with(size: 17.0, design: .round, weight: .semibold, traits: .monospacedNumbers)
let badgeString = NSMutableAttributedString(string: "⭐️ ", font: font, textColor: interfaceState.theme.chat.inputPanel.actionControlForegroundColor)
if let range = badgeString.string.range(of: "⭐️") {
badgeString.addAttribute(.attachment, value: PresentationResourcesChat.chatPlaceholderStarIcon(interfaceState.theme)!, range: NSRange(range, in: badgeString.string))
badgeString.addAttribute(.baselineOffset, value: 1.0, range: NSRange(range, in: badgeString.string))
}
segments.append(.text(0, badgeString))
for char in text {
if let intValue = Int(String(char)) {
segments.append(.number(intValue, NSAttributedString(string: String(char), font: font, textColor: interfaceState.theme.chat.inputPanel.actionControlForegroundColor)))
}
}
titleOffset -= 2.0
buttonInset = 14.0
} else {
segments.append(.text(0, NSAttributedString(string: text, font: Font.semibold(17.0), textColor: interfaceState.theme.chat.inputPanel.actionControlForegroundColor)))
}
self.textNode.segments = segments
let textSize = self.textNode.updateLayout(size: CGSize(width: 100.0, height: 100.0), animated: transition.isAnimated)
if minimized {
width = 44.0
} else {
width = textSize.width + 36.0
width = textSize.width + buttonInset * 2.0
}
let buttonSize = CGSize(width: width, height: size.height)
transition.updateFrame(node: self.textNode, frame: CGRect(origin: CGPoint(x: floorToScreenPixels((width - textSize.width) / 2.0), y: floorToScreenPixels((buttonSize.height - textSize.height) / 2.0)), size: textSize))
transition.updateFrame(node: self.textNode, frame: CGRect(origin: CGPoint(x: floorToScreenPixels((width - textSize.width) / 2.0) + titleOffset, y: floorToScreenPixels((buttonSize.height - textSize.height) / 2.0)), size: textSize))
transition.updateAlpha(node: self.textNode, alpha: minimized ? 0.0 : 1.0)
transition.updateAlpha(node: self.sendButton.imageNode, alpha: minimized ? 1.0 : 0.0)

@ -750,7 +750,7 @@ public class AttachmentTextInputPanelNode: ASDisplayNode, TGCaptionPanelView, AS
}
self.theme = interfaceState.theme
self.actionButtons.updateTheme(theme: interfaceState.theme, wallpaper: interfaceState.chatWallpaper)
let textFieldMinHeight = calclulateTextFieldMinHeight(interfaceState, metrics: metrics)
@ -957,7 +957,17 @@ public class AttachmentTextInputPanelNode: ASDisplayNode, TGCaptionPanelView, AS
var textBackgroundInset: CGFloat = 0.0
let actionButtonsSize: CGSize
if let presentationInterfaceState = self.presentationInterfaceState {
actionButtonsSize = self.actionButtons.updateLayout(size: CGSize(width: 44.0, height: minimalHeight), transition: transition, minimized: !self.isAttachment || inputHasText, interfaceState: presentationInterfaceState)
let isMinimized: Bool
let text: String
if let sendPaidMessageStars = presentationInterfaceState.sendPaidMessageStars {
isMinimized = false
let count = max(1, presentationInterfaceState.interfaceState.forwardMessageIds?.count ?? 1)
text = "⭐️\(sendPaidMessageStars.value * Int64(count))"
} else {
isMinimized = !self.isAttachment || inputHasText
text = presentationInterfaceState.strings.MediaPicker_Send
}
actionButtonsSize = self.actionButtons.updateLayout(size: CGSize(width: 44.0, height: minimalHeight), transition: transition, minimized: isMinimized, text: text, interfaceState: presentationInterfaceState)
textBackgroundInset = 44.0 - actionButtonsSize.width
} else {
actionButtonsSize = CGSize(width: 44.0, height: minimalHeight)

@ -1141,7 +1141,7 @@ public class AttachmentController: ViewController, MinimizableController {
}
let isEffecitvelyCollapsedUpdated = (self.selectionCount > 0) != (self.panel.isSelecting)
let panelHeight = self.panel.update(layout: containerLayout, buttons: self.controller?.buttons ?? [], isSelecting: self.selectionCount > 0, elevateProgress: !hasPanel && !hasButton, transition: transition)
let panelHeight = self.panel.update(layout: containerLayout, buttons: self.controller?.buttons ?? [], isSelecting: self.selectionCount > 0, selectionCount: self.selectionCount, elevateProgress: !hasPanel && !hasButton, transition: transition)
if hasPanel || hasButton {
containerInsets.bottom = panelHeight
}

@ -824,6 +824,7 @@ final class AttachmentPanel: ASDisplayNode, ASScrollViewDelegate {
private var presentationData: PresentationData
private var updatedPresentationData: (initial: PresentationData, signal: Signal<PresentationData, NoError>)?
private var presentationDataDisposable: Disposable?
private var peerDisposable: Disposable?
private var iconDisposables: [MediaId: Disposable] = [:]
@ -852,6 +853,8 @@ final class AttachmentPanel: ASDisplayNode, ASScrollViewDelegate {
private var buttons: [AttachmentButtonType] = []
private var selectedIndex: Int = 0
private(set) var isSelecting: Bool = false
private var selectionCount: Int = 0
private var _isButtonVisible: Bool = false
var isButtonVisible: Bool {
return self.mainButtonState.isVisible || self.secondaryButtonState.isVisible
@ -1167,7 +1170,8 @@ final class AttachmentPanel: ASDisplayNode, ASScrollViewDelegate {
forwardMessageIds: strongSelf.presentationInterfaceState.interfaceState.forwardMessageIds ?? [],
canMakePaidContent: canMakePaidContent,
currentPrice: currentPrice,
hasTimers: hasTimers
hasTimers: hasTimers,
sendPaidMessageStars: strongSelf.presentationInterfaceState.sendPaidMessageStars
)),
hasEntityKeyboard: hasEntityKeyboard,
gesture: gesture,
@ -1258,14 +1262,35 @@ final class AttachmentPanel: ASDisplayNode, ASScrollViewDelegate {
strongSelf.updateChatPresentationInterfaceState({ $0.updatedTheme(presentationData.theme) })
if let layout = strongSelf.validLayout {
let _ = strongSelf.update(layout: layout, buttons: strongSelf.buttons, isSelecting: strongSelf.isSelecting, elevateProgress: strongSelf.elevateProgress, transition: .immediate)
let _ = strongSelf.update(layout: layout, buttons: strongSelf.buttons, isSelecting: strongSelf.isSelecting, selectionCount: strongSelf.selectionCount, elevateProgress: strongSelf.elevateProgress, transition: .immediate)
}
}
}).strict()
if let peerId = chatLocation?.peerId {
self.peerDisposable = ((self.context.account.viewTracker.peerView(peerId)
|> map { view -> StarsAmount? in
if let data = view.cachedData as? CachedUserData {
return data.sendPaidMessageStars
} else if let channel = peerViewMainPeer(view) as? TelegramChannel {
return channel.sendPaidMessageStars
} else {
return nil
}
}
|> distinctUntilChanged
|> deliverOnMainQueue).start(next: { [weak self] amount in
guard let self else {
return
}
self.updateChatPresentationInterfaceState({ $0.updatedSendPaidMessageStars(amount) })
}))
}
}
deinit {
self.presentationDataDisposable?.dispose()
self.peerDisposable?.dispose()
for (_, disposable) in self.iconDisposables {
disposable.dispose()
}
@ -1308,16 +1333,19 @@ final class AttachmentPanel: ASDisplayNode, ASScrollViewDelegate {
self.updateChatPresentationInterfaceState(transition: animated ? .animated(duration: 0.4, curve: .spring) : .immediate, f, completion: completion)
}
private func updateChatPresentationInterfaceState(transition: ContainedViewLayoutTransition, _ f: (ChatPresentationInterfaceState) -> ChatPresentationInterfaceState, completion externalCompletion: @escaping (ContainedViewLayoutTransition) -> Void = { _ in }) {
private func updateChatPresentationInterfaceState(update: Bool = true, transition: ContainedViewLayoutTransition, _ f: (ChatPresentationInterfaceState) -> ChatPresentationInterfaceState, completion externalCompletion: @escaping (ContainedViewLayoutTransition) -> Void = { _ in }) {
let presentationInterfaceState = f(self.presentationInterfaceState)
let updateInputTextState = self.presentationInterfaceState.interfaceState.effectiveInputState != presentationInterfaceState.interfaceState.effectiveInputState
self.presentationInterfaceState = presentationInterfaceState
if let textInputPanelNode = self.textInputPanelNode, updateInputTextState {
textInputPanelNode.updateInputTextState(presentationInterfaceState.interfaceState.effectiveInputState, animated: transition.isAnimated)
self.textUpdated(presentationInterfaceState.interfaceState.effectiveInputState.inputText)
if update {
if let textInputPanelNode = self.textInputPanelNode, updateInputTextState {
textInputPanelNode.updateInputTextState(presentationInterfaceState.interfaceState.effectiveInputState, animated: transition.isAnimated)
self.textUpdated(presentationInterfaceState.interfaceState.effectiveInputState.inputText)
}
}
}
@ -1672,10 +1700,23 @@ final class AttachmentPanel: ASDisplayNode, ASScrollViewDelegate {
}
}
func update(layout: ContainerViewLayout, buttons: [AttachmentButtonType], isSelecting: Bool, elevateProgress: Bool, transition: ContainedViewLayoutTransition) -> CGFloat {
func update(layout: ContainerViewLayout, buttons: [AttachmentButtonType], isSelecting: Bool, selectionCount: Int, elevateProgress: Bool, transition: ContainedViewLayoutTransition) -> CGFloat {
self.validLayout = layout
self.buttons = buttons
self.elevateProgress = elevateProgress
if selectionCount != self.selectionCount {
self.selectionCount = selectionCount
self.updateChatPresentationInterfaceState(update: false, transition: .immediate, { state in
var selectedMessages: [EngineMessage.Id] = []
for i in 0 ..< selectionCount {
selectedMessages.append(EngineMessage.Id(peerId: PeerId(0), namespace: Namespaces.Message.Local, id: Int32(i)))
}
return state.updatedInterfaceState { state in
return state.withUpdatedForwardMessageIds(selectedMessages)
}
})
}
let isButtonVisibleUpdated = self._isButtonVisible != self.mainButtonState.isVisible
self._isButtonVisible = self.mainButtonState.isVisible

@ -23,6 +23,7 @@ public enum SendMessageActionSheetControllerParams {
public let canMakePaidContent: Bool
public let currentPrice: Int64?
public let hasTimers: Bool
public let sendPaidMessageStars: StarsAmount?
public init(
isScheduledMessages: Bool,
@ -34,7 +35,8 @@ public enum SendMessageActionSheetControllerParams {
forwardMessageIds: [EngineMessage.Id],
canMakePaidContent: Bool,
currentPrice: Int64?,
hasTimers: Bool
hasTimers: Bool,
sendPaidMessageStars: StarsAmount?
) {
self.isScheduledMessages = isScheduledMessages
self.mediaPreview = mediaPreview
@ -46,6 +48,7 @@ public enum SendMessageActionSheetControllerParams {
self.canMakePaidContent = canMakePaidContent
self.currentPrice = currentPrice
self.hasTimers = hasTimers
self.sendPaidMessageStars = sendPaidMessageStars
}
}

@ -455,6 +455,9 @@ final class ChatSendMessageContextScreenComponent: Component {
if sendMessage.hasTimers {
canSchedule = false
}
if let _ = sendMessage.sendPaidMessageStars {
canSchedule = false
}
canMakePaidContent = sendMessage.canMakePaidContent
currentPrice = sendMessage.currentPrice
case .editMessage:

@ -30,6 +30,7 @@ public final class MultilineTextWithEntitiesComponent: Component {
public let textShadowColor: UIColor?
public let textStroke: (UIColor, CGFloat)?
public let highlightColor: UIColor?
public let highlightInset: UIEdgeInsets
public let handleSpoilers: Bool
public let manualVisibilityControl: Bool
public let resetAnimationsOnVisibilityChange: Bool
@ -53,6 +54,7 @@ public final class MultilineTextWithEntitiesComponent: Component {
textShadowColor: UIColor? = nil,
textStroke: (UIColor, CGFloat)? = nil,
highlightColor: UIColor? = nil,
highlightInset: UIEdgeInsets = .zero,
handleSpoilers: Bool = false,
manualVisibilityControl: Bool = false,
resetAnimationsOnVisibilityChange: Bool = false,
@ -75,6 +77,7 @@ public final class MultilineTextWithEntitiesComponent: Component {
self.textShadowColor = textShadowColor
self.textStroke = textStroke
self.highlightColor = highlightColor
self.highlightInset = highlightInset
self.highlightAction = highlightAction
self.handleSpoilers = handleSpoilers
self.manualVisibilityControl = manualVisibilityControl
@ -144,6 +147,10 @@ public final class MultilineTextWithEntitiesComponent: Component {
return false
}
if lhs.highlightInset != rhs.highlightInset {
return false
}
return true
}
@ -189,6 +196,7 @@ public final class MultilineTextWithEntitiesComponent: Component {
self.textNode.textShadowColor = component.textShadowColor
self.textNode.textStroke = component.textStroke
self.textNode.linkHighlightColor = component.highlightColor
self.textNode.linkHighlightInset = component.highlightInset
self.textNode.highlightAttributeAction = component.highlightAction
self.textNode.tapAttributeAction = component.tapAction
self.textNode.longTapAttributeAction = component.longTapAction

@ -1793,6 +1793,25 @@ public class ContactsPeerItemNode: ItemListRevealOptionsItemNode {
strongSelf.setRevealOptionsOpened(item.editing.revealed, animated: animated)
}
if item.isAd {
let adButton: HighlightableButtonNode
if let current = strongSelf.adButton {
adButton = current
} else {
adButton = HighlightableButtonNode()
adButton.setImage(UIImage(bundleImageName: "Components/AdMock"), for: .normal)
strongSelf.addSubnode(adButton)
strongSelf.adButton = adButton
adButton.addTarget(strongSelf, action: #selector(strongSelf.adButtonPressed), forControlEvents: .touchUpInside)
}
adButton.frame = CGRect(origin: CGPoint(x: params.width - 20.0 - 31.0 - 13.0, y: 11.0), size: CGSize(width: 31.0, height: 15.0))
} else if let adButton = strongSelf.adButton {
strongSelf.adButton = nil
adButton.removeFromSupernode()
}
strongSelf.updateEnableGestures()
}
})

@ -368,6 +368,86 @@ final class ChatImageGalleryItemNode: ZoomableContentGalleryItemNode {
transition.updateFrame(node: self.statusNode, frame: CGRect(origin: CGPoint(), size: statusSize))
}
private func setupImageRecognition(_ generate: @escaping (TransformImageArguments) -> DrawingContext?, dimensions: PixelDimensions) {
guard let message = self.message, !message.isCopyProtected() && message.paidContent == nil else {
return
}
let displaySize = dimensions.cgSize.fitted(CGSize(width: 1280.0, height: 1280.0)).dividedByScreenScale().integralFloor
self.recognitionDisposable.set((recognizedContent(context: self.context, image: { return generate(TransformImageArguments(corners: ImageCorners(), imageSize: displaySize, boundingSize: displaySize, intrinsicInsets: UIEdgeInsets()))?.generateImage() }, messageId: message.id)
|> deliverOnMainQueue).start(next: { [weak self] results in
if let strongSelf = self {
strongSelf.recognizedContentNode?.removeFromSupernode()
if !results.isEmpty {
let size = strongSelf.imageNode.bounds.size
let recognizedContentNode = RecognizedContentContainer(size: size, recognitions: results, presentationData: strongSelf.context.sharedContext.currentPresentationData.with { $0 }, present: { [weak self] c, a in
if let strongSelf = self {
strongSelf.galleryController()?.presentInGlobalOverlay(c, with: a)
}
}, performAction: { [weak self] string, action in
guard let strongSelf = self else {
return
}
switch action {
case .copy:
UIPasteboard.general.string = string
if let controller = strongSelf.baseNavigationController()?.topViewController as? ViewController {
let presentationData = strongSelf.context.sharedContext.currentPresentationData.with({ $0 })
let tooltipController = UndoOverlayController(presentationData: presentationData, content: .copy(text: presentationData.strings.Conversation_TextCopied), elevatedLayout: true, animateInAsReplacement: false, action: { _ in return false })
controller.present(tooltipController, in: .window(.root))
}
case .share:
if let controller = strongSelf.baseNavigationController()?.topViewController as? ViewController {
let shareController = ShareController(context: strongSelf.context, subject: .text(string), externalShare: true, immediateExternalShare: false, updatedPresentationData: (strongSelf.context.sharedContext.currentPresentationData.with({ $0 }), strongSelf.context.sharedContext.presentationData))
controller.present(shareController, in: .window(.root))
}
case .lookup:
let controller = UIReferenceLibraryViewController(term: string)
if let window = strongSelf.baseNavigationController()?.view.window {
controller.popoverPresentationController?.sourceView = window
controller.popoverPresentationController?.sourceRect = CGRect(origin: CGPoint(x: window.bounds.width / 2.0, y: window.bounds.size.height - 1.0), size: CGSize(width: 1.0, height: 1.0))
window.rootViewController?.present(controller, animated: true)
}
case .speak:
if let speechHolder = speakText(context: strongSelf.context, text: string) {
speechHolder.completion = { [weak self, weak speechHolder] in
if let strongSelf = self, strongSelf.currentSpeechHolder == speechHolder {
strongSelf.currentSpeechHolder = nil
}
}
strongSelf.currentSpeechHolder = speechHolder
}
case .translate:
if let parentController = strongSelf.baseNavigationController()?.topViewController as? ViewController {
let controller = TranslateScreen(context: strongSelf.context, text: string, canCopy: true, fromLanguage: nil)
controller.pushController = { [weak parentController] c in
(parentController?.navigationController as? NavigationController)?._keepModalDismissProgress = true
parentController?.push(c)
}
controller.presentController = { [weak parentController] c in
parentController?.present(c, in: .window(.root))
}
parentController.present(controller, in: .window(.root))
}
}
})
recognizedContentNode.barcodeAction = { [weak self] payload, rect in
guard let strongSelf = self, let message = strongSelf.message else {
return
}
strongSelf.footerContentNode.openActionOptions?(.url(url: payload, concealed: true), message)
}
recognizedContentNode.alpha = 0.0
recognizedContentNode.frame = CGRect(origin: CGPoint(), size: size)
recognizedContentNode.update(size: strongSelf.imageNode.bounds.size, transition: .immediate)
strongSelf.imageNode.addSubnode(recognizedContentNode)
strongSelf.recognizedContentNode = recognizedContentNode
strongSelf.recognitionOverlayContentNode.transitionIn()
}
}
}))
}
fileprivate func setMessage(_ message: Message, displayInfo: Bool, translateToLanguage: String?, peerIsCopyProtected: Bool, isSecret: Bool) {
self.message = message
self.translateToLanguage = translateToLanguage
@ -392,83 +472,11 @@ final class ChatImageGalleryItemNode: ZoomableContentGalleryItemNode {
case .medium, .full:
strongSelf.statusNodeContainer.isHidden = true
Queue.concurrentDefaultQueue().async {
if let message = strongSelf.message, !message.isCopyProtected() && !imageReference.media.flags.contains(.hasStickers) && message.paidContent == nil {
strongSelf.recognitionDisposable.set((recognizedContent(context: strongSelf.context, image: { return generate(TransformImageArguments(corners: ImageCorners(), imageSize: displaySize, boundingSize: displaySize, intrinsicInsets: UIEdgeInsets()))?.generateImage() }, messageId: message.id)
|> deliverOnMainQueue).start(next: { [weak self] results in
if let strongSelf = self {
strongSelf.recognizedContentNode?.removeFromSupernode()
if !results.isEmpty {
let size = strongSelf.imageNode.bounds.size
let recognizedContentNode = RecognizedContentContainer(size: size, recognitions: results, presentationData: strongSelf.context.sharedContext.currentPresentationData.with { $0 }, present: { [weak self] c, a in
if let strongSelf = self {
strongSelf.galleryController()?.presentInGlobalOverlay(c, with: a)
}
}, performAction: { [weak self] string, action in
guard let strongSelf = self else {
return
}
switch action {
case .copy:
UIPasteboard.general.string = string
if let controller = strongSelf.baseNavigationController()?.topViewController as? ViewController {
let presentationData = strongSelf.context.sharedContext.currentPresentationData.with({ $0 })
let tooltipController = UndoOverlayController(presentationData: presentationData, content: .copy(text: presentationData.strings.Conversation_TextCopied), elevatedLayout: true, animateInAsReplacement: false, action: { _ in return false })
controller.present(tooltipController, in: .window(.root))
}
case .share:
if let controller = strongSelf.baseNavigationController()?.topViewController as? ViewController {
let shareController = ShareController(context: strongSelf.context, subject: .text(string), externalShare: true, immediateExternalShare: false, updatedPresentationData: (strongSelf.context.sharedContext.currentPresentationData.with({ $0 }), strongSelf.context.sharedContext.presentationData))
controller.present(shareController, in: .window(.root))
}
case .lookup:
let controller = UIReferenceLibraryViewController(term: string)
if let window = strongSelf.baseNavigationController()?.view.window {
controller.popoverPresentationController?.sourceView = window
controller.popoverPresentationController?.sourceRect = CGRect(origin: CGPoint(x: window.bounds.width / 2.0, y: window.bounds.size.height - 1.0), size: CGSize(width: 1.0, height: 1.0))
window.rootViewController?.present(controller, animated: true)
}
case .speak:
if let speechHolder = speakText(context: strongSelf.context, text: string) {
speechHolder.completion = { [weak self, weak speechHolder] in
if let strongSelf = self, strongSelf.currentSpeechHolder == speechHolder {
strongSelf.currentSpeechHolder = nil
}
}
strongSelf.currentSpeechHolder = speechHolder
}
case .translate:
if let parentController = strongSelf.baseNavigationController()?.topViewController as? ViewController {
let controller = TranslateScreen(context: strongSelf.context, text: string, canCopy: true, fromLanguage: nil)
controller.pushController = { [weak parentController] c in
(parentController?.navigationController as? NavigationController)?._keepModalDismissProgress = true
parentController?.push(c)
}
controller.presentController = { [weak parentController] c in
parentController?.present(c, in: .window(.root))
}
parentController.present(controller, in: .window(.root))
}
}
})
recognizedContentNode.barcodeAction = { [weak self] payload, rect in
guard let strongSelf = self, let message = strongSelf.message else {
return
}
strongSelf.footerContentNode.openActionOptions?(.url(url: payload, concealed: true), message)
}
recognizedContentNode.alpha = 0.0
recognizedContentNode.frame = CGRect(origin: CGPoint(), size: size)
recognizedContentNode.update(size: strongSelf.imageNode.bounds.size, transition: .immediate)
strongSelf.imageNode.addSubnode(recognizedContentNode)
strongSelf.recognizedContentNode = recognizedContentNode
strongSelf.recognitionOverlayContentNode.transitionIn()
}
}
}))
if !imageReference.media.flags.contains(.hasStickers) {
Queue.concurrentDefaultQueue().async {
strongSelf.setupImageRecognition(generate, dimensions: largestSize.dimensions)
}
}
case .none, .blurred:
strongSelf.statusNodeContainer.isHidden = false
}
@ -819,6 +827,7 @@ final class ChatImageGalleryItemNode: ZoomableContentGalleryItemNode {
largestSize = PixelDimensions(width: largestSize.height, height: largestSize.width)
}
}
self.imageNode.asyncLayout()(TransformImageArguments(corners: ImageCorners(), imageSize: displaySize, boundingSize: displaySize, intrinsicInsets: UIEdgeInsets()))()
/*if largestSize.width > 2600 || largestSize.height > 2600 {
@ -833,7 +842,18 @@ final class ChatImageGalleryItemNode: ZoomableContentGalleryItemNode {
strongSelf.updateImageFromFile(path: data.path)
}))
} else {*/
self.imageNode.setSignal(chatMessageImageFile(account: context.account, userLocation: userLocation, fileReference: fileReference, thumbnail: false), dispatchOnDisplayLink: false)
let signal = chatMessageImageFile(account: context.account, userLocation: userLocation, fileReference: fileReference, thumbnail: false)
|> afterNext({ [weak self] generate in
guard let self else {
return
}
Queue.concurrentDefaultQueue().async {
self.setupImageRecognition(generate, dimensions: largestSize)
}
})
self.imageNode.setSignal(signal, dispatchOnDisplayLink: false)
//}
self.zoomableContent = (largestSize.cgSize, self.imageNode)

@ -144,7 +144,7 @@ final class GameControllerNode: ViewControllerTracingNode {
if eventName == "share_game" || eventName == "share_score" {
if let (botPeer, gameName) = self.shareData(), let addressName = botPeer.addressName, !addressName.isEmpty, !gameName.isEmpty {
if eventName == "share_score" {
self.present(ShareController(context: self.context, subject: .fromExternal({ [weak self] peerIds, threadIds, text, account, _ in
self.present(ShareController(context: self.context, subject: .fromExternal(1, { [weak self] peerIds, threadIds, requireStars, text, account, _ in
if let strongSelf = self, let message = strongSelf.message, let account = account as? ShareControllerAppAccountContext {
let signals = peerIds.map { TelegramEngine(account: account.context.account).messages.forwardGameWithScore(messageId: message.id, to: $0, threadId: threadIds[$0], as: nil) }
return .single(.preparing(false))

@ -493,13 +493,10 @@ private func inviteLinkEditControllerEntries(invite: ExportedInvitation?, state:
if state.subscriptionEnabled {
var label: String = ""
if let subscriptionFee = state.subscriptionFee, subscriptionFee > StarsAmount.zero {
var usdRate = 0.012
if let usdWithdrawRate = configuration.usdWithdrawRate {
usdRate = Double(usdWithdrawRate) / 1000.0 / 100.0
}
let usdRate = Double(configuration.usdWithdrawRate) / 1000.0 / 100.0
label = presentationData.strings.InviteLink_Create_FeePerMonth("\(formatTonUsdValue(subscriptionFee.value, divide: false, rate: usdRate, dateTimeFormat: presentationData.dateTimeFormat))").string
}
entries.append(.subscriptionFee(presentationData.theme, presentationData.strings.InviteLink_Create_FeePlaceholder, isEditingEnabled, state.subscriptionFee, label, configuration.maxFee.flatMap({ StarsAmount(value: $0, nanos: 0) })))
entries.append(.subscriptionFee(presentationData.theme, presentationData.strings.InviteLink_Create_FeePlaceholder, isEditingEnabled, state.subscriptionFee, label, StarsAmount(value: configuration.maxFee, nanos: 0)))
}
let infoText: String
if let _ = invite, state.subscriptionEnabled {

@ -594,10 +594,7 @@ public final class InviteLinkViewController: ViewController {
guard let peer else {
return
}
var usdRate = 0.012
if let usdWithdrawRate = configuration.usdWithdrawRate {
usdRate = Double(usdWithdrawRate) / 1000.0 / 100.0
}
let usdRate = Double(configuration.usdWithdrawRate) / 1000.0 / 100.0
let subscriptionController = context.sharedContext.makeStarsSubscriptionScreen(context: context, peer: peer, pricing: pricing, importer: importer, usdRate: usdRate)
self?.controller?.push(subscriptionController)
})
@ -834,11 +831,8 @@ public final class InviteLinkViewController: ViewController {
context.account.postbox.loadedPeerWithId(adminId)
) |> deliverOnMainQueue).start(next: { [weak self] presentationData, state, requestsState, creatorPeer in
if let strongSelf = self {
var usdRate = 0.012
if let usdWithdrawRate = configuration.usdWithdrawRate {
usdRate = Double(usdWithdrawRate) / 1000.0 / 100.0
}
let usdRate = Double(configuration.usdWithdrawRate) / 1000.0 / 100.0
var entries: [InviteLinkViewEntry] = []
entries.append(.link(presentationData.theme, invite))

@ -347,6 +347,8 @@ public class ItemListCheckboxItemNode: ItemListRevealOptionsItemNode {
iconFrame = CGRect(origin: CGPoint(x: params.leftInset + floor((leftInset - params.leftInset - iconSize.width) / 2.0), y: floor((contentSize.height - iconSize.height) / 2.0)), size: iconSize)
}
strongSelf.imageNode.frame = iconFrame
} else {
strongSelf.imageNode.image = nil
}
strongSelf.highlightedBackgroundNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -UIScreenPixel), size: CGSize(width: params.width, height: strongSelf.backgroundNode.frame.height + UIScreenPixel + UIScreenPixel))

@ -47,6 +47,8 @@
@property (nonatomic, readonly) bool inhibitEditing;
@property (nonatomic, assign) int64_t sendPaidMessageStars;
+ (instancetype)contextForCaptionsOnly;
- (SSignal *)imageSignalForItem:(NSObject<TGMediaEditableItem> *)item;

@ -17,6 +17,13 @@
@end
@protocol TGPhotoSendStarsButtonView <NSObject>
- (void)updateFrame:(CGRect)frame;
- (CGSize)updateCount:(int64_t)count;
@end
@protocol TGCaptionPanelView <NSObject>
@ -122,7 +129,7 @@
@property (nonatomic, copy) void (^ _Nullable editCover)(CGSize dimensions, void(^_Nonnull completion)(UIImage * _Nonnull));
- (UIView<TGPhotoSendStarsButtonView> *_Nonnull)sendStarsButtonAction:(void(^_Nonnull)(void))action;
- (UIView<TGPhotoSolidRoundedButtonView> *_Nonnull)solidRoundedButton:(NSString *_Nonnull)title action:(void(^_Nonnull)(void))action;
- (id<TGPhotoDrawingAdapter> _Nonnull)drawingAdapter:(CGSize)size originalSize:(CGSize)originalSize isVideo:(bool)isVideo isAvatar:(bool)isAvatar entitiesView:(UIView<TGPhotoDrawingEntitiesView> * _Nullable)entitiesView;

@ -2,6 +2,8 @@
#import <LegacyComponents/LegacyComponentsContext.h>
@protocol TGPhotoPaintStickersContext;
typedef NS_OPTIONS(NSUInteger, TGPhotoEditorTab) {
TGPhotoEditorNoneTab = 0,
TGPhotoEditorCropTab = 1 << 0,
@ -53,7 +55,9 @@ typedef enum
@property (nonatomic, assign) TGPhotoEditorBackButton backButtonType;
@property (nonatomic, assign) TGPhotoEditorDoneButton doneButtonType;
- (instancetype)initWithContext:(id<LegacyComponentsContext>)context backButton:(TGPhotoEditorBackButton)backButton doneButton:(TGPhotoEditorDoneButton)doneButton solidBackground:(bool)solidBackground;
@property (nonatomic, assign) int64_t sendPaidMessageStars;
- (instancetype)initWithContext:(id<LegacyComponentsContext>)context backButton:(TGPhotoEditorBackButton)backButton doneButton:(TGPhotoEditorDoneButton)doneButton solidBackground:(bool)solidBackground stickersContext:(id<TGPhotoPaintStickersContext>)stickersContext;
- (void)transitionInAnimated:(bool)animated;
- (void)transitionInAnimated:(bool)animated transparent:(bool)transparent;

@ -429,13 +429,13 @@
TGPhotoEditorDoneButton doneButton = isScheduledMessages ? TGPhotoEditorDoneButtonSchedule : TGPhotoEditorDoneButtonSend;
_portraitToolbarView = [[TGPhotoToolbarView alloc] initWithContext:_context backButton:TGPhotoEditorBackButtonBack doneButton:doneButton solidBackground:false];
_portraitToolbarView = [[TGPhotoToolbarView alloc] initWithContext:_context backButton:TGPhotoEditorBackButtonBack doneButton:doneButton solidBackground:false stickersContext:editingContext.sendPaidMessageStars > 0 ? stickersContext : nil];
_portraitToolbarView.cancelPressed = toolbarCancelPressed;
_portraitToolbarView.donePressed = toolbarDonePressed;
_portraitToolbarView.doneLongPressed = toolbarDoneLongPressed;
[_wrapperView addSubview:_portraitToolbarView];
_landscapeToolbarView = [[TGPhotoToolbarView alloc] initWithContext:_context backButton:TGPhotoEditorBackButtonBack doneButton:doneButton solidBackground:false];
_landscapeToolbarView = [[TGPhotoToolbarView alloc] initWithContext:_context backButton:TGPhotoEditorBackButtonBack doneButton:doneButton solidBackground:false stickersContext:nil];
_landscapeToolbarView.cancelPressed = toolbarCancelPressed;
_landscapeToolbarView.donePressed = toolbarDonePressed;
_landscapeToolbarView.doneLongPressed = toolbarDoneLongPressed;
@ -1227,6 +1227,10 @@
if (_ignoreSelectionUpdates)
return;
NSUInteger finalCount = MAX(1, selectedCount);
_portraitToolbarView.sendPaidMessageStars = (_editingContext.sendPaidMessageStars * finalCount);
[_portraitToolbarView setNeedsLayout];
if (counterVisible)
{
bool animateCount = animated && !(counterVisible && _photoCounterButton.internalHidden);

@ -293,7 +293,7 @@
TGPhotoEditorBackButton backButton = TGPhotoEditorBackButtonCancel;
TGPhotoEditorDoneButton doneButton = TGPhotoEditorDoneButtonCheck;
_portraitToolbarView = [[TGPhotoToolbarView alloc] initWithContext:_context backButton:backButton doneButton:doneButton solidBackground:true];
_portraitToolbarView = [[TGPhotoToolbarView alloc] initWithContext:_context backButton:backButton doneButton:doneButton solidBackground:true stickersContext:nil];
[_portraitToolbarView setToolbarTabs:_availableTabs animated:false];
[_portraitToolbarView setActiveTab:_currentTab];
_portraitToolbarView.cancelPressed = toolbarCancelPressed;
@ -302,7 +302,7 @@
_portraitToolbarView.tabPressed = toolbarTabPressed;
[_wrapperView addSubview:_portraitToolbarView];
_landscapeToolbarView = [[TGPhotoToolbarView alloc] initWithContext:_context backButton:backButton doneButton:doneButton solidBackground:true];
_landscapeToolbarView = [[TGPhotoToolbarView alloc] initWithContext:_context backButton:backButton doneButton:doneButton solidBackground:true stickersContext:nil];
[_landscapeToolbarView setToolbarTabs:_availableTabs animated:false];
[_landscapeToolbarView setActiveTab:_currentTab];
_landscapeToolbarView.cancelPressed = toolbarCancelPressed;

@ -141,7 +141,7 @@ const CGFloat TGPhotoEditorSliderViewInternalMargin = 7.0f;
if (vertical)
startPosition = 2 * visualMargin + visualTotalLength - startPosition;
CGFloat endPosition = visualMargin + visualTotalLength / (_maximumValue - _minimumValue) * (ABS(_minimumValue) + 1.0);
CGFloat endPosition = visualMargin + visualTotalLength / (_maximumValue - _minimumValue) * (ABS(_minimumValue) + _maximumValue);
if (vertical)
endPosition = 2 * visualMargin + visualTotalLength - endPosition;

@ -10,6 +10,8 @@
#import "TGMediaAssetsController.h"
#import "TGPhotoPaintStickersContext.h"
@interface TGPhotoToolbarView ()
{
id<LegacyComponentsContext> _context;
@ -19,6 +21,7 @@
UIView *_buttonsWrapperView;
TGModernButton *_cancelButton;
TGModernButton *_doneButton;
UIView<TGPhotoSendStarsButtonView> *_starsDoneButton;
UILabel *_infoLabel;
@ -32,7 +35,7 @@
@implementation TGPhotoToolbarView
- (instancetype)initWithContext:(id<LegacyComponentsContext>)context backButton:(TGPhotoEditorBackButton)backButton doneButton:(TGPhotoEditorDoneButton)doneButton solidBackground:(bool)solidBackground
- (instancetype)initWithContext:(id<LegacyComponentsContext>)context backButton:(TGPhotoEditorBackButton)backButton doneButton:(TGPhotoEditorDoneButton)doneButton solidBackground:(bool)solidBackground stickersContext:(id<TGPhotoPaintStickersContext>)stickersContext
{
self = [super initWithFrame:CGRectZero];
if (self != nil)
@ -56,12 +59,24 @@
[_cancelButton addTarget:self action:@selector(cancelButtonPressed) forControlEvents:UIControlEventTouchUpInside];
[_backgroundView addSubview:_cancelButton];
_doneButton = [[TGModernButton alloc] initWithFrame:CGRectMake(0, 0, buttonSize.width, buttonSize.height)];
_doneButton.exclusiveTouch = true;
_doneButton.adjustsImageWhenHighlighted = false;
[self setDoneButtonType:doneButton];
[_doneButton addTarget:self action:@selector(doneButtonPressed) forControlEvents:UIControlEventTouchUpInside];
[_backgroundView addSubview:_doneButton];
if (stickersContext != nil) {
__weak TGPhotoToolbarView *weakSelf = self;
_starsDoneButton = [stickersContext sendStarsButtonAction:^{
__strong TGPhotoToolbarView *strongSelf = weakSelf;
if (strongSelf == nil)
return;
[strongSelf doneButtonPressed];
}];
_starsDoneButton.exclusiveTouch = true;
[_backgroundView addSubview:_starsDoneButton];
} else {
_doneButton = [[TGModernButton alloc] initWithFrame:CGRectMake(0, 0, buttonSize.width, buttonSize.height)];
_doneButton.exclusiveTouch = true;
_doneButton.adjustsImageWhenHighlighted = false;
[self setDoneButtonType:doneButton];
[_doneButton addTarget:self action:@selector(doneButtonPressed) forControlEvents:UIControlEventTouchUpInside];
[_backgroundView addSubview:_doneButton];
}
_longPressGestureRecognizer = [[UILongPressGestureRecognizer alloc] initWithTarget:self action:@selector(doneButtonLongPressed:)];
_longPressGestureRecognizer.minimumPressDuration = 0.4;
@ -704,6 +719,11 @@
if (_doneButton.frame.size.width > 49.0f)
offset = 60.0f;
if (_starsDoneButton != nil) {
CGSize buttonSize = [_starsDoneButton updateCount:_sendPaidMessageStars];
[_starsDoneButton updateFrame:CGRectMake(self.frame.size.width - buttonSize.width - 2.0, 49.0f - offset + 2.0f, buttonSize.width, buttonSize.height)];
}
_doneButton.frame = CGRectMake(self.frame.size.width - offset, 49.0f - offset, _doneButton.frame.size.width, _doneButton.frame.size.height);
_infoLabel.frame = CGRectMake(49.0f + 10.0f, 0.0f, self.frame.size.width - (49.0f + 10.0f) * 2.0f, 49.0f);

@ -30,6 +30,7 @@ swift_library(
"//submodules/DrawingUI:DrawingUI",
"//submodules/SolidRoundedButtonNode:SolidRoundedButtonNode",
"//submodules/TelegramUI/Components/MediaEditor",
"//submodules/AnimatedCountLabelNode",
],
visibility = [
"//visibility:public",

@ -11,6 +11,8 @@ import StickerResources
import SolidRoundedButtonNode
import MediaEditor
import DrawingUI
import TelegramPresentationData
import AnimatedCountLabelNode
protocol LegacyPaintEntity {
var position: CGPoint { get }
@ -607,12 +609,109 @@ public final class LegacyPaintStickersContext: NSObject, TGPhotoPaintStickersCon
return button
}
public func sendStarsButtonAction(_ action: @escaping () -> Void) -> any UIView & TGPhotoSendStarsButtonView {
let button = SendStarsButtonView()
button.pressed = action
return button
}
public func drawingEntitiesView(with size: CGSize) -> UIView & TGPhotoDrawingEntitiesView {
let view = DrawingEntitiesView(context: self.context, size: size)
return view
}
}
private class SendStarsButtonView: HighlightTrackingButton, TGPhotoSendStarsButtonView {
private let backgroundView: UIView
private let textNode: ImmediateAnimatedCountLabelNode
fileprivate var pressed: (() -> Void)?
override init(frame: CGRect) {
self.backgroundView = UIView()
self.textNode = ImmediateAnimatedCountLabelNode()
self.textNode.isUserInteractionEnabled = false
super.init(frame: frame)
self.addSubview(self.backgroundView)
self.addSubview(self.textNode.view)
self.highligthedChanged = { [weak self] highlighted in
guard let self else {
return
}
if highlighted {
self.backgroundView.layer.removeAnimation(forKey: "opacity")
self.backgroundView.alpha = 0.4
self.textNode.layer.removeAnimation(forKey: "opacity")
self.textNode.alpha = 0.4
} else {
self.backgroundView.alpha = 1.0
self.backgroundView.layer.animateAlpha(from: 0.4, to: 1.0, duration: 0.2)
self.textNode.alpha = 1.0
self.textNode.layer.animateAlpha(from: 0.4, to: 1.0, duration: 0.2)
}
}
}
required init?(coder: NSCoder) {
preconditionFailure()
}
func updateFrame(_ frame: CGRect) {
let transition: ContainedViewLayoutTransition
if self.frame.width.isZero {
transition = .immediate
} else {
transition = .animated(duration: 0.4, curve: .spring)
}
transition.updateFrame(view: self, frame: frame)
}
func updateCount(_ count: Int64) -> CGSize {
let text = "\(count)"
let transition: ContainedViewLayoutTransition
if self.backgroundView.frame.width.isZero {
transition = .immediate
} else {
transition = .animated(duration: 0.4, curve: .spring)
}
var segments: [AnimatedCountLabelNode.Segment] = []
let font = Font.with(size: 17.0, design: .round, weight: .semibold, traits: .monospacedNumbers)
let badgeString = NSMutableAttributedString(string: "⭐️ ", font: font, textColor: .white)
if let range = badgeString.string.range(of: "⭐️") {
badgeString.addAttribute(.attachment, value: PresentationResourcesChat.chatPlaceholderStarIcon(defaultDarkPresentationTheme)!, range: NSRange(range, in: badgeString.string))
badgeString.addAttribute(.baselineOffset, value: 1.0, range: NSRange(range, in: badgeString.string))
}
segments.append(.text(0, badgeString))
for char in text {
if let intValue = Int(String(char)) {
segments.append(.number(intValue, NSAttributedString(string: String(char), font: font, textColor: .white)))
}
}
self.textNode.segments = segments
let buttonInset: CGFloat = 14.0
let textSize = self.textNode.updateLayout(size: CGSize(width: 100.0, height: 100.0), animated: transition.isAnimated)
let width = textSize.width + buttonInset * 2.0
let buttonSize = CGSize(width: width, height: 45.0)
let titleOffset: CGFloat = 0.0
transition.updateFrame(node: self.textNode, frame: CGRect(origin: CGPoint(x: floorToScreenPixels((width - textSize.width) / 2.0) + titleOffset, y: floorToScreenPixels((buttonSize.height - textSize.height) / 2.0)), size: textSize))
let backgroundSize = CGSize(width: width - 11.0, height: 33.0)
transition.updateFrame(view: self.backgroundView, frame: CGRect(origin: CGPoint(x: floorToScreenPixels((width - backgroundSize.width) / 2.0), y: floorToScreenPixels((buttonSize.height - backgroundSize.height) / 2.0)), size: backgroundSize))
self.backgroundView.layer.cornerRadius = backgroundSize.height / 2.0
self.backgroundView.backgroundColor = UIColor(rgb: 0x007aff)
return buttonSize;
}
}
//Xcode 16
#if canImport(ContactProvider)
extension SolidRoundedButtonView: @retroactive TGPhotoSolidRoundedButtonView {

@ -211,7 +211,7 @@ public final class MediaPickerScreenImpl: ViewController, MediaPickerScreen, Att
public var openAvatarEditor: () -> Void = {}
private var completed = false
public var legacyCompletion: (_ signals: [Any], _ silently: Bool, _ scheduleTime: Int32?, ChatSendMessageActionSheetController.SendParameters?, @escaping (String) -> UIView?, @escaping () -> Void) -> Void = { _, _, _, _, _, _ in }
public var legacyCompletion: (_ fromGallery: Bool, _ signals: [Any], _ silently: Bool, _ scheduleTime: Int32?, ChatSendMessageActionSheetController.SendParameters?, @escaping (String) -> UIView?, @escaping () -> Void) -> Void = { _, _, _, _, _, _, _ in }
public var requestAttachmentMenuExpansion: () -> Void = { }
public var updateNavigationStack: (@escaping ([AttachmentContainable]) -> ([AttachmentContainable], AttachmentMediaPickerContext?)) -> Void = { _ in }
@ -1294,7 +1294,7 @@ public final class MediaPickerScreenImpl: ViewController, MediaPickerScreen, Att
}
}
fileprivate func send(asFile: Bool = false, silently: Bool, scheduleTime: Int32?, animated: Bool, parameters: ChatSendMessageActionSheetController.SendParameters?, completion: @escaping () -> Void) {
fileprivate func send(fromGallery: Bool = false, asFile: Bool = false, silently: Bool, scheduleTime: Int32?, animated: Bool, parameters: ChatSendMessageActionSheetController.SendParameters?, completion: @escaping () -> Void) {
guard let controller = self.controller, !controller.completed else {
return
}
@ -1334,7 +1334,7 @@ public final class MediaPickerScreenImpl: ViewController, MediaPickerScreen, Att
return
}
controller.completed = true
controller.legacyCompletion(signals, silently, scheduleTime, parameters, { [weak self] identifier in
controller.legacyCompletion(fromGallery, signals, silently, scheduleTime, parameters, { [weak self] identifier in
return !asFile ? self?.getItemSnapshot(identifier) : nil
}, { [weak self] in
completion()
@ -1834,6 +1834,7 @@ public final class MediaPickerScreenImpl: ViewController, MediaPickerScreen, Att
paidMediaAllowed: Bool = false,
subject: Subject,
forCollage: Bool = false,
sendPaidMessageStars: Int64? = nil,
editingContext: TGMediaEditingContext? = nil,
selectionContext: TGMediaSelectionContext? = nil,
saveEditedPhotos: Bool = false,
@ -2114,7 +2115,7 @@ public final class MediaPickerScreenImpl: ViewController, MediaPickerScreen, Att
if let currentItem = currentItem {
selectionState.setItem(currentItem, selected: true)
}
strongSelf.controllerNode.send(silently: silently, scheduleTime: scheduleTime, animated: animated, parameters: parameters, completion: completion)
strongSelf.controllerNode.send(fromGallery: currentItem != nil, silently: silently, scheduleTime: scheduleTime, animated: animated, parameters: parameters, completion: completion)
}
}, schedule: { [weak self] parameters in
if let strongSelf = self {
@ -2129,6 +2130,8 @@ public final class MediaPickerScreenImpl: ViewController, MediaPickerScreen, Att
}, selectionState: selectionContext, editingState: editingContext ?? TGMediaEditingContext())
self.interaction?.selectionState?.grouping = true
self.interaction?.editingState.sendPaidMessageStars = sendPaidMessageStars ?? 0
if case let .media(media) = self.subject {
for item in media {
selectionContext.setItem(item.asset, selected: true)

@ -93,7 +93,7 @@ private enum ChannelPermissionsEntry: ItemListNodeEntry {
case chargeForMessagesInfo(PresentationTheme, String)
case messagePriceHeader(PresentationTheme, String)
case messagePrice(PresentationTheme, StarsAmount, String)
case messagePrice(PresentationTheme, Int64, Int64, String)
case messagePriceInfo(PresentationTheme, String)
case unrestrictBoostersSwitch(PresentationTheme, String, Bool)
@ -241,8 +241,8 @@ private enum ChannelPermissionsEntry: ItemListNodeEntry {
} else {
return false
}
case let .messagePrice(lhsTheme, lhsValue, lhsPrice):
if case let .messagePrice(rhsTheme, rhsValue, rhsPrice) = rhs, lhsTheme === rhsTheme, lhsValue == rhsValue, lhsPrice == rhsPrice {
case let .messagePrice(lhsTheme, lhsValue, lhsMaxValue, lhsPrice):
if case let .messagePrice(rhsTheme, rhsValue, rhsMaxValue, rhsPrice) = rhs, lhsTheme === rhsTheme, lhsValue == rhsValue, lhsMaxValue == rhsMaxValue, lhsPrice == rhsPrice {
return true
} else {
return false
@ -424,8 +424,8 @@ private enum ChannelPermissionsEntry: ItemListNodeEntry {
return ItemListTextItem(presentationData: presentationData, text: .plain(value), sectionId: self.section)
case let .messagePriceHeader(_, value):
return ItemListSectionHeaderItem(presentationData: presentationData, text: value, sectionId: self.section)
case let .messagePrice(_, value, price):
return MessagePriceItem(theme: presentationData.theme, strings: presentationData.strings, minValue: 10, maxValue: 9000, value: value.value, price: price, sectionId: self.section, updated: { value in
case let .messagePrice(_, value, maxValue, price):
return MessagePriceItem(theme: presentationData.theme, strings: presentationData.strings, isEnabled: true, minValue: 1, maxValue: maxValue, value: value, price: price, sectionId: self.section, updated: { value in
arguments.updateStarsAmount(StarsAmount(value: value, nanos: 0))
})
case let .messagePriceInfo(_, value):
@ -720,23 +720,22 @@ private func channelPermissionsControllerEntries(context: AccountContext, presen
entries.append(.conversionInfo(presentationData.theme, presentationData.strings.GroupInfo_Permissions_BroadcastConvertInfo(presentationStringsFormattedNumber(participantsLimit, presentationData.dateTimeFormat.groupingSeparator)).string))
}
let chargeEnabled = state.modifiedStarsAmount != nil
entries.append(.chargeForMessages(presentationData.theme, presentationData.strings.GroupInfo_Permissions_ChargeForMessages, chargeEnabled))
entries.append(.chargeForMessagesInfo(presentationData.theme, presentationData.strings.GroupInfo_Permissions_ChargeForMessagesInfo))
if chargeEnabled {
var price: String = ""
if let amount = state.modifiedStarsAmount {
var usdRate = 0.012
if let usdWithdrawRate = configuration.usdWithdrawRate {
usdRate = Double(usdWithdrawRate) / 1000.0 / 100.0
}
price = "\(formatTonUsdValue(amount.value, divide: false, rate: usdRate, dateTimeFormat: presentationData.dateTimeFormat))"
if cachedData.flags.contains(.paidMessagesAvailable) && channel.hasPermission(.banMembers) {
let sendPaidMessageStars = state.modifiedStarsAmount?.value ?? (cachedData.sendPaidMessageStars?.value ?? 0)
let chargeEnabled = sendPaidMessageStars > 0
entries.append(.chargeForMessages(presentationData.theme, presentationData.strings.GroupInfo_Permissions_ChargeForMessages, chargeEnabled))
entries.append(.chargeForMessagesInfo(presentationData.theme, presentationData.strings.GroupInfo_Permissions_ChargeForMessagesInfo))
if chargeEnabled {
var price: String = ""
let usdRate = Double(configuration.usdWithdrawRate) / 1000.0 / 100.0
price = "\(formatTonUsdValue(sendPaidMessageStars, divide: false, rate: usdRate, dateTimeFormat: presentationData.dateTimeFormat))"
entries.append(.messagePriceHeader(presentationData.theme, presentationData.strings.GroupInfo_Permissions_MessagePrice))
entries.append(.messagePrice(presentationData.theme, sendPaidMessageStars, configuration.paidMessageMaxAmount, price))
entries.append(.messagePriceInfo(presentationData.theme, presentationData.strings.GroupInfo_Permissions_MessagePriceInfo("\(configuration.paidMessageCommissionPermille / 10)", price).string))
}
entries.append(.messagePriceHeader(presentationData.theme, presentationData.strings.GroupInfo_Permissions_MessagePrice))
entries.append(.messagePrice(presentationData.theme, state.modifiedStarsAmount ?? StarsAmount(value: 4000, nanos: 0), price))
entries.append(.messagePriceInfo(presentationData.theme, presentationData.strings.GroupInfo_Permissions_MessagePriceInfo(price).string))
}
let canSendText = !effectiveRightsFlags.contains(.banSendText)
@ -876,6 +875,9 @@ public func channelPermissionsController(context: AccountContext, updatedPresent
let updateUnrestrictBoostersDisposable = MetaDisposable()
actionsDisposable.add(updateUnrestrictBoostersDisposable)
let updateSendPaidMessageStarsDisposable = MetaDisposable()
actionsDisposable.add(updateSendPaidMessageStarsDisposable)
let peerView = Promise<PeerView>()
peerView.set(sourcePeerId.get()
|> mapToSignal(context.account.viewTracker.peerView))
@ -1252,6 +1254,17 @@ public func channelPermissionsController(context: AccountContext, updatedPresent
state.modifiedStarsAmount = value
return state
}
let _ = (peerView.get()
|> take(1)
|> deliverOnMainQueue).start(next: { view in
var effectiveValue = value
if value?.value == 0 {
effectiveValue = nil
}
updateSendPaidMessageStarsDisposable.set((context.engine.peers.updateChannelPaidMessagesStars(peerId: view.peerId, stars: effectiveValue)
|> deliverOnMainQueue).start())
})
}, toggleIsOptionExpanded: { flags in
updateState { state in
var state = state

@ -1099,6 +1099,25 @@ private final class DemoSheetContent: CombinedComponent {
)
)
availableItems[.paidMessages] = DemoPagerComponent.Item(
AnyComponentWithIdentity(
id: PremiumDemoScreen.Subject.paidMessages,
component: AnyComponent(
PageComponent(
content: AnyComponent(PhoneDemoComponent(
context: component.context,
position: .top,
videoFile: configuration.videos["paid_messages"],
decoration: .badgeStars
)),
title: strings.Premium_PaidMessages,
text: strings.Premium_PaidMessagesInfo,
textColor: textColor
)
)
)
)
let index: Int = 0
var items: [DemoPagerComponent.Item] = []
if let item = availableItems.first(where: { $0.value.content.id == component.subject as AnyHashable }) {
@ -1195,6 +1214,8 @@ private final class DemoSheetContent: CombinedComponent {
text = strings.Premium_FolderTagsStandaloneInfo
case .messageEffects:
text = strings.Premium_MessageEffectsInfo
case .paidMessages:
text = strings.Premium_PaidMessagesInfo
default:
text = ""
}
@ -1279,6 +1300,9 @@ private final class DemoSheetContent: CombinedComponent {
case .emojiStatus:
buttonText = strings.Premium_EmojiStatus_Proceed
buttonAnimationName = "premium_unlock"
case .paidMessages:
buttonText = strings.Premium_PaidMessages_Proceed
buttonAnimationName = "premium_unlock"
default:
buttonText = strings.Common_OK
}
@ -1468,6 +1492,7 @@ public class PremiumDemoScreen: ViewControllerComponentContainer {
case business
case folderTags
case messageEffects
case paidMessages
case businessLocation
case businessHours
@ -1526,6 +1551,8 @@ public class PremiumDemoScreen: ViewControllerComponentContainer {
return .folderTags
case .messageEffects:
return .messageEffects
case .paidMessages:
return .paidMessages
case .businessLocation:
return .businessLocation
case .businessHours:

@ -433,6 +433,7 @@ private final class PremiumGiftScreenContentComponent: CombinedComponent {
UIColor(rgb: 0xdb374b),
UIColor(rgb: 0xcb3e6d),
UIColor(rgb: 0xbc4395),
UIColor(rgb: 0xbc4395),
UIColor(rgb: 0xab4ac4),
UIColor(rgb: 0xab4ac4),
UIColor(rgb: 0xa34cd7),
@ -538,6 +539,8 @@ private final class PremiumGiftScreenContentComponent: CombinedComponent {
demoSubject = .messagePrivacy
case .messageEffects:
demoSubject = .messageEffects
case .paidMessages:
demoSubject = .paidMessages
case .business:
demoSubject = .business
default:

@ -302,6 +302,12 @@ public enum PremiumSource: Equatable {
} else {
return false
}
case .paidMessages:
if case .messageEffects = rhs {
return true
} else {
return false
}
}
}
@ -349,6 +355,7 @@ public enum PremiumSource: Equatable {
case messageTags
case folderTags
case messageEffects
case paidMessages
var identifier: String? {
switch self {
@ -442,6 +449,8 @@ public enum PremiumSource: Equatable {
return "folder_tags"
case .messageEffects:
return "effects"
case .paidMessages:
return "paid_messages"
}
}
}
@ -470,6 +479,7 @@ public enum PremiumPerk: CaseIterable {
case business
case folderTags
case messageEffects
case paidMessages
case businessLocation
case businessHours
@ -504,7 +514,8 @@ public enum PremiumPerk: CaseIterable {
.messagePrivacy,
.folderTags,
.business,
.messageEffects
.messageEffects,
.paidMessages
]
}
@ -578,6 +589,8 @@ public enum PremiumPerk: CaseIterable {
return "folder_tags"
case .messageEffects:
return "effects"
case .paidMessages:
return "paid_messages"
case .business:
return "business"
case .businessLocation:
@ -647,6 +660,8 @@ public enum PremiumPerk: CaseIterable {
return strings.Premium_Business
case .messageEffects:
return strings.Premium_MessageEffects
case .paidMessages:
return strings.Premium_PaidMessages
case .businessLocation:
return strings.Business_Location
case .businessHours:
@ -714,6 +729,8 @@ public enum PremiumPerk: CaseIterable {
return strings.Premium_BusinessInfo
case .messageEffects:
return strings.Premium_MessageEffectsInfo
case .paidMessages:
return strings.Premium_PaidMessagesInfo
case .businessLocation:
return strings.Business_LocationInfo
case .businessHours:
@ -781,7 +798,8 @@ public enum PremiumPerk: CaseIterable {
return "Premium/Perk/Business"
case .messageEffects:
return "Premium/Perk/MessageEffects"
case .paidMessages:
return "Premium/Perk/PaidMessages"
case .businessLocation:
return "Premium/BusinessPerk/Location"
case .businessHours:
@ -819,6 +837,7 @@ struct PremiumIntroConfiguration {
.colors,
.wallpapers,
.profileBadge,
.paidMessages,
.messagePrivacy,
.advancedChatManagement,
.noAds,
@ -867,6 +886,12 @@ struct PremiumIntroConfiguration {
perks = PremiumIntroConfiguration.defaultValue.perks
}
#if DEBUG
if !perks.contains(.paidMessages) {
perks.append(.paidMessages)
}
#endif
var businessPerks: [PremiumPerk] = []
if let values = data["business_promo_order"] as? [String] {
for value in values {
@ -1859,6 +1884,7 @@ private final class PremiumIntroScreenContentComponent: CombinedComponent {
UIColor(rgb: 0xdb374b),
UIColor(rgb: 0xcb3e6d),
UIColor(rgb: 0xbc4395),
UIColor(rgb: 0xbc4395),
UIColor(rgb: 0xab4ac4),
UIColor(rgb: 0xab4ac4),
UIColor(rgb: 0xa34cd7),
@ -2092,6 +2118,8 @@ private final class PremiumIntroScreenContentComponent: CombinedComponent {
demoSubject = .messagePrivacy
case .messageEffects:
demoSubject = .messageEffects
case .paidMessages:
demoSubject = .paidMessages
case .business:
demoSubject = .business
let _ = ApplicationSpecificNotice.setDismissedBusinessBadge(accountManager: accountContext.sharedContext.accountManager).startStandalone()

@ -842,6 +842,24 @@ public class PremiumLimitsListScreen: ViewController {
)
)
)
availableItems[.paidMessages] = DemoPagerComponent.Item(
AnyComponentWithIdentity(
id: PremiumDemoScreen.Subject.paidMessages,
component: AnyComponent(
PageComponent(
content: AnyComponent(PhoneDemoComponent(
context: context,
position: .top,
videoFile: videos["paid_messages"],
decoration: .badgeStars
)),
title: strings.Premium_PaidMessages,
text: strings.Premium_PaidMessagesInfo,
textColor: textColor
)
)
)
)
availableItems[.business] = DemoPagerComponent.Item(
AnyComponentWithIdentity(
id: PremiumDemoScreen.Subject.business,

@ -75,8 +75,9 @@ public final class SelectablePeerNode: ASDisplayNode {
private let avatarSelectionNode: ASImageNode
private let avatarNodeContainer: ASDisplayNode
private let avatarNode: AvatarNode
private var avatarBadgeBackground: UIImageView?
private var avatarBadgeOutline: UIImageView?
private var avatarBadge: UIImageView?
private var avatarBadgeLabel: ImmediateTextView?
private let onlineNode: PeerOnlineMarkerNode
private var checkNode: CheckNode?
private let textNode: ImmediateTextNode
@ -149,7 +150,7 @@ public final class SelectablePeerNode: ASDisplayNode {
}
}
public func setup(context: AccountContext, theme: PresentationTheme, strings: PresentationStrings, peer: EngineRenderedPeer, requiresPremiumForMessaging: Bool, customTitle: String? = nil, iconId: Int64? = nil, iconColor: Int32? = nil, online: Bool = false, numberOfLines: Int = 2, synchronousLoad: Bool) {
public func setup(context: AccountContext, theme: PresentationTheme, strings: PresentationStrings, peer: EngineRenderedPeer, requiresPremiumForMessaging: Bool, requiresStars: Int64? = nil, customTitle: String? = nil, iconId: Int64? = nil, iconColor: Int32? = nil, online: Bool = false, numberOfLines: Int = 2, synchronousLoad: Bool) {
self.setup(
accountPeerId: context.account.peerId,
postbox: context.account.postbox,
@ -165,6 +166,7 @@ public final class SelectablePeerNode: ASDisplayNode {
strings: strings,
peer: peer,
requiresPremiumForMessaging: requiresPremiumForMessaging,
requiresStars: requiresStars,
customTitle: customTitle,
iconId: iconId,
iconColor: iconColor,
@ -184,7 +186,7 @@ public final class SelectablePeerNode: ASDisplayNode {
self.avatarNode.playRepostAnimation()
}
public func setup(accountPeerId: EnginePeer.Id, postbox: Postbox, network: Network, energyUsageSettings: EnergyUsageSettings, contentSettings: ContentSettings, animationCache: AnimationCache, animationRenderer: MultiAnimationRenderer, resolveInlineStickers: @escaping ([Int64]) -> Signal<[Int64: TelegramMediaFile], NoError>, theme: PresentationTheme, strings: PresentationStrings, peer: EngineRenderedPeer, requiresPremiumForMessaging: Bool, customTitle: String? = nil, iconId: Int64? = nil, iconColor: Int32? = nil, online: Bool = false, numberOfLines: Int = 2, synchronousLoad: Bool) {
public func setup(accountPeerId: EnginePeer.Id, postbox: Postbox, network: Network, energyUsageSettings: EnergyUsageSettings, contentSettings: ContentSettings, animationCache: AnimationCache, animationRenderer: MultiAnimationRenderer, resolveInlineStickers: @escaping ([Int64]) -> Signal<[Int64: TelegramMediaFile], NoError>, theme: PresentationTheme, strings: PresentationStrings, peer: EngineRenderedPeer, requiresPremiumForMessaging: Bool, requiresStars: Int64? = nil, customTitle: String? = nil, iconId: Int64? = nil, iconColor: Int32? = nil, online: Bool = false, numberOfLines: Int = 2, synchronousLoad: Bool) {
let isFirstTime = self.peer == nil
self.peer = peer
guard let mainPeer = peer.chatMainPeer else {
@ -223,16 +225,68 @@ public final class SelectablePeerNode: ASDisplayNode {
self.textNode.attributedText = NSAttributedString(string: customTitle ?? text, font: textFont, textColor: self.currentSelected ? self.theme.selectedTextColor : defaultColor, paragraphAlignment: .center)
self.avatarNode.setPeer(accountPeerId: accountPeerId, postbox: postbox, network: network, contentSettings: contentSettings, theme: theme, peer: mainPeer, overrideImage: overrideImage, emptyColor: self.theme.avatarPlaceholderColor, clipStyle: isForum ? .roundedRect : .round, synchronousLoad: synchronousLoad)
if requiresPremiumForMessaging {
let avatarBadgeBackground: UIImageView
if let current = self.avatarBadgeBackground {
avatarBadgeBackground = current
if let requiresStars {
let avatarBadgeOutline: UIImageView
if let current = self.avatarBadgeOutline {
avatarBadgeOutline = current
} else {
avatarBadgeBackground = UIImageView()
avatarBadgeBackground.image = PresentationResourcesChatList.shareAvatarPremiumLockBadgeBackground(theme)
avatarBadgeBackground.tintColor = theme.chatList.itemBackgroundColor
self.avatarBadgeBackground = avatarBadgeBackground
self.avatarNode.view.addSubview(avatarBadgeBackground)
avatarBadgeOutline = UIImageView()
avatarBadgeOutline.contentMode = .scaleToFill
avatarBadgeOutline.image = PresentationResourcesChatList.shareAvatarStarsLockBadgeBackground(theme)
avatarBadgeOutline.tintColor = theme.actionSheet.opaqueItemBackgroundColor
self.avatarBadgeOutline = avatarBadgeOutline
self.avatarNodeContainer.view.addSubview(avatarBadgeOutline)
}
let avatarBadge: UIImageView
if let current = self.avatarBadge {
avatarBadge = current
} else {
avatarBadge = UIImageView()
avatarBadge.contentMode = .scaleToFill
avatarBadge.image = PresentationResourcesChatList.shareAvatarStarsLockBadgeInnerBackground(theme)
avatarBadge.tintColor = theme.actionSheet.controlAccentColor
self.avatarBadge = avatarBadge
self.avatarNodeContainer.view.addSubview(avatarBadge)
}
let avatarBadgeLabel: ImmediateTextView
if let current = self.avatarBadgeLabel {
avatarBadgeLabel = current
} else {
avatarBadgeLabel = ImmediateTextView()
self.avatarBadgeLabel = avatarBadgeLabel
self.avatarNodeContainer.view.addSubview(avatarBadgeLabel)
}
let badgeString = NSMutableAttributedString(string: "⭐️\(presentationStringsFormattedNumber(Int32(requiresStars), " "))", font: Font.with(size: 9.0, design: .round , weight: .bold), textColor: theme.list.itemCheckColors.foregroundColor)
if let range = badgeString.string.range(of: "⭐️") {
badgeString.addAttribute(.attachment, value: UIImage(bundleImageName: "Premium/SendStarsPeerBadgeStarIcon")!, range: NSRange(range, in: badgeString.string))
badgeString.addAttribute(.baselineOffset, value: 1.5, range: NSRange(range, in: badgeString.string))
badgeString.addAttribute(.kern, value: -0.8, range: NSRange(badgeString.string.startIndex ..< badgeString.string.endIndex, in: badgeString.string))
}
avatarBadgeLabel.attributedText = badgeString
let avatarFrame = self.avatarNode.frame
let badgeSize = avatarBadgeLabel.updateLayout(avatarFrame.size)
var badgeFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((avatarFrame.width - badgeSize.width) / 2.0) - (self.currentSelected ? 15.0 : 0.0), y: avatarFrame.height - 13.0), size: badgeSize)
let badgeBackgroundFrame = CGRect(origin: CGPoint(x: badgeFrame.minX - 2.0, y: badgeFrame.minY - 3.0 - UIScreenPixel), size: CGSize(width: badgeFrame.width + 4.0, height: 16.0))
let badgeOutlineFrame = CGRect(origin: CGPoint(x: badgeBackgroundFrame.minX - 2.0, y: badgeBackgroundFrame.minY - 2.0), size: CGSize(width: badgeBackgroundFrame.width + 4.0, height: 20.0))
badgeFrame = badgeFrame.offsetBy(dx: -2.0, dy: 0.0)
avatarBadge.frame = badgeBackgroundFrame
avatarBadgeOutline.frame = badgeOutlineFrame
avatarBadgeLabel.frame = badgeFrame
} else if requiresPremiumForMessaging {
let avatarBadgeOutline: UIImageView
if let current = self.avatarBadgeOutline {
avatarBadgeOutline = current
} else {
avatarBadgeOutline = UIImageView()
avatarBadgeOutline.image = PresentationResourcesChatList.shareAvatarPremiumLockBadgeBackground(theme)
avatarBadgeOutline.tintColor = theme.chatList.itemBackgroundColor
self.avatarBadgeOutline = avatarBadgeOutline
self.avatarNode.view.addSubview(avatarBadgeOutline)
}
let avatarBadge: UIImageView
@ -247,19 +301,23 @@ public final class SelectablePeerNode: ASDisplayNode {
let avatarFrame = self.avatarNode.frame
let badgeFrame = CGRect(origin: CGPoint(x: avatarFrame.width - 20.0, y: avatarFrame.height - 20.0), size: CGSize(width: 20.0, height: 20.0))
let badgeBackgroundFrame = badgeFrame.insetBy(dx: -2.0 + UIScreenPixel, dy: -2.0 + UIScreenPixel)
let badgeBackgroundFrame = badgeFrame.insetBy(dx: -2.0, dy: -2.0)
avatarBadgeBackground.frame = badgeBackgroundFrame
avatarBadgeOutline.frame = badgeBackgroundFrame
avatarBadge.frame = badgeFrame
} else {
if let avatarBadgeBackground = self.avatarBadgeBackground {
self.avatarBadgeBackground = nil
avatarBadgeBackground.removeFromSuperview()
if let avatarBadgeOutline = self.avatarBadgeOutline {
self.avatarBadgeOutline = nil
avatarBadgeOutline.removeFromSuperview()
}
if let avatarBadge = self.avatarBadge {
self.avatarBadge = nil
avatarBadge.removeFromSuperview()
}
if let avatarBadgeLabel = self.avatarBadgeLabel {
self.avatarBadgeLabel = nil
avatarBadgeLabel.removeFromSuperview()
}
}
let onlineLayout = self.onlineNode.asyncLayout()
@ -340,6 +398,19 @@ public final class SelectablePeerNode: ASDisplayNode {
context.fillEllipse(in: bounds.insetBy(dx: 2.0, dy: 2.0))
}
})
if let avatarBadgeLabel = self.avatarBadgeLabel, let avatarBadge = self.avatarBadge, let avatarBadgeOutline = self.avatarBadgeOutline {
avatarBadgeLabel.center = CGPoint(x: self.avatarNode.bounds.width / 2.0 - 17.0, y: avatarBadgeLabel.center.y)
avatarBadge.center = CGPoint(x: self.avatarNode.bounds.width / 2.0 - 15.0, y: avatarBadge.center.y)
avatarBadgeOutline.center = CGPoint(x: self.avatarNode.bounds.width / 2.0 - 15.0, y: avatarBadgeOutline.center.y)
if animated {
avatarBadgeLabel.layer.animatePosition(from: CGPoint(x: 15.0, y: 0.0), to: .zero, duration: 0.2, timingFunction: kCAMediaTimingFunctionSpring, additive: true)
avatarBadge.layer.animatePosition(from: CGPoint(x: 15.0, y: 0.0), to: .zero, duration: 0.2, timingFunction: kCAMediaTimingFunctionSpring, additive: true)
avatarBadgeOutline.layer.animatePosition(from: CGPoint(x: 15.0, y: 0.0), to: .zero, duration: 0.2, timingFunction: kCAMediaTimingFunctionSpring, additive: true)
}
}
if animated {
self.avatarNode.layer.animateScale(from: 1.0, to: 0.866666, duration: 0.2, timingFunction: kCAMediaTimingFunctionSpring)
self.avatarSelectionNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.15)
@ -355,6 +426,18 @@ public final class SelectablePeerNode: ASDisplayNode {
} else {
self.avatarSelectionNode.image = nil
}
if let avatarBadgeLabel = self.avatarBadgeLabel, let avatarBadge = self.avatarBadge, let avatarBadgeOutline = self.avatarBadgeOutline {
avatarBadgeLabel.center = CGPoint(x: self.avatarNode.bounds.width / 2.0 - 2.0, y: avatarBadgeLabel.center.y)
avatarBadge.center = CGPoint(x: self.avatarNode.bounds.width / 2.0, y: avatarBadge.center.y)
avatarBadgeOutline.center = CGPoint(x: self.avatarNode.bounds.width / 2.0, y: avatarBadgeOutline.center.y)
if animated {
avatarBadgeLabel.layer.animatePosition(from: CGPoint(x: -15.0, y: 0.0), to: .zero, duration: 0.2, timingFunction: kCAMediaTimingFunctionSpring, additive: true)
avatarBadge.layer.animatePosition(from: CGPoint(x: -15.0, y: 0.0), to: .zero, duration: 0.2, timingFunction: kCAMediaTimingFunctionSpring, additive: true)
avatarBadgeOutline.layer.animatePosition(from: CGPoint(x: -15.0, y: 0.0), to: .zero, duration: 0.2, timingFunction: kCAMediaTimingFunctionSpring, additive: true)
}
}
}
if selected {
@ -365,8 +448,8 @@ public final class SelectablePeerNode: ASDisplayNode {
self.addSubnode(checkNode)
let avatarFrame = self.avatarNode.frame
let checkSize = CGSize(width: 24.0, height: 24.0)
checkNode.frame = CGRect(origin: CGPoint(x: avatarFrame.maxX - 10.0, y: avatarFrame.maxY - 18.0), size: checkSize)
let checkSize = CGSize(width: 22.0, height: 22.0)
checkNode.frame = CGRect(origin: CGPoint(x: avatarFrame.maxX - 14.0, y: avatarFrame.maxY - 15.0), size: checkSize)
checkNode.setSelected(true, animated: animated)
}
} else if let checkNode = self.checkNode {
@ -416,8 +499,8 @@ public final class SelectablePeerNode: ASDisplayNode {
self.onlineNode.frame = CGRect(origin: CGPoint(x: avatarContainerFrame.maxX - self.onlineNode.frame.width - 2.0, y: avatarContainerFrame.maxY - self.onlineNode.frame.height - 2.0), size: self.onlineNode.frame.size)
if let checkNode = self.checkNode {
let checkSize = CGSize(width: 24.0, height: 24.0)
checkNode.frame = CGRect(origin: CGPoint(x: avatarFrame.maxX - 10.0, y: avatarFrame.maxY - 18.0), size: checkSize)
let checkSize = CGSize(width: 22.0, height: 22.0)
checkNode.frame = CGRect(origin: CGPoint(x: avatarFrame.maxX - 14.0, y: avatarFrame.maxY - 15.0), size: checkSize)
}
}
}

@ -19,19 +19,22 @@ private final class IncomingMessagePrivacyScreenArguments {
let disabledValuePressed: () -> Void
let infoLinkAction: () -> Void
let openExceptions: () -> Void
let openPremiumInfo: () -> Void
init(
context: AccountContext,
updateValue: @escaping (GlobalPrivacySettings.NonContactChatsPrivacy) -> Void,
disabledValuePressed: @escaping () -> Void,
infoLinkAction: @escaping () -> Void,
openExceptions: @escaping () -> Void
openExceptions: @escaping () -> Void,
openPremiumInfo: @escaping () -> Void
) {
self.context = context
self.updateValue = updateValue
self.disabledValuePressed = disabledValuePressed
self.infoLinkAction = infoLinkAction
self.openExceptions = openExceptions
self.openPremiumInfo = openPremiumInfo
}
}
@ -49,8 +52,8 @@ private enum GlobalAutoremoveEntry: ItemListNodeEntry {
case optionChargeForMessages(value: GlobalPrivacySettings.NonContactChatsPrivacy, isEnabled: Bool)
case footer(value: GlobalPrivacySettings.NonContactChatsPrivacy)
case priceHeader
case price(value: Int64, price: String)
case priceInfo(value: String)
case price(value: Int64, maxValue: Int64, price: String, isEnabled: Bool)
case priceInfo(commission: Int32, value: String)
case exceptionsHeader
case exceptions(count: Int)
case exceptionsInfo
@ -128,7 +131,7 @@ private enum GlobalAutoremoveEntry: ItemListNodeEntry {
if case .paidMessages = value {
isChecked = true
}
return ItemListCheckboxItem(presentationData: presentationData, icon: isEnabled ? nil : generateTintedImage(image: UIImage(bundleImageName: "Chat/Stickers/Lock"), color: presentationData.theme.list.itemSecondaryTextColor), iconPlacement: .check, title: presentationData.strings.Privacy_Messages_ChargeForMessages, style: .left, checked: isChecked, zeroSeparatorInsets: false, sectionId: self.section, action: {
return ItemListCheckboxItem(presentationData: presentationData, icon: isEnabled || isChecked ? nil : generateTintedImage(image: UIImage(bundleImageName: "Chat/Stickers/Lock"), color: presentationData.theme.list.itemSecondaryTextColor), iconPlacement: .check, title: presentationData.strings.Privacy_Messages_ChargeForMessages, style: .left, checked: isChecked, zeroSeparatorInsets: false, sectionId: self.section, action: {
arguments.updateValue(.paidMessages(StarsAmount(value: 400, nanos: 0)))
})
case let .footer(value):
@ -145,12 +148,14 @@ private enum GlobalAutoremoveEntry: ItemListNodeEntry {
})
case .priceHeader:
return ItemListSectionHeaderItem(presentationData: presentationData, text: presentationData.strings.Privacy_Messages_MessagePrice, sectionId: self.section)
case let .price(value, price):
return MessagePriceItem(theme: presentationData.theme, strings: presentationData.strings, minValue: 10, maxValue: 9000, value: value, price: price, sectionId: self.section, updated: { value in
case let .price(value, maxValue, price, isEnabled):
return MessagePriceItem(theme: presentationData.theme, strings: presentationData.strings, isEnabled: isEnabled, minValue: 1, maxValue: maxValue, value: value, price: price, sectionId: self.section, updated: { value in
arguments.updateValue(.paidMessages(StarsAmount(value: value, nanos: 0)))
}, openPremiumInfo: {
arguments.openPremiumInfo()
})
case let .priceInfo(value):
return ItemListTextItem(presentationData: presentationData, text: .markdown(presentationData.strings.Privacy_Messages_MessagePriceInfo(value).string), sectionId: self.section)
case let .priceInfo(commission, value):
return ItemListTextItem(presentationData: presentationData, text: .markdown(presentationData.strings.Privacy_Messages_MessagePriceInfo("\(commission)", value).string), sectionId: self.section)
case .exceptionsHeader:
return ItemListSectionHeaderItem(presentationData: presentationData, text: presentationData.strings.Privacy_Messages_RemoveFeeHeader, sectionId: self.section)
case let .exceptions(count):
@ -168,29 +173,32 @@ private struct IncomingMessagePrivacyScreenState: Equatable {
var disableFor: [EnginePeer.Id: SelectivePrivacyPeer]
}
private func incomingMessagePrivacyScreenEntries(presentationData: PresentationData, state: IncomingMessagePrivacyScreenState, isPremium: Bool, configuration: StarsSubscriptionConfiguration) -> [GlobalAutoremoveEntry] {
private func incomingMessagePrivacyScreenEntries(presentationData: PresentationData, state: IncomingMessagePrivacyScreenState, enableSetting: Bool, isPremium: Bool, configuration: StarsSubscriptionConfiguration) -> [GlobalAutoremoveEntry] {
var entries: [GlobalAutoremoveEntry] = []
entries.append(.header)
entries.append(.optionEverybody(value: state.updatedValue))
entries.append(.optionPremium(value: state.updatedValue, isEnabled: isPremium))
entries.append(.optionChargeForMessages(value: state.updatedValue, isEnabled: isPremium))
entries.append(.optionPremium(value: state.updatedValue, isEnabled: enableSetting))
if configuration.paidMessagesAvailable {
entries.append(.optionChargeForMessages(value: state.updatedValue, isEnabled: isPremium))
}
if case let .paidMessages(amount) = state.updatedValue {
entries.append(.footer(value: state.updatedValue))
entries.append(.priceHeader)
var usdRate = 0.012
if let usdWithdrawRate = configuration.usdWithdrawRate {
usdRate = Double(usdWithdrawRate) / 1000.0 / 100.0
}
let usdRate = Double(configuration.usdWithdrawRate) / 1000.0 / 100.0
let price = "\(formatTonUsdValue(amount.value, divide: false, rate: usdRate, dateTimeFormat: presentationData.dateTimeFormat))"
entries.append(.price(value: amount.value, price: price))
entries.append(.priceInfo(value: price))
entries.append(.exceptionsHeader)
entries.append(.exceptions(count: state.disableFor.count))
entries.append(.exceptionsInfo)
entries.append(.price(value: amount.value, maxValue: configuration.paidMessageMaxAmount, price: price, isEnabled: isPremium))
entries.append(.priceInfo(commission: configuration.paidMessageCommissionPermille / 10, value: price))
if isPremium {
entries.append(.exceptionsHeader)
entries.append(.exceptions(count: state.disableFor.count))
entries.append(.exceptionsInfo)
}
} else {
entries.append(.footer(value: state.updatedValue))
entries.append(.info)
@ -345,6 +353,17 @@ public func incomingMessagePrivacyScreen(context: AccountContext, value: GlobalP
})
pushControllerImpl?(controller)
}
},
openPremiumInfo: {
var replaceImpl: ((ViewController) -> Void)?
let controller = context.sharedContext.makePremiumDemoController(context: context, subject: .paidMessages, forceDark: false, action: {
let controller = context.sharedContext.makePremiumIntroController(context: context, source: .paidMessages, forceDark: false, dismissed: nil)
replaceImpl?(controller)
}, dismissed: nil)
replaceImpl = { [weak controller] c in
controller?.replace(with: c)
}
pushControllerImpl?(controller)
}
)
@ -375,7 +394,7 @@ public func incomingMessagePrivacyScreen(context: AccountContext, value: GlobalP
let title: ItemListControllerTitle = .text(presentationData.strings.Privacy_Messages_Title)
let entries: [GlobalAutoremoveEntry] = incomingMessagePrivacyScreenEntries(presentationData: presentationData, state: state, isPremium: enableSetting, configuration: configuration)
let entries: [GlobalAutoremoveEntry] = incomingMessagePrivacyScreenEntries(presentationData: presentationData, state: state, enableSetting: enableSetting, isPremium: context.isPremium, configuration: configuration)
let animateChanges = false
@ -412,7 +431,12 @@ public func incomingMessagePrivacyScreen(context: AccountContext, value: GlobalP
controller?.push(c)
}
controller.attemptNavigation = { _ in
update(stateValue.with({ $0 }).updatedValue)
let updatedValue = stateValue.with({ $0 }).updatedValue
if !context.isPremium, case .paidMessages = updatedValue {
} else {
update(updatedValue)
}
return true
}
dismissImpl = { [weak controller] in

@ -437,9 +437,8 @@ private enum PrivacyAndSecurityEntry: ItemListNodeEntry {
label = presentationData.strings.Settings_Privacy_Messages_ValueEveryone
case .requirePremium:
label = presentationData.strings.Settings_Privacy_Messages_ValueContactsAndPremium
case let .paidMessages(amount):
//TODO:localize
label = "\(amount.value) Stars"
case .paidMessages:
label = presentationData.strings.Settings_Privacy_Messages_ValuePaid
}
return ItemListDisclosureItem(presentationData: presentationData, title: presentationData.strings.Settings_Privacy_Messages, titleIcon: hasPremium ? PresentationResourcesItemList.premiumIcon(theme) : nil, label: label, sectionId: self.section, style: .blocks, action: {
arguments.openMessagePrivacy()
@ -862,7 +861,11 @@ public func privacyAndSecurityController(
updateHasTwoStepAuth()
var setupEmailImpl: ((String?) -> Void)?
var reviewCallPrivacySuggestion = false
var reviewInvitePrivacySuggestion = false
var showPrivacySuggestionImpl: (() -> Void)?
let arguments = PrivacyAndSecurityControllerArguments(account: context.account, openBlockedUsers: {
pushControllerImpl?(blockedPeersController(context: context, blockedPeersContext: blockedPeersContext), true)
}, openLastSeenPrivacy: {
@ -907,6 +910,10 @@ public func privacyAndSecurityController(
return .complete()
}
currentInfoDisposable.set(applySetting.start())
Queue.mainQueue().after(0.3) {
showPrivacySuggestionImpl?()
}
}
}), true)
}
@ -944,6 +951,10 @@ public func privacyAndSecurityController(
return .complete()
}
currentInfoDisposable.set(applySetting.start())
Queue.mainQueue().after(0.3) {
showPrivacySuggestionImpl?()
}
}
}), true)
}
@ -1319,6 +1330,22 @@ public func privacyAndSecurityController(
return state
}
}))
if case .everybody = privacySettings.globalSettings.nonContactChatsPrivacy {
if case .everybody = settingValue {
} else {
if case .enableEveryone = privacySettings.voiceCalls {
reviewCallPrivacySuggestion = true
}
if case .enableEveryone = privacySettings.groupInvitations {
reviewInvitePrivacySuggestion = true
}
Queue.mainQueue().after(0.3) {
showPrivacySuggestionImpl?()
}
}
}
}), true)
})
}, openGiftsPrivacy: {
@ -1442,6 +1469,50 @@ public func privacyAndSecurityController(
}
}
showPrivacySuggestionImpl = {
//TODO:localize
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
if reviewCallPrivacySuggestion {
reviewCallPrivacySuggestion = false
let alertController = textAlertController(
context: context,
title: "Call Settings",
text: "You've restricted who can message you, but anyone can still call you. Would you like to review these settings?",
actions: [
TextAlertAction(type: .defaultAction, title: "Review", action: {
arguments.openVoiceCallPrivacy()
}),
TextAlertAction(type: .genericAction, title: presentationData.strings.Common_Cancel, action: {
Queue.mainQueue().after(0.3) {
showPrivacySuggestionImpl?()
}
})
],
actionLayout: .vertical
)
presentControllerImpl?(alertController)
} else if reviewInvitePrivacySuggestion {
reviewInvitePrivacySuggestion = false
let alertController = textAlertController(
context: context,
title: "Invitation Settings",
text: "You've restricted who can message you, but anyone can still invite you to groups and channels. Would you like to review these settings?",
actions: [
TextAlertAction(type: .defaultAction, title: "Review", action: {
arguments.openGroupsPrivacy()
}),
TextAlertAction(type: .genericAction, title: presentationData.strings.Common_Cancel, action: {
Queue.mainQueue().after(0.3) {
showPrivacySuggestionImpl?()
}
})
],
actionLayout: .vertical
)
presentControllerImpl?(alertController)
}
}
setupEmailImpl = { emailPattern in
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
var dismissEmailControllerImpl: (() -> Void)?

@ -46,6 +46,7 @@ swift_library(
"//submodules/TelegramUI/Components/LottieComponent",
"//submodules/TelegramUI/Components/MessageInputPanelComponent",
"//submodules/TelegramUI/Components/ChatEntityKeyboardInputNode",
"//submodules/TelegramUI/Components/Chat/ChatMessagePaymentAlertController",
"//submodules/ChatPresentationInterfaceState",
"//submodules/CheckNode",
],

@ -21,6 +21,7 @@ import AnimationCache
import MultiAnimationRenderer
import ObjectiveC
import UndoUI
import ChatMessagePaymentAlertController
private var ObjCKey_DeinitWatcher: Int?
@ -405,7 +406,7 @@ public final class ShareController: ViewController {
private let fromForeignApp: Bool
private let collectibleItemInfo: TelegramCollectibleItemInfo?
private let peers = Promise<([(peer: EngineRenderedPeer, presence: EnginePeer.Presence?, requiresPremiumForMessaging: Bool)], EnginePeer)>()
private let peers = Promise<([(peer: EngineRenderedPeer, presence: EnginePeer.Presence?, requiresPremiumForMessaging: Bool, requiresStars: Int64?)], EnginePeer)>()
private let peersDisposable = MetaDisposable()
private let readyDisposable = MetaDisposable()
private let accountActiveDisposable = MetaDisposable()
@ -664,12 +665,21 @@ public final class ShareController: ViewController {
override public func loadDisplayNode() {
var fromPublicChannel = false
var messageCount: Int = 1
if case let .messages(messages) = self.subject, let message = messages.first, let peer = message.peers[message.id.peerId] as? TelegramChannel, case .broadcast = peer.info {
fromPublicChannel = true
} else if case let .url(link) = self.subject, link.contains("t.me/nft/") {
fromPublicChannel = true
}
if case let .messages(messages) = self.subject {
messageCount = messages.count
} else if case let .image(images) = self.subject {
messageCount = images.count
} else if case let .fromExternal(count, _) = self.subject {
messageCount = count
}
var mediaParameters: ShareControllerSubject.MediaParameters?
if case let .media(_, parameters) = self.subject {
mediaParameters = parameters
@ -682,7 +692,7 @@ public final class ShareController: ViewController {
return
}
strongSelf.present(standardTextAlertController(theme: AlertControllerTheme(presentationData: strongSelf.presentationData), title: title, text: text, actions: [TextAlertAction(type: .defaultAction, title: strongSelf.presentationData.strings.Common_OK, action: {})]), in: .window(.root))
}, externalShare: self.externalShare, immediateExternalShare: self.immediateExternalShare, immediatePeerId: self.immediatePeerId, fromForeignApp: self.fromForeignApp, forceTheme: self.forceTheme, fromPublicChannel: fromPublicChannel, segmentedValues: self.segmentedValues, shareStory: self.shareStory, collectibleItemInfo: self.collectibleItemInfo)
}, externalShare: self.externalShare, immediateExternalShare: self.immediateExternalShare, immediatePeerId: self.immediatePeerId, fromForeignApp: self.fromForeignApp, forceTheme: self.forceTheme, fromPublicChannel: fromPublicChannel, segmentedValues: self.segmentedValues, shareStory: self.shareStory, collectibleItemInfo: self.collectibleItemInfo, messageCount: messageCount)
self.controllerNode.completed = self.completed
self.controllerNode.enqueued = self.enqueued
self.controllerNode.present = { [weak self] c in
@ -1240,21 +1250,31 @@ public final class ShareController: ViewController {
return self.currentContext.stateManager.postbox.combinedView(
keys: peerIds.map { peerId in
return PostboxViewKey.basicPeer(peerId)
} + peerIds.map { peerId in
return PostboxViewKey.cachedPeerData(peerId: peerId)
}
)
|> take(1)
|> map { views -> [EnginePeer.Id: EnginePeer?] in
|> map { views -> ([EnginePeer.Id: EnginePeer?], [EnginePeer.Id: StarsAmount]) in
var result: [EnginePeer.Id: EnginePeer?] = [:]
var requiresStars: [EnginePeer.Id: StarsAmount] = [:]
for peerId in peerIds {
if let view = views.views[PostboxViewKey.basicPeer(peerId)] as? BasicPeerView, let peer = view.peer {
result[peerId] = EnginePeer(peer)
if peer is TelegramUser, let cachedPeerDataView = views.views[PostboxViewKey.cachedPeerData(peerId: peerId)] as? CachedPeerDataView {
if let cachedData = cachedPeerDataView.cachedPeerData as? CachedUserData {
requiresStars[peerId] = cachedData.sendPaidMessageStars
}
} else if let channel = peer as? TelegramChannel {
requiresStars[peerId] = channel.sendPaidMessageStars
}
}
}
return result
return (result, requiresStars)
}
|> deliverOnMainQueue
|> castError(ShareControllerError.self)
|> mapToSignal { [weak self] peers -> Signal<ShareState, ShareControllerError> in
|> mapToSignal { [weak self] peers, requiresStars -> Signal<ShareState, ShareControllerError> in
guard let strongSelf = self else {
return .complete()
}
@ -1266,7 +1286,7 @@ public final class ShareController: ViewController {
subject = selectedValue.subject
}
func transformMessages(_ messages: [StandaloneSendEnqueueMessage], showNames: Bool, silently: Bool) -> [StandaloneSendEnqueueMessage] {
func transformMessages(_ messages: [StandaloneSendEnqueueMessage], showNames: Bool, silently: Bool, sendPaidMessageStars: StarsAmount?) -> [StandaloneSendEnqueueMessage] {
return messages.map { message in
var message = message
if !showNames {
@ -1278,6 +1298,7 @@ public final class ShareController: ViewController {
if silently {
message.isSilent = true
}
message.sendPaidMessageStars = sendPaidMessageStars
return message
}
}
@ -1325,7 +1346,7 @@ public final class ShareController: ViewController {
replyToMessageId: replyToMessageId
))
}
messages = transformMessages(messages, showNames: showNames, silently: silently)
messages = transformMessages(messages, showNames: showNames, silently: silently, sendPaidMessageStars: requiresStars[peerId])
shareSignals.append(standaloneSendEnqueueMessages(
accountPeerId: strongSelf.currentContext.accountPeerId,
postbox: strongSelf.currentContext.stateManager.postbox,
@ -1386,7 +1407,7 @@ public final class ShareController: ViewController {
)),
replyToMessageId: replyToMessageId
))
messages = transformMessages(messages, showNames: showNames, silently: silently)
messages = transformMessages(messages, showNames: showNames, silently: silently, sendPaidMessageStars: requiresStars[peerId])
shareSignals.append(standaloneSendEnqueueMessages(
accountPeerId: strongSelf.currentContext.accountPeerId,
postbox: strongSelf.currentContext.stateManager.postbox,
@ -1451,7 +1472,7 @@ public final class ShareController: ViewController {
)),
replyToMessageId: replyToMessageId
))
messages = transformMessages(messages, showNames: showNames, silently: silently)
messages = transformMessages(messages, showNames: showNames, silently: silently, sendPaidMessageStars: requiresStars[peerId])
shareSignals.append(standaloneSendEnqueueMessages(
accountPeerId: strongSelf.currentContext.accountPeerId,
postbox: strongSelf.currentContext.stateManager.postbox,
@ -1511,7 +1532,7 @@ public final class ShareController: ViewController {
replyToMessageId: replyToMessageId
))
}
messages = transformMessages(messages, showNames: showNames, silently: silently)
messages = transformMessages(messages, showNames: showNames, silently: silently, sendPaidMessageStars: requiresStars[peerId])
shareSignals.append(standaloneSendEnqueueMessages(
accountPeerId: strongSelf.currentContext.accountPeerId,
postbox: strongSelf.currentContext.stateManager.postbox,
@ -1621,7 +1642,7 @@ public final class ShareController: ViewController {
),
replyToMessageId: replyToMessageId
))
messages = transformMessages(messages, showNames: showNames, silently: silently)
messages = transformMessages(messages, showNames: showNames, silently: silently, sendPaidMessageStars: requiresStars[peerId])
shareSignals.append(standaloneSendEnqueueMessages(
accountPeerId: strongSelf.currentContext.accountPeerId,
postbox: strongSelf.currentContext.stateManager.postbox,
@ -1680,7 +1701,7 @@ public final class ShareController: ViewController {
content: .map(map: media),
replyToMessageId: replyToMessageId
))
messages = transformMessages(messages, showNames: showNames, silently: silently)
messages = transformMessages(messages, showNames: showNames, silently: silently, sendPaidMessageStars: requiresStars[peerId])
shareSignals.append(standaloneSendEnqueueMessages(
accountPeerId: strongSelf.currentContext.accountPeerId,
postbox: strongSelf.currentContext.stateManager.postbox,
@ -1800,7 +1821,7 @@ public final class ShareController: ViewController {
replyToMessageId: replyToMessageId
))
}
messagesToEnqueue = transformMessages(messagesToEnqueue, showNames: showNames, silently: silently)
messagesToEnqueue = transformMessages(messagesToEnqueue, showNames: showNames, silently: silently, sendPaidMessageStars: requiresStars[peerId])
shareSignals.append(standaloneSendEnqueueMessages(
accountPeerId: strongSelf.currentContext.accountPeerId,
postbox: strongSelf.currentContext.stateManager.postbox,
@ -1820,8 +1841,8 @@ public final class ShareController: ViewController {
messages: messagesToEnqueue
))
}
case let .fromExternal(f):
return f(peerIds, topicIds, text, strongSelf.currentContext, silently)
case let .fromExternal(_, f):
return f(peerIds, topicIds, requiresStars, text, strongSelf.currentContext, silently)
|> map { state -> ShareState in
switch state {
case let .preparing(long):
@ -1880,12 +1901,34 @@ public final class ShareController: ViewController {
guard let currentContext = self.currentContext as? ShareControllerAppAccountContext else {
return .single(.done([]))
}
return currentContext.context.engine.data.get(EngineDataMap(
peerIds.map(TelegramEngine.EngineData.Item.Peer.Peer.init(id:))
))
return currentContext.stateManager.postbox.combinedView(
keys: peerIds.map { peerId in
return PostboxViewKey.basicPeer(peerId)
} + peerIds.map { peerId in
return PostboxViewKey.cachedPeerData(peerId: peerId)
}
)
|> take(1)
|> map { views -> ([EnginePeer.Id: EnginePeer?], [EnginePeer.Id: StarsAmount]) in
var result: [EnginePeer.Id: EnginePeer?] = [:]
var requiresStars: [EnginePeer.Id: StarsAmount] = [:]
for peerId in peerIds {
if let view = views.views[PostboxViewKey.basicPeer(peerId)] as? BasicPeerView, let peer = view.peer {
result[peerId] = EnginePeer(peer)
if peer is TelegramUser, let cachedPeerDataView = views.views[PostboxViewKey.cachedPeerData(peerId: peerId)] as? CachedPeerDataView {
if let cachedData = cachedPeerDataView.cachedPeerData as? CachedUserData {
requiresStars[peerId] = cachedData.sendPaidMessageStars
}
} else if let channel = peer as? TelegramChannel {
requiresStars[peerId] = channel.sendPaidMessageStars
}
}
}
return (result, requiresStars)
}
|> deliverOnMainQueue
|> castError(ShareControllerError.self)
|> mapToSignal { [weak self] peers -> Signal<ShareState, ShareControllerError> in
|> mapToSignal { [weak self] peers, requiresStars -> Signal<ShareState, ShareControllerError> in
guard let strongSelf = self, let currentContext = strongSelf.currentContext as? ShareControllerAppAccountContext else {
return .complete()
}
@ -1897,7 +1940,7 @@ public final class ShareController: ViewController {
subject = selectedValue.subject
}
func transformMessages(_ messages: [EnqueueMessage], showNames: Bool, silently: Bool) -> [EnqueueMessage] {
func transformMessages(_ messages: [EnqueueMessage], showNames: Bool, silently: Bool, sendPaidMessageStars: StarsAmount?) -> [EnqueueMessage] {
return messages.map { message in
return message.withUpdatedAttributes({ attributes in
var attributes = attributes
@ -1907,6 +1950,9 @@ public final class ShareController: ViewController {
if silently {
attributes.append(NotificationInfoMessageAttribute(flags: .muted))
}
if let sendPaidMessageStars {
attributes.append(PaidStarsMessageAttribute(stars: sendPaidMessageStars, postponeSending: false))
}
return attributes
})
}
@ -1949,7 +1995,7 @@ public final class ShareController: ViewController {
} else {
messages.append(.message(text: url, attributes: [], inlineStickers: [:], mediaReference: nil, threadId: threadId, replyToMessageId: replyToMessageId.flatMap { EngineMessageReplySubject(messageId: $0, quote: nil) }, replyToStoryId: nil, localGroupingKey: nil, correlationId: nil, bubbleUpEmojiOrStickersets: []))
}
messages = transformMessages(messages, showNames: showNames, silently: silently)
messages = transformMessages(messages, showNames: showNames, silently: silently, sendPaidMessageStars: requiresStars[peerId])
shareSignals.append(enqueueMessages(account: currentContext.context.account, peerId: peerId, messages: messages))
}
case let .text(string):
@ -1983,7 +2029,7 @@ public final class ShareController: ViewController {
messages.append(.message(text: text, attributes: [], inlineStickers: [:], mediaReference: nil, threadId: threadId, replyToMessageId: replyToMessageId.flatMap { EngineMessageReplySubject(messageId: $0, quote: nil) }, replyToStoryId: nil, localGroupingKey: nil, correlationId: nil, bubbleUpEmojiOrStickersets: []))
}
messages.append(.message(text: string, attributes: [], inlineStickers: [:], mediaReference: nil, threadId: threadId, replyToMessageId: replyToMessageId.flatMap { EngineMessageReplySubject(messageId: $0, quote: nil) }, replyToStoryId: nil, localGroupingKey: nil, correlationId: nil, bubbleUpEmojiOrStickersets: []))
messages = transformMessages(messages, showNames: showNames, silently: silently)
messages = transformMessages(messages, showNames: showNames, silently: silently, sendPaidMessageStars: requiresStars[peerId])
shareSignals.append(enqueueMessages(account: currentContext.context.account, peerId: peerId, messages: messages))
}
case let .quote(string, url):
@ -2020,7 +2066,7 @@ public final class ShareController: ViewController {
attributedText.append(NSAttributedString(string: "\n\n\(url)"))
let entities = generateChatInputTextEntities(attributedText)
messages.append(.message(text: attributedText.string, attributes: [TextEntitiesMessageAttribute(entities: entities)], inlineStickers: [:], mediaReference: nil, threadId: threadId, replyToMessageId: replyToMessageId.flatMap { EngineMessageReplySubject(messageId: $0, quote: nil) }, replyToStoryId: nil, localGroupingKey: nil, correlationId: nil, bubbleUpEmojiOrStickersets: []))
messages = transformMessages(messages, showNames: showNames, silently: silently)
messages = transformMessages(messages, showNames: showNames, silently: silently, sendPaidMessageStars: requiresStars[peerId])
shareSignals.append(enqueueMessages(account: currentContext.context.account, peerId: peerId, messages: messages))
}
case let .image(representations):
@ -2051,7 +2097,7 @@ public final class ShareController: ViewController {
var messages: [EnqueueMessage] = []
messages.append(.message(text: text, attributes: [], inlineStickers: [:], mediaReference: .standalone(media: TelegramMediaImage(imageId: MediaId(namespace: Namespaces.Media.LocalImage, id: Int64.random(in: Int64.min ... Int64.max)), representations: representations.map({ $0.representation }), immediateThumbnailData: nil, reference: nil, partialReference: nil, flags: [])), threadId: threadId, replyToMessageId: replyToMessageId.flatMap { EngineMessageReplySubject(messageId: $0, quote: nil) }, replyToStoryId: nil, localGroupingKey: nil, correlationId: nil, bubbleUpEmojiOrStickersets: []))
messages = transformMessages(messages, showNames: showNames, silently: silently)
messages = transformMessages(messages, showNames: showNames, silently: silently, sendPaidMessageStars: requiresStars[peerId])
shareSignals.append(enqueueMessages(account: currentContext.context.account, peerId: peerId, messages: messages))
}
case let .media(mediaReference, mediaParameters):
@ -2145,7 +2191,7 @@ public final class ShareController: ViewController {
} else {
messages.append(.message(text: sendTextAsCaption ? text : "", attributes: attributes, inlineStickers: [:], mediaReference: mediaReference, threadId: threadId, replyToMessageId: replyToMessageId.flatMap { EngineMessageReplySubject(messageId: $0, quote: nil) }, replyToStoryId: nil, localGroupingKey: nil, correlationId: nil, bubbleUpEmojiOrStickersets: []))
}
messages = transformMessages(messages, showNames: showNames, silently: silently)
messages = transformMessages(messages, showNames: showNames, silently: silently, sendPaidMessageStars: requiresStars[peerId])
shareSignals.append(enqueueMessages(account: currentContext.context.account, peerId: peerId, messages: messages))
}
case let .mapMedia(media):
@ -2179,7 +2225,7 @@ public final class ShareController: ViewController {
messages.append(.message(text: text, attributes: [], inlineStickers: [:], mediaReference: nil, threadId: threadId, replyToMessageId: replyToMessageId.flatMap { EngineMessageReplySubject(messageId: $0, quote: nil) }, replyToStoryId: nil, localGroupingKey: nil, correlationId: nil, bubbleUpEmojiOrStickersets: []))
}
messages.append(.message(text: "", attributes: [], inlineStickers: [:], mediaReference: .standalone(media: media), threadId: threadId, replyToMessageId: replyToMessageId.flatMap { EngineMessageReplySubject(messageId: $0, quote: nil) }, replyToStoryId: nil, localGroupingKey: nil, correlationId: nil, bubbleUpEmojiOrStickersets: []))
messages = transformMessages(messages, showNames: showNames, silently: silently)
messages = transformMessages(messages, showNames: showNames, silently: silently, sendPaidMessageStars: requiresStars[peerId])
shareSignals.append(enqueueMessages(account: currentContext.context.account, peerId: peerId, messages: messages))
}
case let .messages(messages):
@ -2274,11 +2320,11 @@ public final class ShareController: ViewController {
correlationIds.append(correlationId)
messagesToEnqueue.append(.forward(source: message.id, threadId: threadId, grouping: .auto, attributes: [], correlationId: correlationId))
}
messagesToEnqueue = transformMessages(messagesToEnqueue, showNames: showNames, silently: silently)
messagesToEnqueue = transformMessages(messagesToEnqueue, showNames: showNames, silently: silently, sendPaidMessageStars: requiresStars[peerId])
shareSignals.append(enqueueMessages(account: currentContext.context.account, peerId: peerId, messages: messagesToEnqueue))
}
case let .fromExternal(f):
return f(peerIds, topicIds, text, currentContext, silently)
case let .fromExternal(_, f):
return f(peerIds, topicIds, requiresStars, text, currentContext, silently)
|> map { state -> ShareState in
switch state {
case let .preparing(long):
@ -2458,7 +2504,7 @@ public final class ShareController: ViewController {
peer,
tailChatList |> take(1)
)
|> mapToSignal { maybeAccountPeer, view -> Signal<([(peer: EngineRenderedPeer, presence: EnginePeer.Presence?, requiresPremiumForMessaging: Bool)], EnginePeer), NoError> in
|> mapToSignal { maybeAccountPeer, view -> Signal<([(peer: EngineRenderedPeer, presence: EnginePeer.Presence?, requiresPremiumForMessaging: Bool, requiresStars: Int64?)], EnginePeer), NoError> in
let accountPeer = maybeAccountPeer!
var peers: [EngineRenderedPeer] = []
@ -2468,7 +2514,7 @@ public final class ShareController: ViewController {
case let .MessageEntry(entryData):
if let peer = entryData.renderedPeer.peers[entryData.renderedPeer.peerId], peer.id != accountPeer.id, canSendMessagesToPeer(peer) {
peers.append(EngineRenderedPeer(entryData.renderedPeer))
if let user = peer as? TelegramUser, user.flags.contains(.requirePremium) {
if let user = peer as? TelegramUser, user.flags.contains(.requirePremium) || user.flags.contains(.requireStars) {
possiblePremiumRequiredPeers.insert(user.id)
}
}
@ -2487,7 +2533,7 @@ public final class ShareController: ViewController {
}
return account.stateManager.postbox.combinedView(keys: keys)
|> map { views -> ([EnginePeer.Id: EnginePeer.Presence?], [EnginePeer.Id: Bool]) in
|> map { views -> ([EnginePeer.Id: EnginePeer.Presence?], [EnginePeer.Id: Bool], [EnginePeer.Id: Int64]) in
var result: [EnginePeer.Id: EnginePeer.Presence?] = [:]
if let view = views.views[peerPresencesKey] as? PeerPresencesView {
result = view.presences.mapValues { value -> EnginePeer.Presence? in
@ -2495,19 +2541,21 @@ public final class ShareController: ViewController {
}
}
var requiresPremiumForMessaging: [EnginePeer.Id: Bool] = [:]
var requiresStars: [EnginePeer.Id: Int64] = [:]
for id in possiblePremiumRequiredPeers {
if let view = views.views[.cachedPeerData(peerId: id)] as? CachedPeerDataView, let data = view.cachedPeerData as? CachedUserData {
requiresPremiumForMessaging[id] = data.flags.contains(.premiumRequired)
requiresStars[id] = data.sendPaidMessageStars?.value
} else {
requiresPremiumForMessaging[id] = false
}
}
return (result, requiresPremiumForMessaging)
return (result, requiresPremiumForMessaging, requiresStars)
}
|> map { presenceMap, requiresPremiumForMessaging -> ([(peer: EngineRenderedPeer, presence: EnginePeer.Presence?, requiresPremiumForMessaging: Bool)], EnginePeer) in
var resultPeers: [(peer: EngineRenderedPeer, presence: EnginePeer.Presence?, requiresPremiumForMessaging: Bool)] = []
|> map { presenceMap, requiresPremiumForMessaging, requiresStars -> ([(peer: EngineRenderedPeer, presence: EnginePeer.Presence?, requiresPremiumForMessaging: Bool, requiresStars: Int64?)], EnginePeer) in
var resultPeers: [(peer: EngineRenderedPeer, presence: EnginePeer.Presence?, requiresPremiumForMessaging: Bool, requiresStars: Int64?)] = []
for peer in peers {
resultPeers.append((peer, presenceMap[peer.peerId].flatMap { $0 }, requiresPremiumForMessaging[peer.peerId] ?? false))
resultPeers.append((peer, presenceMap[peer.peerId].flatMap { $0 }, requiresPremiumForMessaging[peer.peerId] ?? false, requiresStars[peer.peerId]))
}
return (resultPeers, accountPeer)
}

@ -15,6 +15,7 @@ import TelegramStringFormatting
import BundleIconComponent
import LottieComponent
import CheckNode
import ChatMessagePaymentAlertController
enum ShareState {
case preparing(Bool)
@ -327,6 +328,7 @@ final class ShareControllerNode: ViewControllerTracingNode, ASScrollViewDelegate
private let segmentedValues: [ShareControllerSegmentedValue]?
private let collectibleItemInfo: TelegramCollectibleItemInfo?
private let mediaParameters: ShareControllerSubject.MediaParameters?
private let messageCount: Int
var selectedSegmentedIndex: Int = 0
@ -387,7 +389,7 @@ final class ShareControllerNode: ViewControllerTracingNode, ASScrollViewDelegate
private let showNames = ValuePromise<Bool>(true)
init(controller: ShareController, environment: ShareControllerEnvironment, presentationData: PresentationData, presetText: String?, defaultAction: ShareControllerAction?, mediaParameters: ShareControllerSubject.MediaParameters?, requestLayout: @escaping (ContainedViewLayoutTransition) -> Void, presentError: @escaping (String?, String) -> Void, externalShare: Bool, immediateExternalShare: Bool, immediatePeerId: PeerId?, fromForeignApp: Bool, forceTheme: PresentationTheme?, fromPublicChannel: Bool, segmentedValues: [ShareControllerSegmentedValue]?, shareStory: (() -> Void)?, collectibleItemInfo: TelegramCollectibleItemInfo?) {
init(controller: ShareController, environment: ShareControllerEnvironment, presentationData: PresentationData, presetText: String?, defaultAction: ShareControllerAction?, mediaParameters: ShareControllerSubject.MediaParameters?, requestLayout: @escaping (ContainedViewLayoutTransition) -> Void, presentError: @escaping (String?, String) -> Void, externalShare: Bool, immediateExternalShare: Bool, immediatePeerId: PeerId?, fromForeignApp: Bool, forceTheme: PresentationTheme?, fromPublicChannel: Bool, segmentedValues: [ShareControllerSegmentedValue]?, shareStory: (() -> Void)?, collectibleItemInfo: TelegramCollectibleItemInfo?, messageCount: Int) {
self.controller = controller
self.environment = environment
self.presentationData = presentationData
@ -401,6 +403,7 @@ final class ShareControllerNode: ViewControllerTracingNode, ASScrollViewDelegate
self.segmentedValues = segmentedValues
self.collectibleItemInfo = collectibleItemInfo
self.mediaParameters = mediaParameters
self.messageCount = messageCount
self.presetText = presetText
@ -1260,7 +1263,7 @@ final class ShareControllerNode: ViewControllerTracingNode, ASScrollViewDelegate
})
}
}
func send(peerId: PeerId? = nil, showNames: Bool = true, silently: Bool = false) {
let peerIds: [PeerId]
if let peerId = peerId {
@ -1273,19 +1276,29 @@ final class ShareControllerNode: ViewControllerTracingNode, ASScrollViewDelegate
let _ = (context.stateManager.postbox.combinedView(
keys: peerIds.map { peerId in
return PostboxViewKey.basicPeer(peerId)
} + peerIds.map { peerId in
return PostboxViewKey.cachedPeerData(peerId: peerId)
}
)
|> take(1)
|> map { views -> [EnginePeer.Id: EnginePeer?] in
|> map { views -> ([EnginePeer.Id: EnginePeer?], [EnginePeer.Id: Int64]) in
var result: [EnginePeer.Id: EnginePeer?] = [:]
var requiresStars: [EnginePeer.Id: Int64] = [:]
for peerId in peerIds {
if let view = views.views[PostboxViewKey.basicPeer(peerId)] as? BasicPeerView, let peer = view.peer {
result[peerId] = EnginePeer(peer)
if peer is TelegramUser, let cachedPeerDataView = views.views[PostboxViewKey.cachedPeerData(peerId: peerId)] as? CachedPeerDataView {
if let cachedData = cachedPeerDataView.cachedPeerData as? CachedUserData {
requiresStars[peerId] = cachedData.sendPaidMessageStars?.value
}
} else if let channel = peer as? TelegramChannel {
requiresStars[peerId] = channel.sendPaidMessageStars?.value
}
}
}
return result
return (result, requiresStars)
}
|> deliverOnMainQueue).start(next: { [weak self] peers in
|> deliverOnMainQueue).start(next: { [weak self] peers, requiresStars in
guard let self else {
return
}
@ -1300,14 +1313,49 @@ final class ShareControllerNode: ViewControllerTracingNode, ASScrollViewDelegate
if !tryShare(self.inputFieldNode.text, mappedPeers) {
return
}
self.presentPaidMessageAlertIfNeeded(peers: mappedPeers, requiresStars: requiresStars, completion: { [weak self] in
self?.commitSend(peerId: peerId, showNames: showNames, silently: silently)
})
self.commitSend(peerId: peerId, showNames: showNames, silently: silently)
})
} else {
self.commitSend(peerId: peerId, showNames: showNames, silently: silently)
}
}
private func presentPaidMessageAlertIfNeeded(peers: [EnginePeer], requiresStars: [EnginePeer.Id: Int64], completion: @escaping () -> Void) {
var count: Int32 = Int32(self.messageCount)
if !self.inputFieldNode.text.isEmpty {
count += 1
}
var totalAmount: StarsAmount = .zero
for peer in peers {
if let stars = requiresStars[peer.id] {
totalAmount = totalAmount + StarsAmount(value: stars, nanos: 0)
}
}
if totalAmount.value > 0 {
let controller = chatMessagePaymentAlertController(
context: nil,
presentationData: self.presentationData,
updatedPresentationData: nil,
peers: peers,
count: count,
amount: totalAmount,
totalAmount: totalAmount,
hasCheck: false,
navigationController: nil,
completion: { _ in
completion()
}
)
self.present?(controller)
} else {
completion()
}
}
private func commitSend(peerId: PeerId?, showNames: Bool, silently: Bool) {
if !self.inputFieldNode.text.isEmpty {
for peer in self.controllerInteraction!.selectedPeers {
@ -1522,7 +1570,7 @@ final class ShareControllerNode: ViewControllerTracingNode, ASScrollViewDelegate
}
}
func updatePeers(context: ShareControllerAccountContext, switchableAccounts: [ShareControllerSwitchableAccount], peers: [(peer: EngineRenderedPeer, presence: EnginePeer.Presence?, requiresPremiumForMessaging: Bool)], accountPeer: EnginePeer, defaultAction: ShareControllerAction?) {
func updatePeers(context: ShareControllerAccountContext, switchableAccounts: [ShareControllerSwitchableAccount], peers: [(peer: EngineRenderedPeer, presence: EnginePeer.Presence?, requiresPremiumForMessaging: Bool, requiresStars: Int64?)], accountPeer: EnginePeer, defaultAction: ShareControllerAction?) {
self.context = context
if let peersContentNode = self.peersContentNode, peersContentNode.accountPeer.id == accountPeer.id {

@ -96,11 +96,11 @@ final class ShareControllerGridSectionNode: ASDisplayNode {
final class ShareControllerPeerGridItem: GridItem {
enum ShareItem: Equatable {
case peer(peer: EngineRenderedPeer, presence: EnginePeer.Presence?, topicId: Int64?, threadData: MessageHistoryThreadData?, requiresPremiumForMessaging: Bool)
case peer(peer: EngineRenderedPeer, presence: EnginePeer.Presence?, topicId: Int64?, threadData: MessageHistoryThreadData?, requiresPremiumForMessaging: Bool, requiresStars: Int64?)
case story(isMessage: Bool)
var peerId: EnginePeer.Id? {
if case let .peer(peer, _, _, _, _) = self {
if case let .peer(peer, _, _, _, _, _) = self {
return peer.peerId
} else {
return nil
@ -162,7 +162,7 @@ final class ShareControllerPeerGridItemNode: GridItemNode {
private var absoluteLocation: (CGRect, CGSize)?
var peerId: EnginePeer.Id? {
if let item = self.currentState?.item, case let .peer(peer, _, _, _, _) = item {
if let item = self.currentState?.item, case let .peer(peer, _, _, _, _, _) = item {
return peer.peerId
} else {
return nil
@ -177,7 +177,7 @@ final class ShareControllerPeerGridItemNode: GridItemNode {
self.peerNode.toggleSelection = { [weak self] isDisabled in
if let strongSelf = self {
if let (_, _, _, _, maybeItem, search) = strongSelf.currentState, let item = maybeItem {
if case let .peer(peer, _, _, _, _) = item, let _ = peer.peers[peer.peerId] {
if case let .peer(peer, _, _, _, _, _) = item, let _ = peer.peers[peer.peerId] {
if isDisabled {
strongSelf.controllerInteraction?.disabledPeerSelected(peer)
} else {
@ -213,7 +213,7 @@ final class ShareControllerPeerGridItemNode: GridItemNode {
var effectivePresence: EnginePeer.Presence?
let timestamp = Int32(CFAbsoluteTimeGetCurrent() + NSTimeIntervalSince1970)
self.peerNode.theme = itemTheme
if let item, case let .peer(renderedPeer, presence, _, threadData, requiresPremiumForMessaging) = item, let peer = renderedPeer.peer {
if let item, case let .peer(renderedPeer, presence, _, threadData, requiresPremiumForMessaging, requiresStars) = item, let peer = renderedPeer.peer {
effectivePresence = presence
var isOnline = false
var isSupport = false
@ -243,6 +243,7 @@ final class ShareControllerPeerGridItemNode: GridItemNode {
strings: strings,
peer: renderedPeer,
requiresPremiumForMessaging: requiresPremiumForMessaging,
requiresStars: requiresStars,
customTitle: threadData?.info.title,
iconId: threadData?.info.icon,
iconColor: threadData?.info.iconColor ?? 0,
@ -302,7 +303,7 @@ final class ShareControllerPeerGridItemNode: GridItemNode {
func updateSelection(animated: Bool) {
var selected = false
if let controllerInteraction = self.controllerInteraction, let (_, _, _, _, maybeItem, _) = self.currentState, let item = maybeItem {
if case let .peer(peer, _, _, _, _) = item {
if case let .peer(peer, _, _, _, _, _) = item {
selected = controllerInteraction.selectedPeerIds.contains(peer.peerId)
}
}

@ -43,7 +43,7 @@ private struct SharePeerEntry: Comparable, Identifiable {
var stableId: Int64 {
switch self.item {
case let .peer(peer, _, _, _, _):
case let .peer(peer, _, _, _, _, _):
return peer.peerId.toInt64()
case .story:
return 0
@ -137,7 +137,7 @@ final class SharePeersContainerNode: ASDisplayNode, ShareContentContainerNode {
private var validLayout: (CGSize, CGFloat)?
private var overrideGridOffsetTransition: ContainedViewLayoutTransition?
let peersValue = Promise<[(peer: EngineRenderedPeer, presence: EnginePeer.Presence?, requiresPremiumForMessaging: Bool)]>()
let peersValue = Promise<[(peer: EngineRenderedPeer, presence: EnginePeer.Presence?, requiresPremiumForMessaging: Bool, requiresStars: Int64?)]>()
private var _tick: Int = 0 {
didSet {
@ -146,7 +146,7 @@ final class SharePeersContainerNode: ASDisplayNode, ShareContentContainerNode {
}
private let tick = ValuePromise<Int>(0)
init(environment: ShareControllerEnvironment, context: ShareControllerAccountContext, switchableAccounts: [ShareControllerSwitchableAccount], theme: PresentationTheme, strings: PresentationStrings, nameDisplayOrder: PresentationPersonNameOrder, peers: [(peer: EngineRenderedPeer, presence: EnginePeer.Presence?, requiresPremiumForMessaging: Bool)], accountPeer: EnginePeer, controllerInteraction: ShareControllerInteraction, externalShare: Bool, switchToAnotherAccount: @escaping () -> Void, debugAction: @escaping () -> Void, extendedInitialReveal: Bool, segmentedValues: [ShareControllerSegmentedValue]?, fromPublicChannel: Bool) {
init(environment: ShareControllerEnvironment, context: ShareControllerAccountContext, switchableAccounts: [ShareControllerSwitchableAccount], theme: PresentationTheme, strings: PresentationStrings, nameDisplayOrder: PresentationPersonNameOrder, peers: [(peer: EngineRenderedPeer, presence: EnginePeer.Presence?, requiresPremiumForMessaging: Bool, requiresStars: Int64?)], accountPeer: EnginePeer, controllerInteraction: ShareControllerInteraction, externalShare: Bool, switchToAnotherAccount: @escaping () -> Void, debugAction: @escaping () -> Void, extendedInitialReveal: Bool, segmentedValues: [ShareControllerSegmentedValue]?, fromPublicChannel: Bool) {
self.environment = environment
self.context = context
self.theme = theme
@ -176,22 +176,22 @@ final class SharePeersContainerNode: ASDisplayNode, ShareContentContainerNode {
}
var existingPeerIds: Set<EnginePeer.Id> = Set()
entries.append(SharePeerEntry(index: index, item: .peer(peer: EngineRenderedPeer(peer: accountPeer), presence: nil, topicId: nil, threadData: nil, requiresPremiumForMessaging: false), theme: theme, strings: strings))
entries.append(SharePeerEntry(index: index, item: .peer(peer: EngineRenderedPeer(peer: accountPeer), presence: nil, topicId: nil, threadData: nil, requiresPremiumForMessaging: false, requiresStars: nil), theme: theme, strings: strings))
existingPeerIds.insert(accountPeer.id)
index += 1
for (peer, requiresPremiumForMessaging) in foundPeers.reversed() {
if !existingPeerIds.contains(peer.peerId) {
entries.append(SharePeerEntry(index: index, item: .peer(peer: peer, presence: nil, topicId: nil, threadData: nil, requiresPremiumForMessaging: requiresPremiumForMessaging), theme: theme, strings: strings))
entries.append(SharePeerEntry(index: index, item: .peer(peer: peer, presence: nil, topicId: nil, threadData: nil, requiresPremiumForMessaging: requiresPremiumForMessaging, requiresStars: nil), theme: theme, strings: strings))
existingPeerIds.insert(peer.peerId)
index += 1
}
}
for (peer, presence, requiresPremiumForMessaging) in initialPeers {
for (peer, presence, requiresPremiumForMessaging, requiresStars) in initialPeers {
if !existingPeerIds.contains(peer.peerId) {
let thread = controllerInteraction?.selectedTopics[peer.peerId]
entries.append(SharePeerEntry(index: index, item: .peer(peer: peer, presence: presence, topicId: thread?.0, threadData: thread?.1, requiresPremiumForMessaging: requiresPremiumForMessaging), theme: theme, strings: strings))
entries.append(SharePeerEntry(index: index, item: .peer(peer: peer, presence: presence, topicId: thread?.0, threadData: thread?.1, requiresPremiumForMessaging: requiresPremiumForMessaging, requiresStars: requiresStars), theme: theme, strings: strings))
existingPeerIds.insert(peer.peerId)
index += 1
}

@ -36,13 +36,13 @@ private enum ShareSearchRecentEntryStableId: Hashable {
private enum ShareSearchRecentEntry: Comparable, Identifiable {
case topPeers(PresentationTheme, PresentationStrings)
case peer(index: Int, theme: PresentationTheme, peer: EnginePeer, associatedPeer: EnginePeer?, presence: EnginePeer.Presence?, requiresPremiumForMessaging: Bool, strings: PresentationStrings)
case peer(index: Int, theme: PresentationTheme, peer: EnginePeer, associatedPeer: EnginePeer?, presence: EnginePeer.Presence?, requiresPremiumForMessaging: Bool, requiresStars: Int64?, strings: PresentationStrings)
var stableId: ShareSearchRecentEntryStableId {
switch self {
case .topPeers:
return .topPeers
case let .peer(_, _, peer, _, _, _, _):
case let .peer(_, _, peer, _, _, _, _, _):
return .peerId(peer.id)
}
}
@ -61,8 +61,8 @@ private enum ShareSearchRecentEntry: Comparable, Identifiable {
} else {
return false
}
case let .peer(lhsIndex, lhsTheme, lhsPeer, lhsAssociatedPeer, lhsPresence, lhsRequiresPremiumForMessaging, lhsStrings):
if case let .peer(rhsIndex, rhsTheme, rhsPeer, rhsAssociatedPeer, rhsPresence, rhsRequiresPremiumForMessaging, rhsStrings) = rhs, lhsPeer == rhsPeer && lhsAssociatedPeer == rhsAssociatedPeer && lhsIndex == rhsIndex && lhsStrings === rhsStrings && lhsTheme === rhsTheme && lhsPresence == rhsPresence && lhsRequiresPremiumForMessaging == rhsRequiresPremiumForMessaging {
case let .peer(lhsIndex, lhsTheme, lhsPeer, lhsAssociatedPeer, lhsPresence, lhsRequiresPremiumForMessaging, lhsRequiresStars, lhsStrings):
if case let .peer(rhsIndex, rhsTheme, rhsPeer, rhsAssociatedPeer, rhsPresence, rhsRequiresPremiumForMessaging, rhsRequiresStars, rhsStrings) = rhs, lhsPeer == rhsPeer && lhsAssociatedPeer == rhsAssociatedPeer && lhsIndex == rhsIndex && lhsStrings === rhsStrings && lhsTheme === rhsTheme && lhsPresence == rhsPresence && lhsRequiresPremiumForMessaging == rhsRequiresPremiumForMessaging && lhsRequiresStars == rhsRequiresStars {
return true
} else {
return false
@ -74,11 +74,11 @@ private enum ShareSearchRecentEntry: Comparable, Identifiable {
switch lhs {
case .topPeers:
return true
case let .peer(lhsIndex, _, _, _, _, _, _):
case let .peer(lhsIndex, _, _, _, _, _, _, _):
switch rhs {
case .topPeers:
return false
case let .peer(rhsIndex, _, _, _, _, _, _):
case let .peer(rhsIndex, _, _, _, _, _, _, _):
return lhsIndex <= rhsIndex
}
}
@ -88,13 +88,13 @@ private enum ShareSearchRecentEntry: Comparable, Identifiable {
switch self {
case let .topPeers(theme, strings):
return ShareControllerRecentPeersGridItem(environment: environment, context: context, theme: theme, strings: strings, controllerInteraction: interfaceInteraction)
case let .peer(_, theme, peer, associatedPeer, presence, requiresPremiumForMessaging, strings):
case let .peer(_, theme, peer, associatedPeer, presence, requiresPremiumForMessaging, requiresStars, strings):
var peers: [EnginePeer.Id: EnginePeer] = [peer.id: peer]
if let associatedPeer = associatedPeer {
peers[associatedPeer.id] = associatedPeer
}
let peer = EngineRenderedPeer(peerId: peer.id, peers: peers, associatedMedia: [:])
return ShareControllerPeerGridItem(environment: environment, context: context, theme: theme, strings: strings, item: .peer(peer: peer, presence: presence, topicId: nil, threadData: nil, requiresPremiumForMessaging: requiresPremiumForMessaging), controllerInteraction: interfaceInteraction, sectionTitle: strings.DialogList_SearchSectionRecent, search: true)
return ShareControllerPeerGridItem(environment: environment, context: context, theme: theme, strings: strings, item: .peer(peer: peer, presence: presence, topicId: nil, threadData: nil, requiresPremiumForMessaging: requiresPremiumForMessaging, requiresStars: requiresStars), controllerInteraction: interfaceInteraction, sectionTitle: strings.DialogList_SearchSectionRecent, search: true)
}
}
}
@ -104,6 +104,7 @@ private struct ShareSearchPeerEntry: Comparable, Identifiable {
let peer: EngineRenderedPeer?
let presence: EnginePeer.Presence?
let requiresPremiumForMessaging: Bool
let requiresStars: Int64?
let theme: PresentationTheme
let strings: PresentationStrings
let isGlobal: Bool
@ -129,6 +130,9 @@ private struct ShareSearchPeerEntry: Comparable, Identifiable {
if lhs.requiresPremiumForMessaging != rhs.requiresPremiumForMessaging {
return false
}
if lhs.requiresStars != rhs.requiresStars {
return false
}
if lhs.theme !== rhs.theme {
return false
}
@ -149,7 +153,7 @@ private struct ShareSearchPeerEntry: Comparable, Identifiable {
} else {
sectionTitle = nil
}
return ShareControllerPeerGridItem(environment: environment, context: context, theme: self.theme, strings: self.strings, item: self.peer.flatMap({ .peer(peer: $0, presence: self.presence, topicId: nil, threadData: nil, requiresPremiumForMessaging: self.requiresPremiumForMessaging) }), controllerInteraction: interfaceInteraction, sectionTitle: sectionTitle, search: true)
return ShareControllerPeerGridItem(environment: environment, context: context, theme: self.theme, strings: self.strings, item: self.peer.flatMap({ .peer(peer: $0, presence: self.presence, topicId: nil, threadData: nil, requiresPremiumForMessaging: self.requiresPremiumForMessaging, requiresStars: self.requiresStars) }), controllerInteraction: interfaceInteraction, sectionTitle: sectionTitle, search: true)
}
}
@ -361,7 +365,7 @@ final class ShareSearchContainerNode: ASDisplayNode, ShareContentContainerNode {
if strings.DialogList_SavedMessages.lowercased().hasPrefix(lowercasedQuery) || "saved messages".hasPrefix(lowercasedQuery) {
if !existingPeerIds.contains(accountPeer.id) {
existingPeerIds.insert(accountPeer.id)
entries.append(ShareSearchPeerEntry(index: index, peer: EngineRenderedPeer(peer: EnginePeer(accountPeer)), presence: nil, requiresPremiumForMessaging: false, theme: theme, strings: strings, isGlobal: false))
entries.append(ShareSearchPeerEntry(index: index, peer: EngineRenderedPeer(peer: EnginePeer(accountPeer)), presence: nil, requiresPremiumForMessaging: false, requiresStars: nil, theme: theme, strings: strings, isGlobal: false))
index += 1
}
}
@ -370,7 +374,7 @@ final class ShareSearchContainerNode: ASDisplayNode, ShareContentContainerNode {
if let peer = renderedPeer.peers[renderedPeer.peerId], peer.id != accountPeer.id {
if !existingPeerIds.contains(renderedPeer.peerId) && canSendMessagesToPeer(peer) {
existingPeerIds.insert(renderedPeer.peerId)
entries.append(ShareSearchPeerEntry(index: index, peer: EngineRenderedPeer(renderedPeer), presence: nil, requiresPremiumForMessaging: peerRequiresPremiumForMessaging[peer.id] ?? false, theme: theme, strings: strings, isGlobal: false))
entries.append(ShareSearchPeerEntry(index: index, peer: EngineRenderedPeer(renderedPeer), presence: nil, requiresPremiumForMessaging: peerRequiresPremiumForMessaging[peer.id] ?? false, requiresStars: nil, theme: theme, strings: strings, isGlobal: false))
index += 1
}
}
@ -380,7 +384,7 @@ final class ShareSearchContainerNode: ASDisplayNode, ShareContentContainerNode {
if foundRemotePeers.2 {
isPlaceholder = true
for _ in 0 ..< 4 {
entries.append(ShareSearchPeerEntry(index: index, peer: nil, presence: nil, requiresPremiumForMessaging: false, theme: theme, strings: strings, isGlobal: false))
entries.append(ShareSearchPeerEntry(index: index, peer: nil, presence: nil, requiresPremiumForMessaging: false, requiresStars: nil, theme: theme, strings: strings, isGlobal: false))
index += 1
}
} else {
@ -388,7 +392,7 @@ final class ShareSearchContainerNode: ASDisplayNode, ShareContentContainerNode {
let peer = foundPeer.peer
if !existingPeerIds.contains(peer.id) && canSendMessagesToPeer(peer) {
existingPeerIds.insert(peer.id)
entries.append(ShareSearchPeerEntry(index: index, peer: EngineRenderedPeer(peer: EnginePeer(foundPeer.peer)), presence: nil, requiresPremiumForMessaging: peerRequiresPremiumForMessaging[peer.id] ?? false, theme: theme, strings: strings, isGlobal: false))
entries.append(ShareSearchPeerEntry(index: index, peer: EngineRenderedPeer(peer: EnginePeer(foundPeer.peer)), presence: nil, requiresPremiumForMessaging: peerRequiresPremiumForMessaging[peer.id] ?? false, requiresStars: nil, theme: theme, strings: strings, isGlobal: false))
index += 1
}
}
@ -397,7 +401,7 @@ final class ShareSearchContainerNode: ASDisplayNode, ShareContentContainerNode {
let peer = foundPeer.peer
if !existingPeerIds.contains(peer.id) && canSendMessagesToPeer(peer) {
existingPeerIds.insert(peer.id)
entries.append(ShareSearchPeerEntry(index: index, peer: EngineRenderedPeer(peer: EnginePeer(peer)), presence: nil, requiresPremiumForMessaging: peerRequiresPremiumForMessaging[peer.id] ?? false, theme: theme, strings: strings, isGlobal: true))
entries.append(ShareSearchPeerEntry(index: index, peer: EngineRenderedPeer(peer: EnginePeer(peer)), presence: nil, requiresPremiumForMessaging: peerRequiresPremiumForMessaging[peer.id] ?? false, requiresStars: nil, theme: theme, strings: strings, isGlobal: true))
index += 1
}
}
@ -462,7 +466,7 @@ final class ShareSearchContainerNode: ASDisplayNode, ShareContentContainerNode {
var index = 0
for (peer, requiresPremiumForMessaging) in recentPeerList {
if let mainPeer = peer.peers[peer.peerId], canSendMessagesToPeer(mainPeer._asPeer()) {
recentItemList.append(.peer(index: index, theme: theme, peer: mainPeer, associatedPeer: mainPeer._asPeer().associatedPeerId.flatMap { peer.peers[$0] }, presence: nil, requiresPremiumForMessaging: requiresPremiumForMessaging, strings: strings))
recentItemList.append(.peer(index: index, theme: theme, peer: mainPeer, associatedPeer: mainPeer._asPeer().associatedPeerId.flatMap { peer.peers[$0] }, presence: nil, requiresPremiumForMessaging: requiresPremiumForMessaging, requiresStars: nil, strings: strings))
index += 1
}
}
@ -570,7 +574,7 @@ final class ShareSearchContainerNode: ASDisplayNode, ShareContentContainerNode {
switch $0 {
case .topPeers:
return false
case let .peer(_, _, peer, _, _, _, _):
case let .peer(_, _, peer, _, _, _, _, _):
return peer.id == ensurePeerVisibleOnLayout
}
}) {

@ -409,7 +409,7 @@ public func preparedShareItems(postbox: Postbox, network: Network, to peerId: Pe
})
}
public func sentShareItems(accountPeerId: PeerId, postbox: Postbox, network: Network, stateManager: AccountStateManager, auxiliaryMethods: AccountAuxiliaryMethods, to peerIds: [PeerId], threadIds: [PeerId: Int64], items: [PreparedShareItemContent], silently: Bool, additionalText: String) -> Signal<Float, Void> {
public func sentShareItems(accountPeerId: PeerId, postbox: Postbox, network: Network, stateManager: AccountStateManager, auxiliaryMethods: AccountAuxiliaryMethods, to peerIds: [PeerId], threadIds: [PeerId: Int64], requireStars: [PeerId: StarsAmount], items: [PreparedShareItemContent], silently: Bool, additionalText: String) -> Signal<Float, Void> {
var messages: [StandaloneSendEnqueueMessage] = []
var groupingKey: Int64?
var mediaTypes: (photo: Int, video: Int, music: Int, other: Int) = (0, 0, 0, 0)
@ -498,6 +498,16 @@ public func sentShareItems(accountPeerId: PeerId, postbox: Postbox, network: Net
var peerSignals: Signal<Float, StandaloneSendMessagesError> = .single(0.0)
for peerId in peerIds {
var peerMessages = messages
if let amount = requireStars[peerId] {
var updatedMessages: [StandaloneSendEnqueueMessage] = []
for message in peerMessages {
var message = message
message.sendPaidMessageStars = amount
updatedMessages.append(message)
}
peerMessages = updatedMessages
}
peerSignals = peerSignals |> then(standaloneSendEnqueueMessages(
accountPeerId: accountPeerId,
postbox: postbox,
@ -506,7 +516,7 @@ public func sentShareItems(accountPeerId: PeerId, postbox: Postbox, network: Net
auxiliaryMethods: auxiliaryMethods,
peerId: peerId,
threadId: threadIds[peerId],
messages: messages
messages: peerMessages
)
|> mapToSignal { status -> Signal<Float, StandaloneSendMessagesError> in
switch status {

@ -1182,7 +1182,7 @@ private enum StatsEntry: ItemListNodeEntry {
detailText = stringForMediumCompactDate(timestamp: date, strings: presentationData.strings, dateTimeFormat: presentationData.dateTimeFormat)
}
let label = tonAmountAttributedString(formatTonAmountText(transaction.amount, dateTimeFormat: presentationData.dateTimeFormat, showPlus: true), integralFont: font, fractionalFont: smallLabelFont, color: labelColor).mutableCopy() as! NSMutableAttributedString
let label = tonAmountAttributedString(formatTonAmountText(transaction.amount, dateTimeFormat: presentationData.dateTimeFormat, showPlus: true), integralFont: font, fractionalFont: smallLabelFont, color: labelColor, decimalSeparator: presentationData.dateTimeFormat.decimalSeparator).mutableCopy() as! NSMutableAttributedString
label.insert(NSAttributedString(string: " $ ", font: font, textColor: labelColor), at: 1)
if let range = label.string.range(of: "$"), let icon = generateTintedImage(image: UIImage(bundleImageName: "Ads/TonMedium"), color: labelColor) {

@ -177,7 +177,7 @@ final class MonetizationBalanceItemNode: ListViewItemNode, ItemListItemNode {
var isStars = false
if let stats = item.stats as? RevenueStats {
let cryptoValue = formatTonAmountText(stats.balances.availableBalance, dateTimeFormat: item.presentationData.dateTimeFormat)
amountString = tonAmountAttributedString(cryptoValue, integralFont: integralFont, fractionalFont: fractionalFont, color: item.presentationData.theme.list.itemPrimaryTextColor)
amountString = tonAmountAttributedString(cryptoValue, integralFont: integralFont, fractionalFont: fractionalFont, color: item.presentationData.theme.list.itemPrimaryTextColor, decimalSeparator: item.presentationData.dateTimeFormat.decimalSeparator)
value = stats.balances.availableBalance == 0 ? "" : "\(formatTonUsdValue(stats.balances.availableBalance, rate: stats.usdRate, dateTimeFormat: item.presentationData.dateTimeFormat))"
} else if let stats = item.stats as? StarsRevenueStats {
amountString = NSAttributedString(string: presentationStringsFormattedNumber(stats.balances.availableBalance, item.presentationData.dateTimeFormat.groupingSeparator), font: integralFont, textColor: item.presentationData.theme.list.itemPrimaryTextColor)

@ -201,7 +201,7 @@ private final class ValueItemNode: ASDisplayNode {
let valueString: NSAttributedString
if case .ton = mode {
valueString = tonAmountAttributedString(value, integralFont: valueFont, fractionalFont: smallValueFont, color: valueColor)
valueString = tonAmountAttributedString(value, integralFont: valueFont, fractionalFont: smallValueFont, color: valueColor, decimalSeparator: presentationData.dateTimeFormat.decimalSeparator)
} else {
valueString = NSAttributedString(string: value, font: valueFont, textColor: valueColor)
}

@ -139,7 +139,7 @@ private final class SheetContent: CombinedComponent {
switch component.transaction {
case let .proceeds(amount, fromDate, toDate):
labelColor = theme.list.itemDisclosureActions.constructive.fillColor
amountString = tonAmountAttributedString(formatTonAmountText(amount, dateTimeFormat: dateTimeFormat, showPlus: true), integralFont: integralFont, fractionalFont: fractionalFont, color: labelColor).mutableCopy() as! NSMutableAttributedString
amountString = tonAmountAttributedString(formatTonAmountText(amount, dateTimeFormat: dateTimeFormat, showPlus: true), integralFont: integralFont, fractionalFont: fractionalFont, color: labelColor, decimalSeparator: dateTimeFormat.decimalSeparator).mutableCopy() as! NSMutableAttributedString
dateString = "\(stringForMediumCompactDate(timestamp: fromDate, strings: strings, dateTimeFormat: dateTimeFormat)) \(stringForMediumCompactDate(timestamp: toDate, strings: strings, dateTimeFormat: dateTimeFormat))"
titleString = strings.Monetization_TransactionInfo_Proceeds
buttonTitle = strings.Common_OK
@ -147,7 +147,7 @@ private final class SheetContent: CombinedComponent {
showPeer = true
case let .withdrawal(status, amount, date, provider, _, transactionUrl):
labelColor = theme.list.itemDestructiveColor
amountString = tonAmountAttributedString(formatTonAmountText(amount, dateTimeFormat: dateTimeFormat), integralFont: integralFont, fractionalFont: fractionalFont, color: labelColor).mutableCopy() as! NSMutableAttributedString
amountString = tonAmountAttributedString(formatTonAmountText(amount, dateTimeFormat: dateTimeFormat), integralFont: integralFont, fractionalFont: fractionalFont, color: labelColor, decimalSeparator: dateTimeFormat.decimalSeparator).mutableCopy() as! NSMutableAttributedString
dateString = stringForFullDate(timestamp: date, strings: strings, dateTimeFormat: dateTimeFormat)
switch status {
@ -166,7 +166,7 @@ private final class SheetContent: CombinedComponent {
case let .refund(amount, date, _):
labelColor = theme.list.itemDisclosureActions.constructive.fillColor
titleString = strings.Monetization_TransactionInfo_Refund
amountString = tonAmountAttributedString(formatTonAmountText(amount, dateTimeFormat: dateTimeFormat, showPlus: true), integralFont: integralFont, fractionalFont: fractionalFont, color: labelColor).mutableCopy() as! NSMutableAttributedString
amountString = tonAmountAttributedString(formatTonAmountText(amount, dateTimeFormat: dateTimeFormat, showPlus: true), integralFont: integralFont, fractionalFont: fractionalFont, color: labelColor, decimalSeparator: dateTimeFormat.decimalSeparator).mutableCopy() as! NSMutableAttributedString
dateString = stringForFullDate(timestamp: date, strings: strings, dateTimeFormat: dateTimeFormat)
buttonTitle = strings.Common_OK
explorerUrl = nil

@ -1390,7 +1390,7 @@ fileprivate let parsers: [Int32 : (BufferReader) -> Any?] = {
dict[1314881805] = { return Api.payments.PaymentResult.parse_paymentResult($0) }
dict[-666824391] = { return Api.payments.PaymentResult.parse_paymentVerificationNeeded($0) }
dict[-74456004] = { return Api.payments.SavedInfo.parse_savedInfo($0) }
dict[-1779201615] = { return Api.payments.SavedStarGifts.parse_savedStarGifts($0) }
dict[-418915641] = { return Api.payments.SavedStarGifts.parse_savedStarGifts($0) }
dict[377215243] = { return Api.payments.StarGiftUpgradePreview.parse_starGiftUpgradePreview($0) }
dict[-2069218660] = { return Api.payments.StarGiftWithdrawalUrl.parse_starGiftWithdrawalUrl($0) }
dict[-1877571094] = { return Api.payments.StarGifts.parse_starGifts($0) }

@ -1,16 +1,21 @@
public extension Api.payments {
enum SavedStarGifts: TypeConstructorDescription {
case savedStarGifts(flags: Int32, count: Int32, chatNotificationsEnabled: Api.Bool?, gifts: [Api.SavedStarGift], nextOffset: String?, chats: [Api.Chat], users: [Api.User])
case savedStarGifts(flags: Int32, count: Int32, chatNotificationsEnabled: Api.Bool?, pinnedToTop: [Int64]?, gifts: [Api.SavedStarGift], nextOffset: String?, chats: [Api.Chat], users: [Api.User])
public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) {
switch self {
case .savedStarGifts(let flags, let count, let chatNotificationsEnabled, let gifts, let nextOffset, let chats, let users):
case .savedStarGifts(let flags, let count, let chatNotificationsEnabled, let pinnedToTop, let gifts, let nextOffset, let chats, let users):
if boxed {
buffer.appendInt32(-1779201615)
buffer.appendInt32(-418915641)
}
serializeInt32(flags, buffer: buffer, boxed: false)
serializeInt32(count, buffer: buffer, boxed: false)
if Int(flags) & Int(1 << 1) != 0 {chatNotificationsEnabled!.serialize(buffer, true)}
if Int(flags) & Int(1 << 2) != 0 {buffer.appendInt32(481674261)
buffer.appendInt32(Int32(pinnedToTop!.count))
for item in pinnedToTop! {
serializeInt64(item, buffer: buffer, boxed: false)
}}
buffer.appendInt32(481674261)
buffer.appendInt32(Int32(gifts.count))
for item in gifts {
@ -33,8 +38,8 @@ public extension Api.payments {
public func descriptionFields() -> (String, [(String, Any)]) {
switch self {
case .savedStarGifts(let flags, let count, let chatNotificationsEnabled, let gifts, let nextOffset, let chats, let users):
return ("savedStarGifts", [("flags", flags as Any), ("count", count as Any), ("chatNotificationsEnabled", chatNotificationsEnabled as Any), ("gifts", gifts as Any), ("nextOffset", nextOffset as Any), ("chats", chats as Any), ("users", users as Any)])
case .savedStarGifts(let flags, let count, let chatNotificationsEnabled, let pinnedToTop, let gifts, let nextOffset, let chats, let users):
return ("savedStarGifts", [("flags", flags as Any), ("count", count as Any), ("chatNotificationsEnabled", chatNotificationsEnabled as Any), ("pinnedToTop", pinnedToTop as Any), ("gifts", gifts as Any), ("nextOffset", nextOffset as Any), ("chats", chats as Any), ("users", users as Any)])
}
}
@ -47,29 +52,34 @@ public extension Api.payments {
if Int(_1!) & Int(1 << 1) != 0 {if let signature = reader.readInt32() {
_3 = Api.parse(reader, signature: signature) as? Api.Bool
} }
var _4: [Api.SavedStarGift]?
var _4: [Int64]?
if Int(_1!) & Int(1 << 2) != 0 {if let _ = reader.readInt32() {
_4 = Api.parseVector(reader, elementSignature: 570911930, elementType: Int64.self)
} }
var _5: [Api.SavedStarGift]?
if let _ = reader.readInt32() {
_4 = Api.parseVector(reader, elementSignature: 0, elementType: Api.SavedStarGift.self)
_5 = Api.parseVector(reader, elementSignature: 0, elementType: Api.SavedStarGift.self)
}
var _5: String?
if Int(_1!) & Int(1 << 0) != 0 {_5 = parseString(reader) }
var _6: [Api.Chat]?
var _6: String?
if Int(_1!) & Int(1 << 0) != 0 {_6 = parseString(reader) }
var _7: [Api.Chat]?
if let _ = reader.readInt32() {
_6 = Api.parseVector(reader, elementSignature: 0, elementType: Api.Chat.self)
_7 = Api.parseVector(reader, elementSignature: 0, elementType: Api.Chat.self)
}
var _7: [Api.User]?
var _8: [Api.User]?
if let _ = reader.readInt32() {
_7 = Api.parseVector(reader, elementSignature: 0, elementType: Api.User.self)
_8 = Api.parseVector(reader, elementSignature: 0, elementType: Api.User.self)
}
let _c1 = _1 != nil
let _c2 = _2 != nil
let _c3 = (Int(_1!) & Int(1 << 1) == 0) || _3 != nil
let _c4 = _4 != nil
let _c5 = (Int(_1!) & Int(1 << 0) == 0) || _5 != nil
let _c6 = _6 != nil
let _c4 = (Int(_1!) & Int(1 << 2) == 0) || _4 != nil
let _c5 = _5 != nil
let _c6 = (Int(_1!) & Int(1 << 0) == 0) || _6 != nil
let _c7 = _7 != nil
if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 && _c7 {
return Api.payments.SavedStarGifts.savedStarGifts(flags: _1!, count: _2!, chatNotificationsEnabled: _3, gifts: _4!, nextOffset: _5, chats: _6!, users: _7!)
let _c8 = _8 != nil
if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 && _c7 && _c8 {
return Api.payments.SavedStarGifts.savedStarGifts(flags: _1!, count: _2!, chatNotificationsEnabled: _3, pinnedToTop: _4, gifts: _5!, nextOffset: _6, chats: _7!, users: _8!)
}
else {
return nil

@ -9717,6 +9717,26 @@ public extension Api.functions.payments {
})
}
}
public extension Api.functions.payments {
static func toggleStarGiftsPinnedToTop(peer: Api.InputPeer, stargift: [Api.InputSavedStarGift]) -> (FunctionDescription, Buffer, DeserializeFunctionResponse<Api.Bool>) {
let buffer = Buffer()
buffer.appendInt32(353626032)
peer.serialize(buffer, true)
buffer.appendInt32(481674261)
buffer.appendInt32(Int32(stargift.count))
for item in stargift {
item.serialize(buffer, true)
}
return (FunctionDescription(name: "payments.toggleStarGiftsPinnedToTop", parameters: [("peer", String(describing: peer)), ("stargift", String(describing: stargift))]), buffer, DeserializeFunctionResponse { (buffer: Buffer) -> Api.Bool? in
let reader = BufferReader(buffer)
var result: Api.Bool?
if let signature = reader.readInt32() {
result = Api.parse(reader, signature: signature) as? Api.Bool
}
return result
})
}
}
public extension Api.functions.payments {
static func transferStarGift(stargift: Api.InputSavedStarGift, toId: Api.InputPeer) -> (FunctionDescription, Buffer, DeserializeFunctionResponse<Api.Updates>) {
let buffer = Buffer()

@ -0,0 +1,27 @@
// swift-tools-version:5.5
// The swift-tools-version declares the minimum version of Swift required to build this package.
import PackageDescription
let package = Package(
name: "FlatBuffers",
platforms: [.macOS(.v10_13)],
products: [
// Products define the executables and libraries a package produces, and make them visible to other packages.
.library(
name: "FlatBuffers",
targets: ["FlatBuffers"]),
],
dependencies: [
// Dependencies declare other packages that this package depends on.
// .package(url: /* package url */, from: "1.0.0"),
],
targets: [
// Targets are the basic building blocks of a package. A target can define a module or a test suite.
// Targets can depend on other targets in this package, and on products in packages this package depends on.
.target(
name: "FlatBuffers",
dependencies: [],
path: "Sources"),
]
)

@ -0,0 +1,30 @@
// swift-tools-version:5.5
// The swift-tools-version declares the minimum version of Swift required to build this package.
import PackageDescription
let package = Package(
name: "FlatSerialization",
platforms: [.macOS(.v10_13)],
products: [
// Products define the executables and libraries a package produces, and make them visible to other packages.
.library(
name: "FlatSerialization",
targets: ["FlatSerialization"]),
],
dependencies: [
// Dependencies declare other packages that this package depends on.
// .package(url: /* package url */, from: "1.0.0"),
.package(name: "FlatBuffers", path: "../FlatBuffers")
],
targets: [
// Targets are the basic building blocks of a package. A target can define a module or a test suite.
// Targets can depend on other targets in this package, and on products in packages this package depends on.
.target(
name: "FlatSerialization",
dependencies: [
.product(name: "FlatBuffers", package: "FlatBuffers", condition: nil),
],
path: "Sources"),
]
)

@ -3,10 +3,15 @@
# Default directories
OUTPUT_DIR=""
INPUT_DIR=""
BINARY_PATH=""
# Parse command line arguments
while [ "$#" -gt 0 ]; do
case "$1" in
--binary)
BINARY_PATH="$2"
shift 2
;;
--output)
OUTPUT_DIR="$2"
shift 2
@ -28,6 +33,12 @@ if [ -z "$OUTPUT_DIR" ]; then
exit 1
fi
# Validate output directory
if [ -z "$BINARY_PATH" ]; then
echo "Error: --binary argument is required"
exit 1
fi
if [ ! -d "$OUTPUT_DIR" ]; then
echo "Error: Output directory does not exist: $OUTPUT_DIR"
exit 1
@ -58,4 +69,4 @@ for model in $models; do
flatc_input="$flatc_input $model"
done
flatc --require-explicit-ids --swift -o "$OUTPUT_DIR" ${flatc_input}
$BINARY_PATH --require-explicit-ids --swift -o "$OUTPUT_DIR" ${flatc_input}

@ -15,6 +15,8 @@ let package = Package(
dependencies: [
// Dependencies declare other packages that this package depends on.
// .package(url: /* package url */, from: "1.0.0"),
.package(name: "FlatBuffers", path: "./FlatBuffers"),
.package(name: "FlatSerialization", path: "./FlatSerialization"),
.package(name: "Postbox", path: "../Postbox"),
.package(name: "SSignalKit", path: "../SSignalKit"),
.package(name: "MtProtoKit", path: "../MtProtoKit"),
@ -40,6 +42,8 @@ let package = Package(
.product(name: "DarwinDirStat", package: "DarwinDirStat", condition: nil),
.product(name: "Reachability", package: "Reachability", condition: nil),
.product(name: "Emoji", package: "Emoji", condition: nil),
.product(name: "FlatBuffers", package: "FlatBuffers", condition: nil),
.product(name: "FlatSerialization", package: "FlatSerialization", condition: nil),
.product(name: "EncryptionProvider", package: "EncryptionProvider", condition: nil)],
path: "Sources"),
]

@ -190,6 +190,7 @@ private var declaredEncodables: Void = {
declareEncodable(OutgoingScheduleInfoMessageAttribute.self, f: { OutgoingScheduleInfoMessageAttribute(decoder: $0) })
declareEncodable(UpdateMessageReactionsAction.self, f: { UpdateMessageReactionsAction(decoder: $0) })
declareEncodable(SendStarsReactionsAction.self, f: { SendStarsReactionsAction(decoder: $0) })
declareEncodable(PostponeSendPaidMessageAction.self, f: { PostponeSendPaidMessageAction(decoder: $0) })
declareEncodable(RestrictedContentMessageAttribute.self, f: { RestrictedContentMessageAttribute(decoder: $0) })
declareEncodable(SendScheduledMessageImmediatelyAction.self, f: { SendScheduledMessageImmediatelyAction(decoder: $0) })
declareEncodable(EmbeddedMediaStickersMessageAttribute.self, f: { EmbeddedMediaStickersMessageAttribute(decoder: $0) })

@ -904,7 +904,7 @@ extension StoreMessage {
}
if let paidMessageStars {
attributes.append(PaidStarsMessageAttribute(stars: StarsAmount(value: paidMessageStars, nanos: 0)))
attributes.append(PaidStarsMessageAttribute(stars: StarsAmount(value: paidMessageStars, nanos: 0), postponeSending: false))
}
var entitiesAttribute: TextEntitiesMessageAttribute?

@ -129,6 +129,7 @@ public func standaloneSendEnqueueMessages(
struct MessageResult {
var result: PendingMessageUploadedContentResult
var media: [Media]
var attributes: [MessageAttribute]
}
let signals: [Signal<MessageResult, PendingMessageUploadError>] = messages.map { message in
@ -178,7 +179,10 @@ public func standaloneSendEnqueueMessages(
if message.isSilent {
attributes.append(NotificationInfoMessageAttribute(flags: .muted))
}
if let sendPaidMessageStars = message.sendPaidMessageStars {
attributes.append(PaidStarsMessageAttribute(stars: sendPaidMessageStars, postponeSending: false))
}
let content = messageContentToUpload(accountPeerId: accountPeerId, network: network, postbox: postbox, auxiliaryMethods: auxiliaryMethods, transformOutgoingMessageMedia: { _, _, _, _ in
return .single(nil)
}, messageMediaPreuploadManager: MessageMediaPreuploadManager(), revalidationContext: MediaReferenceRevalidationContext(), forceReupload: false, isGrouped: false, passFetchProgress: true, forceNoBigParts: false, peerId: peerId, messageId: nil, attributes: attributes, text: text, media: media)
@ -191,7 +195,7 @@ public func standaloneSendEnqueueMessages(
}
return contentResult
|> map { contentResult in
return MessageResult(result: contentResult, media: media)
return MessageResult(result: contentResult, media: media, attributes: attributes)
}
}
@ -201,7 +205,7 @@ public func standaloneSendEnqueueMessages(
}
|> mapToSignal { contentResults -> Signal<StandaloneSendMessageStatus, StandaloneSendMessagesError> in
var progressSum: Float = 0.0
var allResults: [(result: PendingMessageUploadedContentAndReuploadInfo, media: [Media])] = []
var allResults: [(result: PendingMessageUploadedContentAndReuploadInfo, media: [Media], attributes: [MessageAttribute])] = []
var allDone = true
for result in contentResults {
switch result.result {
@ -209,13 +213,13 @@ public func standaloneSendEnqueueMessages(
allDone = false
progressSum += value.progress
case let .content(content):
allResults.append((content, result.media))
allResults.append((content, result.media, result.attributes))
}
}
if allDone {
var sendSignals: [Signal<Never, StandaloneSendMessagesError>] = []
for (content, media) in allResults {
for (content, media, attributes) in allResults {
var text: String = ""
switch content.content {
case let .text(textValue):
@ -235,7 +239,7 @@ public func standaloneSendEnqueueMessages(
peerId: peerId,
content: content,
text: text,
attributes: [],
attributes: attributes,
media: media,
threadId: threadId
))
@ -328,6 +332,7 @@ private func sendUploadedMessageContent(
var videoTimestamp: Int32?
var sendAsPeerId: PeerId?
var bubbleUpEmojiOrStickersets = false
var allowPaidStars: Int64?
var flags: Int32 = 0
@ -365,6 +370,8 @@ private func sendUploadedMessageContent(
} else if let attribute = attribute as? ForwardVideoTimestampAttribute {
flags |= Int32(1 << 20)
videoTimestamp = attribute.timestamp
} else if let attribute = attribute as? PaidStarsMessageAttribute {
allowPaidStars = attribute.stars.value
}
}
@ -390,6 +397,11 @@ private func sendUploadedMessageContent(
flags |= (1 << 13)
}
if let _ = allowPaidStars {
flags |= 1 << 21
}
let dependencyTag: PendingMessageRequestDependencyTag? = nil//(messageId: messageId)
let sendMessageRequest: Signal<NetworkRequestResult<Api.Updates>, MTRpcError>
@ -415,7 +427,7 @@ private func sendUploadedMessageContent(
}
}
sendMessageRequest = network.requestWithAdditionalInfo(Api.functions.messages.sendMessage(flags: flags, peer: inputPeer, replyTo: replyTo, message: text, randomId: uniqueId, replyMarkup: nil, entities: messageEntities, scheduleDate: scheduleTime, sendAs: sendAsInputPeer, quickReplyShortcut: nil, effect: nil, allowPaidStars: nil), info: .acknowledgement, tag: dependencyTag)
sendMessageRequest = network.requestWithAdditionalInfo(Api.functions.messages.sendMessage(flags: flags, peer: inputPeer, replyTo: replyTo, message: text, randomId: uniqueId, replyMarkup: nil, entities: messageEntities, scheduleDate: scheduleTime, sendAs: sendAsInputPeer, quickReplyShortcut: nil, effect: nil, allowPaidStars: allowPaidStars), info: .acknowledgement, tag: dependencyTag)
case let .media(inputMedia, text):
if bubbleUpEmojiOrStickersets {
flags |= Int32(1 << 15)
@ -437,7 +449,7 @@ private func sendUploadedMessageContent(
}
}
sendMessageRequest = network.request(Api.functions.messages.sendMedia(flags: flags, peer: inputPeer, replyTo: replyTo, media: inputMedia, message: text, randomId: uniqueId, replyMarkup: nil, entities: messageEntities, scheduleDate: scheduleTime, sendAs: sendAsInputPeer, quickReplyShortcut: nil, effect: nil, allowPaidStars: nil), tag: dependencyTag)
sendMessageRequest = network.request(Api.functions.messages.sendMedia(flags: flags, peer: inputPeer, replyTo: replyTo, media: inputMedia, message: text, randomId: uniqueId, replyMarkup: nil, entities: messageEntities, scheduleDate: scheduleTime, sendAs: sendAsInputPeer, quickReplyShortcut: nil, effect: nil, allowPaidStars: allowPaidStars), tag: dependencyTag)
|> map(NetworkRequestResult.result)
case let .forward(sourceInfo):
var topMsgId: Int32?
@ -447,7 +459,7 @@ private func sendUploadedMessageContent(
}
if let forwardSourceInfoAttribute = forwardSourceInfoAttribute, let sourcePeer = transaction.getPeer(forwardSourceInfoAttribute.messageId.peerId), let sourceInputPeer = apiInputPeer(sourcePeer) {
sendMessageRequest = network.request(Api.functions.messages.forwardMessages(flags: flags, fromPeer: sourceInputPeer, id: [sourceInfo.messageId.id], randomId: [uniqueId], toPeer: inputPeer, topMsgId: topMsgId, scheduleDate: scheduleTime, sendAs: sendAsInputPeer, quickReplyShortcut: nil, videoTimestamp: videoTimestamp, allowPaidStars: nil), tag: dependencyTag)
sendMessageRequest = network.request(Api.functions.messages.forwardMessages(flags: flags, fromPeer: sourceInputPeer, id: [sourceInfo.messageId.id], randomId: [uniqueId], toPeer: inputPeer, topMsgId: topMsgId, scheduleDate: scheduleTime, sendAs: sendAsInputPeer, quickReplyShortcut: nil, videoTimestamp: videoTimestamp, allowPaidStars: allowPaidStars), tag: dependencyTag)
|> map(NetworkRequestResult.result)
} else {
sendMessageRequest = .fail(MTRpcError(errorCode: 400, errorDescription: "internal"))
@ -473,7 +485,7 @@ private func sendUploadedMessageContent(
}
}
sendMessageRequest = network.request(Api.functions.messages.sendInlineBotResult(flags: flags, peer: inputPeer, replyTo: replyTo, randomId: uniqueId, queryId: chatContextResult.queryId, id: chatContextResult.id, scheduleDate: scheduleTime, sendAs: sendAsInputPeer, quickReplyShortcut: nil, allowPaidStars: nil))
sendMessageRequest = network.request(Api.functions.messages.sendInlineBotResult(flags: flags, peer: inputPeer, replyTo: replyTo, randomId: uniqueId, queryId: chatContextResult.queryId, id: chatContextResult.id, scheduleDate: scheduleTime, sendAs: sendAsInputPeer, quickReplyShortcut: nil, allowPaidStars: allowPaidStars))
|> map(NetworkRequestResult.result)
case .messageScreenshot:
let replyTo: Api.InputReplyTo
@ -585,6 +597,7 @@ private func sendMessageContent(account: Account, peerId: PeerId, attributes: [M
var replyToStoryId: StoryId?
var scheduleTime: Int32?
var sendAsPeerId: PeerId?
var allowPaidStars: Int64?
var flags: Int32 = 0
flags |= (1 << 7)
@ -609,6 +622,8 @@ private func sendMessageContent(account: Account, peerId: PeerId, attributes: [M
scheduleTime = attribute.scheduleTime
} else if let attribute = attribute as? SendAsMessageAttribute {
sendAsPeerId = attribute.peerId
} else if let attribute = attribute as? PaidStarsMessageAttribute {
allowPaidStars = attribute.stars.value
}
}
@ -622,6 +637,11 @@ private func sendMessageContent(account: Account, peerId: PeerId, attributes: [M
flags |= (1 << 13)
}
if let _ = allowPaidStars {
flags |= 1 << 21
}
let sendMessageRequest: Signal<Api.Updates, NoError>
switch content {
case let .text(text):
@ -641,7 +661,7 @@ private func sendMessageContent(account: Account, peerId: PeerId, attributes: [M
replyTo = .inputReplyToMessage(flags: flags, replyToMsgId: threadId, topMsgId: threadId, replyToPeerId: nil, quoteText: nil, quoteEntities: nil, quoteOffset: nil)
}
sendMessageRequest = account.network.request(Api.functions.messages.sendMessage(flags: flags, peer: inputPeer, replyTo: replyTo, message: text, randomId: uniqueId, replyMarkup: nil, entities: messageEntities, scheduleDate: scheduleTime, sendAs: sendAsInputPeer, quickReplyShortcut: nil, effect: nil, allowPaidStars: nil))
sendMessageRequest = account.network.request(Api.functions.messages.sendMessage(flags: flags, peer: inputPeer, replyTo: replyTo, message: text, randomId: uniqueId, replyMarkup: nil, entities: messageEntities, scheduleDate: scheduleTime, sendAs: sendAsInputPeer, quickReplyShortcut: nil, effect: nil, allowPaidStars: allowPaidStars))
|> `catch` { _ -> Signal<Api.Updates, NoError> in
return .complete()
}
@ -662,7 +682,7 @@ private func sendMessageContent(account: Account, peerId: PeerId, attributes: [M
replyTo = .inputReplyToMessage(flags: flags, replyToMsgId: threadId, topMsgId: threadId, replyToPeerId: nil, quoteText: nil, quoteEntities: nil, quoteOffset: nil)
}
sendMessageRequest = account.network.request(Api.functions.messages.sendMedia(flags: flags, peer: inputPeer, replyTo: replyTo, media: inputMedia, message: text, randomId: uniqueId, replyMarkup: nil, entities: messageEntities, scheduleDate: scheduleTime, sendAs: sendAsInputPeer, quickReplyShortcut: nil, effect: nil, allowPaidStars: nil))
sendMessageRequest = account.network.request(Api.functions.messages.sendMedia(flags: flags, peer: inputPeer, replyTo: replyTo, media: inputMedia, message: text, randomId: uniqueId, replyMarkup: nil, entities: messageEntities, scheduleDate: scheduleTime, sendAs: sendAsInputPeer, quickReplyShortcut: nil, effect: nil, allowPaidStars: allowPaidStars))
|> `catch` { _ -> Signal<Api.Updates, NoError> in
return .complete()
}

@ -331,6 +331,16 @@ public final class AccountStateManager {
return self.forceSendPendingStarsReactionPipe.signal()
}
fileprivate let forceSendPendingPaidMessagePipe = ValuePipe<PeerId>()
public var forceSendPendingPaidMessage: Signal<PeerId, NoError> {
return self.forceSendPendingPaidMessagePipe.signal()
}
fileprivate let commitSendPendingPaidMessagePipe = ValuePipe<MessageId>()
public var commitSendPendingPaidMessage: Signal<MessageId, NoError> {
return self.commitSendPendingPaidMessagePipe.signal()
}
fileprivate let sentScheduledMessageIdsPipe = ValuePipe<Set<MessageId>>()
public var sentScheduledMessageIds: Signal<Set<MessageId>, NoError> {
return self.sentScheduledMessageIdsPipe.signal()
@ -1951,6 +1961,18 @@ public final class AccountStateManager {
}
}
var forceSendPendingPaidMessage: Signal<PeerId, NoError> {
return self.impl.signalWith { impl, subscriber in
return impl.forceSendPendingPaidMessage.start(next: subscriber.putNext, error: subscriber.putError, completed: subscriber.putCompletion)
}
}
var commitSendPendingPaidMessage: Signal<MessageId, NoError> {
return self.impl.signalWith { impl, subscriber in
return impl.commitSendPendingPaidMessage.start(next: subscriber.putNext, error: subscriber.putError, completed: subscriber.putCompletion)
}
}
public var sentScheduledMessageIds: Signal<Set<MessageId>, NoError> {
return self.impl.signalWith { impl, subscriber in
return impl.sentScheduledMessageIds.start(next: subscriber.putNext, error: subscriber.putError, completed: subscriber.putCompletion)
@ -1963,6 +1985,19 @@ public final class AccountStateManager {
}
}
func forceSendPendingPaidMessage(peerId: PeerId) {
self.impl.with { impl in
impl.forceSendPendingPaidMessagePipe.putNext(peerId)
}
}
func commitSendPendingPaidMessage(messageId: MessageId) {
self.impl.with { impl in
impl.commitSendPendingPaidMessagePipe.putNext(messageId)
}
}
var updateConfigRequested: (() -> Void)?
var isPremiumUpdated: (() -> Void)?

@ -88,6 +88,9 @@ final class AccountTaskManager {
tasks.add(managedSynchronizeMarkAllUnseenReactionsOperations(postbox: self.stateManager.postbox, network: self.stateManager.network, stateManager: self.stateManager).start())
tasks.add(managedApplyPendingMessageReactionsActions(postbox: self.stateManager.postbox, network: self.stateManager.network, stateManager: self.stateManager).start())
tasks.add(managedApplyPendingMessageStarsReactionsActions(postbox: self.stateManager.postbox, network: self.stateManager.network, stateManager: self.stateManager).start())
tasks.add(managedApplyPendingPaidMessageActions(postbox: self.stateManager.postbox, network: self.stateManager.network, stateManager: self.stateManager).start())
tasks.add(managedSynchronizeEmojiKeywordsOperations(postbox: self.stateManager.postbox, network: self.stateManager.network).start())
tasks.add(managedApplyPendingScheduledMessagesActions(postbox: self.stateManager.postbox, network: self.stateManager.network, stateManager: self.stateManager).start())
tasks.add(managedSynchronizeAvailableReactions(postbox: self.stateManager.postbox, network: self.stateManager.network).start())

@ -89,4 +89,169 @@ func _internal_updateChannelPaidMessagesStars(account: Account, peerId: PeerId,
|> switchToLatest
}
public final class PostponeSendPaidMessageAction: PendingMessageActionData {
public let randomId: Int64
public init(randomId: Int64) {
self.randomId = randomId
}
public init(decoder: PostboxDecoder) {
self.randomId = decoder.decodeInt64ForKey("id", orElse: 0)
}
public func encode(_ encoder: PostboxEncoder) {
encoder.encodeInt64(self.randomId, forKey: "id")
}
public func isEqual(to: PendingMessageActionData) -> Bool {
if let other = to as? PostponeSendPaidMessageAction {
if self.randomId != other.randomId {
return false
}
return true
} else {
return false
}
}
}
private final class ManagedApplyPendingPaidMessageActionsHelper {
var operationDisposables: [MessageId: (PendingMessageActionData, Disposable)] = [:]
func update(entries: [PendingMessageActionsEntry]) -> (disposeOperations: [Disposable], beginOperations: [(PendingMessageActionsEntry, MetaDisposable)]) {
var disposeOperations: [Disposable] = []
var beginOperations: [(PendingMessageActionsEntry, MetaDisposable)] = []
var hasRunningOperationForPeerId = Set<PeerId>()
var validIds = Set<MessageId>()
for entry in entries {
if let current = self.operationDisposables[entry.id], !current.0.isEqual(to: entry.action) {
self.operationDisposables.removeValue(forKey: entry.id)
disposeOperations.append(current.1)
}
if !hasRunningOperationForPeerId.contains(entry.id.peerId) {
hasRunningOperationForPeerId.insert(entry.id.peerId)
validIds.insert(entry.id)
let disposable = MetaDisposable()
beginOperations.append((entry, disposable))
self.operationDisposables[entry.id] = (entry.action, disposable)
}
}
var removeMergedIds: [MessageId] = []
for (id, actionAndDisposable) in self.operationDisposables {
if !validIds.contains(id) {
removeMergedIds.append(id)
disposeOperations.append(actionAndDisposable.1)
}
}
for id in removeMergedIds {
self.operationDisposables.removeValue(forKey: id)
}
return (disposeOperations, beginOperations)
}
func reset() -> [Disposable] {
let disposables = Array(self.operationDisposables.values.map(\.1))
self.operationDisposables.removeAll()
return disposables
}
}
private func withTakenStarsAction(postbox: Postbox, type: PendingMessageActionType, id: MessageId, _ f: @escaping (Transaction, PendingMessageActionsEntry?) -> Signal<Never, NoError>) -> Signal<Never, NoError> {
return postbox.transaction { transaction -> Signal<Never, NoError> in
var result: PendingMessageActionsEntry?
if let action = transaction.getPendingMessageAction(type: type, id: id) as? PostponeSendPaidMessageAction {
result = PendingMessageActionsEntry(id: id, action: action)
}
return f(transaction, result)
}
|> switchToLatest
}
private func sendPostponedPaidMessage(transaction: Transaction, postbox: Postbox, network: Network, stateManager: AccountStateManager, id: MessageId) -> Signal<Never, NoError> {
stateManager.commitSendPendingPaidMessage(messageId: id)
return postbox.transaction { transaction -> Void in
transaction.setPendingMessageAction(type: .sendPostponedPaidMessage, id: id, action: nil)
}
|> ignoreValues
}
func managedApplyPendingPaidMessageActions(postbox: Postbox, network: Network, stateManager: AccountStateManager) -> Signal<Void, NoError> {
return Signal { _ in
let helper = Atomic<ManagedApplyPendingPaidMessageActionsHelper>(value: ManagedApplyPendingPaidMessageActionsHelper())
let actionsKey = PostboxViewKey.pendingMessageActions(type: .sendPostponedPaidMessage)
let disposable = postbox.combinedView(keys: [actionsKey]).start(next: { view in
var entries: [PendingMessageActionsEntry] = []
if let v = view.views[actionsKey] as? PendingMessageActionsView {
entries = v.entries
}
let (disposeOperations, beginOperations) = helper.with { helper -> (disposeOperations: [Disposable], beginOperations: [(PendingMessageActionsEntry, MetaDisposable)]) in
return helper.update(entries: entries)
}
for disposable in disposeOperations {
disposable.dispose()
}
for (entry, disposable) in beginOperations {
let signal = withTakenStarsAction(postbox: postbox, type: .sendPostponedPaidMessage, id: entry.id, { transaction, entry -> Signal<Never, NoError> in
if let entry = entry {
if let _ = entry.action as? PostponeSendPaidMessageAction {
let triggerSignal: Signal<Void, NoError> = stateManager.forceSendPendingPaidMessage
|> filter {
$0 == entry.id.peerId
}
|> map { _ -> Void in
return Void()
}
|> take(1)
|> timeout(5.0, queue: .mainQueue(), alternate: .single(Void()))
return triggerSignal
|> mapToSignal { _ -> Signal<Never, NoError> in
return sendPostponedPaidMessage(transaction: transaction, postbox: postbox, network: network, stateManager: stateManager, id: entry.id)
}
} else {
assertionFailure()
}
}
return .complete()
})
|> then(
postbox.transaction { transaction -> Void in
transaction.setPendingMessageAction(type: .sendPostponedPaidMessage, id: entry.id, action: nil)
}
|> ignoreValues
)
disposable.set(signal.start())
}
})
return ActionDisposable {
let disposables = helper.with { helper -> [Disposable] in
return helper.reset()
}
for disposable in disposables {
disposable.dispose()
}
disposable.dispose()
}
}
}
func _internal_forceSendPostponedPaidMessage(account: Account, peerId: PeerId) -> Signal<Never, NoError> {
account.stateManager.forceSendPendingPaidMessage(peerId: peerId)
return .complete()
}

@ -63,6 +63,8 @@ private final class PendingMessageContext {
var error: PendingMessageFailureReason?
var statusSubscribers = Bag<(PendingMessageStatus?, PendingMessageFailureReason?) -> Void>()
var forcedReuploadOnce: Bool = false
let postponeDisposable = MetaDisposable()
var postponeSending = false
}
public enum PendingMessageFailureReason {
@ -486,7 +488,10 @@ public final class PendingMessageManager {
for (messageContext, message, type, contentUploadSignal) in messagesToUpload {
if strongSelf.canBeginUploadingMessage(id: message.id, type: type) {
if let paidStarsAttribute = message.paidStarsAttribute, paidStarsAttribute.postponeSending {
strongSelf.beginWaitingForPostponedMessageCommit(messageContext: messageContext, id: message.id)
}
if strongSelf.canBeginUploadingMessage(id: message.id, type: type), !messageContext.postponeSending {
strongSelf.beginUploadingMessage(messageContext: messageContext, id: message.id, threadId: message.threadId, groupId: message.groupingKey, uploadSignal: contentUploadSignal)
} else {
messageContext.state = .waitingForUploadToStart(groupId: message.groupingKey, upload: contentUploadSignal)
@ -663,6 +668,33 @@ public final class PendingMessageManager {
messageContext.state = .collectingInfo(message: message)
}
private func beginWaitingForPostponedMessageCommit(messageContext: PendingMessageContext, id: MessageId) {
messageContext.postponeSending = true
let signal: Signal<Void, NoError> = self.postbox.transaction { transaction -> Void in
transaction.setPendingMessageAction(type: .sendPostponedPaidMessage, id: id, action: PostponeSendPaidMessageAction(randomId: Int64.random(in: Int64.min ... Int64.max)))
}
|> mapToSignal { _ in
return self.stateManager.commitSendPendingPaidMessage
|> filter {
$0 == id
}
|> take(1)
|> map { _ in
Void()
}
}
|> deliverOn(self.queue)
messageContext.postponeDisposable.set(signal.start(next: { [weak self] _ in
guard let self else {
return
}
messageContext.postponeSending = false
self.updateWaitingUploads(peerId: id.peerId)
}))
}
private func beginUploadingMessage(messageContext: PendingMessageContext, id: MessageId, threadId: Int64?, groupId: Int64?, uploadSignal: Signal<PendingMessageUploadedContentResult, PendingMessageUploadError>) {
messageContext.state = .uploading(groupId: groupId)
@ -740,7 +772,7 @@ public final class PendingMessageManager {
loop: for contextId in messageIdsForPeer {
let context = self.messageContexts[contextId]!
if case let .waitingForUploadToStart(groupId, uploadSignal) = context.state {
if self.canBeginUploadingMessage(id: contextId, type: context.contentType ?? .media) {
if self.canBeginUploadingMessage(id: contextId, type: context.contentType ?? .media), !context.postponeSending {
context.state = .uploading(groupId: groupId)
let status = PendingMessageStatus(isRunning: true, progress: PendingMessageStatus.Progress(progress: 0.0))
context.status = status

@ -4,20 +4,24 @@ import TelegramApi
public final class PaidStarsMessageAttribute: Equatable, MessageAttribute {
public let stars: StarsAmount
public let postponeSending: Bool
public init(stars: StarsAmount) {
public init(stars: StarsAmount, postponeSending: Bool) {
self.stars = stars
self.postponeSending = postponeSending
}
required public init(decoder: PostboxDecoder) {
self.stars = decoder.decodeCodable(StarsAmount.self, forKey: "s") ?? StarsAmount(value: 0, nanos: 0)
self.postponeSending = decoder.decodeBoolForKey("ps", orElse: false)
}
public func encode(_ encoder: PostboxEncoder) {
encoder.encodeCodable(self.stars, forKey: "s")
encoder.encodeBool(self.postponeSending, forKey: "ps")
}
public static func ==(lhs: PaidStarsMessageAttribute, rhs: PaidStarsMessageAttribute) -> Bool {
return lhs.stars == rhs.stars
return lhs.stars == rhs.stars && lhs.postponeSending == rhs.postponeSending
}
}

@ -25,6 +25,7 @@ public struct CachedChannelFlags: OptionSet {
public static let paidMediaAllowed = CachedChannelFlags(rawValue: 1 << 11)
public static let canViewStarsRevenue = CachedChannelFlags(rawValue: 1 << 12)
public static let starGiftsAvailable = CachedChannelFlags(rawValue: 1 << 13)
public static let paidMessagesAvailable = CachedChannelFlags(rawValue: 1 << 14)
}
public struct CachedChannelParticipantsSummary: PostboxCoding, Equatable {

@ -191,6 +191,7 @@ public extension PendingMessageActionType {
static let sendScheduledMessageImmediately = PendingMessageActionType(rawValue: 2)
static let readReaction = PendingMessageActionType(rawValue: 3)
static let sendStarsReaction = PendingMessageActionType(rawValue: 4)
static let sendPostponedPaidMessage = PendingMessageActionType(rawValue: 5)
}
public let peerIdNamespacesWithInitialCloudMessageHoles = [Namespaces.Peer.CloudUser, Namespaces.Peer.CloudGroup, Namespaces.Peer.CloudChannel]

@ -375,22 +375,18 @@ public extension TelegramEngine.EngineData.Item {
}
var key: PostboxViewKey {
return .cachedPeerData(peerId: self.id)
return .peer(peerId: self.id, components: [.cachedData])
}
func extract(view: PostboxView) -> Result {
guard let view = view as? CachedPeerDataView else {
guard let view = view as? PeerView else {
preconditionFailure()
}
guard let cachedPeerData = view.cachedPeerData else {
return nil
}
switch cachedPeerData {
case let user as CachedUserData:
return user.sendPaidMessageStars
case let channel as CachedChannelData:
if let cachedPeerData = view.cachedData as? CachedUserData {
return cachedPeerData.sendPaidMessageStars
} else if let channel = peerViewMainPeer(view) as? TelegramChannel {
return channel.sendPaidMessageStars
default:
} else {
return nil
}
}
@ -861,6 +857,38 @@ public extension TelegramEngine.EngineData.Item {
}
}
public struct PeerSettings: TelegramEngineDataItem, TelegramEngineMapKeyDataItem, PostboxViewDataItem {
public typealias Result = Optional<PeerStatusSettings>
fileprivate var id: EnginePeer.Id
public var mapKey: EnginePeer.Id {
return self.id
}
public init(id: EnginePeer.Id) {
self.id = id
}
var key: PostboxViewKey {
return .cachedPeerData(peerId: self.id)
}
func extract(view: PostboxView) -> Result {
guard let view = view as? CachedPeerDataView else {
preconditionFailure()
}
if let cachedData = view.cachedPeerData as? CachedUserData {
return cachedData.peerStatusSettings
} else if let cachedData = view.cachedPeerData as? CachedChannelData {
return cachedData.peerStatusSettings
} else if let cachedData = view.cachedPeerData as? CachedGroupData {
return cachedData.peerStatusSettings
} else {
return nil
}
}
}
public struct AreVideoCallsAvailable: TelegramEngineDataItem, TelegramEngineMapKeyDataItem, PostboxViewDataItem {
public typealias Result = Bool

@ -2,15 +2,15 @@ import Foundation
import Postbox
import SwiftSignalKit
func _internal_enqueueOutgoingMessageWithChatContextResult(account: Account, to peerId: PeerId, threadId: Int64?, botId: PeerId, result: ChatContextResult, replyToMessageId: EngineMessageReplySubject?, replyToStoryId: StoryId?, hideVia: Bool, silentPosting: Bool, scheduleTime: Int32?, correlationId: Int64?) -> Bool {
guard let message = _internal_outgoingMessageWithChatContextResult(to: peerId, threadId: threadId, botId: botId, result: result, replyToMessageId: replyToMessageId, replyToStoryId: replyToStoryId, hideVia: hideVia, silentPosting: silentPosting, scheduleTime: scheduleTime, correlationId: correlationId) else {
func _internal_enqueueOutgoingMessageWithChatContextResult(account: Account, to peerId: PeerId, threadId: Int64?, botId: PeerId, result: ChatContextResult, replyToMessageId: EngineMessageReplySubject?, replyToStoryId: StoryId?, hideVia: Bool, silentPosting: Bool, scheduleTime: Int32?, sendPaidMessageStars: StarsAmount?, postpone: Bool, correlationId: Int64?) -> Bool {
guard let message = _internal_outgoingMessageWithChatContextResult(to: peerId, threadId: threadId, botId: botId, result: result, replyToMessageId: replyToMessageId, replyToStoryId: replyToStoryId, hideVia: hideVia, silentPosting: silentPosting, scheduleTime: scheduleTime, sendPaidMessageStars: sendPaidMessageStars, postpone: postpone, correlationId: correlationId) else {
return false
}
let _ = enqueueMessages(account: account, peerId: peerId, messages: [message]).start()
return true
}
func _internal_outgoingMessageWithChatContextResult(to peerId: PeerId, threadId: Int64?, botId: PeerId, result: ChatContextResult, replyToMessageId: EngineMessageReplySubject?, replyToStoryId: StoryId?, hideVia: Bool, silentPosting: Bool, scheduleTime: Int32?, correlationId: Int64?) -> EnqueueMessage? {
func _internal_outgoingMessageWithChatContextResult(to peerId: PeerId, threadId: Int64?, botId: PeerId, result: ChatContextResult, replyToMessageId: EngineMessageReplySubject?, replyToStoryId: StoryId?, hideVia: Bool, silentPosting: Bool, scheduleTime: Int32?, sendPaidMessageStars: StarsAmount?, postpone: Bool, correlationId: Int64?) -> EnqueueMessage? {
var replyToMessageId = replyToMessageId
if replyToMessageId == nil, let threadId = threadId {
replyToMessageId = EngineMessageReplySubject(messageId: MessageId(peerId: peerId, namespace: Namespaces.Message.Cloud, id: MessageId.Id(clamping: threadId)), quote: nil)
@ -32,6 +32,9 @@ func _internal_outgoingMessageWithChatContextResult(to peerId: PeerId, threadId:
if silentPosting {
attributes.append(NotificationInfoMessageAttribute(flags: .muted))
}
if let sendPaidMessageStars {
attributes.append(PaidStarsMessageAttribute(stars: sendPaidMessageStars, postponeSending: postpone))
}
switch result.message {
case let .auto(caption, entities, replyMarkup):
if let entities = entities {

@ -247,13 +247,14 @@ public extension TelegramEngine {
storyId: StoryId? = nil,
content: EngineOutgoingMessageContent,
silentPosting: Bool = false,
scheduleTime: Int32? = nil
scheduleTime: Int32? = nil,
sendPaidMessageStars: StarsAmount? = nil
) -> Signal<[MessageId?], NoError> {
var message: EnqueueMessage?
if case let .preparedInlineMessage(preparedInlineMessage) = content {
message = self.outgoingMessageWithChatContextResult(to: peerId, threadId: nil, botId: preparedInlineMessage.botId, result: preparedInlineMessage.result, replyToMessageId: replyToMessageId, replyToStoryId: storyId, hideVia: true, silentPosting: silentPosting, scheduleTime: scheduleTime, correlationId: nil)
message = self.outgoingMessageWithChatContextResult(to: peerId, threadId: nil, botId: preparedInlineMessage.botId, result: preparedInlineMessage.result, replyToMessageId: replyToMessageId, replyToStoryId: storyId, hideVia: true, silentPosting: silentPosting, scheduleTime: scheduleTime, sendPaidMessageStars: sendPaidMessageStars, postpone: false, correlationId: nil)
} else if case let .contextResult(results, result) = content {
message = self.outgoingMessageWithChatContextResult(to: peerId, threadId: nil, botId: results.botId, result: result, replyToMessageId: replyToMessageId, replyToStoryId: storyId, hideVia: true, silentPosting: silentPosting, scheduleTime: scheduleTime, correlationId: nil)
message = self.outgoingMessageWithChatContextResult(to: peerId, threadId: nil, botId: results.botId, result: result, replyToMessageId: replyToMessageId, replyToStoryId: storyId, hideVia: true, silentPosting: silentPosting, scheduleTime: scheduleTime, sendPaidMessageStars: sendPaidMessageStars, postpone: false, correlationId: nil)
} else {
var attributes: [MessageAttribute] = []
if silentPosting {
@ -262,6 +263,9 @@ public extension TelegramEngine {
if let scheduleTime = scheduleTime {
attributes.append(OutgoingScheduleInfoMessageAttribute(scheduleTime: scheduleTime))
}
if let sendPaidMessageStars {
attributes.append(PaidStarsMessageAttribute(stars: sendPaidMessageStars, postponeSending: false))
}
var text: String = ""
var mediaReference: AnyMediaReference?
@ -301,12 +305,12 @@ public extension TelegramEngine {
)
}
public func enqueueOutgoingMessageWithChatContextResult(to peerId: PeerId, threadId: Int64?, botId: PeerId, result: ChatContextResult, replyToMessageId: EngineMessageReplySubject? = nil, replyToStoryId: StoryId? = nil, hideVia: Bool = false, silentPosting: Bool = false, scheduleTime: Int32? = nil, correlationId: Int64? = nil) -> Bool {
return _internal_enqueueOutgoingMessageWithChatContextResult(account: self.account, to: peerId, threadId: threadId, botId: botId, result: result, replyToMessageId: replyToMessageId, replyToStoryId: replyToStoryId, hideVia: hideVia, silentPosting: silentPosting, scheduleTime: scheduleTime, correlationId: correlationId)
public func enqueueOutgoingMessageWithChatContextResult(to peerId: PeerId, threadId: Int64?, botId: PeerId, result: ChatContextResult, replyToMessageId: EngineMessageReplySubject? = nil, replyToStoryId: StoryId? = nil, hideVia: Bool = false, silentPosting: Bool = false, scheduleTime: Int32? = nil, sendPaidMessageStars: StarsAmount?, postpone: Bool = false, correlationId: Int64? = nil) -> Bool {
return _internal_enqueueOutgoingMessageWithChatContextResult(account: self.account, to: peerId, threadId: threadId, botId: botId, result: result, replyToMessageId: replyToMessageId, replyToStoryId: replyToStoryId, hideVia: hideVia, silentPosting: silentPosting, scheduleTime: scheduleTime, sendPaidMessageStars: sendPaidMessageStars, postpone: postpone, correlationId: correlationId)
}
public func outgoingMessageWithChatContextResult(to peerId: PeerId, threadId: Int64?, botId: PeerId, result: ChatContextResult, replyToMessageId: EngineMessageReplySubject?, replyToStoryId: StoryId?, hideVia: Bool, silentPosting: Bool, scheduleTime: Int32?, correlationId: Int64?) -> EnqueueMessage? {
return _internal_outgoingMessageWithChatContextResult(to: peerId, threadId: threadId, botId: botId, result: result, replyToMessageId: replyToMessageId, replyToStoryId: replyToStoryId, hideVia: hideVia, silentPosting: silentPosting, scheduleTime: scheduleTime, correlationId: correlationId)
public func outgoingMessageWithChatContextResult(to peerId: PeerId, threadId: Int64?, botId: PeerId, result: ChatContextResult, replyToMessageId: EngineMessageReplySubject?, replyToStoryId: StoryId?, hideVia: Bool, silentPosting: Bool, scheduleTime: Int32?, sendPaidMessageStars: StarsAmount?, postpone: Bool, correlationId: Int64?) -> EnqueueMessage? {
return _internal_outgoingMessageWithChatContextResult(to: peerId, threadId: threadId, botId: botId, result: result, replyToMessageId: replyToMessageId, replyToStoryId: replyToStoryId, hideVia: hideVia, silentPosting: silentPosting, scheduleTime: scheduleTime, sendPaidMessageStars: sendPaidMessageStars, postpone: postpone, correlationId: correlationId)
}
public func setMessageReactions(
@ -349,6 +353,10 @@ public extension TelegramEngine {
let _ = _internal_forceSendPendingSendStarsReaction(account: self.account, messageId: id).startStandalone()
}
public func forceSendPostponedPaidMessage(peerId: EnginePeer.Id) {
let _ = _internal_forceSendPostponedPaidMessage(account: self.account, peerId: peerId).startStandalone()
}
public func updateStarsReactionPrivacy(id: EngineMessage.Id, privacy: TelegramPaidReactionPrivacy) -> Signal<Never, NoError> {
return _internal_updateStarsReactionPrivacy(account: self.account, messageId: id, privacy: privacy)
}

@ -1064,7 +1064,8 @@ private final class ProfileGiftsContextImpl {
}
return postbox.transaction { transaction -> ([ProfileGiftsContext.State.StarGift], Int32, String?, Bool?) in
switch result {
case let .savedStarGifts(_, count, apiNotificationsEnabled, apiGifts, nextOffset, chats, users):
case let .savedStarGifts(_, count, apiNotificationsEnabled, pinnedToTop, apiGifts, nextOffset, chats, users):
let _ = pinnedToTop
let parsedPeers = AccumulatedPeers(transaction: transaction, chats: chats, users: users)
updatePeers(transaction: transaction, accountPeerId: accountPeerId, peers: parsedPeers)

@ -99,8 +99,8 @@ func _internal_addGroupMember(account: Account, peerId: PeerId, memberId: PeerId
}
})
}
return TelegramInvitePeersResult(forbiddenPeers: missingInviteesValue.compactMap { invitee -> TelegramForbiddenInvitePeer? in
let result = TelegramInvitePeersResult(forbiddenPeers: missingInviteesValue.compactMap { invitee -> TelegramForbiddenInvitePeer? in
switch invitee {
case let .missingInvitee(flags, userId):
guard let peer = transaction.getPeer(PeerId(namespace: Namespaces.Peer.CloudUser, id: PeerId.Id._internalFromInt64Value(userId))) else {
@ -113,6 +113,10 @@ func _internal_addGroupMember(account: Account, peerId: PeerId, memberId: PeerId
)
}
})
let _ = _internal_updateIsPremiumRequiredToContact(account: account, peerIds: result.forbiddenPeers.map { $0.peer.id }).startStandalone()
return result
}
|> mapError { _ -> AddGroupMemberError in }
|> mapToSignal { result -> Signal<Void, AddGroupMemberError> in
@ -186,6 +190,8 @@ func _internal_addChannelMember(account: Account, peerId: PeerId, memberId: Peer
switch result {
case let .invitedUsers(updates, missingInvitees):
if case let .missingInvitee(flags, _) = missingInvitees.first {
let _ = _internal_updateIsPremiumRequiredToContact(account: account, peerIds: [memberPeer.id]).startStandalone()
return .fail(.restricted(TelegramForbiddenInvitePeer(
peer: EnginePeer(memberPeer),
canInviteWithPremium: (flags & (1 << 0)) != 0,
@ -302,7 +308,7 @@ func _internal_addChannelMembers(account: Account, peerId: PeerId, memberIds: [P
account.viewTracker.forceUpdateCachedPeerData(peerId: peerId)
return account.postbox.transaction { transaction -> TelegramInvitePeersResult in
return TelegramInvitePeersResult(forbiddenPeers: missingInviteesValue.compactMap { invitee -> TelegramForbiddenInvitePeer? in
let result = TelegramInvitePeersResult(forbiddenPeers: missingInviteesValue.compactMap { invitee -> TelegramForbiddenInvitePeer? in
switch invitee {
case let .missingInvitee(flags, userId):
guard let peer = transaction.getPeer(PeerId(namespace: Namespaces.Peer.CloudUser, id: PeerId.Id._internalFromInt64Value(userId))) else {
@ -315,6 +321,8 @@ func _internal_addChannelMembers(account: Account, peerId: PeerId, memberIds: [P
)
}
})
let _ = _internal_updateIsPremiumRequiredToContact(account: account, peerIds: result.forbiddenPeers.map { $0.peer.id }).startStandalone()
return result
}
|> castError(AddChannelMemberError.self)
}

@ -620,6 +620,10 @@ func _internal_fetchAndUpdateCachedPeerData(accountPeerId: PeerId, peerId rawPee
if (flags2 & Int32(1 << 19)) != 0 {
channelFlags.insert(.starGiftsAvailable)
}
if (flags2 & Int32(1 << 20)) != 0 {
channelFlags.insert(.paidMessagesAvailable)
}
let sendAsPeerId = defaultSendAs?.peerId
let linkedDiscussionPeerId: PeerId?

@ -238,6 +238,7 @@ private struct ApplicationSpecificNoticeKeys {
private static let groupEmojiPackNamespace: Int32 = 9
private static let dismissedBirthdayPremiumGiftTipNamespace: Int32 = 10
private static let displayedPeerVerificationNamespace: Int32 = 11
private static let dismissedPaidMessageWarningNamespace: Int32 = 11
static func inlineBotLocationRequestNotice(peerId: PeerId) -> NoticeEntryKey {
return NoticeEntryKey(namespace: noticeNamespace(namespace: inlineBotLocationRequestNamespace), key: noticeKey(peerId: peerId, key: 0))
@ -523,6 +524,10 @@ private struct ApplicationSpecificNoticeKeys {
return NoticeEntryKey(namespace: noticeNamespace(namespace: displayedPeerVerificationNamespace), key: noticeKey(peerId: peerId, key: 0))
}
static func dismissedPaidMessageWarning(peerId: PeerId) -> NoticeEntryKey {
return NoticeEntryKey(namespace: noticeNamespace(namespace: dismissedPaidMessageWarningNamespace), key: noticeKey(peerId: peerId, key: 0))
}
static func monetizationIntroDismissed() -> NoticeEntryKey {
return NoticeEntryKey(namespace: noticeNamespace(namespace: globalNamespace), key: ApplicationSpecificGlobalNotice.monetizationIntroDismissed.key)
}
@ -2176,6 +2181,28 @@ public struct ApplicationSpecificNotice {
|> ignoreValues
}
public static func dismissedPaidMessageWarningNamespace(accountManager: AccountManager<TelegramAccountManagerTypes>, peerId: PeerId) -> Signal<Int64?, NoError> {
return accountManager.noticeEntry(key: ApplicationSpecificNoticeKeys.dismissedPaidMessageWarning(peerId: peerId))
|> map { view -> Int64? in
if let counter = view.value?.get(ApplicationSpecificCounterNotice.self) {
return Int64(counter.value)
} else {
return nil
}
}
}
public static func setDismissedPaidMessageWarningNamespace(accountManager: AccountManager<TelegramAccountManagerTypes>, peerId: PeerId, amount: Int64?) -> Signal<Never, NoError> {
return accountManager.transaction { transaction -> Void in
if let amount, let entry = CodableEntry(ApplicationSpecificCounterNotice(value: Int32(amount))) {
transaction.setNotice(ApplicationSpecificNoticeKeys.dismissedPaidMessageWarning(peerId: peerId), entry)
} else {
transaction.setNotice(ApplicationSpecificNoticeKeys.dismissedPaidMessageWarning(peerId: peerId), nil)
}
}
|> ignoreValues
}
public static func setMonetizationIntroDismissed(accountManager: AccountManager<TelegramAccountManagerTypes>) -> Signal<Never, NoError> {
return accountManager.transaction { transaction -> Void in
if let entry = CodableEntry(ApplicationSpecificBoolNotice()) {

@ -313,12 +313,16 @@ public enum PresentationResourceKey: Int32 {
case chatBubbleCloseIcon
case chatEmptyStateStarIcon
case chatPlaceholderStarIcon
case avatarPremiumLockBadgeBackground
case avatarPremiumLockBadge
case shareAvatarPremiumLockBadgeBackground
case shareAvatarPremiumLockBadge
case shareAvatarStarsLockBadgeBackground
case shareAvatarStarsLockBadgeInnerBackground
case sharedLinkIcon
case hideIconImage

@ -1351,4 +1351,19 @@ public struct PresentationResourcesChat {
return nil
})
}
public static func chatPlaceholderStarIcon(_ theme: PresentationTheme) -> UIImage? {
return theme.image(PresentationResourceKey.chatPlaceholderStarIcon.rawValue, { theme in
if let image = UIImage(bundleImageName: "Premium/Stars/ButtonStar") {
return generateImage(image.size, contextGenerator: { size, context in
let bounds = CGRect(origin: .zero, size: size)
context.clear(bounds)
if let cgImage = image.cgImage {
context.draw(cgImage, in: bounds.offsetBy(dx: UIScreenPixel, dy: -UIScreenPixel), byTiling: false)
}
})
}
return nil
})
}
}

@ -488,6 +488,29 @@ public struct PresentationResourcesChatList {
})
}
public static func shareAvatarStarsLockBadgeBackground(_ theme: PresentationTheme) -> UIImage? {
return theme.image(PresentationResourceKey.shareAvatarStarsLockBadgeBackground.rawValue, { theme in
return generateImage(CGSize(width: 20.0, height: 20.0), contextGenerator: { size, context in
context.clear(CGRect(origin: .zero, size: size))
context.setFillColor(UIColor.white.cgColor)
let rect = CGRect(origin: .zero, size: CGSize(width: 20.0, height: 18.0 + UIScreenPixel)).insetBy(dx: 1.0 - UIScreenPixel, dy: 0.0)
context.addPath(UIBezierPath(roundedRect: rect, cornerRadius: rect.height / 2.0).cgPath)
context.fillPath()
})?.withRenderingMode(.alwaysTemplate).stretchableImage(withLeftCapWidth: 10, topCapHeight: 10)
})
}
public static func shareAvatarStarsLockBadgeInnerBackground(_ theme: PresentationTheme) -> UIImage? {
return theme.image(PresentationResourceKey.shareAvatarStarsLockBadgeInnerBackground.rawValue, { theme in
return generateImage(CGSize(width: 20.0, height: 16.0), contextGenerator: { size, context in
context.clear(CGRect(origin: .zero, size: size))
context.setFillColor(UIColor.white.cgColor)
context.addPath(UIBezierPath(roundedRect: CGRect(origin: .zero, size: CGSize(width: 20.0, height: 15.0)), cornerRadius: 7.5).cgPath)
context.fillPath()
})?.withRenderingMode(.alwaysTemplate).stretchableImage(withLeftCapWidth: 10, topCapHeight: 0)
})
}
public static func shareAvatarPremiumLockBadge(_ theme: PresentationTheme) -> UIImage? {
return theme.image(PresentationResourceKey.shareAvatarPremiumLockBadge.rawValue, { theme in
return generateImage(CGSize(width: 20.0, height: 20.0), contextGenerator: { size, context in

@ -79,7 +79,7 @@ private func peerDisplayTitles(_ peers: [Peer], strings: PresentationStrings, na
}
}
public func universalServiceMessageString(presentationData: (PresentationTheme, TelegramWallpaper)?, strings: PresentationStrings, nameDisplayOrder: PresentationPersonNameOrder, dateTimeFormat: PresentationDateTimeFormat, message: EngineMessage, accountPeerId: EnginePeer.Id, forChatList: Bool, forForumOverview: Bool, forAdditionalServiceMessage: Bool = false) -> NSAttributedString? {
public func universalServiceMessageString(presentationData: (PresentationTheme, TelegramWallpaper)?, strings: PresentationStrings, nameDisplayOrder: PresentationPersonNameOrder, dateTimeFormat: PresentationDateTimeFormat, message: EngineMessage, messageCount: Int? = nil, accountPeerId: EnginePeer.Id, forChatList: Bool, forForumOverview: Bool, forAdditionalServiceMessage: Bool = false) -> NSAttributedString? {
var attributedString: NSAttributedString?
let primaryTextColor: UIColor
@ -92,6 +92,33 @@ public func universalServiceMessageString(presentationData: (PresentationTheme,
let bodyAttributes = MarkdownAttributeSet(font: titleFont, textColor: primaryTextColor, additionalAttributes: [:])
let boldAttributes = MarkdownAttributeSet(font: titleBoldFont, textColor: primaryTextColor, additionalAttributes: [:])
if !forAdditionalServiceMessage {
for attribute in message.attributes {
if let attribute = attribute as? PaidStarsMessageAttribute {
let messageCount = Int32(messageCount ?? 1)
let price = strings.Notification_PaidMessage_Stars(Int32(attribute.stars.value) * messageCount)
if message.author?.id == accountPeerId {
if messageCount > 1 {
let messagesString = strings.Notification_PaidMessage_Messages(messageCount)
return addAttributesToStringWithRanges(strings.Notification_PaidMessageYouMany(price, messagesString)._tuple, body: bodyAttributes, argumentAttributes: [0: boldAttributes, 1: boldAttributes])
} else {
return addAttributesToStringWithRanges(strings.Notification_PaidMessageYou(price)._tuple, body: bodyAttributes, argumentAttributes: [0: boldAttributes])
}
} else {
let compactAuthorName = message.author?.compactDisplayTitle ?? ""
var attributes = peerMentionsAttributes(primaryTextColor: primaryTextColor, peerIds: [(0, message.author?.id)])
attributes[1] = boldAttributes
if messageCount > 1 {
let messagesString = strings.Notification_PaidMessage_Messages(messageCount)
return addAttributesToStringWithRanges(strings.Notification_PaidMessageMany(compactAuthorName, price, messagesString)._tuple, body: bodyAttributes, argumentAttributes: attributes)
} else {
return addAttributesToStringWithRanges(strings.Notification_PaidMessage(compactAuthorName, price)._tuple, body: bodyAttributes, argumentAttributes: attributes)
}
}
}
}
}
for media in message.media {
if let action = media as? TelegramMediaAction {
let authorName = message.author?.displayTitle(strings: strings, displayOrder: nameDisplayOrder) ?? ""
@ -1150,11 +1177,11 @@ public func universalServiceMessageString(presentationData: (PresentationTheme,
let attributes = peerMentionsAttributes(primaryTextColor: primaryTextColor, peerIds: peerIds)
attributedString = addAttributesToStringWithRanges(strings.Notification_StarsGift_TransferToChannelYou(peerName)._tuple, body: bodyAttributes, argumentAttributes: attributes)
} else {
let senderPeerName = EnginePeer(targetPeer).compactDisplayTitle
let targetPeerName = EnginePeer(targetPeer).compactDisplayTitle
peerName = EnginePeer(peer).compactDisplayTitle
peerIds = [(0, senderId), (1, peerId)]
peerIds = [(0, peer.id), (1, targetPeer.id)]
let attributes = peerMentionsAttributes(primaryTextColor: primaryTextColor, peerIds: peerIds)
attributedString = addAttributesToStringWithRanges(strings.Notification_StarsGift_TransferToChannel(senderPeerName, peerName)._tuple, body: bodyAttributes, argumentAttributes: attributes)
attributedString = addAttributesToStringWithRanges(strings.Notification_StarsGift_TransferToChannel(peerName, targetPeerName)._tuple, body: bodyAttributes, argumentAttributes: attributes)
}
} else {
peerName = EnginePeer(peer).compactDisplayTitle
@ -1169,18 +1196,6 @@ public func universalServiceMessageString(presentationData: (PresentationTheme,
}
}
}
//TODO:release
/*case let .paidMessage(stars):
if message.author?.id == accountPeerId {
let starsString = strings.Notification_PaidMessage_Stars(Int32(stars))
let resultTitleString = strings.Notification_PaidMessageYou(starsString)
attributedString = addAttributesToStringWithRanges(resultTitleString._tuple, body: bodyAttributes, argumentAttributes: [0: boldAttributes])
} else {
let peerName = message.author?.compactDisplayTitle ?? ""
let starsString = strings.Notification_PaidMessage_Stars(Int32(stars))
let resultTitleString = strings.Notification_PaidMessage(peerName, starsString)
attributedString = addAttributesToStringWithRanges(resultTitleString._tuple, body: bodyAttributes, argumentAttributes: [0: boldAttributes, 1: boldAttributes])
}*/
case .unknown:
attributedString = nil
}

@ -1,5 +1,6 @@
import Foundation
import UIKit
import TelegramCore
import TelegramPresentationData
let walletAddressLength: Int = 48
@ -78,6 +79,20 @@ public func formatTonAmountText(_ value: Int64, dateTimeFormat: PresentationDate
return balanceText
}
public func formatStarsAmountText(_ amount: StarsAmount, dateTimeFormat: PresentationDateTimeFormat, showPlus: Bool = false) -> String {
var balanceText = presentationStringsFormattedNumber(Int32(amount.value), dateTimeFormat.groupingSeparator)
let fraction = Double(amount.nanos) / 10e6
if fraction > 0.0 {
balanceText.append(dateTimeFormat.decimalSeparator)
balanceText.append("\(Int32(fraction))")
}
if amount.value < 0 {
} else if showPlus {
balanceText.insert("+", at: balanceText.startIndex)
}
return balanceText
}
private let invalidAddressCharacters = CharacterSet(charactersIn: "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_=").inverted
public func isValidTonAddress(_ address: String, exactLength: Bool = false) -> Bool {
if address.count > walletAddressLength || address.rangeOfCharacter(from: invalidAddressCharacters) != nil {
@ -89,10 +104,9 @@ public func isValidTonAddress(_ address: String, exactLength: Bool = false) -> B
return true
}
private let amountDelimeterCharacters = CharacterSet(charactersIn: "0123456789-+").inverted
public func tonAmountAttributedString(_ string: String, integralFont: UIFont, fractionalFont: UIFont, color: UIColor) -> NSAttributedString {
public func tonAmountAttributedString(_ string: String, integralFont: UIFont, fractionalFont: UIFont, color: UIColor, decimalSeparator: String) -> NSAttributedString {
let result = NSMutableAttributedString()
if let range = string.rangeOfCharacter(from: amountDelimeterCharacters) {
if let range = string.range(of: decimalSeparator) {
let integralPart = String(string[..<range.lowerBound])
let fractionalPart = String(string[range.lowerBound...])
result.append(NSAttributedString(string: integralPart, font: integralFont, textColor: color))

@ -353,6 +353,7 @@ swift_library(
"//submodules/TelegramUI/Components/Chat/ChatOverscrollControl",
"//submodules/TelegramUI/Components/AudioWaveformNode",
"//submodules/TelegramUI/Components/Chat/ChatBotInfoItem",
"//submodules/TelegramUI/Components/Chat/ChatUserInfoItem",
"//submodules/TelegramUI/Components/Chat/ChatInputPanelNode",
"//submodules/TelegramUI/Components/Chat/ChatBotStartInputPanelNode",
"//submodules/TelegramUI/Components/Chat/ChatButtonKeyboardInputNode",
@ -453,6 +454,7 @@ swift_library(
"//submodules/TelegramUI/Components/Stars/StarsTransactionScreen",
"//submodules/TelegramUI/Components/Stars/StarsWithdrawalScreen",
"//submodules/TelegramUI/Components/Chat/FactCheckAlertController",
"//submodules/TelegramUI/Components/Chat/ChatMessagePaymentAlertController",
"//submodules/TelegramUI/Components/PeerManagement/OwnershipTransferController",
"//submodules/TelegramUI/Components/PeerManagement/OldChannelsController",
"//submodules/TelegramUI/Components/Chat/ChatSendStarsScreen",

@ -179,6 +179,25 @@ private final class ScrollContent: CombinedComponent {
contentSize.height += spacing
let premiumConfiguration = PremiumConfiguration.with(appConfiguration: component.context.currentAppConfiguration.with { $0 })
let respectText: String
let adsText: String
let infoRawText: String
switch component.mode {
case .channel:
respectText = strings.AdsInfo_Respect_Text
adsText = strings.AdsInfo_Ads_Text("\(premiumConfiguration.minChannelRestrictAdsLevel)").string
infoRawText = strings.AdsInfo_Launch_Text
case .bot:
respectText = strings.AdsInfo_Bot_Respect_Text
adsText = strings.AdsInfo_Bot_Ads_Text
infoRawText = strings.AdsInfo_Bot_Launch_Text
case .search:
respectText = "Ads like this do not use your personal information and are based on the search query you entered."
adsText = strings.AdsInfo_Bot_Ads_Text
infoRawText = "Anyone can create an ad to display in search results for any query. Check out the Telegram Ad Platform for details. [Learn More >]()"
}
var items: [AnyComponentWithIdentity<Empty>] = []
items.append(
AnyComponentWithIdentity(
@ -186,7 +205,7 @@ private final class ScrollContent: CombinedComponent {
component: AnyComponent(ParagraphComponent(
title: strings.AdsInfo_Respect_Title,
titleColor: textColor,
text: component.mode == .bot ? strings.AdsInfo_Bot_Respect_Text : strings.AdsInfo_Respect_Text,
text: respectText,
textColor: secondaryTextColor,
accentColor: linkColor,
iconName: "Ads/Privacy",
@ -194,27 +213,31 @@ private final class ScrollContent: CombinedComponent {
))
)
)
items.append(
AnyComponentWithIdentity(
id: "split",
component: AnyComponent(ParagraphComponent(
title: component.mode == .bot ? strings.AdsInfo_Bot_Split_Title : strings.AdsInfo_Split_Title,
titleColor: textColor,
text: component.mode == .bot ? strings.AdsInfo_Bot_Split_Text : strings.AdsInfo_Split_Text,
textColor: secondaryTextColor,
accentColor: linkColor,
iconName: "Ads/Split",
iconColor: linkColor
))
if case .search = component.mode {
} else {
items.append(
AnyComponentWithIdentity(
id: "split",
component: AnyComponent(ParagraphComponent(
title: component.mode == .bot ? strings.AdsInfo_Bot_Split_Title : strings.AdsInfo_Split_Title,
titleColor: textColor,
text: component.mode == .bot ? strings.AdsInfo_Bot_Split_Text : strings.AdsInfo_Split_Text,
textColor: secondaryTextColor,
accentColor: linkColor,
iconName: "Ads/Split",
iconColor: linkColor
))
)
)
)
}
items.append(
AnyComponentWithIdentity(
id: "ads",
component: AnyComponent(ParagraphComponent(
title: strings.AdsInfo_Ads_Title,
titleColor: textColor,
text: component.mode == .bot ? strings.AdsInfo_Bot_Ads_Text : strings.AdsInfo_Ads_Text("\(premiumConfiguration.minChannelRestrictAdsLevel)").string,
text: adsText,
textColor: secondaryTextColor,
accentColor: linkColor,
iconName: "Premium/BoostPerk/NoAds",
@ -253,7 +276,7 @@ private final class ScrollContent: CombinedComponent {
state.cachedChevronImage = (generateTintedImage(image: UIImage(bundleImageName: "Settings/TextArrowRight"), color: linkColor)!, theme)
}
var infoString = component.mode == .bot ? strings.AdsInfo_Bot_Launch_Text : strings.AdsInfo_Launch_Text
var infoString = infoRawText
if let spaceRegex {
let nsRange = NSRange(infoString.startIndex..., in: infoString)
let matches = spaceRegex.matches(in: infoString, options: [], range: nsRange)

@ -5,27 +5,65 @@ import GradientBackground
public enum AvatarBackground: Equatable {
public static let defaultBackgrounds: [AvatarBackground] = [
.gradient([0xFF5A7FFF, 0xFF2CA0F2, 0xFF4DFF89, 0xFF6BFCEB]),
.gradient([0xFFFF011D, 0xFFFF530D, 0xFFFE64DC, 0xFFFFDC61]),
.gradient([0xFFFE64DC, 0xFFFF6847, 0xFFFFDD02, 0xFFFFAE10]),
.gradient([0xFF84EC00, 0xFF00B7C2, 0xFF00C217, 0xFFFFE600]),
.gradient([0xFF86B0FF, 0xFF35FFCF, 0xFF69FFFF, 0xFF76DEFF]),
.gradient([0xFFFAE100, 0xFFFF54EE, 0xFFFC2B78, 0xFFFF52D9]),
.gradient([0xFF73A4FF, 0xFF5F55FF, 0xFFFF49F8, 0xFFEC76FF]),
.gradient([0xFF5bd1ca, 0xFF538edb], false),
.gradient([0xFF61dba8, 0xFF52abd6], false),
.gradient([0xFFbdcb57, 0xFF4abe6e], false),
.gradient([0xFFd971bf, 0xFF986ce9], false),
.gradient([0xFFee8c56, 0xFFec628f], false),
.gradient([0xFFf2994f, 0xFFe76667], false),
.gradient([0xFFf0b948, 0xFFef7e4b], false),
.gradient([0xFF94A3B0, 0xFF6C7B87], true),
.gradient([0xFF949487, 0xFF707062], true),
.gradient([0xFFB09F99, 0xFF8F7E72], true),
.gradient([0xFFEBA15B, 0xFFA16730], true),
.gradient([0xFFE8B948, 0xFFB87C30], true),
.gradient([0xFF5E6F91, 0xFF415275], true),
.gradient([0xFF565D61, 0xFF3B4347], true),
.gradient([0xFF8F6655, 0xFF68443F], true),
.gradient([0xFF1B1B1B, 0xFF000000], true),
.gradient([0xFFAE72E3, 0xFF8854B5], true),
.gradient([0xFFC269BE, 0xFF8B4384], true),
.gradient([0xFF469CD3, 0xFF2E78A8], true),
.gradient([0xFF5BCEC5, 0xFF36928E], true),
.gradient([0xFF5FD66F, 0xFF319F76], true),
.gradient([0xFF66B27A, 0xFF33786D], true),
.gradient([0xFF6C9CF4, 0xFF5C6AEC], true),
.gradient([0xFFDA76A8, 0xFFAE5891], true),
.gradient([0xFFE66473, 0xFFA74559], true),
.gradient([0xFFAF75BC, 0xFF895196], true),
.gradient([0xFF438CB9, 0xFF2D6283], true),
.gradient([0xFF81B6B2, 0xFF4B9A96], true),
.gradient([0xFF66B27A, 0xFF33786D], true),
.gradient([0xFFCAB560, 0xFF8C803C], true),
.gradient([0xFFADB070, 0xFF6B7D54], true),
.gradient([0xFFBC7051, 0xFF975547], true),
.gradient([0xFFC7835E, 0xFF9E6345], true),
.gradient([0xFFE68A3C, 0xFFD45393], true),
.gradient([0xFF6BE2F2, 0xFF6675F7], true),
.gradient([0xFFC56DF4, 0xFF6073F4], true),
.gradient([0xFFEBC92F, 0xFF54B848], true)
]
case gradient([UInt32])
case gradient([UInt32], Bool)
public var colors: [UInt32] {
switch self {
case let .gradient(colors):
case let .gradient(colors, _):
return colors
}
}
public var isPremium: Bool {
switch self {
case let .gradient(_, isPremium):
return isPremium
}
}
public var isLight: Bool {
switch self {
case let .gradient(colors):
case let .gradient(colors, _):
if colors.count == 1 {
return UIColor(rgb: colors.first!).lightness > 0.99
} else if colors.count == 2 {
@ -44,7 +82,7 @@ public enum AvatarBackground: Equatable {
public func generateImage(size: CGSize) -> UIImage {
switch self {
case let .gradient(colors):
case let .gradient(colors, _):
if colors.count == 1 {
return generateSingleColorImage(size: size, color: UIColor(rgb: colors.first!))!
} else if colors.count == 2 {

@ -19,7 +19,7 @@ swift_library(
"//submodules/Components/ViewControllerComponent:ViewControllerComponent",
"//submodules/Components/ComponentDisplayAdapters:ComponentDisplayAdapters",
"//submodules/Components/MultilineTextComponent:MultilineTextComponent",
"//submodules/Components/SolidRoundedButtonComponent",
"//submodules/TelegramUI/Components/ButtonComponent",
"//submodules/TelegramPresentationData:TelegramPresentationData",
"//submodules/AccountContext:AccountContext",
"//submodules/AppBundle:AppBundle",
@ -40,6 +40,8 @@ swift_library(
"//submodules/TelegramUI/Components/EmojiTextAttachmentView:EmojiTextAttachmentView",
"//submodules/TelegramUI/Components/MediaEditor",
"//submodules/TelegramUI/Components/AvatarBackground",
"//submodules/TelegramUI/Components/LottieComponent",
"//submodules/TelegramUI/Components/Premium/PremiumStarComponent",
],
visibility = [
"//visibility:public",

@ -20,11 +20,13 @@ import Markdown
import GradientBackground
import LegacyComponents
import DrawingUI
import SolidRoundedButtonComponent
import ButtonComponent
import AnimationCache
import EmojiTextAttachmentView
import MediaEditor
import AvatarBackground
import LottieComponent
import UndoUI
public struct AvatarKeyboardInputData: Equatable {
var emoji: EmojiPagerContentComponent
@ -126,7 +128,14 @@ final class AvatarEditorScreenComponent: Component {
})
}
self.selectedBackground = .gradient(markup.backgroundColors.map { UInt32(bitPattern: $0) })
var isPremium = false
let colorsValue = markup.backgroundColors.map { UInt32(bitPattern: $0) }
if let defaultColor = AvatarBackground.defaultBackgrounds.first(where: { $0.colors == colorsValue}) {
if defaultColor.isPremium {
isPremium = true
}
}
self.selectedBackground = .gradient(colorsValue, isPremium)
self.previousColor = self.selectedBackground
} else {
self.selectedBackground = AvatarBackground.defaultBackgrounds.first!
@ -188,6 +197,8 @@ final class AvatarEditorScreenComponent: Component {
private let buttonView = ComponentView<Empty>()
private var component: AvatarEditorScreenComponent?
private var environment: EnvironmentType?
private weak var state: State?
private var navigationMetrics: (navigationHeight: CGFloat, statusBarHeight: CGFloat)?
@ -783,12 +794,15 @@ final class AvatarEditorScreenComponent: Component {
private var isExpanded = false
func update(component: AvatarEditorScreenComponent, availableSize: CGSize, state: State, environment: Environment<ViewControllerComponentContainer.Environment>, transition: ComponentTransition) -> CGSize {
func update(component: AvatarEditorScreenComponent, availableSize: CGSize, state: State, environment: Environment<EnvironmentType>, transition: ComponentTransition) -> CGSize {
self.component = component
let environment = environment[EnvironmentType.self].value
self.environment = environment
self.state = state
let environment = environment[ViewControllerComponentContainer.Environment.self].value
let strings = environment.strings
let theme = environment.theme
let controller = environment.controller
self.controller = {
@ -990,6 +1004,7 @@ final class AvatarEditorScreenComponent: Component {
transition: transition,
component: AnyComponent(BackgroundColorComponent(
theme: environment.theme,
isPremium: component.context.isPremium,
values: AvatarBackground.defaultBackgrounds,
selectedValue: state.selectedBackground,
customValue: state.customColor,
@ -1037,8 +1052,8 @@ final class AvatarEditorScreenComponent: Component {
colors: state.selectedBackground.colors,
colorsChanged: { [weak state] colors in
if let state {
state.customColor = .gradient(colors)
state.selectedBackground = .gradient(colors)
state.customColor = .gradient(colors, true)
state.selectedBackground = .gradient(colors, true)
state.updated(transition: .immediate)
}
},
@ -1268,29 +1283,65 @@ final class AvatarEditorScreenComponent: Component {
case .suggest:
buttonText = strings.AvatarEditor_SuggestProfilePhoto
case .user:
buttonText = strings.AvatarEditor_SetProfilePhoto
//TODO:localize
buttonText = "Set My Photo" //strings.AvatarEditor_SetProfilePhoto
case .group, .forum:
buttonText = strings.AvatarEditor_SetGroupPhoto
case .channel:
buttonText = strings.AvatarEditor_SetChannelPhoto
}
var isLocked = false
if component.peerType != .suggest, !component.context.isPremium {
if state.selectedBackground.isPremium {
isLocked = true
}
if let selectedFile = state.selectedFile {
if selectedFile.isSticker {
isLocked = true
}
}
}
var buttonContents: [AnyComponentWithIdentity<Empty>] = []
buttonContents.append(AnyComponentWithIdentity(id: AnyHashable(buttonText), component: AnyComponent(
Text(text: buttonText, font: Font.semibold(17.0), color: theme.list.itemCheckColors.foregroundColor)
)))
if !component.context.isPremium && isLocked {
buttonContents.append(AnyComponentWithIdentity(id: AnyHashable(0 as Int), component: AnyComponent(LottieComponent(
content: LottieComponent.AppBundleContent(name: "premium_unlock"),
color: theme.list.itemCheckColors.foregroundColor,
startingPosition: .begin,
size: CGSize(width: 30.0, height: 30.0),
loop: true
))))
}
let buttonSize = self.buttonView.update(
transition: transition,
component: AnyComponent(
SolidRoundedButtonComponent(
title: buttonText,
theme: SolidRoundedButtonComponent.Theme(theme: environment.theme),
fontSize: 17.0,
height: 50.0,
cornerRadius: 10.0,
ButtonComponent(
background: ButtonComponent.Background(
color: theme.list.itemCheckColors.fillColor,
foreground: theme.list.itemCheckColors.foregroundColor,
pressedColor: theme.list.itemCheckColors.fillColor.withMultipliedAlpha(0.8)
),
content: AnyComponentWithIdentity(id: AnyHashable(0 as Int), component: AnyComponent(
HStack(buttonContents, spacing: 3.0)
)),
isEnabled: true,
displaysProgress: false,
action: { [weak self] in
self?.complete()
if isLocked {
self?.presentPremiumToast()
} else {
self?.complete()
}
}
)
),
environment: {},
containerSize: CGSize(width: availableSize.width - sideInset * 2.0, height: environment.navigationHeight - environment.statusBarHeight)
containerSize: CGSize(width: availableSize.width - sideInset * 2.0, height: 50.0)
)
if let buttonView = self.buttonView.view {
if buttonView.superview == nil {
@ -1298,10 +1349,41 @@ final class AvatarEditorScreenComponent: Component {
}
transition.setFrame(view: buttonView, frame: CGRect(origin: CGPoint(x: sideInset, y: contentHeight), size: buttonSize))
}
let bottomPanelFrame = CGRect(origin: CGPoint(x: 0.0, y: contentHeight - 4.0), size: CGSize(width: availableSize.width, height: availableSize.height - contentHeight + 4.0))
if let controller = environment.controller(), !controller.automaticallyControlPresentationContextLayout {
let layout = ContainerViewLayout(
size: availableSize,
metrics: environment.metrics,
deviceMetrics: environment.deviceMetrics,
intrinsicInsets: UIEdgeInsets(top: 0.0, left: 0.0, bottom: bottomPanelFrame.height, right: 0.0),
safeInsets: UIEdgeInsets(top: 0.0, left: environment.safeInsets.left, bottom: 0.0, right: environment.safeInsets.right),
additionalInsets: .zero,
statusBarHeight: environment.statusBarHeight,
inputHeight: nil,
inputHeightIsInteractivellyChanging: false,
inVoiceOver: false
)
controller.presentationContext.containerLayoutUpdated(layout, transition: transition.containedViewLayoutTransition)
}
return availableSize
}
private func presentPremiumToast() {
guard let environment = self.environment, let component = self.component, let parentController = environment.controller() else {
return
}
HapticFeedback().impact(.light)
let controller = premiumAlertController(
context: component.context,
parentController: parentController,
text: environment.strings.AvatarEditor_PremiumNeeded_Background
)
parentController.present(controller, in: .window(.root))
}
private let queue = Queue()
func complete() {
guard let state = self.state, let file = state.selectedFile, let controller = self.controller?() else {
@ -1531,6 +1613,9 @@ public final class AvatarEditorScreen: ViewControllerComponentContainer {
let componentReady = Promise<Bool>()
super.init(context: context, component: AvatarEditorScreenComponent(context: context, ready: componentReady, peerType: peerType, markup: markup), navigationBarAppearance: .transparent)
self.automaticallyControlPresentationContextLayout = false
self.navigationPresentation = .modal
self.readyValue.set(componentReady.get() |> timeout(0.3, queue: .mainQueue(), alternate: .single(true)))

@ -10,6 +10,7 @@ import AvatarBackground
final class BackgroundColorComponent: Component {
let theme: PresentationTheme
let isPremium: Bool
let values: [AvatarBackground]
let selectedValue: AvatarBackground
let customValue: AvatarBackground?
@ -18,6 +19,7 @@ final class BackgroundColorComponent: Component {
init(
theme: PresentationTheme,
isPremium: Bool,
values: [AvatarBackground],
selectedValue: AvatarBackground,
customValue: AvatarBackground?,
@ -25,6 +27,7 @@ final class BackgroundColorComponent: Component {
openColorPicker: @escaping () -> Void
) {
self.theme = theme
self.isPremium = isPremium
self.values = values
self.selectedValue = selectedValue
self.customValue = customValue
@ -36,6 +39,9 @@ final class BackgroundColorComponent: Component {
if lhs.theme !== rhs.theme {
return false
}
if lhs.isPremium != rhs.isPremium {
return false
}
if lhs.values != rhs.values {
return false
}
@ -48,25 +54,45 @@ final class BackgroundColorComponent: Component {
return true
}
class View: UIView {
private var views: [Int: ComponentView<Empty>] = [:]
class View: UIView, UIScrollViewDelegate {
private var views: [AnyHashable: ComponentView<Empty>] = [:]
private var scrollView: UIScrollView
private var component: BackgroundColorComponent?
private weak var state: EmptyComponentState?
override init(frame: CGRect) {
super.init(frame: frame)
self.scrollView = UIScrollView()
self.scrollView.contentInsetAdjustmentBehavior = .never
self.scrollView.showsHorizontalScrollIndicator = false
self.scrollView.showsVerticalScrollIndicator = false
super.init(frame: frame)
self.clipsToBounds = true
self.scrollView.delegate = self
self.addSubview(self.scrollView)
self.scrollView.disablesInteractiveTransitionGestureRecognizer = true
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func update(component: BackgroundColorComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: ComponentTransition) -> CGSize {
self.component = component
self.state = state
func scrollViewDidScroll(_ scrollView: UIScrollView) {
self.updateScrolling(transition: .immediate)
}
func updateScrolling(transition: ComponentTransition) {
guard let component = self.component else {
return
}
let itemSize = CGSize(width: 30.0, height: 30.0)
let sideInset: CGFloat = 12.0
let spacing: CGFloat = 13.0
var values: [(AvatarBackground?, Bool)] = component.values.map { ($0, false) }
if let customValue = component.customValue {
@ -75,50 +101,97 @@ final class BackgroundColorComponent: Component {
values.append((nil, true))
}
let itemSize = CGSize(width: 30.0, height: 30.0)
let sideInset: CGFloat = 12.0
let height: CGFloat = 50.0
let delta = floorToScreenPixels((availableSize.width - sideInset * 2.0 - CGFloat(values.count) * itemSize.width) / CGFloat(values.count - 1))
let visibleBounds = self.scrollView.bounds.insetBy(dx: 0.0, dy: -10.0)
var validIds: [AnyHashable] = []
for i in 0 ..< values.count {
let view: ComponentView<Empty>
if let current = self.views[i] {
view = current
} else {
view = ComponentView<Empty>()
self.views[i] = view
let position: CGFloat = sideInset + (spacing + itemSize.width) * CGFloat(i)
let itemFrame = CGRect(origin: CGPoint(x: position, y: 10.0), size: itemSize)
var isVisible = false
if visibleBounds.intersects(itemFrame) {
isVisible = true
}
let itemSize = view.update(
transition: transition,
component: AnyComponent(
BackgroundSwatchComponent(
theme: component.theme,
background: values[i].0,
isCustom: values[i].1,
isSelected: component.selectedValue == values[i].0,
action: {
if let value = values[i].0, component.selectedValue != value {
component.updateValue(value)
} else if values[i].1 {
component.openColorPicker()
}
}
)
),
environment: {},
containerSize: itemSize
)
if let itemView = view.view {
if itemView.superview == nil {
self.addSubview(itemView)
if isVisible {
let itemId = AnyHashable(i)
validIds.append(itemId)
let view: ComponentView<Empty>
if let current = self.views[itemId] {
view = current
} else {
view = ComponentView<Empty>()
self.views[itemId] = view
}
let position: CGFloat = sideInset + (delta + itemSize.width) * CGFloat(i)
transition.setFrame(view: itemView, frame: CGRect(origin: CGPoint(x: position, y: 10.0), size: itemSize))
let _ = view.update(
transition: transition,
component: AnyComponent(
BackgroundSwatchComponent(
theme: component.theme,
background: values[i].0,
isCustom: values[i].1,
isSelected: component.selectedValue == values[i].0,
isLocked: i >= 7 && !values[i].1,
action: {
if let value = values[i].0, component.selectedValue != value {
component.updateValue(value)
} else if values[i].1 {
component.openColorPicker()
}
}
)
),
environment: {},
containerSize: itemSize
)
if let itemView = view.view {
if itemView.superview == nil {
self.scrollView.addSubview(itemView)
}
transition.setFrame(view: itemView, frame: itemFrame)
}
}
}
return CGSize(width: availableSize.width, height: height)
var removeIds: [AnyHashable] = []
for (id, item) in self.views {
if !validIds.contains(id) {
removeIds.append(id)
if let itemView = item.view {
itemView.removeFromSuperview()
}
}
}
for id in removeIds {
self.views.removeValue(forKey: id)
}
}
func update(component: BackgroundColorComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: ComponentTransition) -> CGSize {
self.component = component
self.state = state
let height: CGFloat = 50.0
let size = CGSize(width: availableSize.width, height: height)
let scrollFrame = CGRect(origin: .zero, size: size)
let itemSize = CGSize(width: 30.0, height: 30.0)
let sideInset: CGFloat = 12.0
let spacing: CGFloat = 13.0
let count = component.values.count + 1
let contentSize = CGSize(width: sideInset * 2.0 + CGFloat(count) * itemSize.width + CGFloat(count - 1) * spacing, height: height)
if self.scrollView.frame != scrollFrame {
self.scrollView.frame = scrollFrame
}
if self.scrollView.contentSize != contentSize {
self.scrollView.contentSize = contentSize
}
self.updateScrolling(transition: .immediate)
return size
}
}
@ -164,11 +237,22 @@ private func generateMoreIcon() -> UIImage? {
})
}
private var lockIcon: UIImage? = {
let icon = generateTintedImage(image: UIImage(bundleImageName: "Chat/Stickers/SmallLock"), color: .white)
return generateImage(CGSize(width: 30.0, height: 30.0), contextGenerator: { size, context in
context.clear(CGRect(origin: .zero, size: size))
if let icon, let cgImage = icon.cgImage {
context.draw(cgImage, in: CGRect(origin: CGPoint(x: floorToScreenPixels((size.width - icon.size.width) / 2.0), y: floorToScreenPixels((size.height - icon.size.height) / 2.0)), size: icon.size), byTiling: false)
}
})
}()
final class BackgroundSwatchComponent: Component {
let theme: PresentationTheme
let background: AvatarBackground?
let isCustom: Bool
let isSelected: Bool
let isLocked: Bool
let action: () -> Void
init(
@ -176,17 +260,19 @@ final class BackgroundSwatchComponent: Component {
background: AvatarBackground?,
isCustom: Bool,
isSelected: Bool,
isLocked: Bool,
action: @escaping () -> Void
) {
self.theme = theme
self.background = background
self.isCustom = isCustom
self.isSelected = isSelected
self.isLocked = isLocked
self.action = action
}
static func == (lhs: BackgroundSwatchComponent, rhs: BackgroundSwatchComponent) -> Bool {
return lhs.theme === rhs.theme && lhs.background == rhs.background && lhs.isCustom == rhs.isCustom && lhs.isSelected == rhs.isSelected
return lhs.theme === rhs.theme && lhs.background == rhs.background && lhs.isCustom == rhs.isCustom && lhs.isSelected == rhs.isSelected && lhs.isLocked == rhs.isLocked
}
final class View: UIButton {
@ -283,6 +369,8 @@ final class BackgroundSwatchComponent: Component {
self.iconLayer.contents = generateAddIcon(color: component.theme.list.itemAccentColor)?.cgImage
}
}
} else if component.isLocked {
self.iconLayer.contents = lockIcon?.cgImage
} else {
self.iconLayer.contents = nil
}

@ -1893,7 +1893,7 @@ public final class ChatEmptyNode: ASDisplayNode {
node.layer.animateScale(from: 0.0, to: 1.0, duration: duration, timingFunction: curve.timingFunction)
}
}
self.isUserInteractionEnabled = [.peerNearby, .greeting, .premiumRequired, .cloud].contains(contentType)
self.isUserInteractionEnabled = [.peerNearby, .greeting, .premiumRequired, .starsRequired, .cloud].contains(contentType)
let displayRect = CGRect(origin: CGPoint(x: 0.0, y: insets.top), size: CGSize(width: size.width, height: size.height - insets.top - insets.bottom))

@ -41,12 +41,17 @@ public struct ChatMessageEntryAttributes: Equatable {
}
}
public enum ChatInfoData: Equatable {
case botInfo(title: String, text: String, photo: TelegramMediaImage?, video: TelegramMediaFile?)
case userInfo(title: String, registrationDate: String?, phoneCountry: String?, locationCountry: String?, groupsInCommon: [EnginePeer])
}
public enum ChatHistoryEntry: Identifiable, Comparable {
case MessageEntry(Message, ChatPresentationData, Bool, MessageHistoryEntryLocation?, ChatHistoryMessageSelection, ChatMessageEntryAttributes)
case MessageGroupEntry(Int64, [(Message, Bool, ChatHistoryMessageSelection, ChatMessageEntryAttributes, MessageHistoryEntryLocation?)], ChatPresentationData)
case UnreadEntry(MessageIndex, ChatPresentationData)
case ReplyCountEntry(MessageIndex, Bool, Int, ChatPresentationData)
case ChatInfoEntry(String, String, TelegramMediaImage?, TelegramMediaFile?, ChatPresentationData)
case ChatInfoEntry(ChatInfoData, ChatPresentationData)
case SearchEntry(PresentationTheme, PresentationStrings)
public var stableId: UInt64 {
@ -272,8 +277,8 @@ public enum ChatHistoryEntry: Identifiable, Comparable {
} else {
return false
}
case let .ChatInfoEntry(lhsTitle, lhsText, lhsPhoto, lhsVideo, lhsPresentationData):
if case let .ChatInfoEntry(rhsTitle, rhsText, rhsPhoto, rhsVideo, rhsPresentationData) = rhs, lhsTitle == rhsTitle, lhsText == rhsText, lhsPhoto == rhsPhoto, lhsVideo == rhsVideo, lhsPresentationData === rhsPresentationData {
case let .ChatInfoEntry(lhsData, lhsPresentationData):
if case let .ChatInfoEntry(rhsData, rhsPresentationData) = rhs, lhsData == rhsData, lhsPresentationData === rhsPresentationData {
return true
} else {
return false

@ -1050,7 +1050,7 @@ public final class ChatInputTextView: ChatInputTextViewImpl, UITextViewDelegate,
}
public var toggleQuoteCollapse: ((NSRange) -> Void)?
private let displayInternal: ChatInputTextInternal
private let measureInternal: ChatInputTextInternal
@ -1111,6 +1111,10 @@ public final class ChatInputTextView: ChatInputTextViewImpl, UITextViewDelegate,
self.delegate = self
if #available(iOS 18.0, *) {
self.supportsAdaptiveImageGlyph = false
}
self.displayInternal.updateDisplayElements = { [weak self] in
self?.updateTextElements()
}
@ -1217,7 +1221,7 @@ public final class ChatInputTextView: ChatInputTextViewImpl, UITextViewDelegate,
@objc public func textViewDidChange(_ textView: UITextView) {
self.selectionChangedForEditedText = true
self.updateTextContainerInset()
self.customDelegate?.chatInputTextNodeDidUpdateText()

@ -22,8 +22,8 @@ import TextNodeWithEntities
import ChatMessageBubbleContentNode
import ChatMessageItemCommon
private func attributedServiceMessageString(theme: ChatPresentationThemeData, strings: PresentationStrings, nameDisplayOrder: PresentationPersonNameOrder, dateTimeFormat: PresentationDateTimeFormat, message: Message, accountPeerId: PeerId, forForumOverview: Bool) -> NSAttributedString? {
return universalServiceMessageString(presentationData: (theme.theme, theme.wallpaper), strings: strings, nameDisplayOrder: nameDisplayOrder, dateTimeFormat: dateTimeFormat, message: EngineMessage(message), accountPeerId: accountPeerId, forChatList: false, forForumOverview: forForumOverview)
private func attributedServiceMessageString(theme: ChatPresentationThemeData, strings: PresentationStrings, nameDisplayOrder: PresentationPersonNameOrder, dateTimeFormat: PresentationDateTimeFormat, message: Message, messageCount: Int? = nil, accountPeerId: PeerId, forForumOverview: Bool) -> NSAttributedString? {
return universalServiceMessageString(presentationData: (theme.theme, theme.wallpaper), strings: strings, nameDisplayOrder: nameDisplayOrder, dateTimeFormat: dateTimeFormat, message: EngineMessage(message), messageCount: messageCount, accountPeerId: accountPeerId, forChatList: false, forForumOverview: forForumOverview)
}
public class ChatMessageActionBubbleContentNode: ChatMessageBubbleContentNode {
@ -160,7 +160,12 @@ public class ChatMessageActionBubbleContentNode: ChatMessageBubbleContentNode {
let cachedMaskBackgroundImage = self.cachedMaskBackgroundImage
return { item, layoutConstants, _, _, _, _ in
let contentProperties = ChatMessageBubbleContentProperties(hidesSimpleAuthorHeader: true, headerSpacing: 0.0, hidesBackground: .always, forceFullCorners: false, forceAlignment: .center)
var isDetached = false
if let _ = item.message.paidStarsAttribute {
isDetached = true
}
let contentProperties = ChatMessageBubbleContentProperties(hidesSimpleAuthorHeader: true, headerSpacing: 0.0, hidesBackground: .always, forceFullCorners: false, forceAlignment: .center, isDetached: isDetached)
let backgroundImage = PresentationResourcesChat.chatActionPhotoBackgroundImage(item.presentationData.theme.theme, wallpaper: !item.presentationData.theme.wallpaper.isEmpty)
@ -170,7 +175,12 @@ public class ChatMessageActionBubbleContentNode: ChatMessageBubbleContentNode {
forForumOverview = true
}
let attributedString = attributedServiceMessageString(theme: item.presentationData.theme, strings: item.presentationData.strings, nameDisplayOrder: item.presentationData.nameDisplayOrder, dateTimeFormat: item.presentationData.dateTimeFormat, message: item.message, accountPeerId: item.context.account.peerId, forForumOverview: forForumOverview)
var messageCount: Int = 1
if case let .group(messages) = item.content {
messageCount = messages.count
}
let attributedString = attributedServiceMessageString(theme: item.presentationData.theme, strings: item.presentationData.strings, nameDisplayOrder: item.presentationData.nameDisplayOrder, dateTimeFormat: item.presentationData.dateTimeFormat, message: item.message, messageCount: messageCount, accountPeerId: item.context.account.peerId, forForumOverview: forForumOverview)
var image: TelegramMediaImage?
var story: TelegramMediaStory?

@ -62,7 +62,6 @@ private extension UIBezierPath {
}
private final class ChatMessageActionButtonNode: ASDisplayNode {
//private let backgroundBlurNode: NavigationBackgroundNode
private var backgroundBlurView: PortalView?
private var titleNode: TextNode?
@ -84,15 +83,11 @@ private final class ChatMessageActionButtonNode: ASDisplayNode {
private let accessibilityArea: AccessibilityAreaNode
override init() {
//self.backgroundBlurNode = NavigationBackgroundNode(color: .clear)
//self.backgroundBlurNode.isUserInteractionEnabled = false
self.accessibilityArea = AccessibilityAreaNode()
self.accessibilityArea.accessibilityTraits = .button
super.init()
//self.addSubnode(self.backgroundBlurNode)
self.addSubnode(self.accessibilityArea)
self.accessibilityArea.activate = { [weak self] in

@ -21,6 +21,7 @@ swift_library(
"//submodules/ChatMessageBackground",
"//submodules/TelegramUI/Components/ChatControllerInteraction",
"//submodules/TelegramUI/Components/Chat/ChatHistoryEntry",
"//submodules/TelegramUI/Components/Chat/ChatMessageItem",
"//submodules/TelegramUI/Components/Chat/ChatMessageItemCommon",
],
visibility = [

@ -10,6 +10,7 @@ import AccountContext
import ChatMessageBackground
import ChatControllerInteraction
import ChatHistoryEntry
import ChatMessageItem
import ChatMessageItemCommon
import SwiftSignalKit
@ -180,6 +181,7 @@ public final class ChatMessageBubbleContentItem {
public let controllerInteraction: ChatControllerInteraction
public let message: Message
public let topMessage: Message
public let content: ChatMessageItemContent
public let read: Bool
public let chatLocation: ChatLocation
public let presentationData: ChatPresentationData
@ -188,11 +190,12 @@ public final class ChatMessageBubbleContentItem {
public let isItemPinned: Bool
public let isItemEdited: Bool
public init(context: AccountContext, controllerInteraction: ChatControllerInteraction, message: Message, topMessage: Message, read: Bool, chatLocation: ChatLocation, presentationData: ChatPresentationData, associatedData: ChatMessageItemAssociatedData, attributes: ChatMessageEntryAttributes, isItemPinned: Bool, isItemEdited: Bool) {
public init(context: AccountContext, controllerInteraction: ChatControllerInteraction, message: Message, topMessage: Message, content: ChatMessageItemContent, read: Bool, chatLocation: ChatLocation, presentationData: ChatPresentationData, associatedData: ChatMessageItemAssociatedData, attributes: ChatMessageEntryAttributes, isItemPinned: Bool, isItemEdited: Bool) {
self.context = context
self.controllerInteraction = controllerInteraction
self.message = message
self.topMessage = topMessage
self.content = content
self.read = read
self.chatLocation = chatLocation
self.presentationData = presentationData

@ -121,13 +121,18 @@ private func contentNodeMessagesAndClassesForItem(_ item: ChatMessageItem) -> ([
let hideAllAdditionalInfo = item.presentationData.isPreview
var hasSeparateCommentsButton = false
var addedPriceInfo = false
outer: for (message, itemAttributes) in item.content {
for attribute in message.attributes {
if let attribute = attribute as? RestrictedContentMessageAttribute, attribute.platformText(platform: "ios", contentSettings: item.context.currentContentSettings.with { $0 }) != nil {
result.append((message, ChatMessageRestrictedBubbleContentNode.self, itemAttributes, BubbleItemAttributes(isAttachment: false, neighborType: .text, neighborSpacing: .default)))
needReactions = false
break outer
} else if let _ = attribute as? PaidStarsMessageAttribute, !addedPriceInfo {
result.append((message, ChatMessageActionBubbleContentNode.self, itemAttributes, BubbleItemAttributes(isAttachment: false, neighborType: .text, neighborSpacing: .default)))
addedPriceInfo = true
}
}
@ -1791,7 +1796,7 @@ public class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewI
var addedContentNodes: [(Message, Bool, ChatMessageBubbleContentNode, Int?)]?
for contentNodeItemValue in contentNodeMessagesAndClasses {
let contentNodeItem = contentNodeItemValue as (message: Message, type: AnyClass, attributes: ChatMessageEntryAttributes, bubbleAttributes: BubbleItemAttributes)
var found = false
for currentNodeItemValue in currentContentClassesPropertiesAndLayouts {
let currentNodeItem = currentNodeItemValue as (message: Message, type: AnyClass, supportsMosaic: Bool, index: Int?, currentLayout: (ChatMessageBubbleContentItem, ChatMessageItemLayoutConstants, ChatMessageBubblePreparePosition, Bool?, CGSize, CGFloat) -> (ChatMessageBubbleContentProperties, CGSize?, CGFloat, (CGSize, ChatMessageBubbleContentPosition) -> (CGFloat, (CGFloat) -> (CGSize, (ListViewItemUpdateAnimation, Bool, ListViewItemApply?) -> Void))))
@ -1803,7 +1808,7 @@ public class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewI
}
}
if !found {
let contentNode = (contentNodeItem.type as! ChatMessageBubbleContentNode.Type).init()
let contentNode = (contentNodeItem.type as! ChatMessageBubbleContentNode.Type).init()
contentNode.index = contentNodeItem.bubbleAttributes.index
contentPropertiesAndPrepareLayouts.append((contentNodeItem.message, contentNode.supportsMosaic, contentNodeItem.attributes, contentNodeItem.bubbleAttributes, contentNode.asyncLayoutContent()))
if addedContentNodes == nil {
@ -2028,7 +2033,7 @@ public class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewI
prepareContentPosition = .linear(top: topPosition, bottom: refinedBottomPosition)
}
let contentItem = ChatMessageBubbleContentItem(context: item.context, controllerInteraction: item.controllerInteraction, message: message, topMessage: item.content.firstMessage, read: read, chatLocation: item.chatLocation, presentationData: item.presentationData, associatedData: item.associatedData, attributes: attributes, isItemPinned: isItemPinned, isItemEdited: isItemEdited)
let contentItem = ChatMessageBubbleContentItem(context: item.context, controllerInteraction: item.controllerInteraction, message: message, topMessage: item.content.firstMessage, content: item.content, read: read, chatLocation: item.chatLocation, presentationData: item.presentationData, associatedData: item.associatedData, attributes: attributes, isItemPinned: isItemPinned, isItemEdited: isItemEdited)
var itemSelection: Bool?
switch content {
@ -2066,22 +2071,24 @@ public class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewI
contentPropertiesAndLayouts.append((unboundSize, properties, prepareContentPosition, bubbleAttributes, nodeLayout, needSeparateContainers && !bubbleAttributes.isAttachment ? message.stableId : nil, itemSelection))
switch properties.hidesBackground {
case .never:
backgroundHiding = .never
case .emptyWallpaper:
if backgroundHiding == nil {
backgroundHiding = properties.hidesBackground
}
case .always:
backgroundHiding = .always
}
if !properties.isDetached {
switch properties.hidesBackground {
case .never:
backgroundHiding = .never
case .emptyWallpaper:
if backgroundHiding == nil {
backgroundHiding = properties.hidesBackground
}
case .always:
backgroundHiding = .always
}
switch properties.forceAlignment {
switch properties.forceAlignment {
case .none:
break
case .center:
alignment = .center
}
}
index += 1
@ -2870,7 +2877,7 @@ public class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewI
bottomBubbleAttributes = contentPropertiesAndLayouts[i + 1].3
}
if i == 0 {
if i == 0 || (i == 1 && contentPropertiesAndLayouts[0].1.isDetached) {
topPosition = firstNodeTopPosition
} else {
topPosition = .Neighbour(topBubbleAttributes.isAttachment, topBubbleAttributes.neighborType, topBubbleAttributes.neighborSpacing)
@ -2933,15 +2940,20 @@ public class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewI
if let mosaicRange = mosaicRange, mosaicRange.contains(i), let (framesAndPositions, size) = calculatedGroupFramesAndSize {
let mosaicIndex = i - mosaicRange.lowerBound
if mosaicIndex == 0 && i == 0 {
if mosaicIndex == 0 && (i == 0 || (i == 1 && detachedContentNodesHeight > 0)) {
if !headerSize.height.isZero {
contentNodesHeight += 7.0
totalContentNodesHeight += 7.0
}
}
var contentNodeOriginY = contentNodesHeight
if detachedContentNodesHeight > 0 {
contentNodeOriginY -= detachedContentNodesHeight - 4.0
}
let (_, apply) = finalize(maxContentWidth)
let contentNodeFrame = framesAndPositions[mosaicIndex].0.offsetBy(dx: 0.0, dy: contentNodesHeight)
let contentNodeFrame = framesAndPositions[mosaicIndex].0.offsetBy(dx: 0.0, dy: contentNodeOriginY)
contentNodeFramesPropertiesAndApply.append((contentNodeFrame, properties, true, apply))
if i == mosaicRange.upperBound - 1 {
@ -2950,7 +2962,7 @@ public class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewI
contentNodesHeight += size.height
totalContentNodesHeight += size.height
mosaicStatusOrigin = contentNodeFrame.bottomRight
}
} else {
@ -2984,7 +2996,11 @@ public class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewI
currentItemSelection = itemSelection
}
let contentNodeOriginY = contentNodesHeight - detachedContentNodesHeight
var contentNodeOriginY = contentNodesHeight
if detachedContentNodesHeight > 0 {
contentNodeOriginY -= detachedContentNodesHeight - 4.0
}
let (size, apply) = finalize(maxContentWidth)
let containerFrame = CGRect(origin: CGPoint(x: 0.0, y: contentNodeOriginY), size: size)
contentNodeFramesPropertiesAndApply.append((containerFrame, properties, contentGroupId == nil, apply))
@ -2998,7 +3014,8 @@ public class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewI
totalContentNodesHeight += size.height
if properties.isDetached {
detachedContentNodesHeight += size.height
detachedContentNodesHeight += size.height + 4.0
totalContentNodesHeight += 4.0
}
}
}
@ -3160,7 +3177,7 @@ public class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewI
nameNodeSizeApply: nameNodeSizeApply,
viaWidth: viaWidth,
contentOrigin: contentOrigin,
nameNodeOriginY: nameNodeOriginY,
nameNodeOriginY: nameNodeOriginY + detachedContentNodesHeight,
authorNameColor: authorNameColor,
layoutConstants: layoutConstants,
currentCredibilityIcon: currentCredibilityIcon,
@ -3168,11 +3185,11 @@ public class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewI
boostNodeSizeApply: boostNodeSizeApply,
contentUpperRightCorner: contentUpperRightCorner,
threadInfoSizeApply: threadInfoSizeApply,
threadInfoOriginY: threadInfoOriginY,
threadInfoOriginY: threadInfoOriginY + detachedContentNodesHeight,
forwardInfoSizeApply: forwardInfoSizeApply,
forwardInfoOriginY: forwardInfoOriginY,
forwardInfoOriginY: forwardInfoOriginY + detachedContentNodesHeight,
replyInfoSizeApply: replyInfoSizeApply,
replyInfoOriginY: replyInfoOriginY,
replyInfoOriginY: replyInfoOriginY + detachedContentNodesHeight,
removedContentNodeIndices: removedContentNodeIndices,
updatedContentNodeOrder: updatedContentNodeOrder,
addedContentNodes: addedContentNodes,
@ -4049,32 +4066,33 @@ public class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewI
}
if let addedContentNodes = addedContentNodes {
for (contentNodeMessage, isAttachment, contentNode, _ ) in addedContentNodes {
for (contentNodeMessage, isAttachment, contentNode, _) in addedContentNodes {
let index = updatedContentNodes.count
updatedContentNodes.append(contentNode)
let contextSourceNode: ContextExtractedContentContainingNode
let containerSupernode: ASDisplayNode
if isAttachment {
contextSourceNode = strongSelf.mainContextSourceNode
containerSupernode = strongSelf.clippingNode
if index < contentNodeFramesPropertiesAndApply.count && contentNodeFramesPropertiesAndApply[index].1.isDetached {
strongSelf.addSubnode(contentNode)
} else {
contextSourceNode = strongSelf.contentContainers.first(where: { $0.contentMessageStableId == contentNodeMessage.stableId })?.sourceNode ?? strongSelf.mainContextSourceNode
containerSupernode = strongSelf.contentContainers.first(where: { $0.contentMessageStableId == contentNodeMessage.stableId })?.sourceNode.contentNode ?? strongSelf.clippingNode
let contextSourceNode: ContextExtractedContentContainingNode
let containerSupernode: ASDisplayNode
if isAttachment {
contextSourceNode = strongSelf.mainContextSourceNode
containerSupernode = strongSelf.clippingNode
} else {
contextSourceNode = strongSelf.contentContainers.first(where: { $0.contentMessageStableId == contentNodeMessage.stableId })?.sourceNode ?? strongSelf.mainContextSourceNode
containerSupernode = strongSelf.contentContainers.first(where: { $0.contentMessageStableId == contentNodeMessage.stableId })?.sourceNode.contentNode ?? strongSelf.clippingNode
}
containerSupernode.addSubnode(contentNode)
contentNode.updateIsTextSelectionActive = { [weak contextSourceNode] value in
contextSourceNode?.updateDistractionFreeMode?(value)
}
contentNode.updateIsExtractedToContextPreview(contextSourceNode.isExtractedToContextPreview)
}
#if DEBUG && false
contentNode.layer.borderColor = UIColor(white: 0.0, alpha: 0.2).cgColor
contentNode.layer.borderWidth = 1.0
#endif
containerSupernode.addSubnode(contentNode)
contentNode.itemNode = strongSelf
contentNode.bubbleBackgroundNode = strongSelf.backgroundNode
contentNode.bubbleBackdropNode = strongSelf.backgroundWallpaperNode
contentNode.updateIsTextSelectionActive = { [weak contextSourceNode] value in
contextSourceNode?.updateDistractionFreeMode?(value)
}
contentNode.requestInlineUpdate = { [weak strongSelf] in
guard let strongSelf else {
return
@ -4082,7 +4100,6 @@ public class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewI
strongSelf.internalUpdateLayout()
}
contentNode.updateIsExtractedToContextPreview(contextSourceNode.isExtractedToContextPreview)
}
}

@ -273,7 +273,7 @@ public class ChatMessageInstantVideoBubbleContentNode: ChatMessageBubbleContentN
let leftInset: CGFloat = 0.0
let rightInset: CGFloat = 0.0
let (videoLayout, videoApply) = interactiveVideoLayout(ChatMessageBubbleContentItem(context: item.context, controllerInteraction: item.controllerInteraction, message: item.message, topMessage: item.message, read: item.read, chatLocation: item.chatLocation, presentationData: item.presentationData, associatedData: item.associatedData, attributes: item.attributes, isItemPinned: item.message.tags.contains(.pinned) && !isReplyThread, isItemEdited: false), constrainedSize.width - leftInset - rightInset - avatarInset, displaySize, maximumDisplaySize, isPlaying ? 1.0 : 0.0, .free, automaticDownload, avatarInset)
let (videoLayout, videoApply) = interactiveVideoLayout(ChatMessageBubbleContentItem(context: item.context, controllerInteraction: item.controllerInteraction, message: item.message, topMessage: item.message, content: item.content, read: item.read, chatLocation: item.chatLocation, presentationData: item.presentationData, associatedData: item.associatedData, attributes: item.attributes, isItemPinned: item.message.tags.contains(.pinned) && !isReplyThread, isItemEdited: false), constrainedSize.width - leftInset - rightInset - avatarInset, displaySize, maximumDisplaySize, isPlaying ? 1.0 : 0.0, .free, automaticDownload, avatarInset)
let videoFrame = CGRect(origin: CGPoint(x: 1.0, y: 1.0), size: videoLayout.contentSize)

@ -423,7 +423,7 @@ public class ChatMessageInstantVideoItemNode: ChatMessageItemView, ASGestureReco
isReplyThread = true
}
let (videoLayout, videoApply) = makeVideoLayout(ChatMessageBubbleContentItem(context: item.context, controllerInteraction: item.controllerInteraction, message: item.message, topMessage: item.content.firstMessage, read: item.read, chatLocation: item.chatLocation, presentationData: item.presentationData, associatedData: item.associatedData, attributes: item.content.firstMessageAttributes, isItemPinned: item.message.tags.contains(.pinned) && !isReplyThread, isItemEdited: false), params.width - params.leftInset - params.rightInset - avatarInset, displaySize, maximumDisplaySize, isPlaying ? 1.0 : 0.0, .free, automaticDownload, 0.0)
let (videoLayout, videoApply) = makeVideoLayout(ChatMessageBubbleContentItem(context: item.context, controllerInteraction: item.controllerInteraction, message: item.message, topMessage: item.content.firstMessage, content: item.content, read: item.read, chatLocation: item.chatLocation, presentationData: item.presentationData, associatedData: item.associatedData, attributes: item.content.firstMessageAttributes, isItemPinned: item.message.tags.contains(.pinned) && !isReplyThread, isItemEdited: false), params.width - params.leftInset - params.rightInset - avatarInset, displaySize, maximumDisplaySize, isPlaying ? 1.0 : 0.0, .free, automaticDownload, 0.0)
let videoFrame = CGRect(origin: CGPoint(x: (incoming ? (params.leftInset + layoutConstants.bubble.edgeInset + effectiveAvatarInset + layoutConstants.bubble.contentInsets.left) : (params.width - params.rightInset - videoLayout.contentSize.width - layoutConstants.bubble.edgeInset - layoutConstants.bubble.contentInsets.left - deliveryFailedInset)), y: 0.0), size: videoLayout.contentSize)
@ -1373,7 +1373,7 @@ public class ChatMessageInstantVideoItemNode: ChatMessageItemView, ASGestureReco
effectiveAvatarInset *= (1.0 - scaleProgress)
displaySize = CGSize(width: initialSize.width + (targetSize.width - initialSize.width) * animationProgress, height: initialSize.height + (targetSize.height - initialSize.height) * animationProgress)
let (videoLayout, videoApply) = makeVideoLayout(ChatMessageBubbleContentItem(context: item.context, controllerInteraction: item.controllerInteraction, message: item.message, topMessage: item.message, read: item.read, chatLocation: item.chatLocation, presentationData: item.presentationData, associatedData: item.associatedData, attributes: item.content.firstMessageAttributes, isItemPinned: item.message.tags.contains(.pinned) && !isReplyThread, isItemEdited: false), params.width - params.leftInset - params.rightInset - avatarInset, displaySize, maximumDisplaySize, scaleProgress, .free, self.appliedAutomaticDownload, 0.0)
let (videoLayout, videoApply) = makeVideoLayout(ChatMessageBubbleContentItem(context: item.context, controllerInteraction: item.controllerInteraction, message: item.message, topMessage: item.message, content: item.content, read: item.read, chatLocation: item.chatLocation, presentationData: item.presentationData, associatedData: item.associatedData, attributes: item.content.firstMessageAttributes, isItemPinned: item.message.tags.contains(.pinned) && !isReplyThread, isItemEdited: false), params.width - params.leftInset - params.rightInset - avatarInset, displaySize, maximumDisplaySize, scaleProgress, .free, self.appliedAutomaticDownload, 0.0)
let availableContentWidth = params.width - params.leftInset - params.rightInset - layoutConstants.bubble.edgeInset * 2.0 - avatarInset - layoutConstants.bubble.contentInsets.left
let videoFrame = CGRect(origin: CGPoint(x: (incoming ? (params.leftInset + layoutConstants.bubble.edgeInset + effectiveAvatarInset + layoutConstants.bubble.contentInsets.left) : (params.width - params.rightInset - videoLayout.contentSize.width - layoutConstants.bubble.edgeInset - layoutConstants.bubble.contentInsets.left - deliveryFailedInset)), y: 0.0), size: videoLayout.contentSize)

@ -2174,6 +2174,7 @@ public final class ChatMessageInteractiveMediaNode: ASDisplayNode, GalleryItemTr
GiftItemComponent(
context: context,
theme: presentationData.theme.theme,
strings: presentationData.strings,
subject: .uniqueGift(gift: gift),
mode: .preview
)

@ -87,6 +87,11 @@ private func messagesShouldBeMerged(accountPeerId: PeerId, _ lhs: Message, _ rhs
sameChat = false
}
var isPaid = false
if let _ = lhs.paidStarsAttribute, let _ = rhs.paidStarsAttribute {
isPaid = true
}
var sameThread = true
if let lhsPeer = lhs.peers[lhs.id.peerId], let rhsPeer = rhs.peers[rhs.id.peerId], arePeersEqual(lhsPeer, rhsPeer), let channel = lhsPeer as? TelegramChannel, channel.flags.contains(.isForum), lhs.threadId != rhs.threadId {
sameThread = false
@ -136,7 +141,7 @@ private func messagesShouldBeMerged(accountPeerId: PeerId, _ lhs: Message, _ rhs
}
}
if abs(lhsEffectiveTimestamp - rhsEffectiveTimestamp) < Int32(10 * 60) && sameChat && sameAuthor && sameThread {
if abs(lhsEffectiveTimestamp - rhsEffectiveTimestamp) < Int32(10 * 60) && sameChat && sameAuthor && sameThread && !isPaid {
if let channel = lhs.peers[lhs.id.peerId] as? TelegramChannel, case .group = channel.info, lhsEffectiveAuthor?.id == channel.id, !lhs.effectivelyIncoming(accountPeerId) {
return .none
}

@ -0,0 +1,33 @@
load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library")
swift_library(
name = "ChatMessagePaymentAlertController",
module_name = "ChatMessagePaymentAlertController",
srcs = glob([
"Sources/**/*.swift",
]),
copts = [
"-warnings-as-errors",
],
deps = [
"//submodules/SSignalKit/SwiftSignalKit:SwiftSignalKit",
"//submodules/AsyncDisplayKit:AsyncDisplayKit",
"//submodules/Display:Display",
"//submodules/Postbox:Postbox",
"//submodules/TelegramCore:TelegramCore",
"//submodules/AccountContext:AccountContext",
"//submodules/TelegramPresentationData:TelegramPresentationData",
"//submodules/ComponentFlow",
"//submodules/Components/MultilineTextComponent",
"//submodules/Components/ComponentDisplayAdapters",
"//submodules/TextFormat",
"//submodules/AvatarNode",
"//submodules/CheckNode",
"//submodules/TelegramUIPreferences",
"//submodules/TelegramUI/Components/Stars/StarsBalanceOverlayComponent",
"//submodules/Markdown",
],
visibility = [
"//visibility:public",
],
)

@ -55,7 +55,7 @@ private final class ChatMessagePaymentAlertContentNode: AlertContentNode, ASGest
var openTerms: () -> Void = {}
init(context: AccountContext, theme: AlertControllerTheme, ptheme: PresentationTheme, strings: PresentationStrings, title: String, text: String, optionText: String?, actions: [TextAlertAction], alignment: TextAlertContentActionLayout) {
init(theme: AlertControllerTheme, ptheme: PresentationTheme, strings: PresentationStrings, title: String, text: String, optionText: String?, actions: [TextAlertAction], alignment: TextAlertContentActionLayout) {
self.strings = strings
self.title = title
self.text = text
@ -318,94 +318,143 @@ private final class ChatMessagePaymentAlertContentNode: AlertContentNode, ASGest
}
private class ChatMessagePaymentAlertController: AlertController {
private let context: AccountContext
private let context: AccountContext?
private let presentationData: PresentationData
private weak var parentNavigationController: NavigationController?
private let balance = ComponentView<Empty>()
init(context: AccountContext, presentationData: PresentationData, contentNode: AlertContentNode) {
init(context: AccountContext?, presentationData: PresentationData, contentNode: AlertContentNode, navigationController: NavigationController?) {
self.context = context
self.presentationData = presentationData
self.parentNavigationController = navigationController
super.init(theme: AlertControllerTheme(presentationData: presentationData), contentNode: contentNode)
self.willDismiss = { [weak self] in
guard let self else {
return
}
self.animateOut()
}
}
required public init(coder aDecoder: NSCoder) {
preconditionFailure()
}
override func dismissAnimated() {
super.dismissAnimated()
private func animateOut() {
if let view = self.balance.view {
view.layer.animateScale(from: 1.0, to: 0.8, duration: 0.4, removeOnCompletion: false)
view.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3, removeOnCompletion: false)
}
}
override func dismissAnimated() {
super.dismissAnimated()
self.animateOut()
}
override func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) {
super.containerLayoutUpdated(layout, transition: transition)
let insets = layout.insets(options: .statusBar)
let balanceSize = self.balance.update(
transition: .immediate,
component: AnyComponent(
StarsBalanceOverlayComponent(
context: self.context,
theme: self.presentationData.theme,
action: {
}
)
),
environment: {},
containerSize: layout.size
)
if let view = self.balance.view {
if view.superview == nil {
self.view.addSubview(view)
view.layer.animatePosition(from: CGPoint(x: 0.0, y: -64.0), to: .zero, duration: 0.4, timingFunction: kCAMediaTimingFunctionSpring, additive: true)
view.layer.animateSpring(from: 0.8 as NSNumber, to: 1.0 as NSNumber, keyPath: "transform.scale", duration: 0.5, initialVelocity: 0.0, removeOnCompletion: true, additive: false, completion: nil)
view.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.25)
if let context = self.context, let _ = self.parentNavigationController {
let insets = layout.insets(options: .statusBar)
let balanceSize = self.balance.update(
transition: .immediate,
component: AnyComponent(
StarsBalanceOverlayComponent(
context: context,
theme: self.presentationData.theme,
action: { [weak self] in
guard let self, let starsContext = context.starsContext, let navigationController = self.parentNavigationController else {
return
}
self.dismissAnimated()
let _ = (context.engine.payments.starsTopUpOptions()
|> take(1)
|> deliverOnMainQueue).startStandalone(next: { options in
let controller = context.sharedContext.makeStarsPurchaseScreen(
context: context,
starsContext: starsContext,
options: options,
purpose: .generic,
completion: { _ in }
)
navigationController.pushViewController(controller)
})
}
)
),
environment: {},
containerSize: layout.size
)
if let view = self.balance.view {
if view.superview == nil {
self.view.addSubview(view)
view.layer.animatePosition(from: CGPoint(x: 0.0, y: -64.0), to: .zero, duration: 0.4, timingFunction: kCAMediaTimingFunctionSpring, additive: true)
view.layer.animateSpring(from: 0.8 as NSNumber, to: 1.0 as NSNumber, keyPath: "transform.scale", duration: 0.5, initialVelocity: 0.0, removeOnCompletion: true, additive: false, completion: nil)
view.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.25)
}
view.frame = CGRect(origin: CGPoint(x: floorToScreenPixels((layout.size.width - balanceSize.width) / 2.0), y: insets.top + 5.0), size: balanceSize)
}
view.frame = CGRect(origin: CGPoint(x: floorToScreenPixels((layout.size.width - balanceSize.width) / 2.0), y: insets.top + 5.0), size: balanceSize)
}
}
}
public func chatMessagePaymentAlertController(
context: AccountContext,
context: AccountContext?,
presentationData: PresentationData,
updatedPresentationData: (initial: PresentationData, signal: Signal<PresentationData, NoError>)?,
peer: EnginePeer,
peers: [EnginePeer],
count: Int32,
amount: StarsAmount,
totalAmount: StarsAmount?,
hasCheck: Bool = true,
navigationController: NavigationController?,
completion: @escaping (Bool) -> Void
) -> AlertController {
let theme = defaultDarkColorPresentationTheme
let presentationData: PresentationData
if let updatedPresentationData {
presentationData = updatedPresentationData.initial
} else {
presentationData = context.sharedContext.currentPresentationData.with { $0 }
}
let presentationData = updatedPresentationData?.initial ?? presentationData
let strings = presentationData.strings
var completionImpl: (() -> Void)?
var dismissImpl: (() -> Void)?
//TODO:localize
let actions: [TextAlertAction] = [TextAlertAction(type: .defaultAction, title: "Pay for 1 Message", action: {
let title = "Confirm Payment"
let actionTitle: String
let messagesString: String
if count > 1 {
messagesString = "**\(count)** messages"
actionTitle = "Pay for \(count) Messages"
} else {
messagesString = "**\(count)** message"
actionTitle = "Pay for 1 Message"
}
let actions: [TextAlertAction] = [TextAlertAction(type: .defaultAction, title: actionTitle, action: {
completionImpl?()
dismissImpl?()
}), TextAlertAction(type: .genericAction, title: presentationData.strings.Common_Cancel, action: {
dismissImpl?()
})]
let title = "Confirm Payment"
let text = "**\(peer.compactDisplayTitle)** charges **\(amount.value) Stars** per incoming message. Would you like to pay **\(amount.value) Stars** to send one message?"
let optionText = "Don't ask again"
let text: String
if peers.count == 1, let peer = peers.first {
text = "**\(peer.compactDisplayTitle)** charges **\(amount.value) Stars** per incoming message. Would you like to pay **\(amount.value * Int64(count)) Stars** to send \(messagesString)?"
} else {
let amount = totalAmount ?? amount
text = "You selected **\(peers.count)** users who charge Stars for messages. Would you like to pay **\(amount.value)** Stars to send \(messagesString)?"
}
let contentNode = ChatMessagePaymentAlertContentNode(context: context, theme: AlertControllerTheme(presentationData: presentationData), ptheme: theme, strings: strings, title: title, text: text, optionText: optionText, actions: actions, alignment: .vertical)
let optionText = hasCheck ? "Don't ask again" : nil
let contentNode = ChatMessagePaymentAlertContentNode(theme: AlertControllerTheme(presentationData: presentationData), ptheme: theme, strings: strings, title: title, text: text, optionText: optionText, actions: actions, alignment: .vertical)
completionImpl = { [weak contentNode] in
guard let contentNode else {
@ -414,7 +463,7 @@ public func chatMessagePaymentAlertController(
completion(contentNode.dontAskAgain)
}
let controller = ChatMessagePaymentAlertController(context: context, presentationData: presentationData, contentNode: contentNode)
let controller = ChatMessagePaymentAlertController(context: context, presentationData: presentationData, contentNode: contentNode, navigationController: navigationController)
dismissImpl = { [weak controller] in
controller?.dismissAnimated()
}
@ -424,19 +473,16 @@ public func chatMessagePaymentAlertController(
public func chatMessageRemovePaymentAlertController(
context: AccountContext,
context: AccountContext? = nil,
presentationData: PresentationData,
updatedPresentationData: (initial: PresentationData, signal: Signal<PresentationData, NoError>)?,
peer: EnginePeer,
amount: StarsAmount?,
navigationController: NavigationController?,
completion: @escaping (Bool) -> Void
) -> AlertController {
let theme = defaultDarkColorPresentationTheme
let presentationData: PresentationData
if let updatedPresentationData {
presentationData = updatedPresentationData.initial
} else {
presentationData = context.sharedContext.currentPresentationData.with { $0 }
}
let presentationData = updatedPresentationData?.initial ?? presentationData
let strings = presentationData.strings
var completionImpl: (() -> Void)?
@ -457,7 +503,7 @@ public func chatMessageRemovePaymentAlertController(
let text = "Are you sure you want to allow **\(peer.compactDisplayTitle)** to message you for free?"
let optionText = amount.flatMap { "Refund already paid **\($0.value) Stars**" }
let contentNode = ChatMessagePaymentAlertContentNode(context: context, theme: AlertControllerTheme(presentationData: presentationData), ptheme: theme, strings: strings, title: title, text: text, optionText: optionText, actions: actions, alignment: .horizontal)
let contentNode = ChatMessagePaymentAlertContentNode(theme: AlertControllerTheme(presentationData: presentationData), ptheme: theme, strings: strings, title: title, text: text, optionText: optionText, actions: actions, alignment: .horizontal)
completionImpl = { [weak contentNode] in
guard let contentNode else {
@ -466,7 +512,7 @@ public func chatMessageRemovePaymentAlertController(
completion(contentNode.dontAskAgain)
}
let controller = ChatMessagePaymentAlertController(context: context, presentationData: presentationData, contentNode: contentNode)
let controller = ChatMessagePaymentAlertController(context: context, presentationData: presentationData, contentNode: contentNode, navigationController: navigationController)
dismissImpl = { [weak controller] in
controller?.dismissAnimated()
}

@ -523,6 +523,7 @@ public final class ChatSendGroupMediaMessageContextPreview: UIView, ChatSendMess
controllerInteraction: controllerInteraction,
message: message,
topMessage: message,
content: .message(message: message, read: true, selection: .none, attributes: entryAttributes, location: nil),
read: true,
chatLocation: .peer(id: self.context.account.peerId),
presentationData: chatPresentationData,

@ -0,0 +1,34 @@
load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library")
swift_library(
name = "ChatUserInfoItem",
module_name = "ChatUserInfoItem",
srcs = glob([
"Sources/**/*.swift",
]),
copts = [
"-warnings-as-errors",
],
deps = [
"//submodules/Display",
"//submodules/AsyncDisplayKit",
"//submodules/SSignalKit/SwiftSignalKit",
"//submodules/Postbox",
"//submodules/TelegramCore",
"//submodules/TelegramPresentationData",
"//submodules/TextFormat",
"//submodules/UrlEscaping",
"//submodules/PhotoResources",
"//submodules/AccountContext",
"//submodules/MediaPlayer:UniversalMediaPlayer",
"//submodules/TelegramUniversalVideoContent",
"//submodules/WallpaperBackgroundNode",
"//submodules/TelegramUI/Components/ChatControllerInteraction",
"//submodules/TelegramUI/Components/Chat/ChatMessageBubbleContentNode",
"//submodules/CountrySelectionUI",
"//submodules/TelegramStringFormatting",
],
visibility = [
"//visibility:public",
],
)

@ -0,0 +1,589 @@
import Foundation
import UIKit
import Display
import AsyncDisplayKit
import SwiftSignalKit
import Postbox
import TelegramCore
import TelegramPresentationData
import TextFormat
import UrlEscaping
import PhotoResources
import AccountContext
import UniversalMediaPlayer
import TelegramUniversalVideoContent
import WallpaperBackgroundNode
import ChatControllerInteraction
import ChatMessageBubbleContentNode
import CountrySelectionUI
import TelegramStringFormatting
public final class ChatUserInfoItem: ListViewItem {
fileprivate let title: String
fileprivate let registrationDate: String?
fileprivate let phoneCountry: String?
fileprivate let locationCountry: String?
fileprivate let groupsInCommon: [EnginePeer]
fileprivate let controllerInteraction: ChatControllerInteraction
fileprivate let presentationData: ChatPresentationData
fileprivate let context: AccountContext
public init(
title: String,
registrationDate: String?,
phoneCountry: String?,
locationCountry: String?,
groupsInCommon: [EnginePeer],
controllerInteraction: ChatControllerInteraction,
presentationData: ChatPresentationData,
context: AccountContext
) {
self.title = title
self.registrationDate = registrationDate
self.phoneCountry = phoneCountry
self.locationCountry = locationCountry
self.groupsInCommon = groupsInCommon
self.controllerInteraction = controllerInteraction
self.presentationData = presentationData
self.context = context
}
public func nodeConfiguredForParams(async: @escaping (@escaping () -> Void) -> Void, params: ListViewItemLayoutParams, synchronousLoads: Bool, previousItem: ListViewItem?, nextItem: ListViewItem?, completion: @escaping (ListViewItemNode, @escaping () -> (Signal<Void, NoError>?, (ListViewItemApply) -> Void)) -> Void) {
let configure = {
let node = ChatUserInfoItemNode()
let nodeLayout = node.asyncLayout()
let (layout, apply) = nodeLayout(self, params)
node.contentSize = layout.contentSize
node.insets = layout.insets
Queue.mainQueue().async {
completion(node, {
return (nil, { _ in apply(.None) })
})
}
}
if Thread.isMainThread {
async {
configure()
}
} else {
configure()
}
}
public func updateNode(async: @escaping (@escaping () -> Void) -> Void, node: @escaping () -> ListViewItemNode, params: ListViewItemLayoutParams, previousItem: ListViewItem?, nextItem: ListViewItem?, animation: ListViewItemUpdateAnimation, completion: @escaping (ListViewItemNodeLayout, @escaping (ListViewItemApply) -> Void) -> Void) {
Queue.mainQueue().async {
if let nodeValue = node() as? ChatUserInfoItemNode {
let nodeLayout = nodeValue.asyncLayout()
async {
let (layout, apply) = nodeLayout(self, params)
Queue.mainQueue().async {
completion(layout, { _ in
apply(animation)
})
}
}
}
}
}
}
public final class ChatUserInfoItemNode: ListViewItemNode {
public var controllerInteraction: ChatControllerInteraction?
public let offsetContainer: ASDisplayNode
public let titleNode: TextNode
public let subtitleNode: TextNode
private let registrationDateTitleTextNode: TextNode
private let registrationDateValueTextNode: TextNode
private var registrationDateText: String?
private let phoneCountryTitleTextNode: TextNode
private let phoneCountryValueTextNode: TextNode
private var phoneCountryText: String?
private let locationCountryTitleTextNode: TextNode
private let locationCountryValueTextNode: TextNode
private var locationCountryText: String?
private let groupsTextNode: TextNode
private var theme: ChatPresentationThemeData?
private var wallpaperBackgroundNode: WallpaperBackgroundNode?
private var backgroundContent: WallpaperBubbleBackgroundNode?
private var absolutePosition: (CGRect, CGSize)?
private var item: ChatUserInfoItem?
public init() {
self.offsetContainer = ASDisplayNode()
self.titleNode = TextNode()
self.titleNode.isUserInteractionEnabled = false
self.titleNode.displaysAsynchronously = false
self.subtitleNode = TextNode()
self.subtitleNode.isUserInteractionEnabled = false
self.subtitleNode.displaysAsynchronously = false
self.registrationDateTitleTextNode = TextNode()
self.registrationDateTitleTextNode.isUserInteractionEnabled = false
self.registrationDateTitleTextNode.displaysAsynchronously = false
self.registrationDateValueTextNode = TextNode()
self.registrationDateValueTextNode.isUserInteractionEnabled = false
self.registrationDateValueTextNode.displaysAsynchronously = false
self.phoneCountryTitleTextNode = TextNode()
self.phoneCountryTitleTextNode.isUserInteractionEnabled = false
self.phoneCountryTitleTextNode.displaysAsynchronously = false
self.phoneCountryValueTextNode = TextNode()
self.phoneCountryValueTextNode.isUserInteractionEnabled = false
self.phoneCountryValueTextNode.displaysAsynchronously = false
self.locationCountryTitleTextNode = TextNode()
self.locationCountryTitleTextNode.isUserInteractionEnabled = false
self.locationCountryTitleTextNode.displaysAsynchronously = false
self.locationCountryValueTextNode = TextNode()
self.locationCountryValueTextNode.isUserInteractionEnabled = false
self.locationCountryValueTextNode.displaysAsynchronously = false
self.groupsTextNode = TextNode()
self.groupsTextNode.isUserInteractionEnabled = false
self.groupsTextNode.displaysAsynchronously = false
super.init(layerBacked: false, dynamicBounce: true, rotated: true)
self.transform = CATransform3DMakeRotation(CGFloat.pi, 0.0, 0.0, 1.0)
self.addSubnode(self.offsetContainer)
self.offsetContainer.addSubnode(self.titleNode)
self.offsetContainer.addSubnode(self.subtitleNode)
self.offsetContainer.addSubnode(self.groupsTextNode)
self.wantsTrailingItemSpaceUpdates = true
}
override public func didLoad() {
super.didLoad()
// let recognizer = TapLongTapOrDoubleTapGestureRecognizer(target: self, action: #selector(self.tapLongTapOrDoubleTapGesture(_:)))
// recognizer.tapActionAtPoint = { [weak self] point in
// if let strongSelf = self {
// let tapAction = strongSelf.tapActionAtPoint(point, gesture: .tap, isEstimating: true)
// switch tapAction.content {
// case .none:
// break
// case .ignore:
// return .fail
// case .url, .phone, .peerMention, .textMention, .botCommand, .hashtag, .instantPage, .wallpaper, .theme, .call, .openMessage, .timecode, .bankCard, .tooltip, .openPollResults, .copy, .largeEmoji, .customEmoji, .custom:
// return .waitForSingleTap
// }
// }
//
// return .waitForDoubleTap
// }
// recognizer.highlight = { [weak self] point in
// if let strongSelf = self {
// strongSelf.updateTouchesAtPoint(point)
// }
// }
// self.view.addGestureRecognizer(recognizer)
}
override public func updateAbsoluteRect(_ rect: CGRect, within containerSize: CGSize) {
super.updateAbsoluteRect(rect, within: containerSize)
self.absolutePosition = (rect, containerSize)
if let backgroundContent = self.backgroundContent {
var backgroundFrame = backgroundContent.frame
backgroundFrame.origin.x += rect.minX
backgroundFrame.origin.y += containerSize.height - rect.minY
backgroundContent.update(rect: backgroundFrame, within: containerSize, transition: .immediate)
}
}
public func asyncLayout() -> (_ item: ChatUserInfoItem, _ width: ListViewItemLayoutParams) -> (ListViewItemNodeLayout, (ListViewItemUpdateAnimation) -> Void) {
let makeTitleLayout = TextNode.asyncLayout(self.titleNode)
let makeSubtitleLayout = TextNode.asyncLayout(self.subtitleNode)
let makeRegistrationDateTitleLayout = TextNode.asyncLayout(self.registrationDateTitleTextNode)
let makeRegistrationDateValueLayout = TextNode.asyncLayout(self.registrationDateValueTextNode)
let makePhoneCountryTitleLayout = TextNode.asyncLayout(self.phoneCountryTitleTextNode)
let makePhoneCountryValueLayout = TextNode.asyncLayout(self.phoneCountryValueTextNode)
let makeLocationCountryTitleLayout = TextNode.asyncLayout(self.locationCountryTitleTextNode)
let makeLocationCountryValueLayout = TextNode.asyncLayout(self.locationCountryValueTextNode)
let makeGroupsLayout = TextNode.asyncLayout(self.groupsTextNode)
let currentRegistrationDateText = self.registrationDateText
let currentPhoneCountryText = self.phoneCountryText
let currentLocationCountryText = self.locationCountryText
return { [weak self] item, params in
self?.item = item
var backgroundSize = CGSize(width: 240.0, height: 0.0)
let verticalItemInset: CGFloat = 10.0
let horizontalInset: CGFloat = 10.0 + params.leftInset
let horizontalContentInset: CGFloat = 16.0
let verticalInset: CGFloat = 17.0
let verticalSpacing: CGFloat = 6.0
let paragraphSpacing: CGFloat = 3.0
let attributeSpacing: CGFloat = 10.0
let primaryTextColor = serviceMessageColorComponents(theme: item.presentationData.theme.theme, wallpaper: item.presentationData.theme.wallpaper).primaryText
let subtitleColor = primaryTextColor.withAlphaComponent(item.presentationData.theme.theme.overallDarkAppearance ? 0.7 : 0.8)
backgroundSize.height += verticalInset
//TODO:localize
let constrainedWidth = params.width - (horizontalInset + horizontalContentInset) * 2.0
let (titleLayout, titleApply) = makeTitleLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: item.title, font: Font.semibold(15.0), textColor: primaryTextColor), backgroundColor: nil, maximumNumberOfLines: 0, truncationType: .end, constrainedSize: CGSize(width: constrainedWidth, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets()))
backgroundSize.height += titleLayout.size.height
backgroundSize.height += verticalSpacing
let (subtitleLayout, subtitleApply) = makeSubtitleLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: "Not a contact", font: Font.regular(13.0), textColor: subtitleColor), backgroundColor: nil, maximumNumberOfLines: 0, truncationType: .end, constrainedSize: CGSize(width: constrainedWidth, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets()))
backgroundSize.height += subtitleLayout.size.height
backgroundSize.height += verticalSpacing + paragraphSpacing
let infoConstrainedSize = CGSize(width: constrainedWidth * 0.7, height: CGFloat.greatestFiniteMagnitude)
var maxTitleWidth: CGFloat = 0.0
var maxValueWidth: CGFloat = 0.0
var registrationDateText: String?
let registrationDateTitleLayoutAndApply: (TextNodeLayout, () -> TextNode)?
let registrationDateValueLayoutAndApply: (TextNodeLayout, () -> TextNode)?
if let registrationDate = item.registrationDate {
if let currentRegistrationDateText {
registrationDateText = currentRegistrationDateText
} else {
let components = registrationDate.components(separatedBy: ".")
if components.count == 2, let first = Int32(components[0]), let second = Int32(components[1]) {
let month = first - 1
let year = second - 1900
registrationDateText = stringForMonth(strings: item.presentationData.strings, month: month, ofYear: year)
} else {
registrationDateText = ""
}
}
registrationDateTitleLayoutAndApply = makeRegistrationDateTitleLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: "Registration", font: Font.regular(13.0), textColor: subtitleColor), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: infoConstrainedSize, alignment: .center, cutout: nil, insets: UIEdgeInsets()))
registrationDateValueLayoutAndApply = makeRegistrationDateValueLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: registrationDateText ?? "", font: Font.semibold(13.0), textColor: primaryTextColor), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: infoConstrainedSize, alignment: .center, cutout: nil, insets: UIEdgeInsets()))
backgroundSize.height += verticalSpacing
backgroundSize.height += registrationDateValueLayoutAndApply?.0.size.height ?? 0
maxTitleWidth = max(maxTitleWidth, (registrationDateTitleLayoutAndApply?.0.size.width ?? 0))
maxValueWidth = max(maxValueWidth, (registrationDateValueLayoutAndApply?.0.size.width ?? 0))
} else {
registrationDateTitleLayoutAndApply = nil
registrationDateValueLayoutAndApply = nil
}
var phoneCountryText: String?
let phoneCountryTitleLayoutAndApply: (TextNodeLayout, () -> TextNode)?
let phoneCountryValueLayoutAndApply: (TextNodeLayout, () -> TextNode)?
if let phoneCountry = item.phoneCountry {
if let currentPhoneCountryText {
phoneCountryText = currentPhoneCountryText
} else {
var countryName = ""
let countriesConfiguration = item.context.currentCountriesConfiguration.with { $0 }
if let country = countriesConfiguration.countries.first(where: { $0.id == phoneCountry }) {
countryName = country.localizedName ?? country.name
}
phoneCountryText = emojiFlagForISOCountryCode(phoneCountry) + " " + countryName
}
phoneCountryTitleLayoutAndApply = makePhoneCountryTitleLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: "Phone Number", font: Font.regular(13.0), textColor: subtitleColor), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: infoConstrainedSize, alignment: .center, cutout: nil, insets: UIEdgeInsets()))
phoneCountryValueLayoutAndApply = makePhoneCountryValueLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: phoneCountryText ?? "", font: Font.semibold(13.0), textColor: primaryTextColor), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: infoConstrainedSize, alignment: .center, cutout: nil, insets: UIEdgeInsets()))
backgroundSize.height += verticalSpacing
backgroundSize.height += phoneCountryValueLayoutAndApply?.0.size.height ?? 0
maxTitleWidth = max(maxTitleWidth, (phoneCountryTitleLayoutAndApply?.0.size.width ?? 0))
maxValueWidth = max(maxValueWidth, (phoneCountryValueLayoutAndApply?.0.size.width ?? 0))
} else {
phoneCountryTitleLayoutAndApply = nil
phoneCountryValueLayoutAndApply = nil
}
var locationCountryText: String?
let locationCountryTitleLayoutAndApply: (TextNodeLayout, () -> TextNode)?
let locationCountryValueLayoutAndApply: (TextNodeLayout, () -> TextNode)?
if let locationCountry = item.locationCountry {
if let currentLocationCountryText {
locationCountryText = currentLocationCountryText
} else {
var countryName = ""
let countriesConfiguration = item.context.currentCountriesConfiguration.with { $0 }
if let country = countriesConfiguration.countries.first(where: { $0.id == locationCountry }) {
countryName = country.localizedName ?? country.name
}
locationCountryText = emojiFlagForISOCountryCode(locationCountry) + " " + countryName
}
locationCountryTitleLayoutAndApply = makeLocationCountryTitleLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: "Location", font: Font.regular(13.0), textColor: subtitleColor), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: infoConstrainedSize, alignment: .center, cutout: nil, insets: UIEdgeInsets()))
locationCountryValueLayoutAndApply = makeLocationCountryValueLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: locationCountryText ?? "", font: Font.semibold(13.0), textColor: primaryTextColor), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: infoConstrainedSize, alignment: .center, cutout: nil, insets: UIEdgeInsets()))
backgroundSize.height += verticalSpacing
backgroundSize.height += locationCountryValueLayoutAndApply?.0.size.height ?? 0
maxTitleWidth = max(maxTitleWidth, (locationCountryTitleLayoutAndApply?.0.size.width ?? 0))
maxValueWidth = max(maxValueWidth, (locationCountryValueLayoutAndApply?.0.size.width ?? 0))
} else {
locationCountryTitleLayoutAndApply = nil
locationCountryValueLayoutAndApply = nil
}
backgroundSize.width = horizontalContentInset * 3.0 + maxTitleWidth + attributeSpacing + maxValueWidth
let (groupsLayout, groupsApply) = makeGroupsLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: "No groups in common", font: Font.regular(13.0), textColor: subtitleColor), backgroundColor: nil, maximumNumberOfLines: 0, truncationType: .end, constrainedSize: CGSize(width: constrainedWidth, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets()))
backgroundSize.height += verticalSpacing * 2.0 + paragraphSpacing
backgroundSize.height += groupsLayout.size.height
backgroundSize.height += verticalInset
let backgroundFrame = CGRect(origin: CGPoint(x: floor((params.width - backgroundSize.width) / 2.0), y: verticalItemInset + 4.0), size: backgroundSize)
let itemLayout = ListViewItemNodeLayout(contentSize: CGSize(width: params.width, height: backgroundSize.height + verticalItemInset * 2.0), insets: UIEdgeInsets())
return (itemLayout, { _ in
if let strongSelf = self {
strongSelf.theme = item.presentationData.theme
if item.presentationData.theme.theme.overallDarkAppearance {
strongSelf.registrationDateTitleTextNode.layer.compositingFilter = nil
strongSelf.phoneCountryTitleTextNode.layer.compositingFilter = nil
strongSelf.locationCountryTitleTextNode.layer.compositingFilter = nil
strongSelf.subtitleNode.layer.compositingFilter = nil
strongSelf.groupsTextNode.layer.compositingFilter = nil
} else {
strongSelf.registrationDateTitleTextNode.layer.compositingFilter = "overlayBlendMode"
strongSelf.phoneCountryTitleTextNode.layer.compositingFilter = "overlayBlendMode"
strongSelf.locationCountryTitleTextNode.layer.compositingFilter = "overlayBlendMode"
strongSelf.subtitleNode.layer.compositingFilter = "overlayBlendMode"
strongSelf.groupsTextNode.layer.compositingFilter = "overlayBlendMode"
}
strongSelf.registrationDateText = registrationDateText
strongSelf.phoneCountryText = phoneCountryText
strongSelf.locationCountryText = locationCountryText
strongSelf.controllerInteraction = item.controllerInteraction
strongSelf.offsetContainer.frame = CGRect(origin: CGPoint(), size: itemLayout.contentSize)
let _ = titleApply()
var contentOriginY = backgroundFrame.origin.y + verticalInset
let titleFrame = CGRect(origin: CGPoint(x: backgroundFrame.origin.x + floor((backgroundSize.width - titleLayout.size.width) / 2.0), y: contentOriginY), size: titleLayout.size)
strongSelf.titleNode.frame = titleFrame
contentOriginY += titleLayout.size.height
contentOriginY += verticalSpacing - paragraphSpacing
let _ = subtitleApply()
let subtitleFrame = CGRect(origin: CGPoint(x: backgroundFrame.origin.x + floor((backgroundSize.width - subtitleLayout.size.width) / 2.0), y: contentOriginY), size: subtitleLayout.size)
strongSelf.subtitleNode.frame = subtitleFrame
contentOriginY += subtitleLayout.size.height
contentOriginY += verticalSpacing * 2.0 + paragraphSpacing
var attributeMidpoints: [CGFloat] = []
func appendAttributeMidpoint(titleLayout: TextNodeLayout?, valueLayout: TextNodeLayout?) {
if let titleLayout, let valueLayout {
let totalWidth = titleLayout.size.width + attributeSpacing + valueLayout.size.width
let titleOffset = titleLayout.size.width + attributeSpacing / 2.0
let midpoint = (backgroundSize.width - totalWidth) / 2.0 + titleOffset
attributeMidpoints.append(midpoint)
}
}
appendAttributeMidpoint(titleLayout: registrationDateTitleLayoutAndApply?.0, valueLayout: registrationDateValueLayoutAndApply?.0)
appendAttributeMidpoint(titleLayout: phoneCountryTitleLayoutAndApply?.0, valueLayout: phoneCountryValueLayoutAndApply?.0)
appendAttributeMidpoint(titleLayout: locationCountryTitleLayoutAndApply?.0, valueLayout: locationCountryValueLayoutAndApply?.0)
let middleX = floorToScreenPixels(attributeMidpoints.isEmpty ? backgroundSize.width / 2.0 : attributeMidpoints.reduce(0, +) / CGFloat(attributeMidpoints.count))
let titleMaxX: CGFloat = backgroundFrame.minX + middleX - attributeSpacing / 2.0
let valueMinX: CGFloat = backgroundFrame.minX + middleX + attributeSpacing / 2.0
func positionAttributeNodes(
titleTextNode: TextNode,
valueTextNode: TextNode,
titleLayoutAndApply: (TextNodeLayout, () -> TextNode)?,
valueLayoutAndApply: (TextNodeLayout, () -> TextNode)?
) {
if let (titleLayout, titleApply) = titleLayoutAndApply {
if titleTextNode.supernode == nil {
strongSelf.offsetContainer.addSubnode(titleTextNode)
}
let _ = titleApply()
titleTextNode.frame = CGRect(
origin: CGPoint(x: titleMaxX - titleLayout.size.width, y: contentOriginY),
size: titleLayout.size
)
}
if let (valueLayout, valueApply) = valueLayoutAndApply {
if valueTextNode.supernode == nil {
strongSelf.offsetContainer.addSubnode(valueTextNode)
}
let _ = valueApply()
valueTextNode.frame = CGRect(
origin: CGPoint(x: valueMinX, y: contentOriginY),
size: valueLayout.size
)
contentOriginY += valueLayout.size.height + verticalSpacing
}
}
positionAttributeNodes(
titleTextNode: strongSelf.registrationDateTitleTextNode,
valueTextNode: strongSelf.registrationDateValueTextNode,
titleLayoutAndApply: registrationDateTitleLayoutAndApply,
valueLayoutAndApply: registrationDateValueLayoutAndApply
)
positionAttributeNodes(
titleTextNode: strongSelf.phoneCountryTitleTextNode,
valueTextNode: strongSelf.phoneCountryValueTextNode,
titleLayoutAndApply: phoneCountryTitleLayoutAndApply,
valueLayoutAndApply: phoneCountryValueLayoutAndApply
)
positionAttributeNodes(
titleTextNode: strongSelf.locationCountryTitleTextNode,
valueTextNode: strongSelf.locationCountryValueTextNode,
titleLayoutAndApply: locationCountryTitleLayoutAndApply,
valueLayoutAndApply: locationCountryValueLayoutAndApply
)
contentOriginY += verticalSpacing + paragraphSpacing
let _ = groupsApply()
let groupsFrame = CGRect(origin: CGPoint(x: backgroundFrame.origin.x + floor((backgroundSize.width - groupsLayout.size.width) / 2.0), y: contentOriginY), size: groupsLayout.size)
strongSelf.groupsTextNode.frame = groupsFrame
if strongSelf.backgroundContent == nil, let backgroundContent = item.controllerInteraction.presentationContext.backgroundNode?.makeBubbleBackground(for: .free) {
backgroundContent.clipsToBounds = true
strongSelf.backgroundContent = backgroundContent
strongSelf.offsetContainer.insertSubnode(backgroundContent, at: 0)
}
if let backgroundContent = strongSelf.backgroundContent {
backgroundContent.cornerRadius = item.presentationData.chatBubbleCorners.mainRadius
backgroundContent.frame = backgroundFrame
if let (rect, containerSize) = strongSelf.absolutePosition {
var backgroundFrame = backgroundContent.frame
backgroundFrame.origin.x += rect.minX
backgroundFrame.origin.y += containerSize.height - rect.minY
backgroundContent.update(rect: backgroundFrame, within: containerSize, transition: .immediate)
}
}
}
})
}
}
override public func updateTrailingItemSpace(_ height: CGFloat, transition: ContainedViewLayoutTransition) {
if height.isLessThanOrEqualTo(0.0) {
transition.updateFrame(node: self.offsetContainer, frame: CGRect(origin: CGPoint(), size: self.offsetContainer.bounds.size))
} else {
transition.updateFrame(node: self.offsetContainer, frame: CGRect(origin: CGPoint(x: 0.0, y: -floorToScreenPixels(height / 2.0)), size: self.offsetContainer.bounds.size))
}
}
override public func animateAdded(_ currentTimestamp: Double, duration: Double) {
self.layer.animateAlpha(from: 0.0, to: 1.0, duration: duration * 0.5)
}
override public func animateInsertion(_ currentTimestamp: Double, duration: Double, options: ListViewItemAnimationOptions) {
self.layer.animateAlpha(from: 0.0, to: 1.0, duration: duration * 0.5)
}
override public func animateRemoved(_ currentTimestamp: Double, duration: Double) {
self.layer.animateAlpha(from: 1.0, to: 0.0, duration: duration * 0.5, removeOnCompletion: false)
}
override public func point(inside point: CGPoint, with event: UIEvent?) -> Bool {
let result = super.point(inside: point, with: event)
let extra = self.offsetContainer.frame.contains(point)
return result || extra
}
// public func tapActionAtPoint(_ point: CGPoint, gesture: TapLongTapOrDoubleTapGesture, isEstimating: Bool) -> ChatMessageBubbleContentTapAction {
// let textNodeFrame = self.textNode.frame
// if let (index, attributes) = self.textNode.attributesAtPoint(CGPoint(x: point.x - self.offsetContainer.frame.minX - textNodeFrame.minX, y: point.y - self.offsetContainer.frame.minY - textNodeFrame.minY)) {
// if let url = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.URL)] as? String {
// var concealed = true
// if let (attributeText, fullText) = self.textNode.attributeSubstring(name: TelegramTextAttributes.URL, index: index) {
// concealed = !doesUrlMatchText(url: url, text: attributeText, fullText: fullText)
// }
// return ChatMessageBubbleContentTapAction(content: .url(ChatMessageBubbleContentTapAction.Url(url: url, concealed: concealed)))
// } else if let peerMention = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.PeerMention)] as? TelegramPeerMention {
// return ChatMessageBubbleContentTapAction(content: .peerMention(peerId: peerMention.peerId, mention: peerMention.mention, openProfile: false))
// } else if let peerName = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.PeerTextMention)] as? String {
// return ChatMessageBubbleContentTapAction(content: .textMention(peerName))
// } else if let botCommand = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.BotCommand)] as? String {
// return ChatMessageBubbleContentTapAction(content: .botCommand(botCommand))
// } else if let hashtag = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.Hashtag)] as? TelegramHashtag {
// return ChatMessageBubbleContentTapAction(content: .hashtag(hashtag.peerName, hashtag.hashtag))
// } else {
// return ChatMessageBubbleContentTapAction(content: .none)
// }
// } else {
// return ChatMessageBubbleContentTapAction(content: .none)
// }
// }
// @objc private func tapLongTapOrDoubleTapGesture(_ recognizer: TapLongTapOrDoubleTapGestureRecognizer) {
// switch recognizer.state {
// case .ended:
// if let (gesture, location) = recognizer.lastRecognizedGestureAndLocation {
// switch gesture {
// case .tap:
// let tapAction = self.tapActionAtPoint(location, gesture: gesture, isEstimating: false)
// switch tapAction.content {
// case .none, .ignore:
// break
// case let .url(url):
// self.item?.controllerInteraction.openUrl(ChatControllerInteraction.OpenUrl(url: url.url, concealed: url.concealed, progress: tapAction.activate?()))
// case let .peerMention(peerId, _, _):
// if let item = self.item {
// let _ = (item.context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: peerId))
// |> deliverOnMainQueue).startStandalone(next: { [weak self] peer in
// if let peer = peer {
// self?.item?.controllerInteraction.openPeer(peer, .chat(textInputState: nil, subject: nil, peekData: nil), nil, .default)
// }
// })
// }
// case let .textMention(name):
// self.item?.controllerInteraction.openPeerMention(name, tapAction.activate?())
// case let .botCommand(command):
// self.item?.controllerInteraction.sendBotCommand(nil, command)
// case let .hashtag(peerName, hashtag):
// self.item?.controllerInteraction.openHashtag(peerName, hashtag)
// default:
// break
// }
// case .longTap, .doubleTap:
// if let item = self.item, self.backgroundNode.frame.contains(location) {
// let tapAction = self.tapActionAtPoint(location, gesture: gesture, isEstimating: false)
// switch tapAction.content {
// case .none, .ignore:
// break
// case let .url(url):
// item.controllerInteraction.longTap(.url(url.url), ChatControllerInteraction.LongTapParams())
// case let .peerMention(peerId, mention, _):
// item.controllerInteraction.longTap(.peerMention(peerId, mention), ChatControllerInteraction.LongTapParams())
// case let .textMention(name):
// item.controllerInteraction.longTap(.mention(name), ChatControllerInteraction.LongTapParams())
// case let .botCommand(command):
// item.controllerInteraction.longTap(.command(command), ChatControllerInteraction.LongTapParams())
// case let .hashtag(_, hashtag):
// item.controllerInteraction.longTap(.hashtag(hashtag), ChatControllerInteraction.LongTapParams())
// default:
// break
// }
// }
// default:
// break
// }
// }
// default:
// break
// }
// }
}

Some files were not shown because too many files have changed in this diff Show More