Various improvements

This commit is contained in:
Ilya Laktyushin 2025-02-22 19:17:36 +04:00
parent eecb2ef5c0
commit 8d7f9bf372
122 changed files with 3454 additions and 1121 deletions

View file

@ -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,21 @@ 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 >]()";

View file

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

View file

@ -134,6 +134,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 {

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -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, String)
case messagePriceInfo(PresentationTheme, String)
case unrestrictBoostersSwitch(PresentationTheme, String, Bool)
@ -425,7 +425,7 @@ private enum ChannelPermissionsEntry: ItemListNodeEntry {
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
return MessagePriceItem(theme: presentationData.theme, strings: presentationData.strings, minValue: 1, maxValue: 10000, value: value, price: price, sectionId: self.section, updated: { value in
arguments.updateStarsAmount(StarsAmount(value: value, nanos: 0))
})
case let .messagePriceInfo(_, value):
@ -720,23 +720,24 @@ 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 {
if 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 = ""
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))"
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, price))
entries.append(.messagePriceInfo(presentationData.theme, presentationData.strings.GroupInfo_Permissions_MessagePriceInfo(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 +877,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 +1256,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

View file

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

View file

@ -129,7 +129,11 @@ private enum GlobalAutoremoveEntry: ItemListNodeEntry {
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: {
arguments.updateValue(.paidMessages(StarsAmount(value: 400, nanos: 0)))
if isEnabled {
arguments.updateValue(.paidMessages(StarsAmount(value: 400, nanos: 0)))
} else {
arguments.disabledValuePressed()
}
})
case let .footer(value):
let text: String
@ -146,7 +150,7 @@ 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
return MessagePriceItem(theme: presentationData.theme, strings: presentationData.strings, minValue: 1, maxValue: 10000, value: value, price: price, sectionId: self.section, updated: { value in
arguments.updateValue(.paidMessages(StarsAmount(value: value, nanos: 0)))
})
case let .priceInfo(value):
@ -168,12 +172,12 @@ 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(.optionPremium(value: state.updatedValue, isEnabled: enableSetting))
entries.append(.optionChargeForMessages(value: state.updatedValue, isEnabled: isPremium))
if case let .paidMessages(amount) = state.updatedValue {
@ -375,7 +379,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

View file

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

View file

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

View file

@ -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,19 @@ 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
}
var mediaParameters: ShareControllerSubject.MediaParameters?
if case let .media(_, parameters) = self.subject {
mediaParameters = parameters
@ -682,7 +690,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 +1248,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: 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
|> 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 +1284,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: Int64?) -> [StandaloneSendEnqueueMessage] {
return messages.map { message in
var message = message
if !showNames {
@ -1278,6 +1296,7 @@ public final class ShareController: ViewController {
if silently {
message.isSilent = true
}
message.sendPaidMessageStars = sendPaidMessageStars.flatMap { StarsAmount(value: $0, nanos: 0) }
return message
}
}
@ -1325,7 +1344,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 +1405,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 +1470,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 +1530,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 +1640,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 +1699,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 +1819,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,
@ -1880,12 +1899,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: 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, 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 +1938,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: Int64?) -> [EnqueueMessage] {
return messages.map { message in
return message.withUpdatedAttributes({ attributes in
var attributes = attributes
@ -1907,6 +1948,9 @@ public final class ShareController: ViewController {
if silently {
attributes.append(NotificationInfoMessageAttribute(flags: .muted))
}
if let sendPaidMessageStars {
attributes.append(PaidStarsMessageAttribute(stars: StarsAmount(value: sendPaidMessageStars, nanos: 0), postponeSending: false))
}
return attributes
})
}
@ -1949,7 +1993,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 +2027,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 +2064,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 +2095,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 +2189,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 +2223,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,7 +2318,7 @@ 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):
@ -2458,7 +2502,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 +2512,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 +2531,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 +2539,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)
}

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -349,6 +349,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)
}

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -773,14 +773,14 @@ public final class ChatInlineSearchResultsListComponent: Component {
if let forwardInfo = message.forwardInfo {
effectiveAuthor = forwardInfo.author.flatMap(EnginePeer.init)
if effectiveAuthor == nil, let authorSignature = forwardInfo.authorSignature {
effectiveAuthor = EnginePeer(TelegramUser(id: PeerId(namespace: Namespaces.Peer.Empty, id: PeerId.Id._internalFromInt64Value(Int64(authorSignature.persistentHashValue % 32))), accessHash: nil, firstName: authorSignature, lastName: nil, username: nil, phone: nil, photo: [], botInfo: nil, restrictionInfo: nil, flags: [], emojiStatus: nil, usernames: [], storiesHidden: nil, nameColor: nil, backgroundEmojiId: nil, profileColor: nil, profileBackgroundEmojiId: nil, subscriberCount: nil, verificationIconFileId: nil, sendPaidMessageStars: nil))
effectiveAuthor = EnginePeer(TelegramUser(id: PeerId(namespace: Namespaces.Peer.Empty, id: PeerId.Id._internalFromInt64Value(Int64(authorSignature.persistentHashValue % 32))), accessHash: nil, firstName: authorSignature, lastName: nil, username: nil, phone: nil, photo: [], botInfo: nil, restrictionInfo: nil, flags: [], emojiStatus: nil, usernames: [], storiesHidden: nil, nameColor: nil, backgroundEmojiId: nil, profileColor: nil, profileBackgroundEmojiId: nil, subscriberCount: nil, verificationIconFileId: nil))
}
}
if let sourceAuthorInfo = message._asMessage().sourceAuthorInfo {
if let originalAuthor = sourceAuthorInfo.originalAuthor, let peer = message.peers[originalAuthor] {
effectiveAuthor = EnginePeer(peer)
} else if let authorSignature = sourceAuthorInfo.originalAuthorName {
effectiveAuthor = EnginePeer(TelegramUser(id: PeerId(namespace: Namespaces.Peer.Empty, id: PeerId.Id._internalFromInt64Value(Int64(authorSignature.persistentHashValue % 32))), accessHash: nil, firstName: authorSignature, lastName: nil, username: nil, phone: nil, photo: [], botInfo: nil, restrictionInfo: nil, flags: [], emojiStatus: nil, usernames: [], storiesHidden: nil, nameColor: nil, backgroundEmojiId: nil, profileColor: nil, profileBackgroundEmojiId: nil, subscriberCount: nil, verificationIconFileId: nil, sendPaidMessageStars: nil))
effectiveAuthor = EnginePeer(TelegramUser(id: PeerId(namespace: Namespaces.Peer.Empty, id: PeerId.Id._internalFromInt64Value(Int64(authorSignature.persistentHashValue % 32))), accessHash: nil, firstName: authorSignature, lastName: nil, username: nil, phone: nil, photo: [], botInfo: nil, restrictionInfo: nil, flags: [], emojiStatus: nil, usernames: [], storiesHidden: nil, nameColor: nil, backgroundEmojiId: nil, profileColor: nil, profileBackgroundEmojiId: nil, subscriberCount: nil, verificationIconFileId: nil))
}
}
if effectiveAuthor == nil {

View file

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

View file

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

View file

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

View file

@ -236,7 +236,7 @@ public final class ChatMessageAttachedContentNode: ASDisplayNode {
if let peer = forwardInfo.author {
author = peer
} else if let authorSignature = forwardInfo.authorSignature {
author = TelegramUser(id: PeerId(namespace: Namespaces.Peer.Empty, id: PeerId.Id._internalFromInt64Value(Int64(authorSignature.persistentHashValue % 32))), accessHash: nil, firstName: authorSignature, lastName: nil, username: nil, phone: nil, photo: [], botInfo: nil, restrictionInfo: nil, flags: [], emojiStatus: nil, usernames: [], storiesHidden: nil, nameColor: nil, backgroundEmojiId: nil, profileColor: nil, profileBackgroundEmojiId: nil, subscriberCount: nil, verificationIconFileId: nil, sendPaidMessageStars: nil)
author = TelegramUser(id: PeerId(namespace: Namespaces.Peer.Empty, id: PeerId.Id._internalFromInt64Value(Int64(authorSignature.persistentHashValue % 32))), accessHash: nil, firstName: authorSignature, lastName: nil, username: nil, phone: nil, photo: [], botInfo: nil, restrictionInfo: nil, flags: [], emojiStatus: nil, usernames: [], storiesHidden: nil, nameColor: nil, backgroundEmojiId: nil, profileColor: nil, profileBackgroundEmojiId: nil, subscriberCount: nil, verificationIconFileId: nil)
}
}

View file

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

View file

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

View file

@ -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
}
}
@ -1521,11 +1526,11 @@ public class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewI
if let sourceAuthorInfo, let originalAuthorId = sourceAuthorInfo.originalAuthor, let peer = item.message.peers[originalAuthorId] {
effectiveAuthor = peer
} else if let sourceAuthorInfo, let originalAuthorName = sourceAuthorInfo.originalAuthorName {
effectiveAuthor = TelegramUser(id: PeerId(namespace: Namespaces.Peer.Empty, id: PeerId.Id._internalFromInt64Value(Int64(originalAuthorName.persistentHashValue % 32))), accessHash: nil, firstName: originalAuthorName, lastName: nil, username: nil, phone: nil, photo: [], botInfo: nil, restrictionInfo: nil, flags: [], emojiStatus: nil, usernames: [], storiesHidden: nil, nameColor: nil, backgroundEmojiId: nil, profileColor: nil, profileBackgroundEmojiId: nil, subscriberCount: nil, verificationIconFileId: nil, sendPaidMessageStars: nil)
effectiveAuthor = TelegramUser(id: PeerId(namespace: Namespaces.Peer.Empty, id: PeerId.Id._internalFromInt64Value(Int64(originalAuthorName.persistentHashValue % 32))), accessHash: nil, firstName: originalAuthorName, lastName: nil, username: nil, phone: nil, photo: [], botInfo: nil, restrictionInfo: nil, flags: [], emojiStatus: nil, usernames: [], storiesHidden: nil, nameColor: nil, backgroundEmojiId: nil, profileColor: nil, profileBackgroundEmojiId: nil, subscriberCount: nil, verificationIconFileId: nil)
} else {
ignoreForward = true
if effectiveAuthor == nil, let authorSignature = forwardInfo.authorSignature {
effectiveAuthor = TelegramUser(id: PeerId(namespace: Namespaces.Peer.Empty, id: PeerId.Id._internalFromInt64Value(Int64(authorSignature.persistentHashValue % 32))), accessHash: nil, firstName: authorSignature, lastName: nil, username: nil, phone: nil, photo: [], botInfo: nil, restrictionInfo: nil, flags: [], emojiStatus: nil, usernames: [], storiesHidden: nil, nameColor: nil, backgroundEmojiId: nil, profileColor: nil, profileBackgroundEmojiId: nil, subscriberCount: nil, verificationIconFileId: nil, sendPaidMessageStars: nil)
effectiveAuthor = TelegramUser(id: PeerId(namespace: Namespaces.Peer.Empty, id: PeerId.Id._internalFromInt64Value(Int64(authorSignature.persistentHashValue % 32))), accessHash: nil, firstName: authorSignature, lastName: nil, username: nil, phone: nil, photo: [], botInfo: nil, restrictionInfo: nil, flags: [], emojiStatus: nil, usernames: [], storiesHidden: nil, nameColor: nil, backgroundEmojiId: nil, profileColor: nil, profileBackgroundEmojiId: nil, subscriberCount: nil, verificationIconFileId: nil)
}
}
}
@ -1542,7 +1547,7 @@ public class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewI
displayAuthorInfo = !mergedTop.merged && incoming
} else if let forwardInfo = item.content.firstMessage.forwardInfo, forwardInfo.flags.contains(.isImported), let authorSignature = forwardInfo.authorSignature {
ignoreForward = true
effectiveAuthor = TelegramUser(id: PeerId(namespace: Namespaces.Peer.Empty, id: PeerId.Id._internalFromInt64Value(Int64(authorSignature.persistentHashValue % 32))), accessHash: nil, firstName: authorSignature, lastName: nil, username: nil, phone: nil, photo: [], botInfo: nil, restrictionInfo: nil, flags: [], emojiStatus: nil, usernames: [], storiesHidden: nil, nameColor: nil, backgroundEmojiId: nil, profileColor: nil, profileBackgroundEmojiId: nil, subscriberCount: nil, verificationIconFileId: nil, sendPaidMessageStars: nil)
effectiveAuthor = TelegramUser(id: PeerId(namespace: Namespaces.Peer.Empty, id: PeerId.Id._internalFromInt64Value(Int64(authorSignature.persistentHashValue % 32))), accessHash: nil, firstName: authorSignature, lastName: nil, username: nil, phone: nil, photo: [], botInfo: nil, restrictionInfo: nil, flags: [], emojiStatus: nil, usernames: [], storiesHidden: nil, nameColor: nil, backgroundEmojiId: nil, profileColor: nil, profileBackgroundEmojiId: nil, subscriberCount: nil, verificationIconFileId: nil)
displayAuthorInfo = !mergedTop.merged && incoming
} else if let _ = item.content.firstMessage.adAttribute, let author = item.content.firstMessage.author {
ignoreForward = 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
@ -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,9 +2996,17 @@ 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)
var containerFrame = CGRect(origin: CGPoint(x: 0.0, y: contentNodeOriginY), size: size)
if size.height == 33.0 && detachedContentNodesHeight > 0.0 {
//TODO:unmock
containerFrame = containerFrame.offsetBy(dx: 0.0, dy: 2.0)
}
contentNodeFramesPropertiesAndApply.append((containerFrame, properties, contentGroupId == nil, apply))
if contentProperties.neighborType == .media && unlockButtonPosition == nil {
@ -2998,7 +3018,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 +3181,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 +3189,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 +4070,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 +4104,6 @@ public class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewI
strongSelf.internalUpdateLayout()
}
contentNode.updateIsExtractedToContextPreview(contextSourceNode.isExtractedToContextPreview)
}
}

View file

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

View file

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

View file

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

View file

@ -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
}
@ -288,14 +293,14 @@ public final class ChatMessageItemImpl: ChatMessageItem, CustomStringConvertible
if let forwardInfo = content.firstMessage.forwardInfo {
effectiveAuthor = forwardInfo.author
if effectiveAuthor == nil, let authorSignature = forwardInfo.authorSignature {
effectiveAuthor = TelegramUser(id: PeerId(namespace: Namespaces.Peer.Empty, id: PeerId.Id._internalFromInt64Value(Int64(authorSignature.persistentHashValue % 32))), accessHash: nil, firstName: authorSignature, lastName: nil, username: nil, phone: nil, photo: [], botInfo: nil, restrictionInfo: nil, flags: [], emojiStatus: nil, usernames: [], storiesHidden: nil, nameColor: nil, backgroundEmojiId: nil, profileColor: nil, profileBackgroundEmojiId: nil, subscriberCount: nil, verificationIconFileId: nil, sendPaidMessageStars: nil)
effectiveAuthor = TelegramUser(id: PeerId(namespace: Namespaces.Peer.Empty, id: PeerId.Id._internalFromInt64Value(Int64(authorSignature.persistentHashValue % 32))), accessHash: nil, firstName: authorSignature, lastName: nil, username: nil, phone: nil, photo: [], botInfo: nil, restrictionInfo: nil, flags: [], emojiStatus: nil, usernames: [], storiesHidden: nil, nameColor: nil, backgroundEmojiId: nil, profileColor: nil, profileBackgroundEmojiId: nil, subscriberCount: nil, verificationIconFileId: nil)
}
}
if let sourceAuthorInfo = content.firstMessage.sourceAuthorInfo {
if let originalAuthor = sourceAuthorInfo.originalAuthor, let peer = content.firstMessage.peers[originalAuthor] {
effectiveAuthor = peer
} else if let authorSignature = sourceAuthorInfo.originalAuthorName {
effectiveAuthor = TelegramUser(id: PeerId(namespace: Namespaces.Peer.Empty, id: PeerId.Id._internalFromInt64Value(Int64(authorSignature.persistentHashValue % 32))), accessHash: nil, firstName: authorSignature, lastName: nil, username: nil, phone: nil, photo: [], botInfo: nil, restrictionInfo: nil, flags: [], emojiStatus: nil, usernames: [], storiesHidden: nil, nameColor: nil, backgroundEmojiId: nil, profileColor: nil, profileBackgroundEmojiId: nil, subscriberCount: nil, verificationIconFileId: nil, sendPaidMessageStars: nil)
effectiveAuthor = TelegramUser(id: PeerId(namespace: Namespaces.Peer.Empty, id: PeerId.Id._internalFromInt64Value(Int64(authorSignature.persistentHashValue % 32))), accessHash: nil, firstName: authorSignature, lastName: nil, username: nil, phone: nil, photo: [], botInfo: nil, restrictionInfo: nil, flags: [], emojiStatus: nil, usernames: [], storiesHidden: nil, nameColor: nil, backgroundEmojiId: nil, profileColor: nil, profileBackgroundEmojiId: nil, subscriberCount: nil, verificationIconFileId: nil)
}
}
if peerId.isVerificationCodes && effectiveAuthor == nil {

View file

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

View file

@ -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,14 +318,16 @@ 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)
}
@ -346,66 +348,102 @@ private class ChatMessagePaymentAlertController: AlertController {
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 +452,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 +462,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 +492,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 +501,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()
}

View file

@ -209,7 +209,7 @@ public class ChatMessageReplyInfoNode: ASDisplayNode {
if let peer = forwardInfo.author {
author = peer
} else if let authorSignature = forwardInfo.authorSignature {
author = TelegramUser(id: PeerId(namespace: Namespaces.Peer.Empty, id: PeerId.Id._internalFromInt64Value(Int64(authorSignature.persistentHashValue % 32))), accessHash: nil, firstName: authorSignature, lastName: nil, username: nil, phone: nil, photo: [], botInfo: nil, restrictionInfo: nil, flags: [], emojiStatus: nil, usernames: [], storiesHidden: nil, nameColor: nil, backgroundEmojiId: nil, profileColor: nil, profileBackgroundEmojiId: nil, subscriberCount: nil, verificationIconFileId: nil, sendPaidMessageStars: nil)
author = TelegramUser(id: PeerId(namespace: Namespaces.Peer.Empty, id: PeerId.Id._internalFromInt64Value(Int64(authorSignature.persistentHashValue % 32))), accessHash: nil, firstName: authorSignature, lastName: nil, username: nil, phone: nil, photo: [], botInfo: nil, restrictionInfo: nil, flags: [], emojiStatus: nil, usernames: [], storiesHidden: nil, nameColor: nil, backgroundEmojiId: nil, profileColor: nil, profileBackgroundEmojiId: nil, subscriberCount: nil, verificationIconFileId: nil)
}
}

View file

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

View 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",
],
)

View file

@ -0,0 +1,581 @@
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 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
backgroundSize.width = max(backgroundSize.width, horizontalContentInset * 2.0 + (registrationDateTitleLayoutAndApply?.0.size.width ?? 0) + attributeSpacing + (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
backgroundSize.width = max(backgroundSize.width, horizontalContentInset * 2.0 + (phoneCountryTitleLayoutAndApply?.0.size.width ?? 0) + attributeSpacing + (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
backgroundSize.width = max(backgroundSize.width, horizontalContentInset * 2.0 + (locationCountryTitleLayoutAndApply?.0.size.width ?? 0) + attributeSpacing + (locationCountryValueLayoutAndApply?.0.size.width ?? 0))
} else {
locationCountryTitleLayoutAndApply = nil
locationCountryValueLayoutAndApply = nil
}
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
// }
// }
}

View file

@ -93,6 +93,7 @@ public final class GiftItemComponent: Component {
let context: AccountContext
let theme: PresentationTheme
let strings: PresentationStrings
let peer: GiftItemComponent.Peer?
let subject: GiftItemComponent.Subject
let title: String?
@ -108,6 +109,7 @@ public final class GiftItemComponent: Component {
public init(
context: AccountContext,
theme: PresentationTheme,
strings: PresentationStrings,
peer: GiftItemComponent.Peer? = nil,
subject: GiftItemComponent.Subject,
title: String? = nil,
@ -122,6 +124,7 @@ public final class GiftItemComponent: Component {
) {
self.context = context
self.theme = theme
self.strings = strings
self.peer = peer
self.subject = subject
self.title = title
@ -142,6 +145,9 @@ public final class GiftItemComponent: Component {
if lhs.theme !== rhs.theme {
return false
}
if lhs.strings !== rhs.strings {
return false
}
if lhs.peer != rhs.peer {
return false
}
@ -471,8 +477,7 @@ public final class GiftItemComponent: Component {
price = priceValue
case .uniqueGift:
buttonColor = UIColor.white
//TODO:localize
price = "Transfer"
price = component.strings.Gift_Options_Gift_Transfer
}
let buttonSize = self.button.update(
@ -501,10 +506,11 @@ public final class GiftItemComponent: Component {
}
if let label = component.label {
let labelColor = component.theme.overallDarkAppearance ? UIColor(rgb: 0xffc337) : UIColor(rgb: 0xd3720a)
let attributes = MarkdownAttributes(
body: MarkdownAttributeSet(font: Font.regular(11.0), textColor: UIColor(rgb: 0xd3720a)),
bold: MarkdownAttributeSet(font: Font.semibold(11.0), textColor: UIColor(rgb: 0xd3720a)),
link: MarkdownAttributeSet(font: Font.regular(11.0), textColor: UIColor(rgb: 0xd3720a)),
body: MarkdownAttributeSet(font: Font.regular(11.0), textColor: labelColor),
bold: MarkdownAttributeSet(font: Font.semibold(11.0), textColor: labelColor),
link: MarkdownAttributeSet(font: Font.regular(11.0), textColor: labelColor),
linkAttribute: { contents in
return (TelegramTextAttributes.URL, contents)
}

View file

@ -402,6 +402,7 @@ final class GiftOptionsScreenComponent: Component {
GiftItemComponent(
context: component.context,
theme: environment.theme,
strings: environment.strings,
peer: nil,
subject: subject,
ribbon: ribbon,
@ -713,14 +714,16 @@ final class GiftOptionsScreenComponent: Component {
environment: {},
containerSize: availableSize
)
let formattedBalance = formatStarsAmountText(self.starsState?.balance ?? StarsAmount.zero, dateTimeFormat: environment.dateTimeFormat)
let smallLabelFont = Font.regular(11.0)
let labelFont = Font.semibold(14.0)
let balanceText = tonAmountAttributedString(formattedBalance, integralFont: labelFont, fractionalFont: smallLabelFont, color: environment.theme.actionSheet.primaryTextColor, decimalSeparator: environment.dateTimeFormat.decimalSeparator)
let balanceValueSize = self.balanceValue.update(
transition: .immediate,
component: AnyComponent(MultilineTextComponent(
text: .plain(NSAttributedString(
string: presentationStringsFormattedNumber(self.starsState?.balance ?? StarsAmount.zero, environment.dateTimeFormat.groupingSeparator),
font: Font.semibold(14.0),
textColor: environment.theme.actionSheet.primaryTextColor
)),
text: .plain(balanceText),
maximumNumberOfLines: 1
)),
environment: {},
@ -855,12 +858,15 @@ final class GiftOptionsScreenComponent: Component {
contentHeight += 6.0
} else {
if let premiumProducts = state.premiumProducts {
//TODO:unmock
let premiumOptionSize = CGSize(width: optionWidth, height: 178.0 + 23.0)
var premiumOptionSize = CGSize(width: optionWidth, height: 178.0)
var validIds: [AnyHashable] = []
var itemFrame = CGRect(origin: CGPoint(x: sideInset, y: contentHeight), size: premiumOptionSize)
for product in premiumProducts {
if let _ = product.starsPrice {
premiumOptionSize.height = 178.0 + 23.0
}
let itemId = AnyHashable(product.id)
validIds.append(itemId)
@ -885,8 +891,7 @@ final class GiftOptionsScreenComponent: Component {
default:
title = strings.Gift_Options_Premium_Months(3)
}
//TODO:unmock
let _ = visibleItem.update(
transition: itemTransition,
component: AnyComponent(
@ -895,11 +900,12 @@ final class GiftOptionsScreenComponent: Component {
GiftItemComponent(
context: component.context,
theme: theme,
strings: environment.strings,
peer: nil,
subject: .premium(months: product.months, price: product.price),
title: title,
subtitle: strings.Gift_Options_Premium_Premium,
label: "or **#1500**",
label: product.starsPrice.flatMap { strings.Gift_Options_Premium_OrStars("**#\(presentationStringsFormattedNumber(Int32($0), environment.dateTimeFormat.groupingSeparator))**").string },
ribbon: product.discount.flatMap {
GiftItemComponent.Ribbon(
text: "-\($0)%",
@ -1047,10 +1053,9 @@ final class GiftOptionsScreenComponent: Component {
))
if let transferStarGifts = self.state?.transferStarGifts, !transferStarGifts.isEmpty {
//TODO:localize
tabSelectorItems.append(TabSelectorComponent.Item(
id: AnyHashable(StarsFilter.transfer.rawValue),
title: "My Gifts"
title: strings.Gift_Options_Gift_Filter_MyGifts
))
}
@ -1224,6 +1229,7 @@ final class GiftOptionsScreenComponent: Component {
botUrl: "",
storeProductId: option.storeProductId
),
starsGiftOption: nil,
storeProduct: nil,
discount: nil
)
@ -1243,7 +1249,14 @@ final class GiftOptionsScreenComponent: Component {
if let product = availableProducts.first(where: { $0.id == option.storeProductId }), !product.isSubscription {
let fraction = Float(product.priceCurrencyAndAmount.amount) / Float(option.months) / Float(shortestOptionPrice.0)
let discountValue = Int(round((1.0 - fraction) * 20.0) * 5.0)
premiumProducts.append(PremiumGiftProduct(giftOption: option, storeProduct: product, discount: discountValue > 0 ? discountValue : nil))
let starsGiftOption = premiumOptions.first(where: { $0.currency == "XTR" && $0.months == option.months })
premiumProducts.append(PremiumGiftProduct(
giftOption: option,
starsGiftOption: starsGiftOption,
storeProduct: product,
discount: discountValue > 0 ? discountValue : nil
))
}
}
self.premiumProducts = premiumProducts.sorted(by: { $0.months < $1.months })

View file

@ -129,6 +129,7 @@ final class GiftSetupScreenComponent: Component {
private var recenterOnTag: NSObject?
private var peerMap: [EnginePeer.Id: EnginePeer] = [:]
private var sendPaidMessageStars: StarsAmount?
private var starImage: (UIImage, PresentationTheme)?
@ -223,24 +224,29 @@ final class GiftSetupScreenComponent: Component {
return
}
switch component.subject {
case .premium:
if self.payWithStars {
case let .premium(product):
if self.payWithStars, let starsPrice = product.starsPrice, let peer = self.peerMap[component.peerId] {
//TODO:localize
let controller = textAlertController(
context: component.context,
title: "Send a Gift",
text: "Are you sure you want to gift **Telegram Premium** to Alicia for **1500 Stars**?",
actions: [
TextAlertAction(type: .genericAction, title: "Cancel", action: {}),
TextAlertAction(type: .defaultAction, title: "Confirm", action: { [weak self] in
if let self {
self.proceedWithPremiumGift()
}
})
],
parseMarkdown: true
)
environment.controller()?.present(controller, in: .window(.root))
if let balance = component.context.starsContext?.currentState?.balance, balance.value < starsPrice {
self.proceedWithStarGift()
} else {
let priceString = presentationStringsFormattedNumber(Int32(starsPrice), environment.dateTimeFormat.groupingSeparator)
let controller = textAlertController(
context: component.context,
title: "Send a Gift",
text: "Are you sure you want to gift **Telegram Premium** to \(peer.compactDisplayTitle) for **\(priceString) Stars**?",
actions: [
TextAlertAction(type: .genericAction, title: "Cancel", action: {}),
TextAlertAction(type: .defaultAction, title: "Confirm", action: { [weak self] in
if let self {
self.proceedWithStarGift()
}
})
],
parseMarkdown: true
)
environment.controller()?.present(controller, in: .window(.root))
}
} else {
self.proceedWithPremiumGift()
}
@ -343,7 +349,7 @@ final class GiftSetupScreenComponent: Component {
}
private func proceedWithStarGift() {
guard let component = self.component, case let .starGift(starGift) = component.subject, let starsContext = component.context.starsContext, let starsState = starsContext.currentState else {
guard let component = self.component, let starsContext = component.context.starsContext, let starsState = starsContext.currentState else {
return
}
@ -351,9 +357,24 @@ final class GiftSetupScreenComponent: Component {
let presentationData = component.context.sharedContext.currentPresentationData.with { $0 }
let peerId = component.peerId
var finalPrice = starGift.price
if self.includeUpgrade, let upgradeStars = starGift.upgradeStars {
finalPrice += upgradeStars
let entities = generateChatInputTextEntities(self.textInputState.text)
var finalPrice: Int64
let source: BotPaymentInvoiceSource
switch component.subject {
case let .premium(product):
if let option = product.starsGiftOption {
finalPrice = option.amount
source = .premiumGift(peerId: peerId, option: option, text: self.textInputState.text.string, entities: entities)
} else {
fatalError()
}
case let .starGift(starGift):
finalPrice = starGift.price
if self.includeUpgrade, let upgradeStars = starGift.upgradeStars {
finalPrice += upgradeStars
}
source = .starGift(hideName: self.hideName, includeUpgrade: self.includeUpgrade, peerId: peerId, giftId: starGift.id, text: self.textInputState.text.string, entities: entities)
}
let proceed = { [weak self] in
@ -364,10 +385,6 @@ final class GiftSetupScreenComponent: Component {
self.inProgress = true
self.state?.updated()
let entities = generateChatInputTextEntities(self.textInputState.text)
let source: BotPaymentInvoiceSource = .starGift(hideName: self.hideName, includeUpgrade: self.includeUpgrade, peerId: peerId, giftId: starGift.id, text: self.textInputState.text.string, entities: entities)
let completion = component.completion
let signal = BotCheckoutController.InputData.fetch(context: component.context, source: source)
@ -384,7 +401,7 @@ final class GiftSetupScreenComponent: Component {
return
}
if peerId.namespace == Namespaces.Peer.CloudChannel {
if peerId.namespace == Namespaces.Peer.CloudChannel, case let .starGift(starGift) = component.subject {
var controllers = navigationController.viewControllers
controllers = controllers.filter { !($0 is GiftSetupScreen) && !($0 is GiftOptionsScreenProtocol) }
navigationController.setViewControllers(controllers, animated: true)
@ -544,9 +561,10 @@ final class GiftSetupScreenComponent: Component {
let _ = (component.context.engine.data.get(
TelegramEngine.EngineData.Item.Peer.Peer(id: component.peerId),
TelegramEngine.EngineData.Item.Peer.Peer(id: component.context.account.peerId)
TelegramEngine.EngineData.Item.Peer.Peer(id: component.context.account.peerId),
TelegramEngine.EngineData.Item.Peer.SendPaidMessageStars(id: component.peerId)
)
|> deliverOnMainQueue).start(next: { [weak self] peer, accountPeer in
|> deliverOnMainQueue).start(next: { [weak self] peer, accountPeer, sendPaidMessageStars in
guard let self else {
return
}
@ -556,6 +574,7 @@ final class GiftSetupScreenComponent: Component {
if let accountPeer {
self.peerMap[accountPeer.id] = accountPeer
}
self.sendPaidMessageStars = sendPaidMessageStars
self.state?.updated()
})
@ -661,15 +680,15 @@ final class GiftSetupScreenComponent: Component {
}
)
self.optionsDisposable = (component.context.engine.payments.starsTopUpOptions()
|> deliverOnMainQueue).start(next: { [weak self] options in
guard let self else {
return
}
self.options = options
})
if case let .starGift(gift) = component.subject {
self.optionsDisposable = (component.context.engine.payments.starsTopUpOptions()
|> deliverOnMainQueue).start(next: { [weak self] options in
guard let self else {
return
}
self.options = options
})
if let _ = gift.upgradeStars {
self.previewPromise.set(
component.context.engine.payments.starGiftUpgradePreview(giftId: gift.id)
@ -732,9 +751,6 @@ final class GiftSetupScreenComponent: Component {
let total: Int32 = availability.total
let position = CGFloat(remains) / CGFloat(total)
let sold = total - remains
//let remainsString = presentationStringsFormattedNumber(remains, environment.dateTimeFormat.groupingSeparator)
//let soldString = presentationStringsFormattedNumber(total - remains, environment.dateTimeFormat.groupingSeparator)
//let totalString = presentationStringsFormattedNumber(total, environment.dateTimeFormat.groupingSeparator)
let remainingCountSize = self.remainingCount.update(
transition: transition,
component: AnyComponent(RemainingCountComponent(
@ -772,50 +788,53 @@ final class GiftSetupScreenComponent: Component {
var introSectionItems: [AnyComponentWithIdentity<Empty>] = []
introSectionItems.append(AnyComponentWithIdentity(id: introSectionItems.count, component: AnyComponent(Rectangle(color: .clear, height: 346.0, tag: self.introPlaceholderTag))))
introSectionItems.append(AnyComponentWithIdentity(id: introSectionItems.count, component: AnyComponent(ListMultilineTextFieldItemComponent(
externalState: self.textInputState,
context: component.context,
theme: environment.theme,
strings: environment.strings,
initialText: "",
resetText: self.resetText.flatMap {
return ListMultilineTextFieldItemComponent.ResetText(value: $0)
},
placeholder: environment.strings.Gift_Send_Customize_MessagePlaceholder,
autocapitalizationType: .sentences,
autocorrectionType: .yes,
returnKeyType: .done,
characterLimit: Int(giftConfiguration.maxCaptionLength),
displayCharacterLimit: true,
emptyLineHandling: .notAllowed,
formatMenuAvailability: .available([.bold, .italic, .underline, .strikethrough, .spoiler]),
updated: { _ in
},
returnKeyAction: { [weak self] in
guard let self else {
return
}
if let titleView = self.introSection.findTaggedView(tag: self.textInputTag) as? ListMultilineTextFieldItemComponent.View {
titleView.endEditing(true)
}
},
textUpdateTransition: .spring(duration: 0.4),
inputMode: self.currentInputMode,
toggleInputMode: { [weak self] in
guard let self else {
return
}
switch self.currentInputMode {
case .keyboard:
self.currentInputMode = .emoji
case .emoji:
self.currentInputMode = .keyboard
}
self.state?.updated(transition: .spring(duration: 0.4))
},
tag: self.textInputTag
))))
self.resetText = nil
if self.sendPaidMessageStars == nil {
introSectionItems.append(AnyComponentWithIdentity(id: introSectionItems.count, component: AnyComponent(ListMultilineTextFieldItemComponent(
externalState: self.textInputState,
context: component.context,
theme: environment.theme,
strings: environment.strings,
initialText: "",
resetText: self.resetText.flatMap {
return ListMultilineTextFieldItemComponent.ResetText(value: $0)
},
placeholder: environment.strings.Gift_Send_Customize_MessagePlaceholder,
autocapitalizationType: .sentences,
autocorrectionType: .yes,
returnKeyType: .done,
characterLimit: Int(giftConfiguration.maxCaptionLength),
displayCharacterLimit: true,
emptyLineHandling: .notAllowed,
formatMenuAvailability: .available([.bold, .italic, .underline, .strikethrough, .spoiler]),
updated: { _ in
},
returnKeyAction: { [weak self] in
guard let self else {
return
}
if let titleView = self.introSection.findTaggedView(tag: self.textInputTag) as? ListMultilineTextFieldItemComponent.View {
titleView.endEditing(true)
}
},
textUpdateTransition: .spring(duration: 0.4),
inputMode: self.currentInputMode,
toggleInputMode: { [weak self] in
guard let self else {
return
}
switch self.currentInputMode {
case .keyboard:
self.currentInputMode = .emoji
case .emoji:
self.currentInputMode = .keyboard
}
self.state?.updated(transition: .spring(duration: 0.4))
},
tag: self.textInputTag
))))
self.resetText = nil
}
let footerAttributes = MarkdownAttributes(
body: MarkdownAttributeSet(font: Font.regular(13.0), textColor: environment.theme.list.freeTextColor),
@ -838,19 +857,7 @@ final class GiftSetupScreenComponent: Component {
maximumNumberOfLines: 0
))
case .starGift:
//TODO:unmock
//TODO:localize
if self.textInputState.hasText {
introFooter = AnyComponent(MultilineTextComponent(
text: .markdown(
text: "**\(peerName)** charges **250 Stars** for each message. That price has been added to the cost of the gift.",
attributes: footerAttributes
),
maximumNumberOfLines: 0
))
} else {
introFooter = nil
}
introFooter = nil
}
let introSectionSize = self.introSection.update(
@ -900,9 +907,8 @@ final class GiftSetupScreenComponent: Component {
let subject: ChatGiftPreviewItem.Subject
switch component.subject {
case let .premium(product):
if self.payWithStars {
//TODO:unmock
subject = .premium(months: product.months, amount: 1500, currency: "XTR")
if self.payWithStars, let starsPrice = product.starsPrice {
subject = .premium(months: product.months, amount: starsPrice, currency: "XTR")
} else {
let (currency, amount) = product.storeProduct?.priceCurrencyAndAmount ?? ("USD", 1)
subject = .premium(months: product.months, amount: amount, currency: currency)
@ -917,7 +923,6 @@ final class GiftSetupScreenComponent: Component {
peers.append(peer)
}
//TODO:unmock
let introContentSize = self.introContent.update(
transition: transition,
component: AnyComponent(
@ -960,11 +965,12 @@ final class GiftSetupScreenComponent: Component {
}
switch component.subject {
case .premium:
//TODO:unmock
//TODO:localize
if "".isEmpty {
let starsFooterRawString = "Your balance is **# 147 988**. [Get More Stars >]()"
case let .premium(product):
if let starsPrice = product.starsPrice {
let balance = component.context.starsContext?.currentState?.balance.value ?? 0
let balanceString = presentationStringsFormattedNumber(Int32(balance), environment.dateTimeFormat.groupingSeparator)
let starsFooterRawString = environment.strings.Gift_Send_PayWithStars_Info("# \(balanceString)").string
let starsFooterText = NSMutableAttributedString(attributedString: parseMarkdownIntoAttributedString(starsFooterRawString, attributes: footerAttributes))
if self.cachedChevronImage == nil || self.cachedChevronImage?.1 !== environment.theme {
@ -977,7 +983,8 @@ final class GiftSetupScreenComponent: Component {
starsFooterText.addAttribute(.attachment, value: chevronImage, range: NSRange(range, in: starsFooterText.string))
}
let starsAttributedText = NSMutableAttributedString(string: "Pay with #1500", font: Font.regular(presentationData.listsFontSize.baseDisplaySize), textColor: environment.theme.list.itemPrimaryTextColor)
let priceString = presentationStringsFormattedNumber(Int32(starsPrice), environment.dateTimeFormat.groupingSeparator)
let starsAttributedText = NSMutableAttributedString(string: environment.strings.Gift_Send_PayWithStars("#\(priceString)").string, font: Font.regular(presentationData.listsFontSize.baseDisplaySize), textColor: environment.theme.list.itemPrimaryTextColor)
let range = (starsAttributedText.string as NSString).range(of: "#")
if range.location != NSNotFound {
starsAttributedText.addAttribute(ChatTextInputAttributes.customEmoji, value: ChatTextInputTextCustomEmojiAttribute(interactivelySelectedFromPackId: nil, fileId: 0, file: nil, custom: .stars(tinted: false)), range: range)
@ -997,6 +1004,7 @@ final class GiftSetupScreenComponent: Component {
text: .plain(starsFooterText),
maximumNumberOfLines: 0,
highlightColor: environment.theme.list.itemAccentColor.withAlphaComponent(0.1),
highlightInset: UIEdgeInsets(top: 0.0, left: 0.0, bottom: 0.0, right: -8.0),
highlightAction: { attributes in
if let _ = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.URL)] {
return NSAttributedString.Key(rawValue: TelegramTextAttributes.URL)
@ -1005,9 +1013,18 @@ final class GiftSetupScreenComponent: Component {
}
},
tapAction: { [weak self] _, _ in
guard let _ = self else {
guard let self, let component = self.component, let controller = self.environment?.controller(), let starsContext = component.context.starsContext else {
return
}
let _ = (self.optionsPromise.get()
|> filter { $0 != nil }
|> take(1)
|> deliverOnMainQueue).startStandalone(next: { options in
let purchaseController = component.context.sharedContext.makeStarsPurchaseScreen(context: component.context, starsContext: starsContext, options: options ?? [], purpose: .generic, completion: { stars in
starsContext.add(balance: StarsAmount(value: stars, nanos: 0))
})
controller.push(purchaseController)
})
}
)),
items: [
@ -1243,10 +1260,9 @@ final class GiftSetupScreenComponent: Component {
let buttonString: String
switch component.subject {
case let .premium(product):
if self.payWithStars {
//TODO:unmock
let amountString = presentationStringsFormattedNumber(Int32(1500), presentationData.dateTimeFormat.groupingSeparator)
buttonString = "\(environment.strings.Gift_Send_Send) # \(amountString)"
if self.payWithStars, let starsPrice = product.starsPrice {
let amountString = presentationStringsFormattedNumber(Int32(starsPrice), presentationData.dateTimeFormat.groupingSeparator)
buttonString = "\(environment.strings.Gift_Send_Send) # \(amountString)"
} else {
let amountString = product.price
buttonString = "\(environment.strings.Gift_Send_Send) \(amountString)"
@ -1256,13 +1272,9 @@ final class GiftSetupScreenComponent: Component {
if self.includeUpgrade, let upgradePrice = starGift.upgradeStars {
finalPrice += upgradePrice
}
//TODO:unmock
if self.textInputState.hasText {
finalPrice += 250
}
let amountString = presentationStringsFormattedNumber(Int32(finalPrice), presentationData.dateTimeFormat.groupingSeparator)
let buttonTitle = isSelfGift ? environment.strings.Gift_Send_Buy : environment.strings.Gift_Send_Send
buttonString = "\(buttonTitle) # \(amountString)"
buttonString = "\(buttonTitle) # \(amountString)"
if let availability = starGift.availability, availability.remains == 0 {
buttonIsEnabled = false
}
@ -1272,7 +1284,8 @@ final class GiftSetupScreenComponent: Component {
if let range = buttonAttributedString.string.range(of: "#"), let starImage = self.starImage?.0 {
buttonAttributedString.addAttribute(.attachment, value: starImage, range: NSRange(range, in: buttonAttributedString.string))
buttonAttributedString.addAttribute(.foregroundColor, value: environment.theme.list.itemCheckColors.foregroundColor, range: NSRange(range, in: buttonAttributedString.string))
buttonAttributedString.addAttribute(.baselineOffset, value: 1.0, range: NSRange(range, in: buttonAttributedString.string))
buttonAttributedString.addAttribute(.baselineOffset, value: 1.5, range: NSRange(range, in: buttonAttributedString.string))
buttonAttributedString.addAttribute(.kern, value: 2.0, range: NSRange(range, in: buttonAttributedString.string))
}
let buttonSize = self.button.update(
@ -1744,6 +1757,7 @@ private struct GiftConfiguration {
public struct PremiumGiftProduct: Equatable {
public let giftOption: CachedPremiumGiftOption
public let starsGiftOption: CachedPremiumGiftOption?
public let storeProduct: InAppPurchaseManager.Product?
public let discount: Int?
@ -1759,12 +1773,18 @@ public struct PremiumGiftProduct: Equatable {
return self.storeProduct?.price ?? formatCurrencyAmount(self.giftOption.amount, currency: self.giftOption.currency)
}
public var pricePerMonth: String {
return self.storeProduct?.pricePerMonth(Int(self.months)) ?? ""
public var starsPrice: Int64? {
return self.starsGiftOption?.amount
}
public init(giftOption: CachedPremiumGiftOption, storeProduct: InAppPurchaseManager.Product?, discount: Int?) {
public init(
giftOption: CachedPremiumGiftOption,
starsGiftOption: CachedPremiumGiftOption?,
storeProduct: InAppPurchaseManager.Product?,
discount: Int?
) {
self.giftOption = giftOption
self.starsGiftOption = starsGiftOption
self.storeProduct = storeProduct
self.discount = discount
}

View file

@ -150,6 +150,7 @@ private final class GiftTransferAlertContentNode: AlertContentNode {
GiftItemComponent(
context: self.context,
theme: self.presentationTheme,
strings: self.strings,
peer: nil,
subject: .uniqueGift(gift: self.gift),
mode: .thumbnail

View file

@ -1745,7 +1745,7 @@ private final class GiftViewSheetContent: CombinedComponent {
if let upgradeStars, upgradeStars > 0 {
finalStars += upgradeStars
}
let valueString = "⭐️\(presentationStringsFormattedNumber(abs(Int32(finalStars)), dateTimeFormat.groupingSeparator))"
let valueString = "\(presentationStringsFormattedNumber(abs(Int32(finalStars)), dateTimeFormat.groupingSeparator))⭐️"
let valueAttributedString = NSMutableAttributedString(string: valueString, font: tableFont, textColor: tableTextColor)
let range = (valueAttributedString.string as NSString).range(of: "⭐️")
if range.location != NSNotFound {

View file

@ -146,6 +146,7 @@ private final class GiftWithdrawAlertContentNode: AlertContentNode {
GiftItemComponent(
context: self.context,
theme: self.presentationTheme,
strings: self.strings,
peer: nil,
subject: .uniqueGift(gift: self.gift),
mode: .thumbnail

View file

@ -176,7 +176,7 @@ public final class LoadingOverlayNode: ASDisplayNode {
let chatListPresentationData = ChatListPresentationData(theme: presentationData.theme, fontSize: presentationData.chatFontSize, strings: presentationData.strings, dateTimeFormat: presentationData.dateTimeFormat, nameSortOrder: presentationData.nameSortOrder, nameDisplayOrder: presentationData.nameDisplayOrder, disableAnimations: true)
let peer1: EnginePeer = .user(TelegramUser(id: EnginePeer.Id(namespace: Namespaces.Peer.CloudUser, id: EnginePeer.Id.Id._internalFromInt64Value(0)), accessHash: nil, firstName: "FirstName", lastName: nil, username: nil, phone: nil, photo: [], botInfo: nil, restrictionInfo: nil, flags: [], emojiStatus: nil, usernames: [], storiesHidden: nil, nameColor: nil, backgroundEmojiId: nil, profileColor: nil, profileBackgroundEmojiId: nil, subscriberCount: nil, verificationIconFileId: nil, sendPaidMessageStars: nil))
let peer1: EnginePeer = .user(TelegramUser(id: EnginePeer.Id(namespace: Namespaces.Peer.CloudUser, id: EnginePeer.Id.Id._internalFromInt64Value(0)), accessHash: nil, firstName: "FirstName", lastName: nil, username: nil, phone: nil, photo: [], botInfo: nil, restrictionInfo: nil, flags: [], emojiStatus: nil, usernames: [], storiesHidden: nil, nameColor: nil, backgroundEmojiId: nil, profileColor: nil, profileBackgroundEmojiId: nil, subscriberCount: nil, verificationIconFileId: nil))
let timestamp1: Int32 = 100000
let peers: [EnginePeer.Id: EnginePeer] = [:]
let interaction = ChatListNodeInteraction(context: context, animationCache: context.animationCache, animationRenderer: context.animationRenderer, activateSearch: {}, peerSelected: { _, _, _, _, _ in }, disabledPeerSelected: { _, _, _ in }, togglePeerSelected: { _, _ in }, togglePeersSelection: { _, _ in }, additionalCategorySelected: { _ in

View file

@ -1000,14 +1000,18 @@ private func settingsItems(data: PeerInfoScreenData?, context: AccountContext, p
}))
}
if let starsState = data.starsState {
let balanceText: String
if starsState.balance > StarsAmount.zero {
balanceText = presentationStringsFormattedNumber(starsState.balance, presentationData.dateTimeFormat.groupingSeparator)
} else {
balanceText = ""
}
if !isPremiumDisabled || starsState.balance > StarsAmount.zero {
items[.payment]!.append(PeerInfoScreenDisclosureItem(id: 102, label: .text(balanceText), text: presentationData.strings.Settings_Stars, icon: PresentationResourcesSettings.stars, action: {
let balanceText: NSAttributedString
if starsState.balance > StarsAmount.zero {
let formattedLabel = formatStarsAmountText(starsState.balance, dateTimeFormat: presentationData.dateTimeFormat)
let smallLabelFont = Font.regular(floor(presentationData.listsFontSize.itemListBaseFontSize / 17.0 * 13.0))
let labelFont = Font.regular(presentationData.listsFontSize.itemListBaseFontSize)
let labelColor = presentationData.theme.list.itemSecondaryTextColor
balanceText = tonAmountAttributedString(formattedLabel, integralFont: labelFont, fractionalFont: smallLabelFont, color: labelColor, decimalSeparator: presentationData.dateTimeFormat.decimalSeparator)
} else {
balanceText = NSAttributedString()
}
items[.payment]!.append(PeerInfoScreenDisclosureItem(id: 102, label: .attributedText(balanceText), text: presentationData.strings.Settings_Stars, icon: PresentationResourcesSettings.stars, action: {
interaction.openSettings(.stars)
}))
}
@ -1574,8 +1578,13 @@ private func infoItems(data: PeerInfoScreenData?, context: AccountContext, prese
}
if overallStarsBalance > StarsAmount.zero {
let string = "*\(presentationStringsFormattedNumber(starsBalance, presentationData.dateTimeFormat.groupingSeparator))"
let attributedString = NSMutableAttributedString(string: string, font: Font.regular(presentationData.listsFontSize.itemListBaseFontSize), textColor: presentationData.theme.list.itemSecondaryTextColor)
let formattedLabel = formatStarsAmountText(starsBalance, dateTimeFormat: presentationData.dateTimeFormat)
let smallLabelFont = Font.regular(floor(presentationData.listsFontSize.itemListBaseFontSize / 17.0 * 13.0))
let labelFont = Font.regular(presentationData.listsFontSize.itemListBaseFontSize)
let labelColor = presentationData.theme.list.itemSecondaryTextColor
let attributedString = tonAmountAttributedString(formattedLabel, integralFont: labelFont, fractionalFont: smallLabelFont, color: labelColor, decimalSeparator: presentationData.dateTimeFormat.decimalSeparator).mutableCopy() as! NSMutableAttributedString
attributedString.insert(NSAttributedString(string: "*", font: labelFont, textColor: labelColor), at: 0)
if let range = attributedString.string.range(of: "*") {
attributedString.addAttribute(ChatTextInputAttributes.customEmoji, value: ChatTextInputTextCustomEmojiAttribute(interactivelySelectedFromPackId: nil, fileId: 0, file: nil, custom: .stars(tinted: false)), range: NSRange(range, in: attributedString.string))
attributedString.addAttribute(.baselineOffset, value: 1.5, range: NSRange(range, in: attributedString.string))
@ -1854,17 +1863,24 @@ private func infoItems(data: PeerInfoScreenData?, context: AccountContext, prese
let overallStarsBalance = data.starsRevenueStatsState?.balances.overallRevenue ?? StarsAmount.zero
if overallRevenueBalance > 0 || overallStarsBalance > StarsAmount.zero {
var string = ""
let smallLabelFont = Font.regular(floor(presentationData.listsFontSize.itemListBaseFontSize / 17.0 * 13.0))
let labelFont = Font.regular(presentationData.listsFontSize.itemListBaseFontSize)
let labelColor = presentationData.theme.list.itemSecondaryTextColor
let attributedString = NSMutableAttributedString()
if overallRevenueBalance > 0 {
string.append("#\(formatTonAmountText(revenueBalance, dateTimeFormat: presentationData.dateTimeFormat))")
attributedString.append(NSAttributedString(string: "#\(formatTonAmountText(revenueBalance, dateTimeFormat: presentationData.dateTimeFormat))", font: labelFont, textColor: labelColor))
}
if overallStarsBalance > StarsAmount.zero {
if !string.isEmpty {
string.append(" ")
if !attributedString.string.isEmpty {
attributedString.append(NSAttributedString(string: " ", font: labelFont, textColor: labelColor))
}
string.append("*\(presentationStringsFormattedNumber(starsBalance, presentationData.dateTimeFormat.groupingSeparator))")
attributedString.append(NSAttributedString(string: "*", font: labelFont, textColor: labelColor))
let formattedLabel = formatStarsAmountText(starsBalance, dateTimeFormat: presentationData.dateTimeFormat)
let starsAttributedString = tonAmountAttributedString(formattedLabel, integralFont: labelFont, fractionalFont: smallLabelFont, color: labelColor, decimalSeparator: presentationData.dateTimeFormat.decimalSeparator).mutableCopy() as! NSMutableAttributedString
attributedString.append(starsAttributedString)
}
let attributedString = NSMutableAttributedString(string: string, font: Font.regular(presentationData.listsFontSize.itemListBaseFontSize), textColor: presentationData.theme.list.itemSecondaryTextColor)
if let range = attributedString.string.range(of: "#") {
attributedString.addAttribute(ChatTextInputAttributes.customEmoji, value: ChatTextInputTextCustomEmojiAttribute(interactivelySelectedFromPackId: nil, fileId: 0, file: nil, custom: .ton), range: NSRange(range, in: attributedString.string))
attributedString.addAttribute(.baselineOffset, value: 1.5, range: NSRange(range, in: attributedString.string))
@ -2048,9 +2064,14 @@ private func editingItems(data: PeerInfoScreenData?, state: PeerInfoState, chatL
}))
} else if !user.flags.contains(.isSupport) {
let compactName = EnginePeer(user).compactDisplayTitle
items[.peerDataSettings]!.append(PeerInfoScreenActionItem(id: ItemSuggest, text: presentationData.strings.UserInfo_SuggestPhoto(compactName).string, color: .accent, icon: UIImage(bundleImageName: "Peer Info/SuggestAvatar"), action: {
interaction.suggestPhoto()
}))
if let cachedData = data.cachedData as? CachedUserData, let _ = cachedData.sendPaidMessageStars {
} else {
items[.peerDataSettings]!.append(PeerInfoScreenActionItem(id: ItemSuggest, text: presentationData.strings.UserInfo_SuggestPhoto(compactName).string, color: .accent, icon: UIImage(bundleImageName: "Peer Info/SuggestAvatar"), action: {
interaction.suggestPhoto()
}))
}
let setText: String
if user.photo.first?.isPersonal == true || state.updatingAvatar != nil {
@ -9824,7 +9845,7 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro
private func openPremiumGift() {
let premiumGiftOptions = self.data?.premiumGiftOptions ?? []
let premiumOptions = premiumGiftOptions.filter { $0.users == 1 }.map { CachedPremiumGiftOption(months: $0.months, currency: $0.currency, amount: $0.amount, botUrl: "", storeProductId: $0.storeProductId) }
var hasBirthday = false
if let cachedUserData = self.data?.cachedData as? CachedUserData {
hasBirthday = hasBirthdayToday(cachedData: cachedUserData)

View file

@ -231,6 +231,7 @@ public final class PeerInfoGiftsPaneNode: ASDisplayNode, PeerInfoPaneNode, UIScr
GiftItemComponent(
context: self.context,
theme: params.presentationData.theme,
strings: params.presentationData.strings,
peer: peer,
subject: subject,
ribbon: ribbonText.flatMap { GiftItemComponent.Ribbon(text: $0, color: ribbonColor) },

View file

@ -718,7 +718,8 @@ final class PeerSelectionControllerNode: ASDisplayNode {
forwardMessageIds: strongSelf.presentationInterfaceState.interfaceState.forwardMessageIds ?? [],
canMakePaidContent: false,
currentPrice: nil,
hasTimers: false
hasTimers: false,
sendPaidMessageStars: nil
)),
hasEntityKeyboard: hasEntityKeyboard,
gesture: gesture,

View file

@ -1014,8 +1014,7 @@ public class SendInviteLinkScreen: ViewControllerComponentContainer {
profileColor: user.profileColor,
profileBackgroundEmojiId: user.profileBackgroundEmojiId,
subscriberCount: user.subscriberCount,
verificationIconFileId: user.verificationIconFileId,
sendPaidMessageStars: nil
verificationIconFileId: user.verificationIconFileId
)),
canInviteWithPremium: canInviteWithPremium,
premiumRequiredToContact: premiumRequiredToContact

View file

@ -123,6 +123,7 @@ final class GiftListItemComponent: Component {
GiftItemComponent(
context: component.context,
theme: component.theme,
strings: component.context.sharedContext.currentPresentationData.with { $0 }.strings,
peer: nil,
subject: .uniqueGift(gift: gift),
ribbon: nil,

View file

@ -225,12 +225,12 @@ final class PeerNameColorChatPreviewItemNode: ListViewItemNode {
var peers = SimpleDictionary<PeerId, Peer>()
var messages = SimpleDictionary<MessageId, Message>()
peers[authorPeerId] = TelegramUser(id: authorPeerId, accessHash: nil, firstName: messageItem.author, lastName: "", username: nil, phone: nil, photo: messageItem.photo, botInfo: nil, restrictionInfo: nil, flags: [], emojiStatus: nil, usernames: [], storiesHidden: nil, nameColor: messageItem.nameColor, backgroundEmojiId: messageItem.backgroundEmojiId, profileColor: nil, profileBackgroundEmojiId: nil, subscriberCount: nil, verificationIconFileId: nil, sendPaidMessageStars: nil)
peers[authorPeerId] = TelegramUser(id: authorPeerId, accessHash: nil, firstName: messageItem.author, lastName: "", username: nil, phone: nil, photo: messageItem.photo, botInfo: nil, restrictionInfo: nil, flags: [], emojiStatus: nil, usernames: [], storiesHidden: nil, nameColor: messageItem.nameColor, backgroundEmojiId: messageItem.backgroundEmojiId, profileColor: nil, profileBackgroundEmojiId: nil, subscriberCount: nil, verificationIconFileId: nil)
let replyMessageId = MessageId(peerId: peerId, namespace: 0, id: 3)
if let (replyAuthor, text, replyColor) = messageItem.reply {
peers[replyAuthorPeerId] = TelegramUser(id: authorPeerId, accessHash: nil, firstName: replyAuthor, lastName: "", username: nil, phone: nil, photo: [], botInfo: nil, restrictionInfo: nil, flags: [], emojiStatus: nil, usernames: [], storiesHidden: nil, nameColor: replyColor, backgroundEmojiId: messageItem.backgroundEmojiId, profileColor: nil, profileBackgroundEmojiId: nil, subscriberCount: nil, verificationIconFileId: nil, sendPaidMessageStars: nil)
peers[replyAuthorPeerId] = TelegramUser(id: authorPeerId, accessHash: nil, firstName: replyAuthor, lastName: "", username: nil, phone: nil, photo: [], botInfo: nil, restrictionInfo: nil, flags: [], emojiStatus: nil, usernames: [], storiesHidden: nil, nameColor: replyColor, backgroundEmojiId: messageItem.backgroundEmojiId, profileColor: nil, profileBackgroundEmojiId: nil, subscriberCount: nil, verificationIconFileId: nil)
messages[replyMessageId] = Message(stableId: 3, stableVersion: 0, id: replyMessageId, globallyUniqueId: nil, groupingKey: nil, groupInfo: nil, threadId: nil, timestamp: 66000, flags: [.Incoming], tags: [], globalTags: [], localTags: [], customTags: [], forwardInfo: nil, author: peers[replyAuthorPeerId], text: text, attributes: [], media: [], peers: peers, associatedMessages: SimpleDictionary(), associatedMessageIds: [], associatedMedia: [:], associatedThreadInfo: nil, associatedStories: [:])
}

View file

@ -159,7 +159,7 @@ final class PeerSelectionLoadingView: UIView {
if self.currentParams?.size != size || self.currentParams?.presentationData !== presentationData {
self.currentParams = (size, presentationData)
let peer1: EnginePeer = .user(TelegramUser(id: EnginePeer.Id(namespace: Namespaces.Peer.CloudUser, id: EnginePeer.Id.Id._internalFromInt64Value(0)), accessHash: nil, firstName: "FirstName", lastName: nil, username: nil, phone: nil, photo: [], botInfo: nil, restrictionInfo: nil, flags: [], emojiStatus: nil, usernames: [], storiesHidden: nil, nameColor: nil, backgroundEmojiId: nil, profileColor: nil, profileBackgroundEmojiId: nil, subscriberCount: nil, verificationIconFileId: nil, sendPaidMessageStars: nil))
let peer1: EnginePeer = .user(TelegramUser(id: EnginePeer.Id(namespace: Namespaces.Peer.CloudUser, id: EnginePeer.Id.Id._internalFromInt64Value(0)), accessHash: nil, firstName: "FirstName", lastName: nil, username: nil, phone: nil, photo: [], botInfo: nil, restrictionInfo: nil, flags: [], emojiStatus: nil, usernames: [], storiesHidden: nil, nameColor: nil, backgroundEmojiId: nil, profileColor: nil, profileBackgroundEmojiId: nil, subscriberCount: nil, verificationIconFileId: nil))
let items = (0 ..< 1).map { _ -> ContactsPeerItem in
return ContactsPeerItem(

View file

@ -304,7 +304,7 @@ class ReactionChatPreviewItemNode: ListViewItemNode {
var peers = SimpleDictionary<PeerId, Peer>()
let messages = SimpleDictionary<MessageId, Message>()
peers[userPeerId] = TelegramUser(id: userPeerId, accessHash: nil, firstName: item.strings.Settings_QuickReactionSetup_DemoMessageAuthor, lastName: "", username: nil, phone: nil, photo: [], botInfo: nil, restrictionInfo: nil, flags: [], emojiStatus: nil, usernames: [], storiesHidden: nil, nameColor: .blue, backgroundEmojiId: nil, profileColor: nil, profileBackgroundEmojiId: nil, subscriberCount: nil, verificationIconFileId: nil, sendPaidMessageStars: nil)
peers[userPeerId] = TelegramUser(id: userPeerId, accessHash: nil, firstName: item.strings.Settings_QuickReactionSetup_DemoMessageAuthor, lastName: "", username: nil, phone: nil, photo: [], botInfo: nil, restrictionInfo: nil, flags: [], emojiStatus: nil, usernames: [], storiesHidden: nil, nameColor: .blue, backgroundEmojiId: nil, profileColor: nil, profileBackgroundEmojiId: nil, subscriberCount: nil, verificationIconFileId: nil)
let messageText = item.strings.Settings_QuickReactionSetup_DemoMessageText

View file

@ -958,12 +958,12 @@ final class ThemeAccentColorControllerNode: ASDisplayNode, ASScrollViewDelegate
)
}
let selfPeer: EnginePeer = .user(TelegramUser(id: self.context.account.peerId, accessHash: nil, firstName: nil, lastName: nil, username: nil, phone: nil, photo: [], botInfo: nil, restrictionInfo: nil, flags: [], emojiStatus: nil, usernames: [], storiesHidden: nil, nameColor: nil, backgroundEmojiId: nil, profileColor: nil, profileBackgroundEmojiId: nil, subscriberCount: nil, verificationIconFileId: nil, sendPaidMessageStars: nil))
let peer1: EnginePeer = .user(TelegramUser(id: PeerId(namespace: Namespaces.Peer.CloudUser, id: PeerId.Id._internalFromInt64Value(1)), accessHash: nil, firstName: self.presentationData.strings.Appearance_ThemePreview_ChatList_1_Name, lastName: nil, username: nil, phone: nil, photo: [], botInfo: nil, restrictionInfo: nil, flags: [], emojiStatus: nil, usernames: [], storiesHidden: nil, nameColor: nil, backgroundEmojiId: nil, profileColor: nil, profileBackgroundEmojiId: nil, subscriberCount: nil, verificationIconFileId: nil, sendPaidMessageStars: nil))
let peer2: EnginePeer = .user(TelegramUser(id: PeerId(namespace: Namespaces.Peer.CloudUser, id: PeerId.Id._internalFromInt64Value(2)), accessHash: nil, firstName: self.presentationData.strings.Appearance_ThemePreview_ChatList_2_Name, lastName: nil, username: nil, phone: nil, photo: [], botInfo: nil, restrictionInfo: nil, flags: [], emojiStatus: nil, usernames: [], storiesHidden: nil, nameColor: nil, backgroundEmojiId: nil, profileColor: nil, profileBackgroundEmojiId: nil, subscriberCount: nil, verificationIconFileId: nil, sendPaidMessageStars: nil))
let selfPeer: EnginePeer = .user(TelegramUser(id: self.context.account.peerId, accessHash: nil, firstName: nil, lastName: nil, username: nil, phone: nil, photo: [], botInfo: nil, restrictionInfo: nil, flags: [], emojiStatus: nil, usernames: [], storiesHidden: nil, nameColor: nil, backgroundEmojiId: nil, profileColor: nil, profileBackgroundEmojiId: nil, subscriberCount: nil, verificationIconFileId: nil))
let peer1: EnginePeer = .user(TelegramUser(id: PeerId(namespace: Namespaces.Peer.CloudUser, id: PeerId.Id._internalFromInt64Value(1)), accessHash: nil, firstName: self.presentationData.strings.Appearance_ThemePreview_ChatList_1_Name, lastName: nil, username: nil, phone: nil, photo: [], botInfo: nil, restrictionInfo: nil, flags: [], emojiStatus: nil, usernames: [], storiesHidden: nil, nameColor: nil, backgroundEmojiId: nil, profileColor: nil, profileBackgroundEmojiId: nil, subscriberCount: nil, verificationIconFileId: nil))
let peer2: EnginePeer = .user(TelegramUser(id: PeerId(namespace: Namespaces.Peer.CloudUser, id: PeerId.Id._internalFromInt64Value(2)), accessHash: nil, firstName: self.presentationData.strings.Appearance_ThemePreview_ChatList_2_Name, lastName: nil, username: nil, phone: nil, photo: [], botInfo: nil, restrictionInfo: nil, flags: [], emojiStatus: nil, usernames: [], storiesHidden: nil, nameColor: nil, backgroundEmojiId: nil, profileColor: nil, profileBackgroundEmojiId: nil, subscriberCount: nil, verificationIconFileId: nil))
let peer3: EnginePeer = .channel(TelegramChannel(id: PeerId(namespace: Namespaces.Peer.CloudChannel, id: PeerId.Id._internalFromInt64Value(3)), accessHash: nil, title: self.presentationData.strings.Appearance_ThemePreview_ChatList_3_Name, username: nil, photo: [], creationDate: 0, version: 0, participationStatus: .member, info: .group(.init(flags: [])), flags: [], restrictionInfo: nil, adminRights: nil, bannedRights: nil, defaultBannedRights: nil, usernames: [], storiesHidden: nil, nameColor: nil, backgroundEmojiId: nil, profileColor: nil, profileBackgroundEmojiId: nil, emojiStatus: nil, approximateBoostLevel: nil, subscriptionUntilDate: nil, verificationIconFileId: nil, sendPaidMessageStars: nil))
let peer3Author: EnginePeer = .user(TelegramUser(id: PeerId(namespace: Namespaces.Peer.CloudUser, id: PeerId.Id._internalFromInt64Value(4)), accessHash: nil, firstName: self.presentationData.strings.Appearance_ThemePreview_ChatList_3_AuthorName, lastName: nil, username: nil, phone: nil, photo: [], botInfo: nil, restrictionInfo: nil, flags: [], emojiStatus: nil, usernames: [], storiesHidden: nil, nameColor: nil, backgroundEmojiId: nil, profileColor: nil, profileBackgroundEmojiId: nil, subscriberCount: nil, verificationIconFileId: nil, sendPaidMessageStars: nil))
let peer4: EnginePeer = .user(TelegramUser(id: PeerId(namespace: Namespaces.Peer.CloudUser, id: PeerId.Id._internalFromInt64Value(4)), accessHash: nil, firstName: self.presentationData.strings.Appearance_ThemePreview_ChatList_4_Name, lastName: nil, username: nil, phone: nil, photo: [], botInfo: nil, restrictionInfo: nil, flags: [], emojiStatus: nil, usernames: [], storiesHidden: nil, nameColor: nil, backgroundEmojiId: nil, profileColor: nil, profileBackgroundEmojiId: nil, subscriberCount: nil, verificationIconFileId: nil, sendPaidMessageStars: nil))
let peer3Author: EnginePeer = .user(TelegramUser(id: PeerId(namespace: Namespaces.Peer.CloudUser, id: PeerId.Id._internalFromInt64Value(4)), accessHash: nil, firstName: self.presentationData.strings.Appearance_ThemePreview_ChatList_3_AuthorName, lastName: nil, username: nil, phone: nil, photo: [], botInfo: nil, restrictionInfo: nil, flags: [], emojiStatus: nil, usernames: [], storiesHidden: nil, nameColor: nil, backgroundEmojiId: nil, profileColor: nil, profileBackgroundEmojiId: nil, subscriberCount: nil, verificationIconFileId: nil))
let peer4: EnginePeer = .user(TelegramUser(id: PeerId(namespace: Namespaces.Peer.CloudUser, id: PeerId.Id._internalFromInt64Value(4)), accessHash: nil, firstName: self.presentationData.strings.Appearance_ThemePreview_ChatList_4_Name, lastName: nil, username: nil, phone: nil, photo: [], botInfo: nil, restrictionInfo: nil, flags: [], emojiStatus: nil, usernames: [], storiesHidden: nil, nameColor: nil, backgroundEmojiId: nil, profileColor: nil, profileBackgroundEmojiId: nil, subscriberCount: nil, verificationIconFileId: nil))
let timestamp = self.referenceTimestamp
@ -1051,8 +1051,8 @@ final class ThemeAccentColorControllerNode: ASDisplayNode, ASScrollViewDelegate
let otherPeerId = self.context.account.peerId
var peers = SimpleDictionary<PeerId, Peer>()
var messages = SimpleDictionary<MessageId, Message>()
peers[peerId] = TelegramUser(id: peerId, accessHash: nil, firstName: self.presentationData.strings.Appearance_ThemePreview_Chat_2_ReplyName, lastName: "", username: nil, phone: nil, photo: [], botInfo: nil, restrictionInfo: nil, flags: [], emojiStatus: nil, usernames: [], storiesHidden: nil, nameColor: .blue, backgroundEmojiId: nil, profileColor: nil, profileBackgroundEmojiId: nil, subscriberCount: nil, verificationIconFileId: nil, sendPaidMessageStars: nil)
peers[otherPeerId] = TelegramUser(id: otherPeerId, accessHash: nil, firstName: self.presentationData.strings.Appearance_ThemePreview_Chat_2_ReplyName, lastName: "", username: nil, phone: nil, photo: [], botInfo: nil, restrictionInfo: nil, flags: [], emojiStatus: nil, usernames: [], storiesHidden: nil, nameColor: .blue, backgroundEmojiId: nil, profileColor: nil, profileBackgroundEmojiId: nil, subscriberCount: nil, verificationIconFileId: nil, sendPaidMessageStars: nil)
peers[peerId] = TelegramUser(id: peerId, accessHash: nil, firstName: self.presentationData.strings.Appearance_ThemePreview_Chat_2_ReplyName, lastName: "", username: nil, phone: nil, photo: [], botInfo: nil, restrictionInfo: nil, flags: [], emojiStatus: nil, usernames: [], storiesHidden: nil, nameColor: .blue, backgroundEmojiId: nil, profileColor: nil, profileBackgroundEmojiId: nil, subscriberCount: nil, verificationIconFileId: nil)
peers[otherPeerId] = TelegramUser(id: otherPeerId, accessHash: nil, firstName: self.presentationData.strings.Appearance_ThemePreview_Chat_2_ReplyName, lastName: "", username: nil, phone: nil, photo: [], botInfo: nil, restrictionInfo: nil, flags: [], emojiStatus: nil, usernames: [], storiesHidden: nil, nameColor: .blue, backgroundEmojiId: nil, profileColor: nil, profileBackgroundEmojiId: nil, subscriberCount: nil, verificationIconFileId: nil)
var sampleMessages: [Message] = []

View file

@ -1513,8 +1513,8 @@ final class WallpaperGalleryItemNode: GalleryItemNode {
let replyAuthor = self.presentationData.strings.Appearance_ThemePreview_Chat_2_ReplyName
var messageAuthor: Peer = TelegramUser(id: peerId, accessHash: nil, firstName: self.presentationData.strings.Appearance_PreviewReplyAuthor, lastName: "", username: nil, phone: nil, photo: [], botInfo: nil, restrictionInfo: nil, flags: [], emojiStatus: nil, usernames: [], storiesHidden: nil, nameColor: .blue, backgroundEmojiId: nil, profileColor: nil, profileBackgroundEmojiId: nil, subscriberCount: nil, verificationIconFileId: nil, sendPaidMessageStars: nil)
let otherAuthor = TelegramUser(id: otherPeerId, accessHash: nil, firstName: replyAuthor, lastName: "", username: nil, phone: nil, photo: [], botInfo: nil, restrictionInfo: nil, flags: [], emojiStatus: nil, usernames: [], storiesHidden: nil, nameColor: .blue, backgroundEmojiId: nil, profileColor: nil, profileBackgroundEmojiId: nil, subscriberCount: nil, verificationIconFileId: nil, sendPaidMessageStars: nil)
var messageAuthor: Peer = TelegramUser(id: peerId, accessHash: nil, firstName: self.presentationData.strings.Appearance_PreviewReplyAuthor, lastName: "", username: nil, phone: nil, photo: [], botInfo: nil, restrictionInfo: nil, flags: [], emojiStatus: nil, usernames: [], storiesHidden: nil, nameColor: .blue, backgroundEmojiId: nil, profileColor: nil, profileBackgroundEmojiId: nil, subscriberCount: nil, verificationIconFileId: nil)
let otherAuthor = TelegramUser(id: otherPeerId, accessHash: nil, firstName: replyAuthor, lastName: "", username: nil, phone: nil, photo: [], botInfo: nil, restrictionInfo: nil, flags: [], emojiStatus: nil, usernames: [], storiesHidden: nil, nameColor: .blue, backgroundEmojiId: nil, profileColor: nil, profileBackgroundEmojiId: nil, subscriberCount: nil, verificationIconFileId: nil)
peers[otherPeerId] = otherAuthor
var messageAttributes: [MessageAttribute] = []

View file

@ -124,6 +124,7 @@ public final class StarsAvatarComponent: Component {
GiftItemComponent(
context: component.context,
theme: component.theme,
strings: component.context.sharedContext.currentPresentationData.with { $0 }.strings,
peer: nil,
subject: .uniqueGift(gift: gift),
mode: .thumbnail

View file

@ -241,6 +241,12 @@ private final class StarsPurchaseScreenContentComponent: CombinedComponent {
textString = strings.Stars_Purchase_StarGiftInfo(component.peers.first?.value.compactDisplayTitle ?? "").string
case .upgradeStarGift:
textString = strings.Stars_Purchase_UpgradeStarGiftInfo
case let .sendMessage(peerId, _):
if peerId.namespace == Namespaces.Peer.CloudUser {
textString = strings.Stars_Purchase_SendMessageInfo(component.peers.first?.value.compactDisplayTitle ?? "").string
} else {
textString = strings.Stars_Purchase_SendGroupMessageInfo(component.peers.first?.value.compactDisplayTitle ?? "").string
}
}
let markdownAttributes = MarkdownAttributes(body: MarkdownAttributeSet(font: textFont, textColor: textColor), bold: MarkdownAttributeSet(font: boldTextFont, textColor: textColor), link: MarkdownAttributeSet(font: textFont, textColor: accentColor), linkAttribute: { contents in
@ -822,7 +828,7 @@ private final class StarsPurchaseScreenComponent: CombinedComponent {
titleText = strings.Stars_Purchase_GetStars
case .gift:
titleText = strings.Stars_Purchase_GiftStars
case let .topUp(requiredStars, _), let .transfer(_, requiredStars), let .reactions(_, requiredStars), let .subscription(_, requiredStars, _), let .unlockMedia(requiredStars), let .starGift(_, requiredStars), let .upgradeStarGift(requiredStars):
case let .topUp(requiredStars, _), let .transfer(_, requiredStars), let .reactions(_, requiredStars), let .subscription(_, requiredStars, _), let .unlockMedia(requiredStars), let .starGift(_, requiredStars), let .upgradeStarGift(requiredStars), let .sendMessage(_, requiredStars):
titleText = strings.Stars_Purchase_StarsNeeded(Int32(requiredStars))
}
@ -849,14 +855,14 @@ private final class StarsPurchaseScreenComponent: CombinedComponent {
availableSize: context.availableSize,
transition: .immediate
)
let starsBalance: StarsAmount = state.starsState?.balance ?? StarsAmount.zero
let formattedBalance = formatStarsAmountText(state.starsState?.balance ?? StarsAmount.zero, dateTimeFormat: environment.dateTimeFormat)
let smallLabelFont = Font.regular(11.0)
let labelFont = Font.semibold(14.0)
let balanceText = tonAmountAttributedString(formattedBalance, integralFont: labelFont, fractionalFont: smallLabelFont, color: environment.theme.actionSheet.primaryTextColor, decimalSeparator: environment.dateTimeFormat.decimalSeparator)
let balanceValue = balanceValue.update(
component: MultilineTextComponent(
text: .plain(NSAttributedString(
string: presentationStringsFormattedNumber(starsBalance, environment.dateTimeFormat.groupingSeparator),
font: Font.semibold(14.0),
textColor: environment.theme.actionSheet.primaryTextColor
)),
text: .plain(balanceText),
maximumNumberOfLines: 1
),
availableSize: context.availableSize,
@ -1245,6 +1251,8 @@ private extension StarsPurchasePurpose {
return [peerId]
case let .starGift(peerId, _):
return [peerId]
case let .sendMessage(peerId, _):
return [peerId]
default:
return []
}
@ -1266,6 +1274,8 @@ private extension StarsPurchasePurpose {
return requiredStars
case let .upgradeStarGift(requiredStars):
return requiredStars
case let .sendMessage(_, requiredStars):
return requiredStars
default:
return nil
}

View file

@ -19,6 +19,7 @@ swift_library(
"//submodules/Components/ViewControllerComponent",
"//submodules/Components/ComponentDisplayAdapters",
"//submodules/Components/MultilineTextComponent",
"//submodules/Components/MultilineTextWithEntitiesComponent",
"//submodules/Components/BalancedTextComponent",
"//submodules/TelegramPresentationData",
"//submodules/AccountContext",

View file

@ -12,6 +12,7 @@ import ComponentFlow
import ViewControllerComponent
import SheetComponent
import MultilineTextComponent
import MultilineTextWithEntitiesComponent
import BundleIconComponent
import SolidRoundedButtonComponent
import Markdown
@ -37,6 +38,7 @@ private final class StarsTransactionSheetContent: CombinedComponent {
let openMessage: (EngineMessage.Id) -> Void
let openMedia: ([Media], @escaping (Media) -> (ASDisplayNode, CGRect, () -> (UIView?, UIView?))?, @escaping (UIView) -> Void) -> Void
let openAppExamples: () -> Void
let openPaidMessageFee: () -> Void
let copyTransactionId: (String) -> Void
let updateSubscription: () -> Void
let sendGift: (EnginePeer.Id) -> Void
@ -49,6 +51,7 @@ private final class StarsTransactionSheetContent: CombinedComponent {
openMessage: @escaping (EngineMessage.Id) -> Void,
openMedia: @escaping ([Media], @escaping (Media) -> (ASDisplayNode, CGRect, () -> (UIView?, UIView?))?, @escaping (UIView) -> Void) -> Void,
openAppExamples: @escaping () -> Void,
openPaidMessageFee: @escaping () -> Void,
copyTransactionId: @escaping (String) -> Void,
updateSubscription: @escaping () -> Void,
sendGift: @escaping (EnginePeer.Id) -> Void
@ -60,6 +63,7 @@ private final class StarsTransactionSheetContent: CombinedComponent {
self.openMessage = openMessage
self.openMedia = openMedia
self.openAppExamples = openAppExamples
self.openPaidMessageFee = openPaidMessageFee
self.copyTransactionId = copyTransactionId
self.updateSubscription = updateSubscription
self.sendGift = sendGift
@ -238,6 +242,7 @@ private final class StarsTransactionSheetContent: CombinedComponent {
var isGiftUpgrade = false
var giftAvailability: StarGift.Gift.Availability?
var isRefProgram = false
var isPaidMessage = false
var delayedCloseOnOpenPeer = true
switch subject {
@ -414,14 +419,21 @@ private final class StarsTransactionSheetContent: CombinedComponent {
isGift = true
} else if let starrefCommissionPermille = transaction.starrefCommissionPermille {
isRefProgram = true
if transaction.starrefPeerId == nil {
if transaction.flags.contains(.isPaidMessage) {
isPaidMessage = true
titleText = strings.Stars_Transaction_PaidMessage(transaction.paidMessageCount ?? 1)
countOnTop = true
descriptionText = strings.Stars_Transaction_PaidMessage_Text(formatPermille(starrefCommissionPermille)).string
} else if transaction.starrefPeerId == nil {
titleText = strings.StarsTransaction_TitleCommission(formatPermille(starrefCommissionPermille)).string
countOnTop = false
descriptionText = ""
} else {
titleText = transaction.title ?? " "
countOnTop = false
descriptionText = ""
}
descriptionText = ""
count = transaction.count
countOnTop = false
transactionId = transaction.id
date = transaction.date
transactionPeer = transaction.peer
@ -443,7 +455,10 @@ private final class StarsTransactionSheetContent: CombinedComponent {
} else {
switch transaction.peer {
case let .peer(peer):
if !transaction.media.isEmpty {
if transaction.flags.contains(.isPaidMessage) {
isPaidMessage = true
titleText = strings.Stars_Transaction_PaidMessage(transaction.paidMessageCount ?? 1)
} else if !transaction.media.isEmpty {
titleText = strings.Stars_Transaction_MediaPurchase
} else {
titleText = transaction.title ?? peer.compactDisplayTitle
@ -613,7 +628,7 @@ private final class StarsTransactionSheetContent: CombinedComponent {
}
let absCount = StarsAmount(value: abs(count.value), nanos: abs(count.nanos))
let formattedAmount = presentationStringsFormattedNumber(absCount, dateTimeFormat.groupingSeparator)
let formattedAmount = formatStarsAmountText(absCount, dateTimeFormat: dateTimeFormat)
let countColor: UIColor
var countFont: UIFont = isSubscription || isSubscriber ? Font.regular(17.0) : Font.semibold(17.0)
var countBackgroundColor: UIColor?
@ -735,8 +750,15 @@ private final class StarsTransactionSheetContent: CombinedComponent {
transition: .immediate
)
}
let amountAttributedText = NSMutableAttributedString(string: amountText, font: countFont, textColor: countColor)
let amountAttributedText: NSAttributedString
if amountText.contains(environment.dateTimeFormat.decimalSeparator) {
let smallCountFont = Font.regular(14.0)
amountAttributedText = tonAmountAttributedString(amountText, integralFont: countFont, fractionalFont: smallCountFont, color: countColor, decimalSeparator: environment.dateTimeFormat.decimalSeparator)
} else {
amountAttributedText = NSAttributedString(string: amountText, font: countFont, textColor: countColor)
}
let amount = amount.update(
component: BalancedTextComponent(
text: .plain(amountAttributedText),
@ -758,6 +780,7 @@ private final class StarsTransactionSheetContent: CombinedComponent {
)
let tableFont = Font.regular(15.0)
let tableBoldFont = Font.semibold(15.0)
let tableTextColor = theme.list.itemPrimaryTextColor
let tableLinkColor = theme.list.itemAccentColor
var tableItems: [TableComponent.Item] = []
@ -1027,38 +1050,41 @@ private final class StarsTransactionSheetContent: CombinedComponent {
}
}
if let starRefPeerId = transaction.starrefPeerId, let starRefPeer = state.peerMap[starRefPeerId] {
tableItems.append(.init(
id: "to",
title: strings.StarsTransaction_StarRefReason_Affiliate,
component: AnyComponent(
Button(
content: AnyComponent(
PeerCellComponent(
context: component.context,
theme: theme,
peer: starRefPeer
)
),
action: {
if delayedCloseOnOpenPeer {
component.openPeer(starRefPeer, false)
Queue.mainQueue().after(1.0, {
component.cancel(false)
})
} else {
if let controller = controller() as? StarsTransactionScreen, let navigationController = controller.navigationController, let chatController = navigationController.viewControllers.first(where: { $0 is ChatController }) as? ChatController {
chatController.playShakeAnimation()
if !transaction.flags.contains(.isPaidMessage) {
tableItems.append(.init(
id: "to",
title: strings.StarsTransaction_StarRefReason_Affiliate,
component: AnyComponent(
Button(
content: AnyComponent(
PeerCellComponent(
context: component.context,
theme: theme,
peer: starRefPeer
)
),
action: {
if delayedCloseOnOpenPeer {
component.openPeer(starRefPeer, false)
Queue.mainQueue().after(1.0, {
component.cancel(false)
})
} else {
if let controller = controller() as? StarsTransactionScreen, let navigationController = controller.navigationController, let chatController = navigationController.viewControllers.first(where: { $0 is ChatController }) as? ChatController {
chatController.playShakeAnimation()
}
component.cancel(true)
}
component.cancel(true)
}
}
)
)
)
))
))
}
if let toPeer {
tableItems.append(.init(
id: "referred",
title: strings.StarsTransaction_StarRefReason_Referred,
title: transaction.flags.contains(.isPaidMessage) ? strings.Stars_Transaction_From : strings.StarsTransaction_StarRefReason_Referred,
component: AnyComponent(
Button(
content: AnyComponent(
@ -1087,13 +1113,41 @@ private final class StarsTransactionSheetContent: CombinedComponent {
}
}
if let starrefCommissionPermille = transaction.starrefCommissionPermille, transaction.starrefPeerId != nil {
tableItems.append(.init(
id: "commission",
title: strings.StarsTransaction_Commission,
component: AnyComponent(MultilineTextComponent(text: .plain(NSAttributedString(string: "\(formatPermille(starrefCommissionPermille))%", font: tableFont, textColor: tableTextColor))
)),
insets: UIEdgeInsets(top: 0.0, left: 12.0, bottom: 0.0, right: 5.0)
))
if transaction.flags.contains(.isPaidMessage) {
var totalStars = transaction.count
if let starrefCount = transaction.starrefAmount {
totalStars = totalStars + starrefCount
}
let valueString = "\(presentationStringsFormattedNumber(abs(Int32(totalStars.value)), dateTimeFormat.groupingSeparator))⭐️"
let valueAttributedString = NSMutableAttributedString(string: valueString, font: tableBoldFont, textColor: theme.list.itemDisclosureActions.constructive.fillColor)
let range = (valueAttributedString.string as NSString).range(of: "⭐️")
if range.location != NSNotFound {
valueAttributedString.addAttribute(ChatTextInputAttributes.customEmoji, value: ChatTextInputTextCustomEmojiAttribute(interactivelySelectedFromPackId: nil, fileId: 0, file: nil, custom: .stars(tinted: false)), range: range)
valueAttributedString.addAttribute(.baselineOffset, value: 1.0, range: range)
}
tableItems.append(.init(
id: "paid",
title: strings.Stars_Transaction_Paid,
component: AnyComponent(
MultilineTextWithEntitiesComponent(
context: component.context,
animationCache: component.context.animationCache,
animationRenderer: component.context.animationRenderer,
placeholderColor: theme.list.mediaPlaceholderColor,
text: .plain(valueAttributedString),
maximumNumberOfLines: 0
)
),
insets: UIEdgeInsets(top: 0.0, left: 12.0, bottom: 0.0, right: 5.0)
))
} else {
tableItems.append(.init(
id: "commission",
title: strings.StarsTransaction_StarRefReason_Commission,
component: AnyComponent(MultilineTextComponent(text: .plain(NSAttributedString(string: "\(formatPermille(starrefCommissionPermille))%", font: tableFont, textColor: tableTextColor)))),
insets: UIEdgeInsets(top: 0.0, left: 12.0, bottom: 0.0, right: 5.0)
))
}
}
}
@ -1234,19 +1288,21 @@ private final class StarsTransactionSheetContent: CombinedComponent {
var descriptionSize: CGSize = .zero
if !descriptionText.isEmpty {
let openAppExamples = component.openAppExamples
let openPaidMessageFee = component.openPaidMessageFee
if state.cachedChevronImage == nil || state.cachedChevronImage?.1 !== environment.theme {
state.cachedChevronImage = (generateTintedImage(image: UIImage(bundleImageName: "Settings/TextArrowRight"), color: linkColor)!, theme)
}
let textColor = countOnTop && !isSubscriber ? theme.list.itemPrimaryTextColor : textColor
let markdownAttributes = MarkdownAttributes(body: MarkdownAttributeSet(font: textFont, textColor: textColor), bold: MarkdownAttributeSet(font: textFont, textColor: textColor), link: MarkdownAttributeSet(font: textFont, textColor: linkColor), linkAttribute: { contents in
let markdownAttributes = MarkdownAttributes(body: MarkdownAttributeSet(font: textFont, textColor: textColor), bold: MarkdownAttributeSet(font: boldTextFont, textColor: textColor), link: MarkdownAttributeSet(font: textFont, textColor: linkColor), linkAttribute: { contents in
return (TelegramTextAttributes.URL, contents)
})
let attributedString = parseMarkdownIntoAttributedString(descriptionText, attributes: markdownAttributes, textAlignment: .center).mutableCopy() as! NSMutableAttributedString
if let range = attributedString.string.range(of: ">"), let chevronImage = state.cachedChevronImage?.0 {
attributedString.addAttribute(.attachment, value: chevronImage, range: NSRange(range, in: attributedString.string))
}
let descriptionAvailableWidth = isPaidMessage ? context.availableSize.width - sideInset * 2.0 - 16.0 : context.availableSize.width - sideInset * 2.0 - 60.0
let description = description.update(
component: MultilineTextComponent(
text: .plain(attributedString),
@ -1264,11 +1320,15 @@ private final class StarsTransactionSheetContent: CombinedComponent {
},
tapAction: { attributes, _ in
if let _ = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.URL)] {
openAppExamples()
if isPaidMessage {
openPaidMessageFee()
} else {
openAppExamples()
}
}
}
),
availableSize: CGSize(width: context.availableSize.width - sideInset * 2.0 - 60.0, height: CGFloat.greatestFiniteMagnitude),
availableSize: CGSize(width: descriptionAvailableWidth, height: CGFloat.greatestFiniteMagnitude),
transition: .immediate
)
descriptionSize = description.size
@ -1473,6 +1533,7 @@ private final class StarsTransactionSheetComponent: CombinedComponent {
let openMessage: (EngineMessage.Id) -> Void
let openMedia: ([Media], @escaping (Media) -> (ASDisplayNode, CGRect, () -> (UIView?, UIView?))?, @escaping (UIView) -> Void) -> Void
let openAppExamples: () -> Void
let openPaidMessageFee: () -> Void
let copyTransactionId: (String) -> Void
let updateSubscription: () -> Void
let sendGift: (EnginePeer.Id) -> Void
@ -1484,6 +1545,7 @@ private final class StarsTransactionSheetComponent: CombinedComponent {
openMessage: @escaping (EngineMessage.Id) -> Void,
openMedia: @escaping ([Media], @escaping (Media) -> (ASDisplayNode, CGRect, () -> (UIView?, UIView?))?, @escaping (UIView) -> Void) -> Void,
openAppExamples: @escaping () -> Void,
openPaidMessageFee: @escaping () -> Void,
copyTransactionId: @escaping (String) -> Void,
updateSubscription: @escaping () -> Void,
sendGift: @escaping (EnginePeer.Id) -> Void
@ -1494,6 +1556,7 @@ private final class StarsTransactionSheetComponent: CombinedComponent {
self.openMessage = openMessage
self.openMedia = openMedia
self.openAppExamples = openAppExamples
self.openPaidMessageFee = openPaidMessageFee
self.copyTransactionId = copyTransactionId
self.updateSubscription = updateSubscription
self.sendGift = sendGift
@ -1540,6 +1603,7 @@ private final class StarsTransactionSheetComponent: CombinedComponent {
openMessage: context.component.openMessage,
openMedia: context.component.openMedia,
openAppExamples: context.component.openAppExamples,
openPaidMessageFee: context.component.openPaidMessageFee,
copyTransactionId: context.component.copyTransactionId,
updateSubscription: context.component.updateSubscription,
sendGift: context.component.sendGift
@ -1640,6 +1704,7 @@ public class StarsTransactionScreen: ViewControllerComponentContainer {
var openMessageImpl: ((EngineMessage.Id) -> Void)?
var openMediaImpl: (([Media], @escaping (Media) -> (ASDisplayNode, CGRect, () -> (UIView?, UIView?))?, @escaping (UIView) -> Void) -> Void)?
var openAppExamplesImpl: (() -> Void)?
var openPaidMessageFeeImpl: (() -> Void)?
var copyTransactionIdImpl: ((String) -> Void)?
var updateSubscriptionImpl: (() -> Void)?
var sendGiftImpl: ((EnginePeer.Id) -> Void)?
@ -1661,6 +1726,9 @@ public class StarsTransactionScreen: ViewControllerComponentContainer {
openAppExamples: {
openAppExamplesImpl?()
},
openPaidMessageFee: {
openPaidMessageFeeImpl?()
},
copyTransactionId: { transactionId in
copyTransactionIdImpl?(transactionId)
},
@ -1773,6 +1841,20 @@ public class StarsTransactionScreen: ViewControllerComponentContainer {
})
}
openPaidMessageFeeImpl = { [weak self] in
guard let self, let navigationController = self.navigationController as? NavigationController else {
return
}
self.dismissAnimated()
let _ = (context.engine.privacy.requestAccountPrivacySettings()
|> deliverOnMainQueue).start(next: { [weak navigationController] privacySettings in
let controller = context.sharedContext.makeIncomingMessagePrivacyScreen(context: context, value: privacySettings.globalSettings.nonContactChatsPrivacy, exceptions: privacySettings.noPaidMessages, update: { settingValue in
let _ = context.engine.privacy.updateNonContactChatsPrivacy(value: settingValue).start()
})
navigationController?.pushViewController(controller)
})
}
copyTransactionIdImpl = { [weak self] transactionId in
guard let self else {
return

View file

@ -140,12 +140,20 @@ final class StarsBalanceComponent: Component {
let sideInset: CGFloat = 16.0
var contentHeight: CGFloat = sideInset
let balanceString = presentationStringsFormattedNumber(component.count, component.dateTimeFormat.groupingSeparator)
let formattedLabel = formatStarsAmountText(component.count, dateTimeFormat: component.dateTimeFormat)
let labelFont: UIFont
if formattedLabel.contains(component.dateTimeFormat.decimalSeparator) {
labelFont = Font.with(size: 48.0, design: .round, weight: .semibold)
} else {
labelFont = Font.with(size: 48.0, design: .round, weight: .semibold)
}
let smallLabelFont = Font.with(size: 32.0, design: .round, weight: .regular)
let balanceString = tonAmountAttributedString(formattedLabel, integralFont: labelFont, fractionalFont: smallLabelFont, color: component.theme.list.itemPrimaryTextColor, decimalSeparator: component.dateTimeFormat.decimalSeparator)
let titleSize = self.title.update(
transition: .immediate,
component: AnyComponent(
MultilineTextComponent(
text: .plain(NSAttributedString(string: balanceString, font: Font.with(size: 48.0, design: .round, weight: .semibold), textColor: component.theme.list.itemPrimaryTextColor))
text: .plain(balanceString)
)
),
environment: {},

View file

@ -17,6 +17,7 @@ import BundleIconComponent
import PhotoResources
import StarsAvatarComponent
import GiftAnimationComponent
import TelegramStringFormatting
private extension StarsContext.State.Transaction {
var extendedId: String {
@ -303,7 +304,10 @@ final class StarsTransactionsListPanelComponent: Component {
var uniqueGift: StarGift.UniqueGift?
switch item.peer {
case let .peer(peer):
if let starGift = item.starGift {
if item.flags.contains(.isPaidMessage) {
itemTitle = peer.displayTitle(strings: environment.strings, displayOrder: .firstLast)
itemSubtitle = environment.strings.Stars_Intro_Transaction_PaidMessage(item.paidMessageCount ?? 1)
} else if let starGift = item.starGift {
if item.flags.contains(.isStarGiftUpgrade), case let .unique(gift) = starGift {
itemTitle = "\(gift.title) #\(gift.number)"
itemSubtitle = environment.strings.Stars_Intro_Transaction_GiftUpgrade
@ -380,16 +384,12 @@ final class StarsTransactionsListPanelComponent: Component {
}
let itemLabel: NSAttributedString
let labelString: String
let formattedLabel = formatStarsAmountText(item.count, dateTimeFormat: environment.dateTimeFormat, showPlus: true)
let absCount = StarsAmount(value: abs(item.count.value), nanos: abs(item.count.nanos))
let formattedLabel = presentationStringsFormattedNumber(absCount, environment.dateTimeFormat.groupingSeparator)
if item.count < StarsAmount.zero {
labelString = "- \(formattedLabel)"
} else {
labelString = "+ \(formattedLabel)"
}
itemLabel = NSAttributedString(string: labelString, font: Font.medium(fontBaseDisplaySize), textColor: labelString.hasPrefix("-") ? environment.theme.list.itemDestructiveColor : environment.theme.list.itemDisclosureActions.constructive.fillColor)
let smallLabelFont = Font.with(size: floor(fontBaseDisplaySize / 17.0 * 13.0))
let labelFont = Font.medium(fontBaseDisplaySize)
let labelColor = formattedLabel.hasPrefix("-") ? environment.theme.list.itemDestructiveColor : environment.theme.list.itemDisclosureActions.constructive.fillColor
itemLabel = tonAmountAttributedString(formattedLabel, integralFont: labelFont, fractionalFont: smallLabelFont, color: labelColor, decimalSeparator: environment.dateTimeFormat.decimalSeparator)
var itemDateColor = environment.theme.list.itemSecondaryTextColor
itemDate = stringForMediumCompactDate(timestamp: item.date, strings: environment.strings, dateTimeFormat: environment.dateTimeFormat)

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