Story updates

This commit is contained in:
Isaac 2025-10-21 18:28:45 +04:00
parent bcc1213db2
commit e44bd4d858
60 changed files with 1838 additions and 912 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1210,6 +1210,7 @@ final class VideoChatScreenComponent: Component {
defaultParticipantMuteState: nil,
messagesAreEnabled: true,
canEnableMessages: false,
sendPaidMessageStars: nil,
recordingStartTimestamp: nil,
title: nil,
raisedHand: false,

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -440,6 +440,7 @@ public class ChatMessagePaymentAlertController: AlertController {
options: options,
purpose: .generic,
targetPeerId: nil,
customTheme: nil,
completion: { _ in }
)
navigationController.pushViewController(controller)

View File

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

View File

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

View File

@ -26,6 +26,7 @@ swift_library(
"//submodules/ComponentFlow",
"//submodules/AnimatedCountLabelNode",
"//submodules/TelegramUI/Components/GlassBackgroundComponent",
"//submodules/TelegramUI/Components/StarsParticleEffect",
],
visibility = [
"//visibility:public",

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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, _, _):

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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