mirror of
https://github.com/Swiftgram/Telegram-iOS.git
synced 2025-12-04 21:41:45 +00:00
Story updates
This commit is contained in:
parent
bcc1213db2
commit
e44bd4d858
@ -1356,7 +1356,7 @@ public protocol SharedAccountContext: AnyObject {
|
||||
func makeStoryStatsController(context: AccountContext, updatedPresentationData: (initial: PresentationData, signal: Signal<PresentationData, NoError>)?, peerId: EnginePeer.Id, storyId: Int32, storyItem: EngineStoryItem, fromStory: Bool) -> ViewController
|
||||
|
||||
func makeStarsTransactionsScreen(context: AccountContext, starsContext: StarsContext) -> ViewController
|
||||
func makeStarsPurchaseScreen(context: AccountContext, starsContext: StarsContext, options: [Any], purpose: StarsPurchasePurpose, targetPeerId: EnginePeer.Id?, completion: @escaping (Int64) -> Void) -> ViewController
|
||||
func makeStarsPurchaseScreen(context: AccountContext, starsContext: StarsContext, options: [Any], purpose: StarsPurchasePurpose, targetPeerId: EnginePeer.Id?, customTheme: PresentationTheme?, completion: @escaping (Int64) -> Void) -> ViewController
|
||||
func makeStarsTransferScreen(context: AccountContext, starsContext: StarsContext, invoice: TelegramMediaInvoice, source: BotPaymentInvoiceSource, extendedMedia: [TelegramExtendedMedia], inputData: Signal<(StarsContext.State, BotPaymentForm, EnginePeer?, EnginePeer?)?, NoError>, completion: @escaping (Bool) -> Void) -> ViewController
|
||||
func makeStarsSubscriptionTransferScreen(context: AccountContext, starsContext: StarsContext, invoice: TelegramMediaInvoice, link: String, inputData: Signal<(StarsContext.State, BotPaymentForm, EnginePeer?, EnginePeer?)?, NoError>, navigateToPeer: @escaping (EnginePeer) -> Void) -> ViewController
|
||||
func makeStarsTransactionScreen(context: AccountContext, transaction: StarsContext.State.Transaction, peer: EnginePeer) -> ViewController
|
||||
|
||||
@ -224,6 +224,7 @@ public struct PresentationGroupCallState: Equatable {
|
||||
public var defaultParticipantMuteState: DefaultParticipantMuteState?
|
||||
public var messagesAreEnabled: Bool
|
||||
public var canEnableMessages: Bool
|
||||
public var sendPaidMessageStars: Int64?
|
||||
public var recordingStartTimestamp: Int32?
|
||||
public var title: String?
|
||||
public var raisedHand: Bool
|
||||
@ -242,6 +243,7 @@ public struct PresentationGroupCallState: Equatable {
|
||||
defaultParticipantMuteState: DefaultParticipantMuteState?,
|
||||
messagesAreEnabled: Bool,
|
||||
canEnableMessages: Bool,
|
||||
sendPaidMessageStars: Int64?,
|
||||
recordingStartTimestamp: Int32?,
|
||||
title: String?,
|
||||
raisedHand: Bool,
|
||||
@ -259,6 +261,7 @@ public struct PresentationGroupCallState: Equatable {
|
||||
self.defaultParticipantMuteState = defaultParticipantMuteState
|
||||
self.messagesAreEnabled = messagesAreEnabled
|
||||
self.canEnableMessages = canEnableMessages
|
||||
self.sendPaidMessageStars = sendPaidMessageStars
|
||||
self.recordingStartTimestamp = recordingStartTimestamp
|
||||
self.title = title
|
||||
self.raisedHand = raisedHand
|
||||
|
||||
@ -6238,7 +6238,7 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
|
||||
guard let starsContext = self.context.starsContext else {
|
||||
return
|
||||
}
|
||||
let controller = self.context.sharedContext.makeStarsPurchaseScreen(context: self.context, starsContext: starsContext, options: [], purpose: amount.flatMap({ .topUp(requiredStars: $0, purpose: "subs") }) ?? .generic, targetPeerId: nil, completion: { _ in })
|
||||
let controller = self.context.sharedContext.makeStarsPurchaseScreen(context: self.context, starsContext: starsContext, options: [], purpose: amount.flatMap({ .topUp(requiredStars: $0, purpose: "subs") }) ?? .generic, targetPeerId: nil, customTheme: nil, completion: { _ in })
|
||||
self.push(controller)
|
||||
}
|
||||
|
||||
|
||||
@ -9,11 +9,19 @@ import AnimationCache
|
||||
import MultiAnimationRenderer
|
||||
|
||||
public final class MultilineTextWithEntitiesComponent: Component {
|
||||
public final class External {
|
||||
public fileprivate(set) var layout: TextNodeLayout?
|
||||
|
||||
public init() {
|
||||
}
|
||||
}
|
||||
|
||||
public enum TextContent: Equatable {
|
||||
case plain(NSAttributedString)
|
||||
case markdown(text: String, attributes: MarkdownAttributes)
|
||||
}
|
||||
|
||||
public let external: External?
|
||||
public let context: AccountContext?
|
||||
public let animationCache: AnimationCache?
|
||||
public let animationRenderer: MultiAnimationRenderer?
|
||||
@ -42,6 +50,7 @@ public final class MultilineTextWithEntitiesComponent: Component {
|
||||
public let longTapAction: (([NSAttributedString.Key: Any], Int) -> Void)?
|
||||
|
||||
public init(
|
||||
external: External? = nil,
|
||||
context: AccountContext?,
|
||||
animationCache: AnimationCache?,
|
||||
animationRenderer: MultiAnimationRenderer?,
|
||||
@ -68,6 +77,7 @@ public final class MultilineTextWithEntitiesComponent: Component {
|
||||
tapAction: (([NSAttributedString.Key: Any], Int) -> Void)? = nil,
|
||||
longTapAction: (([NSAttributedString.Key: Any], Int) -> Void)? = nil
|
||||
) {
|
||||
self.external = external
|
||||
self.context = context
|
||||
self.animationCache = animationCache
|
||||
self.animationRenderer = animationRenderer
|
||||
@ -96,6 +106,9 @@ public final class MultilineTextWithEntitiesComponent: Component {
|
||||
}
|
||||
|
||||
public static func ==(lhs: MultilineTextWithEntitiesComponent, rhs: MultilineTextWithEntitiesComponent) -> Bool {
|
||||
if lhs.external !== rhs.external {
|
||||
return false
|
||||
}
|
||||
if lhs.text != rhs.text {
|
||||
return false
|
||||
}
|
||||
@ -270,8 +283,8 @@ public final class MultilineTextWithEntitiesComponent: Component {
|
||||
constrainedSize.width = maxWidth
|
||||
}
|
||||
|
||||
let size = self.textNode.updateLayout(constrainedSize)
|
||||
self.textNode.frame = CGRect(origin: .zero, size: size)
|
||||
let layoutInfo = self.textNode.updateLayoutFullInfo(constrainedSize)
|
||||
self.textNode.frame = CGRect(origin: .zero, size: layoutInfo.size)
|
||||
|
||||
if component.handleSpoilers {
|
||||
let spoilerTextNode: ImmediateTextNodeWithEntities
|
||||
@ -312,7 +325,11 @@ public final class MultilineTextWithEntitiesComponent: Component {
|
||||
self.textNode.dustNode?.textNode = nil
|
||||
}
|
||||
|
||||
return size
|
||||
if let external = component.external {
|
||||
external.layout = layoutInfo
|
||||
}
|
||||
|
||||
return layoutInfo.size
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -202,4 +202,9 @@ public extension ContainerViewLayout {
|
||||
var standardInputHeight: CGFloat {
|
||||
return self.deviceMetrics.standardInputHeight(inLandscape: self.orientation == .landscape)
|
||||
}
|
||||
|
||||
static func concentricInsets(bottomInset: CGFloat, innerDiameter: CGFloat, sideInset: CGFloat) -> UIEdgeInsets {
|
||||
let mappedBottomInset: CGFloat = max(bottomInset, sideInset)
|
||||
return UIEdgeInsets(top: 0.0, left: sideInset, bottom: mappedBottomInset, right: sideInset)
|
||||
}
|
||||
}
|
||||
|
||||
@ -301,7 +301,7 @@ fileprivate let parsers: [Int32 : (BufferReader) -> Any?] = {
|
||||
dict[286776671] = { return Api.GeoPoint.parse_geoPointEmpty($0) }
|
||||
dict[-565420653] = { return Api.GeoPointAddress.parse_geoPointAddress($0) }
|
||||
dict[-29248689] = { return Api.GlobalPrivacySettings.parse_globalPrivacySettings($0) }
|
||||
dict[1429932961] = { return Api.GroupCall.parse_groupCall($0) }
|
||||
dict[-674602536] = { return Api.GroupCall.parse_groupCall($0) }
|
||||
dict[2004925620] = { return Api.GroupCall.parse_groupCallDiscarded($0) }
|
||||
dict[445316222] = { return Api.GroupCallMessage.parse_groupCallMessage($0) }
|
||||
dict[708691884] = { return Api.GroupCallParticipant.parse_groupCallParticipant($0) }
|
||||
@ -454,6 +454,7 @@ fileprivate let parsers: [Int32 : (BufferReader) -> Any?] = {
|
||||
dict[55761658] = { return Api.InputPrivacyKey.parse_inputPrivacyKeyPhoneNumber($0) }
|
||||
dict[-610373422] = { return Api.InputPrivacyKey.parse_inputPrivacyKeyPhoneP2P($0) }
|
||||
dict[1461304012] = { return Api.InputPrivacyKey.parse_inputPrivacyKeyProfilePhoto($0) }
|
||||
dict[1304334886] = { return Api.InputPrivacyKey.parse_inputPrivacyKeySavedMusic($0) }
|
||||
dict[-512548031] = { return Api.InputPrivacyKey.parse_inputPrivacyKeyStarGiftsAutoSave($0) }
|
||||
dict[1335282456] = { return Api.InputPrivacyKey.parse_inputPrivacyKeyStatusTimestamp($0) }
|
||||
dict[-1360618136] = { return Api.InputPrivacyKey.parse_inputPrivacyKeyVoiceMessages($0) }
|
||||
@ -665,7 +666,7 @@ fileprivate let parsers: [Int32 : (BufferReader) -> Any?] = {
|
||||
dict[-1974226924] = { return Api.MessageMedia.parse_messageMediaToDo($0) }
|
||||
dict[-1618676578] = { return Api.MessageMedia.parse_messageMediaUnsupported($0) }
|
||||
dict[784356159] = { return Api.MessageMedia.parse_messageMediaVenue($0) }
|
||||
dict[1059290001] = { return Api.MessageMedia.parse_messageMediaVideoStream($0) }
|
||||
dict[-899896439] = { return Api.MessageMedia.parse_messageMediaVideoStream($0) }
|
||||
dict[-571405253] = { return Api.MessageMedia.parse_messageMediaWebPage($0) }
|
||||
dict[-1938180548] = { return Api.MessagePeerReaction.parse_messagePeerReaction($0) }
|
||||
dict[-1228133028] = { return Api.MessagePeerVote.parse_messagePeerVote($0) }
|
||||
@ -813,6 +814,7 @@ fileprivate let parsers: [Int32 : (BufferReader) -> Any?] = {
|
||||
dict[-778378131] = { return Api.PrivacyKey.parse_privacyKeyPhoneNumber($0) }
|
||||
dict[961092808] = { return Api.PrivacyKey.parse_privacyKeyPhoneP2P($0) }
|
||||
dict[-1777000467] = { return Api.PrivacyKey.parse_privacyKeyProfilePhoto($0) }
|
||||
dict[-8759525] = { return Api.PrivacyKey.parse_privacyKeySavedMusic($0) }
|
||||
dict[749010424] = { return Api.PrivacyKey.parse_privacyKeyStarGiftsAutoSave($0) }
|
||||
dict[-1137792208] = { return Api.PrivacyKey.parse_privacyKeyStatusTimestamp($0) }
|
||||
dict[110621716] = { return Api.PrivacyKey.parse_privacyKeyVoiceMessages($0) }
|
||||
@ -1033,7 +1035,7 @@ fileprivate let parsers: [Int32 : (BufferReader) -> Any?] = {
|
||||
dict[-1609668650] = { return Api.Theme.parse_theme($0) }
|
||||
dict[-94849324] = { return Api.ThemeSettings.parse_themeSettings($0) }
|
||||
dict[-7173643] = { return Api.Timezone.parse_timezone($0) }
|
||||
dict[1287725239] = { return Api.TodoCompletion.parse_todoCompletion($0) }
|
||||
dict[572241380] = { return Api.TodoCompletion.parse_todoCompletion($0) }
|
||||
dict[-878074577] = { return Api.TodoItem.parse_todoItem($0) }
|
||||
dict[1236871718] = { return Api.TodoList.parse_todoList($0) }
|
||||
dict[-305282981] = { return Api.TopPeer.parse_topPeer($0) }
|
||||
|
||||
@ -350,6 +350,7 @@ public extension Api {
|
||||
case inputPrivacyKeyPhoneNumber
|
||||
case inputPrivacyKeyPhoneP2P
|
||||
case inputPrivacyKeyProfilePhoto
|
||||
case inputPrivacyKeySavedMusic
|
||||
case inputPrivacyKeyStarGiftsAutoSave
|
||||
case inputPrivacyKeyStatusTimestamp
|
||||
case inputPrivacyKeyVoiceMessages
|
||||
@ -415,6 +416,12 @@ public extension Api {
|
||||
buffer.appendInt32(1461304012)
|
||||
}
|
||||
|
||||
break
|
||||
case .inputPrivacyKeySavedMusic:
|
||||
if boxed {
|
||||
buffer.appendInt32(1304334886)
|
||||
}
|
||||
|
||||
break
|
||||
case .inputPrivacyKeyStarGiftsAutoSave:
|
||||
if boxed {
|
||||
@ -459,6 +466,8 @@ public extension Api {
|
||||
return ("inputPrivacyKeyPhoneP2P", [])
|
||||
case .inputPrivacyKeyProfilePhoto:
|
||||
return ("inputPrivacyKeyProfilePhoto", [])
|
||||
case .inputPrivacyKeySavedMusic:
|
||||
return ("inputPrivacyKeySavedMusic", [])
|
||||
case .inputPrivacyKeyStarGiftsAutoSave:
|
||||
return ("inputPrivacyKeyStarGiftsAutoSave", [])
|
||||
case .inputPrivacyKeyStatusTimestamp:
|
||||
@ -498,6 +507,9 @@ public extension Api {
|
||||
public static func parse_inputPrivacyKeyProfilePhoto(_ reader: BufferReader) -> InputPrivacyKey? {
|
||||
return Api.InputPrivacyKey.inputPrivacyKeyProfilePhoto
|
||||
}
|
||||
public static func parse_inputPrivacyKeySavedMusic(_ reader: BufferReader) -> InputPrivacyKey? {
|
||||
return Api.InputPrivacyKey.inputPrivacyKeySavedMusic
|
||||
}
|
||||
public static func parse_inputPrivacyKeyStarGiftsAutoSave(_ reader: BufferReader) -> InputPrivacyKey? {
|
||||
return Api.InputPrivacyKey.inputPrivacyKeyStarGiftsAutoSave
|
||||
}
|
||||
|
||||
@ -725,7 +725,7 @@ public extension Api {
|
||||
case messageMediaToDo(flags: Int32, todo: Api.TodoList, completions: [Api.TodoCompletion]?)
|
||||
case messageMediaUnsupported
|
||||
case messageMediaVenue(geo: Api.GeoPoint, title: String, address: String, provider: String, venueId: String, venueType: String)
|
||||
case messageMediaVideoStream(call: Api.InputGroupCall)
|
||||
case messageMediaVideoStream(flags: Int32, call: Api.InputGroupCall)
|
||||
case messageMediaWebPage(flags: Int32, webpage: Api.WebPage)
|
||||
|
||||
public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) {
|
||||
@ -909,10 +909,11 @@ public extension Api {
|
||||
serializeString(venueId, buffer: buffer, boxed: false)
|
||||
serializeString(venueType, buffer: buffer, boxed: false)
|
||||
break
|
||||
case .messageMediaVideoStream(let call):
|
||||
case .messageMediaVideoStream(let flags, let call):
|
||||
if boxed {
|
||||
buffer.appendInt32(1059290001)
|
||||
buffer.appendInt32(-899896439)
|
||||
}
|
||||
serializeInt32(flags, buffer: buffer, boxed: false)
|
||||
call.serialize(buffer, true)
|
||||
break
|
||||
case .messageMediaWebPage(let flags, let webpage):
|
||||
@ -961,8 +962,8 @@ public extension Api {
|
||||
return ("messageMediaUnsupported", [])
|
||||
case .messageMediaVenue(let geo, let title, let address, let provider, let venueId, let venueType):
|
||||
return ("messageMediaVenue", [("geo", geo as Any), ("title", title as Any), ("address", address as Any), ("provider", provider as Any), ("venueId", venueId as Any), ("venueType", venueType as Any)])
|
||||
case .messageMediaVideoStream(let call):
|
||||
return ("messageMediaVideoStream", [("call", call as Any)])
|
||||
case .messageMediaVideoStream(let flags, let call):
|
||||
return ("messageMediaVideoStream", [("flags", flags as Any), ("call", call as Any)])
|
||||
case .messageMediaWebPage(let flags, let webpage):
|
||||
return ("messageMediaWebPage", [("flags", flags as Any), ("webpage", webpage as Any)])
|
||||
}
|
||||
@ -1339,13 +1340,16 @@ public extension Api {
|
||||
}
|
||||
}
|
||||
public static func parse_messageMediaVideoStream(_ reader: BufferReader) -> MessageMedia? {
|
||||
var _1: Api.InputGroupCall?
|
||||
var _1: Int32?
|
||||
_1 = reader.readInt32()
|
||||
var _2: Api.InputGroupCall?
|
||||
if let signature = reader.readInt32() {
|
||||
_1 = Api.parse(reader, signature: signature) as? Api.InputGroupCall
|
||||
_2 = Api.parse(reader, signature: signature) as? Api.InputGroupCall
|
||||
}
|
||||
let _c1 = _1 != nil
|
||||
if _c1 {
|
||||
return Api.MessageMedia.messageMediaVideoStream(call: _1!)
|
||||
let _c2 = _2 != nil
|
||||
if _c1 && _c2 {
|
||||
return Api.MessageMedia.messageMediaVideoStream(flags: _1!, call: _2!)
|
||||
}
|
||||
else {
|
||||
return nil
|
||||
|
||||
@ -1134,6 +1134,7 @@ public extension Api {
|
||||
case privacyKeyPhoneNumber
|
||||
case privacyKeyPhoneP2P
|
||||
case privacyKeyProfilePhoto
|
||||
case privacyKeySavedMusic
|
||||
case privacyKeyStarGiftsAutoSave
|
||||
case privacyKeyStatusTimestamp
|
||||
case privacyKeyVoiceMessages
|
||||
@ -1199,6 +1200,12 @@ public extension Api {
|
||||
buffer.appendInt32(-1777000467)
|
||||
}
|
||||
|
||||
break
|
||||
case .privacyKeySavedMusic:
|
||||
if boxed {
|
||||
buffer.appendInt32(-8759525)
|
||||
}
|
||||
|
||||
break
|
||||
case .privacyKeyStarGiftsAutoSave:
|
||||
if boxed {
|
||||
@ -1243,6 +1250,8 @@ public extension Api {
|
||||
return ("privacyKeyPhoneP2P", [])
|
||||
case .privacyKeyProfilePhoto:
|
||||
return ("privacyKeyProfilePhoto", [])
|
||||
case .privacyKeySavedMusic:
|
||||
return ("privacyKeySavedMusic", [])
|
||||
case .privacyKeyStarGiftsAutoSave:
|
||||
return ("privacyKeyStarGiftsAutoSave", [])
|
||||
case .privacyKeyStatusTimestamp:
|
||||
@ -1282,6 +1291,9 @@ public extension Api {
|
||||
public static func parse_privacyKeyProfilePhoto(_ reader: BufferReader) -> PrivacyKey? {
|
||||
return Api.PrivacyKey.privacyKeyProfilePhoto
|
||||
}
|
||||
public static func parse_privacyKeySavedMusic(_ reader: BufferReader) -> PrivacyKey? {
|
||||
return Api.PrivacyKey.privacyKeySavedMusic
|
||||
}
|
||||
public static func parse_privacyKeyStarGiftsAutoSave(_ reader: BufferReader) -> PrivacyKey? {
|
||||
return Api.PrivacyKey.privacyKeyStarGiftsAutoSave
|
||||
}
|
||||
|
||||
@ -186,16 +186,16 @@ public extension Api {
|
||||
}
|
||||
public extension Api {
|
||||
enum TodoCompletion: TypeConstructorDescription {
|
||||
case todoCompletion(id: Int32, completedBy: Int64, date: Int32)
|
||||
case todoCompletion(id: Int32, completedBy: Api.Peer, date: Int32)
|
||||
|
||||
public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) {
|
||||
switch self {
|
||||
case .todoCompletion(let id, let completedBy, let date):
|
||||
if boxed {
|
||||
buffer.appendInt32(1287725239)
|
||||
buffer.appendInt32(572241380)
|
||||
}
|
||||
serializeInt32(id, buffer: buffer, boxed: false)
|
||||
serializeInt64(completedBy, buffer: buffer, boxed: false)
|
||||
completedBy.serialize(buffer, true)
|
||||
serializeInt32(date, buffer: buffer, boxed: false)
|
||||
break
|
||||
}
|
||||
@ -211,8 +211,10 @@ public extension Api {
|
||||
public static func parse_todoCompletion(_ reader: BufferReader) -> TodoCompletion? {
|
||||
var _1: Int32?
|
||||
_1 = reader.readInt32()
|
||||
var _2: Int64?
|
||||
_2 = reader.readInt64()
|
||||
var _2: Api.Peer?
|
||||
if let signature = reader.readInt32() {
|
||||
_2 = Api.parse(reader, signature: signature) as? Api.Peer
|
||||
}
|
||||
var _3: Int32?
|
||||
_3 = reader.readInt32()
|
||||
let _c1 = _1 != nil
|
||||
|
||||
@ -12107,9 +12107,9 @@ public extension Api.functions.stories {
|
||||
}
|
||||
}
|
||||
public extension Api.functions.stories {
|
||||
static func startLive(flags: Int32, peer: Api.InputPeer, caption: String?, entities: [Api.MessageEntity]?, privacyRules: [Api.InputPrivacyRule], randomId: Int64) -> (FunctionDescription, Buffer, DeserializeFunctionResponse<Api.Updates>) {
|
||||
static func startLive(flags: Int32, peer: Api.InputPeer, caption: String?, entities: [Api.MessageEntity]?, privacyRules: [Api.InputPrivacyRule], randomId: Int64, messagesEnabled: Api.Bool?, sendPaidMessagesStars: Int64?) -> (FunctionDescription, Buffer, DeserializeFunctionResponse<Api.Updates>) {
|
||||
let buffer = Buffer()
|
||||
buffer.appendInt32(-1294237155)
|
||||
buffer.appendInt32(-798372642)
|
||||
serializeInt32(flags, buffer: buffer, boxed: false)
|
||||
peer.serialize(buffer, true)
|
||||
if Int(flags) & Int(1 << 0) != 0 {serializeString(caption!, buffer: buffer, boxed: false)}
|
||||
@ -12124,7 +12124,9 @@ public extension Api.functions.stories {
|
||||
item.serialize(buffer, true)
|
||||
}
|
||||
serializeInt64(randomId, buffer: buffer, boxed: false)
|
||||
return (FunctionDescription(name: "stories.startLive", parameters: [("flags", String(describing: flags)), ("peer", String(describing: peer)), ("caption", String(describing: caption)), ("entities", String(describing: entities)), ("privacyRules", String(describing: privacyRules)), ("randomId", String(describing: randomId))]), buffer, DeserializeFunctionResponse { (buffer: Buffer) -> Api.Updates? in
|
||||
if Int(flags) & Int(1 << 6) != 0 {messagesEnabled!.serialize(buffer, true)}
|
||||
if Int(flags) & Int(1 << 7) != 0 {serializeInt64(sendPaidMessagesStars!, buffer: buffer, boxed: false)}
|
||||
return (FunctionDescription(name: "stories.startLive", parameters: [("flags", String(describing: flags)), ("peer", String(describing: peer)), ("caption", String(describing: caption)), ("entities", String(describing: entities)), ("privacyRules", String(describing: privacyRules)), ("randomId", String(describing: randomId)), ("messagesEnabled", String(describing: messagesEnabled)), ("sendPaidMessagesStars", String(describing: sendPaidMessagesStars))]), buffer, DeserializeFunctionResponse { (buffer: Buffer) -> Api.Updates? in
|
||||
let reader = BufferReader(buffer)
|
||||
var result: Api.Updates?
|
||||
if let signature = reader.readInt32() {
|
||||
|
||||
@ -1222,14 +1222,14 @@ public extension Api {
|
||||
}
|
||||
public extension Api {
|
||||
enum GroupCall: TypeConstructorDescription {
|
||||
case groupCall(flags: Int32, id: Int64, accessHash: Int64, participantsCount: Int32, title: String?, streamDcId: Int32?, recordStartDate: Int32?, scheduleDate: Int32?, unmutedVideoCount: Int32?, unmutedVideoLimit: Int32, version: Int32, inviteLink: String?)
|
||||
case groupCall(flags: Int32, id: Int64, accessHash: Int64, participantsCount: Int32, title: String?, streamDcId: Int32?, recordStartDate: Int32?, scheduleDate: Int32?, unmutedVideoCount: Int32?, unmutedVideoLimit: Int32, version: Int32, inviteLink: String?, sendPaidMessagesStars: Int64?)
|
||||
case groupCallDiscarded(id: Int64, accessHash: Int64, duration: Int32)
|
||||
|
||||
public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) {
|
||||
switch self {
|
||||
case .groupCall(let flags, let id, let accessHash, let participantsCount, let title, let streamDcId, let recordStartDate, let scheduleDate, let unmutedVideoCount, let unmutedVideoLimit, let version, let inviteLink):
|
||||
case .groupCall(let flags, let id, let accessHash, let participantsCount, let title, let streamDcId, let recordStartDate, let scheduleDate, let unmutedVideoCount, let unmutedVideoLimit, let version, let inviteLink, let sendPaidMessagesStars):
|
||||
if boxed {
|
||||
buffer.appendInt32(1429932961)
|
||||
buffer.appendInt32(-674602536)
|
||||
}
|
||||
serializeInt32(flags, buffer: buffer, boxed: false)
|
||||
serializeInt64(id, buffer: buffer, boxed: false)
|
||||
@ -1243,6 +1243,7 @@ public extension Api {
|
||||
serializeInt32(unmutedVideoLimit, buffer: buffer, boxed: false)
|
||||
serializeInt32(version, buffer: buffer, boxed: false)
|
||||
if Int(flags) & Int(1 << 16) != 0 {serializeString(inviteLink!, buffer: buffer, boxed: false)}
|
||||
if Int(flags) & Int(1 << 20) != 0 {serializeInt64(sendPaidMessagesStars!, buffer: buffer, boxed: false)}
|
||||
break
|
||||
case .groupCallDiscarded(let id, let accessHash, let duration):
|
||||
if boxed {
|
||||
@ -1257,8 +1258,8 @@ public extension Api {
|
||||
|
||||
public func descriptionFields() -> (String, [(String, Any)]) {
|
||||
switch self {
|
||||
case .groupCall(let flags, let id, let accessHash, let participantsCount, let title, let streamDcId, let recordStartDate, let scheduleDate, let unmutedVideoCount, let unmutedVideoLimit, let version, let inviteLink):
|
||||
return ("groupCall", [("flags", flags as Any), ("id", id as Any), ("accessHash", accessHash as Any), ("participantsCount", participantsCount as Any), ("title", title as Any), ("streamDcId", streamDcId as Any), ("recordStartDate", recordStartDate as Any), ("scheduleDate", scheduleDate as Any), ("unmutedVideoCount", unmutedVideoCount as Any), ("unmutedVideoLimit", unmutedVideoLimit as Any), ("version", version as Any), ("inviteLink", inviteLink as Any)])
|
||||
case .groupCall(let flags, let id, let accessHash, let participantsCount, let title, let streamDcId, let recordStartDate, let scheduleDate, let unmutedVideoCount, let unmutedVideoLimit, let version, let inviteLink, let sendPaidMessagesStars):
|
||||
return ("groupCall", [("flags", flags as Any), ("id", id as Any), ("accessHash", accessHash as Any), ("participantsCount", participantsCount as Any), ("title", title as Any), ("streamDcId", streamDcId as Any), ("recordStartDate", recordStartDate as Any), ("scheduleDate", scheduleDate as Any), ("unmutedVideoCount", unmutedVideoCount as Any), ("unmutedVideoLimit", unmutedVideoLimit as Any), ("version", version as Any), ("inviteLink", inviteLink as Any), ("sendPaidMessagesStars", sendPaidMessagesStars as Any)])
|
||||
case .groupCallDiscarded(let id, let accessHash, let duration):
|
||||
return ("groupCallDiscarded", [("id", id as Any), ("accessHash", accessHash as Any), ("duration", duration as Any)])
|
||||
}
|
||||
@ -1289,6 +1290,8 @@ public extension Api {
|
||||
_11 = reader.readInt32()
|
||||
var _12: String?
|
||||
if Int(_1!) & Int(1 << 16) != 0 {_12 = parseString(reader) }
|
||||
var _13: Int64?
|
||||
if Int(_1!) & Int(1 << 20) != 0 {_13 = reader.readInt64() }
|
||||
let _c1 = _1 != nil
|
||||
let _c2 = _2 != nil
|
||||
let _c3 = _3 != nil
|
||||
@ -1301,8 +1304,9 @@ public extension Api {
|
||||
let _c10 = _10 != nil
|
||||
let _c11 = _11 != nil
|
||||
let _c12 = (Int(_1!) & Int(1 << 16) == 0) || _12 != nil
|
||||
if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 && _c7 && _c8 && _c9 && _c10 && _c11 && _c12 {
|
||||
return Api.GroupCall.groupCall(flags: _1!, id: _2!, accessHash: _3!, participantsCount: _4!, title: _5, streamDcId: _6, recordStartDate: _7, scheduleDate: _8, unmutedVideoCount: _9, unmutedVideoLimit: _10!, version: _11!, inviteLink: _12)
|
||||
let _c13 = (Int(_1!) & Int(1 << 20) == 0) || _13 != nil
|
||||
if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 && _c7 && _c8 && _c9 && _c10 && _c11 && _c12 && _c13 {
|
||||
return Api.GroupCall.groupCall(flags: _1!, id: _2!, accessHash: _3!, participantsCount: _4!, title: _5, streamDcId: _6, recordStartDate: _7, scheduleDate: _8, unmutedVideoCount: _9, unmutedVideoLimit: _10!, version: _11!, inviteLink: _12, sendPaidMessagesStars: _13)
|
||||
}
|
||||
else {
|
||||
return nil
|
||||
|
||||
@ -30,6 +30,7 @@ private extension PresentationGroupCallState {
|
||||
defaultParticipantMuteState: nil,
|
||||
messagesAreEnabled: !isChannel,
|
||||
canEnableMessages: false,
|
||||
sendPaidMessageStars: nil,
|
||||
recordingStartTimestamp: nil,
|
||||
title: title,
|
||||
raisedHand: false,
|
||||
@ -931,33 +932,33 @@ public final class PresentationGroupCallImpl: PresentationGroupCall {
|
||||
messageLifetime = Int32(value)
|
||||
}
|
||||
|
||||
var createMessageContext = true
|
||||
|
||||
if isStream {
|
||||
messageLifetime = Int32.max
|
||||
|
||||
if self.isStream {
|
||||
createMessageContext = false
|
||||
var allowLiveChat = false
|
||||
if let data = self.accountContext.currentAppConfiguration.with({ $0 }).data {
|
||||
if let dev = data["dev"] as? Double, dev != 0.0 {
|
||||
createMessageContext = true
|
||||
allowLiveChat = true
|
||||
}
|
||||
if data["ios_can_join_streams"] != nil {
|
||||
createMessageContext = true
|
||||
allowLiveChat = true
|
||||
}
|
||||
}
|
||||
if !allowLiveChat {
|
||||
preconditionFailure()
|
||||
}
|
||||
}
|
||||
}
|
||||
if createMessageContext {
|
||||
self.messagesContext = accountContext.engine.messages.groupCallMessages(
|
||||
callId: initialCall.description.id,
|
||||
reference: .id(id: initialCall.description.id, accessHash: initialCall.description.accessHash),
|
||||
e2eContext: self.e2eContext,
|
||||
messageLifetime: messageLifetime,
|
||||
isLiveStream: isStream
|
||||
)
|
||||
self.messagesStatePromise.set(self.messagesContext!.state)
|
||||
}
|
||||
|
||||
self.messagesContext = accountContext.engine.messages.groupCallMessages(
|
||||
callId: initialCall.description.id,
|
||||
reference: .id(id: initialCall.description.id, accessHash: initialCall.description.accessHash),
|
||||
e2eContext: self.e2eContext,
|
||||
messageLifetime: messageLifetime,
|
||||
isLiveStream: isStream
|
||||
)
|
||||
self.messagesStatePromise.set(self.messagesContext!.state)
|
||||
}
|
||||
|
||||
var sharedAudioContext = sharedAudioContext
|
||||
@ -1562,7 +1563,7 @@ public final class PresentationGroupCallImpl: PresentationGroupCall {
|
||||
adminIds: Set(),
|
||||
isCreator: false,
|
||||
defaultParticipantsAreMuted: callInfo.defaultParticipantsAreMuted ?? GroupCallParticipantsContext.State.DefaultParticipantsAreMuted(isMuted: self.stateValue.defaultParticipantMuteState == .muted, canChange: true),
|
||||
messagesAreEnabled: callInfo.messagesAreEnabled ?? GroupCallParticipantsContext.State.MessagesAreEnabled(isEnabled: self.stateValue.messagesAreEnabled, canChange: self.stateValue.canEnableMessages),
|
||||
messagesAreEnabled: callInfo.messagesAreEnabled ?? GroupCallParticipantsContext.State.MessagesAreEnabled(isEnabled: self.stateValue.messagesAreEnabled, canChange: self.stateValue.canEnableMessages, sendPaidMessagesStars: self.stateValue.sendPaidMessageStars),
|
||||
sortAscending: true,
|
||||
recordingStartTimestamp: nil,
|
||||
title: self.stateValue.title,
|
||||
|
||||
@ -1210,6 +1210,7 @@ final class VideoChatScreenComponent: Component {
|
||||
defaultParticipantMuteState: nil,
|
||||
messagesAreEnabled: true,
|
||||
canEnableMessages: false,
|
||||
sendPaidMessageStars: nil,
|
||||
recordingStartTimestamp: nil,
|
||||
title: nil,
|
||||
raisedHand: false,
|
||||
|
||||
@ -495,9 +495,15 @@ func textMediaAndExpirationTimerFromApiMedia(_ media: Api.MessageMedia?, _ peerI
|
||||
return (TelegramMediaGiveawayResults(flags: flags, launchMessageId: MessageId(peerId: PeerId(namespace: Namespaces.Peer.CloudChannel, id: PeerId.Id._internalFromInt64Value(channelId)), namespace: Namespaces.Message.Cloud, id: launchMsgId), additionalChannelsCount: additionalPeersCount ?? 0, winnersPeerIds: winners.map { PeerId(namespace: Namespaces.Peer.CloudUser, id: PeerId.Id._internalFromInt64Value($0)) }, winnersCount: winnersCount, unclaimedCount: unclaimedCount, prize: prize, untilDate: untilDate, prizeDescription: prizeDescription), nil, nil, nil, nil, nil)
|
||||
case let .messageMediaPaidMedia(starsAmount, apiExtendedMedia):
|
||||
return (TelegramMediaPaidContent(amount: starsAmount, extendedMedia: apiExtendedMedia.compactMap({ TelegramExtendedMedia(apiExtendedMedia: $0, peerId: peerId) })), nil, nil, nil, nil, nil)
|
||||
case let .messageMediaVideoStream(call):
|
||||
case let .messageMediaVideoStream(flags, call):
|
||||
if let call = GroupCallReference(call) {
|
||||
return (TelegramMediaLiveStream(call: call), nil, nil, nil, nil, nil)
|
||||
let kind: TelegramMediaLiveStream.Kind
|
||||
if (flags & (1 << 0)) != 0 {
|
||||
kind = .rtmp
|
||||
} else {
|
||||
kind = .rtc
|
||||
}
|
||||
return (TelegramMediaLiveStream(call: call, kind: kind), nil, nil, nil, nil, nil)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -27,7 +27,7 @@ extension TelegramMediaTodo.Completion {
|
||||
init(apiCompletion: Api.TodoCompletion) {
|
||||
switch apiCompletion {
|
||||
case let .todoCompletion(id, completedBy, date):
|
||||
self.init(id: id, date: date, completedBy: EnginePeer.Id(namespace: Namespaces.Peer.CloudUser, id: PeerId.Id._internalFromInt64Value(completedBy)))
|
||||
self.init(id: id, date: date, completedBy: completedBy.peerId)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -4906,7 +4906,7 @@ func replayFinalState(
|
||||
}
|
||||
|
||||
switch call {
|
||||
case let .groupCall(flags, _, _, participantsCount, title, _, recordStartDate, scheduleDate, _, _, _, _):
|
||||
case let .groupCall(flags, _, _, participantsCount, title, _, recordStartDate, scheduleDate, _, _, _, _, sendPaidMessagesStars):
|
||||
let isMin = (flags & (1 << 19)) != 0
|
||||
let isMuted = (flags & (1 << 1)) != 0
|
||||
let canChange = (flags & (1 << 2)) != 0
|
||||
@ -4914,7 +4914,7 @@ func replayFinalState(
|
||||
let defaultParticipantsAreMuted = GroupCallParticipantsContext.State.DefaultParticipantsAreMuted(isMuted: isMuted, canChange: canChange)
|
||||
let messagesEnabled = (flags & (1 << 17)) != 0
|
||||
let canChangeMessagesEnabled = (flags & (1 << 18)) != 0
|
||||
let messagesAreEnabled = GroupCallParticipantsContext.State.MessagesAreEnabled(isEnabled: messagesEnabled, canChange: canChangeMessagesEnabled)
|
||||
let messagesAreEnabled = GroupCallParticipantsContext.State.MessagesAreEnabled(isEnabled: messagesEnabled, canChange: canChangeMessagesEnabled, sendPaidMessagesStars: sendPaidMessagesStars)
|
||||
updatedGroupCallParticipants.append((
|
||||
info.id,
|
||||
.call(isTerminated: false, defaultParticipantsAreMuted: defaultParticipantsAreMuted, messagesAreEnabled: messagesAreEnabled, title: title, recordingStartTimestamp: recordStartDate, scheduleTimestamp: scheduleDate, isVideoEnabled: isVideoEnabled, participantCount: Int(participantsCount), isMin: isMin)
|
||||
@ -4926,7 +4926,7 @@ func replayFinalState(
|
||||
case let .groupCallDiscarded(callId, _, _):
|
||||
updatedGroupCallParticipants.append((
|
||||
callId,
|
||||
.call(isTerminated: true, defaultParticipantsAreMuted: GroupCallParticipantsContext.State.DefaultParticipantsAreMuted(isMuted: false, canChange: false), messagesAreEnabled: GroupCallParticipantsContext.State.MessagesAreEnabled(isEnabled: false, canChange: false), title: nil, recordingStartTimestamp: nil, scheduleTimestamp: nil, isVideoEnabled: false, participantCount: nil, isMin: false)
|
||||
.call(isTerminated: true, defaultParticipantsAreMuted: GroupCallParticipantsContext.State.DefaultParticipantsAreMuted(isMuted: false, canChange: false), messagesAreEnabled: GroupCallParticipantsContext.State.MessagesAreEnabled(isEnabled: false, canChange: false, sendPaidMessagesStars: nil), title: nil, recordingStartTimestamp: nil, scheduleTimestamp: nil, isVideoEnabled: false, participantCount: nil, isMin: false)
|
||||
))
|
||||
|
||||
if let peerId {
|
||||
|
||||
@ -2,6 +2,11 @@ import Foundation
|
||||
import Postbox
|
||||
|
||||
public final class TelegramMediaLiveStream: Media, Equatable {
|
||||
public enum Kind: Int32 {
|
||||
case rtmp = 0
|
||||
case rtc = 1
|
||||
}
|
||||
|
||||
public let peerIds: [PeerId] = []
|
||||
|
||||
public var id: MediaId? {
|
||||
@ -9,17 +14,21 @@ public final class TelegramMediaLiveStream: Media, Equatable {
|
||||
}
|
||||
|
||||
public let call: GroupCallReference
|
||||
public let kind: Kind
|
||||
|
||||
public init(call: GroupCallReference) {
|
||||
public init(call: GroupCallReference, kind: Kind) {
|
||||
self.call = call
|
||||
self.kind = kind
|
||||
}
|
||||
|
||||
public init(decoder: PostboxDecoder) {
|
||||
self.call = decoder.decodeCodable(GroupCallReference.self, forKey: "call")!
|
||||
self.kind = Kind(rawValue: decoder.decodeInt32ForKey("k", orElse: 0)) ?? .rtmp
|
||||
}
|
||||
|
||||
public func encode(_ encoder: PostboxEncoder) {
|
||||
encoder.encodeCodable(self.call, forKey: "call")
|
||||
encoder.encodeInt32(self.kind.rawValue, forKey: "k")
|
||||
}
|
||||
|
||||
public static func ==(lhs: TelegramMediaLiveStream, rhs: TelegramMediaLiveStream) -> Bool {
|
||||
@ -34,6 +43,9 @@ public final class TelegramMediaLiveStream: Media, Equatable {
|
||||
if self.call != other.call {
|
||||
return false
|
||||
}
|
||||
if self.kind != other.kind {
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
@ -143,7 +143,7 @@ public struct GroupCallSummary: Equatable {
|
||||
extension GroupCallInfo {
|
||||
init?(_ call: Api.GroupCall) {
|
||||
switch call {
|
||||
case let .groupCall(flags, id, accessHash, participantsCount, title, streamDcId, recordStartDate, scheduleDate, _, unmutedVideoLimit, _, _):
|
||||
case let .groupCall(flags, id, accessHash, participantsCount, title, streamDcId, recordStartDate, scheduleDate, _, unmutedVideoLimit, _, _, sendPaidMessagesStars):
|
||||
self.init(
|
||||
id: id,
|
||||
accessHash: accessHash,
|
||||
@ -155,7 +155,7 @@ extension GroupCallInfo {
|
||||
recordingStartTimestamp: recordStartDate,
|
||||
sortAscending: (flags & (1 << 6)) != 0,
|
||||
defaultParticipantsAreMuted: GroupCallParticipantsContext.State.DefaultParticipantsAreMuted(isMuted: (flags & (1 << 1)) != 0, canChange: (flags & (1 << 2)) != 0),
|
||||
messagesAreEnabled: GroupCallParticipantsContext.State.MessagesAreEnabled(isEnabled: (flags & (1 << 17)) != 0, canChange: (flags & (1 << 18)) != 0),
|
||||
messagesAreEnabled: GroupCallParticipantsContext.State.MessagesAreEnabled(isEnabled: (flags & (1 << 17)) != 0, canChange: (flags & (1 << 18)) != 0, sendPaidMessagesStars: sendPaidMessagesStars),
|
||||
isVideoEnabled: (flags & (1 << 9)) != 0,
|
||||
unmutedVideoLimit: Int(unmutedVideoLimit),
|
||||
isStream: (flags & (1 << 12)) != 0,
|
||||
@ -564,7 +564,7 @@ func _internal_getGroupCallParticipants(account: Account, reference: InternalGro
|
||||
adminIds: Set(),
|
||||
isCreator: isCreator,
|
||||
defaultParticipantsAreMuted: defaultParticipantsAreMuted ?? GroupCallParticipantsContext.State.DefaultParticipantsAreMuted(isMuted: false, canChange: false),
|
||||
messagesAreEnabled: messagesAreEnabled ?? GroupCallParticipantsContext.State.MessagesAreEnabled(isEnabled: true, canChange: false),
|
||||
messagesAreEnabled: messagesAreEnabled ?? GroupCallParticipantsContext.State.MessagesAreEnabled(isEnabled: true, canChange: false, sendPaidMessagesStars: nil),
|
||||
sortAscending: sortAscendingValue,
|
||||
recordingStartTimestamp: nil,
|
||||
title: nil,
|
||||
@ -727,7 +727,7 @@ func _internal_joinGroupCall(account: Account, peerId: PeerId?, joinAs: PeerId?,
|
||||
adminIds: Set(),
|
||||
isCreator: false,
|
||||
defaultParticipantsAreMuted: .init(isMuted: true, canChange: false),
|
||||
messagesAreEnabled: .init(isEnabled: true, canChange: false),
|
||||
messagesAreEnabled: .init(isEnabled: true, canChange: false, sendPaidMessagesStars: nil),
|
||||
sortAscending: true,
|
||||
recordingStartTimestamp: nil,
|
||||
title: nil,
|
||||
@ -784,7 +784,7 @@ func _internal_joinGroupCall(account: Account, peerId: PeerId?, joinAs: PeerId?,
|
||||
maybeParsedCall = GroupCallInfo(call)
|
||||
|
||||
switch call {
|
||||
case let .groupCall(flags, _, _, _, title, _, recordStartDate, scheduleDate, _, unmutedVideoLimit, _, _):
|
||||
case let .groupCall(flags, _, _, _, title, _, recordStartDate, scheduleDate, _, unmutedVideoLimit, _, _, sendPaidMessagesStars):
|
||||
let isMin = (flags & (1 << 19)) != 0
|
||||
let isMuted = (flags & (1 << 1)) != 0
|
||||
let canChange = (flags & (1 << 2)) != 0
|
||||
@ -792,7 +792,7 @@ func _internal_joinGroupCall(account: Account, peerId: PeerId?, joinAs: PeerId?,
|
||||
let messagesEnabled = (flags & (1 << 17)) != 0
|
||||
let canChangeMessagesEnabled = (flags & (1 << 18)) != 0
|
||||
state.defaultParticipantsAreMuted = GroupCallParticipantsContext.State.DefaultParticipantsAreMuted(isMuted: isMuted, canChange: isMin ? state.defaultParticipantsAreMuted.canChange : canChange)
|
||||
state.messagesAreEnabled = GroupCallParticipantsContext.State.MessagesAreEnabled(isEnabled: messagesEnabled, canChange: isMin ? state.messagesAreEnabled.canChange : canChangeMessagesEnabled)
|
||||
state.messagesAreEnabled = GroupCallParticipantsContext.State.MessagesAreEnabled(isEnabled: messagesEnabled, canChange: isMin ? state.messagesAreEnabled.canChange : canChangeMessagesEnabled, sendPaidMessagesStars: sendPaidMessagesStars)
|
||||
state.title = title
|
||||
state.recordingStartTimestamp = recordStartDate
|
||||
state.scheduleTimestamp = scheduleDate
|
||||
@ -1415,10 +1415,12 @@ public final class GroupCallParticipantsContext {
|
||||
public struct MessagesAreEnabled: Equatable {
|
||||
public var isEnabled: Bool
|
||||
public var canChange: Bool
|
||||
public var sendPaidMessagesStars: Int64?
|
||||
|
||||
public init(isEnabled: Bool, canChange: Bool) {
|
||||
public init(isEnabled: Bool, canChange: Bool, sendPaidMessagesStars: Int64?) {
|
||||
self.isEnabled = isEnabled
|
||||
self.canChange = canChange
|
||||
self.sendPaidMessagesStars = sendPaidMessagesStars
|
||||
}
|
||||
}
|
||||
|
||||
@ -1437,6 +1439,7 @@ public final class GroupCallParticipantsContext {
|
||||
public var isVideoEnabled: Bool
|
||||
public var unmutedVideoLimit: Int
|
||||
public var isStream: Bool
|
||||
public var sendPaidMessagesStars: Int64?
|
||||
public var version: Int32
|
||||
|
||||
public mutating func mergeActivity(from other: State, myPeerId: PeerId?, previousMyPeerId: PeerId?, mergeActivityTimestamps: Bool) {
|
||||
@ -2001,7 +2004,7 @@ public final class GroupCallParticipantsContext {
|
||||
} else if case let .call(_, defaultParticipantsAreMuted, messagesAreEnabled, title, recordingStartTimestamp, scheduleTimestamp, isVideoEnabled, participantsCount, isMin) = update {
|
||||
var state = self.stateValue.state
|
||||
state.defaultParticipantsAreMuted = isMin ? State.DefaultParticipantsAreMuted(isMuted: defaultParticipantsAreMuted.isMuted, canChange: state.defaultParticipantsAreMuted.canChange) : defaultParticipantsAreMuted
|
||||
state.messagesAreEnabled = isMin ? State.MessagesAreEnabled(isEnabled: messagesAreEnabled.isEnabled, canChange: state.messagesAreEnabled.canChange) : messagesAreEnabled
|
||||
state.messagesAreEnabled = isMin ? State.MessagesAreEnabled(isEnabled: messagesAreEnabled.isEnabled, canChange: state.messagesAreEnabled.canChange, sendPaidMessagesStars: state.messagesAreEnabled.sendPaidMessagesStars) : messagesAreEnabled
|
||||
state.recordingStartTimestamp = recordingStartTimestamp
|
||||
state.title = title
|
||||
state.scheduleTimestamp = scheduleTimestamp
|
||||
@ -3689,6 +3692,16 @@ public final class GroupCallMessagesContext {
|
||||
}
|
||||
}
|
||||
|
||||
public enum Color {
|
||||
case purple
|
||||
case blue
|
||||
case green
|
||||
case yellow
|
||||
case orange
|
||||
case red
|
||||
case silver
|
||||
}
|
||||
|
||||
public let id: Id
|
||||
public let author: EnginePeer?
|
||||
public let text: String
|
||||
@ -4116,28 +4129,28 @@ public final class GroupCallMessagesContext {
|
||||
}
|
||||
}
|
||||
|
||||
public static func getStarAmountParamMapping(value: Int64) -> (period: Int, maxLength: Int, emojiCount: Int) {
|
||||
public static func getStarAmountParamMapping(value: Int64) -> (period: Int, maxLength: Int, emojiCount: Int, color: Message.Color?) {
|
||||
if value >= 10000 {
|
||||
return (3600, 400, 20)
|
||||
return (3600, 400, 20, .silver)
|
||||
}
|
||||
if value >= 2000 {
|
||||
return (1800, 280, 10)
|
||||
return (1800, 280, 10, .red)
|
||||
}
|
||||
if value >= 500 {
|
||||
return (900, 200, 7)
|
||||
return (900, 200, 7, .orange)
|
||||
}
|
||||
if value >= 250 {
|
||||
return (600, 150, 4)
|
||||
return (600, 150, 4, .yellow)
|
||||
}
|
||||
if value >= 100 {
|
||||
return (300, 110, 3)
|
||||
return (300, 110, 3, .green)
|
||||
}
|
||||
if value >= 50 {
|
||||
return (120, 80, 2)
|
||||
return (120, 80, 2, .blue)
|
||||
}
|
||||
if value >= 10 {
|
||||
return (60, 60, 1)
|
||||
if value >= 1 {
|
||||
return (60, 60, 1, .purple)
|
||||
}
|
||||
return (30, 30, 0)
|
||||
return (30, 30, 0, nil)
|
||||
}
|
||||
}
|
||||
|
||||
@ -1115,7 +1115,8 @@ func _internal_cancelStoryUpload(account: Account, stableId: Int32) {
|
||||
func _internal_beginStoryLivestream(account: Account) -> Signal<Never, NoError> {
|
||||
var flags: Int32 = 0
|
||||
flags |= 1 << 5
|
||||
return account.network.request(Api.functions.stories.startLive(flags: flags, peer: .inputPeerSelf, caption: nil, entities: nil, privacyRules: [.inputPrivacyValueAllowAll], randomId: Int64.random(in: Int64.min ... Int64.max)))
|
||||
flags |= 1 << 6
|
||||
return account.network.request(Api.functions.stories.startLive(flags: flags, peer: .inputPeerSelf, caption: nil, entities: nil, privacyRules: [.inputPrivacyValueAllowAll], randomId: Int64.random(in: Int64.min ... Int64.max), messagesEnabled: .boolTrue, sendPaidMessagesStars: nil))
|
||||
|> map(Optional.init)
|
||||
|> `catch` { _ -> Signal<Api.Updates?, NoError> in
|
||||
return .single(nil)
|
||||
@ -2705,14 +2706,14 @@ public func _internal_setMessageNotificationWasDisplayed(transaction: Transactio
|
||||
transaction.putItemCacheEntry(id: ItemCacheEntryId(collectionId: Namespaces.CachedItemCollection.displayedMessageNotifications, key: key), entry: CodableEntry(data: Data()))
|
||||
}
|
||||
|
||||
func _internal_updateStoryViewsForMyReaction(isChannel: Bool, views: Stories.Item.Views?, previousReaction: MessageReaction.Reaction?, reaction: MessageReaction.Reaction?) -> Stories.Item.Views? {
|
||||
if !isChannel {
|
||||
func _internal_updateStoryViewsForMyReaction(isChannel: Bool, views: Stories.Item.Views?, previousReaction: MessageReaction.Reaction?, reaction: MessageReaction.Reaction?, addedCount: Int = 1) -> Stories.Item.Views? {
|
||||
if !isChannel && reaction != .stars {
|
||||
return views
|
||||
}
|
||||
|
||||
var views = views ?? Stories.Item.Views(seenCount: 0, reactedCount: 0, forwardCount: 0, seenPeerIds: [], reactions: [], hasList: false)
|
||||
|
||||
if let reaction = reaction {
|
||||
if let reaction {
|
||||
if previousReaction == nil {
|
||||
views.reactedCount += 1
|
||||
}
|
||||
@ -2720,17 +2721,19 @@ func _internal_updateStoryViewsForMyReaction(isChannel: Bool, views: Stories.Ite
|
||||
do {
|
||||
var reactions = views.reactions
|
||||
|
||||
if let previousIndex = reactions.firstIndex(where: { $0.chosenOrder != nil }) {
|
||||
reactions[previousIndex].chosenOrder = nil
|
||||
reactions[previousIndex].count = max(0, reactions[previousIndex].count - 1)
|
||||
if reaction != .stars {
|
||||
if let previousIndex = reactions.firstIndex(where: { $0.chosenOrder != nil }) {
|
||||
reactions[previousIndex].chosenOrder = nil
|
||||
reactions[previousIndex].count = max(0, reactions[previousIndex].count - 1)
|
||||
}
|
||||
}
|
||||
if let reactionIndex = reactions.firstIndex(where: { $0.value == reaction }) {
|
||||
reactions[reactionIndex].chosenOrder = 0
|
||||
reactions[reactionIndex].count += 1
|
||||
reactions[reactionIndex].count += Int32(addedCount)
|
||||
} else {
|
||||
reactions.append(MessageReaction(
|
||||
value: reaction,
|
||||
count: 1,
|
||||
count: Int32(addedCount),
|
||||
chosenOrder: 0
|
||||
))
|
||||
}
|
||||
@ -2872,3 +2875,120 @@ func _internal_setStoryReaction(account: Account, peerId: EnginePeer.Id, id: Int
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func _internal_sendStoryStars(account: Account, peerId: EnginePeer.Id, id: Int32, count: Int) -> Signal<Never, NoError> {
|
||||
return account.postbox.transaction { transaction -> (Stories.StoredItem?, Api.InputPeer?) in
|
||||
guard let peer = transaction.getPeer(peerId) else {
|
||||
return (nil, nil)
|
||||
}
|
||||
guard let inputPeer = apiInputPeer(peer) else {
|
||||
return (nil, nil)
|
||||
}
|
||||
|
||||
var updatedItemValue: Stories.StoredItem?
|
||||
|
||||
let updateViews: (Stories.Item.Views?, MessageReaction.Reaction?) -> Stories.Item.Views? = { views, previousReaction in
|
||||
return _internal_updateStoryViewsForMyReaction(isChannel: peerId.namespace == Namespaces.Peer.CloudChannel, views: views, previousReaction: previousReaction, reaction: .stars, addedCount: count)
|
||||
}
|
||||
|
||||
var currentItems = transaction.getStoryItems(peerId: peerId)
|
||||
for i in 0 ..< currentItems.count {
|
||||
if currentItems[i].id == id {
|
||||
if case let .item(item) = currentItems[i].value.get(Stories.StoredItem.self) {
|
||||
let updatedItem: Stories.StoredItem = .item(Stories.Item(
|
||||
id: item.id,
|
||||
timestamp: item.timestamp,
|
||||
expirationTimestamp: item.expirationTimestamp,
|
||||
media: item.media,
|
||||
alternativeMediaList: item.alternativeMediaList,
|
||||
mediaAreas: item.mediaAreas,
|
||||
text: item.text,
|
||||
entities: item.entities,
|
||||
views: updateViews(item.views, item.myReaction),
|
||||
privacy: item.privacy,
|
||||
isPinned: item.isPinned,
|
||||
isExpired: item.isEdited,
|
||||
isPublic: item.isPublic,
|
||||
isCloseFriends: item.isCloseFriends,
|
||||
isContacts: item.isContacts,
|
||||
isSelectedContacts: item.isSelectedContacts,
|
||||
isForwardingDisabled: item.isForwardingDisabled,
|
||||
isEdited: item.isEdited,
|
||||
isMy: item.isMy,
|
||||
myReaction: .stars,
|
||||
forwardInfo: item.forwardInfo,
|
||||
authorId: item.authorId,
|
||||
folderIds: item.folderIds
|
||||
))
|
||||
updatedItemValue = updatedItem
|
||||
if let entry = CodableEntry(updatedItem) {
|
||||
currentItems[i] = StoryItemsTableEntry(value: entry, id: updatedItem.id, expirationTimestamp: updatedItem.expirationTimestamp, isCloseFriends: updatedItem.isCloseFriends, isLiveStream: updatedItem.isLiveStream)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
transaction.setStoryItems(peerId: peerId, items: currentItems)
|
||||
|
||||
if let current = transaction.getStory(id: StoryId(peerId: peerId, id: id))?.get(Stories.StoredItem.self), case let .item(item) = current {
|
||||
let updatedItem: Stories.StoredItem = .item(Stories.Item(
|
||||
id: item.id,
|
||||
timestamp: item.timestamp,
|
||||
expirationTimestamp: item.expirationTimestamp,
|
||||
media: item.media,
|
||||
alternativeMediaList: item.alternativeMediaList,
|
||||
mediaAreas: item.mediaAreas,
|
||||
text: item.text,
|
||||
entities: item.entities,
|
||||
views: updateViews(item.views, item.myReaction),
|
||||
privacy: item.privacy,
|
||||
isPinned: item.isPinned,
|
||||
isExpired: item.isEdited,
|
||||
isPublic: item.isPublic,
|
||||
isCloseFriends: item.isCloseFriends,
|
||||
isContacts: item.isContacts,
|
||||
isSelectedContacts: item.isSelectedContacts,
|
||||
isForwardingDisabled: item.isForwardingDisabled,
|
||||
isEdited: item.isEdited,
|
||||
isMy: item.isMy,
|
||||
myReaction: .stars,
|
||||
forwardInfo: item.forwardInfo,
|
||||
authorId: item.authorId,
|
||||
folderIds: item.folderIds
|
||||
))
|
||||
updatedItemValue = updatedItem
|
||||
if let entry = CodableEntry(updatedItem) {
|
||||
transaction.setStory(id: StoryId(peerId: peerId, id: id), value: entry)
|
||||
}
|
||||
}
|
||||
|
||||
return (updatedItemValue, inputPeer)
|
||||
}
|
||||
|> mapToSignal { storyItem, inputPeer -> Signal<Never, NoError> in
|
||||
guard let inputPeer else {
|
||||
return .complete()
|
||||
}
|
||||
|
||||
if let storyItem {
|
||||
account.stateManager.injectStoryUpdates(updates: [InternalStoryUpdate.added(peerId: peerId, item: storyItem)])
|
||||
}
|
||||
account.stateManager.injectStoryUpdates(updates: [InternalStoryUpdate.updateMyReaction(peerId: peerId, id: id, reaction: .stars)])
|
||||
|
||||
let _ = inputPeer
|
||||
|
||||
//TODO:release
|
||||
return .complete()
|
||||
|
||||
/*return account.network.request(Api.functions.stories.sendReaction(flags: 0, peer: inputPeer, storyId: id, reaction: .reactionPaid))
|
||||
|> map(Optional.init)
|
||||
|> `catch` { _ -> Signal<Api.Updates?, NoError> in
|
||||
return .single(nil)
|
||||
}
|
||||
|> mapToSignal { updates -> Signal<Never, NoError> in
|
||||
if let updates = updates {
|
||||
account.stateManager.addUpdates(updates)
|
||||
}
|
||||
|
||||
return .complete()
|
||||
}*/
|
||||
}
|
||||
}
|
||||
|
||||
@ -1491,6 +1491,10 @@ public extension TelegramEngine {
|
||||
return _internal_setStoryReaction(account: self.account, peerId: peerId, id: id, reaction: reaction)
|
||||
}
|
||||
|
||||
public func sendStoryStars(peerId: EnginePeer.Id, id: Int32, count: Int) -> Signal<Never, NoError> {
|
||||
return _internal_sendStoryStars(account: self.account, peerId: peerId, id: id, count: count)
|
||||
}
|
||||
|
||||
public func getStory(peerId: EnginePeer.Id, id: Int32) -> Signal<EngineStoryItem?, NoError> {
|
||||
return _internal_getStoryById(accountPeerId: self.account.peerId, postbox: self.account.postbox, network: self.account.network, peerId: peerId, id: id)
|
||||
}
|
||||
|
||||
@ -440,6 +440,7 @@ public class ChatMessagePaymentAlertController: AlertController {
|
||||
options: options,
|
||||
purpose: .generic,
|
||||
targetPeerId: nil,
|
||||
customTheme: nil,
|
||||
completion: { _ in }
|
||||
)
|
||||
navigationController.pushViewController(controller)
|
||||
|
||||
@ -37,6 +37,7 @@ swift_library(
|
||||
"//submodules/TelegramUI/Components/Stars/StarsBalanceOverlayComponent",
|
||||
"//submodules/TelegramStringFormatting",
|
||||
"//submodules/TelegramUI/Components/ChatScheduleTimeController",
|
||||
"//submodules/TelegramUI/Components/Stories/LiveChat/StoryLiveChatMessageComponent",
|
||||
],
|
||||
visibility = [
|
||||
"//visibility:public",
|
||||
|
||||
@ -25,6 +25,7 @@ import ContextUI
|
||||
import StarsBalanceOverlayComponent
|
||||
import TelegramStringFormatting
|
||||
import ChatScheduleTimeController
|
||||
import StoryLiveChatMessageComponent
|
||||
|
||||
private final class BadgeComponent: Component {
|
||||
let theme: PresentationTheme
|
||||
@ -883,6 +884,7 @@ private final class ChatSendStarsScreenComponent: Component {
|
||||
private let badge = ComponentView<Empty>()
|
||||
|
||||
private var liveStreamPerks: [ComponentView<Empty>] = []
|
||||
private var liveStreamMessagePreview: ComponentView<Empty>?
|
||||
|
||||
private var topPeersLeftSeparator: SimpleLayer?
|
||||
private var topPeersRightSeparator: SimpleLayer?
|
||||
@ -1299,17 +1301,23 @@ private final class ChatSendStarsScreenComponent: Component {
|
||||
}
|
||||
|
||||
if reactData.myTopPeer != nil {
|
||||
let mappedPrivacy: TelegramPaidReactionPrivacy
|
||||
switch self.privacyPeer {
|
||||
case .account:
|
||||
mappedPrivacy = .default
|
||||
case .anonymous:
|
||||
mappedPrivacy = .anonymous
|
||||
case let .peer(peer):
|
||||
mappedPrivacy = .peer(peer.id)
|
||||
switch reactData.reactSubject {
|
||||
case let .message(messageId):
|
||||
let mappedPrivacy: TelegramPaidReactionPrivacy
|
||||
switch self.privacyPeer {
|
||||
case .account:
|
||||
mappedPrivacy = .default
|
||||
case .anonymous:
|
||||
mappedPrivacy = .anonymous
|
||||
case let .peer(peer):
|
||||
mappedPrivacy = .peer(peer.id)
|
||||
}
|
||||
|
||||
let _ = component.context.engine.messages.updateStarsReactionPrivacy(id: messageId, privacy: mappedPrivacy).startStandalone()
|
||||
case .liveStream:
|
||||
//TODO:release
|
||||
break
|
||||
}
|
||||
|
||||
let _ = component.context.engine.messages.updateStarsReactionPrivacy(id: reactData.messageId, privacy: mappedPrivacy).startStandalone()
|
||||
}
|
||||
}
|
||||
|
||||
@ -1363,6 +1371,8 @@ private final class ChatSendStarsScreenComponent: Component {
|
||||
targetPeerId = liveStreamMessageData.peer.id
|
||||
}
|
||||
|
||||
let customTheme = environment.theme
|
||||
|
||||
let _ = (context.engine.payments.starsTopUpOptions()
|
||||
|> take(1)
|
||||
|> deliverOnMainQueue).startStandalone(next: { options in
|
||||
@ -1372,6 +1382,7 @@ private final class ChatSendStarsScreenComponent: Component {
|
||||
options: options,
|
||||
purpose: .generic,
|
||||
targetPeerId: targetPeerId,
|
||||
customTheme: customTheme,
|
||||
completion: { _ in }
|
||||
)
|
||||
navigationController.pushViewController(controller)
|
||||
@ -1543,7 +1554,10 @@ private final class ChatSendStarsScreenComponent: Component {
|
||||
environment: {},
|
||||
containerSize: CGSize(width: availableSize.width - sliderInset * 2.0, height: 30.0)
|
||||
)
|
||||
let sliderFrame = CGRect(origin: CGPoint(x: sliderInset, y: contentHeight + 127.0), size: sliderSize)
|
||||
|
||||
contentHeight += 148.0
|
||||
|
||||
let sliderFrame = CGRect(origin: CGPoint(x: sliderInset, y: contentHeight), size: sliderSize)
|
||||
let sliderBackgroundFrame = CGRect(origin: CGPoint(x: sliderFrame.minX - 8.0, y: sliderFrame.minY + 7.0), size: CGSize(width: sliderFrame.width + 16.0, height: sliderFrame.height - 14.0))
|
||||
|
||||
let progressFraction: CGFloat = CGFloat(self.amount.sliderValue) / CGFloat(self.amount.maxSliderValue)
|
||||
@ -1578,8 +1592,14 @@ private final class ChatSendStarsScreenComponent: Component {
|
||||
} else {
|
||||
self.isPastTopCutoff = nil
|
||||
}
|
||||
|
||||
if case .liveStream = reactData.reactSubject {
|
||||
let color = GroupCallMessagesContext.getStarAmountParamMapping(value: Int64(self.amount.realValue)).color ?? .purple
|
||||
sliderColor = StoryLiveChatMessageComponent.getMessageColor(color: color)
|
||||
}
|
||||
case .liveStreamMessage:
|
||||
sliderColor = getLiveStreamStarAmountColorMapping(value: Int64(self.amount.realValue))
|
||||
let color = GroupCallMessagesContext.getStarAmountParamMapping(value: Int64(self.amount.realValue)).color ?? .purple
|
||||
sliderColor = StoryLiveChatMessageComponent.getMessageColor(color: color)
|
||||
}
|
||||
|
||||
let _ = self.sliderBackground.update(
|
||||
@ -1651,8 +1671,6 @@ private final class ChatSendStarsScreenComponent: Component {
|
||||
|
||||
switch component.initialData.subjectInitialData {
|
||||
case .liveStreamMessage:
|
||||
//LiveStreamPerkComponent
|
||||
|
||||
//TODO:localize
|
||||
let params = GroupCallMessagesContext.getStarAmountParamMapping(value: Int64(self.amount.realValue))
|
||||
var perks: [(String, String)] = []
|
||||
@ -1672,11 +1690,11 @@ private final class ChatSendStarsScreenComponent: Component {
|
||||
"emoji"
|
||||
))
|
||||
|
||||
contentHeight += 180.0
|
||||
contentHeight += 54.0
|
||||
|
||||
let perkHeight: CGFloat = 58.0
|
||||
let perkSpacing: CGFloat = 10.0
|
||||
let perkWidth: CGFloat = floor((availableSize.width - perkSpacing * CGFloat(perks.count - 1)) / CGFloat(perks.count))
|
||||
let perkWidth: CGFloat = floor((fillingSize - perkSpacing * CGFloat(perks.count - 1)) / CGFloat(perks.count))
|
||||
|
||||
for i in 0 ..< perks.count {
|
||||
var perkFrame = CGRect(origin: CGPoint(x: sideInset + CGFloat(i) * (perkWidth + perkSpacing), y: contentHeight), size: CGSize(width: perkWidth, height: perkHeight))
|
||||
@ -1709,9 +1727,10 @@ private final class ChatSendStarsScreenComponent: Component {
|
||||
}
|
||||
}
|
||||
|
||||
contentHeight += perkHeight - 46.0
|
||||
contentHeight += perkHeight
|
||||
contentHeight += 32.0
|
||||
case .react:
|
||||
contentHeight += 123.0
|
||||
contentHeight += 64.0
|
||||
}
|
||||
|
||||
switch component.initialData.subjectInitialData {
|
||||
@ -1738,7 +1757,7 @@ private final class ChatSendStarsScreenComponent: Component {
|
||||
environment: {},
|
||||
containerSize: CGSize(width: 120.0, height: 100.0)
|
||||
)
|
||||
let peerSelectorButtonFrame = CGRect(origin: CGPoint(x: sideInset, y: 1.0 + floor((72.0 - peerSelectorButtonSize.height) * 0.5)), size: peerSelectorButtonSize)
|
||||
let peerSelectorButtonFrame = CGRect(origin: CGPoint(x: availableSize.width - sideInset - peerSelectorButtonSize.width, y: floor((78.0 - peerSelectorButtonSize.height) * 0.5)), size: peerSelectorButtonSize)
|
||||
if let peerSelectorButtonView = self.peerSelectorButton.view {
|
||||
if peerSelectorButtonView.superview == nil {
|
||||
self.navigationBarContainer.addSubview(peerSelectorButtonView)
|
||||
@ -1753,7 +1772,7 @@ private final class ChatSendStarsScreenComponent: Component {
|
||||
if self.backgroundHandleView.image == nil {
|
||||
self.backgroundHandleView.image = generateStretchableFilledCircleImage(diameter: 5.0, color: .white)?.withRenderingMode(.alwaysTemplate)
|
||||
}
|
||||
self.backgroundHandleView.tintColor = UIColor(rgb: 0x808084, alpha: 0.1)
|
||||
self.backgroundHandleView.tintColor = environment.theme.list.itemPrimaryTextColor.withMultipliedAlpha(environment.theme.overallDarkAppearance ? 0.2 : 0.07)
|
||||
let backgroundHandleFrame = CGRect(origin: CGPoint(x: floor((availableSize.width - 36.0) * 0.5), y: 5.0), size: CGSize(width: 36.0, height: 5.0))
|
||||
if self.backgroundHandleView.superview == nil {
|
||||
self.navigationBarContainer.addSubview(self.backgroundHandleView)
|
||||
@ -1807,8 +1826,12 @@ private final class ChatSendStarsScreenComponent: Component {
|
||||
let subtitleText: String?
|
||||
switch component.initialData.subjectInitialData {
|
||||
case let .react(reactData):
|
||||
let currentMyPeer = self.currentMyPeer ?? reactData.myPeer
|
||||
subtitleText = environment.strings.SendStarReactions_SubtitleFrom(currentMyPeer.compactDisplayTitle).string
|
||||
if case .message = reactData.reactSubject {
|
||||
let currentMyPeer = self.currentMyPeer ?? reactData.myPeer
|
||||
subtitleText = environment.strings.SendStarReactions_SubtitleFrom(currentMyPeer.compactDisplayTitle).string
|
||||
} else {
|
||||
subtitleText = nil
|
||||
}
|
||||
case .liveStreamMessage:
|
||||
subtitleText = nil
|
||||
}
|
||||
@ -1850,7 +1873,7 @@ private final class ChatSendStarsScreenComponent: Component {
|
||||
titleSubtitleHeight = titleSize.height
|
||||
}
|
||||
|
||||
let titleFrame = CGRect(origin: CGPoint(x: floor((availableSize.width - titleSize.width) * 0.5), y: floor((72.0 - titleSubtitleHeight) * 0.5)), size: titleSize)
|
||||
let titleFrame = CGRect(origin: CGPoint(x: floor((availableSize.width - titleSize.width) * 0.5), y: floor((78.0 - titleSubtitleHeight) * 0.5)), size: titleSize)
|
||||
if let titleView = title.view {
|
||||
if titleView.superview == nil {
|
||||
self.navigationBarContainer.addSubview(titleView)
|
||||
@ -1868,16 +1891,18 @@ private final class ChatSendStarsScreenComponent: Component {
|
||||
}
|
||||
}
|
||||
|
||||
contentHeight += 72.0
|
||||
contentHeight += 8.0
|
||||
|
||||
let text: String
|
||||
switch component.initialData.subjectInitialData {
|
||||
case let .react(reactData):
|
||||
if let currentSentAmount = reactData.currentSentAmount {
|
||||
text = environment.strings.SendStarReactions_TextSentStars(Int32(clamping: currentSentAmount))
|
||||
if case .liveStream = reactData.reactSubject {
|
||||
//TODO:localize
|
||||
text = "Highlight and pin a message\nby adding Stars for **\(reactData.peer.displayTitle(strings: environment.strings, displayOrder: .firstLast))**."
|
||||
} else {
|
||||
text = environment.strings.SendStarReactions_TextGeneric(reactData.peer.debugDisplayTitle).string
|
||||
if let currentSentAmount = reactData.currentSentAmount {
|
||||
text = environment.strings.SendStarReactions_TextSentStars(Int32(clamping: currentSentAmount))
|
||||
} else {
|
||||
text = environment.strings.SendStarReactions_TextGeneric(reactData.peer.debugDisplayTitle).string
|
||||
}
|
||||
}
|
||||
case let .liveStreamMessage(liveStreamMessageData):
|
||||
//TODO:localize
|
||||
@ -1910,10 +1935,80 @@ private final class ChatSendStarsScreenComponent: Component {
|
||||
}
|
||||
transition.setFrame(view: descriptionTextView, frame: descriptionTextFrame)
|
||||
}
|
||||
|
||||
contentHeight += descriptionTextFrame.height
|
||||
contentHeight += 22.0
|
||||
contentHeight += 2.0
|
||||
|
||||
var liveStreamMessagePreviewData: GroupCallMessagesContext.Message?
|
||||
if case let .liveStreamMessage(liveStreamMessage) =
|
||||
component.initialData.subjectInitialData, liveStreamMessage.text.length != 0 {
|
||||
let entities = generateChatInputTextEntities(liveStreamMessage.text, generateLinks: false)
|
||||
liveStreamMessagePreviewData = GroupCallMessagesContext.Message(
|
||||
id: GroupCallMessagesContext.Message.Id(
|
||||
space: .local,
|
||||
id: 1
|
||||
),
|
||||
author: liveStreamMessage.myPeer,
|
||||
text: liveStreamMessage.text.string,
|
||||
entities: entities,
|
||||
date: 0,
|
||||
lifetime: 0,
|
||||
paidStars: Int64(self.amount.realValue)
|
||||
)
|
||||
} else if case let .react(reactData) = component.initialData.subjectInitialData, case .liveStream = reactData.reactSubject {
|
||||
liveStreamMessagePreviewData = GroupCallMessagesContext.Message(
|
||||
id: GroupCallMessagesContext.Message.Id(
|
||||
space: .local,
|
||||
id: 1
|
||||
),
|
||||
author: reactData.myPeer,
|
||||
text: "",
|
||||
entities: [],
|
||||
date: 0,
|
||||
lifetime: 0,
|
||||
paidStars: Int64(self.amount.realValue)
|
||||
)
|
||||
}
|
||||
|
||||
if let liveStreamMessagePreviewData {
|
||||
contentHeight += 29.0
|
||||
|
||||
let liveStreamMessagePreview: ComponentView<Empty>
|
||||
if let current = self.liveStreamMessagePreview {
|
||||
liveStreamMessagePreview = current
|
||||
} else {
|
||||
liveStreamMessagePreview = ComponentView()
|
||||
self.liveStreamMessagePreview = liveStreamMessagePreview
|
||||
}
|
||||
|
||||
let liveStreamMessagePreviewSize = liveStreamMessagePreview.update(
|
||||
transition: transition,
|
||||
component: AnyComponent(StoryLiveChatMessageComponent(
|
||||
context: component.context,
|
||||
strings: environment.strings,
|
||||
theme: environment.theme,
|
||||
layout: StoryLiveChatMessageComponent.Layout(
|
||||
isFlipped: false,
|
||||
insets: UIEdgeInsets(top: 0.0, left: 0.0, bottom: 0.0, right: 0.0),
|
||||
fitToWidth: true,
|
||||
transparentBackground: false
|
||||
),
|
||||
message: liveStreamMessagePreviewData,
|
||||
contextGesture: nil
|
||||
)),
|
||||
environment: {},
|
||||
containerSize: CGSize(width: min(fillingSize - sideInset * 2.0, 290.0), height: 100000.0)
|
||||
)
|
||||
let liveStreamMessagePreviewFrame = CGRect(origin: CGPoint(x: floor((fillingSize - liveStreamMessagePreviewSize.width) * 0.5), y: contentHeight), size: liveStreamMessagePreviewSize)
|
||||
if let liveStreamMessagePreviewView = liveStreamMessagePreview.view {
|
||||
if liveStreamMessagePreviewView.superview == nil {
|
||||
self.scrollContentView.addSubview(liveStreamMessagePreviewView)
|
||||
}
|
||||
transition.setFrame(view: liveStreamMessagePreviewView, frame: liveStreamMessagePreviewFrame)
|
||||
}
|
||||
contentHeight += liveStreamMessagePreviewSize.height
|
||||
contentHeight += 28.0
|
||||
} else {
|
||||
contentHeight += 24.0
|
||||
}
|
||||
|
||||
switch component.initialData.subjectInitialData {
|
||||
case let .react(reactData):
|
||||
@ -2144,7 +2239,7 @@ private final class ChatSendStarsScreenComponent: Component {
|
||||
itemComponentView.alpha = 0.0
|
||||
}
|
||||
|
||||
let itemFrame = CGRect(origin: CGPoint(x: itemX, y: contentHeight + 72.0), size: itemSize)
|
||||
let itemFrame = CGRect(origin: CGPoint(x: itemX, y: contentHeight + 60.0), size: itemSize)
|
||||
|
||||
if animateItem {
|
||||
itemPositionTransition.setPosition(view: itemComponentView, position: itemFrame.center)
|
||||
@ -2160,7 +2255,7 @@ private final class ChatSendStarsScreenComponent: Component {
|
||||
itemX += itemSize.width + itemSpacing
|
||||
}
|
||||
|
||||
contentHeight += 161.0
|
||||
contentHeight += 164.0
|
||||
}
|
||||
|
||||
if !reactData.topPeers.isEmpty {
|
||||
@ -2190,7 +2285,7 @@ private final class ChatSendStarsScreenComponent: Component {
|
||||
selected: self.privacyPeer != .anonymous
|
||||
))),
|
||||
AnyComponentWithIdentity(id: AnyHashable(1), component: AnyComponent(MultilineTextComponent(
|
||||
text: .plain(NSAttributedString(string: environment.strings.SendStarReactions_ShowMyselfInTop, font: Font.regular(16.0), textColor: environment.theme.list.itemPrimaryTextColor))
|
||||
text: .plain(NSAttributedString(string: environment.strings.SendStarReactions_ShowMyselfInTop, font: Font.regular(17.0), textColor: environment.theme.list.itemPrimaryTextColor))
|
||||
)))
|
||||
], spacing: 10.0)),
|
||||
effectAlignment: .center,
|
||||
@ -2214,17 +2309,22 @@ private final class ChatSendStarsScreenComponent: Component {
|
||||
self.state?.updated(transition: .easeInOut(duration: 0.2))
|
||||
|
||||
if reactData.myTopPeer != nil {
|
||||
let mappedPrivacy: TelegramPaidReactionPrivacy
|
||||
switch self.privacyPeer {
|
||||
case .account:
|
||||
mappedPrivacy = .default
|
||||
case .anonymous:
|
||||
mappedPrivacy = .anonymous
|
||||
case let .peer(peer):
|
||||
mappedPrivacy = .peer(peer.id)
|
||||
switch reactData.reactSubject {
|
||||
case let .message(messageId):let mappedPrivacy: TelegramPaidReactionPrivacy
|
||||
switch self.privacyPeer {
|
||||
case .account:
|
||||
mappedPrivacy = .default
|
||||
case .anonymous:
|
||||
mappedPrivacy = .anonymous
|
||||
case let .peer(peer):
|
||||
mappedPrivacy = .peer(peer.id)
|
||||
}
|
||||
|
||||
let _ = component.context.engine.messages.updateStarsReactionPrivacy(id: messageId, privacy: mappedPrivacy).startStandalone()
|
||||
case .liveStream:
|
||||
//TODO:release
|
||||
break
|
||||
}
|
||||
|
||||
let _ = component.context.engine.messages.updateStarsReactionPrivacy(id: reactData.messageId, privacy: mappedPrivacy).startStandalone()
|
||||
}
|
||||
},
|
||||
animateAlpha: false,
|
||||
@ -2246,7 +2346,7 @@ private final class ChatSendStarsScreenComponent: Component {
|
||||
transition.setFrame(view: anonymousContentsView, frame: anonymousContentsFrame)
|
||||
}
|
||||
|
||||
contentHeight += anonymousContentsSize.height + 27.0
|
||||
contentHeight += anonymousContentsSize.height + 16.0
|
||||
case .liveStreamMessage:
|
||||
break
|
||||
}
|
||||
@ -2272,6 +2372,8 @@ private final class ChatSendStarsScreenComponent: Component {
|
||||
buttonAttributedString.addAttribute(.baselineOffset, value: 1.0, range: NSRange(range, in: buttonAttributedString.string))
|
||||
}
|
||||
|
||||
let buttonInsets = ContainerViewLayout.concentricInsets(bottomInset: environment.safeInsets.bottom, innerDiameter: 54.0, sideInset: 32.0)
|
||||
|
||||
let actionButtonSize = actionButton.update(
|
||||
transition: transition,
|
||||
component: AnyComponent(ButtonComponent(
|
||||
@ -2280,7 +2382,7 @@ private final class ChatSendStarsScreenComponent: Component {
|
||||
color: environment.theme.list.itemCheckColors.fillColor,
|
||||
foreground: environment.theme.list.itemCheckColors.foregroundColor,
|
||||
pressedColor: environment.theme.list.itemCheckColors.fillColor.withMultipliedAlpha(0.9),
|
||||
cornerRadius: 25.0
|
||||
cornerRadius: 54.0 * 0.5
|
||||
),
|
||||
content: AnyComponentWithIdentity(
|
||||
id: AnyHashable(0),
|
||||
@ -2316,7 +2418,7 @@ private final class ChatSendStarsScreenComponent: Component {
|
||||
purchasePurpose = .reactions(peerId: liveStreamMessageData.peer.id, requiredStars: Int64(self.amount.realValue))
|
||||
}
|
||||
|
||||
let purchaseScreen = component.context.sharedContext.makeStarsPurchaseScreen(context: component.context, starsContext: starsContext, options: options, purpose: purchasePurpose, targetPeerId: nil, completion: { result in
|
||||
let purchaseScreen = component.context.sharedContext.makeStarsPurchaseScreen(context: component.context, starsContext: starsContext, options: options, purpose: purchasePurpose, targetPeerId: nil, customTheme: environment.theme, completion: { result in
|
||||
let _ = result
|
||||
//TODO:release
|
||||
})
|
||||
@ -2369,7 +2471,7 @@ private final class ChatSendStarsScreenComponent: Component {
|
||||
}
|
||||
)),
|
||||
environment: {},
|
||||
containerSize: CGSize(width: availableSize.width - sideInset * 2.0, height: 50.0)
|
||||
containerSize: CGSize(width: availableSize.width - buttonInsets.left - buttonInsets.right, height: 54.0)
|
||||
)
|
||||
|
||||
var buttonDescriptionTextSize: CGSize?
|
||||
@ -2408,13 +2510,14 @@ private final class ChatSendStarsScreenComponent: Component {
|
||||
}
|
||||
let buttonDescriptionSpacing: CGFloat = 14.0
|
||||
|
||||
var bottomPanelHeight = 13.0 + environment.safeInsets.bottom + actionButtonSize.height
|
||||
var bottomPanelHeight = 13.0 + buttonInsets.bottom + actionButtonSize.height
|
||||
var actionButtonFrame = CGRect(origin: CGPoint(x: buttonInsets.left, y: availableSize.height - buttonInsets.bottom - actionButtonSize.height), size: actionButtonSize)
|
||||
if let buttonDescriptionTextSize {
|
||||
bottomPanelHeight += buttonDescriptionSpacing + buttonDescriptionTextSize.height
|
||||
actionButtonFrame.origin.y -= (buttonDescriptionSpacing + buttonDescriptionTextSize.height)
|
||||
} else {
|
||||
bottomPanelHeight -= 1.0
|
||||
}
|
||||
let actionButtonFrame = CGRect(origin: CGPoint(x: sideInset, y: availableSize.height - bottomPanelHeight), size: actionButtonSize)
|
||||
if let actionButtonView = actionButton.view {
|
||||
if actionButtonView.superview == nil {
|
||||
self.containerView.addSubview(actionButtonView)
|
||||
@ -2483,25 +2586,30 @@ private final class ChatSendStarsScreenComponent: Component {
|
||||
}
|
||||
|
||||
public class ChatSendStarsScreen: ViewControllerComponentContainer {
|
||||
public enum ReactSubject {
|
||||
case message(EngineMessage.Id)
|
||||
case liveStream(peerId: EnginePeer.Id, storyId: Int32)
|
||||
}
|
||||
|
||||
fileprivate enum SubjectInitialData {
|
||||
final class React {
|
||||
let peer: EnginePeer
|
||||
let myPeer: EnginePeer
|
||||
let defaultPrivacyPeer: ChatSendStarsScreenComponent.PrivacyPeer
|
||||
let channelsForPublicReaction: [EnginePeer]
|
||||
let messageId: EngineMessage.Id
|
||||
let reactSubject: ReactSubject
|
||||
let currentSentAmount: Int?
|
||||
let topPeers: [ChatSendStarsScreen.TopPeer]
|
||||
let myTopPeer: ChatSendStarsScreen.TopPeer?
|
||||
let maxAmount: Int
|
||||
let completion: (Int64, TelegramPaidReactionPrivacy, Bool, ChatSendStarsScreen.TransitionOut) -> Void
|
||||
|
||||
init(peer: EnginePeer, myPeer: EnginePeer, defaultPrivacyPeer: ChatSendStarsScreenComponent.PrivacyPeer, channelsForPublicReaction: [EnginePeer], messageId: EngineMessage.Id, currentSentAmount: Int?, topPeers: [ChatSendStarsScreen.TopPeer], myTopPeer: ChatSendStarsScreen.TopPeer?, maxAmount: Int, completion: @escaping (Int64, TelegramPaidReactionPrivacy, Bool, ChatSendStarsScreen.TransitionOut) -> Void) {
|
||||
init(peer: EnginePeer, myPeer: EnginePeer, defaultPrivacyPeer: ChatSendStarsScreenComponent.PrivacyPeer, channelsForPublicReaction: [EnginePeer], reactSubject: ReactSubject, currentSentAmount: Int?, topPeers: [ChatSendStarsScreen.TopPeer], myTopPeer: ChatSendStarsScreen.TopPeer?, maxAmount: Int, completion: @escaping (Int64, TelegramPaidReactionPrivacy, Bool, ChatSendStarsScreen.TransitionOut) -> Void) {
|
||||
self.peer = peer
|
||||
self.myPeer = myPeer
|
||||
self.defaultPrivacyPeer = defaultPrivacyPeer
|
||||
self.channelsForPublicReaction = channelsForPublicReaction
|
||||
self.messageId = messageId
|
||||
self.reactSubject = reactSubject
|
||||
self.currentSentAmount = currentSentAmount
|
||||
self.topPeers = topPeers
|
||||
self.myTopPeer = myTopPeer
|
||||
@ -2512,12 +2620,22 @@ public class ChatSendStarsScreen: ViewControllerComponentContainer {
|
||||
|
||||
final class LiveStreamMessage {
|
||||
let peer: EnginePeer
|
||||
let myPeer: EnginePeer
|
||||
let maxAmount: Int
|
||||
let text: NSAttributedString
|
||||
let completion: (Int64, ChatSendStarsScreen.TransitionOut) -> Void
|
||||
|
||||
init(peer: EnginePeer, maxAmount: Int, completion: @escaping (Int64, ChatSendStarsScreen.TransitionOut) -> Void) {
|
||||
init(
|
||||
peer: EnginePeer,
|
||||
myPeer: EnginePeer,
|
||||
maxAmount: Int,
|
||||
text: NSAttributedString,
|
||||
completion: @escaping (Int64, ChatSendStarsScreen.TransitionOut) -> Void
|
||||
) {
|
||||
self.peer = peer
|
||||
self.myPeer = myPeer
|
||||
self.maxAmount = maxAmount
|
||||
self.text = text
|
||||
self.completion = completion
|
||||
}
|
||||
}
|
||||
@ -2638,7 +2756,7 @@ public class ChatSendStarsScreen: ViewControllerComponentContainer {
|
||||
}
|
||||
}
|
||||
|
||||
public static func initialData(context: AccountContext, peerId: EnginePeer.Id, messageId: EngineMessage.Id, topPeers: [ReactionsMessageAttribute.TopPeer], completion: @escaping (Int64, TelegramPaidReactionPrivacy, Bool, TransitionOut) -> Void) -> Signal<InitialData?, NoError> {
|
||||
public static func initialData(context: AccountContext, peerId: EnginePeer.Id, reactSubject: ReactSubject, topPeers: [ReactionsMessageAttribute.TopPeer], completion: @escaping (Int64, TelegramPaidReactionPrivacy, Bool, TransitionOut) -> Void) -> Signal<InitialData?, NoError> {
|
||||
let balance: Signal<StarsAmount?, NoError>
|
||||
if let starsContext = context.starsContext {
|
||||
balance = starsContext.state
|
||||
@ -2717,7 +2835,7 @@ public class ChatSendStarsScreen: ViewControllerComponentContainer {
|
||||
myPeer: myPeer,
|
||||
defaultPrivacyPeer: defaultPrivacyPeer,
|
||||
channelsForPublicReaction: channelsForPublicReaction,
|
||||
messageId: messageId,
|
||||
reactSubject: reactSubject,
|
||||
currentSentAmount: currentSentAmount,
|
||||
topPeers: topPeers.compactMap { topPeer -> ChatSendStarsScreen.TopPeer? in
|
||||
guard let topPeerId = topPeer.peerId else {
|
||||
@ -2775,7 +2893,12 @@ public class ChatSendStarsScreen: ViewControllerComponentContainer {
|
||||
}
|
||||
}
|
||||
|
||||
public static func initialDataLiveStreamMessage(context: AccountContext, peerId: EnginePeer.Id, completion: @escaping (Int64, TransitionOut) -> Void) -> Signal<InitialData?, NoError> {
|
||||
public static func initialDataLiveStreamMessage(
|
||||
context: AccountContext,
|
||||
peerId: EnginePeer.Id,
|
||||
text: NSAttributedString,
|
||||
completion: @escaping (Int64, TransitionOut) -> Void
|
||||
) -> Signal<InitialData?, NoError> {
|
||||
let balance: Signal<StarsAmount?, NoError>
|
||||
if let starsContext = context.starsContext {
|
||||
balance = starsContext.state
|
||||
@ -2795,18 +2918,22 @@ public class ChatSendStarsScreen: ViewControllerComponentContainer {
|
||||
return combineLatest(
|
||||
context.engine.data.get(
|
||||
TelegramEngine.EngineData.Item.Peer.Peer(id: peerId),
|
||||
TelegramEngine.EngineData.Item.Peer.Peer(id: context.account.peerId),
|
||||
),
|
||||
balance
|
||||
)
|
||||
|> map { peer, balance -> InitialData? in
|
||||
guard let peer else {
|
||||
|> map { peers, balance -> InitialData? in
|
||||
let (peer, myPeer) = peers
|
||||
guard let peer, let myPeer else {
|
||||
return nil
|
||||
}
|
||||
|
||||
return InitialData(
|
||||
subjectInitialData: .liveStreamMessage(SubjectInitialData.LiveStreamMessage(
|
||||
peer: peer,
|
||||
myPeer: myPeer,
|
||||
maxAmount: maxAmount,
|
||||
text: text,
|
||||
completion: completion
|
||||
)),
|
||||
balance: balance
|
||||
@ -2976,9 +3103,38 @@ private final class BadgeStarsView: UIView {
|
||||
}
|
||||
|
||||
func update(size: CGSize, color: UIColor, emitterPosition: CGPoint) {
|
||||
if self.staticEmitterLayer.emitterCells == nil || self.currentColor != color {
|
||||
if self.staticEmitterLayer.emitterCells == nil {
|
||||
self.currentColor = color
|
||||
self.setupEmitter()
|
||||
} else if self.currentColor != color {
|
||||
self.currentColor = color
|
||||
|
||||
let staticColors: [Any] = [
|
||||
UIColor.white.withAlphaComponent(0.0).cgColor,
|
||||
UIColor.white.withAlphaComponent(0.35).cgColor,
|
||||
color.cgColor,
|
||||
color.cgColor,
|
||||
color.withAlphaComponent(0.0).cgColor
|
||||
]
|
||||
let staticColorBehavior = CAEmitterCell.createEmitterBehavior(type: "colorOverLife")
|
||||
staticColorBehavior.setValue(staticColors, forKey: "colors")
|
||||
|
||||
let dynamicColors: [Any] = [
|
||||
UIColor.white.withAlphaComponent(0.35).cgColor,
|
||||
color.withAlphaComponent(0.85).cgColor,
|
||||
color.cgColor,
|
||||
color.cgColor,
|
||||
color.withAlphaComponent(0.0).cgColor
|
||||
]
|
||||
let dynamicColorBehavior = CAEmitterCell.createEmitterBehavior(type: "colorOverLife")
|
||||
dynamicColorBehavior.setValue(dynamicColors, forKey: "colors")
|
||||
|
||||
for cell in self.staticEmitterLayer.emitterCells ?? [] {
|
||||
cell.setValue([staticColorBehavior], forKey: "emitterBehaviors")
|
||||
}
|
||||
for cell in self.dynamicEmitterLayer.emitterCells ?? [] {
|
||||
cell.setValue([dynamicColorBehavior], forKey: "emitterBehaviors")
|
||||
}
|
||||
}
|
||||
|
||||
self.staticEmitterLayer.frame = CGRect(origin: .zero, size: size)
|
||||
@ -3328,25 +3484,3 @@ private final class LiveStreamPerkComponent: Component {
|
||||
return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition)
|
||||
}
|
||||
}
|
||||
|
||||
private func getLiveStreamStarAmountColorMapping(value: Int64) -> UIColor {
|
||||
if value >= 10000 {
|
||||
return UIColor(rgb: 0x7C8695)
|
||||
}
|
||||
if value >= 2000 {
|
||||
return UIColor(rgb: 0xE6514E)
|
||||
}
|
||||
if value >= 500 {
|
||||
return UIColor(rgb: 0xEE7E20)
|
||||
}
|
||||
if value >= 250 {
|
||||
return UIColor(rgb: 0xE4A20A)
|
||||
}
|
||||
if value >= 100 {
|
||||
return UIColor(rgb: 0x5AB03D)
|
||||
}
|
||||
if value >= 50 {
|
||||
return UIColor(rgb: 0x3E9CDF)
|
||||
}
|
||||
return UIColor(rgb: 0x985FDC)
|
||||
}
|
||||
|
||||
@ -26,6 +26,7 @@ swift_library(
|
||||
"//submodules/ComponentFlow",
|
||||
"//submodules/AnimatedCountLabelNode",
|
||||
"//submodules/TelegramUI/Components/GlassBackgroundComponent",
|
||||
"//submodules/TelegramUI/Components/StarsParticleEffect",
|
||||
],
|
||||
visibility = [
|
||||
"//visibility:public",
|
||||
|
||||
@ -17,70 +17,7 @@ import ComponentFlow
|
||||
import AnimatedCountLabelNode
|
||||
import GlassBackgroundComponent
|
||||
import ComponentDisplayAdapters
|
||||
|
||||
private final class StarsButtonEffectLayer: SimpleLayer {
|
||||
let emitterLayer = CAEmitterLayer()
|
||||
private var currentColor: UIColor?
|
||||
|
||||
override init() {
|
||||
super.init()
|
||||
|
||||
self.addSublayer(self.emitterLayer)
|
||||
}
|
||||
|
||||
override init(layer: Any) {
|
||||
super.init(layer: layer)
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
private func setup() {
|
||||
guard let currentColor = self.currentColor else {
|
||||
return
|
||||
}
|
||||
let color = currentColor
|
||||
|
||||
let emitter = CAEmitterCell()
|
||||
emitter.name = "emitter"
|
||||
emitter.contents = UIImage(bundleImageName: "Premium/Stars/Particle")?.cgImage
|
||||
emitter.birthRate = 25.0
|
||||
emitter.lifetime = 2.0
|
||||
emitter.velocity = 12.0
|
||||
emitter.velocityRange = 3
|
||||
emitter.scale = 0.1
|
||||
emitter.scaleRange = 0.08
|
||||
emitter.alphaRange = 0.1
|
||||
emitter.emissionRange = .pi * 2.0
|
||||
emitter.setValue(3.0, forKey: "mass")
|
||||
emitter.setValue(2.0, forKey: "massRange")
|
||||
|
||||
let staticColors: [Any] = [
|
||||
color.withAlphaComponent(0.0).cgColor,
|
||||
color.cgColor,
|
||||
color.cgColor,
|
||||
color.withAlphaComponent(0.0).cgColor
|
||||
]
|
||||
let staticColorBehavior = CAEmitterCell.createEmitterBehavior(type: "colorOverLife")
|
||||
staticColorBehavior.setValue(staticColors, forKey: "colors")
|
||||
emitter.setValue([staticColorBehavior], forKey: "emitterBehaviors")
|
||||
|
||||
self.emitterLayer.emitterCells = [emitter]
|
||||
}
|
||||
|
||||
func update(color: UIColor, size: CGSize) {
|
||||
if self.emitterLayer.emitterCells == nil || self.currentColor != color {
|
||||
self.currentColor = color
|
||||
self.setup()
|
||||
}
|
||||
self.emitterLayer.emitterShape = .circle
|
||||
self.emitterLayer.emitterSize = CGSize(width: size.width * 0.7, height: size.height * 0.7)
|
||||
self.emitterLayer.emitterMode = .surface
|
||||
self.emitterLayer.frame = CGRect(origin: .zero, size: size)
|
||||
self.emitterLayer.emitterPosition = CGPoint(x: size.width / 2.0, y: size.height / 2.0)
|
||||
}
|
||||
}
|
||||
import StarsParticleEffect
|
||||
|
||||
private final class EffectBadgeView: UIView {
|
||||
private let context: AccountContext
|
||||
@ -203,7 +140,7 @@ public final class ChatTextInputActionButtonsNode: ASDisplayNode, ChatSendMessag
|
||||
|
||||
public let sendContainerNode: ASDisplayNode
|
||||
public let sendButtonBackgroundView: UIImageView
|
||||
private var sendButtonBackgroundEffectLayer: StarsButtonEffectLayer?
|
||||
private var sendButtonBackgroundEffectLayer: StarsParticleEffectLayer?
|
||||
public let sendButton: HighlightTrackingButtonNode
|
||||
public var sendButtonRadialStatusNode: ChatSendButtonRadialStatusNode?
|
||||
public var sendButtonHasApplyIcon = false
|
||||
@ -234,6 +171,7 @@ public final class ChatTextInputActionButtonsNode: ASDisplayNode, ChatSendMessag
|
||||
private var validLayout: CGSize?
|
||||
|
||||
public var customSendColor: UIColor?
|
||||
public var isSendDisabled: Bool = false
|
||||
|
||||
public init(context: AccountContext, presentationInterfaceState: ChatPresentationInterfaceState, presentationContext: ChatPresentationContext?, presentController: @escaping (ViewController) -> Void) {
|
||||
self.context = context
|
||||
@ -404,7 +342,6 @@ public final class ChatTextInputActionButtonsNode: ASDisplayNode, ChatSendMessag
|
||||
}
|
||||
transition.updateFrame(node: self.textNode, frame: CGRect(origin: CGPoint(x: showTitle ? 5.0 + 7.0 : floorToScreenPixels((innerSize.width - textSize.width) / 2.0), y: floorToScreenPixels((size.height - textSize.height) / 2.0)), size: textSize))
|
||||
} else {
|
||||
self.sendButton.imageNode.alpha = 1.0
|
||||
self.textNode.isHidden = true
|
||||
}
|
||||
|
||||
@ -416,17 +353,29 @@ public final class ChatTextInputActionButtonsNode: ASDisplayNode, ChatSendMessag
|
||||
|
||||
let sendButtonBackgroundFrame = CGRect(origin: CGPoint(), size: innerSize).insetBy(dx: 3.0, dy: 3.0)
|
||||
transition.updateFrame(view: self.sendButtonBackgroundView, frame: sendButtonBackgroundFrame)
|
||||
self.sendButtonBackgroundView.tintColor = self.customSendColor ?? interfaceState.theme.chat.inputPanel.panelControlAccentColor
|
||||
|
||||
if self.isSendDisabled {
|
||||
transition.updateTintColor(view: self.sendButtonBackgroundView, color: interfaceState.theme.chat.inputPanel.panelControlAccentColor.withMultiplied(hue: 1.0, saturation: 0.0, brightness: 0.5).withMultipliedAlpha(0.25))
|
||||
} else {
|
||||
transition.updateTintColor(view: self.sendButtonBackgroundView, color: self.customSendColor ?? interfaceState.theme.chat.inputPanel.panelControlAccentColor)
|
||||
}
|
||||
|
||||
if starsAmount == nil {
|
||||
if self.isSendDisabled {
|
||||
transition.updateAlpha(layer: self.sendButton.imageNode.layer, alpha: 0.4)
|
||||
} else {
|
||||
transition.updateAlpha(layer: self.sendButton.imageNode.layer, alpha: 1.0)
|
||||
}
|
||||
}
|
||||
|
||||
if let _ = self.customSendColor {
|
||||
let sendButtonBackgroundEffectLayer: StarsButtonEffectLayer
|
||||
let sendButtonBackgroundEffectLayer: StarsParticleEffectLayer
|
||||
var sendButtonBackgroundEffectLayerTransition = transition
|
||||
if let current = self.sendButtonBackgroundEffectLayer {
|
||||
sendButtonBackgroundEffectLayer = current
|
||||
} else {
|
||||
sendButtonBackgroundEffectLayerTransition = .immediate
|
||||
sendButtonBackgroundEffectLayer = StarsButtonEffectLayer()
|
||||
sendButtonBackgroundEffectLayer.masksToBounds = true
|
||||
sendButtonBackgroundEffectLayer = StarsParticleEffectLayer()
|
||||
self.sendButtonBackgroundEffectLayer = sendButtonBackgroundEffectLayer
|
||||
self.sendButtonBackgroundView.layer.addSublayer(sendButtonBackgroundEffectLayer)
|
||||
if transition.isAnimated {
|
||||
@ -434,8 +383,7 @@ public final class ChatTextInputActionButtonsNode: ASDisplayNode, ChatSendMessag
|
||||
}
|
||||
}
|
||||
transition.updateFrame(layer: sendButtonBackgroundEffectLayer, frame: CGRect(origin: CGPoint(), size: sendButtonBackgroundFrame.size))
|
||||
sendButtonBackgroundEffectLayerTransition.updateCornerRadius(layer: sendButtonBackgroundEffectLayer, cornerRadius: sendButtonBackgroundFrame.height * 0.5)
|
||||
sendButtonBackgroundEffectLayer.update(color: UIColor(white: 1.0, alpha: 0.5), size: sendButtonBackgroundFrame.size)
|
||||
sendButtonBackgroundEffectLayer.update(color: UIColor(white: 1.0, alpha: 0.5), size: sendButtonBackgroundFrame.size, cornerRadius: sendButtonBackgroundFrame.height * 0.5, transition: ComponentTransition(sendButtonBackgroundEffectLayerTransition))
|
||||
} else if let sendButtonBackgroundEffectLayer = self.sendButtonBackgroundEffectLayer {
|
||||
self.sendButtonBackgroundEffectLayer = nil
|
||||
transition.updateFrame(layer: sendButtonBackgroundEffectLayer, frame: CGRect(origin: CGPoint(), size: sendButtonBackgroundFrame.size))
|
||||
|
||||
@ -63,6 +63,7 @@ swift_library(
|
||||
"//submodules/TelegramUI/Components/Chat/ChatInputAutocompletePanel",
|
||||
"//submodules/TelegramUI/Components/Chat/ChatRecordingPreviewInputPanelNode",
|
||||
"//submodules/TelegramUI/Components/Chat/ChatInputContextPanelNode",
|
||||
"//submodules/TelegramUI/Components/AnimatedTextComponent",
|
||||
],
|
||||
visibility = [
|
||||
"//visibility:public",
|
||||
|
||||
@ -58,7 +58,7 @@ public final class ChatTextInputPanelComponent: Component {
|
||||
public final class LeftAction: Equatable {
|
||||
public enum Kind: Equatable {
|
||||
case attach
|
||||
case toggleExpanded(isVisible: Bool, isExpanded: Bool)
|
||||
case toggleExpanded(isVisible: Bool, isExpanded: Bool, hasUnseen: Bool)
|
||||
}
|
||||
|
||||
public let kind: Kind
|
||||
@ -83,17 +83,22 @@ public final class ChatTextInputPanelComponent: Component {
|
||||
}
|
||||
|
||||
public let kind: Kind
|
||||
public let action: () -> Void
|
||||
public let action: (UIView) -> Void
|
||||
public let longPressAction: ((UIView) -> Void)?
|
||||
|
||||
public init(kind: Kind, action: @escaping () -> Void) {
|
||||
public init(kind: Kind, action: @escaping (UIView) -> Void, longPressAction: ((UIView) -> Void)? = nil) {
|
||||
self.kind = kind
|
||||
self.action = action
|
||||
self.longPressAction = longPressAction
|
||||
}
|
||||
|
||||
public static func ==(lhs: RightAction, rhs: RightAction) -> Bool {
|
||||
if lhs.kind != rhs.kind {
|
||||
return false
|
||||
}
|
||||
if (lhs.longPressAction == nil) != (rhs.longPressAction == nil) {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
}
|
||||
@ -109,6 +114,7 @@ public final class ChatTextInputPanelComponent: Component {
|
||||
let placeholder: String
|
||||
let paidMessagePrice: StarsAmount?
|
||||
let sendColor: UIColor?
|
||||
let isSendDisabled: Bool
|
||||
let hideKeyboard: Bool
|
||||
let insets: UIEdgeInsets
|
||||
let maxHeight: CGFloat
|
||||
@ -128,6 +134,7 @@ public final class ChatTextInputPanelComponent: Component {
|
||||
placeholder: String,
|
||||
paidMessagePrice: StarsAmount?,
|
||||
sendColor: UIColor?,
|
||||
isSendDisabled: Bool,
|
||||
hideKeyboard: Bool,
|
||||
insets: UIEdgeInsets,
|
||||
maxHeight: CGFloat,
|
||||
@ -146,6 +153,7 @@ public final class ChatTextInputPanelComponent: Component {
|
||||
self.placeholder = placeholder
|
||||
self.paidMessagePrice = paidMessagePrice
|
||||
self.sendColor = sendColor
|
||||
self.isSendDisabled = isSendDisabled
|
||||
self.hideKeyboard = hideKeyboard
|
||||
self.insets = insets
|
||||
self.maxHeight = maxHeight
|
||||
@ -188,6 +196,9 @@ public final class ChatTextInputPanelComponent: Component {
|
||||
if lhs.sendColor != rhs.sendColor {
|
||||
return false
|
||||
}
|
||||
if lhs.isSendDisabled != rhs.isSendDisabled {
|
||||
return false
|
||||
}
|
||||
if lhs.hideKeyboard != rhs.hideKeyboard {
|
||||
return false
|
||||
}
|
||||
@ -706,6 +717,9 @@ public final class ChatTextInputPanelComponent: Component {
|
||||
mediaRecordingState: nil
|
||||
)
|
||||
}
|
||||
presentationInterfaceState = presentationInterfaceState.updatedInterfaceState { interfaceState in
|
||||
return interfaceState.withUpdatedEffectiveInputState(component.externalState.textInputState)
|
||||
}
|
||||
presentationInterfaceState = presentationInterfaceState.updatedSendPaidMessageStars(component.paidMessagePrice)
|
||||
|
||||
let panelNode: ChatTextInputPanelNode
|
||||
@ -770,8 +784,8 @@ public final class ChatTextInputPanelComponent: Component {
|
||||
switch leftAction.kind {
|
||||
case .attach:
|
||||
panelNode.customLeftAction = nil
|
||||
case let .toggleExpanded(isVisible, isExpanded):
|
||||
panelNode.customLeftAction = .toggleExpanded(isVisible: isVisible, isExpanded: isExpanded)
|
||||
case let .toggleExpanded(isVisible, isExpanded, hasUnseen):
|
||||
panelNode.customLeftAction = .toggleExpanded(isVisible: isVisible, isExpanded: isExpanded, hasUnseen: hasUnseen)
|
||||
}
|
||||
} else {
|
||||
panelNode.customLeftAction = nil
|
||||
@ -780,8 +794,12 @@ public final class ChatTextInputPanelComponent: Component {
|
||||
if let rightAction = component.rightAction {
|
||||
switch rightAction.kind {
|
||||
case let .stars(count, isFilled):
|
||||
panelNode.customRightAction = .stars(count: count, isFilled: isFilled, action: {
|
||||
rightAction.action()
|
||||
panelNode.customRightAction = .stars(count: count, isFilled: isFilled, action: { sourceView in
|
||||
rightAction.action(sourceView)
|
||||
}, longPressAction: rightAction.longPressAction.flatMap { longPressAction in
|
||||
return { sourceView in
|
||||
longPressAction(sourceView)
|
||||
}
|
||||
})
|
||||
}
|
||||
} else {
|
||||
@ -789,7 +807,22 @@ public final class ChatTextInputPanelComponent: Component {
|
||||
}
|
||||
|
||||
panelNode.customSendColor = component.sendColor
|
||||
panelNode.customSendIsDisabled = component.isSendDisabled
|
||||
panelNode.customInputTextMaxLength = component.maxLength
|
||||
panelNode.customSwitchToKeyboard = { [weak self] in
|
||||
guard let self, let component = self.component else {
|
||||
return
|
||||
}
|
||||
for inlineAction in component.inlineActions {
|
||||
switch inlineAction.kind {
|
||||
case .inputMode:
|
||||
inlineAction.action()
|
||||
return
|
||||
default:
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if let resetInputState = component.externalState.resetInputState {
|
||||
component.externalState.resetInputState = nil
|
||||
|
||||
@ -252,10 +252,12 @@ public class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDeleg
|
||||
public let sendAsAvatarReferenceNode: ContextReferenceContentNode
|
||||
public let sendAsAvatarContainerNode: ContextControllerSourceNode
|
||||
private let sendAsAvatarNode: AvatarNode
|
||||
private let sendAsCloseIconView: UIImageView
|
||||
|
||||
public let attachmentButton: HighlightTrackingButton
|
||||
public let attachmentButtonBackground: GlassBackgroundView
|
||||
public let attachmentButtonIcon: GlassBackgroundView.ContentImageView
|
||||
private var attachmentButtonUnseenIcon: UIImageView?
|
||||
public let attachmentButtonDisabledNode: HighlightableButtonNode
|
||||
|
||||
public var attachmentImageNode: TransformImageNode?
|
||||
@ -376,18 +378,20 @@ public class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDeleg
|
||||
}
|
||||
|
||||
public enum LeftAction {
|
||||
case toggleExpanded(isVisible: Bool, isExpanded: Bool)
|
||||
case toggleExpanded(isVisible: Bool, isExpanded: Bool, hasUnseen: Bool)
|
||||
}
|
||||
|
||||
public enum RightAction {
|
||||
case stars(count: Int, isFilled: Bool, action: () -> Void)
|
||||
case stars(count: Int, isFilled: Bool, action: (UIView) -> Void, longPressAction: ((UIView) -> Void)?)
|
||||
}
|
||||
|
||||
public var customPlaceholder: String?
|
||||
public var customLeftAction: LeftAction?
|
||||
public var customRightAction: RightAction?
|
||||
public var customSendColor: UIColor?
|
||||
public var customSendIsDisabled: Bool = false
|
||||
public var customInputTextMaxLength: Int?
|
||||
public var customSwitchToKeyboard: (() -> Void)?
|
||||
|
||||
private var starReactionButton: ComponentView<Empty>?
|
||||
|
||||
@ -623,6 +627,7 @@ public class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDeleg
|
||||
self.sendAsAvatarContainerNode = ContextControllerSourceNode()
|
||||
self.sendAsAvatarContainerNode.animateScale = false
|
||||
self.sendAsAvatarNode = AvatarNode(font: avatarPlaceholderFont(size: 16.0))
|
||||
self.sendAsCloseIconView = UIImageView()
|
||||
|
||||
self.attachmentButton = HighlightTrackingButton()
|
||||
self.attachmentButton.accessibilityLabel = presentationInterfaceState.strings.VoiceOver_AttachMedia
|
||||
@ -873,8 +878,9 @@ public class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDeleg
|
||||
|
||||
self.sendAsAvatarContainerNode.addSubnode(self.sendAsAvatarReferenceNode)
|
||||
self.sendAsAvatarReferenceNode.addSubnode(self.sendAsAvatarNode)
|
||||
self.sendAsAvatarReferenceNode.view.addSubview(self.sendAsCloseIconView)
|
||||
self.sendAsAvatarButtonNode.addSubnode(self.sendAsAvatarContainerNode)
|
||||
self.glassBackgroundContainer.contentView.addSubview(self.sendAsAvatarButtonNode.view)
|
||||
self.textInputContainerBackgroundView.contentView.addSubview(self.sendAsAvatarButtonNode.view)
|
||||
|
||||
self.glassBackgroundContainer.contentView.addSubview(self.menuButton.view)
|
||||
self.glassBackgroundContainer.contentView.addSubview(self.attachmentButtonBackground)
|
||||
@ -1120,7 +1126,7 @@ public class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDeleg
|
||||
return max(33.0, maxHeight - (textFieldInsets.top + textFieldInsets.bottom + self.textInputViewInternalInsets.top + self.textInputViewInternalInsets.bottom))
|
||||
}
|
||||
|
||||
private func calculateTextFieldMetrics(width: CGFloat, sendActionControlsWidth: CGFloat, maxHeight: CGFloat, metrics: LayoutMetrics, bottomInset: CGFloat) -> (accessoryButtonsWidth: CGFloat, textFieldHeight: CGFloat, isOverflow: Bool) {
|
||||
private func calculateTextFieldMetrics(width: CGFloat, sendActionControlsWidth: CGFloat, maxHeight: CGFloat, metrics: LayoutMetrics, bottomInset: CGFloat, interfaceState: ChatPresentationInterfaceState) -> (accessoryButtonsWidth: CGFloat, textFieldHeight: CGFloat, isOverflow: Bool) {
|
||||
let maxHeight = max(maxHeight, 40.0)
|
||||
|
||||
let textFieldInsets = self.textFieldInsets(metrics: metrics, bottomInset: bottomInset)
|
||||
@ -1154,10 +1160,20 @@ public class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDeleg
|
||||
textInputViewRealInsets = calculateTextFieldRealInsets(presentationInterfaceState: presentationInterfaceState, accessoryButtonsWidth: accessoryButtonsWidth, actionControlsWidth: sendActionControlsWidth)
|
||||
}
|
||||
|
||||
var hasSendAsButton = false
|
||||
if let sendAsPeers = interfaceState.sendAsPeers, !sendAsPeers.isEmpty && interfaceState.editMessageState == nil {
|
||||
hasSendAsButton = true
|
||||
}
|
||||
|
||||
var actualTextInputViewInternalInsets = self.textInputViewInternalInsets
|
||||
if hasSendAsButton {
|
||||
actualTextInputViewInternalInsets.left += 31.0
|
||||
}
|
||||
|
||||
var textFieldHeight: CGFloat
|
||||
var isOverflow = false
|
||||
if let textInputNode = self.textInputNode {
|
||||
let maxTextWidth = width - textFieldInsets.left - textFieldInsets.right - self.textInputViewInternalInsets.left - self.textInputViewInternalInsets.right
|
||||
let maxTextWidth = width - textFieldInsets.left - textFieldInsets.right - actualTextInputViewInternalInsets.left - actualTextInputViewInternalInsets.right
|
||||
let measuredHeight = textInputNode.textHeightForWidth(maxTextWidth, rightInset: textInputViewRealInsets.right)
|
||||
|
||||
let unboundTextFieldHeight = max(textFieldMinHeight, ceil(measuredHeight))
|
||||
@ -1177,7 +1193,10 @@ public class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDeleg
|
||||
}
|
||||
|
||||
private func textFieldInsets(metrics: LayoutMetrics, bottomInset: CGFloat) -> UIEdgeInsets {
|
||||
let insets = UIEdgeInsets(top: 0.0, left: 54.0, bottom: 0.0, right: 8.0)
|
||||
var insets = UIEdgeInsets(top: 0.0, left: 54.0, bottom: 0.0, right: 8.0)
|
||||
if let customLeftAction = self.customLeftAction, case let .toggleExpanded(isVisible, _, _) = customLeftAction, !isVisible {
|
||||
insets.left = 8.0
|
||||
}
|
||||
return insets
|
||||
}
|
||||
|
||||
@ -1349,6 +1368,7 @@ public class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDeleg
|
||||
let placeholderColor: UIColor = interfaceState.theme.chat.inputPanel.inputPlaceholderColor
|
||||
|
||||
self.sendActionButtons.customSendColor = self.customSendColor
|
||||
self.sendActionButtons.isSendDisabled = self.customSendIsDisabled
|
||||
|
||||
var transition = transition
|
||||
var additionalOffset: CGFloat = 0.0
|
||||
@ -1500,7 +1520,6 @@ public class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDeleg
|
||||
|
||||
var hasMenuButton = false
|
||||
var menuButtonExpanded = false
|
||||
var isSendAsButton = false
|
||||
|
||||
var shouldDisplayMenuButton = false
|
||||
if interfaceState.hasBotCommands {
|
||||
@ -1511,10 +1530,8 @@ public class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDeleg
|
||||
|
||||
let mediaRecordingState = interfaceState.inputTextPanelState.mediaRecordingState
|
||||
if let sendAsPeers = interfaceState.sendAsPeers, !sendAsPeers.isEmpty && interfaceState.editMessageState == nil {
|
||||
hasMenuButton = true
|
||||
menuButtonExpanded = false
|
||||
isSendAsButton = true
|
||||
self.sendAsAvatarNode.isHidden = false
|
||||
self.sendAsAvatarButtonNode.isHidden = false
|
||||
|
||||
var currentPeer = sendAsPeers.first(where: { $0.peer.id == interfaceState.currentSendAsPeerId})?.peer
|
||||
if currentPeer == nil {
|
||||
@ -1534,9 +1551,9 @@ public class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDeleg
|
||||
break
|
||||
}
|
||||
}
|
||||
self.sendAsAvatarNode.isHidden = true
|
||||
self.sendAsAvatarButtonNode.isHidden = true
|
||||
} else {
|
||||
self.sendAsAvatarNode.isHidden = true
|
||||
self.sendAsAvatarButtonNode.isHidden = true
|
||||
}
|
||||
if mediaRecordingState != nil || interfaceState.interfaceState.mediaDraftState != nil {
|
||||
hasMenuButton = false
|
||||
@ -1625,13 +1642,26 @@ public class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDeleg
|
||||
} else if case .commands = interfaceState.botMenuButton, self.menuButtonIconNode.iconState == .app {
|
||||
self.menuButtonIconNode.enqueueState(.menu, animated: false)
|
||||
}
|
||||
if themeUpdated {
|
||||
if themeUpdated || isFirstTime {
|
||||
self.menuButtonIconNode.customColor = interfaceState.theme.chat.inputPanel.actionControlForegroundColor
|
||||
self.startButton.updateTheme(SolidRoundedButtonTheme(theme: interfaceState.theme))
|
||||
|
||||
self.sendAsCloseIconView.image = generateImage(CGSize(width: 34.0, height: 34.0), rotatedContext: { size, context in
|
||||
context.clear(CGRect(origin: CGPoint(), size: size))
|
||||
context.setFillColor(interfaceState.theme.list.itemCheckColors.fillColor.cgColor)
|
||||
context.fillEllipse(in: CGRect(origin: CGPoint(), size: size))
|
||||
|
||||
context.setStrokeColor(interfaceState.theme.list.itemCheckColors.foregroundColor.cgColor)
|
||||
context.setLineWidth(1.66)
|
||||
context.setLineCap(.round)
|
||||
context.move(to: CGPoint(x: 11.0, y: 11.0))
|
||||
context.addLine(to: CGPoint(x: size.width - 11.0, y: size.height - 11.0))
|
||||
context.move(to: CGPoint(x: size.width - 11.0, y: 11.0))
|
||||
context.addLine(to: CGPoint(x: 11.0, y: size.height - 11.0))
|
||||
context.strokePath()
|
||||
})
|
||||
}
|
||||
if let sendAsPeers = interfaceState.sendAsPeers, !sendAsPeers.isEmpty {
|
||||
self.menuButtonIconNode.enqueueState(.close, animated: false)
|
||||
} else if case .webView = interfaceState.botMenuButton, let previousShowWebView = previousState?.showWebView, previousShowWebView != interfaceState.showWebView {
|
||||
if case .webView = interfaceState.botMenuButton, let previousShowWebView = previousState?.showWebView, previousShowWebView != interfaceState.showWebView {
|
||||
if interfaceState.showWebView {
|
||||
// self.menuButtonIconNode.enqueueState(.close, animated: true)
|
||||
} else {
|
||||
@ -1912,16 +1942,48 @@ public class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDeleg
|
||||
|
||||
if let customLeftAction = self.customLeftAction {
|
||||
switch customLeftAction {
|
||||
case let .toggleExpanded(_, isExpanded):
|
||||
case let .toggleExpanded(_, isExpanded, hasUnseen):
|
||||
var iconTransform = CATransform3DIdentity
|
||||
iconTransform = CATransform3DTranslate(iconTransform, 0.0, 1.0, 0.0)
|
||||
if isExpanded {
|
||||
if !isExpanded {
|
||||
iconTransform = CATransform3DRotate(iconTransform, CGFloat.pi, 0.0, 0.0, 1.0)
|
||||
}
|
||||
transition.updateTransform(layer: self.attachmentButtonIcon.layer, transform: iconTransform)
|
||||
|
||||
if hasUnseen {
|
||||
let attachmentButtonUnseenIcon: UIImageView
|
||||
if let current = self.attachmentButtonUnseenIcon {
|
||||
attachmentButtonUnseenIcon = current
|
||||
} else {
|
||||
attachmentButtonUnseenIcon = UIImageView()
|
||||
self.attachmentButtonUnseenIcon = attachmentButtonUnseenIcon
|
||||
self.attachmentButtonBackground.contentView.addSubview(attachmentButtonUnseenIcon)
|
||||
attachmentButtonUnseenIcon.image = generateStretchableFilledCircleImage(diameter: 6.0, color: .white)?.withRenderingMode(.alwaysTemplate)
|
||||
}
|
||||
attachmentButtonUnseenIcon.tintColor = interfaceState.theme.list.itemAccentColor
|
||||
|
||||
if let image = attachmentButtonUnseenIcon.image {
|
||||
attachmentButtonUnseenIcon.frame = CGRect(origin: CGPoint(x: 40.0 - 8.0 - image.size.width, y: 8.0), size: image.size)
|
||||
}
|
||||
} else {
|
||||
if let attachmentButtonUnseenIcon = self.attachmentButtonUnseenIcon {
|
||||
self.attachmentButtonUnseenIcon = nil
|
||||
transition.updateTransformScale(layer: attachmentButtonUnseenIcon.layer, scale: 0.001)
|
||||
transition.updateAlpha(layer: attachmentButtonUnseenIcon.layer, alpha: 0.0, completion: { [weak attachmentButtonUnseenIcon] _ in
|
||||
attachmentButtonUnseenIcon?.removeFromSuperview()
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
self.attachmentButtonIcon.layer.transform = CATransform3DIdentity
|
||||
if let attachmentButtonUnseenIcon = self.attachmentButtonUnseenIcon {
|
||||
self.attachmentButtonUnseenIcon = nil
|
||||
transition.updateTransformScale(layer: attachmentButtonUnseenIcon.layer, scale: 0.001)
|
||||
transition.updateAlpha(layer: attachmentButtonUnseenIcon.layer, alpha: 0.0, completion: { [weak attachmentButtonUnseenIcon] _ in
|
||||
attachmentButtonUnseenIcon?.removeFromSuperview()
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
var textFieldMinHeight: CGFloat = 33.0
|
||||
@ -1985,7 +2047,7 @@ public class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDeleg
|
||||
|
||||
let leftMenuInset: CGFloat
|
||||
let menuButtonHeight: CGFloat = 40.0
|
||||
let menuCollapsedButtonWidth: CGFloat = isSendAsButton ? menuButtonHeight : 40.0
|
||||
let menuCollapsedButtonWidth: CGFloat = 40.0
|
||||
let menuButtonWidth = menuTextSize.width + 47.0
|
||||
if hasMenuButton {
|
||||
let menuButtonSpacing: CGFloat = 6.0
|
||||
@ -2015,7 +2077,7 @@ public class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDeleg
|
||||
var attachmentButtonX: CGFloat = hideOffset.x + leftInset + leftMenuInset + 8.0
|
||||
if !displayMediaButton || mediaRecordingState != nil {
|
||||
attachmentButtonX = -48.0
|
||||
} else if let customLeftAction = self.customLeftAction, case let .toggleExpanded(isVisible, _) = customLeftAction, !isVisible {
|
||||
} else if let customLeftAction = self.customLeftAction, case let .toggleExpanded(isVisible, _, _) = customLeftAction, !isVisible {
|
||||
attachmentButtonX = -48.0
|
||||
}
|
||||
|
||||
@ -2023,7 +2085,7 @@ public class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDeleg
|
||||
|
||||
self.updateActionButtons(hasText: inputHasText, transition: transition)
|
||||
|
||||
var actionButtonsSize = CGSize(width: 40.0, height: 40.0)
|
||||
var mediaActionButtonsSize = CGSize(width: 40.0, height: 40.0)
|
||||
var sendActionButtonsSize = CGSize(width: 40.0, height: 40.0)
|
||||
if let presentationInterfaceState = self.presentationInterfaceState {
|
||||
var showTitle = false
|
||||
@ -2038,11 +2100,46 @@ public class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDeleg
|
||||
}
|
||||
}
|
||||
sendActionButtonsSize = self.sendActionButtons.updateLayout(size: CGSize(width: 40.0, height: minimalHeight), isMediaInputExpanded: isMediaInputExpanded, showTitle: showTitle, currentMessageEffectId: presentationInterfaceState.interfaceState.sendMessageEffect, transition: transition, interfaceState: presentationInterfaceState)
|
||||
actionButtonsSize = self.mediaActionButtons.updateLayout(size: CGSize(width: 40.0, height: minimalHeight), isMediaInputExpanded: isMediaInputExpanded, showTitle: false, currentMessageEffectId: presentationInterfaceState.interfaceState.sendMessageEffect, transition: transition, interfaceState: presentationInterfaceState)
|
||||
mediaActionButtonsSize = self.mediaActionButtons.updateLayout(size: CGSize(width: 40.0, height: minimalHeight), isMediaInputExpanded: isMediaInputExpanded, showTitle: false, currentMessageEffectId: presentationInterfaceState.interfaceState.sendMessageEffect, transition: transition, interfaceState: presentationInterfaceState)
|
||||
}
|
||||
|
||||
var starReactionButtonSize: CGSize?
|
||||
if let customRightAction = self.customRightAction, case let .stars(count, isFilled, action, longPressAction) = customRightAction {
|
||||
let starReactionButton: ComponentView<Empty>
|
||||
var starReactionButtonTransition = transition
|
||||
if let current = self.starReactionButton {
|
||||
starReactionButton = current
|
||||
} else {
|
||||
starReactionButton = ComponentView()
|
||||
self.starReactionButton = starReactionButton
|
||||
starReactionButtonTransition = .immediate
|
||||
}
|
||||
starReactionButtonSize = starReactionButton.update(
|
||||
transition: ComponentTransition(starReactionButtonTransition),
|
||||
component: AnyComponent(StarReactionButtonComponent(
|
||||
theme: interfaceState.theme,
|
||||
count: count,
|
||||
isFilled: isFilled,
|
||||
action: action,
|
||||
longPressAction: longPressAction
|
||||
)),
|
||||
environment: {},
|
||||
containerSize: CGSize(width: 40.0, height: 40.0)
|
||||
)
|
||||
} else if let starReactionButton = self.starReactionButton {
|
||||
self.starReactionButton = nil
|
||||
if let starReactionButtonView = starReactionButton.view {
|
||||
transition.updateAlpha(layer: starReactionButtonView.layer, alpha: 0.0, completion: { [weak starReactionButtonView] _ in
|
||||
starReactionButtonView?.removeFromSuperview()
|
||||
})
|
||||
transition.updateTransformScale(layer: starReactionButtonView.layer, scale: 0.001)
|
||||
}
|
||||
}
|
||||
|
||||
let effectiveActionButtonsSize = starReactionButtonSize ?? mediaActionButtonsSize
|
||||
|
||||
let baseWidth = width - leftInset - leftMenuInset - rightInset - rightSlowModeInset
|
||||
let (accessoryButtonsWidth, textFieldHeight, isTextFieldOverflow) = self.calculateTextFieldMetrics(width: baseWidth, sendActionControlsWidth: sendActionButtonsSize.width, maxHeight: maxHeight, metrics: metrics, bottomInset: bottomInset)
|
||||
let (accessoryButtonsWidth, textFieldHeight, isTextFieldOverflow) = self.calculateTextFieldMetrics(width: baseWidth, sendActionControlsWidth: sendActionButtonsSize.width, maxHeight: maxHeight, metrics: metrics, bottomInset: bottomInset, interfaceState: interfaceState)
|
||||
var panelHeight = self.panelHeight(textFieldHeight: textFieldHeight, metrics: metrics, bottomInset: bottomInset)
|
||||
if displayBotStartButton {
|
||||
panelHeight += 27.0
|
||||
@ -2071,35 +2168,11 @@ public class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDeleg
|
||||
transition.updateAlpha(node: self.menuButtonTextNode, alpha: menuButtonExpanded ? 1.0 : 0.0)
|
||||
transition.updateFrame(node: self.menuButtonIconNode, frame: CGRect(x: 7.0, y: 7.0, width: 26.0, height: 26.0))
|
||||
|
||||
transition.updateFrame(node: self.sendAsAvatarButtonNode, frame: menuButtonFrame)
|
||||
transition.updateFrame(node: self.sendAsAvatarContainerNode, frame: CGRect(origin: CGPoint(), size: menuButtonFrame.size))
|
||||
transition.updateFrame(node: self.sendAsAvatarReferenceNode, frame: CGRect(origin: CGPoint(), size: menuButtonFrame.size))
|
||||
transition.updateFrame(node: self.sendAsAvatarNode, frame: CGRect(origin: CGPoint(), size: menuButtonFrame.size))
|
||||
|
||||
let showMenuButton = hasMenuButton && interfaceState.interfaceState.mediaDraftState == nil
|
||||
if isSendAsButton {
|
||||
if interfaceState.showSendAsPeers {
|
||||
transition.updateTransformScale(node: self.menuButton, scale: 1.0)
|
||||
transition.updateAlpha(node: self.menuButton, alpha: 1.0)
|
||||
|
||||
transition.updateTransformScale(node: self.sendAsAvatarButtonNode, scale: 0.001)
|
||||
transition.updateAlpha(node: self.sendAsAvatarButtonNode, alpha: 0.0)
|
||||
} else {
|
||||
transition.updateTransformScale(node: self.menuButton, scale: 0.001)
|
||||
transition.updateAlpha(node: self.menuButton, alpha: 0.0)
|
||||
|
||||
transition.updateTransformScale(node: self.sendAsAvatarButtonNode, scale: showMenuButton ? 1.0 : 0.001)
|
||||
transition.updateAlpha(node: self.sendAsAvatarButtonNode, alpha: showMenuButton ? 1.0 : 0.0)
|
||||
}
|
||||
} else {
|
||||
transition.updateTransformScale(node: self.menuButton, scale: showMenuButton ? 1.0 : 0.001)
|
||||
transition.updateAlpha(node: self.menuButton, alpha: showMenuButton ? 1.0 : 0.0)
|
||||
|
||||
transition.updateTransformScale(node: self.sendAsAvatarButtonNode, scale: 0.001)
|
||||
transition.updateAlpha(node: self.sendAsAvatarButtonNode, alpha: 0.0)
|
||||
}
|
||||
transition.updateTransformScale(node: self.menuButton, scale: showMenuButton ? 1.0 : 0.001)
|
||||
transition.updateAlpha(node: self.menuButton, alpha: showMenuButton ? 1.0 : 0.0)
|
||||
|
||||
self.menuButton.isUserInteractionEnabled = hasMenuButton
|
||||
self.sendAsAvatarButtonNode.isUserInteractionEnabled = hasMenuButton && isSendAsButton
|
||||
|
||||
var textFieldInsets = self.textFieldInsets(metrics: metrics, bottomInset: bottomInset)
|
||||
if additionalSideInsets.right > 0.0 {
|
||||
@ -2107,12 +2180,16 @@ public class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDeleg
|
||||
}
|
||||
if inputHasText || self.extendedSearchLayout || hasMediaDraft || hasForward {
|
||||
} else {
|
||||
textFieldInsets.right = 54.0
|
||||
if let starReactionButtonSize {
|
||||
textFieldInsets.right = 14.0 + starReactionButtonSize.width
|
||||
} else {
|
||||
textFieldInsets.right = 54.0
|
||||
}
|
||||
}
|
||||
if mediaRecordingState != nil {
|
||||
textFieldInsets.left = 8.0
|
||||
}
|
||||
if let customLeftAction = self.customLeftAction, case let .toggleExpanded(isVisible, _) = customLeftAction, !isVisible {
|
||||
if let customLeftAction = self.customLeftAction, case let .toggleExpanded(isVisible, _, _) = customLeftAction, !isVisible {
|
||||
textFieldInsets.left = 8.0
|
||||
}
|
||||
|
||||
@ -2373,7 +2450,7 @@ public class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDeleg
|
||||
|
||||
var textInputViewRealInsets = UIEdgeInsets()
|
||||
if let presentationInterfaceState = self.presentationInterfaceState {
|
||||
textInputViewRealInsets = calculateTextFieldRealInsets(presentationInterfaceState: presentationInterfaceState, accessoryButtonsWidth: accessoryButtonsWidth, actionControlsWidth: actionButtonsSize.width)
|
||||
textInputViewRealInsets = calculateTextFieldRealInsets(presentationInterfaceState: presentationInterfaceState, accessoryButtonsWidth: accessoryButtonsWidth, actionControlsWidth: effectiveActionButtonsSize.width)
|
||||
}
|
||||
|
||||
var contentHeight: CGFloat = 0.0
|
||||
@ -2447,7 +2524,7 @@ public class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDeleg
|
||||
}
|
||||
|
||||
if let _ = interfaceState.interfaceState.mediaDraftState {
|
||||
let mediaPreviewPanelFrame = CGRect(origin: CGPoint(x: 0.0, y: contentHeight), size: CGSize(width: textInputWidth - actionButtonsSize.width - 8.0, height: 40.0))
|
||||
let mediaPreviewPanelFrame = CGRect(origin: CGPoint(x: 0.0, y: contentHeight), size: CGSize(width: textInputWidth - effectiveActionButtonsSize.width - 8.0, height: 40.0))
|
||||
var mediaPreviewPanelTransition = transition
|
||||
|
||||
let mediaPreviewPanelNode: ChatRecordingPreviewInputPanelNodeImpl
|
||||
@ -2521,8 +2598,18 @@ public class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDeleg
|
||||
})
|
||||
}
|
||||
|
||||
let textFieldFrame = CGRect(origin: CGPoint(x: self.textInputViewInternalInsets.left, y: self.textInputViewInternalInsets.top + textFieldTopContentOffset), size: CGSize(width: textInputFrame.size.width - (self.textInputViewInternalInsets.left + self.textInputViewInternalInsets.right), height: textInputHeight - self.textInputViewInternalInsets.top - textInputViewInternalInsets.bottom))
|
||||
let textInputNodeClippingContainerFrame = CGRect(origin: CGPoint(x: textFieldFrame.minX - self.textInputViewInternalInsets.left, y: textFieldFrame.minY - self.textInputViewInternalInsets.top), size: CGSize(width: textFieldFrame.width + self.textInputViewInternalInsets.left + self.textInputViewInternalInsets.right, height: textFieldFrame.height + self.textInputViewInternalInsets.top + self.textInputViewInternalInsets.bottom))
|
||||
var hasSendAsButton = false
|
||||
if let sendAsPeers = interfaceState.sendAsPeers, !sendAsPeers.isEmpty && interfaceState.editMessageState == nil {
|
||||
hasSendAsButton = true
|
||||
}
|
||||
|
||||
var actualTextInputViewInternalInsets = self.textInputViewInternalInsets
|
||||
if hasSendAsButton {
|
||||
actualTextInputViewInternalInsets.left += 31.0
|
||||
}
|
||||
|
||||
let textFieldFrame = CGRect(origin: CGPoint(x: actualTextInputViewInternalInsets.left, y: actualTextInputViewInternalInsets.top + textFieldTopContentOffset), size: CGSize(width: textInputFrame.size.width - (actualTextInputViewInternalInsets.left + actualTextInputViewInternalInsets.right), height: textInputHeight - actualTextInputViewInternalInsets.top - actualTextInputViewInternalInsets.bottom))
|
||||
let textInputNodeClippingContainerFrame = CGRect(origin: CGPoint(x: textFieldFrame.minX - actualTextInputViewInternalInsets.left, y: textFieldFrame.minY - actualTextInputViewInternalInsets.top), size: CGSize(width: textFieldFrame.width + actualTextInputViewInternalInsets.left + actualTextInputViewInternalInsets.right, height: textFieldFrame.height + actualTextInputViewInternalInsets.top + actualTextInputViewInternalInsets.bottom))
|
||||
let shouldUpdateLayout = textInputNodeClippingContainerFrame.size != self.textInputNodeClippingContainer.frame.size
|
||||
transition.updateFrame(node: self.textInputNodeClippingContainer, frame: textInputNodeClippingContainerFrame)
|
||||
|
||||
@ -2530,7 +2617,7 @@ public class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDeleg
|
||||
self.textInputSeparator.backgroundColor = interfaceState.theme.chat.inputPanel.inputPlaceholderColor
|
||||
transition.updateAlpha(layer: self.textInputSeparator.layer, alpha: isTextFieldOverflow ? 1.0 : 0.0)
|
||||
|
||||
let actualTextFieldFrame = CGRect(origin: CGPoint(x: self.textInputViewInternalInsets.left, y: self.textInputViewInternalInsets.top), size: textFieldFrame.size)
|
||||
let actualTextFieldFrame = CGRect(origin: CGPoint(x: actualTextInputViewInternalInsets.left, y: actualTextInputViewInternalInsets.top), size: textFieldFrame.size)
|
||||
self.textInputNodeLayout = (actualTextFieldFrame, textInputViewRealInsets)
|
||||
|
||||
if let textInputNode = self.textInputNode {
|
||||
@ -2547,7 +2634,7 @@ public class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDeleg
|
||||
let placeholderLayout = TextNode.asyncLayout(self.contextPlaceholderNode)
|
||||
let contextPlaceholder = NSMutableAttributedString(attributedString: contextPlaceholder)
|
||||
contextPlaceholder.addAttribute(.foregroundColor, value: placeholderColor.withAlphaComponent(1.0), range: NSRange(location: 0, length: contextPlaceholder.length))
|
||||
let (placeholderSize, placeholderApply) = placeholderLayout(TextNodeLayoutArguments(attributedString: contextPlaceholder, backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: width - leftInset - rightInset - textFieldInsets.left - textFieldInsets.right - self.textInputViewInternalInsets.left - self.textInputViewInternalInsets.right - accessoryButtonsWidth, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets()))
|
||||
let (placeholderSize, placeholderApply) = placeholderLayout(TextNodeLayoutArguments(attributedString: contextPlaceholder, backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: width - leftInset - rightInset - textFieldInsets.left - textFieldInsets.right - actualTextInputViewInternalInsets.left - actualTextInputViewInternalInsets.right - accessoryButtonsWidth, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets()))
|
||||
let tintContextPlaceholder = NSMutableAttributedString(attributedString: contextPlaceholder)
|
||||
tintContextPlaceholder.addAttribute(.foregroundColor, value: UIColor.black, range: NSRange(location: 0, length: tintContextPlaceholder.length))
|
||||
let contextPlaceholderNode = placeholderApply()
|
||||
@ -2571,7 +2658,7 @@ public class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDeleg
|
||||
} else {
|
||||
placeholderTransition = .immediate
|
||||
}
|
||||
placeholderTransition.updateFrame(node: contextPlaceholderNode, frame: CGRect(origin: CGPoint(x: self.textInputViewInternalInsets.left, y: hideOffset.y + textFieldInsets.top + self.textInputViewInternalInsets.top + textInputViewRealInsets.top + UIScreenPixel + (accessoryPanel != nil ? 52.0 : 0.0)), size: placeholderSize.size))
|
||||
placeholderTransition.updateFrame(node: contextPlaceholderNode, frame: CGRect(origin: CGPoint(x: actualTextInputViewInternalInsets.left, y: hideOffset.y + textFieldInsets.top + actualTextInputViewInternalInsets.top + textInputViewRealInsets.top + UIScreenPixel + (accessoryPanel != nil ? 52.0 : 0.0)), size: placeholderSize.size))
|
||||
contextPlaceholderNode.view.setMonochromaticEffect(tintColor: placeholderColor)
|
||||
contextPlaceholderNode.alpha = audioRecordingItemsAlpha * placeholderColor.alpha
|
||||
} else {
|
||||
@ -2591,7 +2678,7 @@ public class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDeleg
|
||||
self.slowmodePlaceholderNode = slowmodePlaceholderNode
|
||||
self.textInputContainerBackgroundView.contentView.insertSubview(slowmodePlaceholderNode.view, aboveSubview: self.textPlaceholderNode.view)
|
||||
}
|
||||
let placeholderFrame = CGRect(origin: CGPoint(x: self.textInputViewInternalInsets.left, y: textFieldInsets.top + self.textInputViewInternalInsets.top + textInputViewRealInsets.top + UIScreenPixel + textFieldTopContentOffset), size: CGSize(width: width - leftInset - rightInset - textFieldInsets.left - textFieldInsets.right - self.textInputViewInternalInsets.left - self.textInputViewInternalInsets.right - accessoryButtonsWidth, height: 30.0))
|
||||
let placeholderFrame = CGRect(origin: CGPoint(x: actualTextInputViewInternalInsets.left, y: textFieldInsets.top + actualTextInputViewInternalInsets.top + textInputViewRealInsets.top + UIScreenPixel + textFieldTopContentOffset), size: CGSize(width: width - leftInset - rightInset - textFieldInsets.left - textFieldInsets.right - actualTextInputViewInternalInsets.left - actualTextInputViewInternalInsets.right - accessoryButtonsWidth, height: 30.0))
|
||||
slowmodePlaceholderNode.updateState(slowmodeState)
|
||||
if slowmodePlaceholderNode.bounds.isEmpty {
|
||||
slowmodePlaceholderNode.frame = placeholderFrame
|
||||
@ -2687,7 +2774,7 @@ public class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDeleg
|
||||
|
||||
let textPlaceholderFrame: CGRect
|
||||
if sendingTextDisabled {
|
||||
textPlaceholderFrame = CGRect(origin: CGPoint(x: floor((textInputContainerBackgroundFrame.width - textPlaceholderSize.width) / 2.0), y: self.textInputViewInternalInsets.top + textInputViewRealInsets.top + UIScreenPixel + textFieldTopContentOffset), size: textPlaceholderSize)
|
||||
textPlaceholderFrame = CGRect(origin: CGPoint(x: floor((textInputContainerBackgroundFrame.width - textPlaceholderSize.width) / 2.0), y: actualTextInputViewInternalInsets.top + textInputViewRealInsets.top + UIScreenPixel + textFieldTopContentOffset), size: textPlaceholderSize)
|
||||
|
||||
let textLockIconNode: ASImageNode
|
||||
var textLockIconTransition = transition
|
||||
@ -2706,7 +2793,7 @@ public class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDeleg
|
||||
textLockIconTransition.updateFrame(node: textLockIconNode, frame: CGRect(origin: CGPoint(x: -image.size.width - 4.0, y: floor((textPlaceholderFrame.height - image.size.height) / 2.0)), size: image.size))
|
||||
}
|
||||
} else {
|
||||
textPlaceholderFrame = CGRect(origin: CGPoint(x: self.textInputViewInternalInsets.left, y: self.textInputViewInternalInsets.top + textInputViewRealInsets.top + UIScreenPixel + textFieldTopContentOffset), size: textPlaceholderSize)
|
||||
textPlaceholderFrame = CGRect(origin: CGPoint(x: actualTextInputViewInternalInsets.left, y: actualTextInputViewInternalInsets.top + textInputViewRealInsets.top + UIScreenPixel + textFieldTopContentOffset), size: textPlaceholderSize)
|
||||
|
||||
if let textLockIconNode = self.textLockIconNode {
|
||||
self.textLockIconNode = nil
|
||||
@ -2715,6 +2802,31 @@ public class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDeleg
|
||||
}
|
||||
transition.updateFrame(node: self.textPlaceholderNode, frame: textPlaceholderFrame)
|
||||
|
||||
let sendAsButtonFrame = CGRect(origin: CGPoint(x: 3.0, y: textInputContainerBackgroundFrame.height - 3.0 - 34.0), size: CGSize(width: 34.0, height: 34.0))
|
||||
transition.updateFrame(node: self.sendAsAvatarButtonNode, frame: sendAsButtonFrame)
|
||||
transition.updateFrame(node: self.sendAsAvatarContainerNode, frame: CGRect(origin: CGPoint(), size: sendAsButtonFrame.size))
|
||||
transition.updateFrame(node: self.sendAsAvatarReferenceNode, frame: CGRect(origin: CGPoint(), size: sendAsButtonFrame.size))
|
||||
transition.updatePosition(node: self.sendAsAvatarNode, position: CGRect(origin: CGPoint(), size: sendAsButtonFrame.size).center)
|
||||
transition.updateBounds(node: self.sendAsAvatarNode, bounds: CGRect(origin: CGPoint(), size: sendAsButtonFrame.size))
|
||||
self.sendAsAvatarNode.updateSize(size: sendAsButtonFrame.size)
|
||||
ComponentTransition(transition).setPosition(view: self.sendAsCloseIconView, position: CGRect(origin: CGPoint(), size: sendAsButtonFrame.size).center)
|
||||
ComponentTransition(transition).setBounds(view: self.sendAsCloseIconView, bounds: CGRect(origin: CGPoint(), size: sendAsButtonFrame.size))
|
||||
self.sendAsAvatarButtonNode.isUserInteractionEnabled = hasSendAsButton
|
||||
|
||||
if interfaceState.showSendAsPeers {
|
||||
transition.updateTransformScale(layer: self.sendAsCloseIconView.layer, scale: 1.0)
|
||||
transition.updateAlpha(layer: self.sendAsCloseIconView.layer, alpha: 1.0)
|
||||
|
||||
transition.updateTransformScale(node: self.sendAsAvatarNode, scale: 0.001)
|
||||
transition.updateAlpha(node: self.sendAsAvatarNode, alpha: 0.0)
|
||||
} else {
|
||||
transition.updateTransformScale(layer: self.sendAsCloseIconView.layer, scale: 0.001)
|
||||
transition.updateAlpha(layer: self.sendAsCloseIconView.layer, alpha: 0.0)
|
||||
|
||||
transition.updateTransformScale(node: self.sendAsAvatarNode, scale: 1.0)
|
||||
transition.updateAlpha(node: self.sendAsAvatarNode, alpha: 1.0)
|
||||
}
|
||||
|
||||
let textPlaceholderAlpha: CGFloat = audioRecordingItemsAlpha * placeholderColor.alpha
|
||||
transition.updateAlpha(node: self.textPlaceholderNode, alpha: textPlaceholderAlpha)
|
||||
|
||||
@ -2734,57 +2846,30 @@ public class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDeleg
|
||||
}
|
||||
}
|
||||
|
||||
var actionButtonsFrame = CGRect(origin: CGPoint(x: textInputContainerBackgroundFrame.maxX + 6.0, y: textInputContainerBackgroundFrame.maxY - actionButtonsSize.height), size: actionButtonsSize)
|
||||
var mediaActionButtonsFrame = CGRect(origin: CGPoint(x: textInputContainerBackgroundFrame.maxX + 6.0, y: textInputContainerBackgroundFrame.maxY - mediaActionButtonsSize.height), size: mediaActionButtonsSize)
|
||||
if inputHasText || self.extendedSearchLayout || hasMediaDraft {
|
||||
actionButtonsFrame.origin.x = width + 8.0
|
||||
mediaActionButtonsFrame.origin.x = width + 8.0
|
||||
}
|
||||
transition.updateFrame(node: self.mediaActionButtons, frame: actionButtonsFrame)
|
||||
transition.updateFrame(node: self.mediaActionButtons, frame: mediaActionButtonsFrame)
|
||||
if let (rect, containerSize) = self.absoluteRect {
|
||||
self.mediaActionButtons.updateAbsoluteRect(CGRect(x: rect.origin.x + actionButtonsFrame.origin.x, y: rect.origin.y + actionButtonsFrame.origin.y, width: actionButtonsFrame.width, height: actionButtonsFrame.height), within: containerSize, transition: transition)
|
||||
self.mediaActionButtons.updateAbsoluteRect(CGRect(x: rect.origin.x + mediaActionButtonsFrame.origin.x, y: rect.origin.y + mediaActionButtonsFrame.origin.y, width: mediaActionButtonsFrame.width, height: mediaActionButtonsFrame.height), within: containerSize, transition: transition)
|
||||
}
|
||||
|
||||
if let customRightAction = self.customRightAction, case let .stars(count, isFilled, action) = customRightAction {
|
||||
let starReactionButton: ComponentView<Empty>
|
||||
var starReactionButtonTransition = transition
|
||||
if let current = self.starReactionButton {
|
||||
starReactionButton = current
|
||||
} else {
|
||||
starReactionButton = ComponentView()
|
||||
self.starReactionButton = starReactionButton
|
||||
starReactionButtonTransition = .immediate
|
||||
if let starReactionButtonView = self.starReactionButton?.view, let starReactionButtonSize {
|
||||
var starReactionButtonFrame = CGRect(origin: CGPoint(x: textInputContainerBackgroundFrame.maxX + 6.0, y: textInputContainerBackgroundFrame.maxY - starReactionButtonSize.height), size: starReactionButtonSize)
|
||||
if inputHasText || self.extendedSearchLayout || hasMediaDraft {
|
||||
starReactionButtonFrame.origin.x = width + 8.0
|
||||
}
|
||||
let starReactionButtonSize = starReactionButton.update(
|
||||
transition: ComponentTransition(starReactionButtonTransition),
|
||||
component: AnyComponent(StarReactionButtonComponent(
|
||||
theme: interfaceState.theme,
|
||||
count: count,
|
||||
isFilled: isFilled,
|
||||
action: {
|
||||
action()
|
||||
}
|
||||
)),
|
||||
environment: {},
|
||||
containerSize: CGSize(width: 40.0, height: 40.0)
|
||||
)
|
||||
let _ = starReactionButtonSize
|
||||
if let starReactionButtonView = starReactionButton.view {
|
||||
if starReactionButtonView.superview == nil {
|
||||
self.glassBackgroundContainer.contentView.addSubview(starReactionButtonView)
|
||||
if transition.isAnimated {
|
||||
starReactionButtonView.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2)
|
||||
transition.animateTransformScale(view: starReactionButtonView, from: 0.001)
|
||||
}
|
||||
|
||||
if starReactionButtonView.superview == nil {
|
||||
self.glassBackgroundContainer.contentView.addSubview(starReactionButtonView)
|
||||
if transition.isAnimated {
|
||||
starReactionButtonView.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2)
|
||||
transition.animateTransformScale(view: starReactionButtonView, from: 0.001)
|
||||
starReactionButtonView.frame = starReactionButtonFrame
|
||||
}
|
||||
starReactionButtonTransition.updateFrame(view: starReactionButtonView, frame: actionButtonsFrame)
|
||||
}
|
||||
} else if let starReactionButton = self.starReactionButton {
|
||||
self.starReactionButton = nil
|
||||
if let starReactionButtonView = starReactionButton.view {
|
||||
transition.updateAlpha(layer: starReactionButtonView.layer, alpha: 0.0, completion: { [weak starReactionButtonView] _ in
|
||||
starReactionButtonView?.removeFromSuperview()
|
||||
})
|
||||
transition.updateTransformScale(layer: starReactionButtonView.layer, scale: 0.001)
|
||||
}
|
||||
transition.updateFrame(view: starReactionButtonView, frame: starReactionButtonFrame)
|
||||
}
|
||||
|
||||
var sendActionButtonsFrame = CGRect(origin: CGPoint(x: textInputContainerBackgroundFrame.maxX - sendActionButtonsSize.width, y: textInputContainerBackgroundFrame.maxY - sendActionButtonsSize.height), size: sendActionButtonsSize)
|
||||
@ -2834,7 +2919,7 @@ public class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDeleg
|
||||
}
|
||||
self.mediaActionButtons.isAccessibilityElement = false
|
||||
let size: CGFloat = 120.0
|
||||
mediaRecordingAccessibilityArea.frame = CGRect(origin: CGPoint(x: actionButtonsFrame.midX - size / 2.0, y: actionButtonsFrame.midY - size / 2.0), size: CGSize(width: size, height: size))
|
||||
mediaRecordingAccessibilityArea.frame = CGRect(origin: CGPoint(x: mediaActionButtonsFrame.midX - size / 2.0, y: mediaActionButtonsFrame.midY - size / 2.0), size: CGSize(width: size, height: size))
|
||||
if added {
|
||||
DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + 0.4, execute: {
|
||||
[weak mediaRecordingAccessibilityArea] in
|
||||
@ -3602,13 +3687,13 @@ public class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDeleg
|
||||
self.counterTextNode.attributedText = NSAttributedString(string: "", font: counterFont, textColor: .black)
|
||||
}
|
||||
|
||||
if let (width, leftInset, rightInset, bottomInset, _, maxHeight, _, metrics, _, _) = self.validLayout {
|
||||
if let (width, leftInset, rightInset, bottomInset, _, maxHeight, _, metrics, _, _) = self.validLayout, let interfaceState = self.presentationInterfaceState {
|
||||
var composeButtonsOffset: CGFloat = 0.0
|
||||
if self.extendedSearchLayout {
|
||||
composeButtonsOffset = 40.0
|
||||
}
|
||||
|
||||
let (_, textFieldHeight, _) = self.calculateTextFieldMetrics(width: width - leftInset - rightInset - self.leftMenuInset - self.rightSlowModeInset + self.currentTextInputBackgroundWidthOffset, sendActionControlsWidth: self.sendActionButtons.bounds.width, maxHeight: maxHeight, metrics: metrics, bottomInset: bottomInset)
|
||||
let (_, textFieldHeight, _) = self.calculateTextFieldMetrics(width: width - leftInset - rightInset - self.leftMenuInset - self.rightSlowModeInset + self.currentTextInputBackgroundWidthOffset, sendActionControlsWidth: self.sendActionButtons.bounds.width, maxHeight: maxHeight, metrics: metrics, bottomInset: bottomInset, interfaceState: interfaceState)
|
||||
let panelHeight = self.panelHeight(textFieldHeight: textFieldHeight, metrics: metrics, bottomInset: bottomInset)
|
||||
var textFieldMinHeight: CGFloat = 33.0
|
||||
if let presentationInterfaceState = self.presentationInterfaceState {
|
||||
@ -4070,8 +4155,16 @@ public class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDeleg
|
||||
}
|
||||
|
||||
private func updateTextHeight(animated: Bool) {
|
||||
if let (width, leftInset, rightInset, bottomInset, additionalSideInsets, maxHeight, _, metrics, _, _) = self.validLayout {
|
||||
let (_, textFieldHeight, _) = self.calculateTextFieldMetrics(width: width - leftInset - rightInset - additionalSideInsets.right - self.leftMenuInset - self.rightSlowModeInset + self.currentTextInputBackgroundWidthOffset, sendActionControlsWidth: self.sendActionButtons.bounds.width, maxHeight: maxHeight, metrics: metrics, bottomInset: bottomInset)
|
||||
if let (width, leftInset, rightInset, bottomInset, additionalSideInsets, maxHeight, _, metrics, _, _) = self.validLayout, let interfaceState = self.presentationInterfaceState {
|
||||
var leftInset = leftInset
|
||||
var rightInset = rightInset
|
||||
if bottomInset <= 32.0 {
|
||||
leftInset += 18.0
|
||||
rightInset += 18.0
|
||||
}
|
||||
|
||||
let baseWidth = width - leftInset - self.leftMenuInset - rightInset - self.rightSlowModeInset + self.currentTextInputBackgroundWidthOffset - additionalSideInsets.right
|
||||
let (_, textFieldHeight, _) = self.calculateTextFieldMetrics(width: baseWidth, sendActionControlsWidth: self.sendActionButtons.bounds.width, maxHeight: maxHeight, metrics: metrics, bottomInset: bottomInset, interfaceState: interfaceState)
|
||||
let panelHeight = self.panelHeight(textFieldHeight: textFieldHeight, metrics: metrics, bottomInset: bottomInset)
|
||||
if !self.bounds.size.height.isEqual(to: panelHeight) {
|
||||
self.updateHeight(animated)
|
||||
@ -4830,9 +4923,13 @@ public class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDeleg
|
||||
case let .input(isEnabled, inputMode), let .botInput(isEnabled, inputMode):
|
||||
switch inputMode {
|
||||
case .keyboard:
|
||||
self.interfaceInteraction?.updateInputModeAndDismissedButtonKeyboardMessageId({ state in
|
||||
return (.text, state.keyboardButtonsMessage?.id)
|
||||
})
|
||||
if let customSwitchToKeyboard = self.customSwitchToKeyboard {
|
||||
customSwitchToKeyboard()
|
||||
} else {
|
||||
self.interfaceInteraction?.updateInputModeAndDismissedButtonKeyboardMessageId({ state in
|
||||
return (.text, state.keyboardButtonsMessage?.id)
|
||||
})
|
||||
}
|
||||
case .stickers, .emoji:
|
||||
if isEnabled {
|
||||
self.interfaceInteraction?.openStickers()
|
||||
|
||||
@ -4,23 +4,27 @@ import Display
|
||||
import TelegramPresentationData
|
||||
import ComponentFlow
|
||||
import GlassBackgroundComponent
|
||||
import AnimatedTextComponent
|
||||
|
||||
final class StarReactionButtonComponent: Component {
|
||||
let theme: PresentationTheme
|
||||
let count: Int
|
||||
let isFilled: Bool
|
||||
let action: () -> Void
|
||||
let action: (UIView) -> Void
|
||||
let longPressAction: ((UIView) -> Void)?
|
||||
|
||||
init(
|
||||
theme: PresentationTheme,
|
||||
count: Int,
|
||||
isFilled: Bool,
|
||||
action: @escaping () -> Void
|
||||
action: @escaping (UIView) -> Void,
|
||||
longPressAction: ((UIView) -> Void)?
|
||||
) {
|
||||
self.theme = theme
|
||||
self.count = count
|
||||
self.isFilled = isFilled
|
||||
self.action = action
|
||||
self.longPressAction = longPressAction
|
||||
}
|
||||
|
||||
static func ==(lhs: StarReactionButtonComponent, rhs: StarReactionButtonComponent) -> Bool {
|
||||
@ -33,13 +37,18 @@ final class StarReactionButtonComponent: Component {
|
||||
if lhs.isFilled != rhs.isFilled {
|
||||
return false
|
||||
}
|
||||
if (lhs.longPressAction == nil) != (rhs.longPressAction == nil) {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
final class View: UIView {
|
||||
private let backgroundView: GlassBackgroundView
|
||||
private let iconView: UIImageView
|
||||
private let button: HighlightTrackingButton
|
||||
private var text: ComponentView<Empty>?
|
||||
|
||||
private var longTapRecognizer: TapLongTapOrDoubleTapGestureRecognizer?
|
||||
|
||||
private var component: StarReactionButtonComponent?
|
||||
private weak var state: EmptyComponentState?
|
||||
@ -47,42 +56,127 @@ final class StarReactionButtonComponent: Component {
|
||||
override init(frame: CGRect) {
|
||||
self.backgroundView = GlassBackgroundView()
|
||||
self.iconView = UIImageView()
|
||||
self.button = HighlightTrackingButton()
|
||||
|
||||
super.init(frame: frame)
|
||||
|
||||
self.addSubview(self.backgroundView)
|
||||
self.backgroundView.contentView.addSubview(self.iconView)
|
||||
self.backgroundView.contentView.addSubview(self.button)
|
||||
|
||||
self.button.addTarget(self, action: #selector(self.buttonPressed), for: .touchUpInside)
|
||||
let longTapRecognizer = TapLongTapOrDoubleTapGestureRecognizer(target: self, action: #selector(self.longTapAction(_:)))
|
||||
longTapRecognizer.tapActionAtPoint = { _ in
|
||||
return .waitForSingleTap
|
||||
}
|
||||
self.longTapRecognizer = longTapRecognizer
|
||||
self.backgroundView.contentView.addGestureRecognizer(longTapRecognizer)
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
@objc private func buttonPressed() {
|
||||
@objc private func longTapAction(_ recogizer: TapLongTapOrDoubleTapGestureRecognizer) {
|
||||
guard let component = self.component else {
|
||||
return
|
||||
}
|
||||
switch recogizer.state {
|
||||
case .ended:
|
||||
if let gesture = recogizer.lastRecognizedGestureAndLocation?.0 {
|
||||
if case .tap = gesture {
|
||||
component.action(self)
|
||||
} else if case .longTap = gesture {
|
||||
component.longPressAction?(self)
|
||||
}
|
||||
}
|
||||
default:
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
func update(component: StarReactionButtonComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: ComponentTransition) -> CGSize {
|
||||
self.component = component
|
||||
self.state = state
|
||||
|
||||
let size = CGSize(width: 40.0, height: 40.0)
|
||||
let backgroundFrame = CGRect(origin: CGPoint(), size: size)
|
||||
self.backgroundView.update(size: backgroundFrame.size, cornerRadius: backgroundFrame.height * 0.5, isDark: component.theme.overallDarkAppearance, tintColor: .init(kind: .panel, color: component.theme.chat.inputPanel.inputBackgroundColor.withMultipliedAlpha(0.7)), isInteractive: true, transition: transition)
|
||||
transition.setFrame(view: self.backgroundView, frame: backgroundFrame)
|
||||
let leftInset: CGFloat = 12.0
|
||||
let rightInset: CGFloat = 12.0
|
||||
let textSpacing: CGFloat = 2.0
|
||||
|
||||
var size = CGSize(width: 40.0, height: 40.0)
|
||||
var textSize: CGSize?
|
||||
|
||||
if self.iconView.image == nil {
|
||||
self.iconView.image = UIImage(bundleImageName: "Premium/Stars/ButtonStar")?.withRenderingMode(.alwaysTemplate)
|
||||
}
|
||||
|
||||
if component.count != 0 {
|
||||
let text: ComponentView<Empty>
|
||||
var textTransition = transition
|
||||
if let current = self.text {
|
||||
text = current
|
||||
} else {
|
||||
textTransition = textTransition.withAnimation(.none)
|
||||
text = ComponentView()
|
||||
self.text = text
|
||||
}
|
||||
let textSizeValue = text.update(
|
||||
transition: textTransition,
|
||||
component: AnyComponent(AnimatedTextComponent(
|
||||
font: Font.regular(17.0),
|
||||
color: component.theme.chat.inputPanel.panelControlColor,
|
||||
items: [AnimatedTextComponent.Item(id: AnyHashable(0), content: .number(component.count, minDigits: 1))],
|
||||
noDelay: true
|
||||
)),
|
||||
environment: {},
|
||||
containerSize: CGSize(width: 100.0, height: 100.0)
|
||||
)
|
||||
textSize = textSizeValue
|
||||
if let image = self.iconView.image {
|
||||
size.width = leftInset + image.size.width + textSpacing + textSizeValue.width + rightInset
|
||||
}
|
||||
} else if let text = self.text {
|
||||
self.text = nil
|
||||
if let textView = text.view {
|
||||
transition.setScale(view: textView, scale: 0.001)
|
||||
transition.setAlpha(view: textView, alpha: 0.0, completion: { [weak textView] _ in
|
||||
textView?.removeFromSuperview()
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
let backgroundFrame = CGRect(origin: CGPoint(), size: size)
|
||||
|
||||
let backgroundTintColor: GlassBackgroundView.TintColor
|
||||
if component.isFilled {
|
||||
backgroundTintColor = .init(kind: .panel, color: component.theme.chat.inputPanel.inputBackgroundColor.withMultipliedAlpha(0.7))
|
||||
} else {
|
||||
backgroundTintColor = .init(kind: .custom, color: UIColor(rgb: 0xFFB10D))
|
||||
}
|
||||
|
||||
self.backgroundView.update(size: backgroundFrame.size, cornerRadius: backgroundFrame.height * 0.5, isDark: component.theme.overallDarkAppearance, tintColor: backgroundTintColor, isInteractive: true, transition: transition)
|
||||
transition.setFrame(view: self.backgroundView, frame: backgroundFrame)
|
||||
|
||||
self.iconView.tintColor = component.theme.chat.inputPanel.panelControlColor
|
||||
|
||||
if let image = self.iconView.image {
|
||||
let iconFrame = image.size.centered(in: CGRect(origin: CGPoint(), size: backgroundFrame.size))
|
||||
let iconFrame: CGRect
|
||||
if textSize == nil {
|
||||
iconFrame = image.size.centered(in: CGRect(origin: CGPoint(), size: backgroundFrame.size))
|
||||
} else {
|
||||
iconFrame = CGRect(origin: CGPoint(x: leftInset, y: floor((backgroundFrame.height - image.size.height) * 0.5)), size: image.size)
|
||||
}
|
||||
transition.setFrame(view: self.iconView, frame: iconFrame)
|
||||
|
||||
if let textView = self.text?.view, let textSize {
|
||||
let textFrame = CGRect(origin: CGPoint(x: iconFrame.maxX + textSpacing, y: floor((backgroundFrame.height - textSize.height) * 0.5)), size: textSize)
|
||||
|
||||
if textView.superview == nil {
|
||||
textView.isUserInteractionEnabled = false
|
||||
self.backgroundView.contentView.addSubview(textView)
|
||||
textView.frame = textFrame
|
||||
transition.animateScale(view: textView, from: 0.001, to: 1.0)
|
||||
transition.animateAlpha(view: textView, from: 0.0, to: 1.0)
|
||||
}
|
||||
transition.setFrame(view: textView, frame: textFrame)
|
||||
}
|
||||
}
|
||||
|
||||
return size
|
||||
|
||||
@ -846,6 +846,7 @@ final class GiftOptionsScreenComponent: Component {
|
||||
options: options ?? [],
|
||||
purpose: .transferStarGift(requiredStars: transferStars),
|
||||
targetPeerId: nil,
|
||||
customTheme: nil,
|
||||
completion: { stars in
|
||||
starsContext.add(balance: StarsAmount(value: stars, nanos: 0))
|
||||
proceed(true)
|
||||
|
||||
@ -554,6 +554,7 @@ final class GiftSetupScreenComponent: Component {
|
||||
options: options ?? [],
|
||||
purpose: .starGift(peerId: component.peerId, requiredStars: finalPrice),
|
||||
targetPeerId: nil,
|
||||
customTheme: nil,
|
||||
completion: { [weak self, weak starsContext] stars in
|
||||
guard let self, let starsContext else {
|
||||
return
|
||||
@ -1169,7 +1170,7 @@ final class GiftSetupScreenComponent: Component {
|
||||
|> filter { $0 != nil }
|
||||
|> take(1)
|
||||
|> deliverOnMainQueue).startStandalone(next: { options in
|
||||
let purchaseController = component.context.sharedContext.makeStarsPurchaseScreen(context: component.context, starsContext: starsContext, options: options ?? [], purpose: .generic, targetPeerId: nil, completion: { stars in
|
||||
let purchaseController = component.context.sharedContext.makeStarsPurchaseScreen(context: component.context, starsContext: starsContext, options: options ?? [], purpose: .generic, targetPeerId: nil, customTheme: nil, completion: { stars in
|
||||
starsContext.add(balance: StarsAmount(value: stars, nanos: 0))
|
||||
})
|
||||
controller.push(purchaseController)
|
||||
|
||||
@ -825,6 +825,7 @@ private final class GiftViewSheetContent: CombinedComponent {
|
||||
options: options ?? [],
|
||||
purpose: .removeOriginalDetailsStarGift(requiredStars: price),
|
||||
targetPeerId: nil,
|
||||
customTheme: nil,
|
||||
completion: { [weak self, weak starsContext] stars in
|
||||
guard let self, let starsContext else {
|
||||
return
|
||||
@ -1831,6 +1832,7 @@ private final class GiftViewSheetContent: CombinedComponent {
|
||||
options: options ?? [],
|
||||
purpose: .buyStarGift(requiredStars: resellAmount.amount.value),
|
||||
targetPeerId: nil,
|
||||
customTheme: nil,
|
||||
completion: { [weak self, weak starsContext] stars in
|
||||
guard let self, let starsContext else {
|
||||
return
|
||||
@ -2114,6 +2116,7 @@ private final class GiftViewSheetContent: CombinedComponent {
|
||||
options: options ?? [],
|
||||
purpose: .upgradeStarGift(requiredStars: price),
|
||||
targetPeerId: nil,
|
||||
customTheme: nil,
|
||||
completion: { [weak self, weak starsContext] stars in
|
||||
guard let self, let starsContext else {
|
||||
return
|
||||
@ -2256,6 +2259,7 @@ private final class GiftViewSheetContent: CombinedComponent {
|
||||
options: options ?? [],
|
||||
purpose: .upgradeStarGift(requiredStars: price),
|
||||
targetPeerId: nil,
|
||||
customTheme: nil,
|
||||
completion: { [weak self, weak starsContext] stars in
|
||||
guard let self, let starsContext else {
|
||||
return
|
||||
@ -5450,6 +5454,7 @@ public class GiftViewScreen: ViewControllerComponentContainer {
|
||||
options: options,
|
||||
purpose: .generic,
|
||||
targetPeerId: nil,
|
||||
customTheme: nil,
|
||||
completion: { _ in }
|
||||
)
|
||||
navigationController.pushViewController(controller)
|
||||
|
||||
@ -43,6 +43,7 @@ swift_library(
|
||||
"//submodules/TelegramUI/Components/PlainButtonComponent",
|
||||
"//submodules/TelegramUI/Components/GlassBackgroundComponent",
|
||||
"//submodules/TelegramUI/Components/Chat/ChatTextInputPanelNode",
|
||||
"//submodules/TelegramUI/Components/Stories/LiveChat/StoryLiveChatMessageComponent",
|
||||
],
|
||||
visibility = [
|
||||
"//visibility:public",
|
||||
|
||||
@ -24,34 +24,12 @@ import MultilineTextComponent
|
||||
import PlainButtonComponent
|
||||
import GlassBackgroundComponent
|
||||
import ChatTextInputPanelNode
|
||||
import StoryLiveChatMessageComponent
|
||||
|
||||
private var sharedIsReduceTransparencyEnabled = UIAccessibility.isReduceTransparencyEnabled
|
||||
|
||||
private let timeoutButtonTag = GenericComponentViewTag()
|
||||
|
||||
private func getStarAmountColorMapping(value: Int64) -> UIColor {
|
||||
//TODO:localize unify
|
||||
if value >= 10000 {
|
||||
return UIColor(rgb: 0x7C8695)
|
||||
}
|
||||
if value >= 2000 {
|
||||
return UIColor(rgb: 0xE6514E)
|
||||
}
|
||||
if value >= 500 {
|
||||
return UIColor(rgb: 0xEE7E20)
|
||||
}
|
||||
if value >= 250 {
|
||||
return UIColor(rgb: 0xE4A20A)
|
||||
}
|
||||
if value >= 100 {
|
||||
return UIColor(rgb: 0x5AB03D)
|
||||
}
|
||||
if value >= 50 {
|
||||
return UIColor(rgb: 0x3E9CDF)
|
||||
}
|
||||
return UIColor(rgb: 0x985FDC)
|
||||
}
|
||||
|
||||
public final class MessageInputPanelComponent: Component {
|
||||
public struct ContextQueryTypes: OptionSet {
|
||||
public var rawValue: Int32
|
||||
@ -194,6 +172,16 @@ public final class MessageInputPanelComponent: Component {
|
||||
}
|
||||
}
|
||||
|
||||
public struct LiveChatState: Equatable {
|
||||
public var isExpanded: Bool
|
||||
public var hasUnseenMessages: Bool
|
||||
|
||||
public init(isExpanded: Bool, hasUnseenMessages: Bool) {
|
||||
self.isExpanded = isExpanded
|
||||
self.hasUnseenMessages = hasUnseenMessages
|
||||
}
|
||||
}
|
||||
|
||||
public let externalState: ExternalState
|
||||
public let context: AccountContext
|
||||
public let theme: PresentationTheme
|
||||
@ -202,6 +190,7 @@ public final class MessageInputPanelComponent: Component {
|
||||
public let placeholder: Placeholder
|
||||
public let sendPaidMessageStars: StarsAmount?
|
||||
public let maxLength: Int?
|
||||
public let maxEmojiCount: Int?
|
||||
public let queryTypes: ContextQueryTypes
|
||||
public let alwaysDarkWhenHasText: Bool
|
||||
public let useGrayBackground: Bool
|
||||
@ -252,8 +241,9 @@ public final class MessageInputPanelComponent: Component {
|
||||
public let isChannel: Bool
|
||||
public let storyItem: EngineStoryItem?
|
||||
public let chatLocation: ChatLocation?
|
||||
public let isLiveChatExpanded: Bool?
|
||||
public let liveChatState: LiveChatState?
|
||||
public let toggleLiveChatExpanded: (() -> Void)?
|
||||
public let sendStarsAction: ((UIView, Bool) -> Void)?
|
||||
|
||||
public init(
|
||||
externalState: ExternalState,
|
||||
@ -264,6 +254,7 @@ public final class MessageInputPanelComponent: Component {
|
||||
placeholder: Placeholder,
|
||||
sendPaidMessageStars: StarsAmount?,
|
||||
maxLength: Int?,
|
||||
maxEmojiCount: Int? = nil,
|
||||
queryTypes: ContextQueryTypes,
|
||||
alwaysDarkWhenHasText: Bool,
|
||||
useGrayBackground: Bool = false,
|
||||
@ -314,8 +305,9 @@ public final class MessageInputPanelComponent: Component {
|
||||
isChannel: Bool,
|
||||
storyItem: EngineStoryItem?,
|
||||
chatLocation: ChatLocation?,
|
||||
isLiveChatExpanded: Bool? = nil,
|
||||
toggleLiveChatExpanded: (() -> Void)? = nil
|
||||
liveChatState: LiveChatState? = nil,
|
||||
toggleLiveChatExpanded: (() -> Void)? = nil,
|
||||
sendStarsAction: ((UIView, Bool) -> Void)? = nil
|
||||
) {
|
||||
self.externalState = externalState
|
||||
self.context = context
|
||||
@ -326,6 +318,7 @@ public final class MessageInputPanelComponent: Component {
|
||||
self.placeholder = placeholder
|
||||
self.sendPaidMessageStars = sendPaidMessageStars
|
||||
self.maxLength = maxLength
|
||||
self.maxEmojiCount = maxEmojiCount
|
||||
self.queryTypes = queryTypes
|
||||
self.alwaysDarkWhenHasText = alwaysDarkWhenHasText
|
||||
self.useGrayBackground = useGrayBackground
|
||||
@ -375,8 +368,9 @@ public final class MessageInputPanelComponent: Component {
|
||||
self.isChannel = isChannel
|
||||
self.storyItem = storyItem
|
||||
self.chatLocation = chatLocation
|
||||
self.isLiveChatExpanded = isLiveChatExpanded
|
||||
self.liveChatState = liveChatState
|
||||
self.toggleLiveChatExpanded = toggleLiveChatExpanded
|
||||
self.sendStarsAction = sendStarsAction
|
||||
}
|
||||
|
||||
public static func ==(lhs: MessageInputPanelComponent, rhs: MessageInputPanelComponent) -> Bool {
|
||||
@ -404,6 +398,9 @@ public final class MessageInputPanelComponent: Component {
|
||||
if lhs.maxLength != rhs.maxLength {
|
||||
return false
|
||||
}
|
||||
if lhs.maxEmojiCount != rhs.maxEmojiCount {
|
||||
return false
|
||||
}
|
||||
if lhs.queryTypes != rhs.queryTypes {
|
||||
return false
|
||||
}
|
||||
@ -503,7 +500,7 @@ public final class MessageInputPanelComponent: Component {
|
||||
if lhs.chatLocation != rhs.chatLocation {
|
||||
return false
|
||||
}
|
||||
if lhs.isLiveChatExpanded != rhs.isLiveChatExpanded {
|
||||
if lhs.liveChatState != rhs.liveChatState {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
@ -927,6 +924,34 @@ public final class MessageInputPanelComponent: Component {
|
||||
placeholder = text
|
||||
}
|
||||
|
||||
var isSendDisabled = false
|
||||
if let maxLength = component.maxLength, self.textInputPanelExternalState.textInputState.inputText.length > maxLength {
|
||||
isSendDisabled = true
|
||||
}
|
||||
if let maxEmojiCount = component.maxEmojiCount {
|
||||
var emojiCount = 0
|
||||
let nsString = self.textInputPanelExternalState.textInputState.inputText.string as NSString
|
||||
var processedRanges = Set<Range<Int>>()
|
||||
nsString.enumerateSubstrings(in: NSRange(location: 0, length: nsString.length), options: .byComposedCharacterSequences, using: {
|
||||
substring, range, _, _ in
|
||||
if let substring, substring.isSingleEmoji {
|
||||
emojiCount += 1
|
||||
processedRanges.insert(range.lowerBound ..< range.upperBound)
|
||||
}
|
||||
})
|
||||
let entities = generateChatInputTextEntities(self.textInputPanelExternalState.textInputState.inputText, generateLinks: false)
|
||||
for entity in entities {
|
||||
if case .CustomEmoji = entity.type {
|
||||
if !processedRanges.contains(entity.range) {
|
||||
emojiCount += 1
|
||||
}
|
||||
}
|
||||
}
|
||||
if emojiCount > maxEmojiCount {
|
||||
isSendDisabled = true
|
||||
}
|
||||
}
|
||||
|
||||
let inputPanelSize = inputPanel.update(
|
||||
transition: transition,
|
||||
component: AnyComponent(ChatTextInputPanelComponent(
|
||||
@ -936,19 +961,30 @@ public final class MessageInputPanelComponent: Component {
|
||||
strings: component.strings,
|
||||
chatPeerId: component.chatLocation?.peerId ?? component.context.account.peerId,
|
||||
inlineActions: inlineActions,
|
||||
leftAction: ChatTextInputPanelComponent.LeftAction(kind: .toggleExpanded(isVisible: component.isLiveChatExpanded != nil, isExpanded: component.isLiveChatExpanded ?? true), action: { [weak self] in
|
||||
leftAction: ChatTextInputPanelComponent.LeftAction(kind: .toggleExpanded(isVisible: component.liveChatState != nil, isExpanded: component.liveChatState?.isExpanded ?? true, hasUnseen: component.liveChatState?.hasUnseenMessages ?? false), action: { [weak self] in
|
||||
guard let self, let component = self.component else {
|
||||
return
|
||||
}
|
||||
component.toggleLiveChatExpanded?()
|
||||
}),
|
||||
rightAction: ChatTextInputPanelComponent.RightAction(kind: .stars(count: 0, isFilled: false), action: {
|
||||
rightAction: ChatTextInputPanelComponent.RightAction(kind: .stars(count: Int(component.storyItem?.views?.reactions.first(where: { $0.value == .stars })?.count ?? 0), isFilled: component.myReaction?.reaction == .stars), action: { [weak self] sourceView in
|
||||
guard let self, let component = self.component else {
|
||||
return
|
||||
}
|
||||
component.sendStarsAction?(sourceView, false)
|
||||
}, longPressAction: { [weak self] sourceView in
|
||||
guard let self, let component = self.component else {
|
||||
return
|
||||
}
|
||||
component.sendStarsAction?(sourceView, true)
|
||||
}),
|
||||
placeholder: placeholder,
|
||||
paidMessagePrice: component.sendPaidMessageStars,
|
||||
sendColor: component.sendPaidMessageStars.flatMap { value in
|
||||
return getStarAmountColorMapping(value: value.value)
|
||||
let color = GroupCallMessagesContext.getStarAmountParamMapping(value: value.value).color ?? .purple
|
||||
return StoryLiveChatMessageComponent.getMessageColor(color: color)
|
||||
},
|
||||
isSendDisabled: isSendDisabled,
|
||||
hideKeyboard: component.hideKeyboard,
|
||||
insets: UIEdgeInsets(top: 0.0, left: 0.0, bottom: component.bottomInset, right: 0.0),
|
||||
maxHeight: availableSize.height,
|
||||
@ -1015,21 +1051,18 @@ public final class MessageInputPanelComponent: Component {
|
||||
insets.left = 41.0
|
||||
}
|
||||
if let _ = component.setMediaRecordingActive {
|
||||
insets.right = 40.0 + 8.0 * 2.0
|
||||
insets.right = 41.0
|
||||
}
|
||||
|
||||
var textFieldSideInset: CGFloat = 8.0
|
||||
if component.bottomInset <= 32.0 && !component.forceIsEditing && !component.hideKeyboard && !self.textFieldExternalState.isEditing {
|
||||
textFieldSideInset += 18.0
|
||||
insets.right += 18.0
|
||||
} else {
|
||||
#if DEBUG
|
||||
textFieldSideInset += 8.0
|
||||
insets.right += 8.0
|
||||
#endif
|
||||
let textFieldSideInset: CGFloat
|
||||
switch component.style {
|
||||
case .media, .glass:
|
||||
textFieldSideInset = 8.0
|
||||
default:
|
||||
textFieldSideInset = 9.0
|
||||
}
|
||||
|
||||
var mediaInsets = UIEdgeInsets(top: insets.top, left: textFieldSideInset, bottom: insets.bottom, right: 40.0 + 8.0)
|
||||
var mediaInsets = UIEdgeInsets(top: insets.top, left: textFieldSideInset, bottom: insets.bottom, right: 41.0)
|
||||
if case .glass = component.style {
|
||||
mediaInsets.right = 54.0
|
||||
}
|
||||
@ -1286,10 +1319,6 @@ public final class MessageInputPanelComponent: Component {
|
||||
} else if isEditing || component.style == .editor || component.style == .media {
|
||||
fieldBackgroundFrame = fieldFrame
|
||||
} else {
|
||||
#if DEBUG
|
||||
fieldBackgroundFrame = fieldFrame
|
||||
fieldBackgroundFrame.size.width += 16.0
|
||||
#else
|
||||
if component.forwardAction != nil && component.likeAction != nil {
|
||||
fieldBackgroundFrame = CGRect(origin: CGPoint(x: mediaInsets.left, y: insets.top), size: CGSize(width: availableSize.width - mediaInsets.left - insets.right - 49.0, height: textFieldSize.height))
|
||||
} else if component.forwardAction != nil {
|
||||
@ -1297,7 +1326,6 @@ public final class MessageInputPanelComponent: Component {
|
||||
} else {
|
||||
fieldBackgroundFrame = CGRect(origin: CGPoint(x: mediaInsets.left, y: insets.top), size: CGSize(width: availableSize.width - mediaInsets.left - 50.0, height: textFieldSize.height))
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
let rawFieldBackgroundFrame = fieldBackgroundFrame
|
||||
@ -1306,7 +1334,7 @@ public final class MessageInputPanelComponent: Component {
|
||||
//transition.setFrame(view: self.vibrancyEffectView, frame: CGRect(origin: CGPoint(), size: fieldBackgroundFrame.size))
|
||||
|
||||
switch component.style {
|
||||
case .glass, .story:
|
||||
case .glass:
|
||||
if self.fieldGlassBackgroundView == nil {
|
||||
let fieldGlassBackgroundView = GlassBackgroundView(frame: fieldBackgroundFrame)
|
||||
self.insertSubview(fieldGlassBackgroundView, aboveSubview: self.fieldBackgroundView)
|
||||
@ -1316,7 +1344,7 @@ public final class MessageInputPanelComponent: Component {
|
||||
self.fieldBackgroundTint.isHidden = true
|
||||
}
|
||||
if let fieldGlassBackgroundView = self.fieldGlassBackgroundView {
|
||||
fieldGlassBackgroundView.update(size: fieldBackgroundFrame.size, cornerRadius: baseFieldHeight * 0.5, isDark: true, tintColor: component.style == .story ? .init(kind: .panel, color: defaultDarkPresentationTheme.chat.inputPanel.inputBackgroundColor.withMultipliedAlpha(0.7)) : .init(kind: .custom, color: UIColor(rgb: 0x25272e, alpha: 0.72)), isInteractive: true, transition: transition)
|
||||
fieldGlassBackgroundView.update(size: fieldBackgroundFrame.size, cornerRadius: baseFieldHeight * 0.5, isDark: true, tintColor: .init(kind: .custom, color: UIColor(rgb: 0x25272e, alpha: 0.72)), transition: transition)
|
||||
transition.setFrame(view: fieldGlassBackgroundView, frame: fieldBackgroundFrame)
|
||||
}
|
||||
default:
|
||||
@ -1762,38 +1790,22 @@ public final class MessageInputPanelComponent: Component {
|
||||
inputActionButtonMode = .close
|
||||
}
|
||||
} else {
|
||||
if case .story = component.style {
|
||||
inputActionButtonAvailableSize = CGSize(width: 40.0, height: 40.0)
|
||||
}
|
||||
|
||||
if let storyItem = component.storyItem, case .liveStream = storyItem.media {
|
||||
if hasMediaEditing {
|
||||
inputActionButtonMode = .send
|
||||
} else {
|
||||
if self.textFieldExternalState.hasText {
|
||||
if let sendPaidMessageStars = component.sendPaidMessageStars, !"".isEmpty {
|
||||
inputActionButtonMode = .stars(sendPaidMessageStars.value)
|
||||
} else {
|
||||
inputActionButtonMode = .send
|
||||
}
|
||||
} else if !isEditing && component.forwardAction != nil {
|
||||
inputActionButtonMode = .forward
|
||||
} else {
|
||||
inputActionButtonMode = .stars(123)
|
||||
}
|
||||
} else {
|
||||
if hasMediaEditing {
|
||||
inputActionButtonMode = .send
|
||||
} else {
|
||||
if self.textFieldExternalState.hasText {
|
||||
if let sendPaidMessageStars = component.sendPaidMessageStars, !"".isEmpty {
|
||||
inputActionButtonMode = .stars(sendPaidMessageStars.value)
|
||||
} else {
|
||||
inputActionButtonMode = .send
|
||||
}
|
||||
} else if !isEditing && component.forwardAction != nil {
|
||||
inputActionButtonMode = .forward
|
||||
if component.areVoiceMessagesAvailable {
|
||||
inputActionButtonMode = self.currentMediaInputIsVoice ? .voiceInput : .videoInput
|
||||
} else {
|
||||
if component.areVoiceMessagesAvailable {
|
||||
inputActionButtonMode = self.currentMediaInputIsVoice ? .voiceInput : .videoInput
|
||||
} else {
|
||||
inputActionButtonMode = .unavailableVoiceInput
|
||||
}
|
||||
inputActionButtonMode = .unavailableVoiceInput
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1802,7 +1814,7 @@ public final class MessageInputPanelComponent: Component {
|
||||
if component.style == .glass {
|
||||
inputActionButtonStyle = .glass(isTinted: true)
|
||||
} else if component.style == .story {
|
||||
inputActionButtonStyle = .glass(isTinted: false)
|
||||
inputActionButtonStyle = .legacy
|
||||
} else {
|
||||
inputActionButtonStyle = .legacy
|
||||
}
|
||||
@ -1925,22 +1937,26 @@ public final class MessageInputPanelComponent: Component {
|
||||
if rightButtonsOffsetX != 0.0 {
|
||||
inputActionButtonOriginX = availableSize.width - 3.0 + rightButtonsOffsetX
|
||||
if displayLikeAction {
|
||||
inputActionButtonOriginX -= 40.0 + 8.0
|
||||
inputActionButtonOriginX -= 39.0
|
||||
}
|
||||
if component.forwardAction != nil {
|
||||
inputActionButtonOriginX -= 40.0 + 8.0
|
||||
inputActionButtonOriginX -= 46.0
|
||||
}
|
||||
} else {
|
||||
if component.setMediaRecordingActive != nil || isEditing || component.style == .glass {
|
||||
switch component.style {
|
||||
case .glass, .story:
|
||||
inputActionButtonOriginX = fieldBackgroundFrame.maxX + 8.0
|
||||
case .glass:
|
||||
inputActionButtonOriginX = fieldBackgroundFrame.maxX + 6.0
|
||||
default:
|
||||
inputActionButtonOriginX = fieldBackgroundFrame.maxX + floorToScreenPixels((41.0 - inputActionButtonSize.width) * 0.5)
|
||||
}
|
||||
} else {
|
||||
inputActionButtonOriginX = size.width
|
||||
}
|
||||
|
||||
if hasLikeAction {
|
||||
inputActionButtonOriginX += 3.0
|
||||
}
|
||||
}
|
||||
|
||||
if let inputActionButtonView = self.inputActionButton.view {
|
||||
@ -1958,27 +1974,21 @@ public final class MessageInputPanelComponent: Component {
|
||||
transition.setBounds(view: inputActionButtonView, bounds: CGRect(origin: CGPoint(), size: inputActionButtonFrame.size))
|
||||
transition.setAlpha(view: inputActionButtonView, alpha: likeActionReplacesInputAction ? 0.0 : inputActionButtonAlpha)
|
||||
|
||||
if hasLikeAction {
|
||||
inputActionButtonOriginX += 40.0 + 8.0
|
||||
if rightButtonsOffsetX != 0.0 {
|
||||
if hasLikeAction {
|
||||
inputActionButtonOriginX += 46.0
|
||||
}
|
||||
} else {
|
||||
if hasLikeAction {
|
||||
inputActionButtonOriginX += 41.0
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let likeActionButtonStyle: MessageInputActionButtonComponent.Style
|
||||
var likeButtonContainerSize = CGSize(width: 33.0, height: 33.0)
|
||||
if component.style == .glass {
|
||||
likeActionButtonStyle = .glass(isTinted: true)
|
||||
likeButtonContainerSize = CGSize(width: 40.0, height: 40.0)
|
||||
} else if component.style == .story {
|
||||
likeActionButtonStyle = .glass(isTinted: false)
|
||||
likeButtonContainerSize = CGSize(width: 40.0, height: 40.0)
|
||||
} else {
|
||||
likeActionButtonStyle = .legacy
|
||||
}
|
||||
let likeButtonSize = self.likeButton.update(
|
||||
transition: transition,
|
||||
component: AnyComponent(MessageInputActionButtonComponent(
|
||||
mode: .like(reaction: component.myReaction?.reaction, file: component.myReaction?.file, animationFileId: component.myReaction?.animationFileId),
|
||||
style: likeActionButtonStyle,
|
||||
storyId: component.storyItem?.id,
|
||||
action: { [weak self] _, action, _ in
|
||||
guard let self, let component = self.component else {
|
||||
@ -2007,7 +2017,7 @@ public final class MessageInputPanelComponent: Component {
|
||||
videoRecordingStatus: nil
|
||||
)),
|
||||
environment: {},
|
||||
containerSize: likeButtonContainerSize
|
||||
containerSize: CGSize(width: 33.0, height: 33.0)
|
||||
)
|
||||
if let likeButtonView = self.likeButton.view {
|
||||
if likeButtonView.superview == nil {
|
||||
@ -2020,7 +2030,7 @@ public final class MessageInputPanelComponent: Component {
|
||||
transition.setPosition(view: likeButtonView, position: likeButtonFrame.center)
|
||||
transition.setBounds(view: likeButtonView, bounds: CGRect(origin: CGPoint(), size: likeButtonFrame.size))
|
||||
transition.setAlpha(view: likeButtonView, alpha: displayLikeAction ? 1.0 : 0.0)
|
||||
inputActionButtonOriginX += 40.0 + 8.0
|
||||
inputActionButtonOriginX += 41.0
|
||||
}
|
||||
|
||||
var fieldIconNextX = fieldBackgroundFrame.maxX - 4.0
|
||||
@ -2029,13 +2039,6 @@ public final class MessageInputPanelComponent: Component {
|
||||
if isEditing {
|
||||
inputModeVisible = true
|
||||
}
|
||||
var isLiveStream = false
|
||||
if let storyItem = component.storyItem, case .liveStream = storyItem.media {
|
||||
isLiveStream = true
|
||||
}
|
||||
if isLiveStream && component.sendPaidMessageStars == nil {
|
||||
inputModeVisible = false
|
||||
}
|
||||
|
||||
let animationName: String
|
||||
var animationPlay = false
|
||||
@ -2099,7 +2102,7 @@ public final class MessageInputPanelComponent: Component {
|
||||
component: AnyComponent(Button(
|
||||
content: AnyComponent(LottieComponent(
|
||||
content: LottieComponent.AppBundleContent(name: animationName),
|
||||
color: defaultDarkPresentationTheme.chat.inputPanel.inputControlColor
|
||||
color: .white
|
||||
)),
|
||||
action: { [weak self] in
|
||||
guard let self else {
|
||||
@ -2133,58 +2136,6 @@ public final class MessageInputPanelComponent: Component {
|
||||
}
|
||||
}
|
||||
|
||||
if let _ = component.paidMessageAction {
|
||||
let paidMessageButton: ComponentView<Empty>
|
||||
var paidMessageButtonTransition = transition
|
||||
if let current = self.paidMessageButton {
|
||||
paidMessageButton = current
|
||||
} else {
|
||||
paidMessageButton = ComponentView()
|
||||
self.paidMessageButton = paidMessageButton
|
||||
paidMessageButtonTransition = paidMessageButtonTransition.withAnimation(.none)
|
||||
}
|
||||
|
||||
let paidMessageButtonSize = paidMessageButton.update(
|
||||
transition: transition,
|
||||
component: AnyComponent(Button(
|
||||
content: AnyComponent(BundleIconComponent(
|
||||
name: "Chat/Input/Text/AccessoryIconSuggestPost",
|
||||
tintColor: defaultDarkPresentationTheme.chat.inputPanel.inputControlColor
|
||||
)),
|
||||
action: { [weak self] in
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
self.component?.paidMessageAction?()
|
||||
}
|
||||
).minSize(CGSize(width: 32.0, height: 32.0))),
|
||||
environment: {},
|
||||
containerSize: CGSize(width: 32.0, height: 32.0)
|
||||
)
|
||||
if let paidMessageButtonView = paidMessageButton.view as? Button.View {
|
||||
if paidMessageButtonView.superview == nil {
|
||||
paidMessageButtonView.alpha = 0.0
|
||||
self.addSubview(paidMessageButtonView)
|
||||
}
|
||||
let paidMessageButtonFrame = CGRect(origin: CGPoint(x: fieldIconNextX - paidMessageButtonSize.width, y: fieldBackgroundFrame.maxY - 4.0 - paidMessageButtonSize.height), size: paidMessageButtonSize)
|
||||
transition.setPosition(view: paidMessageButtonView, position: paidMessageButtonFrame.center)
|
||||
transition.setBounds(view: paidMessageButtonView, bounds: CGRect(origin: CGPoint(), size: paidMessageButtonFrame.size))
|
||||
|
||||
transition.setAlpha(view: paidMessageButtonView, alpha: 1.0)
|
||||
|
||||
fieldIconNextX -= paidMessageButtonSize.width + 2.0
|
||||
}
|
||||
} else {
|
||||
if let paidMessageButton = self.paidMessageButton {
|
||||
self.paidMessageButton = nil
|
||||
if let paidMessageButtonView = paidMessageButton.view {
|
||||
transition.setAlpha(view: paidMessageButtonView, alpha: 0.0, completion: { [weak paidMessageButtonView] _ in
|
||||
paidMessageButtonView?.removeFromSuperview()
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let accentColor = component.theme.chat.inputPanel.panelControlAccentColor
|
||||
if let timeoutAction = component.timeoutAction, let timeoutValue = component.timeoutValue {
|
||||
let timeoutButtonSize = self.timeoutButton.update(
|
||||
|
||||
@ -593,6 +593,7 @@ final class UserAppearanceScreenComponent: Component {
|
||||
options: options ?? [],
|
||||
purpose: .buyStarGift(requiredStars: resellAmount.amount.value),
|
||||
targetPeerId: nil,
|
||||
customTheme: nil,
|
||||
completion: { [weak self, weak starsContext] stars in
|
||||
guard let self, let starsContext else {
|
||||
return
|
||||
|
||||
@ -1017,6 +1017,7 @@ public final class StarsPurchaseScreen: ViewControllerComponentContainer {
|
||||
options: [Any] = [],
|
||||
purpose: StarsPurchasePurpose,
|
||||
targetPeerId: EnginePeer.Id?,
|
||||
customTheme: PresentationTheme? = nil,
|
||||
completion: @escaping (Int64) -> Void = { _ in }
|
||||
) {
|
||||
self.context = context
|
||||
@ -1044,7 +1045,7 @@ public final class StarsPurchaseScreen: ViewControllerComponentContainer {
|
||||
completion: { stars in
|
||||
completionImpl?(stars)
|
||||
}
|
||||
), navigationBarAppearance: .transparent, presentationMode: .modal, theme: .default)
|
||||
), navigationBarAppearance: .transparent, presentationMode: .modal, theme: customTheme.flatMap { .custom($0) } ?? .default)
|
||||
|
||||
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
|
||||
|
||||
|
||||
@ -910,7 +910,7 @@ public final class StarsStatisticsScreen: ViewControllerComponentContainer {
|
||||
guard let self, let starsContext = context.starsContext else {
|
||||
return
|
||||
}
|
||||
let controller = context.sharedContext.makeStarsPurchaseScreen(context: context, starsContext: starsContext, options: options, purpose: .generic, targetPeerId: nil, completion: { [weak self] stars in
|
||||
let controller = context.sharedContext.makeStarsPurchaseScreen(context: context, starsContext: starsContext, options: options, purpose: .generic, targetPeerId: nil, customTheme: nil, completion: { [weak self] stars in
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
|
||||
@ -1340,7 +1340,7 @@ public final class StarsTransactionsScreen: ViewControllerComponentContainer {
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
let controller = context.sharedContext.makeStarsPurchaseScreen(context: context, starsContext: starsContext, options: options, purpose: .generic, targetPeerId: nil, completion: { [weak self] stars in
|
||||
let controller = context.sharedContext.makeStarsPurchaseScreen(context: context, starsContext: starsContext, options: options, purpose: .generic, targetPeerId: nil, customTheme: nil, completion: { [weak self] stars in
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
@ -1465,6 +1465,7 @@ public final class StarsTransactionsScreen: ViewControllerComponentContainer {
|
||||
options: options,
|
||||
purpose: .gift(peerId: peerId),
|
||||
targetPeerId: nil,
|
||||
customTheme: nil,
|
||||
completion: { [weak self] stars in
|
||||
guard let self else {
|
||||
return
|
||||
|
||||
@ -589,6 +589,7 @@ private final class SheetContent: CombinedComponent {
|
||||
options: state?.options ?? [],
|
||||
purpose: purpose,
|
||||
targetPeerId: nil,
|
||||
customTheme: nil,
|
||||
completion: { [weak starsContext] stars in
|
||||
guard let starsContext else {
|
||||
return
|
||||
|
||||
@ -894,7 +894,7 @@ private final class SheetContent: CombinedComponent {
|
||||
guard let controller, let state else {
|
||||
return
|
||||
}
|
||||
let purchaseController = state.context.sharedContext.makeStarsPurchaseScreen(context: state.context, starsContext: starsContext, options: options, purpose: .generic, targetPeerId: nil, completion: { _ in
|
||||
let purchaseController = state.context.sharedContext.makeStarsPurchaseScreen(context: state.context, starsContext: starsContext, options: options, purpose: .generic, targetPeerId: nil, customTheme: nil, completion: { _ in
|
||||
})
|
||||
controller.push(purchaseController)
|
||||
})
|
||||
|
||||
19
submodules/TelegramUI/Components/StarsParticleEffect/BUILD
Normal file
19
submodules/TelegramUI/Components/StarsParticleEffect/BUILD
Normal file
@ -0,0 +1,19 @@
|
||||
load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library")
|
||||
|
||||
swift_library(
|
||||
name = "StarsParticleEffect",
|
||||
module_name = "StarsParticleEffect",
|
||||
srcs = glob([
|
||||
"Sources/**/*.swift",
|
||||
]),
|
||||
copts = [
|
||||
"-warnings-as-errors",
|
||||
],
|
||||
deps = [
|
||||
"//submodules/Display",
|
||||
"//submodules/ComponentFlow",
|
||||
],
|
||||
visibility = [
|
||||
"//visibility:public",
|
||||
],
|
||||
)
|
||||
@ -0,0 +1,71 @@
|
||||
import Foundation
|
||||
import UIKit
|
||||
import ComponentFlow
|
||||
import Display
|
||||
|
||||
public final class StarsParticleEffectLayer: SimpleLayer {
|
||||
private let emitterLayer = CAEmitterLayer()
|
||||
private var currentColor: UIColor?
|
||||
|
||||
override public init() {
|
||||
self.emitterLayer.masksToBounds = true
|
||||
|
||||
super.init()
|
||||
|
||||
self.addSublayer(self.emitterLayer)
|
||||
}
|
||||
|
||||
override public init(layer: Any) {
|
||||
super.init(layer: layer)
|
||||
}
|
||||
|
||||
required public init?(coder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
private func setup() {
|
||||
guard let currentColor = self.currentColor else {
|
||||
return
|
||||
}
|
||||
let color = currentColor
|
||||
|
||||
let emitter = CAEmitterCell()
|
||||
emitter.name = "emitter"
|
||||
emitter.contents = UIImage(bundleImageName: "Premium/Stars/Particle")?.cgImage
|
||||
emitter.birthRate = 25.0
|
||||
emitter.lifetime = 2.0
|
||||
emitter.velocity = 12.0
|
||||
emitter.velocityRange = 3
|
||||
emitter.scale = 0.1
|
||||
emitter.scaleRange = 0.08
|
||||
emitter.alphaRange = 0.1
|
||||
emitter.emissionRange = .pi * 2.0
|
||||
emitter.setValue(3.0, forKey: "mass")
|
||||
emitter.setValue(2.0, forKey: "massRange")
|
||||
|
||||
let staticColors: [Any] = [
|
||||
color.withAlphaComponent(0.0).cgColor,
|
||||
color.cgColor,
|
||||
color.cgColor,
|
||||
color.withAlphaComponent(0.0).cgColor
|
||||
]
|
||||
let staticColorBehavior = CAEmitterCell.createEmitterBehavior(type: "colorOverLife")
|
||||
staticColorBehavior.setValue(staticColors, forKey: "colors")
|
||||
emitter.setValue([staticColorBehavior], forKey: "emitterBehaviors")
|
||||
|
||||
self.emitterLayer.emitterCells = [emitter]
|
||||
}
|
||||
|
||||
public func update(color: UIColor, size: CGSize, cornerRadius: CGFloat, transition: ComponentTransition) {
|
||||
if self.emitterLayer.emitterCells == nil || self.currentColor != color {
|
||||
self.currentColor = color
|
||||
self.setup()
|
||||
}
|
||||
self.emitterLayer.emitterShape = .circle
|
||||
self.emitterLayer.emitterSize = CGSize(width: size.width * 0.7, height: size.height * 0.7)
|
||||
self.emitterLayer.emitterMode = .surface
|
||||
transition.setFrame(layer: self.emitterLayer, frame: CGRect(origin: CGPoint(), size: size))
|
||||
transition.setCornerRadius(layer: self.emitterLayer, cornerRadius: cornerRadius)
|
||||
self.emitterLayer.emitterPosition = CGPoint(x: size.width / 2.0, y: size.height / 2.0)
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,28 @@
|
||||
load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library")
|
||||
|
||||
swift_library(
|
||||
name = "StoryLiveChatMessageComponent",
|
||||
module_name = "StoryLiveChatMessageComponent",
|
||||
srcs = glob([
|
||||
"Sources/**/*.swift",
|
||||
]),
|
||||
copts = [
|
||||
"-warnings-as-errors",
|
||||
],
|
||||
deps = [
|
||||
"//submodules/Display",
|
||||
"//submodules/ComponentFlow",
|
||||
"//submodules/AppBundle",
|
||||
"//submodules/Components/BundleIconComponent",
|
||||
"//submodules/AccountContext",
|
||||
"//submodules/TelegramCore",
|
||||
"//submodules/Components/MultilineTextComponent",
|
||||
"//submodules/Components/MultilineTextWithEntitiesComponent",
|
||||
"//submodules/AvatarNode",
|
||||
"//submodules/TelegramPresentationData",
|
||||
"//submodules/TelegramUI/Components/StarsParticleEffect",
|
||||
],
|
||||
visibility = [
|
||||
"//visibility:public",
|
||||
],
|
||||
)
|
||||
@ -0,0 +1,410 @@
|
||||
import Foundation
|
||||
import UIKit
|
||||
import ComponentFlow
|
||||
import Display
|
||||
import MultilineTextComponent
|
||||
import MultilineTextWithEntitiesComponent
|
||||
import TelegramPresentationData
|
||||
import TelegramCore
|
||||
import AvatarNode
|
||||
import AccountContext
|
||||
import StarsParticleEffect
|
||||
import AppBundle
|
||||
|
||||
private func generateStarsAmountImage() -> UIImage {
|
||||
return UIImage(bundleImageName: "Chat/Message/StarsCount")!.precomposed().withRenderingMode(.alwaysTemplate)
|
||||
}
|
||||
|
||||
public final class StoryLiveChatMessageComponent: Component {
|
||||
public struct Layout: Equatable {
|
||||
public var isFlipped: Bool
|
||||
public var insets: UIEdgeInsets
|
||||
public var fitToWidth: Bool
|
||||
public var transparentBackground: Bool
|
||||
|
||||
public init(isFlipped: Bool, insets: UIEdgeInsets, fitToWidth: Bool, transparentBackground: Bool) {
|
||||
self.isFlipped = isFlipped
|
||||
self.insets = insets
|
||||
self.fitToWidth = fitToWidth
|
||||
self.transparentBackground = transparentBackground
|
||||
}
|
||||
}
|
||||
|
||||
let context: AccountContext
|
||||
let strings: PresentationStrings
|
||||
let theme: PresentationTheme
|
||||
let layout: Layout
|
||||
let message: GroupCallMessagesContext.Message
|
||||
let contextGesture: ((ContextGesture, ContextExtractedContentContainingNode) -> Void)?
|
||||
|
||||
public init(
|
||||
context: AccountContext,
|
||||
strings: PresentationStrings,
|
||||
theme: PresentationTheme,
|
||||
layout: Layout,
|
||||
message: GroupCallMessagesContext.Message,
|
||||
contextGesture: ((ContextGesture, ContextExtractedContentContainingNode) -> Void)?
|
||||
) {
|
||||
self.context = context
|
||||
self.strings = strings
|
||||
self.theme = theme
|
||||
self.layout = layout
|
||||
self.message = message
|
||||
self.contextGesture = contextGesture
|
||||
}
|
||||
|
||||
public static func ==(lhs: StoryLiveChatMessageComponent, rhs: StoryLiveChatMessageComponent) -> Bool {
|
||||
if lhs === rhs {
|
||||
return true
|
||||
}
|
||||
if lhs.context !== rhs.context {
|
||||
return false
|
||||
}
|
||||
if lhs.strings !== rhs.strings {
|
||||
return false
|
||||
}
|
||||
if lhs.theme !== rhs.theme {
|
||||
return false
|
||||
}
|
||||
if lhs.layout != rhs.layout {
|
||||
return false
|
||||
}
|
||||
if lhs.message != rhs.message {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
public final class View: UIView {
|
||||
private let extractedContainerNode: ContextExtractedContentContainingNode
|
||||
private let containerNode: ContextControllerSourceNode
|
||||
|
||||
private let contentContainer: UIView
|
||||
private var avatarNode: AvatarNode?
|
||||
private let textExternal = MultilineTextWithEntitiesComponent.External()
|
||||
private let text = ComponentView<Empty>()
|
||||
private var backgroundView: UIImageView?
|
||||
private var effectLayer: StarsParticleEffectLayer?
|
||||
private var starsAmountBackgroundView: UIImageView?
|
||||
private var starsAmountIcon: UIImageView?
|
||||
private var starsAmountText: ComponentView<Empty>?
|
||||
|
||||
private var component: StoryLiveChatMessageComponent?
|
||||
private weak var state: EmptyComponentState?
|
||||
private var isUpdating: Bool = false
|
||||
|
||||
static let starsAmountImage: UIImage = generateStarsAmountImage()
|
||||
|
||||
override public init(frame: CGRect) {
|
||||
self.contentContainer = UIView()
|
||||
|
||||
self.extractedContainerNode = ContextExtractedContentContainingNode()
|
||||
self.containerNode = ContextControllerSourceNode()
|
||||
|
||||
super.init(frame: frame)
|
||||
|
||||
self.addSubview(self.contentContainer)
|
||||
|
||||
self.containerNode.addSubnode(self.extractedContainerNode)
|
||||
self.containerNode.targetNodeForActivationProgress = self.extractedContainerNode.contentNode
|
||||
self.contentContainer.addSubview(self.containerNode.view)
|
||||
|
||||
self.containerNode.activated = { [weak self] gesture, _ in
|
||||
guard let self, let component = self.component else {
|
||||
return
|
||||
}
|
||||
component.contextGesture?(gesture, self.extractedContainerNode)
|
||||
}
|
||||
}
|
||||
|
||||
required public init?(coder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
deinit {
|
||||
}
|
||||
|
||||
override public func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
|
||||
if !self.bounds.contains(point) {
|
||||
return nil
|
||||
}
|
||||
|
||||
guard let result = super.hitTest(point, with: event) else {
|
||||
return nil
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
func update(component: StoryLiveChatMessageComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: ComponentTransition) -> CGSize {
|
||||
self.isUpdating = true
|
||||
defer {
|
||||
self.isUpdating = false
|
||||
}
|
||||
|
||||
self.component = component
|
||||
self.state = state
|
||||
|
||||
self.contentContainer.transform = component.layout.isFlipped ? CGAffineTransformMakeRotation(-CGFloat.pi) : .identity
|
||||
|
||||
self.containerNode.isGestureEnabled = component.contextGesture != nil
|
||||
|
||||
let insets = component.layout.insets
|
||||
let avatarSize: CGFloat = 24.0
|
||||
let avatarSpacing: CGFloat = 6.0
|
||||
let avatarBackgroundInset: CGFloat = 4.0
|
||||
|
||||
let primaryTextColor = UIColor(white: 1.0, alpha: 1.0)
|
||||
let secondaryTextColor = UIColor(white: 1.0, alpha: 0.8)
|
||||
|
||||
var displayStarsAmountBackground = false
|
||||
var starsAmountTextSize: CGSize?
|
||||
if let paidStars = component.message.paidStars {
|
||||
displayStarsAmountBackground = component.message.text.isEmpty
|
||||
|
||||
let starsAmountIcon: UIImageView
|
||||
if let current = self.starsAmountIcon {
|
||||
starsAmountIcon = current
|
||||
} else {
|
||||
starsAmountIcon = UIImageView()
|
||||
self.starsAmountIcon = starsAmountIcon
|
||||
self.extractedContainerNode.contentNode.view.addSubview(starsAmountIcon)
|
||||
starsAmountIcon.image = View.starsAmountImage
|
||||
}
|
||||
starsAmountIcon.tintColor = secondaryTextColor
|
||||
|
||||
let starsAmountText: ComponentView<Empty>
|
||||
if let current = self.starsAmountText {
|
||||
starsAmountText = current
|
||||
} else {
|
||||
starsAmountText = ComponentView()
|
||||
self.starsAmountText = starsAmountText
|
||||
}
|
||||
|
||||
starsAmountTextSize = starsAmountText.update(
|
||||
transition: .immediate,
|
||||
component: AnyComponent(MultilineTextComponent(
|
||||
text: .plain(NSAttributedString(string: "\(paidStars)", font: Font.semibold(11.0), textColor: displayStarsAmountBackground ? primaryTextColor : secondaryTextColor))
|
||||
)),
|
||||
environment: {},
|
||||
containerSize: CGSize(width: 100.0, height: 100.0)
|
||||
)
|
||||
} else {
|
||||
if let starsAmountIcon = self.starsAmountIcon {
|
||||
self.starsAmountIcon = nil
|
||||
starsAmountIcon.removeFromSuperview()
|
||||
}
|
||||
if let starsAmountText = self.starsAmountText {
|
||||
self.starsAmountText = nil
|
||||
starsAmountText.view?.removeFromSuperview()
|
||||
}
|
||||
}
|
||||
|
||||
if displayStarsAmountBackground, let paidStars = component.message.paidStars, let baseColor = GroupCallMessagesContext.getStarAmountParamMapping(value: paidStars).color {
|
||||
let starsAmountBackgroundView: UIImageView
|
||||
if let current = self.starsAmountBackgroundView {
|
||||
starsAmountBackgroundView = current
|
||||
} else {
|
||||
starsAmountBackgroundView = UIImageView()
|
||||
starsAmountBackgroundView.image = generateStretchableFilledCircleImage(diameter: 20.0, color: .white)?.withRenderingMode(.alwaysTemplate)
|
||||
self.starsAmountBackgroundView = starsAmountBackgroundView
|
||||
|
||||
if let starsAmountIconView = self.starsAmountIcon {
|
||||
self.extractedContainerNode.contentNode.view.insertSubview(starsAmountBackgroundView, belowSubview: starsAmountIconView)
|
||||
} else {
|
||||
self.extractedContainerNode.contentNode.view.addSubview(starsAmountBackgroundView)
|
||||
}
|
||||
}
|
||||
starsAmountBackgroundView.tintColor = StoryLiveChatMessageComponent.getMessageColor(color: baseColor).withMultipliedBrightnessBy(0.7).withMultipliedAlpha(0.5)
|
||||
} else {
|
||||
if let starsAmountBackgroundView = self.starsAmountBackgroundView {
|
||||
self.starsAmountBackgroundView = nil
|
||||
starsAmountBackgroundView.removeFromSuperview()
|
||||
}
|
||||
}
|
||||
|
||||
let textString = NSMutableAttributedString()
|
||||
textString.append(NSAttributedString(string: component.message.author?.displayTitle(strings: component.strings, displayOrder: .firstLast) ?? " ", font: Font.semibold(15.0), textColor: secondaryTextColor))
|
||||
if !component.message.text.isEmpty {
|
||||
textString.append(NSAttributedString(string: " ", font: Font.semibold(15.0), textColor: secondaryTextColor))
|
||||
textString.append(NSAttributedString(string: component.message.text, font: Font.regular(15.0), textColor: primaryTextColor))
|
||||
}
|
||||
|
||||
var textCutout: TextNodeCutout?
|
||||
if let starsAmountTextSize {
|
||||
var cutoutWidth: CGFloat = starsAmountTextSize.width + 20.0
|
||||
if displayStarsAmountBackground {
|
||||
cutoutWidth += 10.0
|
||||
}
|
||||
textCutout = TextNodeCutout(bottomRight: CGSize(width: cutoutWidth, height: 4.0))
|
||||
}
|
||||
|
||||
let textSize = self.text.update(
|
||||
transition: .immediate,
|
||||
component: AnyComponent(MultilineTextWithEntitiesComponent(
|
||||
external: self.textExternal,
|
||||
context: component.context,
|
||||
animationCache: component.context.animationCache,
|
||||
animationRenderer: component.context.animationRenderer,
|
||||
placeholderColor: .gray,
|
||||
text: .plain(textString),
|
||||
maximumNumberOfLines: 0,
|
||||
lineSpacing: 0.1,
|
||||
cutout: textCutout
|
||||
)),
|
||||
environment: {},
|
||||
containerSize: CGSize(width: availableSize.width - insets.left - insets.right - avatarSize - avatarSpacing, height: 100000.0)
|
||||
)
|
||||
|
||||
var avatarFrame = CGRect(origin: CGPoint(x: insets.left, y: insets.top), size: CGSize(width: avatarSize, height: avatarSize))
|
||||
if component.message.paidStars != nil {
|
||||
avatarFrame.origin.y += avatarBackgroundInset
|
||||
if component.layout.fitToWidth {
|
||||
avatarFrame.origin.x += avatarBackgroundInset
|
||||
}
|
||||
}
|
||||
do {
|
||||
let avatarNode: AvatarNode
|
||||
if let current = self.avatarNode {
|
||||
avatarNode = current
|
||||
} else {
|
||||
avatarNode = AvatarNode(font: avatarPlaceholderFont(size: 10.0))
|
||||
self.avatarNode = avatarNode
|
||||
self.extractedContainerNode.contentNode.view.addSubview(avatarNode.view)
|
||||
}
|
||||
transition.setFrame(view: avatarNode.view, frame: avatarFrame)
|
||||
avatarNode.updateSize(size: avatarFrame.size)
|
||||
if let peer = component.message.author {
|
||||
if peer.smallProfileImage != nil {
|
||||
avatarNode.setPeerV2(context: component.context, theme: component.theme, peer: peer, displayDimensions: CGSize(width: avatarSize, height: avatarSize))
|
||||
} else {
|
||||
avatarNode.setPeer(context: component.context, theme: component.theme, peer: peer, displayDimensions: CGSize(width: avatarSize, height: avatarSize))
|
||||
}
|
||||
} else {
|
||||
avatarNode.setCustomLetters([" "])
|
||||
}
|
||||
}
|
||||
|
||||
let textFrame = CGRect(origin: CGPoint(x: insets.left + avatarSize + avatarSpacing, y: avatarFrame.minY + 4.0), size: textSize)
|
||||
if let textView = self.text.view {
|
||||
if textView.superview == nil {
|
||||
textView.layer.anchorPoint = CGPoint()
|
||||
self.extractedContainerNode.contentNode.view.addSubview(textView)
|
||||
}
|
||||
transition.setPosition(view: textView, position: textFrame.origin)
|
||||
textView.bounds = CGRect(origin: CGPoint(), size: textFrame.size)
|
||||
}
|
||||
|
||||
let backgroundOrigin = CGPoint(x: avatarFrame.minX - avatarBackgroundInset, y: avatarFrame.minY - avatarBackgroundInset)
|
||||
var backgroundFrame = CGRect(origin: backgroundOrigin, size: CGSize(width: textFrame.maxX + 8.0 - backgroundOrigin.x, height: avatarFrame.maxY + avatarBackgroundInset - backgroundOrigin.y))
|
||||
if let textLayout = self.textExternal.layout {
|
||||
if textLayout.numberOfLines > 1 {
|
||||
backgroundFrame.size.height = max(backgroundFrame.size.height, textFrame.maxY + 8.0 - backgroundOrigin.y)
|
||||
}
|
||||
}
|
||||
|
||||
if let starsAmountTextSize, let starsAmountTextView = self.starsAmountText?.view, let starsAmountIcon = self.starsAmountIcon {
|
||||
let starsAmountTextFrame: CGRect
|
||||
|
||||
if displayStarsAmountBackground, let starsAmountBackgroundView = self.starsAmountBackgroundView {
|
||||
let starsAmountBackgroundSize = CGSize(width: starsAmountTextSize.width + 5.0 + 20.0, height: 20.0)
|
||||
let starsAmountBackgroundFrame = CGRect(origin: CGPoint(x: backgroundFrame.maxX - 6.0 - starsAmountBackgroundSize.width, y: backgroundFrame.minY + floor((backgroundFrame.height - starsAmountBackgroundSize.height) * 0.5)), size: starsAmountBackgroundSize)
|
||||
transition.setFrame(view: starsAmountBackgroundView, frame: starsAmountBackgroundFrame)
|
||||
|
||||
starsAmountTextFrame = CGRect(origin: CGPoint(x: starsAmountBackgroundFrame.maxX - starsAmountTextSize.width - 5.0, y: starsAmountBackgroundFrame.minY + UIScreenPixel + floor((starsAmountBackgroundFrame.height - starsAmountTextSize.height) * 0.5)), size: starsAmountTextSize)
|
||||
} else {
|
||||
starsAmountTextFrame = CGRect(origin: CGPoint(x: textFrame.maxX - starsAmountTextSize.width - 1.0, y: textFrame.maxY - starsAmountTextSize.height + 1.0), size: starsAmountTextSize)
|
||||
}
|
||||
|
||||
if starsAmountTextView.superview == nil {
|
||||
starsAmountTextView.layer.anchorPoint = CGPoint(x: 1.0, y: 1.0)
|
||||
self.extractedContainerNode.contentNode.view.addSubview(starsAmountTextView)
|
||||
}
|
||||
transition.setPosition(view: starsAmountTextView, position: CGPoint(x: starsAmountTextFrame.maxX, y: starsAmountTextFrame.maxY))
|
||||
starsAmountTextView.bounds = CGRect(origin: CGPoint(), size: starsAmountTextFrame.size)
|
||||
|
||||
if let image = starsAmountIcon.image {
|
||||
let starsAmountIconFrame = CGRect(origin: CGPoint(x: starsAmountTextFrame.minX - 2.0 - image.size.width, y: starsAmountTextFrame.minY + UIScreenPixel), size: image.size)
|
||||
transition.setFrame(view: starsAmountIcon, frame: starsAmountIconFrame)
|
||||
}
|
||||
}
|
||||
|
||||
let size = CGSize(width: component.layout.fitToWidth ? backgroundFrame.maxX : availableSize.width, height: backgroundFrame.maxY)
|
||||
|
||||
let backgroundCornerRadius = (avatarSize + avatarBackgroundInset * 2.0) * 0.5
|
||||
|
||||
if let paidStars = component.message.paidStars, let baseColor = GroupCallMessagesContext.getStarAmountParamMapping(value: paidStars).color {
|
||||
let backgroundView: UIImageView
|
||||
if let current = self.backgroundView {
|
||||
backgroundView = current
|
||||
} else {
|
||||
backgroundView = UIImageView()
|
||||
self.backgroundView = backgroundView
|
||||
self.extractedContainerNode.contentNode.view.insertSubview(backgroundView, at: 0)
|
||||
backgroundView.image = generateStretchableFilledCircleImage(diameter: backgroundCornerRadius * 2.0, color: .white)?.withRenderingMode(.alwaysTemplate)
|
||||
}
|
||||
transition.setFrame(view: backgroundView, frame: backgroundFrame)
|
||||
|
||||
backgroundView.tintColor = StoryLiveChatMessageComponent.getMessageColor(color: baseColor).withAlphaComponent(component.layout.transparentBackground ? 0.7 : 1.0)
|
||||
|
||||
let effectLayer: StarsParticleEffectLayer
|
||||
if let current = self.effectLayer {
|
||||
effectLayer = current
|
||||
} else {
|
||||
effectLayer = StarsParticleEffectLayer()
|
||||
self.effectLayer = effectLayer
|
||||
backgroundView.layer.addSublayer(effectLayer)
|
||||
}
|
||||
|
||||
transition.setFrame(layer: effectLayer, frame: CGRect(origin: CGPoint(), size: backgroundFrame.size))
|
||||
effectLayer.update(color: UIColor(white: 1.0, alpha: 0.5), size: backgroundFrame.size, cornerRadius: backgroundCornerRadius, transition: transition)
|
||||
} else if let backgroundView = self.backgroundView {
|
||||
self.backgroundView = nil
|
||||
backgroundView.removeFromSuperview()
|
||||
|
||||
if let effectLayer = self.effectLayer {
|
||||
self.effectLayer = nil
|
||||
effectLayer.removeFromSuperlayer()
|
||||
}
|
||||
}
|
||||
|
||||
let contentFrame = CGRect(origin: CGPoint(), size: size)
|
||||
transition.setPosition(view: self.contentContainer, position: contentFrame.center)
|
||||
transition.setBounds(view: self.contentContainer, bounds: CGRect(origin: CGPoint(), size: contentFrame.size))
|
||||
|
||||
self.extractedContainerNode.frame = CGRect(origin: CGPoint(), size: size)
|
||||
self.extractedContainerNode.contentNode.frame = CGRect(origin: CGPoint(), size: size)
|
||||
self.extractedContainerNode.contentRect = backgroundFrame.insetBy(dx: -4.0, dy: 0.0)
|
||||
self.containerNode.frame = CGRect(origin: CGPoint(), size: size)
|
||||
|
||||
return size
|
||||
}
|
||||
}
|
||||
|
||||
public func makeView() -> View {
|
||||
return View(frame: CGRect())
|
||||
}
|
||||
|
||||
public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: ComponentTransition) -> CGSize {
|
||||
return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition)
|
||||
}
|
||||
|
||||
public static func getMessageColor(color: GroupCallMessagesContext.Message.Color) -> UIColor {
|
||||
switch color {
|
||||
case .silver:
|
||||
return UIColor(rgb: 0x7C8695)
|
||||
case .red:
|
||||
return UIColor(rgb: 0xE6514E)
|
||||
case .orange:
|
||||
return UIColor(rgb: 0xEE7E20)
|
||||
case .yellow:
|
||||
return UIColor(rgb: 0xE4A20A)
|
||||
case .green:
|
||||
return UIColor(rgb: 0x5AB03D)
|
||||
case .blue:
|
||||
return UIColor(rgb: 0x3E9CDF)
|
||||
case .purple:
|
||||
return UIColor(rgb: 0x985FDC)
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -107,6 +107,8 @@ swift_library(
|
||||
"//submodules/Components/MultilineTextComponent",
|
||||
"//submodules/TelegramUI/Components/Chat/ChatSendStarsScreen",
|
||||
"//submodules/TelegramUI/Components/GlassBackgroundComponent",
|
||||
"//submodules/TelegramUI/Components/Stories/LiveChat/StoryLiveChatMessageComponent",
|
||||
"//submodules/TelegramUI/Components/StarsParticleEffect",
|
||||
],
|
||||
visibility = [
|
||||
"//visibility:public",
|
||||
|
||||
@ -24,8 +24,9 @@ final class StoryAuthorInfoComponent: Component {
|
||||
let counters: Counters?
|
||||
let isEdited: Bool
|
||||
let isLiveStream: Bool
|
||||
let customSubtitle: String?
|
||||
|
||||
init(context: AccountContext, strings: PresentationStrings, peer: EnginePeer?, forwardInfo: EngineStoryItem.ForwardInfo?, author: EnginePeer?, timestamp: Int32, counters: Counters?, isEdited: Bool, isLiveStream: Bool) {
|
||||
init(context: AccountContext, strings: PresentationStrings, peer: EnginePeer?, forwardInfo: EngineStoryItem.ForwardInfo?, author: EnginePeer?, timestamp: Int32, counters: Counters?, isEdited: Bool, isLiveStream: Bool, customSubtitle: String?) {
|
||||
self.context = context
|
||||
self.strings = strings
|
||||
self.peer = peer
|
||||
@ -35,6 +36,7 @@ final class StoryAuthorInfoComponent: Component {
|
||||
self.counters = counters
|
||||
self.isEdited = isEdited
|
||||
self.isLiveStream = isLiveStream
|
||||
self.customSubtitle = customSubtitle
|
||||
}
|
||||
|
||||
static func ==(lhs: StoryAuthorInfoComponent, rhs: StoryAuthorInfoComponent) -> Bool {
|
||||
@ -64,6 +66,9 @@ final class StoryAuthorInfoComponent: Component {
|
||||
}
|
||||
if lhs.isLiveStream != rhs.isLiveStream {
|
||||
return false
|
||||
}
|
||||
if lhs.customSubtitle != rhs.customSubtitle {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
@ -116,7 +121,10 @@ final class StoryAuthorInfoComponent: Component {
|
||||
let subtitleColor = UIColor(white: 1.0, alpha: 0.8)
|
||||
let subtitle: NSAttributedString
|
||||
let subtitleTruncationType: CTLineTruncationType
|
||||
if let forwardInfo = component.forwardInfo {
|
||||
if let customSubtitle = component.customSubtitle {
|
||||
subtitle = NSAttributedString(string: customSubtitle, font: Font.medium(11.0), textColor: titleColor)
|
||||
subtitleTruncationType = .end
|
||||
} else if let forwardInfo = component.forwardInfo {
|
||||
let authorName: String
|
||||
switch forwardInfo {
|
||||
case let .known(peer, _, _):
|
||||
|
||||
@ -1380,6 +1380,13 @@ private final class StoryContainerScreenComponent: Component {
|
||||
self.dismissWithoutTransitionOut = true
|
||||
environment.controller()?.dismiss()
|
||||
} else {
|
||||
var transition: ComponentTransition = .immediate
|
||||
if let previousState = self.stateValue, let previousSlice = previousState.slice, let slice = stateValue?.slice {
|
||||
if previousSlice.item.id == slice.item.id {
|
||||
transition = .spring(duration: 0.4)
|
||||
}
|
||||
}
|
||||
|
||||
self.stateValue = stateValue
|
||||
|
||||
if update {
|
||||
@ -1387,7 +1394,7 @@ private final class StoryContainerScreenComponent: Component {
|
||||
self.environment?.controller()?.dismiss()
|
||||
} else {
|
||||
if !self.isUpdating {
|
||||
self.state?.updated(transition: .immediate)
|
||||
self.state?.updated(transition: transition)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
@ -1395,7 +1402,7 @@ private final class StoryContainerScreenComponent: Component {
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
self.state?.updated(transition: .immediate)
|
||||
self.state?.updated(transition: transition)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -70,6 +70,7 @@ public final class StoryContentItem: Equatable {
|
||||
public let theme: PresentationTheme
|
||||
public let containerInsets: UIEdgeInsets
|
||||
public let presentationProgressUpdated: (Double, Bool, Bool) -> Void
|
||||
public let customItemSubtitleUpdated: () -> Void
|
||||
public let markAsSeen: (StoryId) -> Void
|
||||
|
||||
public init(
|
||||
@ -78,6 +79,7 @@ public final class StoryContentItem: Equatable {
|
||||
theme: PresentationTheme,
|
||||
containerInsets: UIEdgeInsets,
|
||||
presentationProgressUpdated: @escaping (Double, Bool, Bool) -> Void,
|
||||
customItemSubtitleUpdated: @escaping () -> Void,
|
||||
markAsSeen: @escaping (StoryId) -> Void
|
||||
) {
|
||||
self.externalState = externalState
|
||||
@ -85,6 +87,7 @@ public final class StoryContentItem: Equatable {
|
||||
self.theme = theme
|
||||
self.containerInsets = containerInsets
|
||||
self.presentationProgressUpdated = presentationProgressUpdated
|
||||
self.customItemSubtitleUpdated = customItemSubtitleUpdated
|
||||
self.markAsSeen = markAsSeen
|
||||
}
|
||||
|
||||
|
||||
@ -15,249 +15,8 @@ import MultilineTextWithEntitiesComponent
|
||||
import GlassBackgroundComponent
|
||||
import MultilineTextComponent
|
||||
import ContextUI
|
||||
|
||||
private final class MessageItemComponent: Component {
|
||||
let context: AccountContext
|
||||
let strings: PresentationStrings
|
||||
let theme: PresentationTheme
|
||||
let message: GroupCallMessagesContext.Message
|
||||
let contextGesture: ((ContextGesture, ContextExtractedContentContainingNode) -> Void)?
|
||||
|
||||
init(context: AccountContext, strings: PresentationStrings, theme: PresentationTheme, message: GroupCallMessagesContext.Message, contextGesture: ((ContextGesture, ContextExtractedContentContainingNode) -> Void)?) {
|
||||
self.context = context
|
||||
self.strings = strings
|
||||
self.theme = theme
|
||||
self.message = message
|
||||
self.contextGesture = contextGesture
|
||||
}
|
||||
|
||||
static func ==(lhs: MessageItemComponent, rhs: MessageItemComponent) -> Bool {
|
||||
if lhs === rhs {
|
||||
return true
|
||||
}
|
||||
if lhs.context !== rhs.context {
|
||||
return false
|
||||
}
|
||||
if lhs.strings !== rhs.strings {
|
||||
return false
|
||||
}
|
||||
if lhs.theme !== rhs.theme {
|
||||
return false
|
||||
}
|
||||
if lhs.message != rhs.message {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
final class View: UIView {
|
||||
private let extractedContainerNode: ContextExtractedContentContainingNode
|
||||
private let containerNode: ContextControllerSourceNode
|
||||
|
||||
private let contentContainer: UIView
|
||||
private var avatarNode: AvatarNode?
|
||||
private let text = ComponentView<Empty>()
|
||||
private var backgroundView: UIImageView?
|
||||
private var effectLayer: StarsButtonEffectLayer?
|
||||
|
||||
private var component: MessageItemComponent?
|
||||
private weak var state: EmptyComponentState?
|
||||
private var isUpdating: Bool = false
|
||||
|
||||
override init(frame: CGRect) {
|
||||
self.contentContainer = UIView()
|
||||
self.contentContainer.transform = CGAffineTransformMakeRotation(-CGFloat.pi)
|
||||
|
||||
self.extractedContainerNode = ContextExtractedContentContainingNode()
|
||||
self.containerNode = ContextControllerSourceNode()
|
||||
|
||||
super.init(frame: frame)
|
||||
|
||||
self.addSubview(self.contentContainer)
|
||||
|
||||
self.containerNode.addSubnode(self.extractedContainerNode)
|
||||
self.containerNode.targetNodeForActivationProgress = self.extractedContainerNode.contentNode
|
||||
self.contentContainer.addSubview(self.containerNode.view)
|
||||
|
||||
self.containerNode.activated = { [weak self] gesture, _ in
|
||||
guard let self, let component = self.component else {
|
||||
return
|
||||
}
|
||||
component.contextGesture?(gesture, self.extractedContainerNode)
|
||||
}
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
deinit {
|
||||
}
|
||||
|
||||
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
|
||||
if !self.bounds.contains(point) {
|
||||
return nil
|
||||
}
|
||||
|
||||
guard let result = super.hitTest(point, with: event) else {
|
||||
return nil
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
func update(component: MessageItemComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: ComponentTransition) -> CGSize {
|
||||
self.isUpdating = true
|
||||
defer {
|
||||
self.isUpdating = false
|
||||
}
|
||||
|
||||
self.component = component
|
||||
self.state = state
|
||||
|
||||
self.containerNode.isGestureEnabled = component.contextGesture != nil
|
||||
|
||||
let insets = UIEdgeInsets(top: 9.0, left: 20.0, bottom: 9.0, right: 20.0)
|
||||
let avatarSize: CGFloat = 24.0
|
||||
let avatarSpacing: CGFloat = 6.0
|
||||
|
||||
let textString = NSMutableAttributedString()
|
||||
textString.append(NSAttributedString(string: component.message.author?.displayTitle(strings: component.strings, displayOrder: .firstLast) ?? " ", font: Font.semibold(15.0), textColor: UIColor(white: 0.9, alpha: 1.0)))
|
||||
textString.append(NSAttributedString(string: " ", font: Font.semibold(15.0), textColor: UIColor(white: 0.9, alpha: 1.0)))
|
||||
textString.append(NSAttributedString(string: component.message.text, font: Font.regular(15.0), textColor: UIColor(white: 1.0, alpha: 1.0)))
|
||||
|
||||
let textSize = self.text.update(
|
||||
transition: .immediate,
|
||||
component: AnyComponent(MultilineTextWithEntitiesComponent(
|
||||
context: component.context,
|
||||
animationCache: component.context.animationCache,
|
||||
animationRenderer: component.context.animationRenderer,
|
||||
placeholderColor: .gray,
|
||||
text: .plain(textString),
|
||||
maximumNumberOfLines: 0
|
||||
)),
|
||||
environment: {},
|
||||
containerSize: CGSize(width: availableSize.width - insets.left - insets.right - avatarSize - avatarSpacing, height: 100000.0)
|
||||
)
|
||||
|
||||
let size = CGSize(width: availableSize.width, height: insets.top + textSize.height + insets.bottom)
|
||||
|
||||
let avatarFrame = CGRect(origin: CGPoint(x: insets.left, y: insets.top - 4.0), size: CGSize(width: avatarSize, height: avatarSize))
|
||||
do {
|
||||
let avatarNode: AvatarNode
|
||||
if let current = self.avatarNode {
|
||||
avatarNode = current
|
||||
} else {
|
||||
avatarNode = AvatarNode(font: avatarPlaceholderFont(size: 10.0))
|
||||
self.avatarNode = avatarNode
|
||||
self.extractedContainerNode.contentNode.view.addSubview(avatarNode.view)
|
||||
}
|
||||
transition.setFrame(view: avatarNode.view, frame: avatarFrame)
|
||||
avatarNode.updateSize(size: avatarFrame.size)
|
||||
if let peer = component.message.author {
|
||||
if peer.smallProfileImage != nil {
|
||||
avatarNode.setPeerV2(context: component.context, theme: component.theme, peer: peer, displayDimensions: CGSize(width: avatarSize, height: avatarSize))
|
||||
} else {
|
||||
avatarNode.setPeer(context: component.context, theme: component.theme, peer: peer, displayDimensions: CGSize(width: avatarSize, height: avatarSize))
|
||||
}
|
||||
} else {
|
||||
avatarNode.setCustomLetters([" "])
|
||||
}
|
||||
}
|
||||
|
||||
let textFrame = CGRect(origin: CGPoint(x: insets.left + avatarSize + avatarSpacing, y: insets.top), size: textSize)
|
||||
if let textView = self.text.view {
|
||||
if textView.superview == nil {
|
||||
textView.layer.anchorPoint = CGPoint()
|
||||
self.extractedContainerNode.contentNode.view.addSubview(textView)
|
||||
}
|
||||
transition.setPosition(view: textView, position: textFrame.origin)
|
||||
textView.bounds = CGRect(origin: CGPoint(), size: textFrame.size)
|
||||
}
|
||||
|
||||
let backgroundOrigin = CGPoint(x: avatarFrame.minX - 2.0, y: avatarFrame.minY - 2.0)
|
||||
let backgroundFrame = CGRect(origin: backgroundOrigin, size: CGSize(width: textFrame.maxX + 8.0 - backgroundOrigin.x, height: max(avatarFrame.maxY + 2.0, textFrame.maxY + 5.0) - backgroundOrigin.y))
|
||||
|
||||
if let paidStars = component.message.paidStars {
|
||||
let backgroundView: UIImageView
|
||||
if let current = self.backgroundView {
|
||||
backgroundView = current
|
||||
} else {
|
||||
backgroundView = UIImageView()
|
||||
self.backgroundView = backgroundView
|
||||
self.extractedContainerNode.contentNode.view.insertSubview(backgroundView, at: 0)
|
||||
backgroundView.image = generateStretchableFilledCircleImage(diameter: avatarSize + 2.0 * 2.0, color: .white)?.withRenderingMode(.alwaysTemplate)
|
||||
}
|
||||
transition.setFrame(view: backgroundView, frame: backgroundFrame)
|
||||
backgroundView.tintColor = getStarAmountColorMapping(value: paidStars)
|
||||
|
||||
let effectLayer: StarsButtonEffectLayer
|
||||
if let current = self.effectLayer {
|
||||
effectLayer = current
|
||||
} else {
|
||||
effectLayer = StarsButtonEffectLayer()
|
||||
self.effectLayer = effectLayer
|
||||
backgroundView.layer.addSublayer(effectLayer)
|
||||
effectLayer.masksToBounds = true
|
||||
}
|
||||
|
||||
transition.setFrame(layer: effectLayer, frame: CGRect(origin: CGPoint(), size: backgroundFrame.size))
|
||||
transition.setCornerRadius(layer: effectLayer, cornerRadius: min(28.0, backgroundFrame.height * 0.5))
|
||||
effectLayer.update(color: UIColor(white: 1.0, alpha: 0.5), size: backgroundFrame.size)
|
||||
} else if let backgroundView = self.backgroundView {
|
||||
self.backgroundView = nil
|
||||
backgroundView.removeFromSuperview()
|
||||
|
||||
if let effectLayer = self.effectLayer {
|
||||
self.effectLayer = nil
|
||||
effectLayer.removeFromSuperlayer()
|
||||
}
|
||||
}
|
||||
|
||||
let contentFrame = CGRect(origin: CGPoint(), size: size)
|
||||
transition.setPosition(view: self.contentContainer, position: contentFrame.center)
|
||||
transition.setBounds(view: self.contentContainer, bounds: CGRect(origin: CGPoint(), size: contentFrame.size))
|
||||
|
||||
self.extractedContainerNode.frame = CGRect(origin: CGPoint(), size: size)
|
||||
self.extractedContainerNode.contentNode.frame = CGRect(origin: CGPoint(), size: size)
|
||||
self.extractedContainerNode.contentRect = backgroundFrame.insetBy(dx: -4.0, dy: 0.0)
|
||||
self.containerNode.frame = CGRect(origin: CGPoint(), size: size)
|
||||
|
||||
return size
|
||||
}
|
||||
}
|
||||
|
||||
func makeView() -> View {
|
||||
return View(frame: CGRect())
|
||||
}
|
||||
|
||||
func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: ComponentTransition) -> CGSize {
|
||||
return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition)
|
||||
}
|
||||
}
|
||||
|
||||
private func getStarAmountColorMapping(value: Int64) -> UIColor {
|
||||
//TODO:localize unify
|
||||
if value >= 10000 {
|
||||
return UIColor(rgb: 0x7C8695)
|
||||
}
|
||||
if value >= 2000 {
|
||||
return UIColor(rgb: 0xE6514E)
|
||||
}
|
||||
if value >= 500 {
|
||||
return UIColor(rgb: 0xEE7E20)
|
||||
}
|
||||
if value >= 250 {
|
||||
return UIColor(rgb: 0xE4A20A)
|
||||
}
|
||||
if value >= 100 {
|
||||
return UIColor(rgb: 0x5AB03D)
|
||||
}
|
||||
if value >= 50 {
|
||||
return UIColor(rgb: 0x3E9CDF)
|
||||
}
|
||||
return UIColor(rgb: 0x985FDC)
|
||||
}
|
||||
import StarsParticleEffect
|
||||
import StoryLiveChatMessageComponent
|
||||
|
||||
private final class PinnedBarMessageComponent: Component {
|
||||
let context: AccountContext
|
||||
@ -295,7 +54,7 @@ private final class PinnedBarMessageComponent: Component {
|
||||
private let backgroundView: UIImageView
|
||||
private let foregroundClippingView: UIView
|
||||
private let foregroundView: UIImageView
|
||||
private let effectLayer: StarsButtonEffectLayer
|
||||
private let effectLayer: StarsParticleEffectLayer
|
||||
|
||||
private var avatarNode: AvatarNode?
|
||||
private let title = ComponentView<Empty>()
|
||||
@ -311,8 +70,7 @@ private final class PinnedBarMessageComponent: Component {
|
||||
self.foregroundClippingView = UIView()
|
||||
self.foregroundClippingView.clipsToBounds = true
|
||||
self.foregroundView = UIImageView()
|
||||
self.effectLayer = StarsButtonEffectLayer()
|
||||
self.effectLayer.masksToBounds = true
|
||||
self.effectLayer = StarsParticleEffectLayer()
|
||||
|
||||
super.init(frame: frame)
|
||||
|
||||
@ -385,7 +143,7 @@ private final class PinnedBarMessageComponent: Component {
|
||||
self.foregroundView.image = self.backgroundView.image
|
||||
}
|
||||
|
||||
let baseColor = getStarAmountColorMapping(value: component.message.paidStars ?? 0)
|
||||
let baseColor = StoryLiveChatMessageComponent.getMessageColor(color: GroupCallMessagesContext.getStarAmountParamMapping(value: component.message.paidStars ?? 0).color ?? .purple)
|
||||
self.backgroundView.tintColor = baseColor.withMultipliedBrightnessBy(0.7)
|
||||
self.foregroundView.tintColor = baseColor
|
||||
|
||||
@ -398,8 +156,7 @@ private final class PinnedBarMessageComponent: Component {
|
||||
transition.setFrame(view: self.foregroundClippingView, frame: CGRect(origin: CGPoint(), size: CGSize(width: floorToScreenPixels(size.width * timeFraction), height: size.height)))
|
||||
|
||||
transition.setFrame(layer: self.effectLayer, frame: CGRect(origin: CGPoint(), size: size))
|
||||
transition.setCornerRadius(layer: self.effectLayer, cornerRadius: size.height * 0.5)
|
||||
self.effectLayer.update(color: UIColor(white: 1.0, alpha: 0.5), size: size)
|
||||
self.effectLayer.update(color: UIColor(white: 1.0, alpha: 0.5), size: size, cornerRadius: size.height * 0.5, transition: transition)
|
||||
|
||||
let avatarFrame = CGRect(origin: CGPoint(x: avatarInset, y: floor((itemHeight - avatarSize) * 0.5)), size: CGSize(width: avatarSize, height: avatarSize))
|
||||
do {
|
||||
@ -603,6 +360,14 @@ private final class PinnedBarComponent: Component {
|
||||
}
|
||||
|
||||
final class StoryContentLiveChatComponent: Component {
|
||||
final class External {
|
||||
fileprivate(set) var hasUnseenMessages: Bool = false
|
||||
|
||||
init() {
|
||||
}
|
||||
}
|
||||
|
||||
let external: External
|
||||
let context: AccountContext
|
||||
let strings: PresentationStrings
|
||||
let theme: PresentationTheme
|
||||
@ -611,6 +376,7 @@ final class StoryContentLiveChatComponent: Component {
|
||||
let insets: UIEdgeInsets
|
||||
|
||||
init(
|
||||
external: External,
|
||||
context: AccountContext,
|
||||
strings: PresentationStrings,
|
||||
theme: PresentationTheme,
|
||||
@ -618,6 +384,7 @@ final class StoryContentLiveChatComponent: Component {
|
||||
storyPeerId: EnginePeer.Id,
|
||||
insets: UIEdgeInsets
|
||||
) {
|
||||
self.external = external
|
||||
self.context = context
|
||||
self.strings = strings
|
||||
self.theme = theme
|
||||
@ -627,6 +394,9 @@ final class StoryContentLiveChatComponent: Component {
|
||||
}
|
||||
|
||||
static func ==(lhs: StoryContentLiveChatComponent, rhs: StoryContentLiveChatComponent) -> Bool {
|
||||
if lhs.external !== rhs.external {
|
||||
return false
|
||||
}
|
||||
if lhs.context !== rhs.context {
|
||||
return false
|
||||
}
|
||||
@ -736,7 +506,7 @@ final class StoryContentLiveChatComponent: Component {
|
||||
self.addSubview(self.listShadowView)
|
||||
self.addSubview(self.listContainer)
|
||||
|
||||
self.isChatExpanded = true
|
||||
//self.isChatExpanded = true
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
@ -760,7 +530,13 @@ final class StoryContentLiveChatComponent: Component {
|
||||
}
|
||||
|
||||
func toggleLiveChatExpanded() {
|
||||
guard let component = self.component else {
|
||||
return
|
||||
}
|
||||
self.isChatExpanded = !self.isChatExpanded
|
||||
if self.isChatExpanded {
|
||||
component.external.hasUnseenMessages = false
|
||||
}
|
||||
self.state?.updated(transition: .spring(duration: 0.4))
|
||||
}
|
||||
|
||||
@ -862,7 +638,25 @@ final class StoryContentLiveChatComponent: Component {
|
||||
if self.messagesState == nil {
|
||||
updateTransition = .immediate
|
||||
}
|
||||
|
||||
if let component = self.component, let previousMessagesState = self.messagesState, !self.isChatExpanded {
|
||||
var hasNewMessages = false
|
||||
for message in state.messages {
|
||||
//TODO:release
|
||||
//if message.author?.id != component.context.account.peerId {
|
||||
do {
|
||||
if !previousMessagesState.messages.contains(where: { $0.id == message.id }) {
|
||||
hasNewMessages = true
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
if hasNewMessages {
|
||||
component.external.hasUnseenMessages = true
|
||||
}
|
||||
}
|
||||
self.messagesState = state
|
||||
|
||||
if !self.isUpdating {
|
||||
self.state?.updated(transition: updateTransition)
|
||||
}
|
||||
@ -873,6 +667,10 @@ final class StoryContentLiveChatComponent: Component {
|
||||
self.component = component
|
||||
self.state = state
|
||||
|
||||
if self.isChatExpanded {
|
||||
component.external.hasUnseenMessages = false
|
||||
}
|
||||
|
||||
let previousListIsEmpty = self.currentListIsEmpty
|
||||
|
||||
var listItems: [AnyComponentWithIdentity<Empty>] = []
|
||||
@ -880,10 +678,16 @@ final class StoryContentLiveChatComponent: Component {
|
||||
if let messagesState = self.messagesState {
|
||||
for message in messagesState.messages.reversed() {
|
||||
let messageId = message.id
|
||||
listItems.append(AnyComponentWithIdentity(id: message.id, component: AnyComponent(MessageItemComponent(
|
||||
listItems.append(AnyComponentWithIdentity(id: message.id, component: AnyComponent(StoryLiveChatMessageComponent(
|
||||
context: component.context,
|
||||
strings: component.strings,
|
||||
theme: component.theme,
|
||||
layout: StoryLiveChatMessageComponent.Layout(
|
||||
isFlipped: true,
|
||||
insets: UIEdgeInsets(top: 9.0, left: 20.0, bottom: 9.0, right: 20.0),
|
||||
fitToWidth: false,
|
||||
transparentBackground: true
|
||||
),
|
||||
message: message,
|
||||
contextGesture: { [weak self] gesture, sourceNode in
|
||||
guard let self else {
|
||||
@ -1006,70 +810,6 @@ final class StoryContentLiveChatComponent: Component {
|
||||
}
|
||||
}
|
||||
|
||||
private final class StarsButtonEffectLayer: SimpleLayer {
|
||||
let emitterLayer = CAEmitterLayer()
|
||||
private var currentColor: UIColor?
|
||||
|
||||
override init() {
|
||||
super.init()
|
||||
|
||||
self.addSublayer(self.emitterLayer)
|
||||
}
|
||||
|
||||
override init(layer: Any) {
|
||||
super.init(layer: layer)
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
private func setup() {
|
||||
guard let currentColor = self.currentColor else {
|
||||
return
|
||||
}
|
||||
let color = currentColor
|
||||
|
||||
let emitter = CAEmitterCell()
|
||||
emitter.name = "emitter"
|
||||
emitter.contents = UIImage(bundleImageName: "Premium/Stars/Particle")?.cgImage
|
||||
emitter.birthRate = 25.0
|
||||
emitter.lifetime = 2.0
|
||||
emitter.velocity = 12.0
|
||||
emitter.velocityRange = 3
|
||||
emitter.scale = 0.1
|
||||
emitter.scaleRange = 0.08
|
||||
emitter.alphaRange = 0.1
|
||||
emitter.emissionRange = .pi * 2.0
|
||||
emitter.setValue(3.0, forKey: "mass")
|
||||
emitter.setValue(2.0, forKey: "massRange")
|
||||
|
||||
let staticColors: [Any] = [
|
||||
color.withAlphaComponent(0.0).cgColor,
|
||||
color.cgColor,
|
||||
color.cgColor,
|
||||
color.withAlphaComponent(0.0).cgColor
|
||||
]
|
||||
let staticColorBehavior = CAEmitterCell.createEmitterBehavior(type: "colorOverLife")
|
||||
staticColorBehavior.setValue(staticColors, forKey: "colors")
|
||||
emitter.setValue([staticColorBehavior], forKey: "emitterBehaviors")
|
||||
|
||||
self.emitterLayer.emitterCells = [emitter]
|
||||
}
|
||||
|
||||
func update(color: UIColor, size: CGSize) {
|
||||
if self.emitterLayer.emitterCells == nil || self.currentColor != color {
|
||||
self.currentColor = color
|
||||
self.setup()
|
||||
}
|
||||
self.emitterLayer.emitterShape = .circle
|
||||
self.emitterLayer.emitterSize = CGSize(width: size.width * 0.7, height: size.height * 0.7)
|
||||
self.emitterLayer.emitterMode = .surface
|
||||
self.emitterLayer.frame = CGRect(origin: .zero, size: size)
|
||||
self.emitterLayer.emitterPosition = CGPoint(x: size.width / 2.0, y: size.height / 2.0)
|
||||
}
|
||||
}
|
||||
|
||||
private final class ItemExtractedContentSource: ContextExtractedContentSource {
|
||||
let keepInPlace: Bool
|
||||
let ignoreContentTouches: Bool = true
|
||||
|
||||
@ -88,16 +88,28 @@ final class StoryItemContentComponent: Component {
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
struct LiveChatState {
|
||||
var isExpanded: Bool
|
||||
var hasUnseenMessages: Bool
|
||||
|
||||
init(isExpanded: Bool, hasUnseenMessages: Bool) {
|
||||
self.isExpanded = isExpanded
|
||||
self.hasUnseenMessages = hasUnseenMessages
|
||||
}
|
||||
}
|
||||
|
||||
final class View: StoryContentItem.View {
|
||||
private let imageView: StoryItemImageView
|
||||
private let overlaysView: StoryItemOverlaysView
|
||||
private var videoNode: UniversalVideoNode?
|
||||
private(set) var mediaStreamCall: PresentationGroupCallImpl?
|
||||
private var liveCallStateDisposable: Disposable?
|
||||
private var mediaStream: ComponentView<Empty>?
|
||||
private var loadingEffectView: StoryItemLoadingEffectView?
|
||||
private var loadingEffectAppearanceTimer: SwiftSignalKit.Timer?
|
||||
|
||||
private let liveChatExternal = StoryContentLiveChatComponent.External()
|
||||
private var liveChat: ComponentView<Empty>?
|
||||
|
||||
private var mediaAreasEffectView: StoryItemLoadingEffectView?
|
||||
@ -129,20 +141,25 @@ final class StoryItemContentComponent: Component {
|
||||
override var videoPlaybackPosition: Double? {
|
||||
return self.videoPlaybackStatus?.timestamp
|
||||
}
|
||||
|
||||
var customSubtitle: String?
|
||||
|
||||
private let hierarchyTrackingLayer: HierarchyTrackingLayer
|
||||
|
||||
private var fetchPriorityResourceId: String?
|
||||
private var currentFetchPriority: (isMain: Bool, disposable: Disposable)?
|
||||
|
||||
public var isLiveChatExpanded: Bool? {
|
||||
public var liveChatState: LiveChatState? {
|
||||
guard let liveChatView = self.liveChat?.view as? StoryContentLiveChatComponent.View else {
|
||||
return nil
|
||||
}
|
||||
if liveChatView.isChatEmpty {
|
||||
return nil
|
||||
}
|
||||
return liveChatView.isChatExpanded
|
||||
return LiveChatState(
|
||||
isExpanded: liveChatView.isChatExpanded,
|
||||
hasUnseenMessages: self.liveChatExternal.hasUnseenMessages
|
||||
)
|
||||
}
|
||||
|
||||
public func toggleLiveChatExpanded() {
|
||||
@ -195,6 +212,7 @@ final class StoryItemContentComponent: Component {
|
||||
self.currentProgressTimer?.invalidate()
|
||||
self.videoProgressDisposable?.dispose()
|
||||
self.currentFetchPriority?.disposable.dispose()
|
||||
self.liveCallStateDisposable?.dispose()
|
||||
}
|
||||
|
||||
func allowsInstantPauseOnTouch(point: CGPoint) -> Bool {
|
||||
@ -639,6 +657,11 @@ final class StoryItemContentComponent: Component {
|
||||
if case .liveStream = component.item.media {
|
||||
selectedMedia = component.item.media
|
||||
messageMedia = selectedMedia
|
||||
|
||||
//TODO:localize
|
||||
if self.customSubtitle == nil {
|
||||
self.customSubtitle = "loading..."
|
||||
}
|
||||
} else if !component.preferHighQuality, !component.item.isMy, let alternativeMediaValue = component.item.alternativeMediaList.first {
|
||||
selectedMedia = alternativeMediaValue
|
||||
|
||||
@ -818,6 +841,7 @@ final class StoryItemContentComponent: Component {
|
||||
let _ = liveChat.update(
|
||||
transition: mediaStreamTransition,
|
||||
component: AnyComponent(StoryContentLiveChatComponent(
|
||||
external: self.liveChatExternal,
|
||||
context: component.context,
|
||||
strings: component.strings,
|
||||
theme: environment.theme,
|
||||
@ -967,6 +991,31 @@ final class StoryItemContentComponent: Component {
|
||||
}
|
||||
}
|
||||
|
||||
if let mediaStreamCall = self.mediaStreamCall {
|
||||
if self.liveCallStateDisposable == nil {
|
||||
self.liveCallStateDisposable = (mediaStreamCall.members
|
||||
|> deliverOnMainQueue).startStandalone(next: { [weak self] members in
|
||||
guard let self, let environment = self.environment else {
|
||||
return
|
||||
}
|
||||
//TODO:localize
|
||||
let subtitle: String
|
||||
if let members {
|
||||
subtitle = "\(max(1, members.totalCount)) watching"
|
||||
} else {
|
||||
subtitle = "loading..."
|
||||
}
|
||||
if self.customSubtitle != subtitle {
|
||||
self.customSubtitle = subtitle
|
||||
environment.customItemSubtitleUpdated()
|
||||
}
|
||||
})
|
||||
}
|
||||
} else if let liveCallStateDisposable = self.liveCallStateDisposable {
|
||||
self.liveCallStateDisposable = nil
|
||||
liveCallStateDisposable.dispose()
|
||||
}
|
||||
|
||||
switch selectedMedia {
|
||||
case .image, .file, .liveStream:
|
||||
if let unsupportedText = self.unsupportedText {
|
||||
|
||||
@ -323,6 +323,7 @@ public final class StoryItemSetContainerComponent: Component {
|
||||
let view = ComponentView<StoryContentItem.Environment>()
|
||||
var currentProgress: Double = 0.0
|
||||
var isBuffering: Bool = false
|
||||
var customSubtitle: String?
|
||||
var requestedNext: Bool = false
|
||||
var footerPanel: ComponentView<Empty>?
|
||||
|
||||
@ -1561,6 +1562,20 @@ public final class StoryItemSetContainerComponent: Component {
|
||||
}
|
||||
}
|
||||
},
|
||||
customItemSubtitleUpdated: { [weak self, weak visibleItem] in
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
guard let visibleItem, let visibleItemView = visibleItem.view.view as? StoryItemContentComponent.View else {
|
||||
return
|
||||
}
|
||||
if visibleItem.customSubtitle != visibleItemView.customSubtitle {
|
||||
visibleItem.customSubtitle = visibleItemView.customSubtitle
|
||||
if !self.isUpdatingComponent {
|
||||
self.state?.updated(transition: .immediate)
|
||||
}
|
||||
}
|
||||
},
|
||||
markAsSeen: { [weak self] id in
|
||||
guard let self, let component = self.component else {
|
||||
return
|
||||
@ -1596,7 +1611,7 @@ public final class StoryItemSetContainerComponent: Component {
|
||||
},
|
||||
containerSize: itemLayout.contentFrame.size
|
||||
)
|
||||
if let view = visibleItem.view.view {
|
||||
if let view = visibleItem.view.view as? StoryItemContentComponent.View {
|
||||
if visibleItem.contentContainerView.superview == nil {
|
||||
visibleItem.view.parentState = self.state
|
||||
self.itemsContainerView.addSubview(visibleItem.contentContainerView)
|
||||
@ -1605,6 +1620,8 @@ public final class StoryItemSetContainerComponent: Component {
|
||||
visibleItem.contentContainerView.addSubview(view)
|
||||
}
|
||||
|
||||
visibleItem.customSubtitle = view.customSubtitle
|
||||
|
||||
itemTransition.setPosition(view: view, position: CGPoint(x: itemLayout.contentFrame.size.width * 0.5, y: itemLayout.contentFrame.size.height * 0.5))
|
||||
itemTransition.setBounds(view: view, bounds: CGRect(origin: CGPoint(), size: itemLayout.contentFrame.size))
|
||||
|
||||
@ -1667,9 +1684,7 @@ public final class StoryItemSetContainerComponent: Component {
|
||||
itemProgressMode = .pause
|
||||
}
|
||||
|
||||
if let view = view as? StoryContentItem.View {
|
||||
view.setProgressMode(itemProgressMode)
|
||||
}
|
||||
view.setProgressMode(itemProgressMode)
|
||||
|
||||
var isChannel = false
|
||||
var canShare = true
|
||||
@ -2916,13 +2931,21 @@ public final class StoryItemSetContainerComponent: Component {
|
||||
}
|
||||
|
||||
var maxInputLength = 4096
|
||||
var maxEmojiCount: Int?
|
||||
if isLiveStream {
|
||||
maxInputLength = GroupCallMessagesContext.getStarAmountParamMapping(value: self.sendMessageContext.currentLiveStreamMessageStars?.value ?? 0).maxLength
|
||||
let params = GroupCallMessagesContext.getStarAmountParamMapping(value: self.sendMessageContext.currentLiveStreamMessageStars?.value ?? 0)
|
||||
maxInputLength = params.maxLength
|
||||
maxEmojiCount = params.emojiCount
|
||||
}
|
||||
|
||||
var isLiveChatExpanded: Bool?
|
||||
var liveChatState: MessageInputPanelComponent.LiveChatState?
|
||||
if let visibleItemView = self.visibleItems[component.slice.item.id]?.view.view as? StoryItemContentComponent.View {
|
||||
isLiveChatExpanded = visibleItemView.isLiveChatExpanded
|
||||
liveChatState = visibleItemView.liveChatState.flatMap { liveChatState in
|
||||
return MessageInputPanelComponent.LiveChatState(
|
||||
isExpanded: liveChatState.isExpanded,
|
||||
hasUnseenMessages: liveChatState.hasUnseenMessages
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
inputPanelSize = self.inputPanel.update(
|
||||
@ -2936,6 +2959,7 @@ public final class StoryItemSetContainerComponent: Component {
|
||||
placeholder: inputPlaceholder,
|
||||
sendPaidMessageStars: isLiveStream ? self.sendMessageContext.currentLiveStreamMessageStars : component.slice.additionalPeerData.sendPaidMessageStars,
|
||||
maxLength: maxInputLength,
|
||||
maxEmojiCount: maxEmojiCount,
|
||||
queryTypes: [.mention, .hashtag, .emoji],
|
||||
alwaysDarkWhenHasText: component.metrics.widthClass == .regular,
|
||||
resetInputContents: resetInputContents,
|
||||
@ -2965,7 +2989,7 @@ public final class StoryItemSetContainerComponent: Component {
|
||||
}
|
||||
|
||||
if let visibleItemView = self.visibleItems[component.slice.item.id]?.view.view as? StoryItemContentComponent.View {
|
||||
if !(visibleItemView.isLiveChatExpanded ?? true) {
|
||||
if !(visibleItemView.liveChatState?.isExpanded ?? true) {
|
||||
visibleItemView.toggleLiveChatExpanded()
|
||||
}
|
||||
}
|
||||
@ -3071,7 +3095,7 @@ public final class StoryItemSetContainerComponent: Component {
|
||||
if !hasFirstResponder(self) {
|
||||
self.state?.updated(transition: .spring(duration: 0.4))
|
||||
} else {
|
||||
self.state?.updated(transition: .immediate)
|
||||
self.state?.updated(transition: .spring(duration: 0.4))
|
||||
}
|
||||
},
|
||||
timeoutAction: nil,
|
||||
@ -3159,7 +3183,7 @@ public final class StoryItemSetContainerComponent: Component {
|
||||
isChannel: isChannel,
|
||||
storyItem: component.slice.item.storyItem,
|
||||
chatLocation: nil,
|
||||
isLiveChatExpanded: isLiveChatExpanded,
|
||||
liveChatState: liveChatState,
|
||||
toggleLiveChatExpanded: { [weak self] in
|
||||
guard let self else {
|
||||
return
|
||||
@ -3167,7 +3191,17 @@ public final class StoryItemSetContainerComponent: Component {
|
||||
if let visibleItemView = self.visibleItems[component.slice.item.id]?.view.view as? StoryItemContentComponent.View {
|
||||
visibleItemView.toggleLiveChatExpanded()
|
||||
}
|
||||
}
|
||||
},
|
||||
sendStarsAction: isLiveStream ? { [weak self] sourceView, isLongPress in
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
if isLongPress {
|
||||
self.sendMessageContext.openSendStars(view: self)
|
||||
} else {
|
||||
self.sendMessageContext.performSendStars(view: self, buttonView: sourceView, count: 1, isFromExpandedView: false)
|
||||
}
|
||||
} : nil
|
||||
)),
|
||||
environment: {},
|
||||
containerSize: CGSize(width: inputPanelAvailableWidth, height: 200.0)
|
||||
@ -4132,6 +4166,11 @@ public final class StoryItemSetContainerComponent: Component {
|
||||
)
|
||||
}
|
||||
|
||||
var customSubtitle: String?
|
||||
if let visibleItem = self.visibleItems[focusedItem.id] {
|
||||
customSubtitle = visibleItem.customSubtitle
|
||||
}
|
||||
|
||||
let centerInfoComponent = AnyComponent(StoryAuthorInfoComponent(
|
||||
context: component.context,
|
||||
strings: component.strings,
|
||||
@ -4141,7 +4180,8 @@ public final class StoryItemSetContainerComponent: Component {
|
||||
timestamp: component.slice.item.storyItem.timestamp,
|
||||
counters: counters,
|
||||
isEdited: component.slice.item.storyItem.isEdited,
|
||||
isLiveStream: isLiveStream
|
||||
isLiveStream: isLiveStream,
|
||||
customSubtitle: customSubtitle
|
||||
))
|
||||
if let centerInfoItem = self.centerInfoItem, centerInfoItem.component == centerInfoComponent {
|
||||
currentCenterInfoItem = centerInfoItem
|
||||
|
||||
@ -93,7 +93,6 @@ final class StoryItemSetContainerSendMessage {
|
||||
var inputMediaNodeStateContext = ChatEntityKeyboardInputNode.StateContext()
|
||||
var inputMediaInteraction: ChatEntityKeyboardInputNode.Interaction?
|
||||
var inputMediaNode: ChatEntityKeyboardInputNode?
|
||||
var inputMediaNodeBackground = SimpleLayer()
|
||||
|
||||
let controllerNavigationDisposable = MetaDisposable()
|
||||
let enqueueMediaMessageDisposable = MetaDisposable()
|
||||
@ -230,14 +229,40 @@ final class StoryItemSetContainerSendMessage {
|
||||
|
||||
var height: CGFloat = 0.0
|
||||
if let component = self.view?.component, case .media = self.currentInputMode, let inputData = self.inputMediaNodeData {
|
||||
var updatedInputData = inputData
|
||||
var isLiveStream = false
|
||||
if case .liveStream = component.slice.item.storyItem.media {
|
||||
isLiveStream = true
|
||||
}
|
||||
|
||||
if isLiveStream {
|
||||
updatedInputData = ChatEntityKeyboardInputNode.InputData(
|
||||
emoji: updatedInputData.emoji,
|
||||
stickers: nil,
|
||||
gifs: nil,
|
||||
availableGifSearchEmojies: []
|
||||
)
|
||||
}
|
||||
|
||||
let inputMediaNode: ChatEntityKeyboardInputNode
|
||||
if let current = self.inputMediaNode {
|
||||
inputMediaNode = current
|
||||
} else {
|
||||
inputMediaNode = ChatEntityKeyboardInputNode(
|
||||
context: context,
|
||||
currentInputData: inputData,
|
||||
updatedInputData: component.keyboardInputData,
|
||||
currentInputData: updatedInputData,
|
||||
updatedInputData: component.keyboardInputData |> map { inputData in
|
||||
if isLiveStream {
|
||||
return ChatEntityKeyboardInputNode.InputData(
|
||||
emoji: inputData.emoji,
|
||||
stickers: nil,
|
||||
gifs: nil,
|
||||
availableGifSearchEmojies: []
|
||||
)
|
||||
} else {
|
||||
return inputData
|
||||
}
|
||||
},
|
||||
defaultToEmojiTab: self.inputPanelExternalState?.hasText ?? false,
|
||||
opaqueTopPanelBackground: false,
|
||||
interaction: self.inputMediaInteraction,
|
||||
@ -247,8 +272,6 @@ final class StoryItemSetContainerSendMessage {
|
||||
inputMediaNode.externalTopPanelContainerImpl = nil
|
||||
inputMediaNode.useExternalSearchContainer = true
|
||||
if inputMediaNode.view.superview == nil {
|
||||
self.inputMediaNodeBackground.removeAllAnimations()
|
||||
self.inputMediaNodeBackground.backgroundColor = UIColor(rgb: 0x000000, alpha: 0.7).cgColor
|
||||
view.inputPanelContainer.addSubview(inputMediaNode.view)
|
||||
}
|
||||
self.inputMediaNode = inputMediaNode
|
||||
@ -285,24 +308,19 @@ final class StoryItemSetContainerSendMessage {
|
||||
let inputNodeHeight = heightAndOverflow.0
|
||||
let inputNodeFrame = CGRect(origin: CGPoint(x: 0.0, y: availableSize.height - inputNodeHeight), size: CGSize(width: availableSize.width, height: inputNodeHeight))
|
||||
|
||||
if self.needsInputActivation {
|
||||
do {
|
||||
let inputNodeFrame = inputNodeFrame.offsetBy(dx: 0.0, dy: inputNodeHeight)
|
||||
ComponentTransition.immediate.setFrame(layer: inputMediaNode.layer, frame: inputNodeFrame)
|
||||
ComponentTransition.immediate.setFrame(layer: self.inputMediaNodeBackground, frame: inputNodeFrame)
|
||||
}
|
||||
|
||||
transition.setFrame(layer: inputMediaNode.layer, frame: inputNodeFrame)
|
||||
transition.setFrame(layer: self.inputMediaNodeBackground, frame: inputNodeFrame)
|
||||
|
||||
height = heightAndOverflow.0
|
||||
} else if let inputMediaNode = self.inputMediaNode {
|
||||
self.inputMediaNode = nil
|
||||
|
||||
var targetFrame = inputMediaNode.frame
|
||||
if effectiveInputHeight > 0.0 {
|
||||
targetFrame.origin.y = availableSize.height - effectiveInputHeight
|
||||
} else {
|
||||
targetFrame.origin.y = availableSize.height
|
||||
}
|
||||
targetFrame.origin.y = availableSize.height
|
||||
transition.setFrame(view: inputMediaNode.view, frame: targetFrame, completion: { [weak inputMediaNode] _ in
|
||||
if let inputMediaNode {
|
||||
Queue.mainQueue().after(0.3) {
|
||||
@ -312,18 +330,6 @@ final class StoryItemSetContainerSendMessage {
|
||||
}
|
||||
}
|
||||
})
|
||||
transition.setFrame(layer: self.inputMediaNodeBackground, frame: targetFrame, completion: { _ in
|
||||
Queue.mainQueue().after(0.3) {
|
||||
if self.currentInputMode == .text {
|
||||
self.inputMediaNodeBackground.animateAlpha(from: 1.0, to: 0.0, duration: 0.35, removeOnCompletion: false, completion: { finished in
|
||||
if finished {
|
||||
self.inputMediaNodeBackground.removeFromSuperlayer()
|
||||
}
|
||||
self.inputMediaNodeBackground.removeAllAnimations()
|
||||
})
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
if self.needsInputActivation {
|
||||
@ -347,16 +353,6 @@ final class StoryItemSetContainerSendMessage {
|
||||
additive: true
|
||||
)
|
||||
inputMediaNode.layer.animateAlpha(from: inputMediaNode.alpha, to: 0.0, duration: 0.3, removeOnCompletion: false)
|
||||
|
||||
self.inputMediaNodeBackground.animatePosition(
|
||||
from: CGPoint(),
|
||||
to: CGPoint(x: 0.0, y: bounds.height - self.inputMediaNodeBackground.frame.minY),
|
||||
duration: 0.3,
|
||||
timingFunction: kCAMediaTimingFunctionSpring,
|
||||
removeOnCompletion: false,
|
||||
additive: true
|
||||
)
|
||||
self.inputMediaNodeBackground.animateAlpha(from: CGFloat(self.inputMediaNodeBackground.opacity), to: 0.0, duration: 0.3, removeOnCompletion: false)
|
||||
}
|
||||
}
|
||||
|
||||
@ -1276,19 +1272,33 @@ final class StoryItemSetContainerSendMessage {
|
||||
guard let controller = component.controller() else {
|
||||
return
|
||||
}
|
||||
guard let inputPanelView = view.inputPanel.view as? MessageInputPanelComponent.View else {
|
||||
return
|
||||
}
|
||||
let focusedItem = component.slice.item
|
||||
guard let peerId = focusedItem.peerId else {
|
||||
return
|
||||
}
|
||||
|
||||
let initialData = await ChatSendStarsScreen.initialDataLiveStreamMessage(context: component.context, peerId: peerId, completion: { [weak self, weak view] amount, _ in
|
||||
guard let self, let view else {
|
||||
return
|
||||
var inputText = NSAttributedString(string: "")
|
||||
switch inputPanelView.getSendMessageInput() {
|
||||
case let .text(text):
|
||||
inputText = text
|
||||
}
|
||||
|
||||
let initialData = await ChatSendStarsScreen.initialDataLiveStreamMessage(
|
||||
context: component.context,
|
||||
peerId: peerId,
|
||||
text: inputText,
|
||||
completion: { [weak self, weak view] amount, _ in
|
||||
guard let self, let view else {
|
||||
return
|
||||
}
|
||||
|
||||
self.currentLiveStreamMessageStars = StarsAmount(value: amount, nanos: 0)
|
||||
view.state?.updated(transition: .spring(duration: 0.4))
|
||||
}
|
||||
|
||||
self.currentLiveStreamMessageStars = StarsAmount(value: amount, nanos: 0)
|
||||
view.state?.updated(transition: .spring(duration: 0.4))
|
||||
}).get()
|
||||
).get()
|
||||
if let initialData {
|
||||
controller.push(ChatSendStarsScreen(
|
||||
context: component.context,
|
||||
@ -3816,6 +3826,51 @@ final class StoryItemSetContainerSendMessage {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func openSendStars(view: StoryItemSetContainerComponent.View) {
|
||||
Task { @MainActor [weak view] in
|
||||
guard let view else {
|
||||
return
|
||||
}
|
||||
guard let component = view.component else {
|
||||
return
|
||||
}
|
||||
guard let controller = component.controller() else {
|
||||
return
|
||||
}
|
||||
let focusedItem = component.slice.item
|
||||
guard let peerId = focusedItem.peerId else {
|
||||
return
|
||||
}
|
||||
|
||||
let initialData = await ChatSendStarsScreen.initialData(
|
||||
context: component.context,
|
||||
peerId: peerId,
|
||||
reactSubject: .liveStream(peerId: peerId, storyId: focusedItem.storyItem.id),
|
||||
topPeers: [],
|
||||
completion: { [weak view] amount, privacy, isBecomingTop, transitionOut in
|
||||
guard let view, let component = view.component else {
|
||||
return
|
||||
}
|
||||
let _ = component.context.engine.messages.sendStoryStars(peerId: component.slice.effectivePeer.id, id: component.slice.item.storyItem.id, count: Int(amount)).startStandalone()
|
||||
}).get()
|
||||
if let initialData {
|
||||
controller.push(ChatSendStarsScreen(
|
||||
context: component.context,
|
||||
initialData: initialData,
|
||||
theme: component.theme
|
||||
))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func performSendStars(view: StoryItemSetContainerComponent.View, buttonView: UIView, count: Int, isFromExpandedView: Bool) {
|
||||
guard let component = view.component else {
|
||||
return
|
||||
}
|
||||
|
||||
let _ = component.context.engine.messages.sendStoryStars(peerId: component.slice.effectivePeer.id, id: component.slice.item.storyItem.id, count: count).startStandalone()
|
||||
}
|
||||
}
|
||||
|
||||
public class StoryProgressPauseContext {
|
||||
|
||||
@ -4274,7 +4274,7 @@ extension ChatControllerImpl {
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
let controller = self.context.sharedContext.makeStarsPurchaseScreen(context: self.context, starsContext: starsContext, options: options, purpose: .generic, targetPeerId: nil, completion: { _ in
|
||||
let controller = self.context.sharedContext.makeStarsPurchaseScreen(context: self.context, starsContext: starsContext, options: options, purpose: .generic, targetPeerId: nil, customTheme: nil, completion: { _ in
|
||||
})
|
||||
self.push(controller)
|
||||
})
|
||||
|
||||
@ -456,7 +456,7 @@ extension ChatControllerImpl {
|
||||
return
|
||||
}
|
||||
|
||||
let purchaseScreen = strongSelf.context.sharedContext.makeStarsPurchaseScreen(context: strongSelf.context, starsContext: starsContext, options: options, purpose: .reactions(peerId: message.id.peerId, requiredStars: 1), targetPeerId: nil, completion: { result in
|
||||
let purchaseScreen = strongSelf.context.sharedContext.makeStarsPurchaseScreen(context: strongSelf.context, starsContext: starsContext, options: options, purpose: .reactions(peerId: message.id.peerId, requiredStars: 1), targetPeerId: nil, customTheme: nil, completion: { result in
|
||||
let _ = result
|
||||
})
|
||||
strongSelf.push(purchaseScreen)
|
||||
|
||||
@ -76,7 +76,7 @@ extension ChatControllerImpl {
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
let controller = self.context.sharedContext.makeStarsPurchaseScreen(context: self.context, starsContext: starsContext, options: options, purpose: .sendMessage(peerId: peer.id, requiredStars: totalAmount), targetPeerId: nil, completion: { stars in
|
||||
let controller = self.context.sharedContext.makeStarsPurchaseScreen(context: self.context, starsContext: starsContext, options: options, purpose: .sendMessage(peerId: peer.id, requiredStars: totalAmount), targetPeerId: nil, customTheme: nil, completion: { stars in
|
||||
starsContext.add(balance: StarsAmount(value: stars, nanos: 0))
|
||||
let _ = (starsContext.onUpdate
|
||||
|> deliverOnMainQueue).start(next: {
|
||||
|
||||
@ -1840,7 +1840,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
|
||||
return
|
||||
}
|
||||
|
||||
let purchaseScreen = strongSelf.context.sharedContext.makeStarsPurchaseScreen(context: strongSelf.context, starsContext: starsContext, options: options, purpose: .reactions(peerId: peerId, requiredStars: 1), targetPeerId: nil, completion: { result in
|
||||
let purchaseScreen = strongSelf.context.sharedContext.makeStarsPurchaseScreen(context: strongSelf.context, starsContext: starsContext, options: options, purpose: .reactions(peerId: peerId, requiredStars: 1), targetPeerId: nil, customTheme: nil, completion: { result in
|
||||
let _ = result
|
||||
})
|
||||
strongSelf.push(purchaseScreen)
|
||||
@ -2470,7 +2470,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
|
||||
guard let strongSelf else {
|
||||
return
|
||||
}
|
||||
let purchaseController = strongSelf.context.sharedContext.makeStarsPurchaseScreen(context: strongSelf.context, starsContext: starsContext, options: options, purpose: .generic, targetPeerId: nil, completion: { _ in
|
||||
let purchaseController = strongSelf.context.sharedContext.makeStarsPurchaseScreen(context: strongSelf.context, starsContext: starsContext, options: options, purpose: .generic, targetPeerId: nil, customTheme: nil, completion: { _ in
|
||||
})
|
||||
strongSelf.push(purchaseController)
|
||||
})
|
||||
|
||||
@ -385,7 +385,7 @@ extension ChatControllerImpl {
|
||||
}
|
||||
|
||||
let reactionsAttribute = mergedMessageReactions(attributes: message.attributes, isTags: false)
|
||||
let _ = (ChatSendStarsScreen.initialData(context: self.context, peerId: message.id.peerId, messageId: message.id, topPeers: reactionsAttribute?.topPeers ?? [], completion: { [weak self] amount, privacy, isBecomingTop, transitionOut in
|
||||
let _ = (ChatSendStarsScreen.initialData(context: self.context, peerId: message.id.peerId, reactSubject: .message(message.id), topPeers: reactionsAttribute?.topPeers ?? [], completion: { [weak self] amount, privacy, isBecomingTop, transitionOut in
|
||||
guard let self, amount > 0 else {
|
||||
return
|
||||
}
|
||||
|
||||
@ -844,7 +844,7 @@ func openResolvedUrlImpl(
|
||||
dismissInput()
|
||||
if let starsContext = context.starsContext {
|
||||
let proceed = {
|
||||
let controller = context.sharedContext.makeStarsPurchaseScreen(context: context, starsContext: starsContext, options: [], purpose: .topUp(requiredStars: amount, purpose: purpose), targetPeerId: nil, completion: { _ in })
|
||||
let controller = context.sharedContext.makeStarsPurchaseScreen(context: context, starsContext: starsContext, options: [], purpose: .topUp(requiredStars: amount, purpose: purpose), targetPeerId: nil, customTheme: nil, completion: { _ in })
|
||||
if let navigationController = navigationController {
|
||||
navigationController.pushViewController(controller, animated: true)
|
||||
}
|
||||
|
||||
@ -3295,6 +3295,7 @@ public final class SharedAccountContextImpl: SharedAccountContext {
|
||||
options: options ?? [],
|
||||
purpose: .transferStarGift(requiredStars: transferStars),
|
||||
targetPeerId: nil,
|
||||
customTheme: nil,
|
||||
completion: { stars in
|
||||
starsContext.add(balance: StarsAmount(value: stars, nanos: 0))
|
||||
proceed(true)
|
||||
@ -3701,8 +3702,8 @@ public final class SharedAccountContextImpl: SharedAccountContext {
|
||||
return StarsTransactionsScreen(context: context, starsContext: starsContext)
|
||||
}
|
||||
|
||||
public func makeStarsPurchaseScreen(context: AccountContext, starsContext: StarsContext, options: [Any], purpose: StarsPurchasePurpose, targetPeerId: EnginePeer.Id?, completion: @escaping (Int64) -> Void) -> ViewController {
|
||||
return StarsPurchaseScreen(context: context, starsContext: starsContext, options: options, purpose: purpose, targetPeerId: targetPeerId, completion: completion)
|
||||
public func makeStarsPurchaseScreen(context: AccountContext, starsContext: StarsContext, options: [Any], purpose: StarsPurchasePurpose, targetPeerId: EnginePeer.Id?, customTheme: PresentationTheme?, completion: @escaping (Int64) -> Void) -> ViewController {
|
||||
return StarsPurchaseScreen(context: context, starsContext: starsContext, options: options, purpose: purpose, targetPeerId: targetPeerId, customTheme: customTheme, completion: completion)
|
||||
}
|
||||
|
||||
public func makeStarsTransferScreen(context: AccountContext, starsContext: StarsContext, invoice: TelegramMediaInvoice, source: BotPaymentInvoiceSource, extendedMedia: [TelegramExtendedMedia], inputData: Signal<(StarsContext.State, BotPaymentForm, EnginePeer?, EnginePeer?)?, NoError>, completion: @escaping (Bool) -> Void) -> ViewController {
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user