From e44bd4d8582678c663683ed35189efa72d4fe19a Mon Sep 17 00:00:00 2001 From: Isaac <> Date: Tue, 21 Oct 2025 18:28:45 +0400 Subject: [PATCH] Story updates --- .../Sources/AccountContext.swift | 2 +- .../Sources/PresentationCallManager.swift | 3 + .../Sources/ChatListController.swift | 2 +- .../MultilineTextWithEntitiesComponent.swift | 23 +- .../Display/Source/ContainerViewLayout.swift | 5 + submodules/TelegramApi/Sources/Api0.swift | 8 +- submodules/TelegramApi/Sources/Api12.swift | 12 + submodules/TelegramApi/Sources/Api16.swift | 22 +- submodules/TelegramApi/Sources/Api20.swift | 12 + submodules/TelegramApi/Sources/Api27.swift | 12 +- submodules/TelegramApi/Sources/Api39.swift | 8 +- submodules/TelegramApi/Sources/Api7.swift | 18 +- .../Sources/PresentationGroupCall.swift | 33 +- .../Sources/VideoChatScreen.swift | 1 + .../ApiUtils/StoreMessage_Telegram.swift | 10 +- .../Sources/ApiUtils/TelegramMediaTodo.swift | 2 +- .../State/AccountStateManagementUtils.swift | 6 +- .../SyncCore_TelegramMediaLiveStream.swift | 14 +- .../TelegramEngine/Calls/GroupCalls.swift | 49 ++- .../TelegramEngine/Messages/Stories.swift | 138 +++++- .../Messages/TelegramEngineMessages.swift | 4 + .../ChatMessagePaymentAlertController.swift | 1 + .../Components/Chat/ChatSendStarsScreen/BUILD | 1 + .../Sources/ChatSendStarsScreen.swift | 300 +++++++++---- .../Chat/ChatTextInputActionButtonsNode/BUILD | 1 + .../ChatTextInputActionButtonsNode.swift | 92 +--- .../Chat/ChatTextInputPanelNode/BUILD | 1 + .../Sources/ChatTextInputPanelComponent.swift | 47 +- .../Sources/ChatTextInputPanelNode.swift | 323 +++++++++----- .../Sources/StarReactionButtonComponent.swift | 118 ++++- .../Sources/GiftOptionsScreen.swift | 1 + .../Sources/GiftSetupScreen.swift | 3 +- .../Sources/GiftViewScreen.swift | 5 + .../MessageInputPanelComponent/BUILD | 1 + .../Sources/MessageInputPanelComponent.swift | 259 +++++------ .../Sources/UserApperanceScreen.swift | 1 + .../Sources/StarsPurchaseScreen.swift | 3 +- .../Sources/StarsStatisticsScreen.swift | 2 +- .../Sources/StarsTransactionsScreen.swift | 3 +- .../Sources/StarsTransferScreen.swift | 1 + .../Sources/StarsWithdrawalScreen.swift | 2 +- .../Components/StarsParticleEffect/BUILD | 19 + .../Sources/ActionPanelComponent.swift | 71 +++ .../StoryLiveChatMessageComponent/BUILD | 28 ++ .../StoryLiveChatMessageComponent.swift | 410 ++++++++++++++++++ .../Stories/StoryContainerScreen/BUILD | 2 + .../Sources/StoryAuthorInfoComponent.swift | 12 +- .../Sources/StoryContainerScreen.swift | 11 +- .../Sources/StoryContent.swift | 3 + .../StoryContentLiveChatComponent.swift | 370 +++------------- .../Sources/StoryItemContentComponent.swift | 53 ++- .../StoryItemSetContainerComponent.swift | 64 ++- ...StoryItemSetContainerViewSendMessage.swift | 139 ++++-- .../Chat/ChatControllerLoadDisplayNode.swift | 2 +- ...ChatControllerOpenMessageContextMenu.swift | 2 +- .../Chat/ChatControllerPaidMessage.swift | 2 +- .../TelegramUI/Sources/ChatController.swift | 4 +- ...rollerOpenMessageReactionContextMenu.swift | 2 +- .../TelegramUI/Sources/OpenResolvedUrl.swift | 2 +- .../Sources/SharedAccountContext.swift | 5 +- 60 files changed, 1838 insertions(+), 912 deletions(-) create mode 100644 submodules/TelegramUI/Components/StarsParticleEffect/BUILD create mode 100644 submodules/TelegramUI/Components/StarsParticleEffect/Sources/ActionPanelComponent.swift create mode 100644 submodules/TelegramUI/Components/Stories/LiveChat/StoryLiveChatMessageComponent/BUILD create mode 100644 submodules/TelegramUI/Components/Stories/LiveChat/StoryLiveChatMessageComponent/Sources/StoryLiveChatMessageComponent.swift diff --git a/submodules/AccountContext/Sources/AccountContext.swift b/submodules/AccountContext/Sources/AccountContext.swift index 5cdfb1c534..70e8134209 100644 --- a/submodules/AccountContext/Sources/AccountContext.swift +++ b/submodules/AccountContext/Sources/AccountContext.swift @@ -1356,7 +1356,7 @@ public protocol SharedAccountContext: AnyObject { func makeStoryStatsController(context: AccountContext, updatedPresentationData: (initial: PresentationData, signal: Signal)?, 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 diff --git a/submodules/AccountContext/Sources/PresentationCallManager.swift b/submodules/AccountContext/Sources/PresentationCallManager.swift index 52e22072dd..5160053e8c 100644 --- a/submodules/AccountContext/Sources/PresentationCallManager.swift +++ b/submodules/AccountContext/Sources/PresentationCallManager.swift @@ -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 diff --git a/submodules/ChatListUI/Sources/ChatListController.swift b/submodules/ChatListUI/Sources/ChatListController.swift index 8c0c2d688b..fc64fc8a76 100644 --- a/submodules/ChatListUI/Sources/ChatListController.swift +++ b/submodules/ChatListUI/Sources/ChatListController.swift @@ -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) } diff --git a/submodules/Components/MultilineTextWithEntitiesComponent/Sources/MultilineTextWithEntitiesComponent.swift b/submodules/Components/MultilineTextWithEntitiesComponent/Sources/MultilineTextWithEntitiesComponent.swift index b0f0820346..7766272b59 100644 --- a/submodules/Components/MultilineTextWithEntitiesComponent/Sources/MultilineTextWithEntitiesComponent.swift +++ b/submodules/Components/MultilineTextWithEntitiesComponent/Sources/MultilineTextWithEntitiesComponent.swift @@ -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 } } diff --git a/submodules/Display/Source/ContainerViewLayout.swift b/submodules/Display/Source/ContainerViewLayout.swift index bfd209491b..a986149189 100644 --- a/submodules/Display/Source/ContainerViewLayout.swift +++ b/submodules/Display/Source/ContainerViewLayout.swift @@ -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) + } } diff --git a/submodules/TelegramApi/Sources/Api0.swift b/submodules/TelegramApi/Sources/Api0.swift index 413a551a6a..42c327a625 100644 --- a/submodules/TelegramApi/Sources/Api0.swift +++ b/submodules/TelegramApi/Sources/Api0.swift @@ -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) } diff --git a/submodules/TelegramApi/Sources/Api12.swift b/submodules/TelegramApi/Sources/Api12.swift index f7b650e72a..ae88c1490f 100644 --- a/submodules/TelegramApi/Sources/Api12.swift +++ b/submodules/TelegramApi/Sources/Api12.swift @@ -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 } diff --git a/submodules/TelegramApi/Sources/Api16.swift b/submodules/TelegramApi/Sources/Api16.swift index 5c44d32f1e..90af1af4c3 100644 --- a/submodules/TelegramApi/Sources/Api16.swift +++ b/submodules/TelegramApi/Sources/Api16.swift @@ -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 diff --git a/submodules/TelegramApi/Sources/Api20.swift b/submodules/TelegramApi/Sources/Api20.swift index 906318b8be..27e9c3383d 100644 --- a/submodules/TelegramApi/Sources/Api20.swift +++ b/submodules/TelegramApi/Sources/Api20.swift @@ -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 } diff --git a/submodules/TelegramApi/Sources/Api27.swift b/submodules/TelegramApi/Sources/Api27.swift index 118f363b0a..4c93a987d1 100644 --- a/submodules/TelegramApi/Sources/Api27.swift +++ b/submodules/TelegramApi/Sources/Api27.swift @@ -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 diff --git a/submodules/TelegramApi/Sources/Api39.swift b/submodules/TelegramApi/Sources/Api39.swift index bd7057c836..e8f71d64d0 100644 --- a/submodules/TelegramApi/Sources/Api39.swift +++ b/submodules/TelegramApi/Sources/Api39.swift @@ -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) { + 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) { 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() { diff --git a/submodules/TelegramApi/Sources/Api7.swift b/submodules/TelegramApi/Sources/Api7.swift index 9d77c664e9..7cc51fa876 100644 --- a/submodules/TelegramApi/Sources/Api7.swift +++ b/submodules/TelegramApi/Sources/Api7.swift @@ -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 diff --git a/submodules/TelegramCallsUI/Sources/PresentationGroupCall.swift b/submodules/TelegramCallsUI/Sources/PresentationGroupCall.swift index bd0c4fcffc..2de619c870 100644 --- a/submodules/TelegramCallsUI/Sources/PresentationGroupCall.swift +++ b/submodules/TelegramCallsUI/Sources/PresentationGroupCall.swift @@ -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, diff --git a/submodules/TelegramCallsUI/Sources/VideoChatScreen.swift b/submodules/TelegramCallsUI/Sources/VideoChatScreen.swift index 366ab0071b..3d37f0610e 100644 --- a/submodules/TelegramCallsUI/Sources/VideoChatScreen.swift +++ b/submodules/TelegramCallsUI/Sources/VideoChatScreen.swift @@ -1210,6 +1210,7 @@ final class VideoChatScreenComponent: Component { defaultParticipantMuteState: nil, messagesAreEnabled: true, canEnableMessages: false, + sendPaidMessageStars: nil, recordingStartTimestamp: nil, title: nil, raisedHand: false, diff --git a/submodules/TelegramCore/Sources/ApiUtils/StoreMessage_Telegram.swift b/submodules/TelegramCore/Sources/ApiUtils/StoreMessage_Telegram.swift index 465d32ad9e..73d733be1a 100644 --- a/submodules/TelegramCore/Sources/ApiUtils/StoreMessage_Telegram.swift +++ b/submodules/TelegramCore/Sources/ApiUtils/StoreMessage_Telegram.swift @@ -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) } } } diff --git a/submodules/TelegramCore/Sources/ApiUtils/TelegramMediaTodo.swift b/submodules/TelegramCore/Sources/ApiUtils/TelegramMediaTodo.swift index d98d35e499..073b9697c0 100644 --- a/submodules/TelegramCore/Sources/ApiUtils/TelegramMediaTodo.swift +++ b/submodules/TelegramCore/Sources/ApiUtils/TelegramMediaTodo.swift @@ -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) } } } diff --git a/submodules/TelegramCore/Sources/State/AccountStateManagementUtils.swift b/submodules/TelegramCore/Sources/State/AccountStateManagementUtils.swift index c1e43789ae..04e469776d 100644 --- a/submodules/TelegramCore/Sources/State/AccountStateManagementUtils.swift +++ b/submodules/TelegramCore/Sources/State/AccountStateManagementUtils.swift @@ -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 { diff --git a/submodules/TelegramCore/Sources/SyncCore/SyncCore_TelegramMediaLiveStream.swift b/submodules/TelegramCore/Sources/SyncCore/SyncCore_TelegramMediaLiveStream.swift index 8862df8abc..9cc33433dd 100644 --- a/submodules/TelegramCore/Sources/SyncCore/SyncCore_TelegramMediaLiveStream.swift +++ b/submodules/TelegramCore/Sources/SyncCore/SyncCore_TelegramMediaLiveStream.swift @@ -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 } diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Calls/GroupCalls.swift b/submodules/TelegramCore/Sources/TelegramEngine/Calls/GroupCalls.swift index 27fb6ea04f..b92a86fbaf 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Calls/GroupCalls.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Calls/GroupCalls.swift @@ -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) } } diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Messages/Stories.swift b/submodules/TelegramCore/Sources/TelegramEngine/Messages/Stories.swift index a7109eb345..d3f96f7ac9 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Messages/Stories.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Messages/Stories.swift @@ -1115,7 +1115,8 @@ func _internal_cancelStoryUpload(account: Account, stableId: Int32) { func _internal_beginStoryLivestream(account: Account) -> Signal { 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 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 { + 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 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 in + return .single(nil) + } + |> mapToSignal { updates -> Signal in + if let updates = updates { + account.stateManager.addUpdates(updates) + } + + return .complete() + }*/ + } +} diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Messages/TelegramEngineMessages.swift b/submodules/TelegramCore/Sources/TelegramEngine/Messages/TelegramEngineMessages.swift index c4618dfed6..93d14bae12 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Messages/TelegramEngineMessages.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Messages/TelegramEngineMessages.swift @@ -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 { + return _internal_sendStoryStars(account: self.account, peerId: peerId, id: id, count: count) + } + public func getStory(peerId: EnginePeer.Id, id: Int32) -> Signal { return _internal_getStoryById(accountPeerId: self.account.peerId, postbox: self.account.postbox, network: self.account.network, peerId: peerId, id: id) } diff --git a/submodules/TelegramUI/Components/Chat/ChatMessagePaymentAlertController/Sources/ChatMessagePaymentAlertController.swift b/submodules/TelegramUI/Components/Chat/ChatMessagePaymentAlertController/Sources/ChatMessagePaymentAlertController.swift index 93134eede7..c7ccefad81 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessagePaymentAlertController/Sources/ChatMessagePaymentAlertController.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessagePaymentAlertController/Sources/ChatMessagePaymentAlertController.swift @@ -440,6 +440,7 @@ public class ChatMessagePaymentAlertController: AlertController { options: options, purpose: .generic, targetPeerId: nil, + customTheme: nil, completion: { _ in } ) navigationController.pushViewController(controller) diff --git a/submodules/TelegramUI/Components/Chat/ChatSendStarsScreen/BUILD b/submodules/TelegramUI/Components/Chat/ChatSendStarsScreen/BUILD index 8a6876b050..8c7b636dac 100644 --- a/submodules/TelegramUI/Components/Chat/ChatSendStarsScreen/BUILD +++ b/submodules/TelegramUI/Components/Chat/ChatSendStarsScreen/BUILD @@ -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", diff --git a/submodules/TelegramUI/Components/Chat/ChatSendStarsScreen/Sources/ChatSendStarsScreen.swift b/submodules/TelegramUI/Components/Chat/ChatSendStarsScreen/Sources/ChatSendStarsScreen.swift index 329f9b9ca9..859ebd9511 100644 --- a/submodules/TelegramUI/Components/Chat/ChatSendStarsScreen/Sources/ChatSendStarsScreen.swift +++ b/submodules/TelegramUI/Components/Chat/ChatSendStarsScreen/Sources/ChatSendStarsScreen.swift @@ -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() private var liveStreamPerks: [ComponentView] = [] + private var liveStreamMessagePreview: ComponentView? 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 + 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 { + public static func initialData(context: AccountContext, peerId: EnginePeer.Id, reactSubject: ReactSubject, topPeers: [ReactionsMessageAttribute.TopPeer], completion: @escaping (Int64, TelegramPaidReactionPrivacy, Bool, TransitionOut) -> Void) -> Signal { let balance: Signal 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 { + public static func initialDataLiveStreamMessage( + context: AccountContext, + peerId: EnginePeer.Id, + text: NSAttributedString, + completion: @escaping (Int64, TransitionOut) -> Void + ) -> Signal { let balance: Signal 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) -} diff --git a/submodules/TelegramUI/Components/Chat/ChatTextInputActionButtonsNode/BUILD b/submodules/TelegramUI/Components/Chat/ChatTextInputActionButtonsNode/BUILD index 0d85c5c138..dccdb30a07 100644 --- a/submodules/TelegramUI/Components/Chat/ChatTextInputActionButtonsNode/BUILD +++ b/submodules/TelegramUI/Components/Chat/ChatTextInputActionButtonsNode/BUILD @@ -26,6 +26,7 @@ swift_library( "//submodules/ComponentFlow", "//submodules/AnimatedCountLabelNode", "//submodules/TelegramUI/Components/GlassBackgroundComponent", + "//submodules/TelegramUI/Components/StarsParticleEffect", ], visibility = [ "//visibility:public", diff --git a/submodules/TelegramUI/Components/Chat/ChatTextInputActionButtonsNode/Sources/ChatTextInputActionButtonsNode.swift b/submodules/TelegramUI/Components/Chat/ChatTextInputActionButtonsNode/Sources/ChatTextInputActionButtonsNode.swift index ae56e2fe18..a4e00c92e0 100644 --- a/submodules/TelegramUI/Components/Chat/ChatTextInputActionButtonsNode/Sources/ChatTextInputActionButtonsNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatTextInputActionButtonsNode/Sources/ChatTextInputActionButtonsNode.swift @@ -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)) diff --git a/submodules/TelegramUI/Components/Chat/ChatTextInputPanelNode/BUILD b/submodules/TelegramUI/Components/Chat/ChatTextInputPanelNode/BUILD index 7a7bb1e704..bc3258857c 100644 --- a/submodules/TelegramUI/Components/Chat/ChatTextInputPanelNode/BUILD +++ b/submodules/TelegramUI/Components/Chat/ChatTextInputPanelNode/BUILD @@ -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", diff --git a/submodules/TelegramUI/Components/Chat/ChatTextInputPanelNode/Sources/ChatTextInputPanelComponent.swift b/submodules/TelegramUI/Components/Chat/ChatTextInputPanelNode/Sources/ChatTextInputPanelComponent.swift index 102ddc9fba..31797ea02e 100644 --- a/submodules/TelegramUI/Components/Chat/ChatTextInputPanelNode/Sources/ChatTextInputPanelComponent.swift +++ b/submodules/TelegramUI/Components/Chat/ChatTextInputPanelNode/Sources/ChatTextInputPanelComponent.swift @@ -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 diff --git a/submodules/TelegramUI/Components/Chat/ChatTextInputPanelNode/Sources/ChatTextInputPanelNode.swift b/submodules/TelegramUI/Components/Chat/ChatTextInputPanelNode/Sources/ChatTextInputPanelNode.swift index 8938b1cc3e..4f15ef83a9 100644 --- a/submodules/TelegramUI/Components/Chat/ChatTextInputPanelNode/Sources/ChatTextInputPanelNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatTextInputPanelNode/Sources/ChatTextInputPanelNode.swift @@ -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? @@ -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 + 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 - 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() diff --git a/submodules/TelegramUI/Components/Chat/ChatTextInputPanelNode/Sources/StarReactionButtonComponent.swift b/submodules/TelegramUI/Components/Chat/ChatTextInputPanelNode/Sources/StarReactionButtonComponent.swift index fe4c605608..bc957b05dd 100644 --- a/submodules/TelegramUI/Components/Chat/ChatTextInputPanelNode/Sources/StarReactionButtonComponent.swift +++ b/submodules/TelegramUI/Components/Chat/ChatTextInputPanelNode/Sources/StarReactionButtonComponent.swift @@ -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? + + 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, 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 + 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 diff --git a/submodules/TelegramUI/Components/Gifts/GiftOptionsScreen/Sources/GiftOptionsScreen.swift b/submodules/TelegramUI/Components/Gifts/GiftOptionsScreen/Sources/GiftOptionsScreen.swift index ebbd50f265..33a929b71c 100644 --- a/submodules/TelegramUI/Components/Gifts/GiftOptionsScreen/Sources/GiftOptionsScreen.swift +++ b/submodules/TelegramUI/Components/Gifts/GiftOptionsScreen/Sources/GiftOptionsScreen.swift @@ -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) diff --git a/submodules/TelegramUI/Components/Gifts/GiftSetupScreen/Sources/GiftSetupScreen.swift b/submodules/TelegramUI/Components/Gifts/GiftSetupScreen/Sources/GiftSetupScreen.swift index 82f9a77534..6c14a4a8fd 100644 --- a/submodules/TelegramUI/Components/Gifts/GiftSetupScreen/Sources/GiftSetupScreen.swift +++ b/submodules/TelegramUI/Components/Gifts/GiftSetupScreen/Sources/GiftSetupScreen.swift @@ -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) diff --git a/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftViewScreen.swift b/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftViewScreen.swift index a00a28974c..7e04758d60 100644 --- a/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftViewScreen.swift +++ b/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftViewScreen.swift @@ -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) diff --git a/submodules/TelegramUI/Components/MessageInputPanelComponent/BUILD b/submodules/TelegramUI/Components/MessageInputPanelComponent/BUILD index cf37d6ad06..612caadaed 100644 --- a/submodules/TelegramUI/Components/MessageInputPanelComponent/BUILD +++ b/submodules/TelegramUI/Components/MessageInputPanelComponent/BUILD @@ -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", diff --git a/submodules/TelegramUI/Components/MessageInputPanelComponent/Sources/MessageInputPanelComponent.swift b/submodules/TelegramUI/Components/MessageInputPanelComponent/Sources/MessageInputPanelComponent.swift index 7ccc7743ff..ae03aed7f6 100644 --- a/submodules/TelegramUI/Components/MessageInputPanelComponent/Sources/MessageInputPanelComponent.swift +++ b/submodules/TelegramUI/Components/MessageInputPanelComponent/Sources/MessageInputPanelComponent.swift @@ -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>() + 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 - 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( diff --git a/submodules/TelegramUI/Components/Settings/PeerNameColorScreen/Sources/UserApperanceScreen.swift b/submodules/TelegramUI/Components/Settings/PeerNameColorScreen/Sources/UserApperanceScreen.swift index dbc72b0f82..20d3361b50 100644 --- a/submodules/TelegramUI/Components/Settings/PeerNameColorScreen/Sources/UserApperanceScreen.swift +++ b/submodules/TelegramUI/Components/Settings/PeerNameColorScreen/Sources/UserApperanceScreen.swift @@ -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 diff --git a/submodules/TelegramUI/Components/Stars/StarsPurchaseScreen/Sources/StarsPurchaseScreen.swift b/submodules/TelegramUI/Components/Stars/StarsPurchaseScreen/Sources/StarsPurchaseScreen.swift index 92e404b74f..1cceec9923 100644 --- a/submodules/TelegramUI/Components/Stars/StarsPurchaseScreen/Sources/StarsPurchaseScreen.swift +++ b/submodules/TelegramUI/Components/Stars/StarsPurchaseScreen/Sources/StarsPurchaseScreen.swift @@ -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 } diff --git a/submodules/TelegramUI/Components/Stars/StarsTransactionsScreen/Sources/StarsStatisticsScreen.swift b/submodules/TelegramUI/Components/Stars/StarsTransactionsScreen/Sources/StarsStatisticsScreen.swift index 9b7279aec3..fd1770e759 100644 --- a/submodules/TelegramUI/Components/Stars/StarsTransactionsScreen/Sources/StarsStatisticsScreen.swift +++ b/submodules/TelegramUI/Components/Stars/StarsTransactionsScreen/Sources/StarsStatisticsScreen.swift @@ -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 } diff --git a/submodules/TelegramUI/Components/Stars/StarsTransactionsScreen/Sources/StarsTransactionsScreen.swift b/submodules/TelegramUI/Components/Stars/StarsTransactionsScreen/Sources/StarsTransactionsScreen.swift index 1358a3341b..421e7f0f50 100644 --- a/submodules/TelegramUI/Components/Stars/StarsTransactionsScreen/Sources/StarsTransactionsScreen.swift +++ b/submodules/TelegramUI/Components/Stars/StarsTransactionsScreen/Sources/StarsTransactionsScreen.swift @@ -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 diff --git a/submodules/TelegramUI/Components/Stars/StarsTransferScreen/Sources/StarsTransferScreen.swift b/submodules/TelegramUI/Components/Stars/StarsTransferScreen/Sources/StarsTransferScreen.swift index 48f3109be5..2659a9cbde 100644 --- a/submodules/TelegramUI/Components/Stars/StarsTransferScreen/Sources/StarsTransferScreen.swift +++ b/submodules/TelegramUI/Components/Stars/StarsTransferScreen/Sources/StarsTransferScreen.swift @@ -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 diff --git a/submodules/TelegramUI/Components/Stars/StarsWithdrawalScreen/Sources/StarsWithdrawalScreen.swift b/submodules/TelegramUI/Components/Stars/StarsWithdrawalScreen/Sources/StarsWithdrawalScreen.swift index 0faf4a9d5e..df8957dc2b 100644 --- a/submodules/TelegramUI/Components/Stars/StarsWithdrawalScreen/Sources/StarsWithdrawalScreen.swift +++ b/submodules/TelegramUI/Components/Stars/StarsWithdrawalScreen/Sources/StarsWithdrawalScreen.swift @@ -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) }) diff --git a/submodules/TelegramUI/Components/StarsParticleEffect/BUILD b/submodules/TelegramUI/Components/StarsParticleEffect/BUILD new file mode 100644 index 0000000000..3d324c9c19 --- /dev/null +++ b/submodules/TelegramUI/Components/StarsParticleEffect/BUILD @@ -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", + ], +) diff --git a/submodules/TelegramUI/Components/StarsParticleEffect/Sources/ActionPanelComponent.swift b/submodules/TelegramUI/Components/StarsParticleEffect/Sources/ActionPanelComponent.swift new file mode 100644 index 0000000000..1d211b19f8 --- /dev/null +++ b/submodules/TelegramUI/Components/StarsParticleEffect/Sources/ActionPanelComponent.swift @@ -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) + } +} diff --git a/submodules/TelegramUI/Components/Stories/LiveChat/StoryLiveChatMessageComponent/BUILD b/submodules/TelegramUI/Components/Stories/LiveChat/StoryLiveChatMessageComponent/BUILD new file mode 100644 index 0000000000..7838c36532 --- /dev/null +++ b/submodules/TelegramUI/Components/Stories/LiveChat/StoryLiveChatMessageComponent/BUILD @@ -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", + ], +) diff --git a/submodules/TelegramUI/Components/Stories/LiveChat/StoryLiveChatMessageComponent/Sources/StoryLiveChatMessageComponent.swift b/submodules/TelegramUI/Components/Stories/LiveChat/StoryLiveChatMessageComponent/Sources/StoryLiveChatMessageComponent.swift new file mode 100644 index 0000000000..6c9442163e --- /dev/null +++ b/submodules/TelegramUI/Components/Stories/LiveChat/StoryLiveChatMessageComponent/Sources/StoryLiveChatMessageComponent.swift @@ -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() + private var backgroundView: UIImageView? + private var effectLayer: StarsParticleEffectLayer? + private var starsAmountBackgroundView: UIImageView? + private var starsAmountIcon: UIImageView? + private var starsAmountText: ComponentView? + + 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, 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 + 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, 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) + } + } +} diff --git a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/BUILD b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/BUILD index 8169bf3697..2eaca42c2f 100644 --- a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/BUILD +++ b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/BUILD @@ -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", diff --git a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryAuthorInfoComponent.swift b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryAuthorInfoComponent.swift index 411635183b..f13ae01f7d 100644 --- a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryAuthorInfoComponent.swift +++ b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryAuthorInfoComponent.swift @@ -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, _, _): diff --git a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryContainerScreen.swift b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryContainerScreen.swift index 1b7327eb71..a30e67b74a 100644 --- a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryContainerScreen.swift +++ b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryContainerScreen.swift @@ -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) } } } diff --git a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryContent.swift b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryContent.swift index a86c22c1d8..1b1bffb73c 100644 --- a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryContent.swift +++ b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryContent.swift @@ -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 } diff --git a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryContentLiveChatComponent.swift b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryContentLiveChatComponent.swift index 1dce5cd63c..0edb51d73c 100644 --- a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryContentLiveChatComponent.swift +++ b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryContentLiveChatComponent.swift @@ -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() - 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, 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, 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() @@ -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] = [] @@ -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 diff --git a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemContentComponent.swift b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemContentComponent.swift index c2011f550b..d47f666e01 100644 --- a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemContentComponent.swift +++ b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemContentComponent.swift @@ -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? private var loadingEffectView: StoryItemLoadingEffectView? private var loadingEffectAppearanceTimer: SwiftSignalKit.Timer? + private let liveChatExternal = StoryContentLiveChatComponent.External() private var liveChat: ComponentView? 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 { diff --git a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerComponent.swift b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerComponent.swift index 67166f8fee..74fdb9f34b 100644 --- a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerComponent.swift +++ b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerComponent.swift @@ -323,6 +323,7 @@ public final class StoryItemSetContainerComponent: Component { let view = ComponentView() var currentProgress: Double = 0.0 var isBuffering: Bool = false + var customSubtitle: String? var requestedNext: Bool = false var footerPanel: ComponentView? @@ -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 diff --git a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerViewSendMessage.swift b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerViewSendMessage.swift index e317e3d3ad..fea168d6e8 100644 --- a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerViewSendMessage.swift +++ b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerViewSendMessage.swift @@ -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 { diff --git a/submodules/TelegramUI/Sources/Chat/ChatControllerLoadDisplayNode.swift b/submodules/TelegramUI/Sources/Chat/ChatControllerLoadDisplayNode.swift index 02a167eacf..be52c49e1d 100644 --- a/submodules/TelegramUI/Sources/Chat/ChatControllerLoadDisplayNode.swift +++ b/submodules/TelegramUI/Sources/Chat/ChatControllerLoadDisplayNode.swift @@ -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) }) diff --git a/submodules/TelegramUI/Sources/Chat/ChatControllerOpenMessageContextMenu.swift b/submodules/TelegramUI/Sources/Chat/ChatControllerOpenMessageContextMenu.swift index da9d2e03f9..ff3181762d 100644 --- a/submodules/TelegramUI/Sources/Chat/ChatControllerOpenMessageContextMenu.swift +++ b/submodules/TelegramUI/Sources/Chat/ChatControllerOpenMessageContextMenu.swift @@ -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) diff --git a/submodules/TelegramUI/Sources/Chat/ChatControllerPaidMessage.swift b/submodules/TelegramUI/Sources/Chat/ChatControllerPaidMessage.swift index 33eafb721a..ab1b663e74 100644 --- a/submodules/TelegramUI/Sources/Chat/ChatControllerPaidMessage.swift +++ b/submodules/TelegramUI/Sources/Chat/ChatControllerPaidMessage.swift @@ -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: { diff --git a/submodules/TelegramUI/Sources/ChatController.swift b/submodules/TelegramUI/Sources/ChatController.swift index 56ad1b1f2f..5bb882de37 100644 --- a/submodules/TelegramUI/Sources/ChatController.swift +++ b/submodules/TelegramUI/Sources/ChatController.swift @@ -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) }) diff --git a/submodules/TelegramUI/Sources/ChatControllerOpenMessageReactionContextMenu.swift b/submodules/TelegramUI/Sources/ChatControllerOpenMessageReactionContextMenu.swift index 113a15b558..9640bc72eb 100644 --- a/submodules/TelegramUI/Sources/ChatControllerOpenMessageReactionContextMenu.swift +++ b/submodules/TelegramUI/Sources/ChatControllerOpenMessageReactionContextMenu.swift @@ -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 } diff --git a/submodules/TelegramUI/Sources/OpenResolvedUrl.swift b/submodules/TelegramUI/Sources/OpenResolvedUrl.swift index 26d515de15..d3babce5fc 100644 --- a/submodules/TelegramUI/Sources/OpenResolvedUrl.swift +++ b/submodules/TelegramUI/Sources/OpenResolvedUrl.swift @@ -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) } diff --git a/submodules/TelegramUI/Sources/SharedAccountContext.swift b/submodules/TelegramUI/Sources/SharedAccountContext.swift index dcebfd5c1d..8929412b4d 100644 --- a/submodules/TelegramUI/Sources/SharedAccountContext.swift +++ b/submodules/TelegramUI/Sources/SharedAccountContext.swift @@ -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 {