mirror of
https://github.com/Swiftgram/Telegram-iOS.git
synced 2025-06-16 05:55:20 +00:00
Merge commit '84a17115fa6082750c991bde783485fd4d92daf0'
# Conflicts: # submodules/TelegramUI/Components/MessageInputPanelComponent/Sources/MessageInputPanelComponent.swift
This commit is contained in:
commit
034480ba44
.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
PublicHeaders/LegacyComponents
Sources
LegacyMediaPickerUI
MediaPickerUI/Sources
PeerInfoUI/Sources
PremiumUI/Sources
SelectablePeerNode/Sources
SettingsUI/Sources/Privacy and Security
ShareController
BUILD
Sources
ShareItems/Sources
StatisticsUI/Sources
ChannelStatsController.swiftMonetizationBalanceItem.swiftStatsOverviewItem.swiftTransactionInfoScreen.swift
TelegramApi/Sources
TelegramCore
FlatBuffers
FlatSerialization
Package.swiftSources
Account
ApiUtils
PendingMessages
State
SyncCore
TelegramEngine
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
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()
|
||||
|
27
submodules/TelegramCore/FlatBuffers/Package.swift
Normal file
27
submodules/TelegramCore/FlatBuffers/Package.swift
Normal file
@ -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"),
|
||||
]
|
||||
)
|
30
submodules/TelegramCore/FlatSerialization/Package.swift
Normal file
30
submodules/TelegramCore/FlatSerialization/Package.swift
Normal file
@ -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
|
||||
|
||||
|
9
submodules/TelegramCore/Sources/TelegramEngine/Messages/OutgoingMessageWithChatContextResult.swift
9
submodules/TelegramCore/Sources/TelegramEngine/Messages/OutgoingMessageWithChatContextResult.swift
@ -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,
|
||||
|
34
submodules/TelegramUI/Components/Chat/ChatUserInfoItem/BUILD
Normal file
34
submodules/TelegramUI/Components/Chat/ChatUserInfoItem/BUILD
Normal file
@ -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",
|
||||
],
|
||||
)
|
589
submodules/TelegramUI/Components/Chat/ChatUserInfoItem/Sources/ChatUserInfoItem.swift
Normal file
589
submodules/TelegramUI/Components/Chat/ChatUserInfoItem/Sources/ChatUserInfoItem.swift
Normal file
@ -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
Loading…
x
Reference in New Issue
Block a user