mirror of
https://github.com/Swiftgram/Telegram-iOS.git
synced 2025-11-06 17:00:13 +00:00
Merge branch 'master' of gitlab.com:peter-iakovlev/telegram-ios
This commit is contained in:
commit
88d91ee774
@ -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 {
|
||||
|
||||
@ -1265,7 +1265,7 @@ final class AttachmentPanel: ASDisplayNode, ASScrollViewDelegate {
|
||||
}, addDoNotTranslateLanguage: { _ in
|
||||
}, hideTranslationPanel: {
|
||||
}, openPremiumGift: {
|
||||
}, openSuggestPost: {
|
||||
}, openSuggestPost: { _ in
|
||||
}, openPremiumRequiredForMessaging: {
|
||||
}, openStarsPurchase: { _ in
|
||||
}, openMessagePayment: {
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
}
|
||||
|
||||
@ -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: {
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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
|
||||
|
||||
@ -90,7 +90,6 @@ extension CGSize {
|
||||
|
||||
private let springAnimationIn: CABasicAnimation = {
|
||||
let animation = makeSpringAnimation("")
|
||||
animation.duration = 1.0
|
||||
return animation
|
||||
}()
|
||||
|
||||
|
||||
@ -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() {
|
||||
|
||||
@ -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"))
|
||||
|
||||
@ -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"))
|
||||
|
||||
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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?
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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(
|
||||
|
||||
@ -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",
|
||||
|
||||
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
}
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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?
|
||||
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -165,7 +165,7 @@ public final class ChatRecentActionsController: TelegramBaseController {
|
||||
}, addDoNotTranslateLanguage: { _ in
|
||||
}, hideTranslationPanel: {
|
||||
}, openPremiumGift: {
|
||||
}, openSuggestPost: {
|
||||
}, openSuggestPost: { _ in
|
||||
}, openPremiumRequiredForMessaging: {
|
||||
}, openStarsPurchase: { _ in
|
||||
}, openMessagePayment: {
|
||||
|
||||
@ -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
|
||||
}
|
||||
|
||||
@ -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: {
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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
|
||||
|
||||
@ -818,7 +818,7 @@ final class PeerSelectionControllerNode: ASDisplayNode {
|
||||
}, addDoNotTranslateLanguage: { _ in
|
||||
}, hideTranslationPanel: {
|
||||
}, openPremiumGift: {
|
||||
}, openSuggestPost: {
|
||||
}, openSuggestPost: { _ in
|
||||
}, openPremiumRequiredForMessaging: {
|
||||
}, openStarsPurchase: { _ in
|
||||
}, openMessagePayment: {
|
||||
|
||||
@ -38,6 +38,7 @@ swift_library(
|
||||
"//submodules/PasswordSetupUI",
|
||||
"//submodules/TelegramUI/Components/PeerManagement/OwnershipTransferController",
|
||||
"//submodules/TelegramUI/Components/ChatScheduleTimeController",
|
||||
"//submodules/TelegramUI/Components/TabSelectorComponent",
|
||||
],
|
||||
visibility = [
|
||||
"//visibility:public",
|
||||
|
||||
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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?
|
||||
|
||||
@ -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
|
||||
}
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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)
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user