Merge branch 'master' of gitlab.com:peter-iakovlev/telegram-ios

This commit is contained in:
Ilya Laktyushin 2025-06-20 23:01:46 +02:00
commit 88d91ee774
45 changed files with 999 additions and 279 deletions

View File

@ -1033,8 +1033,8 @@ public enum StarsWithdrawalScreenSubject {
case withdraw(completion: (Int64) -> Void)
case enterAmount(current: StarsAmount, minValue: StarsAmount, fractionAfterCommission: Int, kind: PaidMessageKind, completion: (Int64) -> Void)
case postSuggestion(channel: EnginePeer, current: StarsAmount, timestamp: Int32?, completion: (Int64, Int32?) -> Void)
case postSuggestionModification(current: StarsAmount, timestamp: Int32?, completion: (Int64, Int32?) -> Void)
case postSuggestion(channel: EnginePeer, currency: TelegramCurrency, current: StarsAmount, timestamp: Int32?, completion: (TelegramCurrency, Int64, Int32?) -> Void)
case postSuggestionModification(currency: TelegramCurrency, current: StarsAmount, timestamp: Int32?, completion: (TelegramCurrency, Int64, Int32?) -> Void)
}
public protocol SharedAccountContext: AnyObject {

View File

@ -1265,7 +1265,7 @@ final class AttachmentPanel: ASDisplayNode, ASScrollViewDelegate {
}, addDoNotTranslateLanguage: { _ in
}, hideTranslationPanel: {
}, openPremiumGift: {
}, openSuggestPost: {
}, openSuggestPost: { _ in
}, openPremiumRequiredForMessaging: {
}, openStarsPurchase: { _ in
}, openMessagePayment: {

View File

@ -70,7 +70,7 @@ public final class BrowserBookmarksScreen: ViewController {
return false
}, sendBotContextResultAsGif: { _, _, _, _, _, _ in
return false
}, requestMessageActionCallback: { _, _, _, _ in
}, requestMessageActionCallback: { _, _, _, _, _ in
}, requestMessageActionUrlAuth: { _, _ in
}, activateSwitchInline: { _, _, _ in
}, openUrl: { [weak controller] url in

View File

@ -497,11 +497,13 @@ public final class ChatInterfaceState: Codable, Equatable {
public struct PostSuggestionState: Codable, Equatable {
public var editingOriginalMessageId: MessageId?
public var currency: TelegramCurrency
public var price: Int64
public var timestamp: Int32?
public init(editingOriginalMessageId: MessageId?, price: Int64, timestamp: Int32?) {
public init(editingOriginalMessageId: MessageId?, currency: TelegramCurrency, price: Int64, timestamp: Int32?) {
self.editingOriginalMessageId = editingOriginalMessageId
self.currency = currency
self.price = price
self.timestamp = timestamp
}

View File

@ -168,7 +168,7 @@ public final class ChatPanelInterfaceInteraction {
public let addDoNotTranslateLanguage: (String) -> Void
public let hideTranslationPanel: () -> Void
public let openPremiumGift: () -> Void
public let openSuggestPost: () -> Void
public let openSuggestPost: (Message?) -> Void
public let openPremiumRequiredForMessaging: () -> Void
public let openStarsPurchase: (Int64?) -> Void
public let openMessagePayment: () -> Void
@ -291,7 +291,7 @@ public final class ChatPanelInterfaceInteraction {
addDoNotTranslateLanguage: @escaping (String) -> Void,
hideTranslationPanel: @escaping () -> Void,
openPremiumGift: @escaping () -> Void,
openSuggestPost: @escaping () -> Void,
openSuggestPost: @escaping (Message?) -> Void,
openPremiumRequiredForMessaging: @escaping () -> Void,
openStarsPurchase: @escaping (Int64?) -> Void,
openMessagePayment: @escaping () -> Void,
@ -544,7 +544,7 @@ public final class ChatPanelInterfaceInteraction {
}, addDoNotTranslateLanguage: { _ in
}, hideTranslationPanel: {
}, openPremiumGift: {
}, openSuggestPost: {
}, openSuggestPost: { _ in
}, openPremiumRequiredForMessaging: {
}, openStarsPurchase: { _ in
}, openMessagePayment: {

View File

@ -80,15 +80,20 @@ public extension ComponentTransition.DisappearWithGuide {
public extension ComponentTransition.Update {
static let `default` = ComponentTransition.Update { component, view, transition in
let frame = component.size.centered(around: component._position ?? CGPoint())
let position = component._position ?? CGPoint()
let size = component.size
view.layer.anchorPoint = component._anchorPoint ?? CGPoint(x: 0.5, y: 0.5)
if let scale = component._scale {
transition.setBounds(view: view, bounds: CGRect(origin: CGPoint(), size: frame.size))
transition.setPosition(view: view, position: frame.center)
transition.setBounds(view: view, bounds: CGRect(origin: CGPoint(), size: size))
transition.setPosition(view: view, position: position)
transition.setScale(view: view, scale: scale)
} else {
if view.frame != frame {
transition.setFrame(view: view, frame: frame)
if component._anchorPoint != nil {
view.bounds = CGRect(origin: CGPoint(), size: size)
} else {
transition.setBounds(view: view, bounds: CGRect(origin: CGPoint(), size: size))
}
transition.setPosition(view: view, position: position)
}
let opacity = component._opacity ?? 1.0
if view.alpha != opacity {

View File

@ -175,6 +175,7 @@ public final class _UpdatedChildComponent {
public let size: CGSize
var _removed: Bool = false
var _anchorPoint: CGPoint?
var _position: CGPoint?
var _scale: CGFloat?
var _opacity: CGFloat?
@ -237,6 +238,11 @@ public final class _UpdatedChildComponent {
self._removed = removed
return self
}
@discardableResult public func anchorPoint(_ anchorPoint: CGPoint) -> _UpdatedChildComponent {
self._anchorPoint = anchorPoint
return self
}
@discardableResult public func position(_ position: CGPoint) -> _UpdatedChildComponent {
self._position = position
@ -327,6 +333,10 @@ public extension _EnvironmentChildComponent {
func update<ComponentType: Component>(_ component: ComponentType, @EnvironmentBuilder environment: () -> Environment<EnvironmentType>, availableSize: CGSize, transition: ComponentTransition) -> _UpdatedChildComponent where ComponentType.EnvironmentType == EnvironmentType {
return self.update(component: AnyComponent(component), environment: environment, availableSize: availableSize, transition: transition)
}
func update(_ component: AnyComponent<EnvironmentType>, @EnvironmentBuilder environment: () -> Environment<EnvironmentType>, availableSize: CGSize, transition: ComponentTransition) -> _UpdatedChildComponent {
return self.update(component: component, environment: environment, availableSize: availableSize, transition: transition)
}
func update<ComponentType: Component>(_ component: ComponentType, @EnvironmentBuilder environment: () -> Environment<EnvironmentType>, availableSize: CGSize, transition: ComponentTransition) -> _UpdatedChildComponent where ComponentType.EnvironmentType == EnvironmentType, EnvironmentType == Empty {
return self.update(component: AnyComponent(component), environment: {}, availableSize: availableSize, transition: transition)
@ -707,12 +717,19 @@ public extension CombinedComponent {
view.insertSubview(updatedChild.view, at: index)
updatedChild.view.layer.anchorPoint = updatedChild._anchorPoint ?? CGPoint(x: 0.5, y: 0.5)
if let scale = updatedChild._scale {
updatedChild.view.bounds = CGRect(origin: CGPoint(), size: updatedChild.size)
updatedChild.view.center = updatedChild._position ?? CGPoint()
updatedChild.view.transform = CGAffineTransform(scaleX: scale, y: scale)
} else {
updatedChild.view.frame = updatedChild.size.centered(around: updatedChild._position ?? CGPoint())
updatedChild.view.bounds = CGRect(origin: CGPoint(), size: updatedChild.size)
if updatedChild.view.layer.anchorPoint != CGPoint(x: 0.5, y: 0.5) {
updatedChild.view.layer.position = updatedChild._position ?? CGPoint()
} else {
updatedChild.view.center = updatedChild._position ?? CGPoint()
}
}
updatedChild.view.alpha = updatedChild._opacity ?? 1.0

View File

@ -90,7 +90,6 @@ extension CGSize {
private let springAnimationIn: CABasicAnimation = {
let animation = makeSpringAnimation("")
animation.duration = 1.0
return animation
}()

View File

@ -5682,9 +5682,9 @@ public extension Api.functions.messages {
}
}
public extension Api.functions.messages {
static func forwardMessages(flags: Int32, fromPeer: Api.InputPeer, id: [Int32], randomId: [Int64], toPeer: Api.InputPeer, topMsgId: Int32?, replyTo: Api.InputReplyTo?, scheduleDate: Int32?, sendAs: Api.InputPeer?, quickReplyShortcut: Api.InputQuickReplyShortcut?, videoTimestamp: Int32?, allowPaidStars: Int64?) -> (FunctionDescription, Buffer, DeserializeFunctionResponse<Api.Updates>) {
static func forwardMessages(flags: Int32, fromPeer: Api.InputPeer, id: [Int32], randomId: [Int64], toPeer: Api.InputPeer, topMsgId: Int32?, replyTo: Api.InputReplyTo?, scheduleDate: Int32?, sendAs: Api.InputPeer?, quickReplyShortcut: Api.InputQuickReplyShortcut?, videoTimestamp: Int32?, allowPaidStars: Int64?, suggestedPost: Api.SuggestedPost?) -> (FunctionDescription, Buffer, DeserializeFunctionResponse<Api.Updates>) {
let buffer = Buffer()
buffer.appendInt32(955259020)
buffer.appendInt32(-1752618806)
serializeInt32(flags, buffer: buffer, boxed: false)
fromPeer.serialize(buffer, true)
buffer.appendInt32(481674261)
@ -5705,7 +5705,8 @@ public extension Api.functions.messages {
if Int(flags) & Int(1 << 17) != 0 {quickReplyShortcut!.serialize(buffer, true)}
if Int(flags) & Int(1 << 20) != 0 {serializeInt32(videoTimestamp!, buffer: buffer, boxed: false)}
if Int(flags) & Int(1 << 21) != 0 {serializeInt64(allowPaidStars!, buffer: buffer, boxed: false)}
return (FunctionDescription(name: "messages.forwardMessages", parameters: [("flags", String(describing: flags)), ("fromPeer", String(describing: fromPeer)), ("id", String(describing: id)), ("randomId", String(describing: randomId)), ("toPeer", String(describing: toPeer)), ("topMsgId", String(describing: topMsgId)), ("replyTo", String(describing: replyTo)), ("scheduleDate", String(describing: scheduleDate)), ("sendAs", String(describing: sendAs)), ("quickReplyShortcut", String(describing: quickReplyShortcut)), ("videoTimestamp", String(describing: videoTimestamp)), ("allowPaidStars", String(describing: allowPaidStars))]), buffer, DeserializeFunctionResponse { (buffer: Buffer) -> Api.Updates? in
if Int(flags) & Int(1 << 23) != 0 {suggestedPost!.serialize(buffer, true)}
return (FunctionDescription(name: "messages.forwardMessages", parameters: [("flags", String(describing: flags)), ("fromPeer", String(describing: fromPeer)), ("id", String(describing: id)), ("randomId", String(describing: randomId)), ("toPeer", String(describing: toPeer)), ("topMsgId", String(describing: topMsgId)), ("replyTo", String(describing: replyTo)), ("scheduleDate", String(describing: scheduleDate)), ("sendAs", String(describing: sendAs)), ("quickReplyShortcut", String(describing: quickReplyShortcut)), ("videoTimestamp", String(describing: videoTimestamp)), ("allowPaidStars", String(describing: allowPaidStars)), ("suggestedPost", String(describing: suggestedPost))]), buffer, DeserializeFunctionResponse { (buffer: Buffer) -> Api.Updates? in
let reader = BufferReader(buffer)
var result: Api.Updates?
if let signature = reader.readInt32() {

View File

@ -486,7 +486,7 @@ private func sendUploadedMessageContent(
}
if let forwardSourceInfoAttribute = forwardSourceInfoAttribute, let sourcePeer = transaction.getPeer(forwardSourceInfoAttribute.messageId.peerId), let sourceInputPeer = apiInputPeer(sourcePeer) {
sendMessageRequest = network.request(Api.functions.messages.forwardMessages(flags: flags, fromPeer: sourceInputPeer, id: [sourceInfo.messageId.id], randomId: [uniqueId], toPeer: inputPeer, topMsgId: topMsgId, replyTo: nil, scheduleDate: scheduleTime, sendAs: sendAsInputPeer, quickReplyShortcut: nil, videoTimestamp: videoTimestamp, allowPaidStars: allowPaidStars), tag: dependencyTag)
sendMessageRequest = network.request(Api.functions.messages.forwardMessages(flags: flags, fromPeer: sourceInputPeer, id: [sourceInfo.messageId.id], randomId: [uniqueId], toPeer: inputPeer, topMsgId: topMsgId, replyTo: nil, scheduleDate: scheduleTime, sendAs: sendAsInputPeer, quickReplyShortcut: nil, videoTimestamp: videoTimestamp, allowPaidStars: allowPaidStars, suggestedPost: nil), tag: dependencyTag)
|> map(NetworkRequestResult.result)
} else {
sendMessageRequest = .fail(MTRpcError(errorCode: 400, errorDescription: "internal"))

View File

@ -860,6 +860,7 @@ public final class PendingMessageManager {
var quickReply: OutgoingQuickReplyMessageAttribute?
var messageEffect: EffectMessageAttribute?
var allowPaidStars: Int64?
var suggestedPost: Api.SuggestedPost?
var flags: Int32 = 0
@ -898,6 +899,8 @@ public final class PendingMessageManager {
videoTimestamp = attribute.timestamp
} else if let attribute = attribute as? PaidStarsMessageAttribute {
allowPaidStars = attribute.stars.value * Int64(messages.count)
} else if let attribute = attribute as? SuggestedPostMessageAttribute {
suggestedPost = attribute.apiSuggestedPost()
}
}
@ -978,6 +981,10 @@ public final class PendingMessageManager {
flags |= 1 << 22
}
if suggestedPost != nil {
flags |= 1 << 23
}
let forwardPeerIds = Set(forwardIds.map { $0.0.peerId })
if forwardPeerIds.count != 1 {
assertionFailure()
@ -985,7 +992,7 @@ public final class PendingMessageManager {
} else if let inputSourcePeerId = forwardPeerIds.first, let inputSourcePeer = transaction.getPeer(inputSourcePeerId).flatMap(apiInputPeer) {
let dependencyTag = PendingMessageRequestDependencyTag(messageId: messages[0].0.id)
sendMessageRequest = network.request(Api.functions.messages.forwardMessages(flags: flags, fromPeer: inputSourcePeer, id: forwardIds.map { $0.0.id }, randomId: forwardIds.map { $0.1 }, toPeer: inputPeer, topMsgId: topMsgId, replyTo: replyTo, scheduleDate: scheduleTime, sendAs: sendAsInputPeer, quickReplyShortcut: quickReplyShortcut, videoTimestamp: videoTimestamp, allowPaidStars: allowPaidStars), tag: dependencyTag)
sendMessageRequest = network.request(Api.functions.messages.forwardMessages(flags: flags, fromPeer: inputSourcePeer, id: forwardIds.map { $0.0.id }, randomId: forwardIds.map { $0.1 }, toPeer: inputPeer, topMsgId: topMsgId, replyTo: replyTo, scheduleDate: scheduleTime, sendAs: sendAsInputPeer, quickReplyShortcut: quickReplyShortcut, videoTimestamp: videoTimestamp, allowPaidStars: allowPaidStars, suggestedPost: suggestedPost), tag: dependencyTag)
} else {
assertionFailure()
sendMessageRequest = .fail(MTRpcError(errorCode: 400, errorDescription: "Invalid forward source"))
@ -1663,8 +1670,12 @@ public final class PendingMessageManager {
flags |= 1 << 22
}
if suggestedPost != nil {
flags |= 1 << 23
}
if let forwardSourceInfoAttribute = forwardSourceInfoAttribute, let sourcePeer = transaction.getPeer(forwardSourceInfoAttribute.messageId.peerId), let sourceInputPeer = apiInputPeer(sourcePeer) {
sendMessageRequest = network.request(Api.functions.messages.forwardMessages(flags: flags, fromPeer: sourceInputPeer, id: [sourceInfo.messageId.id], randomId: [uniqueId], toPeer: inputPeer, topMsgId: topMsgId, replyTo: replyTo, scheduleDate: scheduleTime, sendAs: sendAsInputPeer, quickReplyShortcut: quickReplyShortcut, videoTimestamp: videoTimestamp, allowPaidStars: allowPaidStars), tag: dependencyTag)
sendMessageRequest = network.request(Api.functions.messages.forwardMessages(flags: flags, fromPeer: sourceInputPeer, id: [sourceInfo.messageId.id], randomId: [uniqueId], toPeer: inputPeer, topMsgId: topMsgId, replyTo: replyTo, scheduleDate: scheduleTime, sendAs: sendAsInputPeer, quickReplyShortcut: quickReplyShortcut, videoTimestamp: videoTimestamp, allowPaidStars: allowPaidStars, suggestedPost: suggestedPost), tag: dependencyTag)
|> map(NetworkRequestResult.result)
} else {
sendMessageRequest = .fail(MTRpcError(errorCode: 400, errorDescription: "internal"))

View File

@ -8,23 +8,27 @@ public final class SuggestedPostMessageAttribute: Equatable, MessageAttribute {
case rejected = 1
}
public let currency: TelegramCurrency
public let amount: Int64
public let timestamp: Int32?
public let state: State?
public init(amount: Int64, timestamp: Int32?, state: State?) {
public init(currency: TelegramCurrency, amount: Int64, timestamp: Int32?, state: State?) {
self.currency = currency
self.amount = amount
self.timestamp = timestamp
self.state = state
}
required public init(decoder: PostboxDecoder) {
self.currency = decoder.decodeCodable(TelegramCurrency.self, forKey: "cur") ?? .stars
self.amount = decoder.decodeInt64ForKey("am", orElse: 0)
self.timestamp = decoder.decodeOptionalInt32ForKey("ts")
self.state = decoder.decodeOptionalInt32ForKey("st").flatMap(State.init(rawValue:))
}
public func encode(_ encoder: PostboxEncoder) {
encoder.encodeCodable(self.currency, forKey: "cur")
encoder.encodeInt64(self.amount, forKey: "am")
if let timestamp = self.timestamp {
encoder.encodeInt32(timestamp, forKey: "ts")
@ -39,6 +43,9 @@ public final class SuggestedPostMessageAttribute: Equatable, MessageAttribute {
}
public static func ==(lhs: SuggestedPostMessageAttribute, rhs: SuggestedPostMessageAttribute) -> Bool {
if lhs.currency != rhs.currency {
return false
}
if lhs.amount != rhs.amount {
return false
}
@ -62,7 +69,7 @@ extension SuggestedPostMessageAttribute {
} else if (flags & (1 << 2)) != 0 {
state = .rejected
}
self.init(amount: starsAmount, timestamp: scheduleDate, state: state)
self.init(currency: .stars, amount: starsAmount, timestamp: scheduleDate, state: state)
}
}

View File

@ -14,7 +14,7 @@ func _internal_forwardGameWithScore(account: Account, messageId: MessageId, to p
flags |= (1 << 13)
}
return account.network.request(Api.functions.messages.forwardMessages(flags: flags, fromPeer: fromInputPeer, id: [messageId.id], randomId: [Int64.random(in: Int64.min ... Int64.max)], toPeer: toInputPeer, topMsgId: threadId.flatMap { Int32(clamping: $0) }, replyTo: nil, scheduleDate: nil, sendAs: sendAsInputPeer, quickReplyShortcut: nil, videoTimestamp: nil, allowPaidStars: nil))
return account.network.request(Api.functions.messages.forwardMessages(flags: flags, fromPeer: fromInputPeer, id: [messageId.id], randomId: [Int64.random(in: Int64.min ... Int64.max)], toPeer: toInputPeer, topMsgId: threadId.flatMap { Int32(clamping: $0) }, replyTo: nil, scheduleDate: nil, sendAs: sendAsInputPeer, quickReplyShortcut: nil, videoTimestamp: nil, allowPaidStars: nil, suggestedPost: nil))
|> map(Optional.init)
|> `catch` { _ -> Signal<Api.Updates?, NoError> in
return .single(nil)

View File

@ -352,6 +352,38 @@ extension StarsAmount {
}
}
public enum TelegramCurrency: Codable {
private enum CodingKeys: String, CodingKey {
case discriminator = "_"
}
case stars
case ton
public init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
switch try container.decode(Int32.self, forKey: .discriminator) {
case 0:
self = .stars
case 1:
self = .ton
default:
assertionFailure()
self = .stars
}
}
public func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
switch self {
case .stars:
try container.encode(0 as Int32, forKey: .discriminator)
case .ton:
try container.encode(1 as Int32, forKey: .discriminator)
}
}
}
struct InternalStarsStatus {
let balance: StarsAmount
let subscriptionsMissingBalance: StarsAmount?

View File

@ -1244,8 +1244,21 @@ public func universalServiceMessageString(presentationData: (PresentationTheme,
}
case let .paidMessagesRefunded(_, stars):
let starsString = strings.Notification_PaidMessageRefund_Stars(Int32(stars))
if message.author?.id == accountPeerId, let messagePeer = message.peers[message.id.peerId] {
let peerName = EnginePeer(messagePeer).compactDisplayTitle
var isOutgoing = false
var messagePeer: EnginePeer?
if message.author?.id == accountPeerId, let messagePeerValue = message.peers[message.id.peerId] {
isOutgoing = true
messagePeer = EnginePeer(messagePeerValue)
} else if message.id.peerId.namespace == Namespaces.Peer.CloudChannel, let peer = message.peers[message.id.peerId] as? TelegramChannel, peer.isMonoForum {
if let author = message.author, let threadId = message.threadId, let threadPeer = message.peers[PeerId(threadId)], author.id != threadPeer.id {
isOutgoing = true
messagePeer = EnginePeer(threadPeer)
}
}
if isOutgoing, let messagePeer {
let peerName = messagePeer.compactDisplayTitle
var attributes = peerMentionsAttributes(primaryTextColor: primaryTextColor, peerIds: [(1, messagePeer.id)])
attributes[0] = boldAttributes
let resultString = strings.Notification_PaidMessageRefundYou(starsString, peerName)

View File

@ -385,11 +385,11 @@ public final class ChatButtonKeyboardInputNode: ChatInputNode {
self.controllerInteraction.shareAccountContact()
case .openWebApp:
if let message = self.message {
self.controllerInteraction.requestMessageActionCallback(message, nil, true, false)
self.controllerInteraction.requestMessageActionCallback(message, nil, true, false, nil)
}
case let .callback(requiresPassword, data):
if let message = self.message {
self.controllerInteraction.requestMessageActionCallback(message, data, false, requiresPassword)
self.controllerInteraction.requestMessageActionCallback(message, data, false, requiresPassword, nil)
}
case let .switchInline(samePeer, query, _):
if let message = message {

View File

@ -261,17 +261,25 @@ public class ChatMessageActionBubbleContentNode: ChatMessageBubbleContentNode {
let amountString = amount == 1 ? "\(amount) Star" : "\(amount) Stars"
let rawString: String
if timestamp != nil {
if !item.message.effectivelyIncoming(item.context.account.peerId) {
rawString = "📅 The post will be automatically published in **\(channelName)** **\(timeString)**.\n\n💰 The user have been charged \(amountString).\n\n⌛ **\(channelName)** will receive the Stars once the post has been live for 24 hours.\n\n🔄 If your remove the post before it has been live for 24 hours, the user's Stars will be refunded."
if let timestamp {
if Int32(Date().timeIntervalSince1970) >= timestamp {
if !item.message.effectivelyIncoming(item.context.account.peerId) {
rawString = "📅 The post has been automatically published in **\(channelName)** **\(timeString)**.\n\n💰 The user have been charged \(amountString).\n\n⌛ **\(channelName)** will receive the Stars once the post has been live for 24 hours.\n\n🔄 If your remove the post before it has been live for 24 hours, the user's Stars will be refunded."
} else {
rawString = "📅 Your post has been automatically published in **\(channelName)** **\(timeString)**.\n\n💰 You have been charged \(amountString).\n\n⌛ **\(channelName)** will receive your Stars once the post has been live for 24 hours.\n\n🔄 If **\(channelName)** removes the post before it has been live for 24 hours, your Stars will be refunded."
}
} else {
rawString = "📅 Your post will be automatically published in **\(channelName)** **\(timeString)**.\n\n💰 You have been charged \(amountString).\n\n⌛ **\(channelName)** will receive your Stars once the post has been live for 24 hours.\n\n🔄 If **\(channelName)** removes the post before it has been live for 24 hours, your Stars will be refunded."
if !item.message.effectivelyIncoming(item.context.account.peerId) {
rawString = "📅 The post will be automatically published in **\(channelName)** **\(timeString)**.\n\n💰 The user have been charged \(amountString).\n\n⌛ **\(channelName)** will receive the Stars once the post has been live for 24 hours.\n\n🔄 If your remove the post before it has been live for 24 hours, the user's Stars will be refunded."
} else {
rawString = "📅 Your post will be automatically published in **\(channelName)** **\(timeString)**.\n\n💰 You have been charged \(amountString).\n\n⌛ **\(channelName)** will receive your Stars once the post has been live for 24 hours.\n\n🔄 If **\(channelName)** removes the post before it has been live for 24 hours, your Stars will be refunded."
}
}
} else {
if !item.message.effectivelyIncoming(item.context.account.peerId) {
rawString = "📅 The post will be automatically published in **\(channelName)**.\n\n💰 The user have been charged \(amountString).\n\n⌛ **\(channelName)** will receive the Stars once the post has been live for 24 hours.\n\n🔄 If your remove the post before it has been live for 24 hours, the user's Stars will be refunded."
rawString = "📅 The post has been automatically published in **\(channelName)**.\n\n💰 The user have been charged \(amountString).\n\n⌛ **\(channelName)** will receive the Stars once the post has been live for 24 hours.\n\n🔄 If your remove the post before it has been live for 24 hours, the user's Stars will be refunded."
} else {
rawString = "📅 Your post will be automatically published in **\(channelName)**.\n\n💰 You have been charged \(amountString).\n\n⌛ **\(channelName)** will receive your Stars once the post has been live for 24 hours.\n\n🔄 If **\(channelName)** removes the post before it has been live for 24 hours, your Stars will be refunded."
rawString = "📅 Your post has been automatically published in **\(channelName)**.\n\n💰 You have been charged \(amountString).\n\n⌛ **\(channelName)** will receive your Stars once the post has been live for 24 hours.\n\n🔄 If **\(channelName)** removes the post before it has been live for 24 hours, your Stars will be refunded."
}
}
updatedAttributedString = parseMarkdownIntoAttributedString(rawString, attributes: MarkdownAttributes(

View File

@ -10,6 +10,7 @@ swift_library(
"-warnings-as-errors",
],
deps = [
"//submodules/SSignalKit/SwiftSignalKit",
"//submodules/AsyncDisplayKit",
"//submodules/TelegramCore",
"//submodules/Postbox",
@ -18,6 +19,7 @@ swift_library(
"//submodules/AccountContext",
"//submodules/WallpaperBackgroundNode",
"//submodules/UrlHandling",
"//submodules/TelegramUI/Components/TextLoadingEffect",
],
visibility = [
"//visibility:public",

View File

@ -8,6 +8,8 @@ import TelegramPresentationData
import AccountContext
import WallpaperBackgroundNode
import UrlHandling
import SwiftSignalKit
import TextLoadingEffect
private let titleFont = Font.medium(16.0)
@ -72,16 +74,21 @@ private final class ChatMessageActionButtonNode: ASDisplayNode {
private var backgroundContent: WallpaperBubbleBackgroundNode?
private var backgroundColorNode: ASDisplayNode?
private var maskPath: CGPath?
private var loadingEffectView: TextLoadingEffectView?
private var absolutePosition: (CGRect, CGSize)?
private var button: ReplyMarkupButton?
var pressed: ((ReplyMarkupButton) -> Void)?
var pressed: ((ReplyMarkupButton, Promise<Bool>) -> Void)?
var longTapped: ((ReplyMarkupButton) -> Void)?
var longTapRecognizer: UILongPressGestureRecognizer?
private let accessibilityArea: AccessibilityAreaNode
private var progressDisposable: Disposable?
override init() {
self.accessibilityArea = AccessibilityAreaNode()
self.accessibilityArea.accessibilityTraits = .button
@ -96,6 +103,10 @@ private final class ChatMessageActionButtonNode: ASDisplayNode {
}
}
deinit {
self.progressDisposable?.dispose()
}
override func didLoad() {
super.didLoad()
@ -140,7 +151,48 @@ private final class ChatMessageActionButtonNode: ASDisplayNode {
@objc func buttonPressed() {
if let button = self.button, let pressed = self.pressed {
pressed(button)
let progressPromise = Promise<Bool>()
pressed(button, progressPromise)
self.progressDisposable?.dispose()
self.progressDisposable = (progressPromise.get()
|> deliverOnMainQueue).startStrict(next: { [weak self] isLoading in
guard let self else {
return
}
self.updateIsLoading(isLoading: isLoading)
})
}
}
private func updateIsLoading(isLoading: Bool) {
if isLoading {
if self.loadingEffectView == nil {
let loadingEffectView = TextLoadingEffectView(frame: CGRect())
self.loadingEffectView = loadingEffectView
if let iconNode = self.iconNode, iconNode.view.superview != nil {
self.view.insertSubview(loadingEffectView, belowSubview: iconNode.view)
} else if let titleNode = self.titleNode, titleNode.view.superview != nil {
self.view.insertSubview(loadingEffectView, belowSubview: titleNode.view)
} else {
self.view.addSubview(loadingEffectView)
}
if let buttonView = self.buttonView, let maskPath = self.maskPath {
let loadingFrame = buttonView.frame
loadingEffectView.frame = loadingFrame
loadingEffectView.update(color: UIColor(white: 1.0, alpha: 1.0), rect: CGRect(origin: CGPoint(), size: loadingFrame.size), path: maskPath)
}
}
} else {
if let loadingEffectView {
self.loadingEffectView = nil
loadingEffectView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { [weak loadingEffectView] _ in
loadingEffectView?.removeFromSuperview()
})
}
}
}
@ -351,6 +403,7 @@ private final class ChatMessageActionButtonNode: ASDisplayNode {
let rect = CGRect(origin: CGPoint(), size: CGSize(width: max(0.0, width), height: 42.0))
let maskPath: CGPath?
var needsMask = true
switch position {
case .bottomSingle:
maskPath = UIBezierPath(roundRect: rect, topLeftRadius: bubbleCorners.auxiliaryRadius, topRightRadius: bubbleCorners.auxiliaryRadius, bottomLeftRadius: bubbleCorners.mainRadius, bottomRightRadius: bubbleCorners.mainRadius).cgPath
@ -359,14 +412,19 @@ private final class ChatMessageActionButtonNode: ASDisplayNode {
case .bottomRight:
maskPath = UIBezierPath(roundRect: rect, topLeftRadius: bubbleCorners.auxiliaryRadius, topRightRadius: bubbleCorners.auxiliaryRadius, bottomLeftRadius: bubbleCorners.auxiliaryRadius, bottomRightRadius: bubbleCorners.mainRadius).cgPath
default:
maskPath = nil
needsMask = false
maskPath = UIBezierPath(roundRect: rect, topLeftRadius: bubbleCorners.auxiliaryRadius, topRightRadius: bubbleCorners.auxiliaryRadius, bottomLeftRadius: bubbleCorners.auxiliaryRadius, bottomRightRadius: bubbleCorners.auxiliaryRadius).cgPath
}
let currentMaskPath = (node.layer.mask as? CAShapeLayer)?.path
if currentMaskPath != maskPath {
if let maskPath = maskPath {
node.maskPath = maskPath
let effectiveMaskPath = needsMask ? maskPath : nil
if currentMaskPath != effectiveMaskPath {
if let effectiveMaskPath = effectiveMaskPath {
let shapeLayer = CAShapeLayer()
shapeLayer.path = maskPath
shapeLayer.path = effectiveMaskPath
node.layer.mask = shapeLayer
} else {
node.layer.mask = nil
@ -437,9 +495,9 @@ public final class ChatMessageActionButtonsNode: ASDisplayNode {
private var buttonNodes: [ChatMessageActionButtonNode] = []
private var buttonPressedWrapper: ((ReplyMarkupButton) -> Void)?
private var buttonPressedWrapper: ((ReplyMarkupButton, Promise<Bool>) -> Void)?
private var buttonLongTappedWrapper: ((ReplyMarkupButton) -> Void)?
public var buttonPressed: ((ReplyMarkupButton) -> Void)?
public var buttonPressed: ((ReplyMarkupButton, Promise<Bool>) -> Void)?
public var buttonLongTapped: ((ReplyMarkupButton) -> Void)?
private var absolutePosition: (CGRect, CGSize)?
@ -447,9 +505,9 @@ public final class ChatMessageActionButtonsNode: ASDisplayNode {
override public init() {
super.init()
self.buttonPressedWrapper = { [weak self] button in
self.buttonPressedWrapper = { [weak self] button, promise in
if let buttonPressed = self?.buttonPressed {
buttonPressed(button)
buttonPressed(button, promise)
}
}

View File

@ -1718,9 +1718,9 @@ public class ChatMessageAnimatedStickerItemNode: ChatMessageItemView {
actionButtonsNode.frame = actionButtonsFrame
if actionButtonsNode !== strongSelf.actionButtonsNode {
strongSelf.actionButtonsNode = actionButtonsNode
actionButtonsNode.buttonPressed = { button in
actionButtonsNode.buttonPressed = { button, progress in
if let strongSelf = weakSelf.value {
strongSelf.performMessageButtonAction(button: button)
strongSelf.performMessageButtonAction(button: button, progress: progress)
}
}
actionButtonsNode.buttonLongTapped = { button in

View File

@ -4700,9 +4700,9 @@ public class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewI
let actionButtonsFrame = CGRect(origin: CGPoint(x: backgroundFrame.minX + (incoming ? layoutConstants.bubble.contentInsets.left : layoutConstants.bubble.contentInsets.right), y: backgroundFrame.maxY), size: actionButtonsSizeAndApply.0)
if actionButtonsNode !== strongSelf.actionButtonsNode {
strongSelf.actionButtonsNode = actionButtonsNode
actionButtonsNode.buttonPressed = { [weak strongSelf] button in
actionButtonsNode.buttonPressed = { [weak strongSelf] button, progress in
if let strongSelf = strongSelf {
strongSelf.performMessageButtonAction(button: button)
strongSelf.performMessageButtonAction(button: button, progress: progress)
}
}
actionButtonsNode.buttonLongTapped = { [weak strongSelf] button in

View File

@ -28,14 +28,14 @@ public final class ChatMessageGameBubbleContentNode: ChatMessageBubbleContentNod
self.addSubnode(self.contentNode)
self.contentNode.openMedia = { [weak self] _ in
if let strongSelf = self, let item = strongSelf.item {
item.controllerInteraction.requestMessageActionCallback(item.message, nil, true, false)
item.controllerInteraction.requestMessageActionCallback(item.message, nil, true, false, nil)
}
}
}
override public func accessibilityActivate() -> Bool {
if let item = self.item {
item.controllerInteraction.requestMessageActionCallback(item.message, nil, true, false)
item.controllerInteraction.requestMessageActionCallback(item.message, nil, true, false, nil)
}
return true
}

View File

@ -890,9 +890,9 @@ public class ChatMessageInstantVideoItemNode: ChatMessageItemView, ASGestureReco
actionButtonsNode.frame = actionButtonsFrame
if actionButtonsNode !== strongSelf.actionButtonsNode {
strongSelf.actionButtonsNode = actionButtonsNode
actionButtonsNode.buttonPressed = { button in
actionButtonsNode.buttonPressed = { button, progress in
if let strongSelf = weakSelf.value {
strongSelf.performMessageButtonAction(button: button)
strongSelf.performMessageButtonAction(button: button, progress: progress)
}
}
actionButtonsNode.buttonLongTapped = { button in

View File

@ -805,7 +805,7 @@ open class ChatMessageItemView: ListViewItemNode, ChatMessageItemNodeProtocol {
}
}
open func performMessageButtonAction(button: ReplyMarkupButton) {
public func performMessageButtonAction(button: ReplyMarkupButton, progress: Promise<Bool>?) {
if let item = self.item {
switch button.action {
case .text:
@ -815,15 +815,15 @@ open class ChatMessageItemView: ListViewItemNode, ChatMessageItemNodeProtocol {
if url.hasPrefix("tg://") {
concealed = false
}
item.controllerInteraction.openUrl(ChatControllerInteraction.OpenUrl(url: url, concealed: concealed, progress: Promise()))
item.controllerInteraction.openUrl(ChatControllerInteraction.OpenUrl(url: url, concealed: concealed, progress: progress))
case .requestMap:
item.controllerInteraction.shareCurrentLocation()
case .requestPhone:
item.controllerInteraction.shareAccountContact()
case .openWebApp:
item.controllerInteraction.requestMessageActionCallback(item.message, nil, true, false)
item.controllerInteraction.requestMessageActionCallback(item.message, nil, true, false, progress)
case let .callback(requiresPassword, data):
item.controllerInteraction.requestMessageActionCallback(item.message, data, false, requiresPassword)
item.controllerInteraction.requestMessageActionCallback(item.message, data, false, requiresPassword, progress)
case let .switchInline(samePeer, query, peerTypes):
var botPeer: Peer?

View File

@ -1276,9 +1276,9 @@ public class ChatMessageStickerItemNode: ChatMessageItemView {
actionButtonsNode.frame = actionButtonsFrame
if actionButtonsNode !== strongSelf.actionButtonsNode {
strongSelf.actionButtonsNode = actionButtonsNode
actionButtonsNode.buttonPressed = { button in
actionButtonsNode.buttonPressed = { button, progress in
if let strongSelf = weakSelf.value {
strongSelf.performMessageButtonAction(button: button)
strongSelf.performMessageButtonAction(button: button, progress: progress)
}
}
actionButtonsNode.buttonLongTapped = { button in

View File

@ -48,11 +48,13 @@ public final class ChatMessageSuggestedPostInfoNode: ASDisplayNode {
let labelSpacing: CGFloat = 8.0
let valuesVerticalSpacing: CGFloat = 2.0
var currency: TelegramCurrency = .stars
var amount: Int64 = 0
var timestamp: Int32?
for attribute in item.message.attributes {
if let attribute = attribute as? SuggestedPostMessageAttribute {
currency = attribute.currency
amount = attribute.amount
timestamp = attribute.timestamp
}
@ -60,12 +62,23 @@ public final class ChatMessageSuggestedPostInfoNode: ASDisplayNode {
//TODO:localize
let amountString: String
if amount == 0 {
amountString = "Free"
} else if amount == 1 {
amountString = "1 Star"
} else {
amountString = "\(amount) Stars"
switch currency {
case .stars:
if amount == 0 {
amountString = "Free"
} else if amount == 1 {
amountString = "1 Star"
} else {
amountString = "\(amount) Stars"
}
case .ton:
if amount == 0 {
amountString = "Free"
} else if amount == 1 {
amountString = "1 TON"
} else {
amountString = "\(amount) TON"
}
}
var timestampString: String

View File

@ -165,7 +165,7 @@ public final class ChatRecentActionsController: TelegramBaseController {
}, addDoNotTranslateLanguage: { _ in
}, hideTranslationPanel: {
}, openPremiumGift: {
}, openSuggestPost: {
}, openSuggestPost: { _ in
}, openPremiumRequiredForMessaging: {
}, openStarsPurchase: { _ in
}, openMessagePayment: {

View File

@ -316,7 +316,7 @@ final class ChatRecentActionsControllerNode: ViewControllerTracingNode {
let _ = context.sharedContext.navigateToForumThread(context: context, peerId: peerId, threadId: threadId, messageId: nil, navigationController: navigationController, activateInput: nil, scrollToEndIfExists: false, keepStack: .always, animated: true).startStandalone()
}
}, tapMessage: nil, clickThroughMessage: { _, _ in }, toggleMessagesSelection: { _, _ in }, sendCurrentMessage: { _, _ in }, sendMessage: { _ in }, sendSticker: { _, _, _, _, _, _, _, _, _ in return false }, sendEmoji: { _, _, _ in }, sendGif: { _, _, _, _, _ in return false }, sendBotContextResultAsGif: { _, _, _, _, _, _ in return false
}, requestMessageActionCallback: { [weak self] message, _, _, _ in
}, requestMessageActionCallback: { [weak self] message, _, _, _, _ in
guard let self else {
return
}

View File

@ -421,7 +421,7 @@ public final class ChatSendGroupMediaMessageContextPreview: UIView, ChatSendMess
}, clickThroughMessage: { _, _ in
}, toggleMessagesSelection: { _, _ in }, sendCurrentMessage: { _, _ in }, sendMessage: { _ in }, sendSticker: { _, _, _, _, _, _, _, _, _ in return false }, sendEmoji: { _, _, _ in }, sendGif: { _, _, _, _, _ in return false }, sendBotContextResultAsGif: { _, _, _, _, _, _ in
return false
}, requestMessageActionCallback: { _, _, _, _ in }, requestMessageActionUrlAuth: { _, _ in }, activateSwitchInline: { _, _, _ in }, openUrl: { _ in }, shareCurrentLocation: {}, shareAccountContact: {}, sendBotCommand: { _, _ in }, openInstantPage: { _, _ in }, openWallpaper: { _ in }, openTheme: { _ in }, openHashtag: { _, _ in }, updateInputState: { _ in }, updateInputMode: { _ in }, openMessageShareMenu: { _ in
}, requestMessageActionCallback: { _, _, _, _, _ in }, requestMessageActionUrlAuth: { _, _ in }, activateSwitchInline: { _, _, _ in }, openUrl: { _ in }, shareCurrentLocation: {}, shareAccountContact: {}, sendBotCommand: { _, _ in }, openInstantPage: { _, _ in }, openWallpaper: { _ in }, openTheme: { _ in }, openHashtag: { _, _ in }, updateInputState: { _ in }, updateInputMode: { _ in }, openMessageShareMenu: { _ in
}, presentController: { _, _ in
}, presentControllerInCurrent: { _, _ in
}, navigationController: {

View File

@ -41,6 +41,7 @@ public final class SuggestPostAccessoryPanelNode: AccessoryPanelNode {
private var validLayout: (size: CGSize, inset: CGFloat, interfaceState: ChatPresentationInterfaceState)?
private var inlineTextStarImage: UIImage?
private var inlineTextTonImage: (UIImage, UIColor)?
public init(context: AccountContext, theme: PresentationTheme, strings: PresentationStrings, nameDisplayOrder: PresentationPersonNameOrder, dateTimeFormat: PresentationDateTimeFormat, animationCache: AnimationCache?, animationRenderer: MultiAnimationRenderer?) {
self.context = context
@ -203,6 +204,26 @@ public final class SuggestPostAccessoryPanelNode: AccessoryPanelNode {
}
}
var inlineTextTonImage: UIImage?
if let current = self.inlineTextTonImage, current.1 == self.theme.list.itemAccentColor {
inlineTextTonImage = current.0
} else {
if let image = UIImage(bundleImageName: "Ads/TonMedium") {
let tonInsets = UIEdgeInsets(top: 0.0, left: 0.0, bottom: 0.0, right: 0.0)
let inlineTextTonImageValue = generateTintedImage(image: generateImage(CGSize(width: tonInsets.left + image.size.width + tonInsets.right, height: image.size.height), rotatedContext: { size, context in
context.clear(CGRect(origin: CGPoint(), size: size))
UIGraphicsPushContext(context)
defer {
UIGraphicsPopContext()
}
image.draw(at: CGPoint(x: tonInsets.left, y: tonInsets.top))
}), color: self.theme.list.itemAccentColor)!.withRenderingMode(.alwaysOriginal)
inlineTextTonImage = inlineTextTonImageValue
self.inlineTextTonImage = (inlineTextTonImageValue, self.theme.list.itemAccentColor)
}
}
//TODO:localize
var titleText: [CompositeTextNode.Component] = []
if let postSuggestionState = interfaceState.interfaceState.postSuggestionState, postSuggestionState.editingOriginalMessageId != nil {
@ -219,6 +240,13 @@ public final class SuggestPostAccessoryPanelNode: AccessoryPanelNode {
let textString: NSAttributedString
if let postSuggestionState = interfaceState.interfaceState.postSuggestionState, postSuggestionState.price != 0 {
let currencySymbol: String
switch postSuggestionState.currency {
case .stars:
currencySymbol = "#"
case .ton:
currencySymbol = "$"
}
if let timestamp = postSuggestionState.timestamp {
let timeString = humanReadableStringForTimestamp(strings: interfaceState.strings, dateTimeFormat: interfaceState.dateTimeFormat, timestamp: timestamp, alwaysShowTime: true, allowYesterday: false, format: HumanReadableStringFormat(
dateFormatString: { value in
@ -234,57 +262,70 @@ public final class SuggestPostAccessoryPanelNode: AccessoryPanelNode {
return PresentationStrings.FormattedString(string: interfaceState.strings.SuggestPost_SetTimeFormat_TodayAt(value).string, ranges: [])
}
)).string
textString = NSAttributedString(string: "#\(postSuggestionState.price) 📅 \(timeString)", font: textFont, textColor: self.theme.chat.inputPanel.primaryTextColor)
textString = NSAttributedString(string: "\(currencySymbol)\(postSuggestionState.price) 📅 \(timeString)", font: textFont, textColor: self.theme.chat.inputPanel.primaryTextColor)
} else {
textString = NSAttributedString(string: "#\(postSuggestionState.price) for publishing anytime", font: textFont, textColor: self.theme.chat.inputPanel.primaryTextColor)
textString = NSAttributedString(string: "\(currencySymbol)\(postSuggestionState.price) for publishing anytime", font: textFont, textColor: self.theme.chat.inputPanel.primaryTextColor)
}
} else {
textString = NSAttributedString(string: "Tap to offer a price for publishing", font: textFont, textColor: self.theme.chat.inputPanel.primaryTextColor)
}
let mutableTextString = NSMutableAttributedString(attributedString: textString)
if let range = mutableTextString.string.range(of: "#"), let starImage = inlineTextStarImage {
final class RunDelegateData {
let ascent: CGFloat
let descent: CGFloat
let width: CGFloat
init(ascent: CGFloat, descent: CGFloat, width: CGFloat) {
self.ascent = ascent
self.descent = descent
self.width = width
}
for currency in [.stars, .ton] as [TelegramCurrency] {
let currencySymbol: String
let currencyImage: UIImage?
switch currency {
case .stars:
currencySymbol = "#"
currencyImage = inlineTextStarImage
case .ton:
currencySymbol = "$"
currencyImage = inlineTextTonImage
}
let runDelegateData = RunDelegateData(
ascent: Font.regular(15.0).ascender,
descent: Font.regular(15.0).descender,
width: starImage.size.width + 2.0
)
var callbacks = CTRunDelegateCallbacks(
version: kCTRunDelegateCurrentVersion,
dealloc: { dataRef in
Unmanaged<RunDelegateData>.fromOpaque(dataRef).release()
},
getAscent: { dataRef in
let data = Unmanaged<RunDelegateData>.fromOpaque(dataRef)
return data.takeUnretainedValue().ascent
},
getDescent: { dataRef in
let data = Unmanaged<RunDelegateData>.fromOpaque(dataRef)
return data.takeUnretainedValue().descent
},
getWidth: { dataRef in
let data = Unmanaged<RunDelegateData>.fromOpaque(dataRef)
return data.takeUnretainedValue().width
if let range = mutableTextString.string.range(of: currencySymbol), let currencyImage {
final class RunDelegateData {
let ascent: CGFloat
let descent: CGFloat
let width: CGFloat
init(ascent: CGFloat, descent: CGFloat, width: CGFloat) {
self.ascent = ascent
self.descent = descent
self.width = width
}
}
)
if let runDelegate = CTRunDelegateCreate(&callbacks, Unmanaged.passRetained(runDelegateData).toOpaque()) {
mutableTextString.addAttribute(NSAttributedString.Key(kCTRunDelegateAttributeName as String), value: runDelegate, range: NSRange(range, in: mutableTextString.string))
let runDelegateData = RunDelegateData(
ascent: Font.regular(15.0).ascender,
descent: Font.regular(15.0).descender,
width: currencyImage.size.width + 2.0
)
var callbacks = CTRunDelegateCallbacks(
version: kCTRunDelegateCurrentVersion,
dealloc: { dataRef in
Unmanaged<RunDelegateData>.fromOpaque(dataRef).release()
},
getAscent: { dataRef in
let data = Unmanaged<RunDelegateData>.fromOpaque(dataRef)
return data.takeUnretainedValue().ascent
},
getDescent: { dataRef in
let data = Unmanaged<RunDelegateData>.fromOpaque(dataRef)
return data.takeUnretainedValue().descent
},
getWidth: { dataRef in
let data = Unmanaged<RunDelegateData>.fromOpaque(dataRef)
return data.takeUnretainedValue().width
}
)
if let runDelegate = CTRunDelegateCreate(&callbacks, Unmanaged.passRetained(runDelegateData).toOpaque()) {
mutableTextString.addAttribute(NSAttributedString.Key(kCTRunDelegateAttributeName as String), value: runDelegate, range: NSRange(range, in: mutableTextString.string))
}
mutableTextString.addAttribute(.attachment, value: currencyImage, range: NSRange(range, in: mutableTextString.string))
mutableTextString.addAttribute(.foregroundColor, value: UIColor(rgb: 0xffffff), range: NSRange(range, in: mutableTextString.string))
mutableTextString.addAttribute(.baselineOffset, value: 1.0, range: NSRange(range, in: mutableTextString.string))
}
mutableTextString.addAttribute(.attachment, value: starImage, range: NSRange(range, in: mutableTextString.string))
mutableTextString.addAttribute(.foregroundColor, value: UIColor(rgb: 0xffffff), range: NSRange(range, in: mutableTextString.string))
mutableTextString.addAttribute(.baselineOffset, value: 1.0, range: NSRange(range, in: mutableTextString.string))
}
self.textNode.attributedText = mutableTextString

View File

@ -193,7 +193,7 @@ public final class ChatControllerInteraction: ChatControllerInteractionProtocol
public let sendEmoji: (String, ChatTextInputTextCustomEmojiAttribute, Bool) -> Void
public let sendGif: (FileMediaReference, UIView, CGRect, Bool, Bool) -> Bool
public let sendBotContextResultAsGif: (ChatContextResultCollection, ChatContextResult, UIView, CGRect, Bool, Bool) -> Bool
public let requestMessageActionCallback: (Message, MemoryBuffer?, Bool, Bool) -> Void
public let requestMessageActionCallback: (Message, MemoryBuffer?, Bool, Bool, Promise<Bool>?) -> Void
public let requestMessageActionUrlAuth: (String, MessageActionUrlSubject) -> Void
public let activateSwitchInline: (PeerId?, String, ReplyMarkupButtonAction.PeerTypes?) -> Void
public let openUrl: (OpenUrl) -> Void
@ -360,7 +360,7 @@ public final class ChatControllerInteraction: ChatControllerInteractionProtocol
sendEmoji: @escaping (String, ChatTextInputTextCustomEmojiAttribute, Bool) -> Void,
sendGif: @escaping (FileMediaReference, UIView, CGRect, Bool, Bool) -> Bool,
sendBotContextResultAsGif: @escaping (ChatContextResultCollection, ChatContextResult, UIView, CGRect, Bool, Bool) -> Bool,
requestMessageActionCallback: @escaping (Message, MemoryBuffer?, Bool, Bool) -> Void,
requestMessageActionCallback: @escaping (Message, MemoryBuffer?, Bool, Bool, Promise<Bool>?) -> Void,
requestMessageActionUrlAuth: @escaping (String, MessageActionUrlSubject) -> Void,
activateSwitchInline: @escaping (PeerId?, String, ReplyMarkupButtonAction.PeerTypes?) -> Void,
openUrl: @escaping (OpenUrl) -> Void,

View File

@ -430,7 +430,7 @@ final class PeerInfoSelectionPanelNode: ASDisplayNode {
}, addDoNotTranslateLanguage: { _ in
}, hideTranslationPanel: {
}, openPremiumGift: {
}, openSuggestPost: {
}, openSuggestPost: { _ in
}, openPremiumRequiredForMessaging: {
}, openStarsPurchase: { _ in
}, openMessagePayment: {
@ -3692,7 +3692,7 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro
return false
}, sendBotContextResultAsGif: { _, _, _, _, _, _ in
return false
}, requestMessageActionCallback: { _, _, _, _ in
}, requestMessageActionCallback: { _, _, _, _, _ in
}, requestMessageActionUrlAuth: { _, _ in
}, activateSwitchInline: { _, _, _ in
}, openUrl: { [weak self] url in

View File

@ -818,7 +818,7 @@ final class PeerSelectionControllerNode: ASDisplayNode {
}, addDoNotTranslateLanguage: { _ in
}, hideTranslationPanel: {
}, openPremiumGift: {
}, openSuggestPost: {
}, openSuggestPost: { _ in
}, openPremiumRequiredForMessaging: {
}, openStarsPurchase: { _ in
}, openMessagePayment: {

View File

@ -38,6 +38,7 @@ swift_library(
"//submodules/PasswordSetupUI",
"//submodules/TelegramUI/Components/PeerManagement/OwnershipTransferController",
"//submodules/TelegramUI/Components/ChatScheduleTimeController",
"//submodules/TelegramUI/Components/TabSelectorComponent",
],
visibility = [
"//visibility:public",

View File

@ -23,6 +23,7 @@ import TelegramStringFormatting
import UndoUI
import ListActionItemComponent
import ChatScheduleTimeController
import TabSelectorComponent
private let amountTag = GenericComponentViewTag()
@ -54,6 +55,7 @@ private final class SheetContent: CombinedComponent {
let closeButton = Child(Button.self)
let balance = Child(BalanceComponent.self)
let title = Child(Text.self)
let currencyToggle = Child(TabSelectorComponent.self)
let amountSection = Child(ListSectionComponent.self)
let amountAdditionalLabel = Child(MultilineTextComponent.self)
let timestampSection = Child(ListSectionComponent.self)
@ -80,7 +82,7 @@ private final class SheetContent: CombinedComponent {
let constrainedTitleWidth = context.availableSize.width - 16.0 * 2.0
if case let .suggestedPost(mode, _, _, _) = component.mode {
if case let .suggestedPost(mode, _, _, _, _) = component.mode {
switch mode {
case .sender:
let balance = balance.update(
@ -88,6 +90,7 @@ private final class SheetContent: CombinedComponent {
context: component.context,
theme: environment.theme,
strings: environment.strings,
currency: state.currency,
balance: state.balance,
alignment: .right
),
@ -96,7 +99,8 @@ private final class SheetContent: CombinedComponent {
)
let balanceFrame = CGRect(origin: CGPoint(x: context.availableSize.width - balance.size.width - 15.0, y: floor((56.0 - balance.size.height) * 0.5)), size: balance.size)
context.add(balance
.position(balanceFrame.center)
.anchorPoint(CGPoint(x: 1.0, y: 0.0))
.position(CGPoint(x: balanceFrame.maxX, y: balanceFrame.minY))
)
case .admin:
break
@ -199,7 +203,7 @@ private final class SheetContent: CombinedComponent {
minAmount = StarsAmount(value: minAmountValue, nanos: 0)
maxAmount = StarsAmount(value: resaleConfiguration.paidMessageMaxAmount, nanos: 0)
case let .suggestedPost(mode, _, _, _):
case let .suggestedPost(mode, _, _, _, _):
//TODO:localize
switch mode {
case .sender:
@ -207,7 +211,12 @@ private final class SheetContent: CombinedComponent {
case .admin:
titleString = "Suggest Changes"
}
amountTitle = "ENTER A PRICE IN STARS"
switch state.currency {
case .stars:
amountTitle = "ENTER A PRICE IN STARS"
case .ton:
amountTitle = "ENTER A PRICE IN TON"
}
amountPlaceholder = "Price"
minAmount = StarsAmount(value: 0, nanos: 0)
@ -280,6 +289,65 @@ private final class SheetContent: CombinedComponent {
)
}
if case let .suggestedPost(mode, _, _, _, _) = component.mode {
//TODO:localize
let selectedId: AnyHashable = state.currency == .stars ? AnyHashable(0 as Int) : AnyHashable(1 as Int)
let starsTitle: String
let tonTitle: String
switch mode {
case .sender:
starsTitle = "Offer Stars"
tonTitle = "Offer TON"
case .admin:
starsTitle = "Request Stars"
tonTitle = "Request TON"
}
let currencyToggle = currencyToggle.update(
component: TabSelectorComponent(
colors: TabSelectorComponent.Colors(
foreground: theme.list.itemSecondaryTextColor,
selection: theme.list.itemSecondaryTextColor.withMultipliedAlpha(0.15),
simple: true
),
customLayout: TabSelectorComponent.CustomLayout(
font: Font.medium(14.0),
spacing: 10.0
),
items: [
TabSelectorComponent.Item(
id: AnyHashable(0),
content: .component(AnyComponent(CurrencyTabItemComponent(icon: .stars, title: starsTitle, theme: theme)))
),
TabSelectorComponent.Item(
id: AnyHashable(1),
content: .component(AnyComponent(CurrencyTabItemComponent(icon: .ton, title: tonTitle, theme: theme)))
)
],
selectedId: selectedId,
setSelectedId: { [weak state] id in
guard let state else {
return
}
if id == AnyHashable(0) {
state.currency = .stars
} else {
state.currency = .ton
}
state.updated(transition: .spring(duration: 0.4))
}
),
availableSize: CGSize(width: context.availableSize.width - sideInset * 2.0, height: 100.0),
transition: context.transition
)
contentSize.height -= 17.0
let currencyToggleFrame = CGRect(origin: CGPoint(x: floor((context.availableSize.width - currencyToggle.size.width) * 0.5), y: contentSize.height), size: currencyToggle.size)
context.add(currencyToggle
.position(currencyToggle.size.centered(in: currencyToggleFrame).center))
contentSize.height += currencyToggle.size.height + 29.0
}
let amountFont = Font.regular(13.0)
let boldAmountFont = Font.semibold(13.0)
let amountTextColor = theme.list.freeTextColor
@ -356,18 +424,32 @@ private final class SheetContent: CombinedComponent {
text: .plain(amountInfoString),
maximumNumberOfLines: 0
))
case let .suggestedPost(mode, _, _, _):
case let .suggestedPost(mode, _, _, _, _):
switch mode {
case let .sender(channel):
//TODO:localize
let amountInfoString = NSAttributedString(attributedString: parseMarkdownIntoAttributedString("Choose how many Stars you want to offer \(channel.compactDisplayTitle) to publish this message.", attributes: amountMarkdownAttributes, textAlignment: .natural))
let string: String
switch state.currency {
case .stars:
string = "Choose how many Stars you want to offer \(channel.compactDisplayTitle) to publish this message."
case .ton:
string = "Choose how many TON you want to offer \(channel.compactDisplayTitle) to publish this message."
}
let amountInfoString = NSAttributedString(attributedString: parseMarkdownIntoAttributedString(string, attributes: amountMarkdownAttributes, textAlignment: .natural))
amountFooter = AnyComponent(MultilineTextComponent(
text: .plain(amountInfoString),
maximumNumberOfLines: 0
))
case .admin:
//TODO:localize
let amountInfoString = NSAttributedString(attributedString: parseMarkdownIntoAttributedString("Choose how many Stars you charge for the message.", attributes: amountMarkdownAttributes, textAlignment: .natural))
let string: String
switch state.currency {
case .stars:
string = "Choose how many Stars you charge for the message."
case .ton:
string = "Choose how many TON you charge for the message."
}
let amountInfoString = NSAttributedString(attributedString: parseMarkdownIntoAttributedString(string, attributes: amountMarkdownAttributes, textAlignment: .natural))
amountFooter = AnyComponent(MultilineTextComponent(
text: .plain(amountInfoString),
maximumNumberOfLines: 0
@ -396,11 +478,13 @@ private final class SheetContent: CombinedComponent {
textColor: theme.list.itemPrimaryTextColor,
secondaryColor: theme.list.itemSecondaryTextColor,
placeholderColor: theme.list.itemPlaceholderTextColor,
accentColor: theme.list.itemAccentColor,
value: state.amount?.value,
minValue: minAmount?.value,
maxValue: maxAmount?.value,
placeholderText: amountPlaceholder,
labelText: amountLabel,
currency: state.currency,
amountUpdated: { [weak state] amount in
state?.amount = amount.flatMap { StarsAmount(value: $0, nanos: 0) }
state?.updated()
@ -413,7 +497,7 @@ private final class SheetContent: CombinedComponent {
),
environment: {},
availableSize: CGSize(width: context.availableSize.width - sideInset * 2.0, height: .greatestFiniteMagnitude),
transition: context.transition
transition: .immediate
)
context.add(amountSection
.position(CGPoint(x: context.availableSize.width / 2.0, y: contentSize.height + amountSection.size.height / 2.0))
@ -431,7 +515,7 @@ private final class SheetContent: CombinedComponent {
.position(CGPoint(x: context.availableSize.width - amountAdditionalLabel.size.width / 2.0 - sideInset - 16.0, y: contentSize.height - amountAdditionalLabel.size.height / 2.0)))
}
if case let .suggestedPost(mode, _, _, _) = component.mode {
if case let .suggestedPost(mode, _, _, _, _) = component.mode {
contentSize.height += 24.0
//TODO:localize
@ -542,12 +626,19 @@ private final class SheetContent: CombinedComponent {
}
} else if case .paidMessages = component.mode {
buttonString = environment.strings.Stars_SendMessage_AdjustmentAction
} else if case let .suggestedPost(mode, _, _, _) = component.mode {
} else if case let .suggestedPost(mode, _, _, _, _) = component.mode {
//TODO:localize
switch mode {
case .sender:
if let amount = state.amount {
buttonString = "Offer # \(presentationStringsFormattedNumber(amount, environment.dateTimeFormat.groupingSeparator))"
let currencySymbol: String
switch state.currency {
case .stars:
currencySymbol = "#"
case .ton:
currencySymbol = "$"
}
buttonString = "Offer \(currencySymbol) \(presentationStringsFormattedNumber(amount, environment.dateTimeFormat.groupingSeparator))"
} else {
buttonString = "Offer for Free"
}
@ -563,6 +654,9 @@ private final class SheetContent: CombinedComponent {
if state.cachedStarImage == nil || state.cachedStarImage?.1 !== theme {
state.cachedStarImage = (generateTintedImage(image: UIImage(bundleImageName: "Item List/PremiumIcon"), color: theme.list.itemCheckColors.foregroundColor)!, theme)
}
if state.cachedTonImage == nil || state.cachedTonImage?.1 !== theme {
state.cachedTonImage = (generateTintedImage(image: UIImage(bundleImageName: "Ads/TonAbout"), color: theme.list.itemCheckColors.foregroundColor)!, theme)
}
let buttonAttributedString = NSMutableAttributedString(string: buttonString, font: Font.semibold(17.0), textColor: theme.list.itemCheckColors.foregroundColor, paragraphAlignment: .center)
if let range = buttonAttributedString.string.range(of: "#"), let starImage = state.cachedStarImage?.0 {
@ -571,6 +665,12 @@ private final class SheetContent: CombinedComponent {
buttonAttributedString.addAttribute(.baselineOffset, value: 1.5, range: NSRange(range, in: buttonAttributedString.string))
buttonAttributedString.addAttribute(.kern, value: 2.0, range: NSRange(range, in: buttonAttributedString.string))
}
if let range = buttonAttributedString.string.range(of: "$"), let tonImage = state.cachedTonImage?.0 {
buttonAttributedString.addAttribute(.attachment, value: tonImage, range: NSRange(range, in: buttonAttributedString.string))
buttonAttributedString.addAttribute(.foregroundColor, value: theme.list.itemCheckColors.foregroundColor, 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))
}
var isButtonEnabled = false
let amount = state.amount ?? StarsAmount.zero
@ -618,8 +718,8 @@ private final class SheetContent: CombinedComponent {
completion(amount.value)
case let .paidMessages(_, _, _, _, completion):
completion(amount.value)
case let .suggestedPost(_, _, _, completion):
completion(amount.value, state.timestamp)
case let .suggestedPost(_, _, _, _, completion):
completion(state.currency, amount.value, state.timestamp)
}
controller.dismissAnimated()
@ -653,6 +753,7 @@ private final class SheetContent: CombinedComponent {
fileprivate var component: SheetContent
fileprivate var amount: StarsAmount?
fileprivate var currency: TelegramCurrency
fileprivate var timestamp: Int32?
fileprivate var balance: StarsAmount?
@ -660,6 +761,7 @@ private final class SheetContent: CombinedComponent {
var cachedCloseImage: (UIImage, PresentationTheme)?
var cachedStarImage: (UIImage, PresentationTheme)?
var cachedTonImage: (UIImage, PresentationTheme)?
var cachedChevronImage: (UIImage, PresentationTheme)?
init(component: SheetContent) {
@ -668,6 +770,7 @@ private final class SheetContent: CombinedComponent {
self.component = component
var amount: StarsAmount?
var currency: TelegramCurrency = .stars
switch mode {
case let .withdraw(stats, _):
amount = StarsAmount(value: stats.balances.availableBalance.value, nanos: 0)
@ -681,13 +784,15 @@ private final class SheetContent: CombinedComponent {
amount = nil
case let .paidMessages(initialValue, _, _, _, _):
amount = StarsAmount(value: initialValue, nanos: 0)
case let .suggestedPost(_, initialValue, initialTimestamp, _):
case let .suggestedPost(_, currencyValue, initialValue, initialTimestamp, _):
currency = currencyValue
if initialValue != 0 {
amount = StarsAmount(value: initialValue, nanos: 0)
}
self.timestamp = initialTimestamp
}
self.currency = currency
self.amount = amount
super.init()
@ -696,7 +801,7 @@ private final class SheetContent: CombinedComponent {
switch self.mode {
case .reaction:
needsBalance = true
case let .suggestedPost(mode, _, _, _):
case let .suggestedPost(mode, _, _, _, _):
switch mode {
case .sender:
needsBalance = true
@ -854,7 +959,7 @@ public final class StarsWithdrawScreen: ViewControllerComponentContainer {
case reaction(Int64?, completion: (Int64) -> Void)
case starGiftResell(StarGift.UniqueGift, Bool, completion: (Int64) -> Void)
case paidMessages(current: Int64, minValue: Int64, fractionAfterCommission: Int, kind: StarsWithdrawalScreenSubject.PaidMessageKind, completion: (Int64) -> Void)
case suggestedPost(mode: SuggestedPostMode, price: Int64, timestamp: Int32?, completion: (Int64, Int32?) -> Void)
case suggestedPost(mode: SuggestedPostMode, currency: TelegramCurrency, price: Int64, timestamp: Int32?, completion: (TelegramCurrency, Int64, Int32?) -> Void)
}
private let context: AccountContext
@ -938,11 +1043,13 @@ private final class AmountFieldComponent: Component {
let textColor: UIColor
let secondaryColor: UIColor
let placeholderColor: UIColor
let accentColor: UIColor
let value: Int64?
let minValue: Int64?
let maxValue: Int64?
let placeholderText: String
let labelText: String?
let currency: TelegramCurrency
let amountUpdated: (Int64?) -> Void
let tag: AnyObject?
@ -950,22 +1057,26 @@ private final class AmountFieldComponent: Component {
textColor: UIColor,
secondaryColor: UIColor,
placeholderColor: UIColor,
accentColor: UIColor,
value: Int64?,
minValue: Int64?,
maxValue: Int64?,
placeholderText: String,
labelText: String?,
currency: TelegramCurrency,
amountUpdated: @escaping (Int64?) -> Void,
tag: AnyObject? = nil
) {
self.textColor = textColor
self.secondaryColor = secondaryColor
self.placeholderColor = placeholderColor
self.accentColor = accentColor
self.value = value
self.minValue = minValue
self.maxValue = maxValue
self.placeholderText = placeholderText
self.labelText = labelText
self.currency = currency
self.amountUpdated = amountUpdated
self.tag = tag
}
@ -980,6 +1091,9 @@ private final class AmountFieldComponent: Component {
if lhs.placeholderColor != rhs.placeholderColor {
return false
}
if lhs.accentColor != rhs.accentColor {
return false
}
if lhs.value != rhs.value {
return false
}
@ -995,6 +1109,9 @@ private final class AmountFieldComponent: Component {
if lhs.labelText != rhs.labelText {
return false
}
if lhs.currency != rhs.currency {
return false
}
return true
}
@ -1010,7 +1127,7 @@ private final class AmountFieldComponent: Component {
}
private let placeholderView: ComponentView<Empty>
private let iconView: UIImageView
private let icon = ComponentView<Empty>()
private let textField: TextFieldNodeView
private let labelView: ComponentView<Empty>
@ -1021,8 +1138,6 @@ private final class AmountFieldComponent: Component {
self.placeholderView = ComponentView<Empty>()
self.textField = TextFieldNodeView(frame: .zero)
self.labelView = ComponentView<Empty>()
self.iconView = UIImageView(image: UIImage(bundleImageName: "Premium/Stars/StarLarge"))
super.init(frame: frame)
@ -1030,7 +1145,6 @@ private final class AmountFieldComponent: Component {
self.textField.addTarget(self, action: #selector(self.textChanged(_:)), for: .editingChanged)
self.addSubview(self.textField)
self.addSubview(self.iconView)
}
required init?(coder: NSCoder) {
@ -1125,10 +1239,40 @@ private final class AmountFieldComponent: Component {
let sideInset: CGFloat = 16.0
var leftInset: CGFloat = 16.0
if let icon = self.iconView.image {
leftInset += icon.size.width + 6.0
self.iconView.frame = CGRect(origin: CGPoint(x: 15.0, y: floorToScreenPixels((size.height - icon.size.height) / 2.0)), size: icon.size)
let iconName: String
var iconTintColor: UIColor?
let iconMaxSize: CGSize?
var iconOffset = CGPoint()
switch component.currency {
case .stars:
iconName = "Premium/Stars/StarLarge"
iconMaxSize = CGSize(width: 22.0, height: 22.0)
case .ton:
iconName = "Ads/TonBig"
iconTintColor = component.accentColor
iconMaxSize = CGSize(width: 18.0, height: 18.0)
iconOffset = CGPoint(x: 3.0, y: 1.0)
}
let iconSize = self.icon.update(
transition: .immediate,
component: AnyComponent(BundleIconComponent(
name: iconName,
tintColor: iconTintColor,
maxSize: iconMaxSize
)),
environment: {},
containerSize: CGSize(width: 100.0, height: 100.0)
)
if let iconView = self.icon.view {
if iconView.superview == nil {
self.addSubview(iconView)
}
iconView.frame = CGRect(origin: CGPoint(x: iconOffset.x + 15.0, y: iconOffset.y - 1.0 + floorToScreenPixels((size.height - iconSize.height) / 2.0)), size: iconSize)
}
leftInset += 24.0 + 6.0
let placeholderSize = self.placeholderView.update(
transition: .easeInOut(duration: 0.2),
@ -1148,7 +1292,7 @@ private final class AmountFieldComponent: Component {
self.insertSubview(placeholderComponentView, at: 0)
}
placeholderComponentView.frame = CGRect(origin: CGPoint(x: leftInset, y: floorToScreenPixels((size.height - placeholderSize.height) / 2.0) + 1.0 - UIScreenPixel), size: placeholderSize)
placeholderComponentView.frame = CGRect(origin: CGPoint(x: leftInset, y: -1.0 + floorToScreenPixels((size.height - placeholderSize.height) / 2.0) + 1.0 - UIScreenPixel), size: placeholderSize)
placeholderComponentView.isHidden = !(self.textField.text ?? "").isEmpty
}
@ -1255,6 +1399,7 @@ private final class BalanceComponent: CombinedComponent {
let context: AccountContext
let theme: PresentationTheme
let strings: PresentationStrings
let currency: TelegramCurrency
let balance: StarsAmount?
let alignment: NSTextAlignment
@ -1262,12 +1407,14 @@ private final class BalanceComponent: CombinedComponent {
context: AccountContext,
theme: PresentationTheme,
strings: PresentationStrings,
currency: TelegramCurrency,
balance: StarsAmount?,
alignment: NSTextAlignment
) {
self.context = context
self.theme = theme
self.strings = strings
self.currency = currency
self.balance = balance
self.alignment = alignment
}
@ -1282,6 +1429,9 @@ private final class BalanceComponent: CombinedComponent {
if lhs.strings !== rhs.strings {
return false
}
if lhs.currency != rhs.currency {
return false
}
if lhs.balance != rhs.balance {
return false
}
@ -1324,11 +1474,25 @@ private final class BalanceComponent: CombinedComponent {
transition: .immediate
)
let iconSize = CGSize(width: 18.0, height: 18.0)
let iconSize: CGSize
let iconName: String
var iconOffset = CGPoint()
var iconTintColor: UIColor?
switch context.component.currency {
case .stars:
iconSize = CGSize(width: 18.0, height: 18.0)
iconName = "Premium/Stars/StarLarge"
case .ton:
iconSize = CGSize(width: 13.0, height: 13.0)
iconName = "Ads/TonBig"
iconTintColor = context.component.theme.list.itemAccentColor
iconOffset = CGPoint(x: 0.0, y: 2.33)
}
let icon = icon.update(
component: BundleIconComponent(
name: "Premium/Stars/StarLarge",
tintColor: nil
name: iconName,
tintColor: iconTintColor
),
availableSize: iconSize,
transition: context.transition
@ -1355,7 +1519,7 @@ private final class BalanceComponent: CombinedComponent {
)
context.add(
icon.position(
icon.size.centered(in: CGRect(origin: CGPoint(x: size.width - balance.size.width - icon.size.width - 1.0, y: title.size.height + titleSpacing), size: icon.size)).center
icon.size.centered(in: CGRect(origin: CGPoint(x: iconOffset.x + size.width - balance.size.width - icon.size.width - 1.0, y: iconOffset.y + title.size.height + titleSpacing), size: icon.size)).center
)
)
} else {
@ -1380,3 +1544,103 @@ private final class BalanceComponent: CombinedComponent {
}
}
}
private final class CurrencyTabItemComponent: Component {
typealias EnvironmentType = TabSelectorComponent.ItemEnvironment
enum Icon {
case stars
case ton
}
let icon: Icon
let title: String
let theme: PresentationTheme
init(
icon: Icon,
title: String,
theme: PresentationTheme
) {
self.icon = icon
self.title = title
self.theme = theme
}
static func ==(lhs: CurrencyTabItemComponent, rhs: CurrencyTabItemComponent) -> Bool {
if lhs.icon != rhs.icon {
return false
}
if lhs.title != rhs.title {
return false
}
if lhs.theme !== rhs.theme {
return false
}
return true
}
final class View: UIView {
private let title = ComponentView<Empty>()
private let icon = ComponentView<Empty>()
override init(frame: CGRect) {
super.init(frame: frame)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func update(component: CurrencyTabItemComponent, availableSize: CGSize, state: State, environment: Environment<EnvironmentType>, transition: ComponentTransition) -> CGSize {
let iconSpacing: CGFloat = 4.0
let iconSize = self.icon.update(
transition: .immediate,
component: AnyComponent(BundleIconComponent(
name: component.icon == .stars ? "Premium/Stars/StarLarge" : "Ads/TonAbout",
tintColor: component.icon == .stars ? nil : component.theme.list.itemAccentColor
)),
environment: {},
containerSize: CGSize(width: 100.0, height: 100.0)
)
let titleSize = self.title.update(
transition: .immediate,
component: AnyComponent(MultilineTextComponent(
text: .plain(NSAttributedString(string: component.title, font: Font.medium(14.0), textColor: .white))
)),
environment: {},
containerSize: CGSize(width: availableSize.width, height: 100.0)
)
let titleFrame = CGRect(origin: CGPoint(x: iconSize.width + iconSpacing, y: 0.0), size: titleSize)
if let titleView = self.title.view {
if titleView.superview == nil {
self.addSubview(titleView)
}
titleView.frame = titleFrame
transition.setTintColor(layer: titleView.layer, color: component.theme.list.freeTextColor.mixedWith(component.theme.list.itemPrimaryTextColor.withMultipliedAlpha(0.5), alpha: environment[TabSelectorComponent.ItemEnvironment.self].value.selectionFraction))
}
let iconFrame = CGRect(origin: CGPoint(x: 0.0, y: floorToScreenPixels((titleSize.height - iconSize.height) * 0.5)), size: iconSize)
if let iconView = self.icon.view {
if iconView.superview == nil {
self.addSubview(iconView)
}
iconView.frame = iconFrame
}
return CGSize(width: iconSize.width + iconSpacing + titleSize.width, height: titleSize.height)
}
}
func makeView() -> View {
return View(frame: CGRect())
}
func update(view: View, availableSize: CGSize, state: State, environment: Environment<EnvironmentType>, transition: ComponentTransition) -> CGSize {
return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition)
}
}

View File

@ -8,6 +8,21 @@ import TextFormat
import AccountContext
public final class TabSelectorComponent: Component {
public final class ItemEnvironment: Equatable {
public let selectionFraction: CGFloat
init(selectionFraction: CGFloat) {
self.selectionFraction = selectionFraction
}
public static func ==(lhs: ItemEnvironment, rhs: ItemEnvironment) -> Bool {
if lhs.selectionFraction != rhs.selectionFraction {
return false
}
return true
}
}
public struct Colors: Equatable {
public var foreground: UIColor
public var selection: UIColor
@ -43,15 +58,27 @@ public final class TabSelectorComponent: Component {
}
public struct Item: Equatable {
public enum Content: Equatable {
case text(String)
case component(AnyComponent<ItemEnvironment>)
}
public var id: AnyHashable
public var title: String
public var content: Content
public init(
id: AnyHashable,
content: Content
) {
self.id = id
self.content = content
}
public init(
id: AnyHashable,
title: String
) {
self.id = id
self.title = title
self.init(id: id, content: .text(title))
}
}
@ -227,16 +254,21 @@ public final class TabSelectorComponent: Component {
selectionFraction = item.id == component.selectedId ? 1.0 : 0.0
}
var useSelectionFraction = isLineSelection
if case .component = item.content {
useSelectionFraction = true
}
let itemSize = itemView.title.update(
transition: .immediate,
component: AnyComponent(PlainButtonComponent(
content: AnyComponent(ItemComponent(
context: component.context,
text: item.title,
content: item.content,
font: itemFont,
color: component.colors.foreground,
selectedColor: component.colors.selection,
selectionFraction: isLineSelection ? selectionFraction : 0.0
selectionFraction: useSelectionFraction ? selectionFraction : 0.0
)),
effectAlignment: .center,
minSize: nil,
@ -379,7 +411,7 @@ extension CGRect {
private final class ItemComponent: CombinedComponent {
let context: AccountContext?
let text: String
let content: TabSelectorComponent.Item.Content
let font: UIFont
let color: UIColor
let selectedColor: UIColor
@ -387,14 +419,14 @@ private final class ItemComponent: CombinedComponent {
init(
context: AccountContext?,
text: String,
content: TabSelectorComponent.Item.Content,
font: UIFont,
color: UIColor,
selectedColor: UIColor,
selectionFraction: CGFloat
) {
self.context = context
self.text = text
self.content = content
self.font = font
self.color = color
self.selectedColor = selectedColor
@ -405,7 +437,7 @@ private final class ItemComponent: CombinedComponent {
if lhs.context !== rhs.context {
return false
}
if lhs.text != rhs.text {
if lhs.content != rhs.content {
return false
}
if lhs.font != rhs.font {
@ -426,55 +458,73 @@ private final class ItemComponent: CombinedComponent {
static var body: Body {
let title = Child(MultilineTextWithEntitiesComponent.self)
let selectedTitle = Child(MultilineTextWithEntitiesComponent.self)
let contentComponent = Child(environment: TabSelectorComponent.ItemEnvironment.self)
return { context in
let component = context.component
let attributedTitle = NSMutableAttributedString(string: component.text, font: component.font, textColor: component.color)
var range = (attributedTitle.string as NSString).range(of: "⭐️")
if range.location != NSNotFound {
attributedTitle.addAttribute(ChatTextInputAttributes.customEmoji, value: ChatTextInputTextCustomEmojiAttribute(interactivelySelectedFromPackId: nil, fileId: 0, file: nil, custom: .stars(tinted: false)), range: range)
switch component.content {
case let .text(text):
let attributedTitle = NSMutableAttributedString(string: text, font: component.font, textColor: component.color)
var range = (attributedTitle.string as NSString).range(of: "⭐️")
if range.location != NSNotFound {
attributedTitle.addAttribute(ChatTextInputAttributes.customEmoji, value: ChatTextInputTextCustomEmojiAttribute(interactivelySelectedFromPackId: nil, fileId: 0, file: nil, custom: .stars(tinted: false)), range: range)
}
let title = title.update(
component: MultilineTextWithEntitiesComponent(
context: component.context,
animationCache: component.context?.animationCache,
animationRenderer: component.context?.animationRenderer,
placeholderColor: .white,
text: .plain(attributedTitle)
),
availableSize: context.availableSize,
transition: .immediate
)
context.add(title
.position(CGPoint(x: title.size.width / 2.0, y: title.size.height / 2.0))
.opacity(1.0 - component.selectionFraction)
)
let selectedAttributedTitle = NSMutableAttributedString(string: text, font: component.font, textColor: component.selectedColor)
range = (selectedAttributedTitle.string as NSString).range(of: "⭐️")
if range.location != NSNotFound {
selectedAttributedTitle.addAttribute(ChatTextInputAttributes.customEmoji, value: ChatTextInputTextCustomEmojiAttribute(interactivelySelectedFromPackId: nil, fileId: 0, file: nil, custom: .stars(tinted: false)), range: range)
}
let selectedTitle = selectedTitle.update(
component: MultilineTextWithEntitiesComponent(
context: nil,
animationCache: nil,
animationRenderer: nil,
placeholderColor: .white,
text: .plain(selectedAttributedTitle)
),
availableSize: context.availableSize,
transition: .immediate
)
context.add(selectedTitle
.position(CGPoint(x: selectedTitle.size.width / 2.0, y: selectedTitle.size.height / 2.0))
.opacity(component.selectionFraction)
)
return title.size
case let .component(contentComponentValue):
let content = contentComponent.update(
contentComponentValue,
environment: {
TabSelectorComponent.ItemEnvironment(selectionFraction: component.selectionFraction)
},
availableSize: context.availableSize,
transition: .immediate
)
context.add(content
.position(CGPoint(x: content.size.width / 2.0, y: content.size.height / 2.0))
)
return content.size
}
let title = title.update(
component: MultilineTextWithEntitiesComponent(
context: component.context,
animationCache: component.context?.animationCache,
animationRenderer: component.context?.animationRenderer,
placeholderColor: .white,
text: .plain(attributedTitle)
),
availableSize: context.availableSize,
transition: .immediate
)
context.add(title
.position(CGPoint(x: title.size.width / 2.0, y: title.size.height / 2.0))
.opacity(1.0 - component.selectionFraction)
)
let selectedAttributedTitle = NSMutableAttributedString(string: component.text, font: component.font, textColor: component.selectedColor)
range = (selectedAttributedTitle.string as NSString).range(of: "⭐️")
if range.location != NSNotFound {
selectedAttributedTitle.addAttribute(ChatTextInputAttributes.customEmoji, value: ChatTextInputTextCustomEmojiAttribute(interactivelySelectedFromPackId: nil, fileId: 0, file: nil, custom: .stars(tinted: false)), range: range)
}
let selectedTitle = selectedTitle.update(
component: MultilineTextWithEntitiesComponent(
context: nil,
animationCache: nil,
animationRenderer: nil,
placeholderColor: .white,
text: .plain(selectedAttributedTitle)
),
availableSize: context.availableSize,
transition: .immediate
)
context.add(selectedTitle
.position(CGPoint(x: selectedTitle.size.width / 2.0, y: selectedTitle.size.height / 2.0))
.opacity(component.selectionFraction)
)
return title.size
}
}
}

View File

@ -13,9 +13,11 @@ public final class TextLoadingEffectView: UIView {
private let maskContentsView: UIView
private let maskHighlightNode: LinkHighlightingNode
private var maskShapeLayer: SimpleShapeLayer?
private let maskBorderContentsView: UIView
private let maskBorderHighlightNode: LinkHighlightingNode
private var maskBorderShapeLayer: SimpleShapeLayer?
private let backgroundView: UIImageView
private let borderBackgroundView: UIImageView
@ -201,4 +203,63 @@ public final class TextLoadingEffectView: UIView {
self.updateAnimations(size: maskFrame.size)
}
}
public func update(color: UIColor, rect: CGRect, path: CGPath) {
let maskShapeLayer: SimpleShapeLayer
if let current = self.maskShapeLayer {
maskShapeLayer = current
} else {
maskShapeLayer = SimpleShapeLayer()
maskShapeLayer.fillColor = UIColor.white.cgColor
self.maskShapeLayer = maskShapeLayer
}
let maskBorderShapeLayer: SimpleShapeLayer
if let current = self.maskBorderShapeLayer {
maskBorderShapeLayer = current
} else {
maskBorderShapeLayer = SimpleShapeLayer()
maskBorderShapeLayer.fillColor = nil
maskBorderShapeLayer.strokeColor = UIColor.white.cgColor
maskBorderShapeLayer.lineWidth = 4.0
self.maskBorderShapeLayer = maskBorderShapeLayer
}
maskShapeLayer.path = path
maskBorderShapeLayer.path = path
if self.maskContentsView.layer.mask !== maskShapeLayer {
self.maskContentsView.layer.mask = maskShapeLayer
}
if self.maskBorderContentsView.layer.mask !== maskBorderShapeLayer {
self.maskBorderContentsView.layer.mask = maskBorderShapeLayer
}
let maskFrame = CGRect(origin: CGPoint(), size: rect.size)
self.gradientWidth = 260.0
self.duration = 0.7
self.maskContentsView.backgroundColor = .clear
self.backgroundView.alpha = 0.25
self.backgroundView.tintColor = color
self.borderBackgroundView.alpha = 0.5
self.borderBackgroundView.tintColor = color
self.maskContentsView.frame = maskFrame
self.maskBorderContentsView.frame = maskFrame
maskShapeLayer.frame = CGRect(origin: CGPoint(x: -maskFrame.minX, y: -maskFrame.minY), size: CGSize())
if self.size != maskFrame.size {
self.size = maskFrame.size
self.backgroundView.frame = CGRect(origin: CGPoint(x: -self.gradientWidth, y: 0.0), size: CGSize(width: self.gradientWidth, height: maskFrame.height))
self.borderBackgroundView.frame = CGRect(origin: CGPoint(x: -self.gradientWidth, y: 0.0), size: CGSize(width: self.gradientWidth, height: maskFrame.height))
self.updateAnimations(size: maskFrame.size)
}
}
}

View File

@ -4131,23 +4131,80 @@ extension ChatControllerImpl {
})
self.push(controller)
}
}, openSuggestPost: { [weak self] in
}, openSuggestPost: { [weak self] message in
guard let self else {
return
}
self.updateChatPresentationInterfaceState(interactive: true, { state in
var state = state
state = state.updatedInterfaceState { interfaceState in
var interfaceState = interfaceState
interfaceState = interfaceState.withUpdatedPostSuggestionState(ChatInterfaceState.PostSuggestionState(
editingOriginalMessageId: nil,
price: 0,
timestamp: nil
))
return interfaceState
}
return state
})
if let message {
let attribute = message.attributes.first(where: { $0 is SuggestedPostMessageAttribute }) as? SuggestedPostMessageAttribute
self.updateChatPresentationInterfaceState(interactive: true, { state in
var entities: [MessageTextEntity] = []
for attribute in message.attributes {
if let attribute = attribute as? TextEntitiesMessageAttribute {
entities = attribute.entities
break
}
}
var inputTextMaxLength: Int32 = 4096
var webpageUrl: String?
for media in message.media {
if media is TelegramMediaImage || media is TelegramMediaFile {
inputTextMaxLength = self.context.userLimits.maxCaptionLength
} else if let webpage = media as? TelegramMediaWebpage, case let .Loaded(content) = webpage.content {
webpageUrl = content.url
}
}
let inputText = chatInputStateStringWithAppliedEntities(message.text, entities: entities)
var disableUrlPreviews: [String] = []
if webpageUrl == nil {
disableUrlPreviews = detectUrls(inputText)
}
var updated = state.updatedInterfaceState { interfaceState in
return interfaceState.withUpdatedEditMessage(ChatEditMessageState(messageId: message.id, inputState: ChatTextInputState(inputText: inputText), disableUrlPreviews: disableUrlPreviews, inputTextMaxLength: inputTextMaxLength, mediaCaptionIsAbove: nil))
}
let (updatedState, updatedPreviewQueryState) = updatedChatEditInterfaceMessageState(context: self.context, state: updated, message: message)
updated = updatedState
self.editingUrlPreviewQueryState?.1.dispose()
self.editingUrlPreviewQueryState = updatedPreviewQueryState
updated = updated.updatedInputMode({ _ in
return .text
})
updated = updated.updatedShowCommands(false)
updated = updated.updatedInterfaceState { interfaceState in
var interfaceState = interfaceState
interfaceState = interfaceState.withUpdatedPostSuggestionState(ChatInterfaceState.PostSuggestionState(
editingOriginalMessageId: message.id,
currency: attribute?.currency ?? .stars,
price: attribute?.amount ?? 0,
timestamp: attribute?.timestamp
))
return interfaceState
}
return updated
})
} else {
self.updateChatPresentationInterfaceState(interactive: true, { state in
var state = state
state = state.updatedInterfaceState { interfaceState in
var interfaceState = interfaceState
interfaceState = interfaceState.withUpdatedPostSuggestionState(ChatInterfaceState.PostSuggestionState(
editingOriginalMessageId: nil,
currency: .stars,
price: 0,
timestamp: nil
))
return interfaceState
}
return state
})
}
self.presentSuggestPostOptions()
}, openPremiumRequiredForMessaging: { [weak self] in
guard let self else {

View File

@ -986,13 +986,35 @@ extension ChatControllerImpl {
guard let postSuggestionState = self.presentationInterfaceState.interfaceState.postSuggestionState else {
return
}
self.push(self.context.sharedContext.makeStarsWithdrawalScreen(
context: self.context,
subject: .postSuggestion(
let subject: StarsWithdrawalScreenSubject
if postSuggestionState.editingOriginalMessageId != nil {
subject = .postSuggestionModification(currency: postSuggestionState.currency, current: StarsAmount(value: postSuggestionState.price, nanos: 0), timestamp: postSuggestionState.timestamp, completion: { [weak self] currency, price, timestamp in
guard let self else {
return
}
self.updateChatPresentationInterfaceState(interactive: true, { state in
var state = state
state = state.updatedInterfaceState { interfaceState in
var interfaceState = interfaceState
interfaceState = interfaceState.withUpdatedPostSuggestionState(ChatInterfaceState.PostSuggestionState(
editingOriginalMessageId: interfaceState.postSuggestionState?.editingOriginalMessageId,
currency: currency,
price: price,
timestamp: timestamp
))
return interfaceState
}
return state
})
})
} else {
subject = .postSuggestion(
channel: .channel(channel),
currency: postSuggestionState.currency,
current: StarsAmount(value: postSuggestionState.price, nanos: 0),
timestamp: postSuggestionState.timestamp,
completion: { [weak self] price, timestamp in
completion: { [weak self] currency, price, timestamp in
guard let self else {
return
}
@ -1002,6 +1024,7 @@ extension ChatControllerImpl {
var interfaceState = interfaceState
interfaceState = interfaceState.withUpdatedPostSuggestionState(ChatInterfaceState.PostSuggestionState(
editingOriginalMessageId: interfaceState.postSuggestionState?.editingOriginalMessageId,
currency: currency,
price: price,
timestamp: timestamp
))
@ -1011,6 +1034,11 @@ extension ChatControllerImpl {
})
}
)
}
self.push(self.context.sharedContext.makeStarsWithdrawalScreen(
context: self.context,
subject: subject
))
}
}

View File

@ -2305,7 +2305,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
strongSelf.enqueueChatContextResult(collection, result, hideVia: true, closeMediaInput: true, silentPosting: silentPosting, resetTextInputState: resetTextInputState)
return true
}, requestMessageActionCallback: { [weak self] message, data, isGame, requiresPassword in
}, requestMessageActionCallback: { [weak self] message, data, isGame, requiresPassword, progress in
guard let strongSelf = self else {
return
}
@ -2337,7 +2337,10 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
return
}
if let value {
let _ = self.context.engine.messages.monoforumPerformSuggestedPostAction(id: message.id, action: .reject(comment: value.isEmpty ? nil : value)).startStandalone()
progress?.set(.single(true))
let _ = self.context.engine.messages.monoforumPerformSuggestedPostAction(id: message.id, action: .reject(comment: value.isEmpty ? nil : value)).startStandalone(completed: {
progress?.set(.single(false))
})
}
})
strongSelf.present(promptController, in: .window(.root))
@ -2348,7 +2351,10 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
guard let strongSelf else {
return
}
let _ = strongSelf.context.engine.messages.monoforumPerformSuggestedPostAction(id: message.id, action: .approve(timestamp: time != 0 ? time : nil)).startStandalone()
progress?.set(.single(true))
let _ = strongSelf.context.engine.messages.monoforumPerformSuggestedPostAction(id: message.id, action: .approve(timestamp: time != 0 ? time : nil)).startStandalone(completed: {
progress?.set(.single(false))
})
})
strongSelf.view.endEditing(true)
strongSelf.present(controller, in: .window(.root))
@ -2358,55 +2364,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
let _ = strongSelf.context.engine.messages.monoforumPerformSuggestedPostAction(id: message.id, action: .approve(timestamp: timestamp)).startStandalone()
}
case 2:
strongSelf.updateChatPresentationInterfaceState(interactive: true, { state in
var entities: [MessageTextEntity] = []
for attribute in message.attributes {
if let attribute = attribute as? TextEntitiesMessageAttribute {
entities = attribute.entities
break
}
}
var inputTextMaxLength: Int32 = 4096
var webpageUrl: String?
for media in message.media {
if media is TelegramMediaImage || media is TelegramMediaFile {
inputTextMaxLength = strongSelf.context.userLimits.maxCaptionLength
} else if let webpage = media as? TelegramMediaWebpage, case let .Loaded(content) = webpage.content {
webpageUrl = content.url
}
}
let inputText = chatInputStateStringWithAppliedEntities(message.text, entities: entities)
var disableUrlPreviews: [String] = []
if webpageUrl == nil {
disableUrlPreviews = detectUrls(inputText)
}
var updated = state.updatedInterfaceState { interfaceState in
return interfaceState.withUpdatedEditMessage(ChatEditMessageState(messageId: messageId, inputState: ChatTextInputState(inputText: inputText), disableUrlPreviews: disableUrlPreviews, inputTextMaxLength: inputTextMaxLength, mediaCaptionIsAbove: nil))
}
let (updatedState, updatedPreviewQueryState) = updatedChatEditInterfaceMessageState(context: strongSelf.context, state: updated, message: message)
updated = updatedState
strongSelf.editingUrlPreviewQueryState?.1.dispose()
strongSelf.editingUrlPreviewQueryState = updatedPreviewQueryState
updated = updated.updatedInputMode({ _ in
return .text
})
updated = updated.updatedShowCommands(false)
updated = updated.updatedInterfaceState { interfaceState in
var interfaceState = interfaceState
interfaceState = interfaceState.withUpdatedPostSuggestionState(ChatInterfaceState.PostSuggestionState(
editingOriginalMessageId: message.id,
price: attribute.amount,
timestamp: attribute.timestamp
))
return interfaceState
}
return updated
})
strongSelf.interfaceInteraction?.openSuggestPost(message)
default:
break
}
@ -2555,8 +2513,10 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
strongSelf.present(controller, in: .window(.root))
}))
} else {
progress?.set(.single(true))
strongSelf.messageActionCallbackDisposable.set(((context.engine.messages.requestMessageActionCallback(messageId: messageId, isGame: isGame, password: nil, data: data)
|> afterDisposed {
progress?.set(.single(false))
updateProgress()
})
|> deliverOnMainQueue).startStrict(next: { result in
@ -8040,6 +8000,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
if let postSuggestionState = self.presentationInterfaceState.interfaceState.postSuggestionState {
if attributes.first(where: { $0 is SuggestedPostMessageAttribute }) == nil {
attributes.append(SuggestedPostMessageAttribute(
currency: postSuggestionState.currency,
amount: postSuggestionState.price,
timestamp: postSuggestionState.timestamp,
state: nil

View File

@ -1069,7 +1069,7 @@ func contextMenuForChatPresentationInterfaceState(chatPresentationInterfaceState
controllerInteraction.displayUndo(.info(title: presentationData.strings.Notifications_UploadError_TooLong_Title(fileName).string, text: presentationData.strings.Notifications_UploadError_TooLong_Text(stringForDuration(Int32(settings.maxDuration))).string, timeout: nil, customUndoText: nil))
} else {
let _ = (context.engine.peers.saveNotificationSound(file: .message(message: MessageReference(message), media: file))
|> deliverOnMainQueue).startStandalone(completed: {
|> deliverOnMainQueue).startStandalone(completed: {
controllerInteraction.displayUndo(.notificationSoundAdded(title: presentationData.strings.Notifications_UploadSuccess_Title, text: presentationData.strings.Notifications_SaveSuccess_Text, action: {
controllerInteraction.navigationController()?.pushViewController(notificationsAndSoundsController(context: context, exceptionsList: nil))
}))
@ -1299,8 +1299,8 @@ func contextMenuForChatPresentationInterfaceState(chatPresentationInterfaceState
for media in message.media {
if let image = media as? TelegramMediaImage, let largest = largestImageRepresentation(image.representations) {
let _ = (context.account.postbox.mediaBox.resourceData(largest.resource, option: .incremental(waitUntilFetchStatus: false))
|> take(1)
|> deliverOnMainQueue).startStandalone(next: { data in
|> take(1)
|> deliverOnMainQueue).startStandalone(next: { data in
if data.complete, let imageData = try? Data(contentsOf: URL(fileURLWithPath: data.path)) {
if let image = UIImage(data: imageData) {
if !messageText.isEmpty {
@ -1384,7 +1384,7 @@ func contextMenuForChatPresentationInterfaceState(chatPresentationInterfaceState
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Save"), color: theme.actionSheet.primaryTextColor)
}, action: { _, f in
let _ = (saveToCameraRoll(context: context, postbox: context.account.postbox, userLocation: .peer(message.id.peerId), mediaReference: mediaReference)
|> deliverOnMainQueue).startStandalone(completed: {
|> deliverOnMainQueue).startStandalone(completed: {
Queue.mainQueue().after(0.2) {
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
controllerInteraction.presentControllerInCurrent(UndoOverlayController(presentationData: presentationData, content: .mediaSaved(text: isVideo ? presentationData.strings.Gallery_VideoSaved : presentationData.strings.Gallery_ImageSaved), elevatedLayout: false, animateInAsReplacement: false, action: { _ in return true }), nil)
@ -1483,7 +1483,7 @@ func contextMenuForChatPresentationInterfaceState(chatPresentationInterfaceState
} else {
isMigrated = false
}
var activePoll: TelegramMediaPoll?
var activeTodo: TelegramMediaTodo?
for media in message.media {
@ -1510,6 +1510,17 @@ func contextMenuForChatPresentationInterfaceState(chatPresentationInterfaceState
}
})))
}
if let message = messages.first, message.id.namespace == Namespaces.Message.Cloud, let channel = message.peers[message.id.peerId] as? TelegramChannel, channel.isMonoForum {
//TODO:localize
actions.append(.action(ContextMenuActionItem(text: "Suggest a Post", icon: { theme in
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Customize"), color: theme.actionSheet.primaryTextColor)
}, action: { c, _ in
c?.dismiss(completion: {
interfaceInteraction.openSuggestPost(message)
})
})))
}
if let activePoll = activePoll, let voters = activePoll.results.voters {
var hasSelected = false

View File

@ -935,10 +935,29 @@ final class ChatPinnedMessageTitlePanelNode: ChatTitleAccessoryPanelNode {
controllerInteraction.shareAccountContact()
return
case .openWebApp:
controllerInteraction.requestMessageActionCallback(message, nil, true, false)
let progressPromise = Promise<Bool>()
controllerInteraction.requestMessageActionCallback(message, nil, true, false, progressPromise)
self.progressDisposable?.dispose()
self.progressDisposable = (progressPromise.get()
|> deliverOnMainQueue).startStrict(next: { [weak self] value in
guard let self else {
return
}
self.updateIsLoading(isLoading: value)
})
return
case let .callback(requiresPassword, data):
controllerInteraction.requestMessageActionCallback(message, data, false, requiresPassword)
let progressPromise = Promise<Bool>()
controllerInteraction.requestMessageActionCallback(message, data, false, requiresPassword, progressPromise)
self.progressDisposable?.dispose()
self.progressDisposable = (progressPromise.get()
|> deliverOnMainQueue).startStrict(next: { [weak self] value in
guard let self else {
return
}
self.updateIsLoading(isLoading: value)
})
return
case let .switchInline(samePeer, query, peerTypes):
var botPeer: Peer?

View File

@ -46,6 +46,7 @@ import AnimatedCountLabelNode
import TelegramStringFormatting
import TextNodeWithEntities
import DeviceModel
import PhotoResources
private let accessoryButtonFont = Font.medium(14.0)
private let counterFont = Font.with(size: 14.0, design: .regular, traits: [.monospacedNumbers])
@ -565,7 +566,7 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate, Ch
let attachmentButton: HighlightableButtonNode
let attachmentButtonDisabledNode: HighlightableButtonNode
var attachmentImageNode: TransformImageNode?
let searchLayoutClearButton: HighlightableButton
private let searchLayoutClearImageNode: ASImageNode
@ -1563,6 +1564,7 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate, Ch
} else {
attachmentButtonAlpha = 0.0
}
transition.updateAlpha(layer: self.attachmentButton.layer, alpha: attachmentButtonAlpha)
self.attachmentButton.isEnabled = isMediaEnabled && !isRecording
self.attachmentButton.accessibilityTraits = (!isSlowmodeActive || isMediaEnabled) ? [.button] : [.button, .notEnabled]
@ -2469,9 +2471,66 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate, Ch
leftInset += leftMenuInset
transition.updateFrame(layer: self.attachmentButton.layer, frame: CGRect(origin: CGPoint(x: attachmentButtonX, y: hideOffset.y + panelHeight - minimalHeight), size: CGSize(width: 40.0, height: minimalHeight)))
let attachmentButtonFrame = CGRect(origin: CGPoint(x: attachmentButtonX, y: hideOffset.y + panelHeight - minimalHeight), size: CGSize(width: 40.0, height: minimalHeight))
transition.updateFrame(layer: self.attachmentButton.layer, frame: attachmentButtonFrame)
transition.updateFrame(node: self.attachmentButtonDisabledNode, frame: self.attachmentButton.frame)
if let context = self.context, let interfaceState = self.presentationInterfaceState, let editMessageState = interfaceState.editMessageState, let updatedMediaReference = editMessageState.mediaReference {
let attachmentImageNode: TransformImageNode
if let current = self.attachmentImageNode {
attachmentImageNode = current
} else {
attachmentImageNode = TransformImageNode()
attachmentImageNode.isUserInteractionEnabled = false
self.attachmentImageNode = attachmentImageNode
self.addSubnode(attachmentImageNode)
}
let attachmentImageSize = CGSize(width: 26.0, height: 26.0)
let attachmentImageFrame = CGRect(origin: CGPoint(x: attachmentButtonFrame.minX + floorToScreenPixels((40.0 - attachmentImageSize.width) * 0.5), y: attachmentButtonFrame.minY + floorToScreenPixels((attachmentButtonFrame.height - attachmentImageSize.height) * 0.5)), size: attachmentImageSize)
attachmentImageNode.frame = attachmentImageFrame
let hasSpoiler: Bool = false
var updateImageSignal: Signal<(TransformImageArguments) -> DrawingContext?, NoError>?
var imageDimensions: CGSize?
if let imageReference = updatedMediaReference.concrete(TelegramMediaImage.self) {
imageDimensions = imageReference.media.representations.last?.dimensions.cgSize
updateImageSignal = chatMessagePhotoThumbnail(account: context.account, userLocation: .other, photoReference: imageReference, blurred: hasSpoiler)
} else if let fileReference = updatedMediaReference.concrete(TelegramMediaFile.self) {
imageDimensions = fileReference.media.dimensions?.cgSize
if fileReference.media.isVideo {
updateImageSignal = chatMessageVideoThumbnail(account: context.account, userLocation: .other, fileReference: fileReference, blurred: hasSpoiler)
} else if let iconImageRepresentation = smallestImageRepresentation(fileReference.media.previewRepresentations) {
updateImageSignal = chatWebpageSnippetFile(account: context.account, userLocation: .other, mediaReference: fileReference.abstract, representation: iconImageRepresentation)
}
}
//TODO:release catch updates
if let updateImageSignal {
attachmentImageNode.setSignal(updateImageSignal)
}
let makeAttachmentImageNodeLayout = attachmentImageNode.asyncLayout()
let isRoundImage = !"".isEmpty
if let imageDimensions {
let boundingSize = attachmentImageSize
var radius: CGFloat = 4.0
var imageSize = imageDimensions.aspectFilled(boundingSize)
if isRoundImage {
radius = floor(boundingSize.width / 2.0)
imageSize.width += 2.0
imageSize.height += 2.0
}
let applyImage = makeAttachmentImageNodeLayout(TransformImageArguments(corners: ImageCorners(radius: radius), imageSize: imageSize, boundingSize: boundingSize, intrinsicInsets: UIEdgeInsets()))
applyImage()
}
} else if let attachmentImageNode = self.attachmentImageNode {
self.attachmentImageNode = nil
attachmentImageNode.removeFromSupernode()
}
var composeButtonsOffset: CGFloat = 0.0
if self.extendedSearchLayout {
composeButtonsOffset = 44.0
@ -4714,7 +4773,7 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate, Ch
case .gift:
self.interfaceInteraction?.openPremiumGift()
case .suggestPost:
self.interfaceInteraction?.openSuggestPost()
self.interfaceInteraction?.openSuggestPost(nil)
}
break
}

View File

@ -95,7 +95,7 @@ final class OverlayAudioPlayerControllerNode: ViewControllerTracingNode, ASGestu
return false
}, sendBotContextResultAsGif: { _, _, _, _, _, _ in
return false
}, requestMessageActionCallback: { _, _, _, _ in
}, requestMessageActionCallback: { _, _, _, _, _ in
}, requestMessageActionUrlAuth: { _, _ in
}, activateSwitchInline: { _, _, _ in
}, openUrl: { _ in

View File

@ -2319,7 +2319,7 @@ public final class SharedAccountContextImpl: SharedAccountContext {
clickThroughMessage?(view, location)
}, toggleMessagesSelection: { _, _ in }, sendCurrentMessage: { _, _ in }, sendMessage: { _ in }, sendSticker: { _, _, _, _, _, _, _, _, _ in return false }, sendEmoji: { _, _, _ in }, sendGif: { _, _, _, _, _ in return false }, sendBotContextResultAsGif: { _, _, _, _, _, _ in
return false
}, requestMessageActionCallback: { _, _, _, _ in }, requestMessageActionUrlAuth: { _, _ in }, activateSwitchInline: { _, _, _ in }, openUrl: { _ in }, shareCurrentLocation: {}, shareAccountContact: {}, sendBotCommand: { _, _ in }, openInstantPage: { _, _ in }, openWallpaper: { _ in }, openTheme: { _ in }, openHashtag: { _, _ in }, updateInputState: { _ in }, updateInputMode: { _ in }, openMessageShareMenu: { _ in
}, requestMessageActionCallback: { _, _, _, _, _ in }, requestMessageActionUrlAuth: { _, _ in }, activateSwitchInline: { _, _, _ in }, openUrl: { _ in }, shareCurrentLocation: {}, shareAccountContact: {}, sendBotCommand: { _, _ in }, openInstantPage: { _, _ in }, openWallpaper: { _ in }, openTheme: { _ in }, openHashtag: { _, _ in }, updateInputState: { _ in }, updateInputMode: { _ in }, openMessageShareMenu: { _ in
}, presentController: { _, _ in
}, presentControllerInCurrent: { _, _ in
}, navigationController: {
@ -3734,10 +3734,10 @@ public final class SharedAccountContextImpl: SharedAccountContext {
mode = .accountWithdraw(completion: completion)
case let .enterAmount(current, minValue, fractionAfterCommission, kind, completion):
mode = .paidMessages(current: current.value, minValue: minValue.value, fractionAfterCommission: fractionAfterCommission, kind: kind, completion: completion)
case let .postSuggestion(channel, current, timestamp, completion):
mode = .suggestedPost(mode: .sender(channel: channel), price: current.value, timestamp: timestamp, completion: completion)
case let .postSuggestionModification(current, timestamp, completion):
mode = .suggestedPost(mode: .admin, price: current.value, timestamp: timestamp, completion: completion)
case let .postSuggestion(channel, currency, current, timestamp, completion):
mode = .suggestedPost(mode: .sender(channel: channel), currency: currency, price: current.value, timestamp: timestamp, completion: completion)
case let .postSuggestionModification(currency, current, timestamp, completion):
mode = .suggestedPost(mode: .admin, currency: currency, price: current.value, timestamp: timestamp, completion: completion)
}
return StarsWithdrawScreen(context: context, mode: mode)
}