Various improvements
This commit is contained in:
parent
eecb2ef5c0
commit
8d7f9bf372
122 changed files with 3454 additions and 1121 deletions
|
|
@ -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 >]()";
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -35,6 +35,7 @@ swift_library(
|
|||
"//submodules/TelegramUI/Components/MultiAnimationRenderer:MultiAnimationRenderer",
|
||||
"//submodules/TelegramUI/Components/TextNodeWithEntities:TextNodeWithEntities",
|
||||
"//submodules/TelegramUI/Components/Chat/ChatInputTextNode",
|
||||
"//submodules/AnimatedCountLabelNode",
|
||||
],
|
||||
visibility = [
|
||||
"//visibility:public",
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ import ContextUI
|
|||
import ChatPresentationInterfaceState
|
||||
import ComponentFlow
|
||||
import AccountContext
|
||||
import AnimatedCountLabelNode
|
||||
|
||||
final class AttachmentTextInputActionButtonsNode: ASDisplayNode, ChatSendMessageActionSheetControllerSourceSendButtonNode {
|
||||
private let strings: PresentationStrings
|
||||
|
|
@ -17,7 +18,7 @@ final class AttachmentTextInputActionButtonsNode: ASDisplayNode, ChatSendMessage
|
|||
let sendButton: HighlightTrackingButtonNode
|
||||
var sendButtonHasApplyIcon = false
|
||||
var animatingSendButton = false
|
||||
let textNode: ImmediateTextNode
|
||||
let textNode: ImmediateAnimatedCountLabelNode
|
||||
|
||||
private var theme: PresentationTheme
|
||||
|
||||
|
|
@ -46,8 +47,7 @@ final class AttachmentTextInputActionButtonsNode: ASDisplayNode, ChatSendMessage
|
|||
self.backgroundNode.clipsToBounds = true
|
||||
self.sendButton = HighlightTrackingButtonNode(pointerStyle: nil)
|
||||
|
||||
self.textNode = ImmediateTextNode()
|
||||
self.textNode.attributedText = NSAttributedString(string: self.strings.MediaPicker_Send, font: Font.semibold(17.0), textColor: theme.chat.inputPanel.actionControlForegroundColor)
|
||||
self.textNode = ImmediateAnimatedCountLabelNode()
|
||||
self.textNode.isUserInteractionEnabled = false
|
||||
|
||||
super.init()
|
||||
|
|
@ -104,8 +104,6 @@ final class AttachmentTextInputActionButtonsNode: ASDisplayNode, ChatSendMessage
|
|||
|
||||
func updateTheme(theme: PresentationTheme, wallpaper: TelegramWallpaper) {
|
||||
self.backgroundNode.backgroundColor = theme.chat.inputPanel.actionControlFillColor
|
||||
|
||||
self.textNode.attributedText = NSAttributedString(string: self.strings.MediaPicker_Send, font: Font.semibold(17.0), textColor: theme.chat.inputPanel.actionControlForegroundColor)
|
||||
}
|
||||
|
||||
private var absoluteRect: (CGRect, CGSize)?
|
||||
|
|
@ -113,20 +111,44 @@ final class AttachmentTextInputActionButtonsNode: ASDisplayNode, ChatSendMessage
|
|||
self.absoluteRect = (rect, containerSize)
|
||||
}
|
||||
|
||||
func updateLayout(size: CGSize, transition: ContainedViewLayoutTransition, minimized: Bool, interfaceState: ChatPresentationInterfaceState) -> CGSize {
|
||||
func updateLayout(size: CGSize, transition: ContainedViewLayoutTransition, minimized: Bool, text: String, interfaceState: ChatPresentationInterfaceState) -> CGSize {
|
||||
self.validLayout = size
|
||||
|
||||
let width: CGFloat
|
||||
let textSize = self.textNode.updateLayout(CGSize(width: 100.0, height: 100.0))
|
||||
|
||||
var titleOffset: CGFloat = 0.0
|
||||
var segments: [AnimatedCountLabelNode.Segment] = []
|
||||
var buttonInset: CGFloat = 18.0
|
||||
if text.hasPrefix("⭐️") {
|
||||
let font = Font.with(size: 17.0, design: .round, weight: .semibold, traits: .monospacedNumbers)
|
||||
let badgeString = NSMutableAttributedString(string: "⭐️ ", font: font, textColor: interfaceState.theme.chat.inputPanel.actionControlForegroundColor)
|
||||
if let range = badgeString.string.range(of: "⭐️") {
|
||||
badgeString.addAttribute(.attachment, value: PresentationResourcesChat.chatPlaceholderStarIcon(interfaceState.theme)!, range: NSRange(range, in: badgeString.string))
|
||||
badgeString.addAttribute(.baselineOffset, value: 1.0, range: NSRange(range, in: badgeString.string))
|
||||
}
|
||||
segments.append(.text(0, badgeString))
|
||||
for char in text {
|
||||
if let intValue = Int(String(char)) {
|
||||
segments.append(.number(intValue, NSAttributedString(string: String(char), font: font, textColor: interfaceState.theme.chat.inputPanel.actionControlForegroundColor)))
|
||||
}
|
||||
}
|
||||
titleOffset -= 2.0
|
||||
buttonInset = 14.0
|
||||
} else {
|
||||
segments.append(.text(0, NSAttributedString(string: text, font: Font.semibold(17.0), textColor: interfaceState.theme.chat.inputPanel.actionControlForegroundColor)))
|
||||
}
|
||||
self.textNode.segments = segments
|
||||
|
||||
let textSize = self.textNode.updateLayout(size: CGSize(width: 100.0, height: 100.0), animated: transition.isAnimated)
|
||||
if minimized {
|
||||
width = 44.0
|
||||
} else {
|
||||
width = textSize.width + 36.0
|
||||
width = textSize.width + buttonInset * 2.0
|
||||
}
|
||||
|
||||
let buttonSize = CGSize(width: width, height: size.height)
|
||||
|
||||
transition.updateFrame(node: self.textNode, frame: CGRect(origin: CGPoint(x: floorToScreenPixels((width - textSize.width) / 2.0), y: floorToScreenPixels((buttonSize.height - textSize.height) / 2.0)), size: textSize))
|
||||
transition.updateFrame(node: self.textNode, frame: CGRect(origin: CGPoint(x: floorToScreenPixels((width - textSize.width) / 2.0) + titleOffset, y: floorToScreenPixels((buttonSize.height - textSize.height) / 2.0)), size: textSize))
|
||||
transition.updateAlpha(node: self.textNode, alpha: minimized ? 0.0 : 1.0)
|
||||
transition.updateAlpha(node: self.sendButton.imageNode, alpha: minimized ? 1.0 : 0.0)
|
||||
|
||||
|
|
|
|||
|
|
@ -750,7 +750,7 @@ public class AttachmentTextInputPanelNode: ASDisplayNode, TGCaptionPanelView, AS
|
|||
}
|
||||
|
||||
self.theme = interfaceState.theme
|
||||
|
||||
|
||||
self.actionButtons.updateTheme(theme: interfaceState.theme, wallpaper: interfaceState.chatWallpaper)
|
||||
|
||||
let textFieldMinHeight = calclulateTextFieldMinHeight(interfaceState, metrics: metrics)
|
||||
|
|
@ -957,7 +957,17 @@ public class AttachmentTextInputPanelNode: ASDisplayNode, TGCaptionPanelView, AS
|
|||
var textBackgroundInset: CGFloat = 0.0
|
||||
let actionButtonsSize: CGSize
|
||||
if let presentationInterfaceState = self.presentationInterfaceState {
|
||||
actionButtonsSize = self.actionButtons.updateLayout(size: CGSize(width: 44.0, height: minimalHeight), transition: transition, minimized: !self.isAttachment || inputHasText, interfaceState: presentationInterfaceState)
|
||||
let isMinimized: Bool
|
||||
let text: String
|
||||
if let sendPaidMessageStars = presentationInterfaceState.sendPaidMessageStars {
|
||||
isMinimized = false
|
||||
let count = max(1, presentationInterfaceState.interfaceState.forwardMessageIds?.count ?? 1)
|
||||
text = "⭐️\(sendPaidMessageStars.value * Int64(count))"
|
||||
} else {
|
||||
isMinimized = !self.isAttachment || inputHasText
|
||||
text = presentationInterfaceState.strings.MediaPicker_Send
|
||||
}
|
||||
actionButtonsSize = self.actionButtons.updateLayout(size: CGSize(width: 44.0, height: minimalHeight), transition: transition, minimized: isMinimized, text: text, interfaceState: presentationInterfaceState)
|
||||
textBackgroundInset = 44.0 - actionButtonsSize.width
|
||||
} else {
|
||||
actionButtonsSize = CGSize(width: 44.0, height: minimalHeight)
|
||||
|
|
|
|||
|
|
@ -1141,7 +1141,7 @@ public class AttachmentController: ViewController, MinimizableController {
|
|||
}
|
||||
|
||||
let isEffecitvelyCollapsedUpdated = (self.selectionCount > 0) != (self.panel.isSelecting)
|
||||
let panelHeight = self.panel.update(layout: containerLayout, buttons: self.controller?.buttons ?? [], isSelecting: self.selectionCount > 0, elevateProgress: !hasPanel && !hasButton, transition: transition)
|
||||
let panelHeight = self.panel.update(layout: containerLayout, buttons: self.controller?.buttons ?? [], isSelecting: self.selectionCount > 0, selectionCount: self.selectionCount, elevateProgress: !hasPanel && !hasButton, transition: transition)
|
||||
if hasPanel || hasButton {
|
||||
containerInsets.bottom = panelHeight
|
||||
}
|
||||
|
|
|
|||
|
|
@ -824,6 +824,7 @@ final class AttachmentPanel: ASDisplayNode, ASScrollViewDelegate {
|
|||
private var presentationData: PresentationData
|
||||
private var updatedPresentationData: (initial: PresentationData, signal: Signal<PresentationData, NoError>)?
|
||||
private var presentationDataDisposable: Disposable?
|
||||
private var peerDisposable: Disposable?
|
||||
|
||||
private var iconDisposables: [MediaId: Disposable] = [:]
|
||||
|
||||
|
|
@ -852,6 +853,8 @@ final class AttachmentPanel: ASDisplayNode, ASScrollViewDelegate {
|
|||
private var buttons: [AttachmentButtonType] = []
|
||||
private var selectedIndex: Int = 0
|
||||
private(set) var isSelecting: Bool = false
|
||||
private var selectionCount: Int = 0
|
||||
|
||||
private var _isButtonVisible: Bool = false
|
||||
var isButtonVisible: Bool {
|
||||
return self.mainButtonState.isVisible || self.secondaryButtonState.isVisible
|
||||
|
|
@ -1167,7 +1170,8 @@ final class AttachmentPanel: ASDisplayNode, ASScrollViewDelegate {
|
|||
forwardMessageIds: strongSelf.presentationInterfaceState.interfaceState.forwardMessageIds ?? [],
|
||||
canMakePaidContent: canMakePaidContent,
|
||||
currentPrice: currentPrice,
|
||||
hasTimers: hasTimers
|
||||
hasTimers: hasTimers,
|
||||
sendPaidMessageStars: strongSelf.presentationInterfaceState.sendPaidMessageStars
|
||||
)),
|
||||
hasEntityKeyboard: hasEntityKeyboard,
|
||||
gesture: gesture,
|
||||
|
|
@ -1258,14 +1262,35 @@ final class AttachmentPanel: ASDisplayNode, ASScrollViewDelegate {
|
|||
strongSelf.updateChatPresentationInterfaceState({ $0.updatedTheme(presentationData.theme) })
|
||||
|
||||
if let layout = strongSelf.validLayout {
|
||||
let _ = strongSelf.update(layout: layout, buttons: strongSelf.buttons, isSelecting: strongSelf.isSelecting, elevateProgress: strongSelf.elevateProgress, transition: .immediate)
|
||||
let _ = strongSelf.update(layout: layout, buttons: strongSelf.buttons, isSelecting: strongSelf.isSelecting, selectionCount: strongSelf.selectionCount, elevateProgress: strongSelf.elevateProgress, transition: .immediate)
|
||||
}
|
||||
}
|
||||
}).strict()
|
||||
|
||||
if let peerId = chatLocation?.peerId {
|
||||
self.peerDisposable = ((self.context.account.viewTracker.peerView(peerId)
|
||||
|> map { view -> StarsAmount? in
|
||||
if let data = view.cachedData as? CachedUserData {
|
||||
return data.sendPaidMessageStars
|
||||
} else if let channel = peerViewMainPeer(view) as? TelegramChannel {
|
||||
return channel.sendPaidMessageStars
|
||||
} else {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|> distinctUntilChanged
|
||||
|> deliverOnMainQueue).start(next: { [weak self] amount in
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
self.updateChatPresentationInterfaceState({ $0.updatedSendPaidMessageStars(amount) })
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
||||
deinit {
|
||||
self.presentationDataDisposable?.dispose()
|
||||
self.peerDisposable?.dispose()
|
||||
for (_, disposable) in self.iconDisposables {
|
||||
disposable.dispose()
|
||||
}
|
||||
|
|
@ -1308,16 +1333,19 @@ final class AttachmentPanel: ASDisplayNode, ASScrollViewDelegate {
|
|||
self.updateChatPresentationInterfaceState(transition: animated ? .animated(duration: 0.4, curve: .spring) : .immediate, f, completion: completion)
|
||||
}
|
||||
|
||||
private func updateChatPresentationInterfaceState(transition: ContainedViewLayoutTransition, _ f: (ChatPresentationInterfaceState) -> ChatPresentationInterfaceState, completion externalCompletion: @escaping (ContainedViewLayoutTransition) -> Void = { _ in }) {
|
||||
private func updateChatPresentationInterfaceState(update: Bool = true, transition: ContainedViewLayoutTransition, _ f: (ChatPresentationInterfaceState) -> ChatPresentationInterfaceState, completion externalCompletion: @escaping (ContainedViewLayoutTransition) -> Void = { _ in }) {
|
||||
let presentationInterfaceState = f(self.presentationInterfaceState)
|
||||
|
||||
let updateInputTextState = self.presentationInterfaceState.interfaceState.effectiveInputState != presentationInterfaceState.interfaceState.effectiveInputState
|
||||
|
||||
self.presentationInterfaceState = presentationInterfaceState
|
||||
|
||||
if let textInputPanelNode = self.textInputPanelNode, updateInputTextState {
|
||||
textInputPanelNode.updateInputTextState(presentationInterfaceState.interfaceState.effectiveInputState, animated: transition.isAnimated)
|
||||
|
||||
self.textUpdated(presentationInterfaceState.interfaceState.effectiveInputState.inputText)
|
||||
if update {
|
||||
if let textInputPanelNode = self.textInputPanelNode, updateInputTextState {
|
||||
textInputPanelNode.updateInputTextState(presentationInterfaceState.interfaceState.effectiveInputState, animated: transition.isAnimated)
|
||||
|
||||
self.textUpdated(presentationInterfaceState.interfaceState.effectiveInputState.inputText)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -1672,10 +1700,23 @@ final class AttachmentPanel: ASDisplayNode, ASScrollViewDelegate {
|
|||
}
|
||||
}
|
||||
|
||||
func update(layout: ContainerViewLayout, buttons: [AttachmentButtonType], isSelecting: Bool, elevateProgress: Bool, transition: ContainedViewLayoutTransition) -> CGFloat {
|
||||
func update(layout: ContainerViewLayout, buttons: [AttachmentButtonType], isSelecting: Bool, selectionCount: Int, elevateProgress: Bool, transition: ContainedViewLayoutTransition) -> CGFloat {
|
||||
self.validLayout = layout
|
||||
self.buttons = buttons
|
||||
self.elevateProgress = elevateProgress
|
||||
|
||||
if selectionCount != self.selectionCount {
|
||||
self.selectionCount = selectionCount
|
||||
self.updateChatPresentationInterfaceState(update: false, transition: .immediate, { state in
|
||||
var selectedMessages: [EngineMessage.Id] = []
|
||||
for i in 0 ..< selectionCount {
|
||||
selectedMessages.append(EngineMessage.Id(peerId: PeerId(0), namespace: Namespaces.Message.Local, id: Int32(i)))
|
||||
}
|
||||
return state.updatedInterfaceState { state in
|
||||
return state.withUpdatedForwardMessageIds(selectedMessages)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
let isButtonVisibleUpdated = self._isButtonVisible != self.mainButtonState.isVisible
|
||||
self._isButtonVisible = self.mainButtonState.isVisible
|
||||
|
|
|
|||
|
|
@ -23,6 +23,7 @@ public enum SendMessageActionSheetControllerParams {
|
|||
public let canMakePaidContent: Bool
|
||||
public let currentPrice: Int64?
|
||||
public let hasTimers: Bool
|
||||
public let sendPaidMessageStars: StarsAmount?
|
||||
|
||||
public init(
|
||||
isScheduledMessages: Bool,
|
||||
|
|
@ -34,7 +35,8 @@ public enum SendMessageActionSheetControllerParams {
|
|||
forwardMessageIds: [EngineMessage.Id],
|
||||
canMakePaidContent: Bool,
|
||||
currentPrice: Int64?,
|
||||
hasTimers: Bool
|
||||
hasTimers: Bool,
|
||||
sendPaidMessageStars: StarsAmount?
|
||||
) {
|
||||
self.isScheduledMessages = isScheduledMessages
|
||||
self.mediaPreview = mediaPreview
|
||||
|
|
@ -46,6 +48,7 @@ public enum SendMessageActionSheetControllerParams {
|
|||
self.canMakePaidContent = canMakePaidContent
|
||||
self.currentPrice = currentPrice
|
||||
self.hasTimers = hasTimers
|
||||
self.sendPaidMessageStars = sendPaidMessageStars
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -455,6 +455,9 @@ final class ChatSendMessageContextScreenComponent: Component {
|
|||
if sendMessage.hasTimers {
|
||||
canSchedule = false
|
||||
}
|
||||
if let _ = sendMessage.sendPaidMessageStars {
|
||||
canSchedule = false
|
||||
}
|
||||
canMakePaidContent = sendMessage.canMakePaidContent
|
||||
currentPrice = sendMessage.currentPrice
|
||||
case .editMessage:
|
||||
|
|
|
|||
|
|
@ -30,6 +30,7 @@ public final class MultilineTextWithEntitiesComponent: Component {
|
|||
public let textShadowColor: UIColor?
|
||||
public let textStroke: (UIColor, CGFloat)?
|
||||
public let highlightColor: UIColor?
|
||||
public let highlightInset: UIEdgeInsets
|
||||
public let handleSpoilers: Bool
|
||||
public let manualVisibilityControl: Bool
|
||||
public let resetAnimationsOnVisibilityChange: Bool
|
||||
|
|
@ -53,6 +54,7 @@ public final class MultilineTextWithEntitiesComponent: Component {
|
|||
textShadowColor: UIColor? = nil,
|
||||
textStroke: (UIColor, CGFloat)? = nil,
|
||||
highlightColor: UIColor? = nil,
|
||||
highlightInset: UIEdgeInsets = .zero,
|
||||
handleSpoilers: Bool = false,
|
||||
manualVisibilityControl: Bool = false,
|
||||
resetAnimationsOnVisibilityChange: Bool = false,
|
||||
|
|
@ -75,6 +77,7 @@ public final class MultilineTextWithEntitiesComponent: Component {
|
|||
self.textShadowColor = textShadowColor
|
||||
self.textStroke = textStroke
|
||||
self.highlightColor = highlightColor
|
||||
self.highlightInset = highlightInset
|
||||
self.highlightAction = highlightAction
|
||||
self.handleSpoilers = handleSpoilers
|
||||
self.manualVisibilityControl = manualVisibilityControl
|
||||
|
|
@ -144,6 +147,10 @@ public final class MultilineTextWithEntitiesComponent: Component {
|
|||
return false
|
||||
}
|
||||
|
||||
if lhs.highlightInset != rhs.highlightInset {
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
|
|
@ -189,6 +196,7 @@ public final class MultilineTextWithEntitiesComponent: Component {
|
|||
self.textNode.textShadowColor = component.textShadowColor
|
||||
self.textNode.textStroke = component.textStroke
|
||||
self.textNode.linkHighlightColor = component.highlightColor
|
||||
self.textNode.linkHighlightInset = component.highlightInset
|
||||
self.textNode.highlightAttributeAction = component.highlightAction
|
||||
self.textNode.tapAttributeAction = component.tapAction
|
||||
self.textNode.longTapAttributeAction = component.longTapAction
|
||||
|
|
|
|||
|
|
@ -1793,6 +1793,25 @@ public class ContactsPeerItemNode: ItemListRevealOptionsItemNode {
|
|||
strongSelf.setRevealOptionsOpened(item.editing.revealed, animated: animated)
|
||||
}
|
||||
|
||||
if item.isAd {
|
||||
let adButton: HighlightableButtonNode
|
||||
if let current = strongSelf.adButton {
|
||||
adButton = current
|
||||
} else {
|
||||
adButton = HighlightableButtonNode()
|
||||
adButton.setImage(UIImage(bundleImageName: "Components/AdMock"), for: .normal)
|
||||
strongSelf.addSubnode(adButton)
|
||||
strongSelf.adButton = adButton
|
||||
|
||||
adButton.addTarget(strongSelf, action: #selector(strongSelf.adButtonPressed), forControlEvents: .touchUpInside)
|
||||
}
|
||||
|
||||
adButton.frame = CGRect(origin: CGPoint(x: params.width - 20.0 - 31.0 - 13.0, y: 11.0), size: CGSize(width: 31.0, height: 15.0))
|
||||
} else if let adButton = strongSelf.adButton {
|
||||
strongSelf.adButton = nil
|
||||
adButton.removeFromSupernode()
|
||||
}
|
||||
|
||||
strongSelf.updateEnableGestures()
|
||||
}
|
||||
})
|
||||
|
|
|
|||
|
|
@ -368,6 +368,86 @@ final class ChatImageGalleryItemNode: ZoomableContentGalleryItemNode {
|
|||
transition.updateFrame(node: self.statusNode, frame: CGRect(origin: CGPoint(), size: statusSize))
|
||||
}
|
||||
|
||||
private func setupImageRecognition(_ generate: @escaping (TransformImageArguments) -> DrawingContext?, dimensions: PixelDimensions) {
|
||||
guard let message = self.message, !message.isCopyProtected() && message.paidContent == nil else {
|
||||
return
|
||||
}
|
||||
let displaySize = dimensions.cgSize.fitted(CGSize(width: 1280.0, height: 1280.0)).dividedByScreenScale().integralFloor
|
||||
|
||||
self.recognitionDisposable.set((recognizedContent(context: self.context, image: { return generate(TransformImageArguments(corners: ImageCorners(), imageSize: displaySize, boundingSize: displaySize, intrinsicInsets: UIEdgeInsets()))?.generateImage() }, messageId: message.id)
|
||||
|> deliverOnMainQueue).start(next: { [weak self] results in
|
||||
if let strongSelf = self {
|
||||
strongSelf.recognizedContentNode?.removeFromSupernode()
|
||||
if !results.isEmpty {
|
||||
let size = strongSelf.imageNode.bounds.size
|
||||
let recognizedContentNode = RecognizedContentContainer(size: size, recognitions: results, presentationData: strongSelf.context.sharedContext.currentPresentationData.with { $0 }, present: { [weak self] c, a in
|
||||
if let strongSelf = self {
|
||||
strongSelf.galleryController()?.presentInGlobalOverlay(c, with: a)
|
||||
}
|
||||
}, performAction: { [weak self] string, action in
|
||||
guard let strongSelf = self else {
|
||||
return
|
||||
}
|
||||
switch action {
|
||||
case .copy:
|
||||
UIPasteboard.general.string = string
|
||||
if let controller = strongSelf.baseNavigationController()?.topViewController as? ViewController {
|
||||
let presentationData = strongSelf.context.sharedContext.currentPresentationData.with({ $0 })
|
||||
let tooltipController = UndoOverlayController(presentationData: presentationData, content: .copy(text: presentationData.strings.Conversation_TextCopied), elevatedLayout: true, animateInAsReplacement: false, action: { _ in return false })
|
||||
controller.present(tooltipController, in: .window(.root))
|
||||
}
|
||||
case .share:
|
||||
if let controller = strongSelf.baseNavigationController()?.topViewController as? ViewController {
|
||||
let shareController = ShareController(context: strongSelf.context, subject: .text(string), externalShare: true, immediateExternalShare: false, updatedPresentationData: (strongSelf.context.sharedContext.currentPresentationData.with({ $0 }), strongSelf.context.sharedContext.presentationData))
|
||||
controller.present(shareController, in: .window(.root))
|
||||
}
|
||||
case .lookup:
|
||||
let controller = UIReferenceLibraryViewController(term: string)
|
||||
if let window = strongSelf.baseNavigationController()?.view.window {
|
||||
controller.popoverPresentationController?.sourceView = window
|
||||
controller.popoverPresentationController?.sourceRect = CGRect(origin: CGPoint(x: window.bounds.width / 2.0, y: window.bounds.size.height - 1.0), size: CGSize(width: 1.0, height: 1.0))
|
||||
window.rootViewController?.present(controller, animated: true)
|
||||
}
|
||||
case .speak:
|
||||
if let speechHolder = speakText(context: strongSelf.context, text: string) {
|
||||
speechHolder.completion = { [weak self, weak speechHolder] in
|
||||
if let strongSelf = self, strongSelf.currentSpeechHolder == speechHolder {
|
||||
strongSelf.currentSpeechHolder = nil
|
||||
}
|
||||
}
|
||||
strongSelf.currentSpeechHolder = speechHolder
|
||||
}
|
||||
case .translate:
|
||||
if let parentController = strongSelf.baseNavigationController()?.topViewController as? ViewController {
|
||||
let controller = TranslateScreen(context: strongSelf.context, text: string, canCopy: true, fromLanguage: nil)
|
||||
controller.pushController = { [weak parentController] c in
|
||||
(parentController?.navigationController as? NavigationController)?._keepModalDismissProgress = true
|
||||
parentController?.push(c)
|
||||
}
|
||||
controller.presentController = { [weak parentController] c in
|
||||
parentController?.present(c, in: .window(.root))
|
||||
}
|
||||
parentController.present(controller, in: .window(.root))
|
||||
}
|
||||
}
|
||||
})
|
||||
recognizedContentNode.barcodeAction = { [weak self] payload, rect in
|
||||
guard let strongSelf = self, let message = strongSelf.message else {
|
||||
return
|
||||
}
|
||||
strongSelf.footerContentNode.openActionOptions?(.url(url: payload, concealed: true), message)
|
||||
}
|
||||
recognizedContentNode.alpha = 0.0
|
||||
recognizedContentNode.frame = CGRect(origin: CGPoint(), size: size)
|
||||
recognizedContentNode.update(size: strongSelf.imageNode.bounds.size, transition: .immediate)
|
||||
strongSelf.imageNode.addSubnode(recognizedContentNode)
|
||||
strongSelf.recognizedContentNode = recognizedContentNode
|
||||
strongSelf.recognitionOverlayContentNode.transitionIn()
|
||||
}
|
||||
}
|
||||
}))
|
||||
}
|
||||
|
||||
fileprivate func setMessage(_ message: Message, displayInfo: Bool, translateToLanguage: String?, peerIsCopyProtected: Bool, isSecret: Bool) {
|
||||
self.message = message
|
||||
self.translateToLanguage = translateToLanguage
|
||||
|
|
@ -392,83 +472,11 @@ final class ChatImageGalleryItemNode: ZoomableContentGalleryItemNode {
|
|||
case .medium, .full:
|
||||
strongSelf.statusNodeContainer.isHidden = true
|
||||
|
||||
Queue.concurrentDefaultQueue().async {
|
||||
if let message = strongSelf.message, !message.isCopyProtected() && !imageReference.media.flags.contains(.hasStickers) && message.paidContent == nil {
|
||||
strongSelf.recognitionDisposable.set((recognizedContent(context: strongSelf.context, image: { return generate(TransformImageArguments(corners: ImageCorners(), imageSize: displaySize, boundingSize: displaySize, intrinsicInsets: UIEdgeInsets()))?.generateImage() }, messageId: message.id)
|
||||
|> deliverOnMainQueue).start(next: { [weak self] results in
|
||||
if let strongSelf = self {
|
||||
strongSelf.recognizedContentNode?.removeFromSupernode()
|
||||
if !results.isEmpty {
|
||||
let size = strongSelf.imageNode.bounds.size
|
||||
let recognizedContentNode = RecognizedContentContainer(size: size, recognitions: results, presentationData: strongSelf.context.sharedContext.currentPresentationData.with { $0 }, present: { [weak self] c, a in
|
||||
if let strongSelf = self {
|
||||
strongSelf.galleryController()?.presentInGlobalOverlay(c, with: a)
|
||||
}
|
||||
}, performAction: { [weak self] string, action in
|
||||
guard let strongSelf = self else {
|
||||
return
|
||||
}
|
||||
switch action {
|
||||
case .copy:
|
||||
UIPasteboard.general.string = string
|
||||
if let controller = strongSelf.baseNavigationController()?.topViewController as? ViewController {
|
||||
let presentationData = strongSelf.context.sharedContext.currentPresentationData.with({ $0 })
|
||||
let tooltipController = UndoOverlayController(presentationData: presentationData, content: .copy(text: presentationData.strings.Conversation_TextCopied), elevatedLayout: true, animateInAsReplacement: false, action: { _ in return false })
|
||||
controller.present(tooltipController, in: .window(.root))
|
||||
}
|
||||
case .share:
|
||||
if let controller = strongSelf.baseNavigationController()?.topViewController as? ViewController {
|
||||
let shareController = ShareController(context: strongSelf.context, subject: .text(string), externalShare: true, immediateExternalShare: false, updatedPresentationData: (strongSelf.context.sharedContext.currentPresentationData.with({ $0 }), strongSelf.context.sharedContext.presentationData))
|
||||
controller.present(shareController, in: .window(.root))
|
||||
}
|
||||
case .lookup:
|
||||
let controller = UIReferenceLibraryViewController(term: string)
|
||||
if let window = strongSelf.baseNavigationController()?.view.window {
|
||||
controller.popoverPresentationController?.sourceView = window
|
||||
controller.popoverPresentationController?.sourceRect = CGRect(origin: CGPoint(x: window.bounds.width / 2.0, y: window.bounds.size.height - 1.0), size: CGSize(width: 1.0, height: 1.0))
|
||||
window.rootViewController?.present(controller, animated: true)
|
||||
}
|
||||
case .speak:
|
||||
if let speechHolder = speakText(context: strongSelf.context, text: string) {
|
||||
speechHolder.completion = { [weak self, weak speechHolder] in
|
||||
if let strongSelf = self, strongSelf.currentSpeechHolder == speechHolder {
|
||||
strongSelf.currentSpeechHolder = nil
|
||||
}
|
||||
}
|
||||
strongSelf.currentSpeechHolder = speechHolder
|
||||
}
|
||||
case .translate:
|
||||
if let parentController = strongSelf.baseNavigationController()?.topViewController as? ViewController {
|
||||
let controller = TranslateScreen(context: strongSelf.context, text: string, canCopy: true, fromLanguage: nil)
|
||||
controller.pushController = { [weak parentController] c in
|
||||
(parentController?.navigationController as? NavigationController)?._keepModalDismissProgress = true
|
||||
parentController?.push(c)
|
||||
}
|
||||
controller.presentController = { [weak parentController] c in
|
||||
parentController?.present(c, in: .window(.root))
|
||||
}
|
||||
parentController.present(controller, in: .window(.root))
|
||||
}
|
||||
}
|
||||
})
|
||||
recognizedContentNode.barcodeAction = { [weak self] payload, rect in
|
||||
guard let strongSelf = self, let message = strongSelf.message else {
|
||||
return
|
||||
}
|
||||
strongSelf.footerContentNode.openActionOptions?(.url(url: payload, concealed: true), message)
|
||||
}
|
||||
recognizedContentNode.alpha = 0.0
|
||||
recognizedContentNode.frame = CGRect(origin: CGPoint(), size: size)
|
||||
recognizedContentNode.update(size: strongSelf.imageNode.bounds.size, transition: .immediate)
|
||||
strongSelf.imageNode.addSubnode(recognizedContentNode)
|
||||
strongSelf.recognizedContentNode = recognizedContentNode
|
||||
strongSelf.recognitionOverlayContentNode.transitionIn()
|
||||
}
|
||||
}
|
||||
}))
|
||||
if !imageReference.media.flags.contains(.hasStickers) {
|
||||
Queue.concurrentDefaultQueue().async {
|
||||
strongSelf.setupImageRecognition(generate, dimensions: largestSize.dimensions)
|
||||
}
|
||||
}
|
||||
|
||||
case .none, .blurred:
|
||||
strongSelf.statusNodeContainer.isHidden = false
|
||||
}
|
||||
|
|
@ -819,6 +827,7 @@ final class ChatImageGalleryItemNode: ZoomableContentGalleryItemNode {
|
|||
largestSize = PixelDimensions(width: largestSize.height, height: largestSize.width)
|
||||
}
|
||||
}
|
||||
|
||||
self.imageNode.asyncLayout()(TransformImageArguments(corners: ImageCorners(), imageSize: displaySize, boundingSize: displaySize, intrinsicInsets: UIEdgeInsets()))()
|
||||
|
||||
/*if largestSize.width > 2600 || largestSize.height > 2600 {
|
||||
|
|
@ -833,7 +842,18 @@ final class ChatImageGalleryItemNode: ZoomableContentGalleryItemNode {
|
|||
strongSelf.updateImageFromFile(path: data.path)
|
||||
}))
|
||||
} else {*/
|
||||
self.imageNode.setSignal(chatMessageImageFile(account: context.account, userLocation: userLocation, fileReference: fileReference, thumbnail: false), dispatchOnDisplayLink: false)
|
||||
|
||||
let signal = chatMessageImageFile(account: context.account, userLocation: userLocation, fileReference: fileReference, thumbnail: false)
|
||||
|> afterNext({ [weak self] generate in
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
Queue.concurrentDefaultQueue().async {
|
||||
self.setupImageRecognition(generate, dimensions: largestSize)
|
||||
}
|
||||
})
|
||||
|
||||
self.imageNode.setSignal(signal, dispatchOnDisplayLink: false)
|
||||
//}
|
||||
|
||||
self.zoomableContent = (largestSize.cgSize, self.imageNode)
|
||||
|
|
|
|||
|
|
@ -47,6 +47,8 @@
|
|||
|
||||
@property (nonatomic, readonly) bool inhibitEditing;
|
||||
|
||||
@property (nonatomic, assign) int64_t sendPaidMessageStars;
|
||||
|
||||
+ (instancetype)contextForCaptionsOnly;
|
||||
|
||||
- (SSignal *)imageSignalForItem:(NSObject<TGMediaEditableItem> *)item;
|
||||
|
|
|
|||
|
|
@ -17,6 +17,13 @@
|
|||
|
||||
@end
|
||||
|
||||
@protocol TGPhotoSendStarsButtonView <NSObject>
|
||||
|
||||
- (void)updateFrame:(CGRect)frame;
|
||||
- (CGSize)updateCount:(int64_t)count;
|
||||
|
||||
@end
|
||||
|
||||
|
||||
@protocol TGCaptionPanelView <NSObject>
|
||||
|
||||
|
|
@ -122,7 +129,7 @@
|
|||
|
||||
@property (nonatomic, copy) void (^ _Nullable editCover)(CGSize dimensions, void(^_Nonnull completion)(UIImage * _Nonnull));
|
||||
|
||||
|
||||
- (UIView<TGPhotoSendStarsButtonView> *_Nonnull)sendStarsButtonAction:(void(^_Nonnull)(void))action;
|
||||
- (UIView<TGPhotoSolidRoundedButtonView> *_Nonnull)solidRoundedButton:(NSString *_Nonnull)title action:(void(^_Nonnull)(void))action;
|
||||
- (id<TGPhotoDrawingAdapter> _Nonnull)drawingAdapter:(CGSize)size originalSize:(CGSize)originalSize isVideo:(bool)isVideo isAvatar:(bool)isAvatar entitiesView:(UIView<TGPhotoDrawingEntitiesView> * _Nullable)entitiesView;
|
||||
|
||||
|
|
|
|||
|
|
@ -2,6 +2,8 @@
|
|||
|
||||
#import <LegacyComponents/LegacyComponentsContext.h>
|
||||
|
||||
@protocol TGPhotoPaintStickersContext;
|
||||
|
||||
typedef NS_OPTIONS(NSUInteger, TGPhotoEditorTab) {
|
||||
TGPhotoEditorNoneTab = 0,
|
||||
TGPhotoEditorCropTab = 1 << 0,
|
||||
|
|
@ -53,7 +55,9 @@ typedef enum
|
|||
@property (nonatomic, assign) TGPhotoEditorBackButton backButtonType;
|
||||
@property (nonatomic, assign) TGPhotoEditorDoneButton doneButtonType;
|
||||
|
||||
- (instancetype)initWithContext:(id<LegacyComponentsContext>)context backButton:(TGPhotoEditorBackButton)backButton doneButton:(TGPhotoEditorDoneButton)doneButton solidBackground:(bool)solidBackground;
|
||||
@property (nonatomic, assign) int64_t sendPaidMessageStars;
|
||||
|
||||
- (instancetype)initWithContext:(id<LegacyComponentsContext>)context backButton:(TGPhotoEditorBackButton)backButton doneButton:(TGPhotoEditorDoneButton)doneButton solidBackground:(bool)solidBackground stickersContext:(id<TGPhotoPaintStickersContext>)stickersContext;
|
||||
|
||||
- (void)transitionInAnimated:(bool)animated;
|
||||
- (void)transitionInAnimated:(bool)animated transparent:(bool)transparent;
|
||||
|
|
|
|||
|
|
@ -429,13 +429,13 @@
|
|||
|
||||
TGPhotoEditorDoneButton doneButton = isScheduledMessages ? TGPhotoEditorDoneButtonSchedule : TGPhotoEditorDoneButtonSend;
|
||||
|
||||
_portraitToolbarView = [[TGPhotoToolbarView alloc] initWithContext:_context backButton:TGPhotoEditorBackButtonBack doneButton:doneButton solidBackground:false];
|
||||
_portraitToolbarView = [[TGPhotoToolbarView alloc] initWithContext:_context backButton:TGPhotoEditorBackButtonBack doneButton:doneButton solidBackground:false stickersContext:editingContext.sendPaidMessageStars > 0 ? stickersContext : nil];
|
||||
_portraitToolbarView.cancelPressed = toolbarCancelPressed;
|
||||
_portraitToolbarView.donePressed = toolbarDonePressed;
|
||||
_portraitToolbarView.doneLongPressed = toolbarDoneLongPressed;
|
||||
[_wrapperView addSubview:_portraitToolbarView];
|
||||
|
||||
_landscapeToolbarView = [[TGPhotoToolbarView alloc] initWithContext:_context backButton:TGPhotoEditorBackButtonBack doneButton:doneButton solidBackground:false];
|
||||
_landscapeToolbarView = [[TGPhotoToolbarView alloc] initWithContext:_context backButton:TGPhotoEditorBackButtonBack doneButton:doneButton solidBackground:false stickersContext:nil];
|
||||
_landscapeToolbarView.cancelPressed = toolbarCancelPressed;
|
||||
_landscapeToolbarView.donePressed = toolbarDonePressed;
|
||||
_landscapeToolbarView.doneLongPressed = toolbarDoneLongPressed;
|
||||
|
|
@ -1227,6 +1227,10 @@
|
|||
if (_ignoreSelectionUpdates)
|
||||
return;
|
||||
|
||||
NSUInteger finalCount = MAX(1, selectedCount);
|
||||
_portraitToolbarView.sendPaidMessageStars = (_editingContext.sendPaidMessageStars * finalCount);
|
||||
[_portraitToolbarView setNeedsLayout];
|
||||
|
||||
if (counterVisible)
|
||||
{
|
||||
bool animateCount = animated && !(counterVisible && _photoCounterButton.internalHidden);
|
||||
|
|
|
|||
|
|
@ -293,7 +293,7 @@
|
|||
|
||||
TGPhotoEditorBackButton backButton = TGPhotoEditorBackButtonCancel;
|
||||
TGPhotoEditorDoneButton doneButton = TGPhotoEditorDoneButtonCheck;
|
||||
_portraitToolbarView = [[TGPhotoToolbarView alloc] initWithContext:_context backButton:backButton doneButton:doneButton solidBackground:true];
|
||||
_portraitToolbarView = [[TGPhotoToolbarView alloc] initWithContext:_context backButton:backButton doneButton:doneButton solidBackground:true stickersContext:nil];
|
||||
[_portraitToolbarView setToolbarTabs:_availableTabs animated:false];
|
||||
[_portraitToolbarView setActiveTab:_currentTab];
|
||||
_portraitToolbarView.cancelPressed = toolbarCancelPressed;
|
||||
|
|
@ -302,7 +302,7 @@
|
|||
_portraitToolbarView.tabPressed = toolbarTabPressed;
|
||||
[_wrapperView addSubview:_portraitToolbarView];
|
||||
|
||||
_landscapeToolbarView = [[TGPhotoToolbarView alloc] initWithContext:_context backButton:backButton doneButton:doneButton solidBackground:true];
|
||||
_landscapeToolbarView = [[TGPhotoToolbarView alloc] initWithContext:_context backButton:backButton doneButton:doneButton solidBackground:true stickersContext:nil];
|
||||
[_landscapeToolbarView setToolbarTabs:_availableTabs animated:false];
|
||||
[_landscapeToolbarView setActiveTab:_currentTab];
|
||||
_landscapeToolbarView.cancelPressed = toolbarCancelPressed;
|
||||
|
|
|
|||
|
|
@ -10,6 +10,8 @@
|
|||
|
||||
#import "TGMediaAssetsController.h"
|
||||
|
||||
#import "TGPhotoPaintStickersContext.h"
|
||||
|
||||
@interface TGPhotoToolbarView ()
|
||||
{
|
||||
id<LegacyComponentsContext> _context;
|
||||
|
|
@ -19,6 +21,7 @@
|
|||
UIView *_buttonsWrapperView;
|
||||
TGModernButton *_cancelButton;
|
||||
TGModernButton *_doneButton;
|
||||
UIView<TGPhotoSendStarsButtonView> *_starsDoneButton;
|
||||
|
||||
UILabel *_infoLabel;
|
||||
|
||||
|
|
@ -32,7 +35,7 @@
|
|||
|
||||
@implementation TGPhotoToolbarView
|
||||
|
||||
- (instancetype)initWithContext:(id<LegacyComponentsContext>)context backButton:(TGPhotoEditorBackButton)backButton doneButton:(TGPhotoEditorDoneButton)doneButton solidBackground:(bool)solidBackground
|
||||
- (instancetype)initWithContext:(id<LegacyComponentsContext>)context backButton:(TGPhotoEditorBackButton)backButton doneButton:(TGPhotoEditorDoneButton)doneButton solidBackground:(bool)solidBackground stickersContext:(id<TGPhotoPaintStickersContext>)stickersContext
|
||||
{
|
||||
self = [super initWithFrame:CGRectZero];
|
||||
if (self != nil)
|
||||
|
|
@ -56,12 +59,24 @@
|
|||
[_cancelButton addTarget:self action:@selector(cancelButtonPressed) forControlEvents:UIControlEventTouchUpInside];
|
||||
[_backgroundView addSubview:_cancelButton];
|
||||
|
||||
_doneButton = [[TGModernButton alloc] initWithFrame:CGRectMake(0, 0, buttonSize.width, buttonSize.height)];
|
||||
_doneButton.exclusiveTouch = true;
|
||||
_doneButton.adjustsImageWhenHighlighted = false;
|
||||
[self setDoneButtonType:doneButton];
|
||||
[_doneButton addTarget:self action:@selector(doneButtonPressed) forControlEvents:UIControlEventTouchUpInside];
|
||||
[_backgroundView addSubview:_doneButton];
|
||||
if (stickersContext != nil) {
|
||||
__weak TGPhotoToolbarView *weakSelf = self;
|
||||
_starsDoneButton = [stickersContext sendStarsButtonAction:^{
|
||||
__strong TGPhotoToolbarView *strongSelf = weakSelf;
|
||||
if (strongSelf == nil)
|
||||
return;
|
||||
[strongSelf doneButtonPressed];
|
||||
}];
|
||||
_starsDoneButton.exclusiveTouch = true;
|
||||
[_backgroundView addSubview:_starsDoneButton];
|
||||
} else {
|
||||
_doneButton = [[TGModernButton alloc] initWithFrame:CGRectMake(0, 0, buttonSize.width, buttonSize.height)];
|
||||
_doneButton.exclusiveTouch = true;
|
||||
_doneButton.adjustsImageWhenHighlighted = false;
|
||||
[self setDoneButtonType:doneButton];
|
||||
[_doneButton addTarget:self action:@selector(doneButtonPressed) forControlEvents:UIControlEventTouchUpInside];
|
||||
[_backgroundView addSubview:_doneButton];
|
||||
}
|
||||
|
||||
_longPressGestureRecognizer = [[UILongPressGestureRecognizer alloc] initWithTarget:self action:@selector(doneButtonLongPressed:)];
|
||||
_longPressGestureRecognizer.minimumPressDuration = 0.4;
|
||||
|
|
@ -704,6 +719,11 @@
|
|||
if (_doneButton.frame.size.width > 49.0f)
|
||||
offset = 60.0f;
|
||||
|
||||
if (_starsDoneButton != nil) {
|
||||
CGSize buttonSize = [_starsDoneButton updateCount:_sendPaidMessageStars];
|
||||
[_starsDoneButton updateFrame:CGRectMake(self.frame.size.width - buttonSize.width - 2.0, 49.0f - offset + 2.0f, buttonSize.width, buttonSize.height)];
|
||||
}
|
||||
|
||||
_doneButton.frame = CGRectMake(self.frame.size.width - offset, 49.0f - offset, _doneButton.frame.size.width, _doneButton.frame.size.height);
|
||||
|
||||
_infoLabel.frame = CGRectMake(49.0f + 10.0f, 0.0f, self.frame.size.width - (49.0f + 10.0f) * 2.0f, 49.0f);
|
||||
|
|
|
|||
|
|
@ -30,6 +30,7 @@ swift_library(
|
|||
"//submodules/DrawingUI:DrawingUI",
|
||||
"//submodules/SolidRoundedButtonNode:SolidRoundedButtonNode",
|
||||
"//submodules/TelegramUI/Components/MediaEditor",
|
||||
"//submodules/AnimatedCountLabelNode",
|
||||
],
|
||||
visibility = [
|
||||
"//visibility:public",
|
||||
|
|
|
|||
|
|
@ -11,6 +11,8 @@ import StickerResources
|
|||
import SolidRoundedButtonNode
|
||||
import MediaEditor
|
||||
import DrawingUI
|
||||
import TelegramPresentationData
|
||||
import AnimatedCountLabelNode
|
||||
|
||||
protocol LegacyPaintEntity {
|
||||
var position: CGPoint { get }
|
||||
|
|
@ -607,12 +609,109 @@ public final class LegacyPaintStickersContext: NSObject, TGPhotoPaintStickersCon
|
|||
return button
|
||||
}
|
||||
|
||||
public func sendStarsButtonAction(_ action: @escaping () -> Void) -> any UIView & TGPhotoSendStarsButtonView {
|
||||
let button = SendStarsButtonView()
|
||||
button.pressed = action
|
||||
return button
|
||||
}
|
||||
|
||||
public func drawingEntitiesView(with size: CGSize) -> UIView & TGPhotoDrawingEntitiesView {
|
||||
let view = DrawingEntitiesView(context: self.context, size: size)
|
||||
return view
|
||||
}
|
||||
}
|
||||
|
||||
private class SendStarsButtonView: HighlightTrackingButton, TGPhotoSendStarsButtonView {
|
||||
private let backgroundView: UIView
|
||||
private let textNode: ImmediateAnimatedCountLabelNode
|
||||
|
||||
fileprivate var pressed: (() -> Void)?
|
||||
|
||||
override init(frame: CGRect) {
|
||||
self.backgroundView = UIView()
|
||||
|
||||
self.textNode = ImmediateAnimatedCountLabelNode()
|
||||
self.textNode.isUserInteractionEnabled = false
|
||||
|
||||
super.init(frame: frame)
|
||||
|
||||
self.addSubview(self.backgroundView)
|
||||
self.addSubview(self.textNode.view)
|
||||
|
||||
self.highligthedChanged = { [weak self] highlighted in
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
if highlighted {
|
||||
self.backgroundView.layer.removeAnimation(forKey: "opacity")
|
||||
self.backgroundView.alpha = 0.4
|
||||
self.textNode.layer.removeAnimation(forKey: "opacity")
|
||||
self.textNode.alpha = 0.4
|
||||
} else {
|
||||
self.backgroundView.alpha = 1.0
|
||||
self.backgroundView.layer.animateAlpha(from: 0.4, to: 1.0, duration: 0.2)
|
||||
self.textNode.alpha = 1.0
|
||||
self.textNode.layer.animateAlpha(from: 0.4, to: 1.0, duration: 0.2)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
preconditionFailure()
|
||||
}
|
||||
|
||||
func updateFrame(_ frame: CGRect) {
|
||||
let transition: ContainedViewLayoutTransition
|
||||
if self.frame.width.isZero {
|
||||
transition = .immediate
|
||||
} else {
|
||||
transition = .animated(duration: 0.4, curve: .spring)
|
||||
}
|
||||
transition.updateFrame(view: self, frame: frame)
|
||||
}
|
||||
|
||||
func updateCount(_ count: Int64) -> CGSize {
|
||||
let text = "\(count)"
|
||||
let transition: ContainedViewLayoutTransition
|
||||
if self.backgroundView.frame.width.isZero {
|
||||
transition = .immediate
|
||||
} else {
|
||||
transition = .animated(duration: 0.4, curve: .spring)
|
||||
}
|
||||
|
||||
var segments: [AnimatedCountLabelNode.Segment] = []
|
||||
let font = Font.with(size: 17.0, design: .round, weight: .semibold, traits: .monospacedNumbers)
|
||||
let badgeString = NSMutableAttributedString(string: "⭐️ ", font: font, textColor: .white)
|
||||
if let range = badgeString.string.range(of: "⭐️") {
|
||||
badgeString.addAttribute(.attachment, value: PresentationResourcesChat.chatPlaceholderStarIcon(defaultDarkPresentationTheme)!, range: NSRange(range, in: badgeString.string))
|
||||
badgeString.addAttribute(.baselineOffset, value: 1.0, range: NSRange(range, in: badgeString.string))
|
||||
}
|
||||
segments.append(.text(0, badgeString))
|
||||
for char in text {
|
||||
if let intValue = Int(String(char)) {
|
||||
segments.append(.number(intValue, NSAttributedString(string: String(char), font: font, textColor: .white)))
|
||||
}
|
||||
}
|
||||
|
||||
self.textNode.segments = segments
|
||||
|
||||
let buttonInset: CGFloat = 14.0
|
||||
let textSize = self.textNode.updateLayout(size: CGSize(width: 100.0, height: 100.0), animated: transition.isAnimated)
|
||||
let width = textSize.width + buttonInset * 2.0
|
||||
let buttonSize = CGSize(width: width, height: 45.0)
|
||||
let titleOffset: CGFloat = 0.0
|
||||
|
||||
transition.updateFrame(node: self.textNode, frame: CGRect(origin: CGPoint(x: floorToScreenPixels((width - textSize.width) / 2.0) + titleOffset, y: floorToScreenPixels((buttonSize.height - textSize.height) / 2.0)), size: textSize))
|
||||
|
||||
let backgroundSize = CGSize(width: width - 11.0, height: 33.0)
|
||||
transition.updateFrame(view: self.backgroundView, frame: CGRect(origin: CGPoint(x: floorToScreenPixels((width - backgroundSize.width) / 2.0), y: floorToScreenPixels((buttonSize.height - backgroundSize.height) / 2.0)), size: backgroundSize))
|
||||
self.backgroundView.layer.cornerRadius = backgroundSize.height / 2.0
|
||||
self.backgroundView.backgroundColor = UIColor(rgb: 0x007aff)
|
||||
|
||||
return buttonSize;
|
||||
}
|
||||
}
|
||||
|
||||
//Xcode 16
|
||||
#if canImport(ContactProvider)
|
||||
extension SolidRoundedButtonView: @retroactive TGPhotoSolidRoundedButtonView {
|
||||
|
|
|
|||
|
|
@ -211,7 +211,7 @@ public final class MediaPickerScreenImpl: ViewController, MediaPickerScreen, Att
|
|||
public var openAvatarEditor: () -> Void = {}
|
||||
|
||||
private var completed = false
|
||||
public var legacyCompletion: (_ signals: [Any], _ silently: Bool, _ scheduleTime: Int32?, ChatSendMessageActionSheetController.SendParameters?, @escaping (String) -> UIView?, @escaping () -> Void) -> Void = { _, _, _, _, _, _ in }
|
||||
public var legacyCompletion: (_ fromGallery: Bool, _ signals: [Any], _ silently: Bool, _ scheduleTime: Int32?, ChatSendMessageActionSheetController.SendParameters?, @escaping (String) -> UIView?, @escaping () -> Void) -> Void = { _, _, _, _, _, _, _ in }
|
||||
|
||||
public var requestAttachmentMenuExpansion: () -> Void = { }
|
||||
public var updateNavigationStack: (@escaping ([AttachmentContainable]) -> ([AttachmentContainable], AttachmentMediaPickerContext?)) -> Void = { _ in }
|
||||
|
|
@ -1294,7 +1294,7 @@ public final class MediaPickerScreenImpl: ViewController, MediaPickerScreen, Att
|
|||
}
|
||||
}
|
||||
|
||||
fileprivate func send(asFile: Bool = false, silently: Bool, scheduleTime: Int32?, animated: Bool, parameters: ChatSendMessageActionSheetController.SendParameters?, completion: @escaping () -> Void) {
|
||||
fileprivate func send(fromGallery: Bool = false, asFile: Bool = false, silently: Bool, scheduleTime: Int32?, animated: Bool, parameters: ChatSendMessageActionSheetController.SendParameters?, completion: @escaping () -> Void) {
|
||||
guard let controller = self.controller, !controller.completed else {
|
||||
return
|
||||
}
|
||||
|
|
@ -1334,7 +1334,7 @@ public final class MediaPickerScreenImpl: ViewController, MediaPickerScreen, Att
|
|||
return
|
||||
}
|
||||
controller.completed = true
|
||||
controller.legacyCompletion(signals, silently, scheduleTime, parameters, { [weak self] identifier in
|
||||
controller.legacyCompletion(fromGallery, signals, silently, scheduleTime, parameters, { [weak self] identifier in
|
||||
return !asFile ? self?.getItemSnapshot(identifier) : nil
|
||||
}, { [weak self] in
|
||||
completion()
|
||||
|
|
@ -1834,6 +1834,7 @@ public final class MediaPickerScreenImpl: ViewController, MediaPickerScreen, Att
|
|||
paidMediaAllowed: Bool = false,
|
||||
subject: Subject,
|
||||
forCollage: Bool = false,
|
||||
sendPaidMessageStars: Int64? = nil,
|
||||
editingContext: TGMediaEditingContext? = nil,
|
||||
selectionContext: TGMediaSelectionContext? = nil,
|
||||
saveEditedPhotos: Bool = false,
|
||||
|
|
@ -2114,7 +2115,7 @@ public final class MediaPickerScreenImpl: ViewController, MediaPickerScreen, Att
|
|||
if let currentItem = currentItem {
|
||||
selectionState.setItem(currentItem, selected: true)
|
||||
}
|
||||
strongSelf.controllerNode.send(silently: silently, scheduleTime: scheduleTime, animated: animated, parameters: parameters, completion: completion)
|
||||
strongSelf.controllerNode.send(fromGallery: currentItem != nil, silently: silently, scheduleTime: scheduleTime, animated: animated, parameters: parameters, completion: completion)
|
||||
}
|
||||
}, schedule: { [weak self] parameters in
|
||||
if let strongSelf = self {
|
||||
|
|
@ -2129,6 +2130,8 @@ public final class MediaPickerScreenImpl: ViewController, MediaPickerScreen, Att
|
|||
}, selectionState: selectionContext, editingState: editingContext ?? TGMediaEditingContext())
|
||||
self.interaction?.selectionState?.grouping = true
|
||||
|
||||
self.interaction?.editingState.sendPaidMessageStars = sendPaidMessageStars ?? 0
|
||||
|
||||
if case let .media(media) = self.subject {
|
||||
for item in media {
|
||||
selectionContext.setItem(item.asset, selected: true)
|
||||
|
|
|
|||
|
|
@ -93,7 +93,7 @@ private enum ChannelPermissionsEntry: ItemListNodeEntry {
|
|||
case chargeForMessagesInfo(PresentationTheme, String)
|
||||
|
||||
case messagePriceHeader(PresentationTheme, String)
|
||||
case messagePrice(PresentationTheme, StarsAmount, String)
|
||||
case messagePrice(PresentationTheme, Int64, 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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -437,9 +437,8 @@ private enum PrivacyAndSecurityEntry: ItemListNodeEntry {
|
|||
label = presentationData.strings.Settings_Privacy_Messages_ValueEveryone
|
||||
case .requirePremium:
|
||||
label = presentationData.strings.Settings_Privacy_Messages_ValueContactsAndPremium
|
||||
case let .paidMessages(amount):
|
||||
//TODO:localize
|
||||
label = "\(amount.value) Stars"
|
||||
case .paidMessages:
|
||||
label = presentationData.strings.Settings_Privacy_Messages_ValuePaid
|
||||
}
|
||||
return ItemListDisclosureItem(presentationData: presentationData, title: presentationData.strings.Settings_Privacy_Messages, titleIcon: hasPremium ? PresentationResourcesItemList.premiumIcon(theme) : nil, label: label, sectionId: self.section, style: .blocks, action: {
|
||||
arguments.openMessagePrivacy()
|
||||
|
|
@ -862,7 +861,11 @@ public func privacyAndSecurityController(
|
|||
updateHasTwoStepAuth()
|
||||
|
||||
var setupEmailImpl: ((String?) -> Void)?
|
||||
|
||||
|
||||
var reviewCallPrivacySuggestion = false
|
||||
var reviewInvitePrivacySuggestion = false
|
||||
var showPrivacySuggestionImpl: (() -> Void)?
|
||||
|
||||
let arguments = PrivacyAndSecurityControllerArguments(account: context.account, openBlockedUsers: {
|
||||
pushControllerImpl?(blockedPeersController(context: context, blockedPeersContext: blockedPeersContext), true)
|
||||
}, openLastSeenPrivacy: {
|
||||
|
|
@ -907,6 +910,10 @@ public func privacyAndSecurityController(
|
|||
return .complete()
|
||||
}
|
||||
currentInfoDisposable.set(applySetting.start())
|
||||
|
||||
Queue.mainQueue().after(0.3) {
|
||||
showPrivacySuggestionImpl?()
|
||||
}
|
||||
}
|
||||
}), true)
|
||||
}
|
||||
|
|
@ -944,6 +951,10 @@ public func privacyAndSecurityController(
|
|||
return .complete()
|
||||
}
|
||||
currentInfoDisposable.set(applySetting.start())
|
||||
|
||||
Queue.mainQueue().after(0.3) {
|
||||
showPrivacySuggestionImpl?()
|
||||
}
|
||||
}
|
||||
}), true)
|
||||
}
|
||||
|
|
@ -1319,6 +1330,22 @@ public func privacyAndSecurityController(
|
|||
return state
|
||||
}
|
||||
}))
|
||||
|
||||
if case .everybody = privacySettings.globalSettings.nonContactChatsPrivacy {
|
||||
if case .everybody = settingValue {
|
||||
|
||||
} else {
|
||||
if case .enableEveryone = privacySettings.voiceCalls {
|
||||
reviewCallPrivacySuggestion = true
|
||||
}
|
||||
if case .enableEveryone = privacySettings.groupInvitations {
|
||||
reviewInvitePrivacySuggestion = true
|
||||
}
|
||||
Queue.mainQueue().after(0.3) {
|
||||
showPrivacySuggestionImpl?()
|
||||
}
|
||||
}
|
||||
}
|
||||
}), true)
|
||||
})
|
||||
}, openGiftsPrivacy: {
|
||||
|
|
@ -1442,6 +1469,50 @@ public func privacyAndSecurityController(
|
|||
}
|
||||
}
|
||||
|
||||
showPrivacySuggestionImpl = {
|
||||
//TODO:localize
|
||||
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
|
||||
if reviewCallPrivacySuggestion {
|
||||
reviewCallPrivacySuggestion = false
|
||||
let alertController = textAlertController(
|
||||
context: context,
|
||||
title: "Call Settings",
|
||||
text: "You've restricted who can message you, but anyone can still call you. Would you like to review these settings?",
|
||||
actions: [
|
||||
TextAlertAction(type: .defaultAction, title: "Review", action: {
|
||||
arguments.openVoiceCallPrivacy()
|
||||
}),
|
||||
TextAlertAction(type: .genericAction, title: presentationData.strings.Common_Cancel, action: {
|
||||
Queue.mainQueue().after(0.3) {
|
||||
showPrivacySuggestionImpl?()
|
||||
}
|
||||
})
|
||||
],
|
||||
actionLayout: .vertical
|
||||
)
|
||||
presentControllerImpl?(alertController)
|
||||
} else if reviewInvitePrivacySuggestion {
|
||||
reviewInvitePrivacySuggestion = false
|
||||
let alertController = textAlertController(
|
||||
context: context,
|
||||
title: "Invitation Settings",
|
||||
text: "You've restricted who can message you, but anyone can still invite you to groups and channels. Would you like to review these settings?",
|
||||
actions: [
|
||||
TextAlertAction(type: .defaultAction, title: "Review", action: {
|
||||
arguments.openGroupsPrivacy()
|
||||
}),
|
||||
TextAlertAction(type: .genericAction, title: presentationData.strings.Common_Cancel, action: {
|
||||
Queue.mainQueue().after(0.3) {
|
||||
showPrivacySuggestionImpl?()
|
||||
}
|
||||
})
|
||||
],
|
||||
actionLayout: .vertical
|
||||
)
|
||||
presentControllerImpl?(alertController)
|
||||
}
|
||||
}
|
||||
|
||||
setupEmailImpl = { emailPattern in
|
||||
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
|
||||
var dismissEmailControllerImpl: (() -> Void)?
|
||||
|
|
|
|||
|
|
@ -46,6 +46,7 @@ swift_library(
|
|||
"//submodules/TelegramUI/Components/LottieComponent",
|
||||
"//submodules/TelegramUI/Components/MessageInputPanelComponent",
|
||||
"//submodules/TelegramUI/Components/ChatEntityKeyboardInputNode",
|
||||
"//submodules/TelegramUI/Components/Chat/ChatMessagePaymentAlertController",
|
||||
"//submodules/ChatPresentationInterfaceState",
|
||||
"//submodules/CheckNode",
|
||||
],
|
||||
|
|
|
|||
|
|
@ -21,6 +21,7 @@ import AnimationCache
|
|||
import MultiAnimationRenderer
|
||||
import ObjectiveC
|
||||
import UndoUI
|
||||
import ChatMessagePaymentAlertController
|
||||
|
||||
private var ObjCKey_DeinitWatcher: Int?
|
||||
|
||||
|
|
@ -405,7 +406,7 @@ public final class ShareController: ViewController {
|
|||
private let fromForeignApp: Bool
|
||||
private let collectibleItemInfo: TelegramCollectibleItemInfo?
|
||||
|
||||
private let peers = Promise<([(peer: EngineRenderedPeer, presence: EnginePeer.Presence?, requiresPremiumForMessaging: Bool)], EnginePeer)>()
|
||||
private let peers = Promise<([(peer: EngineRenderedPeer, presence: EnginePeer.Presence?, requiresPremiumForMessaging: Bool, requiresStars: Int64?)], EnginePeer)>()
|
||||
private let peersDisposable = MetaDisposable()
|
||||
private let readyDisposable = MetaDisposable()
|
||||
private let accountActiveDisposable = MetaDisposable()
|
||||
|
|
@ -664,12 +665,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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -15,6 +15,7 @@ import TelegramStringFormatting
|
|||
import BundleIconComponent
|
||||
import LottieComponent
|
||||
import CheckNode
|
||||
import ChatMessagePaymentAlertController
|
||||
|
||||
enum ShareState {
|
||||
case preparing(Bool)
|
||||
|
|
@ -327,6 +328,7 @@ final class ShareControllerNode: ViewControllerTracingNode, ASScrollViewDelegate
|
|||
private let segmentedValues: [ShareControllerSegmentedValue]?
|
||||
private let collectibleItemInfo: TelegramCollectibleItemInfo?
|
||||
private let mediaParameters: ShareControllerSubject.MediaParameters?
|
||||
private let messageCount: Int
|
||||
|
||||
var selectedSegmentedIndex: Int = 0
|
||||
|
||||
|
|
@ -387,7 +389,7 @@ final class ShareControllerNode: ViewControllerTracingNode, ASScrollViewDelegate
|
|||
|
||||
private let showNames = ValuePromise<Bool>(true)
|
||||
|
||||
init(controller: ShareController, environment: ShareControllerEnvironment, presentationData: PresentationData, presetText: String?, defaultAction: ShareControllerAction?, mediaParameters: ShareControllerSubject.MediaParameters?, requestLayout: @escaping (ContainedViewLayoutTransition) -> Void, presentError: @escaping (String?, String) -> Void, externalShare: Bool, immediateExternalShare: Bool, immediatePeerId: PeerId?, fromForeignApp: Bool, forceTheme: PresentationTheme?, fromPublicChannel: Bool, segmentedValues: [ShareControllerSegmentedValue]?, shareStory: (() -> Void)?, collectibleItemInfo: TelegramCollectibleItemInfo?) {
|
||||
init(controller: ShareController, environment: ShareControllerEnvironment, presentationData: PresentationData, presetText: String?, defaultAction: ShareControllerAction?, mediaParameters: ShareControllerSubject.MediaParameters?, requestLayout: @escaping (ContainedViewLayoutTransition) -> Void, presentError: @escaping (String?, String) -> Void, externalShare: Bool, immediateExternalShare: Bool, immediatePeerId: PeerId?, fromForeignApp: Bool, forceTheme: PresentationTheme?, fromPublicChannel: Bool, segmentedValues: [ShareControllerSegmentedValue]?, shareStory: (() -> Void)?, collectibleItemInfo: TelegramCollectibleItemInfo?, messageCount: Int) {
|
||||
self.controller = controller
|
||||
self.environment = environment
|
||||
self.presentationData = presentationData
|
||||
|
|
@ -401,6 +403,7 @@ final class ShareControllerNode: ViewControllerTracingNode, ASScrollViewDelegate
|
|||
self.segmentedValues = segmentedValues
|
||||
self.collectibleItemInfo = collectibleItemInfo
|
||||
self.mediaParameters = mediaParameters
|
||||
self.messageCount = messageCount
|
||||
|
||||
self.presetText = presetText
|
||||
|
||||
|
|
@ -1260,7 +1263,7 @@ final class ShareControllerNode: ViewControllerTracingNode, ASScrollViewDelegate
|
|||
})
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
func send(peerId: PeerId? = nil, showNames: Bool = true, silently: Bool = false) {
|
||||
let peerIds: [PeerId]
|
||||
if let peerId = peerId {
|
||||
|
|
@ -1273,19 +1276,29 @@ final class ShareControllerNode: ViewControllerTracingNode, ASScrollViewDelegate
|
|||
let _ = (context.stateManager.postbox.combinedView(
|
||||
keys: peerIds.map { peerId in
|
||||
return PostboxViewKey.basicPeer(peerId)
|
||||
} + peerIds.map { peerId in
|
||||
return PostboxViewKey.cachedPeerData(peerId: peerId)
|
||||
}
|
||||
)
|
||||
|> take(1)
|
||||
|> map { views -> [EnginePeer.Id: EnginePeer?] in
|
||||
|> map { views -> ([EnginePeer.Id: EnginePeer?], [EnginePeer.Id: Int64]) in
|
||||
var result: [EnginePeer.Id: EnginePeer?] = [:]
|
||||
var requiresStars: [EnginePeer.Id: Int64] = [:]
|
||||
for peerId in peerIds {
|
||||
if let view = views.views[PostboxViewKey.basicPeer(peerId)] as? BasicPeerView, let peer = view.peer {
|
||||
result[peerId] = EnginePeer(peer)
|
||||
if peer is TelegramUser, let cachedPeerDataView = views.views[PostboxViewKey.cachedPeerData(peerId: peerId)] as? CachedPeerDataView {
|
||||
if let cachedData = cachedPeerDataView.cachedPeerData as? CachedUserData {
|
||||
requiresStars[peerId] = cachedData.sendPaidMessageStars?.value
|
||||
}
|
||||
} else if let channel = peer as? TelegramChannel {
|
||||
requiresStars[peerId] = channel.sendPaidMessageStars?.value
|
||||
}
|
||||
}
|
||||
}
|
||||
return result
|
||||
return (result, requiresStars)
|
||||
}
|
||||
|> deliverOnMainQueue).start(next: { [weak self] peers in
|
||||
|> deliverOnMainQueue).start(next: { [weak self] peers, requiresStars in
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
|
|
@ -1300,14 +1313,49 @@ final class ShareControllerNode: ViewControllerTracingNode, ASScrollViewDelegate
|
|||
if !tryShare(self.inputFieldNode.text, mappedPeers) {
|
||||
return
|
||||
}
|
||||
|
||||
self.presentPaidMessageAlertIfNeeded(peers: mappedPeers, requiresStars: requiresStars, completion: { [weak self] in
|
||||
self?.commitSend(peerId: peerId, showNames: showNames, silently: silently)
|
||||
})
|
||||
|
||||
self.commitSend(peerId: peerId, showNames: showNames, silently: silently)
|
||||
})
|
||||
} else {
|
||||
self.commitSend(peerId: peerId, showNames: showNames, silently: silently)
|
||||
}
|
||||
}
|
||||
|
||||
private func presentPaidMessageAlertIfNeeded(peers: [EnginePeer], requiresStars: [EnginePeer.Id: Int64], completion: @escaping () -> Void) {
|
||||
var count: Int32 = Int32(self.messageCount)
|
||||
if !self.inputFieldNode.text.isEmpty {
|
||||
count += 1
|
||||
}
|
||||
var totalAmount: StarsAmount = .zero
|
||||
for peer in peers {
|
||||
if let stars = requiresStars[peer.id] {
|
||||
totalAmount = totalAmount + StarsAmount(value: stars, nanos: 0)
|
||||
}
|
||||
}
|
||||
if totalAmount.value > 0 {
|
||||
let controller = chatMessagePaymentAlertController(
|
||||
context: nil,
|
||||
presentationData: self.presentationData,
|
||||
updatedPresentationData: nil,
|
||||
peers: peers,
|
||||
count: count,
|
||||
amount: totalAmount,
|
||||
totalAmount: totalAmount,
|
||||
hasCheck: false,
|
||||
navigationController: nil,
|
||||
completion: { _ in
|
||||
completion()
|
||||
}
|
||||
)
|
||||
self.present?(controller)
|
||||
} else {
|
||||
completion()
|
||||
}
|
||||
}
|
||||
|
||||
private func commitSend(peerId: PeerId?, showNames: Bool, silently: Bool) {
|
||||
if !self.inputFieldNode.text.isEmpty {
|
||||
for peer in self.controllerInteraction!.selectedPeers {
|
||||
|
|
@ -1522,7 +1570,7 @@ final class ShareControllerNode: ViewControllerTracingNode, ASScrollViewDelegate
|
|||
}
|
||||
}
|
||||
|
||||
func updatePeers(context: ShareControllerAccountContext, switchableAccounts: [ShareControllerSwitchableAccount], peers: [(peer: EngineRenderedPeer, presence: EnginePeer.Presence?, requiresPremiumForMessaging: Bool)], accountPeer: EnginePeer, defaultAction: ShareControllerAction?) {
|
||||
func updatePeers(context: ShareControllerAccountContext, switchableAccounts: [ShareControllerSwitchableAccount], peers: [(peer: EngineRenderedPeer, presence: EnginePeer.Presence?, requiresPremiumForMessaging: Bool, requiresStars: Int64?)], accountPeer: EnginePeer, defaultAction: ShareControllerAction?) {
|
||||
self.context = context
|
||||
|
||||
if let peersContentNode = self.peersContentNode, peersContentNode.accountPeer.id == accountPeer.id {
|
||||
|
|
|
|||
|
|
@ -96,11 +96,11 @@ final class ShareControllerGridSectionNode: ASDisplayNode {
|
|||
|
||||
final class ShareControllerPeerGridItem: GridItem {
|
||||
enum ShareItem: Equatable {
|
||||
case peer(peer: EngineRenderedPeer, presence: EnginePeer.Presence?, topicId: Int64?, threadData: MessageHistoryThreadData?, requiresPremiumForMessaging: Bool)
|
||||
case peer(peer: EngineRenderedPeer, presence: EnginePeer.Presence?, topicId: Int64?, threadData: MessageHistoryThreadData?, requiresPremiumForMessaging: Bool, requiresStars: Int64?)
|
||||
case story(isMessage: Bool)
|
||||
|
||||
var peerId: EnginePeer.Id? {
|
||||
if case let .peer(peer, _, _, _, _) = self {
|
||||
if case let .peer(peer, _, _, _, _, _) = self {
|
||||
return peer.peerId
|
||||
} else {
|
||||
return nil
|
||||
|
|
@ -162,7 +162,7 @@ final class ShareControllerPeerGridItemNode: GridItemNode {
|
|||
private var absoluteLocation: (CGRect, CGSize)?
|
||||
|
||||
var peerId: EnginePeer.Id? {
|
||||
if let item = self.currentState?.item, case let .peer(peer, _, _, _, _) = item {
|
||||
if let item = self.currentState?.item, case let .peer(peer, _, _, _, _, _) = item {
|
||||
return peer.peerId
|
||||
} else {
|
||||
return nil
|
||||
|
|
@ -177,7 +177,7 @@ final class ShareControllerPeerGridItemNode: GridItemNode {
|
|||
self.peerNode.toggleSelection = { [weak self] isDisabled in
|
||||
if let strongSelf = self {
|
||||
if let (_, _, _, _, maybeItem, search) = strongSelf.currentState, let item = maybeItem {
|
||||
if case let .peer(peer, _, _, _, _) = item, let _ = peer.peers[peer.peerId] {
|
||||
if case let .peer(peer, _, _, _, _, _) = item, let _ = peer.peers[peer.peerId] {
|
||||
if isDisabled {
|
||||
strongSelf.controllerInteraction?.disabledPeerSelected(peer)
|
||||
} else {
|
||||
|
|
@ -213,7 +213,7 @@ final class ShareControllerPeerGridItemNode: GridItemNode {
|
|||
var effectivePresence: EnginePeer.Presence?
|
||||
let timestamp = Int32(CFAbsoluteTimeGetCurrent() + NSTimeIntervalSince1970)
|
||||
self.peerNode.theme = itemTheme
|
||||
if let item, case let .peer(renderedPeer, presence, _, threadData, requiresPremiumForMessaging) = item, let peer = renderedPeer.peer {
|
||||
if let item, case let .peer(renderedPeer, presence, _, threadData, requiresPremiumForMessaging, requiresStars) = item, let peer = renderedPeer.peer {
|
||||
effectivePresence = presence
|
||||
var isOnline = false
|
||||
var isSupport = false
|
||||
|
|
@ -243,6 +243,7 @@ final class ShareControllerPeerGridItemNode: GridItemNode {
|
|||
strings: strings,
|
||||
peer: renderedPeer,
|
||||
requiresPremiumForMessaging: requiresPremiumForMessaging,
|
||||
requiresStars: requiresStars,
|
||||
customTitle: threadData?.info.title,
|
||||
iconId: threadData?.info.icon,
|
||||
iconColor: threadData?.info.iconColor ?? 0,
|
||||
|
|
@ -302,7 +303,7 @@ final class ShareControllerPeerGridItemNode: GridItemNode {
|
|||
func updateSelection(animated: Bool) {
|
||||
var selected = false
|
||||
if let controllerInteraction = self.controllerInteraction, let (_, _, _, _, maybeItem, _) = self.currentState, let item = maybeItem {
|
||||
if case let .peer(peer, _, _, _, _) = item {
|
||||
if case let .peer(peer, _, _, _, _, _) = item {
|
||||
selected = controllerInteraction.selectedPeerIds.contains(peer.peerId)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -43,7 +43,7 @@ private struct SharePeerEntry: Comparable, Identifiable {
|
|||
|
||||
var stableId: Int64 {
|
||||
switch self.item {
|
||||
case let .peer(peer, _, _, _, _):
|
||||
case let .peer(peer, _, _, _, _, _):
|
||||
return peer.peerId.toInt64()
|
||||
case .story:
|
||||
return 0
|
||||
|
|
@ -137,7 +137,7 @@ final class SharePeersContainerNode: ASDisplayNode, ShareContentContainerNode {
|
|||
private var validLayout: (CGSize, CGFloat)?
|
||||
private var overrideGridOffsetTransition: ContainedViewLayoutTransition?
|
||||
|
||||
let peersValue = Promise<[(peer: EngineRenderedPeer, presence: EnginePeer.Presence?, requiresPremiumForMessaging: Bool)]>()
|
||||
let peersValue = Promise<[(peer: EngineRenderedPeer, presence: EnginePeer.Presence?, requiresPremiumForMessaging: Bool, requiresStars: Int64?)]>()
|
||||
|
||||
private var _tick: Int = 0 {
|
||||
didSet {
|
||||
|
|
@ -146,7 +146,7 @@ final class SharePeersContainerNode: ASDisplayNode, ShareContentContainerNode {
|
|||
}
|
||||
private let tick = ValuePromise<Int>(0)
|
||||
|
||||
init(environment: ShareControllerEnvironment, context: ShareControllerAccountContext, switchableAccounts: [ShareControllerSwitchableAccount], theme: PresentationTheme, strings: PresentationStrings, nameDisplayOrder: PresentationPersonNameOrder, peers: [(peer: EngineRenderedPeer, presence: EnginePeer.Presence?, requiresPremiumForMessaging: Bool)], accountPeer: EnginePeer, controllerInteraction: ShareControllerInteraction, externalShare: Bool, switchToAnotherAccount: @escaping () -> Void, debugAction: @escaping () -> Void, extendedInitialReveal: Bool, segmentedValues: [ShareControllerSegmentedValue]?, fromPublicChannel: Bool) {
|
||||
init(environment: ShareControllerEnvironment, context: ShareControllerAccountContext, switchableAccounts: [ShareControllerSwitchableAccount], theme: PresentationTheme, strings: PresentationStrings, nameDisplayOrder: PresentationPersonNameOrder, peers: [(peer: EngineRenderedPeer, presence: EnginePeer.Presence?, requiresPremiumForMessaging: Bool, requiresStars: Int64?)], accountPeer: EnginePeer, controllerInteraction: ShareControllerInteraction, externalShare: Bool, switchToAnotherAccount: @escaping () -> Void, debugAction: @escaping () -> Void, extendedInitialReveal: Bool, segmentedValues: [ShareControllerSegmentedValue]?, fromPublicChannel: Bool) {
|
||||
self.environment = environment
|
||||
self.context = context
|
||||
self.theme = theme
|
||||
|
|
@ -176,22 +176,22 @@ final class SharePeersContainerNode: ASDisplayNode, ShareContentContainerNode {
|
|||
}
|
||||
|
||||
var existingPeerIds: Set<EnginePeer.Id> = Set()
|
||||
entries.append(SharePeerEntry(index: index, item: .peer(peer: EngineRenderedPeer(peer: accountPeer), presence: nil, topicId: nil, threadData: nil, requiresPremiumForMessaging: false), theme: theme, strings: strings))
|
||||
entries.append(SharePeerEntry(index: index, item: .peer(peer: EngineRenderedPeer(peer: accountPeer), presence: nil, topicId: nil, threadData: nil, requiresPremiumForMessaging: false, requiresStars: nil), theme: theme, strings: strings))
|
||||
existingPeerIds.insert(accountPeer.id)
|
||||
index += 1
|
||||
|
||||
for (peer, requiresPremiumForMessaging) in foundPeers.reversed() {
|
||||
if !existingPeerIds.contains(peer.peerId) {
|
||||
entries.append(SharePeerEntry(index: index, item: .peer(peer: peer, presence: nil, topicId: nil, threadData: nil, requiresPremiumForMessaging: requiresPremiumForMessaging), theme: theme, strings: strings))
|
||||
entries.append(SharePeerEntry(index: index, item: .peer(peer: peer, presence: nil, topicId: nil, threadData: nil, requiresPremiumForMessaging: requiresPremiumForMessaging, requiresStars: nil), theme: theme, strings: strings))
|
||||
existingPeerIds.insert(peer.peerId)
|
||||
index += 1
|
||||
}
|
||||
}
|
||||
|
||||
for (peer, presence, requiresPremiumForMessaging) in initialPeers {
|
||||
for (peer, presence, requiresPremiumForMessaging, requiresStars) in initialPeers {
|
||||
if !existingPeerIds.contains(peer.peerId) {
|
||||
let thread = controllerInteraction?.selectedTopics[peer.peerId]
|
||||
entries.append(SharePeerEntry(index: index, item: .peer(peer: peer, presence: presence, topicId: thread?.0, threadData: thread?.1, requiresPremiumForMessaging: requiresPremiumForMessaging), theme: theme, strings: strings))
|
||||
entries.append(SharePeerEntry(index: index, item: .peer(peer: peer, presence: presence, topicId: thread?.0, threadData: thread?.1, requiresPremiumForMessaging: requiresPremiumForMessaging, requiresStars: requiresStars), theme: theme, strings: strings))
|
||||
existingPeerIds.insert(peer.peerId)
|
||||
index += 1
|
||||
}
|
||||
|
|
|
|||
|
|
@ -36,13 +36,13 @@ private enum ShareSearchRecentEntryStableId: Hashable {
|
|||
|
||||
private enum ShareSearchRecentEntry: Comparable, Identifiable {
|
||||
case topPeers(PresentationTheme, PresentationStrings)
|
||||
case peer(index: Int, theme: PresentationTheme, peer: EnginePeer, associatedPeer: EnginePeer?, presence: EnginePeer.Presence?, requiresPremiumForMessaging: Bool, strings: PresentationStrings)
|
||||
case peer(index: Int, theme: PresentationTheme, peer: EnginePeer, associatedPeer: EnginePeer?, presence: EnginePeer.Presence?, requiresPremiumForMessaging: Bool, requiresStars: Int64?, strings: PresentationStrings)
|
||||
|
||||
var stableId: ShareSearchRecentEntryStableId {
|
||||
switch self {
|
||||
case .topPeers:
|
||||
return .topPeers
|
||||
case let .peer(_, _, peer, _, _, _, _):
|
||||
case let .peer(_, _, peer, _, _, _, _, _):
|
||||
return .peerId(peer.id)
|
||||
}
|
||||
}
|
||||
|
|
@ -61,8 +61,8 @@ private enum ShareSearchRecentEntry: Comparable, Identifiable {
|
|||
} else {
|
||||
return false
|
||||
}
|
||||
case let .peer(lhsIndex, lhsTheme, lhsPeer, lhsAssociatedPeer, lhsPresence, lhsRequiresPremiumForMessaging, lhsStrings):
|
||||
if case let .peer(rhsIndex, rhsTheme, rhsPeer, rhsAssociatedPeer, rhsPresence, rhsRequiresPremiumForMessaging, rhsStrings) = rhs, lhsPeer == rhsPeer && lhsAssociatedPeer == rhsAssociatedPeer && lhsIndex == rhsIndex && lhsStrings === rhsStrings && lhsTheme === rhsTheme && lhsPresence == rhsPresence && lhsRequiresPremiumForMessaging == rhsRequiresPremiumForMessaging {
|
||||
case let .peer(lhsIndex, lhsTheme, lhsPeer, lhsAssociatedPeer, lhsPresence, lhsRequiresPremiumForMessaging, lhsRequiresStars, lhsStrings):
|
||||
if case let .peer(rhsIndex, rhsTheme, rhsPeer, rhsAssociatedPeer, rhsPresence, rhsRequiresPremiumForMessaging, rhsRequiresStars, rhsStrings) = rhs, lhsPeer == rhsPeer && lhsAssociatedPeer == rhsAssociatedPeer && lhsIndex == rhsIndex && lhsStrings === rhsStrings && lhsTheme === rhsTheme && lhsPresence == rhsPresence && lhsRequiresPremiumForMessaging == rhsRequiresPremiumForMessaging && lhsRequiresStars == rhsRequiresStars {
|
||||
return true
|
||||
} else {
|
||||
return false
|
||||
|
|
@ -74,11 +74,11 @@ private enum ShareSearchRecentEntry: Comparable, Identifiable {
|
|||
switch lhs {
|
||||
case .topPeers:
|
||||
return true
|
||||
case let .peer(lhsIndex, _, _, _, _, _, _):
|
||||
case let .peer(lhsIndex, _, _, _, _, _, _, _):
|
||||
switch rhs {
|
||||
case .topPeers:
|
||||
return false
|
||||
case let .peer(rhsIndex, _, _, _, _, _, _):
|
||||
case let .peer(rhsIndex, _, _, _, _, _, _, _):
|
||||
return lhsIndex <= rhsIndex
|
||||
}
|
||||
}
|
||||
|
|
@ -88,13 +88,13 @@ private enum ShareSearchRecentEntry: Comparable, Identifiable {
|
|||
switch self {
|
||||
case let .topPeers(theme, strings):
|
||||
return ShareControllerRecentPeersGridItem(environment: environment, context: context, theme: theme, strings: strings, controllerInteraction: interfaceInteraction)
|
||||
case let .peer(_, theme, peer, associatedPeer, presence, requiresPremiumForMessaging, strings):
|
||||
case let .peer(_, theme, peer, associatedPeer, presence, requiresPremiumForMessaging, requiresStars, strings):
|
||||
var peers: [EnginePeer.Id: EnginePeer] = [peer.id: peer]
|
||||
if let associatedPeer = associatedPeer {
|
||||
peers[associatedPeer.id] = associatedPeer
|
||||
}
|
||||
let peer = EngineRenderedPeer(peerId: peer.id, peers: peers, associatedMedia: [:])
|
||||
return ShareControllerPeerGridItem(environment: environment, context: context, theme: theme, strings: strings, item: .peer(peer: peer, presence: presence, topicId: nil, threadData: nil, requiresPremiumForMessaging: requiresPremiumForMessaging), controllerInteraction: interfaceInteraction, sectionTitle: strings.DialogList_SearchSectionRecent, search: true)
|
||||
return ShareControllerPeerGridItem(environment: environment, context: context, theme: theme, strings: strings, item: .peer(peer: peer, presence: presence, topicId: nil, threadData: nil, requiresPremiumForMessaging: requiresPremiumForMessaging, requiresStars: requiresStars), controllerInteraction: interfaceInteraction, sectionTitle: strings.DialogList_SearchSectionRecent, search: true)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -104,6 +104,7 @@ private struct ShareSearchPeerEntry: Comparable, Identifiable {
|
|||
let peer: EngineRenderedPeer?
|
||||
let presence: EnginePeer.Presence?
|
||||
let requiresPremiumForMessaging: Bool
|
||||
let requiresStars: Int64?
|
||||
let theme: PresentationTheme
|
||||
let strings: PresentationStrings
|
||||
let isGlobal: Bool
|
||||
|
|
@ -129,6 +130,9 @@ private struct ShareSearchPeerEntry: Comparable, Identifiable {
|
|||
if lhs.requiresPremiumForMessaging != rhs.requiresPremiumForMessaging {
|
||||
return false
|
||||
}
|
||||
if lhs.requiresStars != rhs.requiresStars {
|
||||
return false
|
||||
}
|
||||
if lhs.theme !== rhs.theme {
|
||||
return false
|
||||
}
|
||||
|
|
@ -149,7 +153,7 @@ private struct ShareSearchPeerEntry: Comparable, Identifiable {
|
|||
} else {
|
||||
sectionTitle = nil
|
||||
}
|
||||
return ShareControllerPeerGridItem(environment: environment, context: context, theme: self.theme, strings: self.strings, item: self.peer.flatMap({ .peer(peer: $0, presence: self.presence, topicId: nil, threadData: nil, requiresPremiumForMessaging: self.requiresPremiumForMessaging) }), controllerInteraction: interfaceInteraction, sectionTitle: sectionTitle, search: true)
|
||||
return ShareControllerPeerGridItem(environment: environment, context: context, theme: self.theme, strings: self.strings, item: self.peer.flatMap({ .peer(peer: $0, presence: self.presence, topicId: nil, threadData: nil, requiresPremiumForMessaging: self.requiresPremiumForMessaging, requiresStars: self.requiresStars) }), controllerInteraction: interfaceInteraction, sectionTitle: sectionTitle, search: true)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -361,7 +365,7 @@ final class ShareSearchContainerNode: ASDisplayNode, ShareContentContainerNode {
|
|||
if strings.DialogList_SavedMessages.lowercased().hasPrefix(lowercasedQuery) || "saved messages".hasPrefix(lowercasedQuery) {
|
||||
if !existingPeerIds.contains(accountPeer.id) {
|
||||
existingPeerIds.insert(accountPeer.id)
|
||||
entries.append(ShareSearchPeerEntry(index: index, peer: EngineRenderedPeer(peer: EnginePeer(accountPeer)), presence: nil, requiresPremiumForMessaging: false, theme: theme, strings: strings, isGlobal: false))
|
||||
entries.append(ShareSearchPeerEntry(index: index, peer: EngineRenderedPeer(peer: EnginePeer(accountPeer)), presence: nil, requiresPremiumForMessaging: false, requiresStars: nil, theme: theme, strings: strings, isGlobal: false))
|
||||
index += 1
|
||||
}
|
||||
}
|
||||
|
|
@ -370,7 +374,7 @@ final class ShareSearchContainerNode: ASDisplayNode, ShareContentContainerNode {
|
|||
if let peer = renderedPeer.peers[renderedPeer.peerId], peer.id != accountPeer.id {
|
||||
if !existingPeerIds.contains(renderedPeer.peerId) && canSendMessagesToPeer(peer) {
|
||||
existingPeerIds.insert(renderedPeer.peerId)
|
||||
entries.append(ShareSearchPeerEntry(index: index, peer: EngineRenderedPeer(renderedPeer), presence: nil, requiresPremiumForMessaging: peerRequiresPremiumForMessaging[peer.id] ?? false, theme: theme, strings: strings, isGlobal: false))
|
||||
entries.append(ShareSearchPeerEntry(index: index, peer: EngineRenderedPeer(renderedPeer), presence: nil, requiresPremiumForMessaging: peerRequiresPremiumForMessaging[peer.id] ?? false, requiresStars: nil, theme: theme, strings: strings, isGlobal: false))
|
||||
index += 1
|
||||
}
|
||||
}
|
||||
|
|
@ -380,7 +384,7 @@ final class ShareSearchContainerNode: ASDisplayNode, ShareContentContainerNode {
|
|||
if foundRemotePeers.2 {
|
||||
isPlaceholder = true
|
||||
for _ in 0 ..< 4 {
|
||||
entries.append(ShareSearchPeerEntry(index: index, peer: nil, presence: nil, requiresPremiumForMessaging: false, theme: theme, strings: strings, isGlobal: false))
|
||||
entries.append(ShareSearchPeerEntry(index: index, peer: nil, presence: nil, requiresPremiumForMessaging: false, requiresStars: nil, theme: theme, strings: strings, isGlobal: false))
|
||||
index += 1
|
||||
}
|
||||
} else {
|
||||
|
|
@ -388,7 +392,7 @@ final class ShareSearchContainerNode: ASDisplayNode, ShareContentContainerNode {
|
|||
let peer = foundPeer.peer
|
||||
if !existingPeerIds.contains(peer.id) && canSendMessagesToPeer(peer) {
|
||||
existingPeerIds.insert(peer.id)
|
||||
entries.append(ShareSearchPeerEntry(index: index, peer: EngineRenderedPeer(peer: EnginePeer(foundPeer.peer)), presence: nil, requiresPremiumForMessaging: peerRequiresPremiumForMessaging[peer.id] ?? false, theme: theme, strings: strings, isGlobal: false))
|
||||
entries.append(ShareSearchPeerEntry(index: index, peer: EngineRenderedPeer(peer: EnginePeer(foundPeer.peer)), presence: nil, requiresPremiumForMessaging: peerRequiresPremiumForMessaging[peer.id] ?? false, requiresStars: nil, theme: theme, strings: strings, isGlobal: false))
|
||||
index += 1
|
||||
}
|
||||
}
|
||||
|
|
@ -397,7 +401,7 @@ final class ShareSearchContainerNode: ASDisplayNode, ShareContentContainerNode {
|
|||
let peer = foundPeer.peer
|
||||
if !existingPeerIds.contains(peer.id) && canSendMessagesToPeer(peer) {
|
||||
existingPeerIds.insert(peer.id)
|
||||
entries.append(ShareSearchPeerEntry(index: index, peer: EngineRenderedPeer(peer: EnginePeer(peer)), presence: nil, requiresPremiumForMessaging: peerRequiresPremiumForMessaging[peer.id] ?? false, theme: theme, strings: strings, isGlobal: true))
|
||||
entries.append(ShareSearchPeerEntry(index: index, peer: EngineRenderedPeer(peer: EnginePeer(peer)), presence: nil, requiresPremiumForMessaging: peerRequiresPremiumForMessaging[peer.id] ?? false, requiresStars: nil, theme: theme, strings: strings, isGlobal: true))
|
||||
index += 1
|
||||
}
|
||||
}
|
||||
|
|
@ -462,7 +466,7 @@ final class ShareSearchContainerNode: ASDisplayNode, ShareContentContainerNode {
|
|||
var index = 0
|
||||
for (peer, requiresPremiumForMessaging) in recentPeerList {
|
||||
if let mainPeer = peer.peers[peer.peerId], canSendMessagesToPeer(mainPeer._asPeer()) {
|
||||
recentItemList.append(.peer(index: index, theme: theme, peer: mainPeer, associatedPeer: mainPeer._asPeer().associatedPeerId.flatMap { peer.peers[$0] }, presence: nil, requiresPremiumForMessaging: requiresPremiumForMessaging, strings: strings))
|
||||
recentItemList.append(.peer(index: index, theme: theme, peer: mainPeer, associatedPeer: mainPeer._asPeer().associatedPeerId.flatMap { peer.peers[$0] }, presence: nil, requiresPremiumForMessaging: requiresPremiumForMessaging, requiresStars: nil, strings: strings))
|
||||
index += 1
|
||||
}
|
||||
}
|
||||
|
|
@ -570,7 +574,7 @@ final class ShareSearchContainerNode: ASDisplayNode, ShareContentContainerNode {
|
|||
switch $0 {
|
||||
case .topPeers:
|
||||
return false
|
||||
case let .peer(_, _, peer, _, _, _, _):
|
||||
case let .peer(_, _, peer, _, _, _, _, _):
|
||||
return peer.id == ensurePeerVisibleOnLayout
|
||||
}
|
||||
}) {
|
||||
|
|
|
|||
|
|
@ -1182,7 +1182,7 @@ private enum StatsEntry: ItemListNodeEntry {
|
|||
detailText = stringForMediumCompactDate(timestamp: date, strings: presentationData.strings, dateTimeFormat: presentationData.dateTimeFormat)
|
||||
}
|
||||
|
||||
let label = tonAmountAttributedString(formatTonAmountText(transaction.amount, dateTimeFormat: presentationData.dateTimeFormat, showPlus: true), integralFont: font, fractionalFont: smallLabelFont, color: labelColor).mutableCopy() as! NSMutableAttributedString
|
||||
let label = tonAmountAttributedString(formatTonAmountText(transaction.amount, dateTimeFormat: presentationData.dateTimeFormat, showPlus: true), integralFont: font, fractionalFont: smallLabelFont, color: labelColor, decimalSeparator: presentationData.dateTimeFormat.decimalSeparator).mutableCopy() as! NSMutableAttributedString
|
||||
|
||||
label.insert(NSAttributedString(string: " $ ", font: font, textColor: labelColor), at: 1)
|
||||
if let range = label.string.range(of: "$"), let icon = generateTintedImage(image: UIImage(bundleImageName: "Ads/TonMedium"), color: labelColor) {
|
||||
|
|
|
|||
|
|
@ -177,7 +177,7 @@ final class MonetizationBalanceItemNode: ListViewItemNode, ItemListItemNode {
|
|||
var isStars = false
|
||||
if let stats = item.stats as? RevenueStats {
|
||||
let cryptoValue = formatTonAmountText(stats.balances.availableBalance, dateTimeFormat: item.presentationData.dateTimeFormat)
|
||||
amountString = tonAmountAttributedString(cryptoValue, integralFont: integralFont, fractionalFont: fractionalFont, color: item.presentationData.theme.list.itemPrimaryTextColor)
|
||||
amountString = tonAmountAttributedString(cryptoValue, integralFont: integralFont, fractionalFont: fractionalFont, color: item.presentationData.theme.list.itemPrimaryTextColor, decimalSeparator: item.presentationData.dateTimeFormat.decimalSeparator)
|
||||
value = stats.balances.availableBalance == 0 ? "" : "≈\(formatTonUsdValue(stats.balances.availableBalance, rate: stats.usdRate, dateTimeFormat: item.presentationData.dateTimeFormat))"
|
||||
} else if let stats = item.stats as? StarsRevenueStats {
|
||||
amountString = NSAttributedString(string: presentationStringsFormattedNumber(stats.balances.availableBalance, item.presentationData.dateTimeFormat.groupingSeparator), font: integralFont, textColor: item.presentationData.theme.list.itemPrimaryTextColor)
|
||||
|
|
|
|||
|
|
@ -201,7 +201,7 @@ private final class ValueItemNode: ASDisplayNode {
|
|||
|
||||
let valueString: NSAttributedString
|
||||
if case .ton = mode {
|
||||
valueString = tonAmountAttributedString(value, integralFont: valueFont, fractionalFont: smallValueFont, color: valueColor)
|
||||
valueString = tonAmountAttributedString(value, integralFont: valueFont, fractionalFont: smallValueFont, color: valueColor, decimalSeparator: presentationData.dateTimeFormat.decimalSeparator)
|
||||
} else {
|
||||
valueString = NSAttributedString(string: value, font: valueFont, textColor: valueColor)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -139,7 +139,7 @@ private final class SheetContent: CombinedComponent {
|
|||
switch component.transaction {
|
||||
case let .proceeds(amount, fromDate, toDate):
|
||||
labelColor = theme.list.itemDisclosureActions.constructive.fillColor
|
||||
amountString = tonAmountAttributedString(formatTonAmountText(amount, dateTimeFormat: dateTimeFormat, showPlus: true), integralFont: integralFont, fractionalFont: fractionalFont, color: labelColor).mutableCopy() as! NSMutableAttributedString
|
||||
amountString = tonAmountAttributedString(formatTonAmountText(amount, dateTimeFormat: dateTimeFormat, showPlus: true), integralFont: integralFont, fractionalFont: fractionalFont, color: labelColor, decimalSeparator: dateTimeFormat.decimalSeparator).mutableCopy() as! NSMutableAttributedString
|
||||
dateString = "\(stringForMediumCompactDate(timestamp: fromDate, strings: strings, dateTimeFormat: dateTimeFormat)) – \(stringForMediumCompactDate(timestamp: toDate, strings: strings, dateTimeFormat: dateTimeFormat))"
|
||||
titleString = strings.Monetization_TransactionInfo_Proceeds
|
||||
buttonTitle = strings.Common_OK
|
||||
|
|
@ -147,7 +147,7 @@ private final class SheetContent: CombinedComponent {
|
|||
showPeer = true
|
||||
case let .withdrawal(status, amount, date, provider, _, transactionUrl):
|
||||
labelColor = theme.list.itemDestructiveColor
|
||||
amountString = tonAmountAttributedString(formatTonAmountText(amount, dateTimeFormat: dateTimeFormat), integralFont: integralFont, fractionalFont: fractionalFont, color: labelColor).mutableCopy() as! NSMutableAttributedString
|
||||
amountString = tonAmountAttributedString(formatTonAmountText(amount, dateTimeFormat: dateTimeFormat), integralFont: integralFont, fractionalFont: fractionalFont, color: labelColor, decimalSeparator: dateTimeFormat.decimalSeparator).mutableCopy() as! NSMutableAttributedString
|
||||
dateString = stringForFullDate(timestamp: date, strings: strings, dateTimeFormat: dateTimeFormat)
|
||||
|
||||
switch status {
|
||||
|
|
@ -166,7 +166,7 @@ private final class SheetContent: CombinedComponent {
|
|||
case let .refund(amount, date, _):
|
||||
labelColor = theme.list.itemDisclosureActions.constructive.fillColor
|
||||
titleString = strings.Monetization_TransactionInfo_Refund
|
||||
amountString = tonAmountAttributedString(formatTonAmountText(amount, dateTimeFormat: dateTimeFormat, showPlus: true), integralFont: integralFont, fractionalFont: fractionalFont, color: labelColor).mutableCopy() as! NSMutableAttributedString
|
||||
amountString = tonAmountAttributedString(formatTonAmountText(amount, dateTimeFormat: dateTimeFormat, showPlus: true), integralFont: integralFont, fractionalFont: fractionalFont, color: labelColor, decimalSeparator: dateTimeFormat.decimalSeparator).mutableCopy() as! NSMutableAttributedString
|
||||
dateString = stringForFullDate(timestamp: date, strings: strings, dateTimeFormat: dateTimeFormat)
|
||||
buttonTitle = strings.Common_OK
|
||||
explorerUrl = nil
|
||||
|
|
|
|||
|
|
@ -190,6 +190,7 @@ private var declaredEncodables: Void = {
|
|||
declareEncodable(OutgoingScheduleInfoMessageAttribute.self, f: { OutgoingScheduleInfoMessageAttribute(decoder: $0) })
|
||||
declareEncodable(UpdateMessageReactionsAction.self, f: { UpdateMessageReactionsAction(decoder: $0) })
|
||||
declareEncodable(SendStarsReactionsAction.self, f: { SendStarsReactionsAction(decoder: $0) })
|
||||
declareEncodable(PostponeSendPaidMessageAction.self, f: { PostponeSendPaidMessageAction(decoder: $0) })
|
||||
declareEncodable(RestrictedContentMessageAttribute.self, f: { RestrictedContentMessageAttribute(decoder: $0) })
|
||||
declareEncodable(SendScheduledMessageImmediatelyAction.self, f: { SendScheduledMessageImmediatelyAction(decoder: $0) })
|
||||
declareEncodable(EmbeddedMediaStickersMessageAttribute.self, f: { EmbeddedMediaStickersMessageAttribute(decoder: $0) })
|
||||
|
|
|
|||
|
|
@ -904,7 +904,7 @@ extension StoreMessage {
|
|||
}
|
||||
|
||||
if let paidMessageStars {
|
||||
attributes.append(PaidStarsMessageAttribute(stars: StarsAmount(value: paidMessageStars, nanos: 0)))
|
||||
attributes.append(PaidStarsMessageAttribute(stars: StarsAmount(value: paidMessageStars, nanos: 0), postponeSending: false))
|
||||
}
|
||||
|
||||
var entitiesAttribute: TextEntitiesMessageAttribute?
|
||||
|
|
|
|||
|
|
@ -331,6 +331,16 @@ public final class AccountStateManager {
|
|||
return self.forceSendPendingStarsReactionPipe.signal()
|
||||
}
|
||||
|
||||
fileprivate let forceSendPendingPaidMessagePipe = ValuePipe<PeerId>()
|
||||
public var forceSendPendingPaidMessage: Signal<PeerId, NoError> {
|
||||
return self.forceSendPendingPaidMessagePipe.signal()
|
||||
}
|
||||
|
||||
fileprivate let commitSendPendingPaidMessagePipe = ValuePipe<MessageId>()
|
||||
public var commitSendPendingPaidMessage: Signal<MessageId, NoError> {
|
||||
return self.commitSendPendingPaidMessagePipe.signal()
|
||||
}
|
||||
|
||||
fileprivate let sentScheduledMessageIdsPipe = ValuePipe<Set<MessageId>>()
|
||||
public var sentScheduledMessageIds: Signal<Set<MessageId>, NoError> {
|
||||
return self.sentScheduledMessageIdsPipe.signal()
|
||||
|
|
@ -1951,6 +1961,18 @@ public final class AccountStateManager {
|
|||
}
|
||||
}
|
||||
|
||||
var forceSendPendingPaidMessage: Signal<PeerId, NoError> {
|
||||
return self.impl.signalWith { impl, subscriber in
|
||||
return impl.forceSendPendingPaidMessage.start(next: subscriber.putNext, error: subscriber.putError, completed: subscriber.putCompletion)
|
||||
}
|
||||
}
|
||||
|
||||
var commitSendPendingPaidMessage: Signal<MessageId, NoError> {
|
||||
return self.impl.signalWith { impl, subscriber in
|
||||
return impl.commitSendPendingPaidMessage.start(next: subscriber.putNext, error: subscriber.putError, completed: subscriber.putCompletion)
|
||||
}
|
||||
}
|
||||
|
||||
public var sentScheduledMessageIds: Signal<Set<MessageId>, NoError> {
|
||||
return self.impl.signalWith { impl, subscriber in
|
||||
return impl.sentScheduledMessageIds.start(next: subscriber.putNext, error: subscriber.putError, completed: subscriber.putCompletion)
|
||||
|
|
@ -1963,6 +1985,19 @@ public final class AccountStateManager {
|
|||
}
|
||||
}
|
||||
|
||||
|
||||
func forceSendPendingPaidMessage(peerId: PeerId) {
|
||||
self.impl.with { impl in
|
||||
impl.forceSendPendingPaidMessagePipe.putNext(peerId)
|
||||
}
|
||||
}
|
||||
|
||||
func commitSendPendingPaidMessage(messageId: MessageId) {
|
||||
self.impl.with { impl in
|
||||
impl.commitSendPendingPaidMessagePipe.putNext(messageId)
|
||||
}
|
||||
}
|
||||
|
||||
var updateConfigRequested: (() -> Void)?
|
||||
var isPremiumUpdated: (() -> Void)?
|
||||
|
||||
|
|
|
|||
|
|
@ -88,6 +88,9 @@ final class AccountTaskManager {
|
|||
tasks.add(managedSynchronizeMarkAllUnseenReactionsOperations(postbox: self.stateManager.postbox, network: self.stateManager.network, stateManager: self.stateManager).start())
|
||||
tasks.add(managedApplyPendingMessageReactionsActions(postbox: self.stateManager.postbox, network: self.stateManager.network, stateManager: self.stateManager).start())
|
||||
tasks.add(managedApplyPendingMessageStarsReactionsActions(postbox: self.stateManager.postbox, network: self.stateManager.network, stateManager: self.stateManager).start())
|
||||
|
||||
tasks.add(managedApplyPendingPaidMessageActions(postbox: self.stateManager.postbox, network: self.stateManager.network, stateManager: self.stateManager).start())
|
||||
|
||||
tasks.add(managedSynchronizeEmojiKeywordsOperations(postbox: self.stateManager.postbox, network: self.stateManager.network).start())
|
||||
tasks.add(managedApplyPendingScheduledMessagesActions(postbox: self.stateManager.postbox, network: self.stateManager.network, stateManager: self.stateManager).start())
|
||||
tasks.add(managedSynchronizeAvailableReactions(postbox: self.stateManager.postbox, network: self.stateManager.network).start())
|
||||
|
|
|
|||
|
|
@ -89,4 +89,169 @@ func _internal_updateChannelPaidMessagesStars(account: Account, peerId: PeerId,
|
|||
|> switchToLatest
|
||||
}
|
||||
|
||||
public final class PostponeSendPaidMessageAction: PendingMessageActionData {
|
||||
public let randomId: Int64
|
||||
|
||||
public init(randomId: Int64) {
|
||||
self.randomId = randomId
|
||||
}
|
||||
|
||||
public init(decoder: PostboxDecoder) {
|
||||
self.randomId = decoder.decodeInt64ForKey("id", orElse: 0)
|
||||
}
|
||||
|
||||
public func encode(_ encoder: PostboxEncoder) {
|
||||
encoder.encodeInt64(self.randomId, forKey: "id")
|
||||
}
|
||||
|
||||
public func isEqual(to: PendingMessageActionData) -> Bool {
|
||||
if let other = to as? PostponeSendPaidMessageAction {
|
||||
if self.randomId != other.randomId {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private final class ManagedApplyPendingPaidMessageActionsHelper {
|
||||
var operationDisposables: [MessageId: (PendingMessageActionData, Disposable)] = [:]
|
||||
|
||||
func update(entries: [PendingMessageActionsEntry]) -> (disposeOperations: [Disposable], beginOperations: [(PendingMessageActionsEntry, MetaDisposable)]) {
|
||||
var disposeOperations: [Disposable] = []
|
||||
var beginOperations: [(PendingMessageActionsEntry, MetaDisposable)] = []
|
||||
|
||||
var hasRunningOperationForPeerId = Set<PeerId>()
|
||||
var validIds = Set<MessageId>()
|
||||
for entry in entries {
|
||||
if let current = self.operationDisposables[entry.id], !current.0.isEqual(to: entry.action) {
|
||||
self.operationDisposables.removeValue(forKey: entry.id)
|
||||
disposeOperations.append(current.1)
|
||||
}
|
||||
|
||||
if !hasRunningOperationForPeerId.contains(entry.id.peerId) {
|
||||
hasRunningOperationForPeerId.insert(entry.id.peerId)
|
||||
validIds.insert(entry.id)
|
||||
|
||||
let disposable = MetaDisposable()
|
||||
beginOperations.append((entry, disposable))
|
||||
self.operationDisposables[entry.id] = (entry.action, disposable)
|
||||
}
|
||||
}
|
||||
|
||||
var removeMergedIds: [MessageId] = []
|
||||
for (id, actionAndDisposable) in self.operationDisposables {
|
||||
if !validIds.contains(id) {
|
||||
removeMergedIds.append(id)
|
||||
disposeOperations.append(actionAndDisposable.1)
|
||||
}
|
||||
}
|
||||
|
||||
for id in removeMergedIds {
|
||||
self.operationDisposables.removeValue(forKey: id)
|
||||
}
|
||||
|
||||
return (disposeOperations, beginOperations)
|
||||
}
|
||||
|
||||
func reset() -> [Disposable] {
|
||||
let disposables = Array(self.operationDisposables.values.map(\.1))
|
||||
self.operationDisposables.removeAll()
|
||||
return disposables
|
||||
}
|
||||
}
|
||||
|
||||
private func withTakenStarsAction(postbox: Postbox, type: PendingMessageActionType, id: MessageId, _ f: @escaping (Transaction, PendingMessageActionsEntry?) -> Signal<Never, NoError>) -> Signal<Never, NoError> {
|
||||
return postbox.transaction { transaction -> Signal<Never, NoError> in
|
||||
var result: PendingMessageActionsEntry?
|
||||
|
||||
if let action = transaction.getPendingMessageAction(type: type, id: id) as? PostponeSendPaidMessageAction {
|
||||
result = PendingMessageActionsEntry(id: id, action: action)
|
||||
}
|
||||
|
||||
return f(transaction, result)
|
||||
}
|
||||
|> switchToLatest
|
||||
}
|
||||
|
||||
private func sendPostponedPaidMessage(transaction: Transaction, postbox: Postbox, network: Network, stateManager: AccountStateManager, id: MessageId) -> Signal<Never, NoError> {
|
||||
stateManager.commitSendPendingPaidMessage(messageId: id)
|
||||
return postbox.transaction { transaction -> Void in
|
||||
transaction.setPendingMessageAction(type: .sendPostponedPaidMessage, id: id, action: nil)
|
||||
}
|
||||
|> ignoreValues
|
||||
}
|
||||
|
||||
func managedApplyPendingPaidMessageActions(postbox: Postbox, network: Network, stateManager: AccountStateManager) -> Signal<Void, NoError> {
|
||||
return Signal { _ in
|
||||
let helper = Atomic<ManagedApplyPendingPaidMessageActionsHelper>(value: ManagedApplyPendingPaidMessageActionsHelper())
|
||||
|
||||
let actionsKey = PostboxViewKey.pendingMessageActions(type: .sendPostponedPaidMessage)
|
||||
let disposable = postbox.combinedView(keys: [actionsKey]).start(next: { view in
|
||||
var entries: [PendingMessageActionsEntry] = []
|
||||
if let v = view.views[actionsKey] as? PendingMessageActionsView {
|
||||
entries = v.entries
|
||||
}
|
||||
|
||||
let (disposeOperations, beginOperations) = helper.with { helper -> (disposeOperations: [Disposable], beginOperations: [(PendingMessageActionsEntry, MetaDisposable)]) in
|
||||
return helper.update(entries: entries)
|
||||
}
|
||||
|
||||
for disposable in disposeOperations {
|
||||
disposable.dispose()
|
||||
}
|
||||
|
||||
for (entry, disposable) in beginOperations {
|
||||
let signal = withTakenStarsAction(postbox: postbox, type: .sendPostponedPaidMessage, id: entry.id, { transaction, entry -> Signal<Never, NoError> in
|
||||
if let entry = entry {
|
||||
if let _ = entry.action as? PostponeSendPaidMessageAction {
|
||||
let triggerSignal: Signal<Void, NoError> = stateManager.forceSendPendingPaidMessage
|
||||
|> filter {
|
||||
$0 == entry.id.peerId
|
||||
}
|
||||
|> map { _ -> Void in
|
||||
return Void()
|
||||
}
|
||||
|> take(1)
|
||||
|> timeout(5.0, queue: .mainQueue(), alternate: .single(Void()))
|
||||
|
||||
return triggerSignal
|
||||
|> mapToSignal { _ -> Signal<Never, NoError> in
|
||||
return sendPostponedPaidMessage(transaction: transaction, postbox: postbox, network: network, stateManager: stateManager, id: entry.id)
|
||||
}
|
||||
} else {
|
||||
assertionFailure()
|
||||
}
|
||||
}
|
||||
return .complete()
|
||||
})
|
||||
|> then(
|
||||
postbox.transaction { transaction -> Void in
|
||||
transaction.setPendingMessageAction(type: .sendPostponedPaidMessage, id: entry.id, action: nil)
|
||||
}
|
||||
|> ignoreValues
|
||||
)
|
||||
|
||||
disposable.set(signal.start())
|
||||
}
|
||||
})
|
||||
|
||||
return ActionDisposable {
|
||||
let disposables = helper.with { helper -> [Disposable] in
|
||||
return helper.reset()
|
||||
}
|
||||
for disposable in disposables {
|
||||
disposable.dispose()
|
||||
}
|
||||
disposable.dispose()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func _internal_forceSendPostponedPaidMessage(account: Account, peerId: PeerId) -> Signal<Never, NoError> {
|
||||
account.stateManager.forceSendPendingPaidMessage(peerId: peerId)
|
||||
|
||||
return .complete()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -63,6 +63,8 @@ private final class PendingMessageContext {
|
|||
var error: PendingMessageFailureReason?
|
||||
var statusSubscribers = Bag<(PendingMessageStatus?, PendingMessageFailureReason?) -> Void>()
|
||||
var forcedReuploadOnce: Bool = false
|
||||
let postponeDisposable = MetaDisposable()
|
||||
var postponeSending = false
|
||||
}
|
||||
|
||||
public enum PendingMessageFailureReason {
|
||||
|
|
@ -486,7 +488,10 @@ public final class PendingMessageManager {
|
|||
|
||||
|
||||
for (messageContext, message, type, contentUploadSignal) in messagesToUpload {
|
||||
if strongSelf.canBeginUploadingMessage(id: message.id, type: type) {
|
||||
if let paidStarsAttribute = message.paidStarsAttribute, paidStarsAttribute.postponeSending {
|
||||
strongSelf.beginWaitingForPostponedMessageCommit(messageContext: messageContext, id: message.id)
|
||||
}
|
||||
if strongSelf.canBeginUploadingMessage(id: message.id, type: type), !messageContext.postponeSending {
|
||||
strongSelf.beginUploadingMessage(messageContext: messageContext, id: message.id, threadId: message.threadId, groupId: message.groupingKey, uploadSignal: contentUploadSignal)
|
||||
} else {
|
||||
messageContext.state = .waitingForUploadToStart(groupId: message.groupingKey, upload: contentUploadSignal)
|
||||
|
|
@ -663,6 +668,33 @@ public final class PendingMessageManager {
|
|||
messageContext.state = .collectingInfo(message: message)
|
||||
}
|
||||
|
||||
private func beginWaitingForPostponedMessageCommit(messageContext: PendingMessageContext, id: MessageId) {
|
||||
messageContext.postponeSending = true
|
||||
|
||||
let signal: Signal<Void, NoError> = self.postbox.transaction { transaction -> Void in
|
||||
transaction.setPendingMessageAction(type: .sendPostponedPaidMessage, id: id, action: PostponeSendPaidMessageAction(randomId: Int64.random(in: Int64.min ... Int64.max)))
|
||||
}
|
||||
|> mapToSignal { _ in
|
||||
return self.stateManager.commitSendPendingPaidMessage
|
||||
|> filter {
|
||||
$0 == id
|
||||
}
|
||||
|> take(1)
|
||||
|> map { _ in
|
||||
Void()
|
||||
}
|
||||
}
|
||||
|> deliverOn(self.queue)
|
||||
|
||||
messageContext.postponeDisposable.set(signal.start(next: { [weak self] _ in
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
messageContext.postponeSending = false
|
||||
self.updateWaitingUploads(peerId: id.peerId)
|
||||
}))
|
||||
}
|
||||
|
||||
private func beginUploadingMessage(messageContext: PendingMessageContext, id: MessageId, threadId: Int64?, groupId: Int64?, uploadSignal: Signal<PendingMessageUploadedContentResult, PendingMessageUploadError>) {
|
||||
messageContext.state = .uploading(groupId: groupId)
|
||||
|
||||
|
|
@ -740,7 +772,7 @@ public final class PendingMessageManager {
|
|||
loop: for contextId in messageIdsForPeer {
|
||||
let context = self.messageContexts[contextId]!
|
||||
if case let .waitingForUploadToStart(groupId, uploadSignal) = context.state {
|
||||
if self.canBeginUploadingMessage(id: contextId, type: context.contentType ?? .media) {
|
||||
if self.canBeginUploadingMessage(id: contextId, type: context.contentType ?? .media), !context.postponeSending {
|
||||
context.state = .uploading(groupId: groupId)
|
||||
let status = PendingMessageStatus(isRunning: true, progress: PendingMessageStatus.Progress(progress: 0.0))
|
||||
context.status = status
|
||||
|
|
|
|||
|
|
@ -4,20 +4,24 @@ import TelegramApi
|
|||
|
||||
public final class PaidStarsMessageAttribute: Equatable, MessageAttribute {
|
||||
public let stars: StarsAmount
|
||||
public let postponeSending: Bool
|
||||
|
||||
public init(stars: StarsAmount) {
|
||||
public init(stars: StarsAmount, postponeSending: Bool) {
|
||||
self.stars = stars
|
||||
self.postponeSending = postponeSending
|
||||
}
|
||||
|
||||
required public init(decoder: PostboxDecoder) {
|
||||
self.stars = decoder.decodeCodable(StarsAmount.self, forKey: "s") ?? StarsAmount(value: 0, nanos: 0)
|
||||
self.postponeSending = decoder.decodeBoolForKey("ps", orElse: false)
|
||||
}
|
||||
|
||||
public func encode(_ encoder: PostboxEncoder) {
|
||||
encoder.encodeCodable(self.stars, forKey: "s")
|
||||
encoder.encodeBool(self.postponeSending, forKey: "ps")
|
||||
}
|
||||
|
||||
public static func ==(lhs: PaidStarsMessageAttribute, rhs: PaidStarsMessageAttribute) -> Bool {
|
||||
return lhs.stars == rhs.stars
|
||||
return lhs.stars == rhs.stars && lhs.postponeSending == rhs.postponeSending
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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]
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -238,6 +238,7 @@ private struct ApplicationSpecificNoticeKeys {
|
|||
private static let groupEmojiPackNamespace: Int32 = 9
|
||||
private static let dismissedBirthdayPremiumGiftTipNamespace: Int32 = 10
|
||||
private static let displayedPeerVerificationNamespace: Int32 = 11
|
||||
private static let dismissedPaidMessageWarningNamespace: Int32 = 11
|
||||
|
||||
static func inlineBotLocationRequestNotice(peerId: PeerId) -> NoticeEntryKey {
|
||||
return NoticeEntryKey(namespace: noticeNamespace(namespace: inlineBotLocationRequestNamespace), key: noticeKey(peerId: peerId, key: 0))
|
||||
|
|
@ -523,6 +524,10 @@ private struct ApplicationSpecificNoticeKeys {
|
|||
return NoticeEntryKey(namespace: noticeNamespace(namespace: displayedPeerVerificationNamespace), key: noticeKey(peerId: peerId, key: 0))
|
||||
}
|
||||
|
||||
static func dismissedPaidMessageWarning(peerId: PeerId) -> NoticeEntryKey {
|
||||
return NoticeEntryKey(namespace: noticeNamespace(namespace: dismissedPaidMessageWarningNamespace), key: noticeKey(peerId: peerId, key: 0))
|
||||
}
|
||||
|
||||
static func monetizationIntroDismissed() -> NoticeEntryKey {
|
||||
return NoticeEntryKey(namespace: noticeNamespace(namespace: globalNamespace), key: ApplicationSpecificGlobalNotice.monetizationIntroDismissed.key)
|
||||
}
|
||||
|
|
@ -2176,6 +2181,28 @@ public struct ApplicationSpecificNotice {
|
|||
|> ignoreValues
|
||||
}
|
||||
|
||||
public static func dismissedPaidMessageWarningNamespace(accountManager: AccountManager<TelegramAccountManagerTypes>, peerId: PeerId) -> Signal<Int64?, NoError> {
|
||||
return accountManager.noticeEntry(key: ApplicationSpecificNoticeKeys.dismissedPaidMessageWarning(peerId: peerId))
|
||||
|> map { view -> Int64? in
|
||||
if let counter = view.value?.get(ApplicationSpecificCounterNotice.self) {
|
||||
return Int64(counter.value)
|
||||
} else {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static func setDismissedPaidMessageWarningNamespace(accountManager: AccountManager<TelegramAccountManagerTypes>, peerId: PeerId, amount: Int64?) -> Signal<Never, NoError> {
|
||||
return accountManager.transaction { transaction -> Void in
|
||||
if let amount, let entry = CodableEntry(ApplicationSpecificCounterNotice(value: Int32(amount))) {
|
||||
transaction.setNotice(ApplicationSpecificNoticeKeys.dismissedPaidMessageWarning(peerId: peerId), entry)
|
||||
} else {
|
||||
transaction.setNotice(ApplicationSpecificNoticeKeys.dismissedPaidMessageWarning(peerId: peerId), nil)
|
||||
}
|
||||
}
|
||||
|> ignoreValues
|
||||
}
|
||||
|
||||
public static func setMonetizationIntroDismissed(accountManager: AccountManager<TelegramAccountManagerTypes>) -> Signal<Never, NoError> {
|
||||
return accountManager.transaction { transaction -> Void in
|
||||
if let entry = CodableEntry(ApplicationSpecificBoolNotice()) {
|
||||
|
|
|
|||
|
|
@ -313,12 +313,16 @@ public enum PresentationResourceKey: Int32 {
|
|||
case chatBubbleCloseIcon
|
||||
|
||||
case chatEmptyStateStarIcon
|
||||
case chatPlaceholderStarIcon
|
||||
|
||||
case avatarPremiumLockBadgeBackground
|
||||
case avatarPremiumLockBadge
|
||||
case shareAvatarPremiumLockBadgeBackground
|
||||
case shareAvatarPremiumLockBadge
|
||||
|
||||
case shareAvatarStarsLockBadgeBackground
|
||||
case shareAvatarStarsLockBadgeInnerBackground
|
||||
|
||||
case sharedLinkIcon
|
||||
|
||||
case hideIconImage
|
||||
|
|
|
|||
|
|
@ -1351,4 +1351,19 @@ public struct PresentationResourcesChat {
|
|||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
public static func chatPlaceholderStarIcon(_ theme: PresentationTheme) -> UIImage? {
|
||||
return theme.image(PresentationResourceKey.chatPlaceholderStarIcon.rawValue, { theme in
|
||||
if let image = UIImage(bundleImageName: "Premium/Stars/ButtonStar") {
|
||||
return generateImage(image.size, contextGenerator: { size, context in
|
||||
let bounds = CGRect(origin: .zero, size: size)
|
||||
context.clear(bounds)
|
||||
if let cgImage = image.cgImage {
|
||||
context.draw(cgImage, in: bounds.offsetBy(dx: UIScreenPixel, dy: -UIScreenPixel), byTiling: false)
|
||||
}
|
||||
})
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -488,6 +488,29 @@ public struct PresentationResourcesChatList {
|
|||
})
|
||||
}
|
||||
|
||||
public static func shareAvatarStarsLockBadgeBackground(_ theme: PresentationTheme) -> UIImage? {
|
||||
return theme.image(PresentationResourceKey.shareAvatarStarsLockBadgeBackground.rawValue, { theme in
|
||||
return generateImage(CGSize(width: 20.0, height: 20.0), contextGenerator: { size, context in
|
||||
context.clear(CGRect(origin: .zero, size: size))
|
||||
context.setFillColor(UIColor.white.cgColor)
|
||||
let rect = CGRect(origin: .zero, size: CGSize(width: 20.0, height: 18.0 + UIScreenPixel)).insetBy(dx: 1.0 - UIScreenPixel, dy: 0.0)
|
||||
context.addPath(UIBezierPath(roundedRect: rect, cornerRadius: rect.height / 2.0).cgPath)
|
||||
context.fillPath()
|
||||
})?.withRenderingMode(.alwaysTemplate).stretchableImage(withLeftCapWidth: 10, topCapHeight: 10)
|
||||
})
|
||||
}
|
||||
|
||||
public static func shareAvatarStarsLockBadgeInnerBackground(_ theme: PresentationTheme) -> UIImage? {
|
||||
return theme.image(PresentationResourceKey.shareAvatarStarsLockBadgeInnerBackground.rawValue, { theme in
|
||||
return generateImage(CGSize(width: 20.0, height: 16.0), contextGenerator: { size, context in
|
||||
context.clear(CGRect(origin: .zero, size: size))
|
||||
context.setFillColor(UIColor.white.cgColor)
|
||||
context.addPath(UIBezierPath(roundedRect: CGRect(origin: .zero, size: CGSize(width: 20.0, height: 15.0)), cornerRadius: 7.5).cgPath)
|
||||
context.fillPath()
|
||||
})?.withRenderingMode(.alwaysTemplate).stretchableImage(withLeftCapWidth: 10, topCapHeight: 0)
|
||||
})
|
||||
}
|
||||
|
||||
public static func shareAvatarPremiumLockBadge(_ theme: PresentationTheme) -> UIImage? {
|
||||
return theme.image(PresentationResourceKey.shareAvatarPremiumLockBadge.rawValue, { theme in
|
||||
return generateImage(CGSize(width: 20.0, height: 20.0), contextGenerator: { size, context in
|
||||
|
|
|
|||
|
|
@ -79,7 +79,7 @@ private func peerDisplayTitles(_ peers: [Peer], strings: PresentationStrings, na
|
|||
}
|
||||
}
|
||||
|
||||
public func universalServiceMessageString(presentationData: (PresentationTheme, TelegramWallpaper)?, strings: PresentationStrings, nameDisplayOrder: PresentationPersonNameOrder, dateTimeFormat: PresentationDateTimeFormat, message: EngineMessage, accountPeerId: EnginePeer.Id, forChatList: Bool, forForumOverview: Bool, forAdditionalServiceMessage: Bool = false) -> NSAttributedString? {
|
||||
public func universalServiceMessageString(presentationData: (PresentationTheme, TelegramWallpaper)?, strings: PresentationStrings, nameDisplayOrder: PresentationPersonNameOrder, dateTimeFormat: PresentationDateTimeFormat, message: EngineMessage, messageCount: Int? = nil, accountPeerId: EnginePeer.Id, forChatList: Bool, forForumOverview: Bool, forAdditionalServiceMessage: Bool = false) -> NSAttributedString? {
|
||||
var attributedString: NSAttributedString?
|
||||
|
||||
let primaryTextColor: UIColor
|
||||
|
|
@ -92,6 +92,33 @@ public func universalServiceMessageString(presentationData: (PresentationTheme,
|
|||
let bodyAttributes = MarkdownAttributeSet(font: titleFont, textColor: primaryTextColor, additionalAttributes: [:])
|
||||
let boldAttributes = MarkdownAttributeSet(font: titleBoldFont, textColor: primaryTextColor, additionalAttributes: [:])
|
||||
|
||||
if !forAdditionalServiceMessage {
|
||||
for attribute in message.attributes {
|
||||
if let attribute = attribute as? PaidStarsMessageAttribute {
|
||||
let messageCount = Int32(messageCount ?? 1)
|
||||
let price = strings.Notification_PaidMessage_Stars(Int32(attribute.stars.value) * messageCount)
|
||||
if message.author?.id == accountPeerId {
|
||||
if messageCount > 1 {
|
||||
let messagesString = strings.Notification_PaidMessage_Messages(messageCount)
|
||||
return addAttributesToStringWithRanges(strings.Notification_PaidMessageYouMany(price, messagesString)._tuple, body: bodyAttributes, argumentAttributes: [0: boldAttributes, 1: boldAttributes])
|
||||
} else {
|
||||
return addAttributesToStringWithRanges(strings.Notification_PaidMessageYou(price)._tuple, body: bodyAttributes, argumentAttributes: [0: boldAttributes])
|
||||
}
|
||||
} else {
|
||||
let compactAuthorName = message.author?.compactDisplayTitle ?? ""
|
||||
var attributes = peerMentionsAttributes(primaryTextColor: primaryTextColor, peerIds: [(0, message.author?.id)])
|
||||
attributes[1] = boldAttributes
|
||||
if messageCount > 1 {
|
||||
let messagesString = strings.Notification_PaidMessage_Messages(messageCount)
|
||||
return addAttributesToStringWithRanges(strings.Notification_PaidMessageMany(compactAuthorName, price, messagesString)._tuple, body: bodyAttributes, argumentAttributes: attributes)
|
||||
} else {
|
||||
return addAttributesToStringWithRanges(strings.Notification_PaidMessage(compactAuthorName, price)._tuple, body: bodyAttributes, argumentAttributes: attributes)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for media in message.media {
|
||||
if let action = media as? TelegramMediaAction {
|
||||
let authorName = message.author?.displayTitle(strings: strings, displayOrder: nameDisplayOrder) ?? ""
|
||||
|
|
@ -1150,11 +1177,11 @@ public func universalServiceMessageString(presentationData: (PresentationTheme,
|
|||
let attributes = peerMentionsAttributes(primaryTextColor: primaryTextColor, peerIds: peerIds)
|
||||
attributedString = addAttributesToStringWithRanges(strings.Notification_StarsGift_TransferToChannelYou(peerName)._tuple, body: bodyAttributes, argumentAttributes: attributes)
|
||||
} else {
|
||||
let senderPeerName = EnginePeer(targetPeer).compactDisplayTitle
|
||||
let targetPeerName = EnginePeer(targetPeer).compactDisplayTitle
|
||||
peerName = EnginePeer(peer).compactDisplayTitle
|
||||
peerIds = [(0, senderId), (1, peerId)]
|
||||
peerIds = [(0, peer.id), (1, targetPeer.id)]
|
||||
let attributes = peerMentionsAttributes(primaryTextColor: primaryTextColor, peerIds: peerIds)
|
||||
attributedString = addAttributesToStringWithRanges(strings.Notification_StarsGift_TransferToChannel(senderPeerName, peerName)._tuple, body: bodyAttributes, argumentAttributes: attributes)
|
||||
attributedString = addAttributesToStringWithRanges(strings.Notification_StarsGift_TransferToChannel(peerName, targetPeerName)._tuple, body: bodyAttributes, argumentAttributes: attributes)
|
||||
}
|
||||
} else {
|
||||
peerName = EnginePeer(peer).compactDisplayTitle
|
||||
|
|
@ -1169,18 +1196,6 @@ public func universalServiceMessageString(presentationData: (PresentationTheme,
|
|||
}
|
||||
}
|
||||
}
|
||||
//TODO:release
|
||||
/*case let .paidMessage(stars):
|
||||
if message.author?.id == accountPeerId {
|
||||
let starsString = strings.Notification_PaidMessage_Stars(Int32(stars))
|
||||
let resultTitleString = strings.Notification_PaidMessageYou(starsString)
|
||||
attributedString = addAttributesToStringWithRanges(resultTitleString._tuple, body: bodyAttributes, argumentAttributes: [0: boldAttributes])
|
||||
} else {
|
||||
let peerName = message.author?.compactDisplayTitle ?? ""
|
||||
let starsString = strings.Notification_PaidMessage_Stars(Int32(stars))
|
||||
let resultTitleString = strings.Notification_PaidMessage(peerName, starsString)
|
||||
attributedString = addAttributesToStringWithRanges(resultTitleString._tuple, body: bodyAttributes, argumentAttributes: [0: boldAttributes, 1: boldAttributes])
|
||||
}*/
|
||||
case .unknown:
|
||||
attributedString = nil
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
import Foundation
|
||||
import UIKit
|
||||
import TelegramCore
|
||||
import TelegramPresentationData
|
||||
|
||||
let walletAddressLength: Int = 48
|
||||
|
|
@ -78,6 +79,20 @@ public func formatTonAmountText(_ value: Int64, dateTimeFormat: PresentationDate
|
|||
return balanceText
|
||||
}
|
||||
|
||||
public func formatStarsAmountText(_ amount: StarsAmount, dateTimeFormat: PresentationDateTimeFormat, showPlus: Bool = false) -> String {
|
||||
var balanceText = presentationStringsFormattedNumber(Int32(amount.value), dateTimeFormat.groupingSeparator)
|
||||
let fraction = Double(amount.nanos) / 10e6
|
||||
if fraction > 0.0 {
|
||||
balanceText.append(dateTimeFormat.decimalSeparator)
|
||||
balanceText.append("\(Int32(fraction))")
|
||||
}
|
||||
if amount.value < 0 {
|
||||
} else if showPlus {
|
||||
balanceText.insert("+", at: balanceText.startIndex)
|
||||
}
|
||||
return balanceText
|
||||
}
|
||||
|
||||
private let invalidAddressCharacters = CharacterSet(charactersIn: "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_=").inverted
|
||||
public func isValidTonAddress(_ address: String, exactLength: Bool = false) -> Bool {
|
||||
if address.count > walletAddressLength || address.rangeOfCharacter(from: invalidAddressCharacters) != nil {
|
||||
|
|
@ -89,10 +104,9 @@ public func isValidTonAddress(_ address: String, exactLength: Bool = false) -> B
|
|||
return true
|
||||
}
|
||||
|
||||
private let amountDelimeterCharacters = CharacterSet(charactersIn: "0123456789-+").inverted
|
||||
public func tonAmountAttributedString(_ string: String, integralFont: UIFont, fractionalFont: UIFont, color: UIColor) -> NSAttributedString {
|
||||
public func tonAmountAttributedString(_ string: String, integralFont: UIFont, fractionalFont: UIFont, color: UIColor, decimalSeparator: String) -> NSAttributedString {
|
||||
let result = NSMutableAttributedString()
|
||||
if let range = string.rangeOfCharacter(from: amountDelimeterCharacters) {
|
||||
if let range = string.range(of: decimalSeparator) {
|
||||
let integralPart = String(string[..<range.lowerBound])
|
||||
let fractionalPart = String(string[range.lowerBound...])
|
||||
result.append(NSAttributedString(string: integralPart, font: integralFont, textColor: color))
|
||||
|
|
|
|||
|
|
@ -353,6 +353,7 @@ swift_library(
|
|||
"//submodules/TelegramUI/Components/Chat/ChatOverscrollControl",
|
||||
"//submodules/TelegramUI/Components/AudioWaveformNode",
|
||||
"//submodules/TelegramUI/Components/Chat/ChatBotInfoItem",
|
||||
"//submodules/TelegramUI/Components/Chat/ChatUserInfoItem",
|
||||
"//submodules/TelegramUI/Components/Chat/ChatInputPanelNode",
|
||||
"//submodules/TelegramUI/Components/Chat/ChatBotStartInputPanelNode",
|
||||
"//submodules/TelegramUI/Components/Chat/ChatButtonKeyboardInputNode",
|
||||
|
|
@ -453,6 +454,7 @@ swift_library(
|
|||
"//submodules/TelegramUI/Components/Stars/StarsTransactionScreen",
|
||||
"//submodules/TelegramUI/Components/Stars/StarsWithdrawalScreen",
|
||||
"//submodules/TelegramUI/Components/Chat/FactCheckAlertController",
|
||||
"//submodules/TelegramUI/Components/Chat/ChatMessagePaymentAlertController",
|
||||
"//submodules/TelegramUI/Components/PeerManagement/OwnershipTransferController",
|
||||
"//submodules/TelegramUI/Components/PeerManagement/OldChannelsController",
|
||||
"//submodules/TelegramUI/Components/Chat/ChatSendStarsScreen",
|
||||
|
|
|
|||
|
|
@ -179,6 +179,25 @@ private final class ScrollContent: CombinedComponent {
|
|||
contentSize.height += spacing
|
||||
|
||||
let premiumConfiguration = PremiumConfiguration.with(appConfiguration: component.context.currentAppConfiguration.with { $0 })
|
||||
|
||||
let respectText: String
|
||||
let adsText: String
|
||||
let infoRawText: String
|
||||
switch component.mode {
|
||||
case .channel:
|
||||
respectText = strings.AdsInfo_Respect_Text
|
||||
adsText = strings.AdsInfo_Ads_Text("\(premiumConfiguration.minChannelRestrictAdsLevel)").string
|
||||
infoRawText = strings.AdsInfo_Launch_Text
|
||||
case .bot:
|
||||
respectText = strings.AdsInfo_Bot_Respect_Text
|
||||
adsText = strings.AdsInfo_Bot_Ads_Text
|
||||
infoRawText = strings.AdsInfo_Bot_Launch_Text
|
||||
case .search:
|
||||
respectText = "Ads like this do not use your personal information and are based on the search query you entered."
|
||||
adsText = strings.AdsInfo_Bot_Ads_Text
|
||||
infoRawText = "Anyone can create an ad to display in search results for any query. Check out the Telegram Ad Platform for details. [Learn More >]()"
|
||||
}
|
||||
|
||||
var items: [AnyComponentWithIdentity<Empty>] = []
|
||||
items.append(
|
||||
AnyComponentWithIdentity(
|
||||
|
|
@ -186,7 +205,7 @@ private final class ScrollContent: CombinedComponent {
|
|||
component: AnyComponent(ParagraphComponent(
|
||||
title: strings.AdsInfo_Respect_Title,
|
||||
titleColor: textColor,
|
||||
text: component.mode == .bot ? strings.AdsInfo_Bot_Respect_Text : strings.AdsInfo_Respect_Text,
|
||||
text: respectText,
|
||||
textColor: secondaryTextColor,
|
||||
accentColor: linkColor,
|
||||
iconName: "Ads/Privacy",
|
||||
|
|
@ -194,27 +213,31 @@ private final class ScrollContent: CombinedComponent {
|
|||
))
|
||||
)
|
||||
)
|
||||
items.append(
|
||||
AnyComponentWithIdentity(
|
||||
id: "split",
|
||||
component: AnyComponent(ParagraphComponent(
|
||||
title: component.mode == .bot ? strings.AdsInfo_Bot_Split_Title : strings.AdsInfo_Split_Title,
|
||||
titleColor: textColor,
|
||||
text: component.mode == .bot ? strings.AdsInfo_Bot_Split_Text : strings.AdsInfo_Split_Text,
|
||||
textColor: secondaryTextColor,
|
||||
accentColor: linkColor,
|
||||
iconName: "Ads/Split",
|
||||
iconColor: linkColor
|
||||
))
|
||||
if case .search = component.mode {
|
||||
|
||||
} else {
|
||||
items.append(
|
||||
AnyComponentWithIdentity(
|
||||
id: "split",
|
||||
component: AnyComponent(ParagraphComponent(
|
||||
title: component.mode == .bot ? strings.AdsInfo_Bot_Split_Title : strings.AdsInfo_Split_Title,
|
||||
titleColor: textColor,
|
||||
text: component.mode == .bot ? strings.AdsInfo_Bot_Split_Text : strings.AdsInfo_Split_Text,
|
||||
textColor: secondaryTextColor,
|
||||
accentColor: linkColor,
|
||||
iconName: "Ads/Split",
|
||||
iconColor: linkColor
|
||||
))
|
||||
)
|
||||
)
|
||||
)
|
||||
}
|
||||
items.append(
|
||||
AnyComponentWithIdentity(
|
||||
id: "ads",
|
||||
component: AnyComponent(ParagraphComponent(
|
||||
title: strings.AdsInfo_Ads_Title,
|
||||
titleColor: textColor,
|
||||
text: component.mode == .bot ? strings.AdsInfo_Bot_Ads_Text : strings.AdsInfo_Ads_Text("\(premiumConfiguration.minChannelRestrictAdsLevel)").string,
|
||||
text: adsText,
|
||||
textColor: secondaryTextColor,
|
||||
accentColor: linkColor,
|
||||
iconName: "Premium/BoostPerk/NoAds",
|
||||
|
|
@ -253,7 +276,7 @@ private final class ScrollContent: CombinedComponent {
|
|||
state.cachedChevronImage = (generateTintedImage(image: UIImage(bundleImageName: "Settings/TextArrowRight"), color: linkColor)!, theme)
|
||||
}
|
||||
|
||||
var infoString = component.mode == .bot ? strings.AdsInfo_Bot_Launch_Text : strings.AdsInfo_Launch_Text
|
||||
var infoString = infoRawText
|
||||
if let spaceRegex {
|
||||
let nsRange = NSRange(infoString.startIndex..., in: infoString)
|
||||
let matches = spaceRegex.matches(in: infoString, options: [], range: nsRange)
|
||||
|
|
|
|||
|
|
@ -5,27 +5,65 @@ import GradientBackground
|
|||
|
||||
public enum AvatarBackground: Equatable {
|
||||
public static let defaultBackgrounds: [AvatarBackground] = [
|
||||
.gradient([0xFF5A7FFF, 0xFF2CA0F2, 0xFF4DFF89, 0xFF6BFCEB]),
|
||||
.gradient([0xFFFF011D, 0xFFFF530D, 0xFFFE64DC, 0xFFFFDC61]),
|
||||
.gradient([0xFFFE64DC, 0xFFFF6847, 0xFFFFDD02, 0xFFFFAE10]),
|
||||
.gradient([0xFF84EC00, 0xFF00B7C2, 0xFF00C217, 0xFFFFE600]),
|
||||
.gradient([0xFF86B0FF, 0xFF35FFCF, 0xFF69FFFF, 0xFF76DEFF]),
|
||||
.gradient([0xFFFAE100, 0xFFFF54EE, 0xFFFC2B78, 0xFFFF52D9]),
|
||||
.gradient([0xFF73A4FF, 0xFF5F55FF, 0xFFFF49F8, 0xFFEC76FF]),
|
||||
.gradient([0xFF5bd1ca, 0xFF538edb], false),
|
||||
.gradient([0xFF61dba8, 0xFF52abd6], false),
|
||||
.gradient([0xFFbdcb57, 0xFF4abe6e], false),
|
||||
.gradient([0xFFd971bf, 0xFF986ce9], false),
|
||||
.gradient([0xFFee8c56, 0xFFec628f], false),
|
||||
.gradient([0xFFf2994f, 0xFFe76667], false),
|
||||
.gradient([0xFFf0b948, 0xFFef7e4b], false),
|
||||
|
||||
.gradient([0xFF94A3B0, 0xFF6C7B87], true),
|
||||
.gradient([0xFF949487, 0xFF707062], true),
|
||||
.gradient([0xFFB09F99, 0xFF8F7E72], true),
|
||||
.gradient([0xFFEBA15B, 0xFFA16730], true),
|
||||
.gradient([0xFFE8B948, 0xFFB87C30], true),
|
||||
.gradient([0xFF5E6F91, 0xFF415275], true),
|
||||
.gradient([0xFF565D61, 0xFF3B4347], true),
|
||||
.gradient([0xFF8F6655, 0xFF68443F], true),
|
||||
.gradient([0xFF1B1B1B, 0xFF000000], true),
|
||||
.gradient([0xFFAE72E3, 0xFF8854B5], true),
|
||||
.gradient([0xFFC269BE, 0xFF8B4384], true),
|
||||
.gradient([0xFF469CD3, 0xFF2E78A8], true),
|
||||
.gradient([0xFF5BCEC5, 0xFF36928E], true),
|
||||
.gradient([0xFF5FD66F, 0xFF319F76], true),
|
||||
.gradient([0xFF66B27A, 0xFF33786D], true),
|
||||
.gradient([0xFF6C9CF4, 0xFF5C6AEC], true),
|
||||
.gradient([0xFFDA76A8, 0xFFAE5891], true),
|
||||
.gradient([0xFFE66473, 0xFFA74559], true),
|
||||
.gradient([0xFFAF75BC, 0xFF895196], true),
|
||||
.gradient([0xFF438CB9, 0xFF2D6283], true),
|
||||
.gradient([0xFF81B6B2, 0xFF4B9A96], true),
|
||||
.gradient([0xFF66B27A, 0xFF33786D], true),
|
||||
.gradient([0xFFCAB560, 0xFF8C803C], true),
|
||||
.gradient([0xFFADB070, 0xFF6B7D54], true),
|
||||
.gradient([0xFFBC7051, 0xFF975547], true),
|
||||
.gradient([0xFFC7835E, 0xFF9E6345], true),
|
||||
.gradient([0xFFE68A3C, 0xFFD45393], true),
|
||||
.gradient([0xFF6BE2F2, 0xFF6675F7], true),
|
||||
.gradient([0xFFC56DF4, 0xFF6073F4], true),
|
||||
.gradient([0xFFEBC92F, 0xFF54B848], true)
|
||||
]
|
||||
|
||||
case gradient([UInt32])
|
||||
case gradient([UInt32], Bool)
|
||||
|
||||
public var colors: [UInt32] {
|
||||
switch self {
|
||||
case let .gradient(colors):
|
||||
case let .gradient(colors, _):
|
||||
return colors
|
||||
}
|
||||
}
|
||||
|
||||
public var isPremium: Bool {
|
||||
switch self {
|
||||
case let .gradient(_, isPremium):
|
||||
return isPremium
|
||||
}
|
||||
}
|
||||
|
||||
public var isLight: Bool {
|
||||
switch self {
|
||||
case let .gradient(colors):
|
||||
case let .gradient(colors, _):
|
||||
if colors.count == 1 {
|
||||
return UIColor(rgb: colors.first!).lightness > 0.99
|
||||
} else if colors.count == 2 {
|
||||
|
|
@ -44,7 +82,7 @@ public enum AvatarBackground: Equatable {
|
|||
|
||||
public func generateImage(size: CGSize) -> UIImage {
|
||||
switch self {
|
||||
case let .gradient(colors):
|
||||
case let .gradient(colors, _):
|
||||
if colors.count == 1 {
|
||||
return generateSingleColorImage(size: size, color: UIColor(rgb: colors.first!))!
|
||||
} else if colors.count == 2 {
|
||||
|
|
|
|||
|
|
@ -19,7 +19,7 @@ swift_library(
|
|||
"//submodules/Components/ViewControllerComponent:ViewControllerComponent",
|
||||
"//submodules/Components/ComponentDisplayAdapters:ComponentDisplayAdapters",
|
||||
"//submodules/Components/MultilineTextComponent:MultilineTextComponent",
|
||||
"//submodules/Components/SolidRoundedButtonComponent",
|
||||
"//submodules/TelegramUI/Components/ButtonComponent",
|
||||
"//submodules/TelegramPresentationData:TelegramPresentationData",
|
||||
"//submodules/AccountContext:AccountContext",
|
||||
"//submodules/AppBundle:AppBundle",
|
||||
|
|
@ -40,6 +40,8 @@ swift_library(
|
|||
"//submodules/TelegramUI/Components/EmojiTextAttachmentView:EmojiTextAttachmentView",
|
||||
"//submodules/TelegramUI/Components/MediaEditor",
|
||||
"//submodules/TelegramUI/Components/AvatarBackground",
|
||||
"//submodules/TelegramUI/Components/LottieComponent",
|
||||
"//submodules/TelegramUI/Components/Premium/PremiumStarComponent",
|
||||
],
|
||||
visibility = [
|
||||
"//visibility:public",
|
||||
|
|
|
|||
|
|
@ -20,11 +20,13 @@ import Markdown
|
|||
import GradientBackground
|
||||
import LegacyComponents
|
||||
import DrawingUI
|
||||
import SolidRoundedButtonComponent
|
||||
import ButtonComponent
|
||||
import AnimationCache
|
||||
import EmojiTextAttachmentView
|
||||
import MediaEditor
|
||||
import AvatarBackground
|
||||
import LottieComponent
|
||||
import UndoUI
|
||||
|
||||
public struct AvatarKeyboardInputData: Equatable {
|
||||
var emoji: EmojiPagerContentComponent
|
||||
|
|
@ -126,7 +128,14 @@ final class AvatarEditorScreenComponent: Component {
|
|||
})
|
||||
}
|
||||
|
||||
self.selectedBackground = .gradient(markup.backgroundColors.map { UInt32(bitPattern: $0) })
|
||||
var isPremium = false
|
||||
let colorsValue = markup.backgroundColors.map { UInt32(bitPattern: $0) }
|
||||
if let defaultColor = AvatarBackground.defaultBackgrounds.first(where: { $0.colors == colorsValue}) {
|
||||
if defaultColor.isPremium {
|
||||
isPremium = true
|
||||
}
|
||||
}
|
||||
self.selectedBackground = .gradient(colorsValue, isPremium)
|
||||
self.previousColor = self.selectedBackground
|
||||
} else {
|
||||
self.selectedBackground = AvatarBackground.defaultBackgrounds.first!
|
||||
|
|
@ -188,6 +197,8 @@ final class AvatarEditorScreenComponent: Component {
|
|||
private let buttonView = ComponentView<Empty>()
|
||||
|
||||
private var component: AvatarEditorScreenComponent?
|
||||
private var environment: EnvironmentType?
|
||||
|
||||
private weak var state: State?
|
||||
|
||||
private var navigationMetrics: (navigationHeight: CGFloat, statusBarHeight: CGFloat)?
|
||||
|
|
@ -783,12 +794,15 @@ final class AvatarEditorScreenComponent: Component {
|
|||
|
||||
private var isExpanded = false
|
||||
|
||||
func update(component: AvatarEditorScreenComponent, availableSize: CGSize, state: State, environment: Environment<ViewControllerComponentContainer.Environment>, transition: ComponentTransition) -> CGSize {
|
||||
func update(component: AvatarEditorScreenComponent, availableSize: CGSize, state: State, environment: Environment<EnvironmentType>, transition: ComponentTransition) -> CGSize {
|
||||
self.component = component
|
||||
|
||||
let environment = environment[EnvironmentType.self].value
|
||||
self.environment = environment
|
||||
self.state = state
|
||||
|
||||
let environment = environment[ViewControllerComponentContainer.Environment.self].value
|
||||
let strings = environment.strings
|
||||
let theme = environment.theme
|
||||
|
||||
let controller = environment.controller
|
||||
self.controller = {
|
||||
|
|
@ -990,6 +1004,7 @@ final class AvatarEditorScreenComponent: Component {
|
|||
transition: transition,
|
||||
component: AnyComponent(BackgroundColorComponent(
|
||||
theme: environment.theme,
|
||||
isPremium: component.context.isPremium,
|
||||
values: AvatarBackground.defaultBackgrounds,
|
||||
selectedValue: state.selectedBackground,
|
||||
customValue: state.customColor,
|
||||
|
|
@ -1037,8 +1052,8 @@ final class AvatarEditorScreenComponent: Component {
|
|||
colors: state.selectedBackground.colors,
|
||||
colorsChanged: { [weak state] colors in
|
||||
if let state {
|
||||
state.customColor = .gradient(colors)
|
||||
state.selectedBackground = .gradient(colors)
|
||||
state.customColor = .gradient(colors, true)
|
||||
state.selectedBackground = .gradient(colors, true)
|
||||
state.updated(transition: .immediate)
|
||||
}
|
||||
},
|
||||
|
|
@ -1268,29 +1283,65 @@ final class AvatarEditorScreenComponent: Component {
|
|||
case .suggest:
|
||||
buttonText = strings.AvatarEditor_SuggestProfilePhoto
|
||||
case .user:
|
||||
buttonText = strings.AvatarEditor_SetProfilePhoto
|
||||
//TODO:localize
|
||||
buttonText = "Set My Photo" //strings.AvatarEditor_SetProfilePhoto
|
||||
case .group, .forum:
|
||||
buttonText = strings.AvatarEditor_SetGroupPhoto
|
||||
case .channel:
|
||||
buttonText = strings.AvatarEditor_SetChannelPhoto
|
||||
}
|
||||
|
||||
var isLocked = false
|
||||
if component.peerType != .suggest, !component.context.isPremium {
|
||||
if state.selectedBackground.isPremium {
|
||||
isLocked = true
|
||||
}
|
||||
if let selectedFile = state.selectedFile {
|
||||
if selectedFile.isSticker {
|
||||
isLocked = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var buttonContents: [AnyComponentWithIdentity<Empty>] = []
|
||||
buttonContents.append(AnyComponentWithIdentity(id: AnyHashable(buttonText), component: AnyComponent(
|
||||
Text(text: buttonText, font: Font.semibold(17.0), color: theme.list.itemCheckColors.foregroundColor)
|
||||
)))
|
||||
if !component.context.isPremium && isLocked {
|
||||
buttonContents.append(AnyComponentWithIdentity(id: AnyHashable(0 as Int), component: AnyComponent(LottieComponent(
|
||||
content: LottieComponent.AppBundleContent(name: "premium_unlock"),
|
||||
color: theme.list.itemCheckColors.foregroundColor,
|
||||
startingPosition: .begin,
|
||||
size: CGSize(width: 30.0, height: 30.0),
|
||||
loop: true
|
||||
))))
|
||||
}
|
||||
|
||||
let buttonSize = self.buttonView.update(
|
||||
transition: transition,
|
||||
component: AnyComponent(
|
||||
SolidRoundedButtonComponent(
|
||||
title: buttonText,
|
||||
theme: SolidRoundedButtonComponent.Theme(theme: environment.theme),
|
||||
fontSize: 17.0,
|
||||
height: 50.0,
|
||||
cornerRadius: 10.0,
|
||||
ButtonComponent(
|
||||
background: ButtonComponent.Background(
|
||||
color: theme.list.itemCheckColors.fillColor,
|
||||
foreground: theme.list.itemCheckColors.foregroundColor,
|
||||
pressedColor: theme.list.itemCheckColors.fillColor.withMultipliedAlpha(0.8)
|
||||
),
|
||||
content: AnyComponentWithIdentity(id: AnyHashable(0 as Int), component: AnyComponent(
|
||||
HStack(buttonContents, spacing: 3.0)
|
||||
)),
|
||||
isEnabled: true,
|
||||
displaysProgress: false,
|
||||
action: { [weak self] in
|
||||
self?.complete()
|
||||
if isLocked {
|
||||
self?.presentPremiumToast()
|
||||
} else {
|
||||
self?.complete()
|
||||
}
|
||||
}
|
||||
)
|
||||
),
|
||||
environment: {},
|
||||
containerSize: CGSize(width: availableSize.width - sideInset * 2.0, height: environment.navigationHeight - environment.statusBarHeight)
|
||||
containerSize: CGSize(width: availableSize.width - sideInset * 2.0, height: 50.0)
|
||||
)
|
||||
if let buttonView = self.buttonView.view {
|
||||
if buttonView.superview == nil {
|
||||
|
|
@ -1298,10 +1349,41 @@ final class AvatarEditorScreenComponent: Component {
|
|||
}
|
||||
transition.setFrame(view: buttonView, frame: CGRect(origin: CGPoint(x: sideInset, y: contentHeight), size: buttonSize))
|
||||
}
|
||||
|
||||
let bottomPanelFrame = CGRect(origin: CGPoint(x: 0.0, y: contentHeight - 4.0), size: CGSize(width: availableSize.width, height: availableSize.height - contentHeight + 4.0))
|
||||
if let controller = environment.controller(), !controller.automaticallyControlPresentationContextLayout {
|
||||
let layout = ContainerViewLayout(
|
||||
size: availableSize,
|
||||
metrics: environment.metrics,
|
||||
deviceMetrics: environment.deviceMetrics,
|
||||
intrinsicInsets: UIEdgeInsets(top: 0.0, left: 0.0, bottom: bottomPanelFrame.height, right: 0.0),
|
||||
safeInsets: UIEdgeInsets(top: 0.0, left: environment.safeInsets.left, bottom: 0.0, right: environment.safeInsets.right),
|
||||
additionalInsets: .zero,
|
||||
statusBarHeight: environment.statusBarHeight,
|
||||
inputHeight: nil,
|
||||
inputHeightIsInteractivellyChanging: false,
|
||||
inVoiceOver: false
|
||||
)
|
||||
controller.presentationContext.containerLayoutUpdated(layout, transition: transition.containedViewLayoutTransition)
|
||||
}
|
||||
|
||||
return availableSize
|
||||
}
|
||||
|
||||
private func presentPremiumToast() {
|
||||
guard let environment = self.environment, let component = self.component, let parentController = environment.controller() else {
|
||||
return
|
||||
}
|
||||
HapticFeedback().impact(.light)
|
||||
|
||||
let controller = premiumAlertController(
|
||||
context: component.context,
|
||||
parentController: parentController,
|
||||
text: environment.strings.AvatarEditor_PremiumNeeded_Background
|
||||
)
|
||||
parentController.present(controller, in: .window(.root))
|
||||
}
|
||||
|
||||
private let queue = Queue()
|
||||
func complete() {
|
||||
guard let state = self.state, let file = state.selectedFile, let controller = self.controller?() else {
|
||||
|
|
@ -1531,6 +1613,9 @@ public final class AvatarEditorScreen: ViewControllerComponentContainer {
|
|||
|
||||
let componentReady = Promise<Bool>()
|
||||
super.init(context: context, component: AvatarEditorScreenComponent(context: context, ready: componentReady, peerType: peerType, markup: markup), navigationBarAppearance: .transparent)
|
||||
|
||||
self.automaticallyControlPresentationContextLayout = false
|
||||
|
||||
self.navigationPresentation = .modal
|
||||
|
||||
self.readyValue.set(componentReady.get() |> timeout(0.3, queue: .mainQueue(), alternate: .single(true)))
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ import AvatarBackground
|
|||
|
||||
final class BackgroundColorComponent: Component {
|
||||
let theme: PresentationTheme
|
||||
let isPremium: Bool
|
||||
let values: [AvatarBackground]
|
||||
let selectedValue: AvatarBackground
|
||||
let customValue: AvatarBackground?
|
||||
|
|
@ -18,6 +19,7 @@ final class BackgroundColorComponent: Component {
|
|||
|
||||
init(
|
||||
theme: PresentationTheme,
|
||||
isPremium: Bool,
|
||||
values: [AvatarBackground],
|
||||
selectedValue: AvatarBackground,
|
||||
customValue: AvatarBackground?,
|
||||
|
|
@ -25,6 +27,7 @@ final class BackgroundColorComponent: Component {
|
|||
openColorPicker: @escaping () -> Void
|
||||
) {
|
||||
self.theme = theme
|
||||
self.isPremium = isPremium
|
||||
self.values = values
|
||||
self.selectedValue = selectedValue
|
||||
self.customValue = customValue
|
||||
|
|
@ -36,6 +39,9 @@ final class BackgroundColorComponent: Component {
|
|||
if lhs.theme !== rhs.theme {
|
||||
return false
|
||||
}
|
||||
if lhs.isPremium != rhs.isPremium {
|
||||
return false
|
||||
}
|
||||
if lhs.values != rhs.values {
|
||||
return false
|
||||
}
|
||||
|
|
@ -48,25 +54,45 @@ final class BackgroundColorComponent: Component {
|
|||
return true
|
||||
}
|
||||
|
||||
class View: UIView {
|
||||
private var views: [Int: ComponentView<Empty>] = [:]
|
||||
class View: UIView, UIScrollViewDelegate {
|
||||
private var views: [AnyHashable: ComponentView<Empty>] = [:]
|
||||
private var scrollView: UIScrollView
|
||||
|
||||
private var component: BackgroundColorComponent?
|
||||
private weak var state: EmptyComponentState?
|
||||
|
||||
override init(frame: CGRect) {
|
||||
super.init(frame: frame)
|
||||
self.scrollView = UIScrollView()
|
||||
self.scrollView.contentInsetAdjustmentBehavior = .never
|
||||
self.scrollView.showsHorizontalScrollIndicator = false
|
||||
self.scrollView.showsVerticalScrollIndicator = false
|
||||
|
||||
super.init(frame: frame)
|
||||
|
||||
self.clipsToBounds = true
|
||||
|
||||
self.scrollView.delegate = self
|
||||
self.addSubview(self.scrollView)
|
||||
|
||||
self.scrollView.disablesInteractiveTransitionGestureRecognizer = true
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
func update(component: BackgroundColorComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: ComponentTransition) -> CGSize {
|
||||
self.component = component
|
||||
self.state = state
|
||||
func scrollViewDidScroll(_ scrollView: UIScrollView) {
|
||||
self.updateScrolling(transition: .immediate)
|
||||
}
|
||||
|
||||
func updateScrolling(transition: ComponentTransition) {
|
||||
guard let component = self.component else {
|
||||
return
|
||||
}
|
||||
|
||||
let itemSize = CGSize(width: 30.0, height: 30.0)
|
||||
let sideInset: CGFloat = 12.0
|
||||
let spacing: CGFloat = 13.0
|
||||
|
||||
var values: [(AvatarBackground?, Bool)] = component.values.map { ($0, false) }
|
||||
if let customValue = component.customValue {
|
||||
|
|
@ -75,50 +101,97 @@ final class BackgroundColorComponent: Component {
|
|||
values.append((nil, true))
|
||||
}
|
||||
|
||||
let itemSize = CGSize(width: 30.0, height: 30.0)
|
||||
let sideInset: CGFloat = 12.0
|
||||
let height: CGFloat = 50.0
|
||||
let delta = floorToScreenPixels((availableSize.width - sideInset * 2.0 - CGFloat(values.count) * itemSize.width) / CGFloat(values.count - 1))
|
||||
let visibleBounds = self.scrollView.bounds.insetBy(dx: 0.0, dy: -10.0)
|
||||
|
||||
var validIds: [AnyHashable] = []
|
||||
for i in 0 ..< values.count {
|
||||
let view: ComponentView<Empty>
|
||||
if let current = self.views[i] {
|
||||
view = current
|
||||
} else {
|
||||
view = ComponentView<Empty>()
|
||||
self.views[i] = view
|
||||
let position: CGFloat = sideInset + (spacing + itemSize.width) * CGFloat(i)
|
||||
let itemFrame = CGRect(origin: CGPoint(x: position, y: 10.0), size: itemSize)
|
||||
var isVisible = false
|
||||
if visibleBounds.intersects(itemFrame) {
|
||||
isVisible = true
|
||||
}
|
||||
|
||||
let itemSize = view.update(
|
||||
transition: transition,
|
||||
component: AnyComponent(
|
||||
BackgroundSwatchComponent(
|
||||
theme: component.theme,
|
||||
background: values[i].0,
|
||||
isCustom: values[i].1,
|
||||
isSelected: component.selectedValue == values[i].0,
|
||||
action: {
|
||||
if let value = values[i].0, component.selectedValue != value {
|
||||
component.updateValue(value)
|
||||
} else if values[i].1 {
|
||||
component.openColorPicker()
|
||||
}
|
||||
}
|
||||
)
|
||||
),
|
||||
environment: {},
|
||||
containerSize: itemSize
|
||||
)
|
||||
if let itemView = view.view {
|
||||
if itemView.superview == nil {
|
||||
self.addSubview(itemView)
|
||||
if isVisible {
|
||||
let itemId = AnyHashable(i)
|
||||
validIds.append(itemId)
|
||||
|
||||
let view: ComponentView<Empty>
|
||||
if let current = self.views[itemId] {
|
||||
view = current
|
||||
} else {
|
||||
view = ComponentView<Empty>()
|
||||
self.views[itemId] = view
|
||||
}
|
||||
|
||||
let position: CGFloat = sideInset + (delta + itemSize.width) * CGFloat(i)
|
||||
transition.setFrame(view: itemView, frame: CGRect(origin: CGPoint(x: position, y: 10.0), size: itemSize))
|
||||
let _ = view.update(
|
||||
transition: transition,
|
||||
component: AnyComponent(
|
||||
BackgroundSwatchComponent(
|
||||
theme: component.theme,
|
||||
background: values[i].0,
|
||||
isCustom: values[i].1,
|
||||
isSelected: component.selectedValue == values[i].0,
|
||||
isLocked: i >= 7 && !values[i].1,
|
||||
action: {
|
||||
if let value = values[i].0, component.selectedValue != value {
|
||||
component.updateValue(value)
|
||||
} else if values[i].1 {
|
||||
component.openColorPicker()
|
||||
}
|
||||
}
|
||||
)
|
||||
),
|
||||
environment: {},
|
||||
containerSize: itemSize
|
||||
)
|
||||
if let itemView = view.view {
|
||||
if itemView.superview == nil {
|
||||
self.scrollView.addSubview(itemView)
|
||||
}
|
||||
transition.setFrame(view: itemView, frame: itemFrame)
|
||||
}
|
||||
}
|
||||
}
|
||||
return CGSize(width: availableSize.width, height: height)
|
||||
|
||||
var removeIds: [AnyHashable] = []
|
||||
for (id, item) in self.views {
|
||||
if !validIds.contains(id) {
|
||||
removeIds.append(id)
|
||||
if let itemView = item.view {
|
||||
itemView.removeFromSuperview()
|
||||
}
|
||||
}
|
||||
}
|
||||
for id in removeIds {
|
||||
self.views.removeValue(forKey: id)
|
||||
}
|
||||
}
|
||||
|
||||
func update(component: BackgroundColorComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: ComponentTransition) -> CGSize {
|
||||
self.component = component
|
||||
self.state = state
|
||||
|
||||
let height: CGFloat = 50.0
|
||||
let size = CGSize(width: availableSize.width, height: height)
|
||||
let scrollFrame = CGRect(origin: .zero, size: size)
|
||||
|
||||
let itemSize = CGSize(width: 30.0, height: 30.0)
|
||||
let sideInset: CGFloat = 12.0
|
||||
let spacing: CGFloat = 13.0
|
||||
|
||||
let count = component.values.count + 1
|
||||
let contentSize = CGSize(width: sideInset * 2.0 + CGFloat(count) * itemSize.width + CGFloat(count - 1) * spacing, height: height)
|
||||
|
||||
if self.scrollView.frame != scrollFrame {
|
||||
self.scrollView.frame = scrollFrame
|
||||
}
|
||||
if self.scrollView.contentSize != contentSize {
|
||||
self.scrollView.contentSize = contentSize
|
||||
}
|
||||
|
||||
self.updateScrolling(transition: .immediate)
|
||||
|
||||
return size
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -164,11 +237,22 @@ private func generateMoreIcon() -> UIImage? {
|
|||
})
|
||||
}
|
||||
|
||||
private var lockIcon: UIImage? = {
|
||||
let icon = generateTintedImage(image: UIImage(bundleImageName: "Chat/Stickers/SmallLock"), color: .white)
|
||||
return generateImage(CGSize(width: 30.0, height: 30.0), contextGenerator: { size, context in
|
||||
context.clear(CGRect(origin: .zero, size: size))
|
||||
if let icon, let cgImage = icon.cgImage {
|
||||
context.draw(cgImage, in: CGRect(origin: CGPoint(x: floorToScreenPixels((size.width - icon.size.width) / 2.0), y: floorToScreenPixels((size.height - icon.size.height) / 2.0)), size: icon.size), byTiling: false)
|
||||
}
|
||||
})
|
||||
}()
|
||||
|
||||
final class BackgroundSwatchComponent: Component {
|
||||
let theme: PresentationTheme
|
||||
let background: AvatarBackground?
|
||||
let isCustom: Bool
|
||||
let isSelected: Bool
|
||||
let isLocked: Bool
|
||||
let action: () -> Void
|
||||
|
||||
init(
|
||||
|
|
@ -176,17 +260,19 @@ final class BackgroundSwatchComponent: Component {
|
|||
background: AvatarBackground?,
|
||||
isCustom: Bool,
|
||||
isSelected: Bool,
|
||||
isLocked: Bool,
|
||||
action: @escaping () -> Void
|
||||
) {
|
||||
self.theme = theme
|
||||
self.background = background
|
||||
self.isCustom = isCustom
|
||||
self.isSelected = isSelected
|
||||
self.isLocked = isLocked
|
||||
self.action = action
|
||||
}
|
||||
|
||||
static func == (lhs: BackgroundSwatchComponent, rhs: BackgroundSwatchComponent) -> Bool {
|
||||
return lhs.theme === rhs.theme && lhs.background == rhs.background && lhs.isCustom == rhs.isCustom && lhs.isSelected == rhs.isSelected
|
||||
return lhs.theme === rhs.theme && lhs.background == rhs.background && lhs.isCustom == rhs.isCustom && lhs.isSelected == rhs.isSelected && lhs.isLocked == rhs.isLocked
|
||||
}
|
||||
|
||||
final class View: UIButton {
|
||||
|
|
@ -283,6 +369,8 @@ final class BackgroundSwatchComponent: Component {
|
|||
self.iconLayer.contents = generateAddIcon(color: component.theme.list.itemAccentColor)?.cgImage
|
||||
}
|
||||
}
|
||||
} else if component.isLocked {
|
||||
self.iconLayer.contents = lockIcon?.cgImage
|
||||
} else {
|
||||
self.iconLayer.contents = nil
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1893,7 +1893,7 @@ public final class ChatEmptyNode: ASDisplayNode {
|
|||
node.layer.animateScale(from: 0.0, to: 1.0, duration: duration, timingFunction: curve.timingFunction)
|
||||
}
|
||||
}
|
||||
self.isUserInteractionEnabled = [.peerNearby, .greeting, .premiumRequired, .cloud].contains(contentType)
|
||||
self.isUserInteractionEnabled = [.peerNearby, .greeting, .premiumRequired, .starsRequired, .cloud].contains(contentType)
|
||||
|
||||
let displayRect = CGRect(origin: CGPoint(x: 0.0, y: insets.top), size: CGSize(width: size.width, height: size.height - insets.top - insets.bottom))
|
||||
|
||||
|
|
|
|||
|
|
@ -41,12 +41,17 @@ public struct ChatMessageEntryAttributes: Equatable {
|
|||
}
|
||||
}
|
||||
|
||||
public enum ChatInfoData: Equatable {
|
||||
case botInfo(title: String, text: String, photo: TelegramMediaImage?, video: TelegramMediaFile?)
|
||||
case userInfo(title: String, registrationDate: String?, phoneCountry: String?, locationCountry: String?, groupsInCommon: [EnginePeer])
|
||||
}
|
||||
|
||||
public enum ChatHistoryEntry: Identifiable, Comparable {
|
||||
case MessageEntry(Message, ChatPresentationData, Bool, MessageHistoryEntryLocation?, ChatHistoryMessageSelection, ChatMessageEntryAttributes)
|
||||
case MessageGroupEntry(Int64, [(Message, Bool, ChatHistoryMessageSelection, ChatMessageEntryAttributes, MessageHistoryEntryLocation?)], ChatPresentationData)
|
||||
case UnreadEntry(MessageIndex, ChatPresentationData)
|
||||
case ReplyCountEntry(MessageIndex, Bool, Int, ChatPresentationData)
|
||||
case ChatInfoEntry(String, String, TelegramMediaImage?, TelegramMediaFile?, ChatPresentationData)
|
||||
case ChatInfoEntry(ChatInfoData, ChatPresentationData)
|
||||
case SearchEntry(PresentationTheme, PresentationStrings)
|
||||
|
||||
public var stableId: UInt64 {
|
||||
|
|
@ -272,8 +277,8 @@ public enum ChatHistoryEntry: Identifiable, Comparable {
|
|||
} else {
|
||||
return false
|
||||
}
|
||||
case let .ChatInfoEntry(lhsTitle, lhsText, lhsPhoto, lhsVideo, lhsPresentationData):
|
||||
if case let .ChatInfoEntry(rhsTitle, rhsText, rhsPhoto, rhsVideo, rhsPresentationData) = rhs, lhsTitle == rhsTitle, lhsText == rhsText, lhsPhoto == rhsPhoto, lhsVideo == rhsVideo, lhsPresentationData === rhsPresentationData {
|
||||
case let .ChatInfoEntry(lhsData, lhsPresentationData):
|
||||
if case let .ChatInfoEntry(rhsData, rhsPresentationData) = rhs, lhsData == rhsData, lhsPresentationData === rhsPresentationData {
|
||||
return true
|
||||
} else {
|
||||
return false
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -1050,7 +1050,7 @@ public final class ChatInputTextView: ChatInputTextViewImpl, UITextViewDelegate,
|
|||
}
|
||||
|
||||
public var toggleQuoteCollapse: ((NSRange) -> Void)?
|
||||
|
||||
|
||||
private let displayInternal: ChatInputTextInternal
|
||||
private let measureInternal: ChatInputTextInternal
|
||||
|
||||
|
|
@ -1111,6 +1111,10 @@ public final class ChatInputTextView: ChatInputTextViewImpl, UITextViewDelegate,
|
|||
|
||||
self.delegate = self
|
||||
|
||||
if #available(iOS 18.0, *) {
|
||||
self.supportsAdaptiveImageGlyph = false
|
||||
}
|
||||
|
||||
self.displayInternal.updateDisplayElements = { [weak self] in
|
||||
self?.updateTextElements()
|
||||
}
|
||||
|
|
@ -1217,7 +1221,7 @@ public final class ChatInputTextView: ChatInputTextViewImpl, UITextViewDelegate,
|
|||
|
||||
@objc public func textViewDidChange(_ textView: UITextView) {
|
||||
self.selectionChangedForEditedText = true
|
||||
|
||||
|
||||
self.updateTextContainerInset()
|
||||
|
||||
self.customDelegate?.chatInputTextNodeDidUpdateText()
|
||||
|
|
|
|||
|
|
@ -22,8 +22,8 @@ import TextNodeWithEntities
|
|||
import ChatMessageBubbleContentNode
|
||||
import ChatMessageItemCommon
|
||||
|
||||
private func attributedServiceMessageString(theme: ChatPresentationThemeData, strings: PresentationStrings, nameDisplayOrder: PresentationPersonNameOrder, dateTimeFormat: PresentationDateTimeFormat, message: Message, accountPeerId: PeerId, forForumOverview: Bool) -> NSAttributedString? {
|
||||
return universalServiceMessageString(presentationData: (theme.theme, theme.wallpaper), strings: strings, nameDisplayOrder: nameDisplayOrder, dateTimeFormat: dateTimeFormat, message: EngineMessage(message), accountPeerId: accountPeerId, forChatList: false, forForumOverview: forForumOverview)
|
||||
private func attributedServiceMessageString(theme: ChatPresentationThemeData, strings: PresentationStrings, nameDisplayOrder: PresentationPersonNameOrder, dateTimeFormat: PresentationDateTimeFormat, message: Message, messageCount: Int? = nil, accountPeerId: PeerId, forForumOverview: Bool) -> NSAttributedString? {
|
||||
return universalServiceMessageString(presentationData: (theme.theme, theme.wallpaper), strings: strings, nameDisplayOrder: nameDisplayOrder, dateTimeFormat: dateTimeFormat, message: EngineMessage(message), messageCount: messageCount, accountPeerId: accountPeerId, forChatList: false, forForumOverview: forForumOverview)
|
||||
}
|
||||
|
||||
public class ChatMessageActionBubbleContentNode: ChatMessageBubbleContentNode {
|
||||
|
|
@ -160,7 +160,12 @@ public class ChatMessageActionBubbleContentNode: ChatMessageBubbleContentNode {
|
|||
let cachedMaskBackgroundImage = self.cachedMaskBackgroundImage
|
||||
|
||||
return { item, layoutConstants, _, _, _, _ in
|
||||
let contentProperties = ChatMessageBubbleContentProperties(hidesSimpleAuthorHeader: true, headerSpacing: 0.0, hidesBackground: .always, forceFullCorners: false, forceAlignment: .center)
|
||||
var isDetached = false
|
||||
if let _ = item.message.paidStarsAttribute {
|
||||
isDetached = true
|
||||
}
|
||||
|
||||
let contentProperties = ChatMessageBubbleContentProperties(hidesSimpleAuthorHeader: true, headerSpacing: 0.0, hidesBackground: .always, forceFullCorners: false, forceAlignment: .center, isDetached: isDetached)
|
||||
|
||||
let backgroundImage = PresentationResourcesChat.chatActionPhotoBackgroundImage(item.presentationData.theme.theme, wallpaper: !item.presentationData.theme.wallpaper.isEmpty)
|
||||
|
||||
|
|
@ -170,7 +175,12 @@ public class ChatMessageActionBubbleContentNode: ChatMessageBubbleContentNode {
|
|||
forForumOverview = true
|
||||
}
|
||||
|
||||
let attributedString = attributedServiceMessageString(theme: item.presentationData.theme, strings: item.presentationData.strings, nameDisplayOrder: item.presentationData.nameDisplayOrder, dateTimeFormat: item.presentationData.dateTimeFormat, message: item.message, accountPeerId: item.context.account.peerId, forForumOverview: forForumOverview)
|
||||
var messageCount: Int = 1
|
||||
if case let .group(messages) = item.content {
|
||||
messageCount = messages.count
|
||||
}
|
||||
|
||||
let attributedString = attributedServiceMessageString(theme: item.presentationData.theme, strings: item.presentationData.strings, nameDisplayOrder: item.presentationData.nameDisplayOrder, dateTimeFormat: item.presentationData.dateTimeFormat, message: item.message, messageCount: messageCount, accountPeerId: item.context.account.peerId, forForumOverview: forForumOverview)
|
||||
|
||||
var image: TelegramMediaImage?
|
||||
var story: TelegramMediaStory?
|
||||
|
|
|
|||
|
|
@ -62,7 +62,6 @@ private extension UIBezierPath {
|
|||
}
|
||||
|
||||
private final class ChatMessageActionButtonNode: ASDisplayNode {
|
||||
//private let backgroundBlurNode: NavigationBackgroundNode
|
||||
private var backgroundBlurView: PortalView?
|
||||
|
||||
private var titleNode: TextNode?
|
||||
|
|
@ -84,15 +83,11 @@ private final class ChatMessageActionButtonNode: ASDisplayNode {
|
|||
private let accessibilityArea: AccessibilityAreaNode
|
||||
|
||||
override init() {
|
||||
//self.backgroundBlurNode = NavigationBackgroundNode(color: .clear)
|
||||
//self.backgroundBlurNode.isUserInteractionEnabled = false
|
||||
|
||||
self.accessibilityArea = AccessibilityAreaNode()
|
||||
self.accessibilityArea.accessibilityTraits = .button
|
||||
|
||||
super.init()
|
||||
|
||||
//self.addSubnode(self.backgroundBlurNode)
|
||||
self.addSubnode(self.accessibilityArea)
|
||||
|
||||
self.accessibilityArea.activate = { [weak self] in
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -21,6 +21,7 @@ swift_library(
|
|||
"//submodules/ChatMessageBackground",
|
||||
"//submodules/TelegramUI/Components/ChatControllerInteraction",
|
||||
"//submodules/TelegramUI/Components/Chat/ChatHistoryEntry",
|
||||
"//submodules/TelegramUI/Components/Chat/ChatMessageItem",
|
||||
"//submodules/TelegramUI/Components/Chat/ChatMessageItemCommon",
|
||||
],
|
||||
visibility = [
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ import AccountContext
|
|||
import ChatMessageBackground
|
||||
import ChatControllerInteraction
|
||||
import ChatHistoryEntry
|
||||
import ChatMessageItem
|
||||
import ChatMessageItemCommon
|
||||
import SwiftSignalKit
|
||||
|
||||
|
|
@ -180,6 +181,7 @@ public final class ChatMessageBubbleContentItem {
|
|||
public let controllerInteraction: ChatControllerInteraction
|
||||
public let message: Message
|
||||
public let topMessage: Message
|
||||
public let content: ChatMessageItemContent
|
||||
public let read: Bool
|
||||
public let chatLocation: ChatLocation
|
||||
public let presentationData: ChatPresentationData
|
||||
|
|
@ -188,11 +190,12 @@ public final class ChatMessageBubbleContentItem {
|
|||
public let isItemPinned: Bool
|
||||
public let isItemEdited: Bool
|
||||
|
||||
public init(context: AccountContext, controllerInteraction: ChatControllerInteraction, message: Message, topMessage: Message, read: Bool, chatLocation: ChatLocation, presentationData: ChatPresentationData, associatedData: ChatMessageItemAssociatedData, attributes: ChatMessageEntryAttributes, isItemPinned: Bool, isItemEdited: Bool) {
|
||||
public init(context: AccountContext, controllerInteraction: ChatControllerInteraction, message: Message, topMessage: Message, content: ChatMessageItemContent, read: Bool, chatLocation: ChatLocation, presentationData: ChatPresentationData, associatedData: ChatMessageItemAssociatedData, attributes: ChatMessageEntryAttributes, isItemPinned: Bool, isItemEdited: Bool) {
|
||||
self.context = context
|
||||
self.controllerInteraction = controllerInteraction
|
||||
self.message = message
|
||||
self.topMessage = topMessage
|
||||
self.content = content
|
||||
self.read = read
|
||||
self.chatLocation = chatLocation
|
||||
self.presentationData = presentationData
|
||||
|
|
|
|||
|
|
@ -121,13 +121,18 @@ private func contentNodeMessagesAndClassesForItem(_ item: ChatMessageItem) -> ([
|
|||
let hideAllAdditionalInfo = item.presentationData.isPreview
|
||||
|
||||
var hasSeparateCommentsButton = false
|
||||
|
||||
|
||||
var addedPriceInfo = false
|
||||
|
||||
outer: for (message, itemAttributes) in item.content {
|
||||
for attribute in message.attributes {
|
||||
if let attribute = attribute as? RestrictedContentMessageAttribute, attribute.platformText(platform: "ios", contentSettings: item.context.currentContentSettings.with { $0 }) != nil {
|
||||
result.append((message, ChatMessageRestrictedBubbleContentNode.self, itemAttributes, BubbleItemAttributes(isAttachment: false, neighborType: .text, neighborSpacing: .default)))
|
||||
needReactions = false
|
||||
break outer
|
||||
} else if let _ = attribute as? PaidStarsMessageAttribute, !addedPriceInfo {
|
||||
result.append((message, ChatMessageActionBubbleContentNode.self, itemAttributes, BubbleItemAttributes(isAttachment: false, neighborType: .text, neighborSpacing: .default)))
|
||||
addedPriceInfo = true
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -273,7 +273,7 @@ public class ChatMessageInstantVideoBubbleContentNode: ChatMessageBubbleContentN
|
|||
let leftInset: CGFloat = 0.0
|
||||
let rightInset: CGFloat = 0.0
|
||||
|
||||
let (videoLayout, videoApply) = interactiveVideoLayout(ChatMessageBubbleContentItem(context: item.context, controllerInteraction: item.controllerInteraction, message: item.message, topMessage: item.message, read: item.read, chatLocation: item.chatLocation, presentationData: item.presentationData, associatedData: item.associatedData, attributes: item.attributes, isItemPinned: item.message.tags.contains(.pinned) && !isReplyThread, isItemEdited: false), constrainedSize.width - leftInset - rightInset - avatarInset, displaySize, maximumDisplaySize, isPlaying ? 1.0 : 0.0, .free, automaticDownload, avatarInset)
|
||||
let (videoLayout, videoApply) = interactiveVideoLayout(ChatMessageBubbleContentItem(context: item.context, controllerInteraction: item.controllerInteraction, message: item.message, topMessage: item.message, content: item.content, read: item.read, chatLocation: item.chatLocation, presentationData: item.presentationData, associatedData: item.associatedData, attributes: item.attributes, isItemPinned: item.message.tags.contains(.pinned) && !isReplyThread, isItemEdited: false), constrainedSize.width - leftInset - rightInset - avatarInset, displaySize, maximumDisplaySize, isPlaying ? 1.0 : 0.0, .free, automaticDownload, avatarInset)
|
||||
|
||||
let videoFrame = CGRect(origin: CGPoint(x: 1.0, y: 1.0), size: videoLayout.contentSize)
|
||||
|
||||
|
|
|
|||
|
|
@ -423,7 +423,7 @@ public class ChatMessageInstantVideoItemNode: ChatMessageItemView, ASGestureReco
|
|||
isReplyThread = true
|
||||
}
|
||||
|
||||
let (videoLayout, videoApply) = makeVideoLayout(ChatMessageBubbleContentItem(context: item.context, controllerInteraction: item.controllerInteraction, message: item.message, topMessage: item.content.firstMessage, read: item.read, chatLocation: item.chatLocation, presentationData: item.presentationData, associatedData: item.associatedData, attributes: item.content.firstMessageAttributes, isItemPinned: item.message.tags.contains(.pinned) && !isReplyThread, isItemEdited: false), params.width - params.leftInset - params.rightInset - avatarInset, displaySize, maximumDisplaySize, isPlaying ? 1.0 : 0.0, .free, automaticDownload, 0.0)
|
||||
let (videoLayout, videoApply) = makeVideoLayout(ChatMessageBubbleContentItem(context: item.context, controllerInteraction: item.controllerInteraction, message: item.message, topMessage: item.content.firstMessage, content: item.content, read: item.read, chatLocation: item.chatLocation, presentationData: item.presentationData, associatedData: item.associatedData, attributes: item.content.firstMessageAttributes, isItemPinned: item.message.tags.contains(.pinned) && !isReplyThread, isItemEdited: false), params.width - params.leftInset - params.rightInset - avatarInset, displaySize, maximumDisplaySize, isPlaying ? 1.0 : 0.0, .free, automaticDownload, 0.0)
|
||||
|
||||
let videoFrame = CGRect(origin: CGPoint(x: (incoming ? (params.leftInset + layoutConstants.bubble.edgeInset + effectiveAvatarInset + layoutConstants.bubble.contentInsets.left) : (params.width - params.rightInset - videoLayout.contentSize.width - layoutConstants.bubble.edgeInset - layoutConstants.bubble.contentInsets.left - deliveryFailedInset)), y: 0.0), size: videoLayout.contentSize)
|
||||
|
||||
|
|
@ -1373,7 +1373,7 @@ public class ChatMessageInstantVideoItemNode: ChatMessageItemView, ASGestureReco
|
|||
effectiveAvatarInset *= (1.0 - scaleProgress)
|
||||
displaySize = CGSize(width: initialSize.width + (targetSize.width - initialSize.width) * animationProgress, height: initialSize.height + (targetSize.height - initialSize.height) * animationProgress)
|
||||
|
||||
let (videoLayout, videoApply) = makeVideoLayout(ChatMessageBubbleContentItem(context: item.context, controllerInteraction: item.controllerInteraction, message: item.message, topMessage: item.message, read: item.read, chatLocation: item.chatLocation, presentationData: item.presentationData, associatedData: item.associatedData, attributes: item.content.firstMessageAttributes, isItemPinned: item.message.tags.contains(.pinned) && !isReplyThread, isItemEdited: false), params.width - params.leftInset - params.rightInset - avatarInset, displaySize, maximumDisplaySize, scaleProgress, .free, self.appliedAutomaticDownload, 0.0)
|
||||
let (videoLayout, videoApply) = makeVideoLayout(ChatMessageBubbleContentItem(context: item.context, controllerInteraction: item.controllerInteraction, message: item.message, topMessage: item.message, content: item.content, read: item.read, chatLocation: item.chatLocation, presentationData: item.presentationData, associatedData: item.associatedData, attributes: item.content.firstMessageAttributes, isItemPinned: item.message.tags.contains(.pinned) && !isReplyThread, isItemEdited: false), params.width - params.leftInset - params.rightInset - avatarInset, displaySize, maximumDisplaySize, scaleProgress, .free, self.appliedAutomaticDownload, 0.0)
|
||||
|
||||
let availableContentWidth = params.width - params.leftInset - params.rightInset - layoutConstants.bubble.edgeInset * 2.0 - avatarInset - layoutConstants.bubble.contentInsets.left
|
||||
let videoFrame = CGRect(origin: CGPoint(x: (incoming ? (params.leftInset + layoutConstants.bubble.edgeInset + effectiveAvatarInset + layoutConstants.bubble.contentInsets.left) : (params.width - params.rightInset - videoLayout.contentSize.width - layoutConstants.bubble.edgeInset - layoutConstants.bubble.contentInsets.left - deliveryFailedInset)), y: 0.0), size: videoLayout.contentSize)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -0,0 +1,33 @@
|
|||
load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library")
|
||||
|
||||
swift_library(
|
||||
name = "ChatMessagePaymentAlertController",
|
||||
module_name = "ChatMessagePaymentAlertController",
|
||||
srcs = glob([
|
||||
"Sources/**/*.swift",
|
||||
]),
|
||||
copts = [
|
||||
"-warnings-as-errors",
|
||||
],
|
||||
deps = [
|
||||
"//submodules/SSignalKit/SwiftSignalKit:SwiftSignalKit",
|
||||
"//submodules/AsyncDisplayKit:AsyncDisplayKit",
|
||||
"//submodules/Display:Display",
|
||||
"//submodules/Postbox:Postbox",
|
||||
"//submodules/TelegramCore:TelegramCore",
|
||||
"//submodules/AccountContext:AccountContext",
|
||||
"//submodules/TelegramPresentationData:TelegramPresentationData",
|
||||
"//submodules/ComponentFlow",
|
||||
"//submodules/Components/MultilineTextComponent",
|
||||
"//submodules/Components/ComponentDisplayAdapters",
|
||||
"//submodules/TextFormat",
|
||||
"//submodules/AvatarNode",
|
||||
"//submodules/CheckNode",
|
||||
"//submodules/TelegramUIPreferences",
|
||||
"//submodules/TelegramUI/Components/Stars/StarsBalanceOverlayComponent",
|
||||
"//submodules/Markdown",
|
||||
],
|
||||
visibility = [
|
||||
"//visibility:public",
|
||||
],
|
||||
)
|
||||
|
|
@ -55,7 +55,7 @@ private final class ChatMessagePaymentAlertContentNode: AlertContentNode, ASGest
|
|||
|
||||
var openTerms: () -> Void = {}
|
||||
|
||||
init(context: AccountContext, theme: AlertControllerTheme, ptheme: PresentationTheme, strings: PresentationStrings, title: String, text: String, optionText: String?, actions: [TextAlertAction], alignment: TextAlertContentActionLayout) {
|
||||
init(theme: AlertControllerTheme, ptheme: PresentationTheme, strings: PresentationStrings, title: String, text: String, optionText: String?, actions: [TextAlertAction], alignment: TextAlertContentActionLayout) {
|
||||
self.strings = strings
|
||||
self.title = title
|
||||
self.text = text
|
||||
|
|
@ -318,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()
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -523,6 +523,7 @@ public final class ChatSendGroupMediaMessageContextPreview: UIView, ChatSendMess
|
|||
controllerInteraction: controllerInteraction,
|
||||
message: message,
|
||||
topMessage: message,
|
||||
content: .message(message: message, read: true, selection: .none, attributes: entryAttributes, location: nil),
|
||||
read: true,
|
||||
chatLocation: .peer(id: self.context.account.peerId),
|
||||
presentationData: chatPresentationData,
|
||||
|
|
|
|||
34
submodules/TelegramUI/Components/Chat/ChatUserInfoItem/BUILD
Normal file
34
submodules/TelegramUI/Components/Chat/ChatUserInfoItem/BUILD
Normal file
|
|
@ -0,0 +1,34 @@
|
|||
load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library")
|
||||
|
||||
swift_library(
|
||||
name = "ChatUserInfoItem",
|
||||
module_name = "ChatUserInfoItem",
|
||||
srcs = glob([
|
||||
"Sources/**/*.swift",
|
||||
]),
|
||||
copts = [
|
||||
"-warnings-as-errors",
|
||||
],
|
||||
deps = [
|
||||
"//submodules/Display",
|
||||
"//submodules/AsyncDisplayKit",
|
||||
"//submodules/SSignalKit/SwiftSignalKit",
|
||||
"//submodules/Postbox",
|
||||
"//submodules/TelegramCore",
|
||||
"//submodules/TelegramPresentationData",
|
||||
"//submodules/TextFormat",
|
||||
"//submodules/UrlEscaping",
|
||||
"//submodules/PhotoResources",
|
||||
"//submodules/AccountContext",
|
||||
"//submodules/MediaPlayer:UniversalMediaPlayer",
|
||||
"//submodules/TelegramUniversalVideoContent",
|
||||
"//submodules/WallpaperBackgroundNode",
|
||||
"//submodules/TelegramUI/Components/ChatControllerInteraction",
|
||||
"//submodules/TelegramUI/Components/Chat/ChatMessageBubbleContentNode",
|
||||
"//submodules/CountrySelectionUI",
|
||||
"//submodules/TelegramStringFormatting",
|
||||
],
|
||||
visibility = [
|
||||
"//visibility:public",
|
||||
],
|
||||
)
|
||||
|
|
@ -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
|
||||
// }
|
||||
// }
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 })
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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) },
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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: [:])
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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] = []
|
||||
|
||||
|
|
|
|||
|
|
@ -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] = []
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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: {},
|
||||
|
|
|
|||
|
|
@ -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
Loading…
Add table
Add a link
Reference in a new issue