From 7e3abe798f953ce4d541f3a8d57f389b24d944f2 Mon Sep 17 00:00:00 2001 From: Isaac <> Date: Fri, 17 Oct 2025 23:51:58 +0800 Subject: [PATCH] Update --- ...tControllerExtractedPresentationNode.swift | 8 +- .../Sources/ContextSourceContainer.swift | 8 +- submodules/TelegramApi/Sources/Api0.swift | 3 +- submodules/TelegramApi/Sources/Api27.swift | 32 + submodules/TelegramApi/Sources/Api39.swift | 48 +- submodules/TelegramApi/Sources/Api7.swift | 26 +- .../Sources/PresentationGroupCall.swift | 8 +- .../Account/AccountIntermediateState.swift | 6 +- .../State/AccountStateManagementUtils.swift | 8 +- .../TelegramEngine/Calls/GroupCalls.swift | 144 ++- submodules/TelegramUI/BUILD | 1 + .../Components/Chat/ChatPanelsComponent/BUILD | 26 + .../Sources/ChatPanelsComponent.swift | 69 ++ .../Sources/ChatSendStarsScreen.swift | 419 +++++---- .../ChatTextInputActionButtonsNode.swift | 99 ++- .../Chat/ChatTextInputPanelNode/BUILD | 1 - .../Sources/ChatTextInputPanelComponent.swift | 817 ++++++++++++++++++ .../Sources/ChatTextInputPanelNode.swift | 187 +++- .../Sources/StarReactionButtonComponent.swift | 99 +++ .../Sources/GlassBackgroundComponent.swift | 2 - .../MessageInputPanelComponent/BUILD | 1 + .../Sources/MessageInputPanelComponent.swift | 201 ++++- .../StoryContentLiveChatComponent.swift | 314 ++++++- .../Sources/StoryItemContentComponent.swift | 19 + .../StoryItemSetContainerComponent.swift | 50 +- ...StoryItemSetContainerViewSendMessage.swift | 84 +- .../Chat/ChatControllerOpenWebApp.swift | 13 +- .../Sources/ChatControllerNode.swift | 66 +- .../ChatInterfaceTitlePanelNodes.swift | 42 - ...ChatTopicListTitleAccessoryPanelNode.swift | 160 ---- 30 files changed, 2378 insertions(+), 583 deletions(-) create mode 100644 submodules/TelegramUI/Components/Chat/ChatPanelsComponent/BUILD create mode 100644 submodules/TelegramUI/Components/Chat/ChatPanelsComponent/Sources/ChatPanelsComponent.swift create mode 100644 submodules/TelegramUI/Components/Chat/ChatTextInputPanelNode/Sources/ChatTextInputPanelComponent.swift create mode 100644 submodules/TelegramUI/Components/Chat/ChatTextInputPanelNode/Sources/StarReactionButtonComponent.swift delete mode 100644 submodules/TelegramUI/Sources/ChatTopicListTitleAccessoryPanelNode.swift diff --git a/submodules/ContextUI/Sources/ContextControllerExtractedPresentationNode.swift b/submodules/ContextUI/Sources/ContextControllerExtractedPresentationNode.swift index e4bb7bc615..0fda18d4a3 100644 --- a/submodules/ContextUI/Sources/ContextControllerExtractedPresentationNode.swift +++ b/submodules/ContextUI/Sources/ContextControllerExtractedPresentationNode.swift @@ -935,8 +935,12 @@ final class ContextControllerExtractedPresentationNode: ASDisplayNode, ContextCo switch self.source { case .location, .reference, .controller: actionsStackPresentation = .inline - case .extracted: - actionsStackPresentation = .modal + case let .extracted(extracted): + if extracted.blurBackground { + actionsStackPresentation = .modal + } else { + actionsStackPresentation = .inline + } } let additionalActionsSize = self.additionalActionsStackNode.update( diff --git a/submodules/ContextUI/Sources/ContextSourceContainer.swift b/submodules/ContextUI/Sources/ContextSourceContainer.swift index c5e5101c6c..5692c6e8de 100644 --- a/submodules/ContextUI/Sources/ContextSourceContainer.swift +++ b/submodules/ContextUI/Sources/ContextSourceContainer.swift @@ -625,11 +625,11 @@ final class ContextSourceContainer: ASDisplayNode { forceKeepBlur: false, transition: .immediate ) - case .extracted: + case let .extracted(extracted): self.backgroundNode.updateColor( - color: presentationData.theme.contextMenu.dimColor, - enableBlur: true, - forceKeepBlur: true, + color: extracted.blurBackground ? presentationData.theme.contextMenu.dimColor : .clear, + enableBlur: extracted.blurBackground, + forceKeepBlur: extracted.blurBackground, transition: .immediate ) case .controller: diff --git a/submodules/TelegramApi/Sources/Api0.swift b/submodules/TelegramApi/Sources/Api0.swift index 354c9f0b7b..413a551a6a 100644 --- a/submodules/TelegramApi/Sources/Api0.swift +++ b/submodules/TelegramApi/Sources/Api0.swift @@ -303,7 +303,7 @@ fileprivate let parsers: [Int32 : (BufferReader) -> Any?] = { dict[-29248689] = { return Api.GlobalPrivacySettings.parse_globalPrivacySettings($0) } dict[1429932961] = { return Api.GroupCall.parse_groupCall($0) } dict[2004925620] = { return Api.GroupCall.parse_groupCallDiscarded($0) } - dict[-2018173984] = { return Api.GroupCallMessage.parse_groupCallMessage($0) } + dict[445316222] = { return Api.GroupCallMessage.parse_groupCallMessage($0) } dict[708691884] = { return Api.GroupCallParticipant.parse_groupCallParticipant($0) } dict[1735736008] = { return Api.GroupCallParticipantVideo.parse_groupCallParticipantVideo($0) } dict[-592373577] = { return Api.GroupCallParticipantVideoSourceGroup.parse_groupCallParticipantVideoSourceGroup($0) } @@ -1091,6 +1091,7 @@ fileprivate let parsers: [Int32 : (BufferReader) -> Any?] = { dict[1887741886] = { return Api.Update.parse_updateContactsReset($0) } dict[-1906403213] = { return Api.Update.parse_updateDcOptions($0) } dict[-1020437742] = { return Api.Update.parse_updateDeleteChannelMessages($0) } + dict[1048963372] = { return Api.Update.parse_updateDeleteGroupCallMessages($0) } dict[-1576161051] = { return Api.Update.parse_updateDeleteMessages($0) } dict[1407644140] = { return Api.Update.parse_updateDeleteQuickReply($0) } dict[1450174413] = { return Api.Update.parse_updateDeleteQuickReplyMessages($0) } diff --git a/submodules/TelegramApi/Sources/Api27.swift b/submodules/TelegramApi/Sources/Api27.swift index 17524c8ac0..118f363b0a 100644 --- a/submodules/TelegramApi/Sources/Api27.swift +++ b/submodules/TelegramApi/Sources/Api27.swift @@ -586,6 +586,7 @@ public extension Api { case updateContactsReset case updateDcOptions(dcOptions: [Api.DcOption]) case updateDeleteChannelMessages(channelId: Int64, messages: [Int32], pts: Int32, ptsCount: Int32) + case updateDeleteGroupCallMessages(call: Api.InputGroupCall, messages: [Int32]) case updateDeleteMessages(messages: [Int32], pts: Int32, ptsCount: Int32) case updateDeleteQuickReply(shortcutId: Int32) case updateDeleteQuickReplyMessages(shortcutId: Int32, messages: [Int32]) @@ -1113,6 +1114,17 @@ public extension Api { serializeInt32(pts, buffer: buffer, boxed: false) serializeInt32(ptsCount, buffer: buffer, boxed: false) break + case .updateDeleteGroupCallMessages(let call, let messages): + if boxed { + buffer.appendInt32(1048963372) + } + call.serialize(buffer, true) + buffer.appendInt32(481674261) + buffer.appendInt32(Int32(messages.count)) + for item in messages { + serializeInt32(item, buffer: buffer, boxed: false) + } + break case .updateDeleteMessages(let messages, let pts, let ptsCount): if boxed { buffer.appendInt32(-1576161051) @@ -2072,6 +2084,8 @@ public extension Api { return ("updateDcOptions", [("dcOptions", dcOptions as Any)]) case .updateDeleteChannelMessages(let channelId, let messages, let pts, let ptsCount): return ("updateDeleteChannelMessages", [("channelId", channelId as Any), ("messages", messages as Any), ("pts", pts as Any), ("ptsCount", ptsCount as Any)]) + case .updateDeleteGroupCallMessages(let call, let messages): + return ("updateDeleteGroupCallMessages", [("call", call as Any), ("messages", messages as Any)]) case .updateDeleteMessages(let messages, let pts, let ptsCount): return ("updateDeleteMessages", [("messages", messages as Any), ("pts", pts as Any), ("ptsCount", ptsCount as Any)]) case .updateDeleteQuickReply(let shortcutId): @@ -3212,6 +3226,24 @@ public extension Api { return nil } } + public static func parse_updateDeleteGroupCallMessages(_ reader: BufferReader) -> Update? { + var _1: Api.InputGroupCall? + if let signature = reader.readInt32() { + _1 = Api.parse(reader, signature: signature) as? Api.InputGroupCall + } + var _2: [Int32]? + if let _ = reader.readInt32() { + _2 = Api.parseVector(reader, elementSignature: -1471112230, elementType: Int32.self) + } + let _c1 = _1 != nil + let _c2 = _2 != nil + if _c1 && _c2 { + return Api.Update.updateDeleteGroupCallMessages(call: _1!, messages: _2!) + } + else { + return nil + } + } public static func parse_updateDeleteMessages(_ reader: BufferReader) -> Update? { var _1: [Int32]? if let _ = reader.readInt32() { diff --git a/submodules/TelegramApi/Sources/Api39.swift b/submodules/TelegramApi/Sources/Api39.swift index 26acd6c292..bd7057c836 100644 --- a/submodules/TelegramApi/Sources/Api39.swift +++ b/submodules/TelegramApi/Sources/Api39.swift @@ -10402,6 +10402,44 @@ public extension Api.functions.phone { }) } } +public extension Api.functions.phone { + static func deleteGroupCallMessages(flags: Int32, call: Api.InputGroupCall, messages: [Int32]) -> (FunctionDescription, Buffer, DeserializeFunctionResponse) { + let buffer = Buffer() + buffer.appendInt32(-162573065) + serializeInt32(flags, buffer: buffer, boxed: false) + call.serialize(buffer, true) + buffer.appendInt32(481674261) + buffer.appendInt32(Int32(messages.count)) + for item in messages { + serializeInt32(item, buffer: buffer, boxed: false) + } + return (FunctionDescription(name: "phone.deleteGroupCallMessages", parameters: [("flags", String(describing: flags)), ("call", String(describing: call)), ("messages", String(describing: messages))]), buffer, DeserializeFunctionResponse { (buffer: Buffer) -> Api.Updates? in + let reader = BufferReader(buffer) + var result: Api.Updates? + if let signature = reader.readInt32() { + result = Api.parse(reader, signature: signature) as? Api.Updates + } + return result + }) + } +} +public extension Api.functions.phone { + static func deleteGroupCallParticipantMessages(flags: Int32, call: Api.InputGroupCall, participant: Api.InputPeer) -> (FunctionDescription, Buffer, DeserializeFunctionResponse) { + let buffer = Buffer() + buffer.appendInt32(499117216) + serializeInt32(flags, buffer: buffer, boxed: false) + call.serialize(buffer, true) + participant.serialize(buffer, true) + return (FunctionDescription(name: "phone.deleteGroupCallParticipantMessages", parameters: [("flags", String(describing: flags)), ("call", String(describing: call)), ("participant", String(describing: participant))]), buffer, DeserializeFunctionResponse { (buffer: Buffer) -> Api.Updates? in + let reader = BufferReader(buffer) + var result: Api.Updates? + if let signature = reader.readInt32() { + result = Api.parse(reader, signature: signature) as? Api.Updates + } + return result + }) + } +} public extension Api.functions.phone { static func discardCall(flags: Int32, peer: Api.InputPhoneCall, duration: Int32, reason: Api.PhoneCallDiscardReason, connectionId: Int64) -> (FunctionDescription, Buffer, DeserializeFunctionResponse) { let buffer = Buffer() @@ -10834,19 +10872,19 @@ public extension Api.functions.phone { } } public extension Api.functions.phone { - static func sendGroupCallMessage(flags: Int32, call: Api.InputGroupCall, randomId: Int64, message: Api.TextWithEntities, allowPaidStars: Int64?) -> (FunctionDescription, Buffer, DeserializeFunctionResponse) { + static func sendGroupCallMessage(flags: Int32, call: Api.InputGroupCall, randomId: Int64, message: Api.TextWithEntities, allowPaidStars: Int64?) -> (FunctionDescription, Buffer, DeserializeFunctionResponse) { let buffer = Buffer() - buffer.appendInt32(2124127245) + buffer.appendInt32(445465039) serializeInt32(flags, buffer: buffer, boxed: false) call.serialize(buffer, true) serializeInt64(randomId, buffer: buffer, boxed: false) message.serialize(buffer, true) if Int(flags) & Int(1 << 0) != 0 {serializeInt64(allowPaidStars!, buffer: buffer, boxed: false)} - return (FunctionDescription(name: "phone.sendGroupCallMessage", parameters: [("flags", String(describing: flags)), ("call", String(describing: call)), ("randomId", String(describing: randomId)), ("message", String(describing: message)), ("allowPaidStars", String(describing: allowPaidStars))]), buffer, DeserializeFunctionResponse { (buffer: Buffer) -> Api.Bool? in + return (FunctionDescription(name: "phone.sendGroupCallMessage", parameters: [("flags", String(describing: flags)), ("call", String(describing: call)), ("randomId", String(describing: randomId)), ("message", String(describing: message)), ("allowPaidStars", String(describing: allowPaidStars))]), buffer, DeserializeFunctionResponse { (buffer: Buffer) -> Api.Updates? in let reader = BufferReader(buffer) - var result: Api.Bool? + var result: Api.Updates? if let signature = reader.readInt32() { - result = Api.parse(reader, signature: signature) as? Api.Bool + result = Api.parse(reader, signature: signature) as? Api.Updates } return result }) diff --git a/submodules/TelegramApi/Sources/Api7.swift b/submodules/TelegramApi/Sources/Api7.swift index 8caae0f72d..9d77c664e9 100644 --- a/submodules/TelegramApi/Sources/Api7.swift +++ b/submodules/TelegramApi/Sources/Api7.swift @@ -1330,18 +1330,18 @@ public extension Api { } public extension Api { enum GroupCallMessage: TypeConstructorDescription { - case groupCallMessage(flags: Int32, fromId: Api.Peer, date: Int32, randomId: Int64, message: Api.TextWithEntities, paidMessageStars: Int64?) + case groupCallMessage(flags: Int32, id: Int32, fromId: Api.Peer, date: Int32, message: Api.TextWithEntities, paidMessageStars: Int64?) public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { switch self { - case .groupCallMessage(let flags, let fromId, let date, let randomId, let message, let paidMessageStars): + case .groupCallMessage(let flags, let id, let fromId, let date, let message, let paidMessageStars): if boxed { - buffer.appendInt32(-2018173984) + buffer.appendInt32(445316222) } serializeInt32(flags, buffer: buffer, boxed: false) + serializeInt32(id, buffer: buffer, boxed: false) fromId.serialize(buffer, true) serializeInt32(date, buffer: buffer, boxed: false) - serializeInt64(randomId, buffer: buffer, boxed: false) message.serialize(buffer, true) if Int(flags) & Int(1 << 0) != 0 {serializeInt64(paidMessageStars!, buffer: buffer, boxed: false)} break @@ -1350,22 +1350,22 @@ public extension Api { public func descriptionFields() -> (String, [(String, Any)]) { switch self { - case .groupCallMessage(let flags, let fromId, let date, let randomId, let message, let paidMessageStars): - return ("groupCallMessage", [("flags", flags as Any), ("fromId", fromId as Any), ("date", date as Any), ("randomId", randomId as Any), ("message", message as Any), ("paidMessageStars", paidMessageStars as Any)]) + case .groupCallMessage(let flags, let id, let fromId, let date, let message, let paidMessageStars): + return ("groupCallMessage", [("flags", flags as Any), ("id", id as Any), ("fromId", fromId as Any), ("date", date as Any), ("message", message as Any), ("paidMessageStars", paidMessageStars as Any)]) } } public static func parse_groupCallMessage(_ reader: BufferReader) -> GroupCallMessage? { var _1: Int32? _1 = reader.readInt32() - var _2: Api.Peer? + var _2: Int32? + _2 = reader.readInt32() + var _3: Api.Peer? if let signature = reader.readInt32() { - _2 = Api.parse(reader, signature: signature) as? Api.Peer + _3 = Api.parse(reader, signature: signature) as? Api.Peer } - var _3: Int32? - _3 = reader.readInt32() - var _4: Int64? - _4 = reader.readInt64() + var _4: Int32? + _4 = reader.readInt32() var _5: Api.TextWithEntities? if let signature = reader.readInt32() { _5 = Api.parse(reader, signature: signature) as? Api.TextWithEntities @@ -1379,7 +1379,7 @@ public extension Api { let _c5 = _5 != nil let _c6 = (Int(_1!) & Int(1 << 0) == 0) || _6 != nil if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 { - return Api.GroupCallMessage.groupCallMessage(flags: _1!, fromId: _2!, date: _3!, randomId: _4!, message: _5!, paidMessageStars: _6) + return Api.GroupCallMessage.groupCallMessage(flags: _1!, id: _2!, fromId: _3!, date: _4!, message: _5!, paidMessageStars: _6) } else { return nil diff --git a/submodules/TelegramCallsUI/Sources/PresentationGroupCall.swift b/submodules/TelegramCallsUI/Sources/PresentationGroupCall.swift index 583d4eb3f2..028bfe5803 100644 --- a/submodules/TelegramCallsUI/Sources/PresentationGroupCall.swift +++ b/submodules/TelegramCallsUI/Sources/PresentationGroupCall.swift @@ -831,7 +831,7 @@ public final class PresentationGroupCallImpl: PresentationGroupCall { } } } - private let messagesStatePromise = Promise(GroupCallMessagesContext.State(messages: [])) + private let messagesStatePromise = Promise(GroupCallMessagesContext.State(messages: [], pinnedMessages: [])) public var messagesState: Signal { return self.messagesStatePromise.get() } @@ -4041,6 +4041,12 @@ public final class PresentationGroupCallImpl: PresentationGroupCall { messagesContext.send(fromId: self.joinAsPeerId, randomId: randomId, text: text, entities: entities, paidStars: paidStars) } } + + public func deleteMessage(id: Int64) { + if let messagesContext = self.messagesContext { + messagesContext.deleteMessage(id: id) + } + } } public final class TelegramE2EEncryptionProviderImpl: TelegramE2EEncryptionProvider { diff --git a/submodules/TelegramCore/Sources/Account/AccountIntermediateState.swift b/submodules/TelegramCore/Sources/Account/AccountIntermediateState.swift index ee058951af..2596eb6c16 100644 --- a/submodules/TelegramCore/Sources/Account/AccountIntermediateState.swift +++ b/submodules/TelegramCore/Sources/Account/AccountIntermediateState.swift @@ -118,7 +118,7 @@ enum AccountStateMutationOperation { case UpdateGroupCallParticipants(id: Int64, accessHash: Int64, participants: [Api.GroupCallParticipant], version: Int32) case UpdateGroupCall(peerId: PeerId?, call: Api.GroupCall) case UpdateGroupCallChainBlocks(id: Int64, accessHash: Int64, subChainId: Int32, blocks: [Data], nextOffset: Int32) - case UpdateGroupCallMessage(id: Int64, authorId: PeerId, randomId: Int64, text: Api.TextWithEntities, date: Int32, paidMessageStars: Int64?) + case UpdateGroupCallMessage(id: Int64, authorId: PeerId, messageId: Int32, text: Api.TextWithEntities, date: Int32, paidMessageStars: Int64?) case UpdateGroupCallOpaqueMessage(id: Int64, authorId: PeerId, data: Data) case UpdateAutoremoveTimeout(peer: Api.Peer, value: CachedPeerAutoremoveTimeout.Value?) case UpdateAttachMenuBots @@ -411,8 +411,8 @@ struct AccountMutableState { self.addOperation(.UpdateGroupCallChainBlocks(id: id, accessHash: accessHash, subChainId: subChainId, blocks: blocks, nextOffset: nextOffset)) } - mutating func updateGroupCallMessage(id: Int64, authorId: PeerId, randomId: Int64, text: Api.TextWithEntities, date: Int32, paidMessageStars: Int64?) { - self.addOperation(.UpdateGroupCallMessage(id: id, authorId: authorId, randomId: randomId, text: text, date: date, paidMessageStars: paidMessageStars)) + mutating func updateGroupCallMessage(id: Int64, authorId: PeerId, messageId: Int32, text: Api.TextWithEntities, date: Int32, paidMessageStars: Int64?) { + self.addOperation(.UpdateGroupCallMessage(id: id, authorId: authorId, messageId: messageId, text: text, date: date, paidMessageStars: paidMessageStars)) } mutating func updateGroupCallOpaqueMessage(id: Int64, authorId: PeerId, data: Data) { diff --git a/submodules/TelegramCore/Sources/State/AccountStateManagementUtils.swift b/submodules/TelegramCore/Sources/State/AccountStateManagementUtils.swift index 3569f9df1b..c1e43789ae 100644 --- a/submodules/TelegramCore/Sources/State/AccountStateManagementUtils.swift +++ b/submodules/TelegramCore/Sources/State/AccountStateManagementUtils.swift @@ -1686,8 +1686,8 @@ private func finalStateWithUpdatesAndServerTime(accountPeerId: PeerId, postbox: case let .updateGroupCallMessage(call, message): if case let .inputGroupCall(id, _) = call { switch message { - case let .groupCallMessage(_, fromId, date, randomId, message, paidMessageStars): - updatedState.updateGroupCallMessage(id: id, authorId: fromId.peerId, randomId: randomId, text: message, date: date, paidMessageStars: paidMessageStars) + case let .groupCallMessage(_, messageId, fromId, date, message, paidMessageStars): + updatedState.updateGroupCallMessage(id: id, authorId: fromId.peerId, messageId: messageId, text: message, date: date, paidMessageStars: paidMessageStars) } } case let .updateGroupCallEncryptedMessage(call, fromId, encryptedMessage): @@ -4882,10 +4882,10 @@ func replayFinalState( callId, .state(update: GroupCallParticipantsContext.Update.StateUpdate(participants: participants, version: version)) )) - case let .UpdateGroupCallMessage(callId, authorId, randomId, text, date, paidMessageStars): + case let .UpdateGroupCallMessage(callId, authorId, messageId, text, date, paidMessageStars): switch text { case let .textWithEntities(text, entities): - groupCallMessageUpdates.append(GroupCallMessageUpdate(callId: callId, update: .newPlaintextMessage(authorId: authorId, randomId: randomId, text: text, entities: messageTextEntitiesFromApiEntities(entities), timestamp: date, paidMessageStars: paidMessageStars))) + groupCallMessageUpdates.append(GroupCallMessageUpdate(callId: callId, update: .newPlaintextMessage(authorId: authorId, messageId: messageId, text: text, entities: messageTextEntitiesFromApiEntities(entities), timestamp: date, paidMessageStars: paidMessageStars))) } case let .UpdateGroupCallOpaqueMessage(callId, authorId, data): groupCallMessageUpdates.append(GroupCallMessageUpdate(callId: callId, update: .newOpaqueMessage(authorId: authorId, data: data))) diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Calls/GroupCalls.swift b/submodules/TelegramCore/Sources/TelegramEngine/Calls/GroupCalls.swift index 57ac29b79f..be7dd051ab 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Calls/GroupCalls.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Calls/GroupCalls.swift @@ -3460,7 +3460,7 @@ func _internal_refreshInlineGroupCall(account: Account, messageId: MessageId) -> struct GroupCallMessageUpdate { enum Update { - case newPlaintextMessage(authorId: PeerId, randomId: Int64, text: String, entities: [MessageTextEntity], timestamp: Int32, paidMessageStars: Int64?) + case newPlaintextMessage(authorId: PeerId, messageId: Int32, text: String, entities: [MessageTextEntity], timestamp: Int32, paidMessageStars: Int64?) case newOpaqueMessage(authorId: PeerId, data: Data) } @@ -3692,6 +3692,18 @@ public final class GroupCallMessagesContext { self.paidStars = paidStars } + public func withId(_ id: Int64) -> Message { + return Message( + id: id, + author: self.author, + text: self.text, + entities: self.entities, + date: self.date, + lifetime: self.lifetime, + paidStars: self.paidStars + ) + } + public static func ==(lhs: Message, rhs: Message) -> Bool { if lhs.id != rhs.id { return false @@ -3723,9 +3735,11 @@ public final class GroupCallMessagesContext { public struct State: Equatable { public var messages: [Message] + public var pinnedMessages: [Message] - public init(messages: [Message]) { + public init(messages: [Message], pinnedMessages: [Message]) { self.messages = messages + self.pinnedMessages = pinnedMessages } } @@ -3762,7 +3776,7 @@ public final class GroupCallMessagesContext { self.messageLifetime = messageLifetime self.isLiveStream = isLiveStream - self.state = State(messages: []) + self.state = State(messages: [], pinnedMessages: []) self.stateValue.set(self.state) self.updatesDisposable = (account.stateManager.groupCallMessageUpdates @@ -3770,17 +3784,17 @@ public final class GroupCallMessagesContext { guard let self else { return } - let currentTime = Int32(CFAbsoluteTimeGetCurrent()) - var addedMessages: [(authorId: PeerId, randomId: Int64, text: String, entities: [MessageTextEntity], timestamp: Int32, paidMessageStars: Int64?)] = [] + let currentTime = Int32(CFAbsoluteTimeGetCurrent() + kCFAbsoluteTimeIntervalSince1970) + var addedMessages: [(authorId: PeerId, messageId: Int32, text: String, entities: [MessageTextEntity], timestamp: Int32, paidMessageStars: Int64?)] = [] var addedOpaqueMessages: [(authorId: PeerId, data: Data)] = [] for update in updates { if update.callId != self.callId { continue } switch update.update { - case let .newPlaintextMessage(authorId, randomId, text, entities, timestamp, paidMessageStars): + case let .newPlaintextMessage(authorId, messageId, text, entities, timestamp, paidMessageStars): if authorId != self.account.peerId { - addedMessages.append((authorId, randomId, text, entities, timestamp, paidMessageStars)) + addedMessages.append((authorId, messageId, text, entities, timestamp, paidMessageStars)) } case let .newOpaqueMessage(authorId, data): if authorId != self.account.peerId { @@ -3830,7 +3844,7 @@ public final class GroupCallMessagesContext { } } else { for addedMessage in addedMessages { - if self.processedIds.contains(addedMessage.randomId) { + if self.processedIds.contains(Int64(addedMessage.messageId)) { continue } @@ -3841,15 +3855,16 @@ public final class GroupCallMessagesContext { lifetime = self.messageLifetime } - messages.append(Message( - id: addedMessage.randomId, + let message = Message( + id: Int64(addedMessage.messageId), author: transaction.getPeer(addedMessage.authorId).flatMap(EnginePeer.init), text: addedMessage.text, entities: addedMessage.entities, date: addedMessage.timestamp, lifetime: lifetime, paidStars: addedMessage.paidMessageStars - )) + ) + messages.append(message) } } return messages @@ -3862,7 +3877,17 @@ public final class GroupCallMessagesContext { self.processedIds.insert(message.id) } var state = self.state - state.messages.append(contentsOf: messages) + var existingIds = Set(state.messages.map(\.id)) + for message in messages { + if existingIds.contains(message.id) { + continue + } + existingIds.insert(message.id) + state.messages.append(message) + if self.isLiveStream && message.paidStars != nil { + state.pinnedMessages.append(message) + } + } self.state = state }) } @@ -3882,12 +3907,32 @@ public final class GroupCallMessagesContext { } private func messageLifetimeTick() { - let now = Int32(CFAbsoluteTimeGetCurrent()) - let filtered = self.state.messages.filter { now - $0.date < $0.lifetime } - if filtered.count != self.state.messages.count { - var state = self.state - state.messages = filtered - self.state = state + let now = Int32(CFAbsoluteTimeGetCurrent() + kCFAbsoluteTimeIntervalSince1970) + var updatedState: State? + if !self.isLiveStream { + for i in (0 ..< self.state.messages.count).reversed() { + let message = self.state.messages[i] + if (now - message.date) < message.lifetime { + if updatedState == nil { + updatedState = self.state + } + updatedState?.messages.remove(at: i) + } + } + } + + for i in (0 ..< self.state.pinnedMessages.count).reversed() { + let message = self.state.pinnedMessages[i] + if (now - message.date) < message.lifetime { + if updatedState == nil { + updatedState = self.state + } + updatedState?.pinnedMessages.remove(at: i) + } + } + + if let updatedState { + self.state = updatedState } } @@ -3900,7 +3945,7 @@ public final class GroupCallMessagesContext { return } - let currentTime = Int32(CFAbsoluteTimeGetCurrent()) + let currentTime = Int32(CFAbsoluteTimeGetCurrent() + kCFAbsoluteTimeIntervalSince1970) var randomId: Int64 = 0 if let requestedRandomId { @@ -3917,7 +3962,7 @@ public final class GroupCallMessagesContext { } var state = self.state - state.messages.append(Message( + let message = Message( id: randomId, author: fromPeer.flatMap(EnginePeer.init), text: text, @@ -3925,7 +3970,13 @@ public final class GroupCallMessagesContext { date: currentTime, lifetime: lifetime, paidStars: paidStars - )) + ) + state.messages.append(message) + if self.isLiveStream { + if paidStars != nil { + state.pinnedMessages.append(message) + } + } self.state = state #if DEBUG @@ -3961,7 +4012,7 @@ public final class GroupCallMessagesContext { if paidStars != nil { flags |= 1 << 0 } - self.sendMessageDisposables.add(self.account.network.request(Api.functions.phone.sendGroupCallMessage( + self.sendMessageDisposables.add((self.account.network.request(Api.functions.phone.sendGroupCallMessage( flags: 0, call: self.reference.apiInputGroupCall, randomId: randomId, @@ -3970,10 +4021,51 @@ public final class GroupCallMessagesContext { entities: apiEntitiesFromMessageTextEntities(entities, associatedPeers: SimpleDictionary()) ), allowPaidStars: paidStars - )).startStrict()) + )) |> deliverOn(self.queue)).startStrict(next: { [weak self] updates in + guard let self else { + return + } + self.account.stateManager.addUpdates(updates) + for update in updates.allUpdates { + if case let .updateMessageID(id, randomIdValue) = update { + if randomIdValue == randomId { + var state = self.state + if let index = state.messages.firstIndex(where: { $0.id == randomId }) { + state.messages[index] = state.messages[index].withId(Int64(id)) + } + if let index = state.pinnedMessages.firstIndex(where: { $0.id == randomId }) { + state.pinnedMessages[index] = state.pinnedMessages[index].withId(Int64(id)) + } + self.state = state + break + } + } + } + }, error: { _ in + + })) } }) } + + func deleteMessage(id: Int64) { + var updatedState: State? + if let index = self.state.messages.firstIndex(where: { $0.id == id }) { + if updatedState == nil { + updatedState = self.state + } + updatedState?.messages.remove(at: index) + } + if let index = self.state.pinnedMessages.firstIndex(where: { $0.id == id }) { + if updatedState == nil { + updatedState = self.state + } + updatedState?.pinnedMessages.remove(at: index) + } + if let updatedState { + self.state = updatedState + } + } } private let queue: Queue @@ -3999,6 +4091,12 @@ public final class GroupCallMessagesContext { } } + public func deleteMessage(id: Int64) { + self.impl.with { impl in + impl.deleteMessage(id: id) + } + } + public static func getStarAmountParamMapping(value: Int64) -> (period: Int, maxLength: Int, emojiCount: Int) { if value >= 10000 { return (3600, 400, 20) diff --git a/submodules/TelegramUI/BUILD b/submodules/TelegramUI/BUILD index 46b169144b..ee25931c58 100644 --- a/submodules/TelegramUI/BUILD +++ b/submodules/TelegramUI/BUILD @@ -491,6 +491,7 @@ swift_library( "//submodules/TelegramUI/Components/Chat/ChatInputAccessoryPanel", "//submodules/TelegramUI/Components/Chat/ChatInputMessageAccessoryPanel", "//submodules/TelegramUI/Components/Chat/ChatRecordingViewOnceButtonNode", + "//submodules/TelegramUI/Components/Chat/ChatPanelsComponent", "//submodules/TelegramUI/Components/EdgeEffect", ] + select({ "@build_bazel_rules_apple//apple:ios_arm64": appcenter_targets, diff --git a/submodules/TelegramUI/Components/Chat/ChatPanelsComponent/BUILD b/submodules/TelegramUI/Components/Chat/ChatPanelsComponent/BUILD new file mode 100644 index 0000000000..49c5e938e3 --- /dev/null +++ b/submodules/TelegramUI/Components/Chat/ChatPanelsComponent/BUILD @@ -0,0 +1,26 @@ +load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library") + +swift_library( + name = "ChatPanelsComponent", + module_name = "ChatPanelsComponent", + srcs = glob([ + "Sources/**/*.swift", + ]), + copts = [ + "-warnings-as-errors", + ], + deps = [ + "//submodules/Postbox", + "//submodules/TelegramCore", + "//submodules/Display", + "//submodules/AsyncDisplayKit", + "//submodules/TelegramPresentationData", + "//submodules/ChatPresentationInterfaceState", + "//submodules/ComponentFlow", + "//submodules/AccountContext", + "//submodules/TelegramUI/Components/GlassBackgroundComponent", + ], + visibility = [ + "//visibility:public", + ], +) diff --git a/submodules/TelegramUI/Components/Chat/ChatPanelsComponent/Sources/ChatPanelsComponent.swift b/submodules/TelegramUI/Components/Chat/ChatPanelsComponent/Sources/ChatPanelsComponent.swift new file mode 100644 index 0000000000..6c59e9a0c5 --- /dev/null +++ b/submodules/TelegramUI/Components/Chat/ChatPanelsComponent/Sources/ChatPanelsComponent.swift @@ -0,0 +1,69 @@ +import Foundation +import UIKit +import Display +import ComponentFlow +import AccountContext +import TelegramPresentationData +import TelegramCore +import GlassBackgroundComponent + +public final class ChatPanelsComponent: Component { + public let context: AccountContext + public let theme: PresentationTheme + public let strings: PresentationStrings + + public init( + context: AccountContext, + theme: PresentationTheme, + strings: PresentationStrings + ) { + self.context = context + self.theme = theme + self.strings = strings + } + + public static func ==(lhs: ChatPanelsComponent, rhs: ChatPanelsComponent) -> Bool { + if lhs.context !== rhs.context { + return false + } + if lhs.theme !== rhs.theme { + return false + } + if lhs.strings !== rhs.strings { + return false + } + return true + } + + public final class View: UIView { + override public init(frame: CGRect) { + super.init(frame: frame) + } + + required public init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override public func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { + guard let result = super.hitTest(point, with: event) else { + return nil + } + if result === self { + return nil + } + return result + } + + func update(component: ChatPanelsComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { + return availableSize + } + } + + 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) + } +} diff --git a/submodules/TelegramUI/Components/Chat/ChatSendStarsScreen/Sources/ChatSendStarsScreen.swift b/submodules/TelegramUI/Components/Chat/ChatSendStarsScreen/Sources/ChatSendStarsScreen.swift index 308aef55be..329f9b9ca9 100644 --- a/submodules/TelegramUI/Components/Chat/ChatSendStarsScreen/Sources/ChatSendStarsScreen.swift +++ b/submodules/TelegramUI/Components/Chat/ChatSendStarsScreen/Sources/ChatSendStarsScreen.swift @@ -26,112 +26,6 @@ import StarsBalanceOverlayComponent import TelegramStringFormatting import ChatScheduleTimeController -private final class BalanceComponent: CombinedComponent { - let context: AccountContext - let theme: PresentationTheme - let strings: PresentationStrings - let balance: StarsAmount? - - init( - context: AccountContext, - theme: PresentationTheme, - strings: PresentationStrings, - balance: StarsAmount? - ) { - self.context = context - self.theme = theme - self.strings = strings - self.balance = balance - } - - static func ==(lhs: BalanceComponent, rhs: BalanceComponent) -> Bool { - if lhs.context !== rhs.context { - return false - } - if lhs.theme !== rhs.theme { - return false - } - if lhs.strings !== rhs.strings { - return false - } - if lhs.balance != rhs.balance { - return false - } - return true - } - - static var body: Body { - let title = Child(MultilineTextComponent.self) - let balance = Child(MultilineTextComponent.self) - let icon = Child(BundleIconComponent.self) - - return { context in - var size = CGSize(width: 0.0, height: 0.0) - - let title = title.update( - component: MultilineTextComponent( - text: .plain(NSAttributedString(string: context.component.strings.SendStarReactions_Balance, font: Font.regular(14.0), textColor: context.component.theme.list.itemPrimaryTextColor)) - ), - availableSize: context.availableSize, - transition: .immediate - ) - - size.width = max(size.width, title.size.width) - size.height += title.size.height - - let balanceText: String - if let value = context.component.balance { - balanceText = "\(value.stringValue)" - } else { - balanceText = "..." - } - let balance = balance.update( - component: MultilineTextComponent( - text: .plain(NSAttributedString(string: balanceText, font: Font.medium(15.0), textColor: context.component.theme.list.itemPrimaryTextColor)) - ), - availableSize: context.availableSize, - transition: .immediate - ) - - let iconSize = CGSize(width: 18.0, height: 18.0) - let icon = icon.update( - component: BundleIconComponent( - name: "Premium/Stars/StarLarge", - tintColor: nil - ), - availableSize: iconSize, - transition: context.transition - ) - - let titleSpacing: CGFloat = 1.0 - let iconSpacing: CGFloat = 2.0 - - size.height += titleSpacing - - size.width = max(size.width, icon.size.width + iconSpacing + balance.size.width) - size.height += balance.size.height - - context.add( - title.position( - title.size.centered(in: CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: title.size)).center - ) - ) - context.add( - balance.position( - balance.size.centered(in: CGRect(origin: CGPoint(x: icon.size.width + iconSpacing, y: title.size.height + titleSpacing), size: balance.size)).center - ) - ) - context.add( - icon.position( - icon.size.centered(in: CGRect(origin: CGPoint(x: -1.0, y: title.size.height + titleSpacing), size: icon.size)).center - ) - ) - - return size - } - } -} - private final class BadgeComponent: Component { let theme: PresentationTheme let title: String @@ -859,12 +753,14 @@ private final class ChatSendStarsScreenComponent: Component { private struct ItemLayout: Equatable { var containerSize: CGSize var containerInset: CGFloat + var containerCornerRadius: CGFloat var bottomInset: CGFloat var topInset: CGFloat - init(containerSize: CGSize, containerInset: CGFloat, bottomInset: CGFloat, topInset: CGFloat) { + init(containerSize: CGSize, containerInset: CGFloat, containerCornerRadius: CGFloat, bottomInset: CGFloat, topInset: CGFloat) { self.containerSize = containerSize self.containerInset = containerInset + self.containerCornerRadius = containerCornerRadius self.bottomInset = bottomInset self.topInset = topInset } @@ -961,6 +857,7 @@ private final class ChatSendStarsScreenComponent: Component { final class View: UIView, UIScrollViewDelegate { private let dimView: UIView + private let containerView: UIView private let backgroundLayer: SimpleLayer private let navigationBarContainer: SparseContainerView private let scrollView: ScrollView @@ -970,6 +867,7 @@ private final class ChatSendStarsScreenComponent: Component { private var balanceOverlay = ComponentView() + private let backgroundHandleView: UIImageView private let leftButton = ComponentView() private let peerSelectorButton = ComponentView() private let closeButton = ComponentView() @@ -984,6 +882,8 @@ private final class ChatSendStarsScreenComponent: Component { private let slider = ComponentView() private let badge = ComponentView() + private var liveStreamPerks: [ComponentView] = [] + private var topPeersLeftSeparator: SimpleLayer? private var topPeersRightSeparator: SimpleLayer? private var topPeersTitleBackground: SimpleLayer? @@ -1032,10 +932,17 @@ private final class ChatSendStarsScreenComponent: Component { self.bottomOverscrollLimit = 200.0 self.dimView = UIView() + self.containerView = UIView() + + self.containerView.clipsToBounds = true + self.containerView.layer.cornerRadius = 40.0 + self.containerView.layer.maskedCorners = [.layerMinXMaxYCorner, .layerMaxXMaxYCorner] self.backgroundLayer = SimpleLayer() self.backgroundLayer.maskedCorners = [.layerMinXMinYCorner, .layerMaxXMinYCorner] - self.backgroundLayer.cornerRadius = 10.0 + self.backgroundLayer.cornerRadius = 40.0 + + self.backgroundHandleView = UIImageView() self.navigationBarContainer = SparseContainerView() @@ -1051,7 +958,8 @@ private final class ChatSendStarsScreenComponent: Component { super.init(frame: frame) self.addSubview(self.dimView) - self.layer.addSublayer(self.backgroundLayer) + self.addSubview(self.containerView) + self.containerView.layer.addSublayer(self.backgroundLayer) self.scrollView.delaysContentTouches = true self.scrollView.canCancelContentTouches = true @@ -1070,16 +978,16 @@ private final class ChatSendStarsScreenComponent: Component { self.scrollView.delegate = self self.scrollView.clipsToBounds = true - self.addSubview(self.scrollContentClippingView) + self.containerView.addSubview(self.scrollContentClippingView) self.scrollContentClippingView.addSubview(self.scrollView) self.scrollView.addSubview(self.scrollContentView) - self.addSubview(self.navigationBarContainer) + self.containerView.addSubview(self.navigationBarContainer) self.dimView.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.dimTapGesture(_:)))) - self.addSubnode(self.hierarchyTrackingNode) + self.containerView.addSubnode(self.hierarchyTrackingNode) self.hierarchyTrackingNode.updated = { [weak self] value in guard let self else { @@ -1120,7 +1028,7 @@ private final class ChatSendStarsScreenComponent: Component { } func scrollViewWillEndDragging(_ scrollView: UIScrollView, withVelocity velocity: CGPoint, targetContentOffset: UnsafeMutablePointer) { - guard let itemLayout = self.itemLayout, let topOffsetDistance = self.topOffsetDistance else { + /*guard let itemLayout = self.itemLayout, let topOffsetDistance = self.topOffsetDistance else { return } @@ -1130,7 +1038,7 @@ private final class ChatSendStarsScreenComponent: Component { if topOffset < topOffsetDistance { targetContentOffset.pointee.y = scrollView.contentOffset.y scrollView.setContentOffset(CGPoint(x: 0.0, y: itemLayout.topInset), animated: true) - } + }*/ } override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { @@ -1170,7 +1078,7 @@ private final class ChatSendStarsScreenComponent: Component { } private func updateScrolling(transition: ComponentTransition) { - guard let environment = self.environment, let controller = environment.controller(), let itemLayout = self.itemLayout else { + guard let itemLayout = self.itemLayout else { return } var topOffset = -self.scrollView.bounds.minY + itemLayout.topInset @@ -1179,13 +1087,22 @@ private final class ChatSendStarsScreenComponent: Component { transition.setPosition(view: self.navigationBarContainer, position: CGPoint(x: 0.0, y: topOffset + itemLayout.containerInset)) - let topOffsetDistance: CGFloat = min(60.0, floor(itemLayout.containerSize.height * 0.25)) - self.topOffsetDistance = topOffsetDistance - var topOffsetFraction = topOffset / topOffsetDistance + var topOffsetFraction = self.scrollView.bounds.minY / 100.0 topOffsetFraction = max(0.0, min(1.0, topOffsetFraction)) - let transitionFactor: CGFloat = 1.0 - topOffsetFraction - controller.updateModalStyleOverlayTransitionFactor(transitionFactor, transition: transition.containedViewLayoutTransition) + let minScale: CGFloat = (itemLayout.containerSize.width - 6.0 * 2.0) / itemLayout.containerSize.width + let minScaledTranslation: CGFloat = (itemLayout.containerSize.height - itemLayout.containerSize.height * minScale) * 0.5 - 6.0 + let minScaledCornerRadius: CGFloat = itemLayout.containerCornerRadius + + let scale = minScale * (1.0 - topOffsetFraction) + 1.0 * topOffsetFraction + let scaledTranslation = minScaledTranslation * (1.0 - topOffsetFraction) + let scaledCornerRadius = minScaledCornerRadius * (1.0 - topOffsetFraction) + itemLayout.containerCornerRadius * topOffsetFraction + + var containerTransform = CATransform3DIdentity + containerTransform = CATransform3DTranslate(containerTransform, 0.0, scaledTranslation, 0.0) + containerTransform = CATransform3DScale(containerTransform, scale, scale, scale) + transition.setTransform(view: self.containerView, transform: containerTransform) + transition.setCornerRadius(layer: self.containerView.layer, cornerRadius: scaledCornerRadius) } func animateIn() { @@ -1546,7 +1463,7 @@ private final class ChatSendStarsScreenComponent: Component { if themeUpdated { self.dimView.backgroundColor = UIColor(white: 0.0, alpha: 0.5) - self.backgroundLayer.backgroundColor = environment.theme.list.plainBackgroundColor.cgColor + self.backgroundLayer.backgroundColor = environment.theme.actionSheet.opaqueItemBackgroundColor.cgColor var locations: [NSNumber] = [] var colors: [CGColor] = [] @@ -1729,38 +1646,79 @@ private final class ChatSendStarsScreenComponent: Component { let starsRect = CGRect(origin: .zero, size: CGSize(width: availableSize.width, height: sliderForegroundFrame.midY)) self.badgeStars.frame = starsRect - self.badgeStars.update(size: starsRect.size, emitterPosition: CGPoint(x: badgeFrame.minX, y: badgeFrame.midY - 64.0)) + self.badgeStars.update(size: starsRect.size, color: sliderColor, emitterPosition: CGPoint(x: badgeFrame.minX, y: badgeFrame.midY - 64.0)) } - contentHeight += 123.0 + switch component.initialData.subjectInitialData { + case .liveStreamMessage: + //LiveStreamPerkComponent + + //TODO:localize + let params = GroupCallMessagesContext.getStarAmountParamMapping(value: Int64(self.amount.realValue)) + var perks: [(String, String)] = [] + + perks.append(( + shortTimeIntervalString(strings: environment.strings, value: Int32(params.period), useLargeFormat: false), + "pin in chat" + )) + + perks.append(( + "\(params.maxLength)", + "characters" + )) + + perks.append(( + "\(params.emojiCount)", + "emoji" + )) + + contentHeight += 180.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)) + + 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)) + if i == perks.count - 1 { + perkFrame.size.width = max(0.0, availableSize.width - sideInset - perkFrame.minX) + } + let perkView: ComponentView + if self.liveStreamPerks.count > i { + perkView = self.liveStreamPerks[i] + } else { + perkView = ComponentView() + self.liveStreamPerks.append(perkView) + } + let perk = perks[i] + let _ = perkView.update( + transition: transition, + component: AnyComponent(LiveStreamPerkComponent( + title: perk.0, + subtitle: perk.1, + theme: environment.theme + )), + environment: {}, + containerSize: perkFrame.size + ) + if let perkComponentView = perkView.view { + if perkComponentView.superview == nil { + self.scrollContentView.addSubview(perkComponentView) + } + transition.setFrame(view: perkComponentView, frame: perkFrame) + } + } + + contentHeight += perkHeight - 46.0 + case .react: + contentHeight += 123.0 + } - var leftButtonFrameValue: CGRect? switch component.initialData.subjectInitialData { case let .react(reactData): var sendAsPeers: [EnginePeer] = [reactData.myPeer] sendAsPeers.append(contentsOf: self.channelsForPublicReaction) - let leftButtonSize = self.leftButton.update( - transition: transition, - component: AnyComponent(BalanceComponent( - context: component.context, - theme: environment.theme, - strings: environment.strings, - balance: self.balance - )), - environment: {}, - containerSize: CGSize(width: 120.0, height: 100.0) - ) - let leftButtonFrame = CGRect(origin: CGPoint(x: sideInset, y: floor((56.0 - leftButtonSize.height) * 0.5)), size: leftButtonSize) - if let leftButtonView = self.leftButton.view { - if leftButtonView.superview == nil { - self.navigationBarContainer.addSubview(leftButtonView) - } - transition.setFrame(view: leftButtonView, frame: leftButtonFrame) - leftButtonView.isHidden = sendAsPeers.count > 1 - } - leftButtonFrameValue = leftButtonFrame - let currentMyPeer = self.currentMyPeer ?? reactData.myPeer let peerSelectorButtonSize = self.peerSelectorButton.update( @@ -1780,7 +1738,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((56.0 - peerSelectorButtonSize.height) * 0.5)), size: peerSelectorButtonSize) + let peerSelectorButtonFrame = CGRect(origin: CGPoint(x: sideInset, y: 1.0 + floor((72.0 - peerSelectorButtonSize.height) * 0.5)), size: peerSelectorButtonSize) if let peerSelectorButtonView = self.peerSelectorButton.view { if peerSelectorButtonView.superview == nil { self.navigationBarContainer.addSubview(peerSelectorButtonView) @@ -1792,6 +1750,16 @@ private final class ChatSendStarsScreenComponent: Component { break } + 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) + 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) + } + transition.setFrame(view: self.backgroundHandleView, frame: backgroundHandleFrame) + if themeUpdated { self.cachedCloseImage = nil } @@ -1799,7 +1767,7 @@ private final class ChatSendStarsScreenComponent: Component { if let current = self.cachedCloseImage { closeImage = current } else { - closeImage = generateCloseButtonImage(backgroundColor: UIColor(rgb: 0x808084, alpha: 0.1), foregroundColor: environment.theme.actionSheet.inputClearButtonColor)! + closeImage = generateCloseButtonImage(backgroundColor: UIColor(rgb: 0x808084, alpha: 0.1), foregroundColor: environment.theme.chat.inputPanel.panelControlColor)! self.cachedCloseImage = closeImage } let closeButtonSize = self.closeButton.update( @@ -1815,9 +1783,9 @@ private final class ChatSendStarsScreenComponent: Component { } )), environment: {}, - containerSize: CGSize(width: 30.0, height: 30.0) + containerSize: CGSize(width: 40.0, height: 40.0) ) - let closeButtonFrame = CGRect(origin: CGPoint(x: availableSize.width - sideInset - closeButtonSize.width, y: floor((56.0 - 34.0) * 0.5)), size: closeButtonSize) + let closeButtonFrame = CGRect(origin: CGPoint(x: sideInset, y: 16.0), size: closeButtonSize) if let closeButtonView = self.closeButton.view { if closeButtonView.superview == nil { self.navigationBarContainer.addSubview(closeButtonView) @@ -1853,7 +1821,7 @@ private final class ChatSendStarsScreenComponent: Component { text: .plain(NSAttributedString(string: subtitleText, font: Font.regular(12.0), textColor: environment.theme.list.itemSecondaryTextColor)) )), environment: {}, - containerSize: CGSize(width: availableSize.width - (leftButtonFrameValue?.maxX ?? sideInset) * 2.0, height: 100.0) + containerSize: CGSize(width: availableSize.width - sideInset * 2.0, height: 100.0) ) } @@ -1872,7 +1840,7 @@ private final class ChatSendStarsScreenComponent: Component { text: .plain(NSAttributedString(string: titleText, font: Font.semibold(17.0), textColor: environment.theme.list.itemPrimaryTextColor)) )), environment: {}, - containerSize: CGSize(width: availableSize.width - (leftButtonFrameValue?.maxX ?? sideInset) * 2.0, height: 100.0) + containerSize: CGSize(width: availableSize.width - sideInset * 2.0, height: 100.0) ) let titleSubtitleHeight: CGFloat @@ -1882,7 +1850,7 @@ private final class ChatSendStarsScreenComponent: Component { titleSubtitleHeight = titleSize.height } - let titleFrame = CGRect(origin: CGPoint(x: floor((availableSize.width - titleSize.width) * 0.5), y: floor((56.0 - titleSubtitleHeight) * 0.5)), size: titleSize) + let titleFrame = CGRect(origin: CGPoint(x: floor((availableSize.width - titleSize.width) * 0.5), y: floor((72.0 - titleSubtitleHeight) * 0.5)), size: titleSize) if let titleView = title.view { if titleView.superview == nil { self.navigationBarContainer.addSubview(titleView) @@ -1900,7 +1868,7 @@ private final class ChatSendStarsScreenComponent: Component { } } - contentHeight += 56.0 + contentHeight += 72.0 contentHeight += 8.0 let text: String @@ -2176,7 +2144,7 @@ private final class ChatSendStarsScreenComponent: Component { itemComponentView.alpha = 0.0 } - let itemFrame = CGRect(origin: CGPoint(x: itemX, y: contentHeight + 56.0), size: itemSize) + let itemFrame = CGRect(origin: CGPoint(x: itemX, y: contentHeight + 72.0), size: itemSize) if animateItem { itemPositionTransition.setPosition(view: itemComponentView, position: itemFrame.center) @@ -2308,10 +2276,11 @@ private final class ChatSendStarsScreenComponent: Component { transition: transition, component: AnyComponent(ButtonComponent( background: ButtonComponent.Background( + style: .glass, color: environment.theme.list.itemCheckColors.fillColor, foreground: environment.theme.list.itemCheckColors.foregroundColor, pressedColor: environment.theme.list.itemCheckColors.fillColor.withMultipliedAlpha(0.9), - cornerRadius: 10.0 + cornerRadius: 25.0 ), content: AnyComponentWithIdentity( id: AnyHashable(0), @@ -2448,7 +2417,7 @@ private final class ChatSendStarsScreenComponent: Component { let actionButtonFrame = CGRect(origin: CGPoint(x: sideInset, y: availableSize.height - bottomPanelHeight), size: actionButtonSize) if let actionButtonView = actionButton.view { if actionButtonView.superview == nil { - self.addSubview(actionButtonView) + self.containerView.addSubview(actionButtonView) } transition.setFrame(view: actionButtonView, frame: actionButtonFrame) } @@ -2457,7 +2426,7 @@ private final class ChatSendStarsScreenComponent: Component { let buttonDescriptionTextFrame = CGRect(origin: CGPoint(x: floor((availableSize.width - buttonDescriptionTextSize.width) * 0.5), y: actionButtonFrame.maxY + buttonDescriptionSpacing), size: buttonDescriptionTextSize) if let buttonDescriptionTextView = buttonDescriptionText.view { if buttonDescriptionTextView.superview == nil { - self.addSubview(buttonDescriptionTextView) + self.containerView.addSubview(buttonDescriptionTextView) } transition.setFrame(view: buttonDescriptionTextView, frame: buttonDescriptionTextFrame) } @@ -2474,7 +2443,7 @@ private final class ChatSendStarsScreenComponent: Component { self.scrollContentClippingView.layer.cornerRadius = 10.0 - self.itemLayout = ItemLayout(containerSize: availableSize, containerInset: containerInset, bottomInset: environment.safeInsets.bottom, topInset: topInset) + self.itemLayout = ItemLayout(containerSize: availableSize, containerInset: containerInset, containerCornerRadius: environment.deviceMetrics.screenCornerRadius, bottomInset: environment.safeInsets.bottom, topInset: topInset) transition.setFrame(view: self.scrollContentView, frame: CGRect(origin: CGPoint(x: 0.0, y: topInset + containerInset), size: CGSize(width: availableSize.width, height: contentHeight))) @@ -2497,6 +2466,9 @@ private final class ChatSendStarsScreenComponent: Component { self.ignoreScrolling = false self.updateScrolling(transition: transition) + transition.setPosition(view: self.containerView, position: CGRect(origin: CGPoint(), size: availableSize).center) + transition.setBounds(view: self.containerView, bounds: CGRect(origin: CGPoint(), size: availableSize)) + return availableSize } } @@ -2859,7 +2831,7 @@ public class ChatSendStarsScreen: ViewControllerComponentContainer { } private func generateCloseButtonImage(backgroundColor: UIColor, foregroundColor: UIColor) -> UIImage? { - return generateImage(CGSize(width: 30.0, height: 30.0), contextGenerator: { size, context in + return generateImage(CGSize(width: 40.0, height: 40.0), contextGenerator: { size, context in context.clear(CGRect(origin: CGPoint(), size: size)) context.setFillColor(backgroundColor.cgColor) @@ -2870,10 +2842,10 @@ private func generateCloseButtonImage(backgroundColor: UIColor, foregroundColor: context.setStrokeColor(foregroundColor.cgColor) context.beginPath() - context.move(to: CGPoint(x: 10.0, y: 10.0)) - context.addLine(to: CGPoint(x: 20.0, y: 20.0)) - context.move(to: CGPoint(x: 20.0, y: 10.0)) - context.addLine(to: CGPoint(x: 10.0, y: 20.0)) + context.move(to: CGPoint(x: 12.0, y: 12.0)) + context.addLine(to: CGPoint(x: size.width - 12.0, y: size.height - 12.0)) + context.move(to: CGPoint(x: size.width - 12.0, y: 12.0)) + context.addLine(to: CGPoint(x: 12.0, y: size.height - 12.0)) context.strokePath() }) } @@ -2881,6 +2853,7 @@ private func generateCloseButtonImage(backgroundColor: UIColor, foregroundColor: private final class BadgeStarsView: UIView { private let staticEmitterLayer = CAEmitterLayer() private let dynamicEmitterLayer = CAEmitterLayer() + private var currentColor: UIColor? override init(frame: CGRect) { super.init(frame: frame) @@ -2894,7 +2867,10 @@ private final class BadgeStarsView: UIView { } private func setupEmitter() { - let color = UIColor(rgb: 0xffbe27) + guard let currentColor = self.currentColor else { + return + } + let color = currentColor self.staticEmitterLayer.emitterShape = .circle self.staticEmitterLayer.emitterSize = CGSize(width: 10.0, height: 5.0) @@ -2999,8 +2975,9 @@ private final class BadgeStarsView: UIView { } } - func update(size: CGSize, emitterPosition: CGPoint) { - if self.staticEmitterLayer.emitterCells == nil { + func update(size: CGSize, color: UIColor, emitterPosition: CGPoint) { + if self.staticEmitterLayer.emitterCells == nil || self.currentColor != color { + self.currentColor = color self.setupEmitter() } @@ -3242,6 +3219,116 @@ final class HeaderContextReferenceContentSource: ContextReferenceContentSource { } } +private final class LiveStreamPerkComponent: Component { + let title: String + let subtitle: String + let theme: PresentationTheme + + init( + title: String, + subtitle: String, + theme: PresentationTheme + ) { + self.title = title + self.subtitle = subtitle + self.theme = theme + } + + static func ==(lhs: LiveStreamPerkComponent, rhs: LiveStreamPerkComponent) -> Bool { + if lhs.title != rhs.title { + return false + } + if lhs.subtitle != rhs.subtitle { + return false + } + if lhs.theme != rhs.theme { + return false + } + return true + } + + final class View: UIView { + let background = ComponentView() + let title = ComponentView() + let subtitle = ComponentView() + + override init(frame: CGRect) { + super.init(frame: frame) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + func update(component: LiveStreamPerkComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { + let backgroundFrame = CGRect(origin: CGPoint(), size: availableSize) + let _ = self.background.update( + transition: transition, + component: AnyComponent(FilledRoundedRectangleComponent( + color: UIColor(rgb: 0x808084, alpha: 0.1), + cornerRadius: .value(10.0), + smoothCorners: true + )), + environment: {}, + containerSize: backgroundFrame.size + ) + if let backgroundView = self.background.view { + if backgroundView.superview == nil { + self.addSubview(backgroundView) + } + transition.setFrame(view: backgroundView, frame: backgroundFrame) + } + + let titleSize = self.title.update( + transition: .immediate, + component: AnyComponent(MultilineTextComponent( + text: .plain(NSAttributedString(string: component.title, font: Font.semibold(20.0), textColor: component.theme.list.itemPrimaryTextColor)) + )), + environment: {}, + containerSize: backgroundFrame.size + ) + + let subtitleSize = self.subtitle.update( + transition: .immediate, + component: AnyComponent(MultilineTextComponent( + text: .plain(NSAttributedString(string: component.subtitle, font: Font.regular(11.0), textColor: component.theme.list.itemPrimaryTextColor)) + )), + environment: {}, + containerSize: backgroundFrame.size + ) + + let spacing: CGFloat = 2.0 + + let titleFrame = CGRect(origin: CGPoint(x: floor((backgroundFrame.width - titleSize.width) * 0.5), y: floor((backgroundFrame.height - titleSize.height - spacing - subtitleSize.height) * 0.5)), size: titleSize) + let subtitleFrame = CGRect(origin: CGPoint(x: floor((backgroundFrame.width - subtitleSize.width) * 0.5), y: titleFrame.maxY + spacing), size: subtitleSize) + + if let titleView = self.title.view { + if titleView.superview == nil { + self.addSubview(titleView) + } + titleView.frame = titleFrame + } + + if let subtitleView = self.subtitle.view { + if subtitleView.superview == nil { + self.addSubview(subtitleView) + } + subtitleView.frame = subtitleFrame + } + + return availableSize + } + } + + 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 getLiveStreamStarAmountColorMapping(value: Int64) -> UIColor { if value >= 10000 { return UIColor(rgb: 0x7C8695) diff --git a/submodules/TelegramUI/Components/Chat/ChatTextInputActionButtonsNode/Sources/ChatTextInputActionButtonsNode.swift b/submodules/TelegramUI/Components/Chat/ChatTextInputActionButtonsNode/Sources/ChatTextInputActionButtonsNode.swift index 672e296b85..ae56e2fe18 100644 --- a/submodules/TelegramUI/Components/Chat/ChatTextInputActionButtonsNode/Sources/ChatTextInputActionButtonsNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatTextInputActionButtonsNode/Sources/ChatTextInputActionButtonsNode.swift @@ -18,6 +18,70 @@ 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) + } +} + private final class EffectBadgeView: UIView { private let context: AccountContext private var currentEffectId: Int64? @@ -139,6 +203,7 @@ public final class ChatTextInputActionButtonsNode: ASDisplayNode, ChatSendMessag public let sendContainerNode: ASDisplayNode public let sendButtonBackgroundView: UIImageView + private var sendButtonBackgroundEffectLayer: StarsButtonEffectLayer? public let sendButton: HighlightTrackingButtonNode public var sendButtonRadialStatusNode: ChatSendButtonRadialStatusNode? public var sendButtonHasApplyIcon = false @@ -168,6 +233,8 @@ public final class ChatTextInputActionButtonsNode: ASDisplayNode, ChatSendMessag private var validLayout: CGSize? + public var customSendColor: UIColor? + public init(context: AccountContext, presentationInterfaceState: ChatPresentationInterfaceState, presentationContext: ChatPresentationContext?, presentController: @escaping (ViewController) -> Void) { self.context = context self.presentationContext = presentationContext @@ -347,8 +414,36 @@ public final class ChatTextInputActionButtonsNode: ASDisplayNode, ChatSendMessag transition.updateFrame(layer: self.micButton.layer, frame: CGRect(origin: CGPoint(), size: size)) self.micButton.layoutItems() - transition.updateFrame(view: self.sendButtonBackgroundView, frame: CGRect(origin: CGPoint(), size: innerSize).insetBy(dx: 3.0, dy: 3.0)) - self.sendButtonBackgroundView.tintColor = interfaceState.theme.chat.inputPanel.panelControlAccentColor + 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 let _ = self.customSendColor { + let sendButtonBackgroundEffectLayer: StarsButtonEffectLayer + var sendButtonBackgroundEffectLayerTransition = transition + if let current = self.sendButtonBackgroundEffectLayer { + sendButtonBackgroundEffectLayer = current + } else { + sendButtonBackgroundEffectLayerTransition = .immediate + sendButtonBackgroundEffectLayer = StarsButtonEffectLayer() + sendButtonBackgroundEffectLayer.masksToBounds = true + self.sendButtonBackgroundEffectLayer = sendButtonBackgroundEffectLayer + self.sendButtonBackgroundView.layer.addSublayer(sendButtonBackgroundEffectLayer) + if transition.isAnimated { + sendButtonBackgroundEffectLayer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) + } + } + 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) + } else if let sendButtonBackgroundEffectLayer = self.sendButtonBackgroundEffectLayer { + self.sendButtonBackgroundEffectLayer = nil + transition.updateFrame(layer: sendButtonBackgroundEffectLayer, frame: CGRect(origin: CGPoint(), size: sendButtonBackgroundFrame.size)) + transition.updateAlpha(layer: sendButtonBackgroundEffectLayer, alpha: 0.0, completion: { [weak sendButtonBackgroundEffectLayer] _ in + sendButtonBackgroundEffectLayer?.removeFromSuperlayer() + }) + } + transition.updateFrame(layer: self.sendButton.layer, frame: CGRect(origin: CGPoint(), size: innerSize)) let sendContainerFrame = CGRect(origin: CGPoint(), size: innerSize) transition.updatePosition(node: self.sendContainerNode, position: sendContainerFrame.center) diff --git a/submodules/TelegramUI/Components/Chat/ChatTextInputPanelNode/BUILD b/submodules/TelegramUI/Components/Chat/ChatTextInputPanelNode/BUILD index 25a1e1b6ad..7a7bb1e704 100644 --- a/submodules/TelegramUI/Components/Chat/ChatTextInputPanelNode/BUILD +++ b/submodules/TelegramUI/Components/Chat/ChatTextInputPanelNode/BUILD @@ -31,7 +31,6 @@ swift_library( "//submodules/Pasteboard", "//submodules/ChatPresentationInterfaceState", "//submodules/ManagedAnimationNode", - "//submodules/AttachmentUI", "//submodules/TelegramUI/Components/EditableChatTextNode", "//submodules/TelegramUI/Components/EmojiTextAttachmentView", "//submodules/Components/LottieAnimationComponent", diff --git a/submodules/TelegramUI/Components/Chat/ChatTextInputPanelNode/Sources/ChatTextInputPanelComponent.swift b/submodules/TelegramUI/Components/Chat/ChatTextInputPanelNode/Sources/ChatTextInputPanelComponent.swift new file mode 100644 index 0000000000..b975c01992 --- /dev/null +++ b/submodules/TelegramUI/Components/Chat/ChatTextInputPanelNode/Sources/ChatTextInputPanelComponent.swift @@ -0,0 +1,817 @@ +import Foundation +import UIKit +import Display +import AsyncDisplayKit +import TelegramPresentationData +import ComponentFlow +import ChatControllerInteraction +import AccountContext +import ChatPresentationInterfaceState +import TelegramCore +import ComponentDisplayAdapters + +private final class EmptyInputView: UIView, UIInputViewAudioFeedback { + var enableInputClicksWhenVisible: Bool { + return true + } +} + +public final class ChatTextInputPanelComponent: Component { + public final class ExternalState { + public fileprivate(set) var isEditing: Bool = false + public fileprivate(set) var textInputState: ChatTextInputState = ChatTextInputState() + public var resetInputState: ChatTextInputState? + + public init() { + } + } + + public enum InputMode { + case text + case emoji + case stickers + case commands + } + + public final class InlineAction: Equatable { + public enum Kind: Equatable { + case paidMessage + case inputMode(InputMode) + } + + public let kind: Kind + public let action: () -> Void + + public init(kind: Kind, action: @escaping () -> Void) { + self.kind = kind + self.action = action + } + + public static func ==(lhs: InlineAction, rhs: InlineAction) -> Bool { + if lhs.kind != rhs.kind { + return false + } + return true + } + } + + public final class LeftAction: Equatable { + public enum Kind: Equatable { + case attach + case toggleExpanded(isVisible: Bool, isExpanded: Bool) + } + + public let kind: Kind + public let action: () -> Void + + public init(kind: Kind, action: @escaping () -> Void) { + self.kind = kind + self.action = action + } + + public static func ==(lhs: LeftAction, rhs: LeftAction) -> Bool { + if lhs.kind != rhs.kind { + return false + } + return true + } + } + + public final class RightAction: Equatable { + public enum Kind: Equatable { + case stars(count: Int, isFilled: Bool) + } + + public let kind: Kind + public let action: () -> Void + + public init(kind: Kind, action: @escaping () -> Void) { + self.kind = kind + self.action = action + } + + public static func ==(lhs: RightAction, rhs: RightAction) -> Bool { + if lhs.kind != rhs.kind { + return false + } + return true + } + } + + let externalState: ExternalState + let context: AccountContext + let theme: PresentationTheme + let strings: PresentationStrings + let chatPeerId: EnginePeer.Id + let inlineActions: [InlineAction] + let leftAction: LeftAction? + let rightAction: RightAction? + let placeholder: String + let paidMessagePrice: StarsAmount? + let sendColor: UIColor? + let hideKeyboard: Bool + let insets: UIEdgeInsets + let maxHeight: CGFloat + let sendAction: (() -> Void)? + let sendContextAction: ((UIView, ContextGesture) -> Void)? + + public init( + externalState: ExternalState, + context: AccountContext, + theme: PresentationTheme, + strings: PresentationStrings, + chatPeerId: EnginePeer.Id, + inlineActions: [InlineAction], + leftAction: LeftAction?, + rightAction: RightAction?, + placeholder: String, + paidMessagePrice: StarsAmount?, + sendColor: UIColor?, + hideKeyboard: Bool, + insets: UIEdgeInsets, + maxHeight: CGFloat, + sendAction: (() -> Void)?, + sendContextAction: ((UIView, ContextGesture) -> Void)? + ) { + self.externalState = externalState + self.context = context + self.theme = theme + self.strings = strings + self.chatPeerId = chatPeerId + self.inlineActions = inlineActions + self.leftAction = leftAction + self.rightAction = rightAction + self.placeholder = placeholder + self.paidMessagePrice = paidMessagePrice + self.sendColor = sendColor + self.hideKeyboard = hideKeyboard + self.insets = insets + self.maxHeight = maxHeight + self.sendAction = sendAction + self.sendContextAction = sendContextAction + } + + public static func ==(lhs: ChatTextInputPanelComponent, rhs: ChatTextInputPanelComponent) -> Bool { + if lhs.externalState !== rhs.externalState { + return false + } + if lhs.context !== rhs.context { + return false + } + if lhs.theme !== rhs.theme { + return false + } + if lhs.strings !== rhs.strings { + return false + } + if lhs.chatPeerId != rhs.chatPeerId { + return false + } + if lhs.inlineActions != rhs.inlineActions { + return false + } + if lhs.leftAction != rhs.leftAction { + return false + } + if lhs.rightAction != rhs.rightAction { + return false + } + if lhs.placeholder != rhs.placeholder { + return false + } + if lhs.paidMessagePrice != rhs.paidMessagePrice { + return false + } + if lhs.sendColor != rhs.sendColor { + return false + } + if lhs.hideKeyboard != rhs.hideKeyboard { + return false + } + if lhs.insets != rhs.insets { + return false + } + if lhs.maxHeight != rhs.maxHeight { + return false + } + if (lhs.sendAction == nil) != (rhs.sendAction == nil) { + return false + } + if (lhs.sendContextAction == nil) != (rhs.sendContextAction == nil) { + return false + } + return true + } + + public final class View: UIView { + private var panelNode: ChatTextInputPanelNode? + + private var interfaceInteraction: ChatPanelInterfaceInteraction? + + private var component: ChatTextInputPanelComponent? + private weak var state: EmptyComponentState? + private var isUpdating: Bool = false + + override public init(frame: CGRect) { + super.init(frame: frame) + } + + required public init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + public func insertText(text: NSAttributedString) { + guard let panelNode = self.panelNode else { + return + } + panelNode.insertText(text: text) + } + + public func deleteBackward() { + guard let panelNode = self.panelNode, let textView = panelNode.textInputNode?.textView else { + return + } + textView.deleteBackward() + } + + public func updateState(transition: ComponentTransition) { + self.state?.updated(transition: transition) + } + + func update(component: ChatTextInputPanelComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { + self.isUpdating = true + defer { + self.isUpdating = false + } + + self.component = component + self.state = state + + if self.interfaceInteraction == nil { + let inputModeFromComponent: (ChatTextInputPanelComponent) -> ChatInputMode = { component in + for inlineAction in component.inlineActions { + switch inlineAction.kind { + case let .inputMode(inputMode): + switch inputMode { + case .text: + return .media(mode: .other, expanded: nil, focused: false) + case .commands: + return .text + case .stickers: + return .media(mode: .other, expanded: nil, focused: false) + case .emoji: + return .text + } + default: + break + } + } + + return .text + } + + self.interfaceInteraction = ChatPanelInterfaceInteraction( + setupReplyMessage: { _, _, _ in + }, + setupEditMessage: { _, _ in + }, + beginMessageSelection: { _, _ in + }, + cancelMessageSelection: { _ in + }, + deleteSelectedMessages: { + }, + reportSelectedMessages: { + }, + reportMessages: { _, _ in + }, + blockMessageAuthor: { _, _ in + }, + deleteMessages: { _, _, f in + f(.default) + }, + forwardSelectedMessages: { + }, + forwardCurrentForwardMessages: { + }, + forwardMessages: { _ in + }, + updateForwardOptionsState: { _ in + }, + presentForwardOptions: { _ in + }, + presentReplyOptions: { _ in + }, + presentLinkOptions: { _ in + }, + presentSuggestPostOptions: { + }, + shareSelectedMessages: { + }, + updateTextInputStateAndMode: { [weak self] f in + guard let self else { + return + } + if let component = self.component { + let currentMode = inputModeFromComponent(component) + let (updatedTextInputState, updatedMode) = f(component.externalState.textInputState, currentMode) + component.externalState.textInputState = updatedTextInputState + if !self.isUpdating { + self.state?.updated(transition: .spring(duration: 0.4)) + } + + if updatedMode != currentMode { + /*for inlineAction in component.inlineActions { + switch inlineAction.kind { + case .inputMode: + inlineAction.action() + return + default: + break + } + }*/ + } + } + }, + updateInputModeAndDismissedButtonKeyboardMessageId: { [weak self] f in + guard let self, let component = self.component else { + return + } + + var presentationInterfaceState = ChatPresentationInterfaceState( + chatWallpaper: .color(0), + theme: component.theme, + strings: component.strings, + dateTimeFormat: PresentationDateTimeFormat(), + nameDisplayOrder: .firstLast, + limitsConfiguration: component.context.currentLimitsConfiguration.with({ $0 }), + fontSize: .regular, + bubbleCorners: PresentationChatBubbleCorners( + mainRadius: 16.0, + auxiliaryRadius: 8.0, + mergeBubbleCorners: true + ), + accountPeerId: component.context.account.peerId, + mode: .standard(.default), + chatLocation: .peer(id: component.chatPeerId), + subject: nil, + peerNearbyData: nil, + greetingData: nil, + pendingUnpinnedAllMessages: false, + activeGroupCallInfo: nil, + hasActiveGroupCall: false, + importState: nil, + threadData: nil, + isGeneralThreadClosed: false, + replyMessage: nil, + accountPeerColor: nil, + businessIntro: nil + ) + let currentMode = inputModeFromComponent(component) + presentationInterfaceState = presentationInterfaceState.updatedInputMode { _ in + return currentMode + } + + let (updatedMode, _) = f(presentationInterfaceState) + + if updatedMode != currentMode { + /*for inlineAction in component.inlineActions { + switch inlineAction.kind { + case .inputMode: + inlineAction.action() + return + default: + break + } + }*/ + } + + if let panelNode = self.panelNode, let textView = panelNode.textInputNode?.textView { + component.externalState.isEditing = textView.isFirstResponder + } else { + component.externalState.isEditing = false + } + }, + openStickers: { [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 + } + } + }, + editMessage: { + }, + beginMessageSearch: { _, _ in + }, + dismissMessageSearch: { + }, + updateMessageSearch: { _ in + }, + openSearchResults: { + }, + navigateMessageSearch: { _ in + }, + openCalendarSearch: { + }, + toggleMembersSearch: { _ in + }, + navigateToMessage: { _, _, _, _ in + }, + navigateToChat: { _ in + }, + navigateToProfile: { _ in + }, + openPeerInfo: { + }, + togglePeerNotifications: { + }, + sendContextResult: { _, _, _, _ in + return false + }, + sendBotCommand: { _, _ in + }, + sendShortcut: { _ in + }, + openEditShortcuts: { + }, + sendBotStart: { _ in + }, + botSwitchChatWithPayload: { _, _ in + }, + beginMediaRecording: { _ in + }, + finishMediaRecording: { _ in + }, + stopMediaRecording: { + }, + lockMediaRecording: { + }, + resumeMediaRecording: { + }, + deleteRecordedMedia: { + }, + sendRecordedMedia: { _, _ in + }, + displayRestrictedInfo: { _, _ in + }, + displayVideoUnmuteTip: { _ in + }, + switchMediaRecordingMode: { + }, + setupMessageAutoremoveTimeout: { + }, + sendSticker: { _, _, _, _, _, _ in + return false + }, + unblockPeer: { + }, + pinMessage: { _, _ in + }, + unpinMessage: { _, _, _ in + }, + unpinAllMessages: { + }, + openPinnedList: { _ in + }, + shareAccountContact: { + }, + reportPeer: { + }, + presentPeerContact: { + }, + dismissReportPeer: { + }, + deleteChat: { + }, + beginCall: { _ in + }, + toggleMessageStickerStarred: { _ in + }, + presentController: { _, _ in + }, + presentControllerInCurrent: { _, _ in + }, + getNavigationController: { + return nil + }, + presentGlobalOverlayController: { _, _ in + }, + navigateFeed: { + }, + openGrouping: { + }, + toggleSilentPost: { + }, + requestUnvoteInMessage: { _ in + }, + requestStopPollInMessage: { _ in + }, + updateInputLanguage: { _ in + }, + unarchiveChat: { + }, + openLinkEditing: { + }, + displaySlowmodeTooltip: { _, _ in + }, + displaySendMessageOptions: { [weak self] node, gesture in + guard let self, let component = self.component else { + return + } + + component.sendContextAction?(node.view, gesture) + }, + openScheduledMessages: { + }, + openPeersNearby: { + }, + displaySearchResultsTooltip: { _, _ in + }, + unarchivePeer: { + }, + scrollToTop: { + }, + viewReplies: { _, _ in + }, + activatePinnedListPreview: { _, _ in + }, + joinGroupCall: { _ in + }, + presentInviteMembers: { + }, + presentGigagroupHelp: { + }, + openMonoforum: { + }, + editMessageMedia: { _, _ in + }, + updateShowCommands: { _ in + }, + updateShowSendAsPeers: { _ in + }, + openInviteRequests: { + }, + openSendAsPeer: { _, _ in + }, + presentChatRequestAdminInfo: { + }, + displayCopyProtectionTip: { _, _ in + }, + openWebView: { _, _, _, _ in + }, + updateShowWebView: { _ in + }, + insertText: { _ in + }, + backwardsDeleteText: { + }, + restartTopic: { + }, + toggleTranslation: { _ in + }, + changeTranslationLanguage: { _ in + }, + addDoNotTranslateLanguage: { _ in + }, + hideTranslationPanel: { + }, + openPremiumGift: { + }, + openSuggestPost: { [weak self] _, _ in + guard let self, let component = self.component else { + return + } + for action in component.inlineActions { + if case .paidMessage = action.kind { + action.action() + break + } + } + }, + openPremiumRequiredForMessaging: { + }, + openStarsPurchase: { _ in + }, + openMessagePayment: { + }, + openBoostToUnrestrict: { + }, + updateRecordingTrimRange: { _, _, _, _ in + }, + dismissAllTooltips: { + }, + editTodoMessage: { _, _, _ in + }, + dismissUrlPreview: { + }, + dismissForwardMessages: { + }, + dismissSuggestPost: { + }, + displayUndo: { _ in + }, + sendEmoji: { _, _, _ in + }, + updateHistoryFilter: { _ in + }, + updateChatLocationThread: { _, _ in + }, + toggleChatSidebarMode: { + }, + updateDisplayHistoryFilterAsList: { _ in + }, + requestLayout: { _ in + }, + chatController: { + return nil + }, + statuses: nil + ) + } + + var presentationInterfaceState = ChatPresentationInterfaceState( + chatWallpaper: .color(0), + theme: component.theme, + strings: component.strings, + dateTimeFormat: PresentationDateTimeFormat(), + nameDisplayOrder: .firstLast, + limitsConfiguration: component.context.currentLimitsConfiguration.with({ $0 }), + fontSize: .regular, + bubbleCorners: PresentationChatBubbleCorners( + mainRadius: 16.0, + auxiliaryRadius: 8.0, + mergeBubbleCorners: true + ), + accountPeerId: component.context.account.peerId, + mode: .standard(.default), + chatLocation: .peer(id: component.chatPeerId), + subject: nil, + peerNearbyData: nil, + greetingData: nil, + pendingUnpinnedAllMessages: false, + activeGroupCallInfo: nil, + hasActiveGroupCall: false, + importState: nil, + threadData: nil, + isGeneralThreadClosed: false, + replyMessage: nil, + accountPeerColor: nil, + businessIntro: nil + ) + + var inputAccessoryItems: [ChatTextInputAccessoryItem] = [] + for inlineAction in component.inlineActions { + switch inlineAction.kind { + case .paidMessage: + inputAccessoryItems.append(.suggestPost) + case let .inputMode(inputMode): + let mappedInputMode: ChatTextInputAccessoryItem.InputMode + switch inputMode { + case .emoji: + mappedInputMode = .emoji + case .stickers: + mappedInputMode = .stickers + case .text: + mappedInputMode = .keyboard + case .commands: + mappedInputMode = .bot + } + inputAccessoryItems.append(.input(isEnabled: true, inputMode: mappedInputMode)) + } + } + presentationInterfaceState = presentationInterfaceState.updatedInputTextPanelState { _ in + return ChatTextInputPanelState( + accessoryItems: inputAccessoryItems, + contextPlaceholder: nil, + mediaRecordingState: nil + ) + } + presentationInterfaceState = presentationInterfaceState.updatedSendPaidMessageStars(component.paidMessagePrice) + + let panelNode: ChatTextInputPanelNode + if let current = self.panelNode { + panelNode = current + } else { + panelNode = ChatTextInputPanelNode( + context: component.context, + presentationInterfaceState: presentationInterfaceState, + presentationContext: ChatPresentationContext( + context: component.context, + backgroundNode: nil + ), + presentController: { c in + + } + ) + self.panelNode = panelNode + self.addSubview(panelNode.view) + panelNode.interfaceInteraction = self.interfaceInteraction + panelNode.loadTextInputNodeIfNeeded() + + panelNode.sendMessage = { [weak self] in + guard let self, let component = self.component else { + return + } + component.sendAction?() + } + panelNode.updateHeight = { [weak self] _ in + guard let self else { + return + } + if !self.isUpdating { + self.state?.updated(transition: .spring(duration: 0.4)) + } + } + panelNode.displayAttachmentMenu = { [weak self] in + guard let self, let component = self.component else { + return + } + if let leftAction = component.leftAction { + leftAction.action() + } + } + } + + if let textView = panelNode.textInputNode?.textView { + if component.hideKeyboard { + if textView.inputView == nil { + textView.inputView = EmptyInputView() + textView.reloadInputViews() + } + } else if textView.inputView != nil { + textView.inputView = nil + textView.reloadInputViews() + } + } + + panelNode.customPlaceholder = component.placeholder + + if let leftAction = component.leftAction { + switch leftAction.kind { + case .attach: + panelNode.customLeftAction = nil + case let .toggleExpanded(isVisible, isExpanded): + panelNode.customLeftAction = .toggleExpanded(isVisible: isVisible, isExpanded: isExpanded) + } + } else { + panelNode.customLeftAction = nil + } + + if let rightAction = component.rightAction { + switch rightAction.kind { + case let .stars(count, isFilled): + panelNode.customRightAction = .stars(count: count, isFilled: isFilled, action: { + rightAction.action() + }) + } + } else { + panelNode.customRightAction = nil + } + + panelNode.customSendColor = component.sendColor + + if let resetInputState = component.externalState.resetInputState { + component.externalState.resetInputState = nil + let _ = resetInputState + panelNode.text = "" + } + + let panelHeight = panelNode.updateLayout( + width: availableSize.width, + leftInset: component.insets.left, + rightInset: component.insets.right, + bottomInset: component.insets.bottom, + additionalSideInsets: UIEdgeInsets(), + maxHeight: component.maxHeight, + maxOverlayHeight: component.maxHeight, + isSecondary: false, + transition: transition.containedViewLayoutTransition, + interfaceState: presentationInterfaceState, + metrics: LayoutMetrics(widthClass: .compact, heightClass: .compact, orientation: nil), + isMediaInputExpanded: false + ) + + let panelSize = CGSize(width: availableSize.width, height: panelHeight) + let panelFrame = CGRect(origin: CGPoint(), size: panelSize) + + transition.setFrame(view: panelNode.view, frame: panelFrame) + + return panelSize + } + } + + public func makeView() -> View { + return View() + } + + 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) + } +} diff --git a/submodules/TelegramUI/Components/Chat/ChatTextInputPanelNode/Sources/ChatTextInputPanelNode.swift b/submodules/TelegramUI/Components/Chat/ChatTextInputPanelNode/Sources/ChatTextInputPanelNode.swift index 5757aaec60..143dd4b16d 100644 --- a/submodules/TelegramUI/Components/Chat/ChatTextInputPanelNode/Sources/ChatTextInputPanelNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatTextInputPanelNode/Sources/ChatTextInputPanelNode.swift @@ -23,7 +23,6 @@ import TextInputMenu import Pasteboard import ChatPresentationInterfaceState import ManagedAnimationNode -import AttachmentUI import EditableChatTextNode import EmojiTextAttachmentView import LottieAnimationComponent @@ -375,6 +374,63 @@ public class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDeleg } } } + + public enum LeftAction { + case toggleExpanded(isVisible: Bool, isExpanded: Bool) + } + + public enum RightAction { + case stars(count: Int, isFilled: Bool, action: () -> Void) + } + + public var customPlaceholder: String? + public var customLeftAction: LeftAction? + public var customRightAction: RightAction? + public var customSendColor: UIColor? + + private var starReactionButton: ComponentView? + + public func insertText(text: NSAttributedString) { + guard let textInputState = self.presentationInterfaceState?.interfaceState.effectiveInputState else { + return + } + + let inputText = NSMutableAttributedString(attributedString: textInputState.inputText) + + let range = textInputState.selectionRange + + let updatedText = NSMutableAttributedString(attributedString: text) + if range.lowerBound < inputText.length { + if let quote = inputText.attribute(ChatTextInputAttributes.block, at: range.lowerBound, effectiveRange: nil) { + updatedText.addAttribute(ChatTextInputAttributes.block, value: quote, range: NSRange(location: 0, length: updatedText.length)) + } + } + inputText.replaceCharacters(in: NSMakeRange(range.lowerBound, range.count), with: updatedText) + + let selectionPosition = range.lowerBound + (updatedText.string as NSString).length + let updatedState = ChatTextInputState(inputText: inputText, selectionRange: selectionPosition ..< selectionPosition) + + if let textInputNode = self.textInputNode, let context = self.context { + var textColor: UIColor = .black + var accentTextColor: UIColor = .blue + var baseFontSize: CGFloat = 17.0 + + if let presentationInterfaceState = self.presentationInterfaceState { + textColor = presentationInterfaceState.theme.chat.inputPanel.inputTextColor + accentTextColor = presentationInterfaceState.theme.chat.inputPanel.panelControlAccentColor + baseFontSize = max(minInputFontSize, presentationInterfaceState.fontSize.baseDisplaySize) + } + if "".isEmpty { + baseFontSize = 17.0 + } + + textInputNode.attributedText = textAttributedStringForStateText(context: context, stateText: updatedState.inputText, fontSize: baseFontSize, textColor: textColor, accentTextColor: accentTextColor, writingDirection: nil, spoilersRevealed: self.spoilersRevealed, availableEmojis: Set(context.animatedEmojiStickersValue.keys), emojiViewProvider: self.emojiViewProvider, makeCollapsedQuoteAttachment: { text, attributes in + return ChatInputTextCollapsedQuoteAttachmentImpl(text: text, attributes: attributes) + }) + textInputNode.selectedRange = NSMakeRange(updatedState.selectionRange.lowerBound, updatedState.selectionRange.count) + self.chatInputTextNodeDidUpdateText() + } + } public func updateInputTextState(_ state: ChatTextInputState, keepSendButtonEnabled: Bool, extendedSearchLayout: Bool, accessoryItems: [ChatTextInputAccessoryItem], animated: Bool) { if self.ignoreInputStateUpdates { @@ -1276,6 +1332,8 @@ public class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDeleg metrics: LayoutMetrics, isMediaInputExpanded: Bool ) -> CGFloat { + let isFirstTime = self.validLayout == nil + let previousAdditionalSideInsets = self.validLayout?.4 self.validLayout = (width, leftInset, rightInset, bottomInset, additionalSideInsets, maxHeight, maxOverlayHeight, metrics, isSecondary, isMediaInputExpanded) @@ -1288,6 +1346,8 @@ public class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDeleg } let placeholderColor: UIColor = interfaceState.theme.chat.inputPanel.inputPlaceholderColor + + self.sendActionButtons.customSendColor = self.customSendColor var transition = transition var additionalOffset: CGFloat = 0.0 @@ -1555,7 +1615,7 @@ public class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDeleg var buttonTitleUpdated = false var menuTextSize = self.menuButtonTextNode.frame.size - if self.presentationInterfaceState != interfaceState { + if self.presentationInterfaceState != interfaceState || isFirstTime { let previousState = self.presentationInterfaceState self.presentationInterfaceState = interfaceState @@ -1655,7 +1715,13 @@ public class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDeleg self.theme = interfaceState.theme - if interfaceState.interfaceState.mediaDraftState != nil { + if let customLeftAction = self.customLeftAction { + switch customLeftAction { + case .toggleExpanded: + self.attachmentButtonIcon.image = UIImage(bundleImageName: "Chat/Context Menu/ReactionExpandArrow")?.withRenderingMode(.alwaysTemplate) + self.attachmentButtonIcon.tintColor = interfaceState.theme.chat.inputPanel.panelControlColor + } + } else if interfaceState.interfaceState.mediaDraftState != nil { self.attachmentButtonIcon.image = UIImage(bundleImageName: "Chat/Context Menu/Delete")?.withRenderingMode(.alwaysTemplate) self.attachmentButtonIcon.tintColor = interfaceState.theme.chat.inputPanel.panelControlColor } else if isEditingMedia { @@ -1688,8 +1754,15 @@ public class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDeleg } } - if wasEditingMedia != isEditingMedia || hadMediaDraft != hasMediaDraft { - if interfaceState.interfaceState.mediaDraftState != nil { + if wasEditingMedia != isEditingMedia || hadMediaDraft != hasMediaDraft || isFirstTime { + + if let customLeftAction = self.customLeftAction { + switch customLeftAction { + case .toggleExpanded: + self.attachmentButtonIcon.image = UIImage(bundleImageName: "Chat/Context Menu/ReactionExpandArrow")?.withRenderingMode(.alwaysTemplate) + self.attachmentButtonIcon.tintColor = interfaceState.theme.chat.inputPanel.panelControlColor + } + } else if interfaceState.interfaceState.mediaDraftState != nil { self.attachmentButtonIcon.image = UIImage(bundleImageName: "Chat/Context Menu/Delete")?.withRenderingMode(.alwaysTemplate) self.attachmentButtonIcon.tintColor = interfaceState.theme.chat.inputPanel.panelControlColor } else if isEditingMedia { @@ -1710,6 +1783,20 @@ public class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDeleg peerUpdated = true } + if let customLeftAction = self.customLeftAction { + switch customLeftAction { + case let .toggleExpanded(_, isExpanded): + var iconTransform = CATransform3DIdentity + iconTransform = CATransform3DTranslate(iconTransform, 0.0, 1.0, 0.0) + if isExpanded || "".isEmpty { + iconTransform = CATransform3DRotate(iconTransform, CGFloat.pi, 0.0, 0.0, 1.0) + } + transition.updateTransform(layer: self.attachmentButtonIcon.layer, transform: iconTransform) + } + } else { + self.attachmentButtonIcon.layer.transform = CATransform3DIdentity + } + if peerUpdated || previousState?.chatLocation != interfaceState.chatLocation || previousState?.interfaceState.silentPosting != interfaceState.interfaceState.silentPosting || themeUpdated || !self.initializedPlaceholder || previousState?.keyboardButtonsMessage?.id != interfaceState.keyboardButtonsMessage?.id || previousState?.keyboardButtonsMessage?.visibleReplyMarkupPlaceholder != interfaceState.keyboardButtonsMessage?.visibleReplyMarkupPlaceholder || dismissedButtonMessageUpdated || replyMessageUpdated || (previousState?.interfaceState.editMessage == nil) != (interfaceState.interfaceState.editMessage == nil) || previousState?.forumTopicData != interfaceState.forumTopicData || previousState?.replyMessage?.id != interfaceState.replyMessage?.id || previousState?.sendPaidMessageStars != interfaceState.sendPaidMessageStars { self.initializedPlaceholder = true @@ -1788,6 +1875,10 @@ public class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDeleg self.sendActionButtons.sendButtonLongPressEnabled = !isScheduledMessages } + if let customPlaceholder = self.customPlaceholder { + updatedPlaceholder = customPlaceholder + } + var sendButtonHasApplyIcon = interfaceState.interfaceState.editMessage != nil if let interfaceState = self.presentationInterfaceState { if case let .customChatContents(customChatContents) = interfaceState.subject { @@ -1923,6 +2014,8 @@ 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 { + attachmentButtonX = -48.0 } self.mediaActionButtons.micButton.updateMode(mode: interfaceState.interfaceState.mediaRecordingMode, animated: transition.isAnimated) @@ -2018,6 +2111,9 @@ public class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDeleg if mediaRecordingState != nil { textFieldInsets.left = 8.0 } + if let customLeftAction = self.customLeftAction, case let .toggleExpanded(isVisible, _) = customLeftAction, !isVisible { + textFieldInsets.left = 8.0 + } var audioRecordingItemsAlpha: CGFloat = 1.0 if interfaceState.interfaceState.mediaDraftState != nil { @@ -2646,6 +2742,50 @@ public class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDeleg 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) } + 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 + } + 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) + } + } + 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) + } + } + var sendActionButtonsFrame = CGRect(origin: CGPoint(x: textInputContainerBackgroundFrame.maxX - sendActionButtonsSize.width, y: textInputContainerBackgroundFrame.maxY - sendActionButtonsSize.height), size: sendActionButtonsSize) let sendActionsScale: CGFloat @@ -3764,6 +3904,10 @@ public class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDeleg var hideMicButton = false var hideMicButtonBackground = false + if self.customRightAction != nil { + self.mediaActionButtons.isHidden = true + } + var mediaInputIsActive = false var keepSendButtonEnabled = self.keepSendButtonEnabled var hasForward = false @@ -4849,7 +4993,36 @@ public class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDeleg ) } - public func makeAttachmentMenuTransition(accessoryPanelNode: ASDisplayNode?) -> AttachmentController.InputPanelTransition { - return AttachmentController.InputPanelTransition(inputNode: self, accessoryPanelNode: accessoryPanelNode, menuButtonNode: self.menuButton, menuButtonBackgroundView: self.menuButtonBackgroundView, menuIconNode: self.menuButtonIconNode, menuTextNode: self.menuButtonTextNode, prepareForDismiss: { self.menuButtonIconNode.enqueueState(.app, animated: false) }) + public final class AttachmentInputPanelTransition { + public let inputNode: ASDisplayNode + public let accessoryPanelNode: ASDisplayNode? + public let menuButtonNode: ASDisplayNode + public let menuButtonBackgroundView: UIView + public let menuIconNode: ASDisplayNode + public let menuTextNode: ASDisplayNode + public let prepareForDismiss: () -> Void + + public init( + inputNode: ASDisplayNode, + accessoryPanelNode: ASDisplayNode?, + menuButtonNode: ASDisplayNode, + menuButtonBackgroundView: UIView, + menuIconNode: ASDisplayNode, + menuTextNode: ASDisplayNode, + prepareForDismiss: @escaping () -> Void + ) { + self.inputNode = inputNode + self.accessoryPanelNode = accessoryPanelNode + self.menuButtonNode = menuButtonNode + self.menuButtonBackgroundView = menuButtonBackgroundView + self.menuIconNode = menuIconNode + self.menuTextNode = menuTextNode + self.prepareForDismiss = prepareForDismiss + } + } + + + public func makeAttachmentMenuTransition(accessoryPanelNode: ASDisplayNode?) -> AttachmentInputPanelTransition { + return AttachmentInputPanelTransition(inputNode: self, accessoryPanelNode: accessoryPanelNode, menuButtonNode: self.menuButton, menuButtonBackgroundView: self.menuButtonBackgroundView, menuIconNode: self.menuButtonIconNode, menuTextNode: self.menuButtonTextNode, prepareForDismiss: { self.menuButtonIconNode.enqueueState(.app, animated: false) }) } } diff --git a/submodules/TelegramUI/Components/Chat/ChatTextInputPanelNode/Sources/StarReactionButtonComponent.swift b/submodules/TelegramUI/Components/Chat/ChatTextInputPanelNode/Sources/StarReactionButtonComponent.swift new file mode 100644 index 0000000000..fe4c605608 --- /dev/null +++ b/submodules/TelegramUI/Components/Chat/ChatTextInputPanelNode/Sources/StarReactionButtonComponent.swift @@ -0,0 +1,99 @@ +import Foundation +import UIKit +import Display +import TelegramPresentationData +import ComponentFlow +import GlassBackgroundComponent + +final class StarReactionButtonComponent: Component { + let theme: PresentationTheme + let count: Int + let isFilled: Bool + let action: () -> Void + + init( + theme: PresentationTheme, + count: Int, + isFilled: Bool, + action: @escaping () -> Void + ) { + self.theme = theme + self.count = count + self.isFilled = isFilled + self.action = action + } + + static func ==(lhs: StarReactionButtonComponent, rhs: StarReactionButtonComponent) -> Bool { + if lhs.theme !== rhs.theme { + return false + } + if lhs.count != rhs.count { + return false + } + if lhs.isFilled != rhs.isFilled { + return false + } + return true + } + + final class View: UIView { + private let backgroundView: GlassBackgroundView + private let iconView: UIImageView + private let button: HighlightTrackingButton + + private var component: StarReactionButtonComponent? + private weak var state: EmptyComponentState? + + 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) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + @objc private func buttonPressed() { + } + + 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) + + if self.iconView.image == nil { + self.iconView.image = UIImage(bundleImageName: "Premium/Stars/ButtonStar")?.withRenderingMode(.alwaysTemplate) + } + + 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)) + transition.setFrame(view: self.iconView, frame: iconFrame) + } + + 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) + } +} diff --git a/submodules/TelegramUI/Components/GlassBackgroundComponent/Sources/GlassBackgroundComponent.swift b/submodules/TelegramUI/Components/GlassBackgroundComponent/Sources/GlassBackgroundComponent.swift index c0ce3d9f34..b0f1da67ad 100644 --- a/submodules/TelegramUI/Components/GlassBackgroundComponent/Sources/GlassBackgroundComponent.swift +++ b/submodules/TelegramUI/Components/GlassBackgroundComponent/Sources/GlassBackgroundComponent.swift @@ -469,8 +469,6 @@ public class GlassBackgroundView: UIView { nativeParamsView.lumaMin = 0.25 nativeParamsView.lumaMax = 1.0 } - - nativeView.overrideUserInterfaceStyle = isDark ? .dark : .light } } } diff --git a/submodules/TelegramUI/Components/MessageInputPanelComponent/BUILD b/submodules/TelegramUI/Components/MessageInputPanelComponent/BUILD index c6366615c5..cf37d6ad06 100644 --- a/submodules/TelegramUI/Components/MessageInputPanelComponent/BUILD +++ b/submodules/TelegramUI/Components/MessageInputPanelComponent/BUILD @@ -42,6 +42,7 @@ swift_library( "//submodules/TelegramUI/Components/Stories/ForwardInfoPanelComponent", "//submodules/TelegramUI/Components/PlainButtonComponent", "//submodules/TelegramUI/Components/GlassBackgroundComponent", + "//submodules/TelegramUI/Components/Chat/ChatTextInputPanelNode", ], visibility = [ "//visibility:public", diff --git a/submodules/TelegramUI/Components/MessageInputPanelComponent/Sources/MessageInputPanelComponent.swift b/submodules/TelegramUI/Components/MessageInputPanelComponent/Sources/MessageInputPanelComponent.swift index 785fe06cb8..1b8bc3d897 100644 --- a/submodules/TelegramUI/Components/MessageInputPanelComponent/Sources/MessageInputPanelComponent.swift +++ b/submodules/TelegramUI/Components/MessageInputPanelComponent/Sources/MessageInputPanelComponent.swift @@ -23,11 +23,35 @@ import ForwardInfoPanelComponent import MultilineTextComponent import PlainButtonComponent import GlassBackgroundComponent +import ChatTextInputPanelNode 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 @@ -228,6 +252,8 @@ public final class MessageInputPanelComponent: Component { public let isChannel: Bool public let storyItem: EngineStoryItem? public let chatLocation: ChatLocation? + public let isLiveChatExpanded: Bool? + public let toggleLiveChatExpanded: (() -> Void)? public init( externalState: ExternalState, @@ -287,7 +313,9 @@ public final class MessageInputPanelComponent: Component { header: AnyComponent?, isChannel: Bool, storyItem: EngineStoryItem?, - chatLocation: ChatLocation? + chatLocation: ChatLocation?, + isLiveChatExpanded: Bool? = nil, + toggleLiveChatExpanded: (() -> Void)? = nil ) { self.externalState = externalState self.context = context @@ -347,6 +375,8 @@ public final class MessageInputPanelComponent: Component { self.isChannel = isChannel self.storyItem = storyItem self.chatLocation = chatLocation + self.isLiveChatExpanded = isLiveChatExpanded + self.toggleLiveChatExpanded = toggleLiveChatExpanded } public static func ==(lhs: MessageInputPanelComponent, rhs: MessageInputPanelComponent) -> Bool { @@ -473,6 +503,9 @@ public final class MessageInputPanelComponent: Component { if lhs.chatLocation != rhs.chatLocation { return false } + if lhs.isLiveChatExpanded != rhs.isLiveChatExpanded { + return false + } return true } @@ -481,6 +514,9 @@ public final class MessageInputPanelComponent: Component { } public final class View: UIView { + private var inputPanel: ComponentView? + private let textInputPanelExternalState = ChatTextInputPanelComponent.ExternalState() + private let fieldBackgroundView: BlurredBackgroundView private let fieldBackgroundTint: UIView private var fieldGlassBackgroundView: GlassBackgroundView? @@ -612,6 +648,11 @@ public final class MessageInputPanelComponent: Component { } public func getSendMessageInput() -> SendMessageInput { + if let inputPanelView = self.inputPanel?.view as? ChatTextInputPanelComponent.View { + let _ = inputPanelView + return .text(expandedInputStateAttributedString(self.textInputPanelExternalState.textInputState.inputText)) + } + guard let textFieldView = self.textField.view as? TextFieldComponent.View else { return .text(NSAttributedString()) } @@ -638,6 +679,15 @@ public final class MessageInputPanelComponent: Component { } public func clearSendMessageInput(updateState: Bool) { + if let inputPanelView = self.inputPanel?.view as? ChatTextInputPanelComponent.View { + let _ = inputPanelView + self.textInputPanelExternalState.resetInputState = ChatTextInputState() + if updateState { + inputPanelView.updateState(transition: .spring(duration: 0.4)) + } + return + } + if let textFieldView = self.textField.view as? TextFieldComponent.View { textFieldView.setAttributedText(NSAttributedString(), updateState: updateState) } @@ -802,6 +852,152 @@ public final class MessageInputPanelComponent: Component { } func update(component: MessageInputPanelComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { + self.component = component + self.state = state + + if case .story = component.style, case .liveStream = component.storyItem?.media { + let inputPanel: ComponentView + if let current = self.inputPanel { + inputPanel = current + } else { + inputPanel = ComponentView() + self.inputPanel = inputPanel + + for subview in Array(self.subviews) { + subview.removeFromSuperview() + } + } + + let inputMode = component.nextInputMode(self.textInputPanelExternalState.textInputState.inputText.length != 0) + self.currentInputMode = inputMode + + var inlineActions: [ChatTextInputPanelComponent.InlineAction] = [] + if component.paidMessageAction != nil && self.textInputPanelExternalState.textInputState.inputText.length == 0 { + inlineActions.append(ChatTextInputPanelComponent.InlineAction( + kind: .paidMessage, + action: { [weak self] in + guard let self else { + return + } + self.component?.paidMessageAction?() + } + )) + } else if let inputMode { + let mappedInputMode: ChatTextInputPanelComponent.InputMode + switch inputMode { + case .text: + mappedInputMode = .text + case .emoji: + mappedInputMode = .emoji + case .stickers: + mappedInputMode = .stickers + } + inlineActions.append(ChatTextInputPanelComponent.InlineAction( + kind: .inputMode(mappedInputMode), + action: { [weak self] in + guard let self, let component = self.component else { + return + } + component.inputModeAction?() + } + )) + } + + let placeholder: String + switch component.placeholder { + case let .counter(items): + placeholder = items.map({ item -> String in + switch item.content { + case let .number(value, minDigits): + var result = "\(value)" + while result.count < minDigits { + result.insert("0", at: result.startIndex) + } + return result + case let .text(text): + return text + } + }).joined(separator: "") + case let .plain(text): + placeholder = text + } + + let inputPanelSize = inputPanel.update( + transition: transition, + component: AnyComponent(ChatTextInputPanelComponent( + externalState: self.textInputPanelExternalState, + context: component.context, + theme: component.theme, + 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 + guard let self, let component = self.component else { + return + } + component.toggleLiveChatExpanded?() + }), + rightAction: ChatTextInputPanelComponent.RightAction(kind: .stars(count: 0, isFilled: false), action: { + }), + placeholder: placeholder, + paidMessagePrice: component.sendPaidMessageStars, + sendColor: component.sendPaidMessageStars.flatMap { value in + return getStarAmountColorMapping(value: value.value) + }, + hideKeyboard: component.hideKeyboard, + insets: UIEdgeInsets(top: 0.0, left: 0.0, bottom: component.bottomInset, right: 0.0), + maxHeight: availableSize.height, + sendAction: { [weak self] in + guard let self, let component = self.component else { + return + } + component.sendMessageAction(nil) + }, + sendContextAction: component.sendMessageOptionsAction == nil ? nil : { [weak self] view, gesture in + guard let self, let component = self.component else { + return + } + component.sendMessageOptionsAction?(view, gesture) + } + )), + environment: {}, + containerSize: availableSize + ) + let inputPanelFrame = CGRect(origin: CGPoint(), size: inputPanelSize) + if let inputPanelView = inputPanel.view { + if inputPanelView.superview == nil { + inputPanel.parentState = state + self.addSubview(inputPanelView) + } + transition.setFrame(view: inputPanelView, frame: inputPanelFrame) + } + + component.externalState.isEditing = self.textInputPanelExternalState.isEditing + component.externalState.hasText = self.textInputPanelExternalState.textInputState.inputText.length != 0 + component.externalState.isKeyboardHidden = component.hideKeyboard + component.externalState.insertText = { [weak self] text in + guard let self, let inputPanelView = self.inputPanel?.view as? ChatTextInputPanelComponent.View else { + return + } + inputPanelView.insertText(text: text) + } + component.externalState.deleteBackward = { [weak self] in + guard let self, let inputPanelView = self.inputPanel?.view as? ChatTextInputPanelComponent.View else { + return + } + inputPanelView.deleteBackward() + } + + var size = inputPanelSize + if component.bottomInset <= 32.0 { + size.height += 7.0 + } else { + size.height += 4.0 + } + + return size + } + let previousPlaceholder = self.component?.placeholder let defaultInsets = UIEdgeInsets(top: 14.0, left: 9.0, bottom: 6.0, right: 41.0) @@ -839,9 +1035,6 @@ public final class MessageInputPanelComponent: Component { if transition.animation.isImmediate, let previousComponent, previousComponent.storyItem?.id == component.storyItem?.id, component.isChannel { transition = transition.withAnimation(.curve(duration: 0.3, curve: .spring)) } - - self.component = component - self.state = state if let initialText = component.externalState.initialText { component.externalState.initialText = nil diff --git a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryContentLiveChatComponent.swift b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryContentLiveChatComponent.swift index fbacc9adb2..385bbc5bc8 100644 --- a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryContentLiveChatComponent.swift +++ b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryContentLiveChatComponent.swift @@ -14,18 +14,21 @@ import AvatarNode 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) { + 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 { @@ -48,10 +51,14 @@ private final class MessageItemComponent: Component { } 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? @@ -61,9 +68,23 @@ private final class MessageItemComponent: Component { 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) { @@ -94,7 +115,9 @@ private final class MessageItemComponent: Component { self.component = component self.state = state - let insets = UIEdgeInsets(top: 8.0, left: 20.0, bottom: 8.0, right: 20.0) + self.containerNode.isGestureEnabled = component.contextGesture != nil + + let insets = UIEdgeInsets(top: 8.0, left: 8.0, bottom: 8.0, right: 8.0) let avatarSize: CGFloat = 24.0 let avatarSpacing: CGFloat = 6.0 @@ -127,7 +150,7 @@ private final class MessageItemComponent: Component { } else { avatarNode = AvatarNode(font: avatarPlaceholderFont(size: 10.0)) self.avatarNode = avatarNode - self.contentContainer.addSubview(avatarNode.view) + self.extractedContainerNode.contentNode.view.addSubview(avatarNode.view) } transition.setFrame(view: avatarNode.view, frame: avatarFrame) avatarNode.updateSize(size: avatarFrame.size) @@ -146,12 +169,14 @@ private final class MessageItemComponent: Component { if let textView = self.text.view { if textView.superview == nil { textView.layer.anchorPoint = CGPoint() - self.contentContainer.addSubview(textView) + self.extractedContainerNode.contentNode.view.addSubview(textView) } transition.setPosition(view: textView, position: textFrame.origin) textView.bounds = CGRect(origin: CGPoint(), size: textFrame.size) } + let backgroundFrame = CGRect(origin: CGPoint(x: 6.0, y: 2.0), size: CGSize(width: textFrame.maxX + 8.0 - 6.0, height: textFrame.maxY + 3.0)) + if let paidStars = component.message.paidStars { let backgroundView: UIImageView if let current = self.backgroundView { @@ -159,21 +184,44 @@ private final class MessageItemComponent: Component { } else { backgroundView = UIImageView() self.backgroundView = backgroundView - self.contentContainer.insertSubview(backgroundView, at: 0) + self.extractedContainerNode.contentNode.view.insertSubview(backgroundView, at: 0) backgroundView.image = generateStretchableFilledCircleImage(diameter: 28.0, color: .white)?.withRenderingMode(.alwaysTemplate) } - let backgroundFrame = CGRect(origin: CGPoint(x: 16.0, y: 2.0), size: CGSize(width: textFrame.maxX + 8.0 - 16.0, height: textFrame.maxY + 3.0)) 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 + self.containerNode.frame = CGRect(origin: CGPoint(), size: size) + return size } } @@ -188,6 +236,7 @@ private final class MessageItemComponent: Component { } private func getStarAmountColorMapping(value: Int64) -> UIColor { + //TODO:localize unify if value >= 10000 { return UIColor(rgb: 0x7C8695) } @@ -245,6 +294,7 @@ private final class PinnedBarMessageComponent: Component { private let backgroundView: UIImageView private let foregroundClippingView: UIView private let foregroundView: UIImageView + private let effectLayer: StarsButtonEffectLayer private var avatarNode: AvatarNode? private let title = ComponentView() @@ -260,6 +310,8 @@ private final class PinnedBarMessageComponent: Component { self.foregroundClippingView = UIView() self.foregroundClippingView.clipsToBounds = true self.foregroundView = UIImageView() + self.effectLayer = StarsButtonEffectLayer() + self.effectLayer.masksToBounds = true super.init(frame: frame) @@ -267,6 +319,7 @@ private final class PinnedBarMessageComponent: Component { self.foregroundClippingView.addSubview(self.foregroundView) self.addSubview(self.foregroundClippingView) + self.layer.addSublayer(self.effectLayer) } required init?(coder: NSCoder) { @@ -343,6 +396,10 @@ private final class PinnedBarMessageComponent: Component { transition.setFrame(view: self.foregroundView, frame: CGRect(origin: CGPoint(), size: size)) 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) + let avatarFrame = CGRect(origin: CGPoint(x: avatarInset, y: floor((itemHeight - avatarSize) * 0.5)), size: CGSize(width: avatarSize, height: avatarSize)) do { let avatarNode: AvatarNode @@ -376,7 +433,7 @@ private final class PinnedBarMessageComponent: Component { titleView.bounds = CGRect(origin: CGPoint(), size: titleFrame.size) } - return size + return CGSize(width: size.width + 10.0, height: size.height) } } @@ -452,12 +509,6 @@ private final class PinnedBarComponent: Component { super.init(frame: frame) self.addSubview(self.listContainer) - - self.toggleButtonBackground.contentView.addSubview(self.toggleButtonIcon) - self.toggleButtonBackground.contentView.addSubview(self.toggleButton) - self.addSubview(self.toggleButtonBackground) - - self.toggleButton.addTarget(self, action: #selector(self.toggleButtonPressed), for: .touchUpInside) } required init?(coder: NSCoder) { @@ -501,22 +552,6 @@ private final class PinnedBarComponent: Component { let size = CGSize(width: availableSize.width, height: insets.top + itemHeight + insets.bottom) - let toggleButtonFrame = CGRect(origin: CGPoint(x: insets.left, y: insets.top), size: CGSize(width: itemHeight, height: itemHeight)) - transition.setFrame(view: self.toggleButtonBackground, frame: toggleButtonFrame) - self.toggleButtonBackground.update(size: toggleButtonFrame.size, cornerRadius: toggleButtonFrame.height * 0.5, isDark: true, tintColor: .init(kind: .panel, color: component.theme.chat.inputPanel.inputBackgroundColor.withMultipliedAlpha(0.7)), isInteractive: true, transition: transition) - transition.setFrame(view: self.toggleButton, frame: CGRect(origin: CGPoint(), size: toggleButtonFrame.size)) - if self.toggleButtonIcon.image == nil { - self.toggleButtonIcon.image = UIImage(bundleImageName: "Chat/Context Menu/ReactionExpandArrow")?.withRenderingMode(.alwaysTemplate) - self.toggleButtonIcon.tintColor = .white - } - if let image = self.toggleButtonIcon.image { - var iconFrame = image.size.centered(in: CGRect(origin: CGPoint(), size: toggleButtonFrame.size)) - iconFrame.origin.y += 1.0 - transition.setTransform(view: self.toggleButtonIcon, transform: CATransform3DMakeRotation((!component.isExpanded) ? CGFloat.pi : 0.0, 0.0, 0.0, 1.0)) - transition.setPosition(view: self.toggleButtonIcon, position: iconFrame.center) - transition.setBounds(view: self.toggleButtonIcon, bounds: CGRect(origin: CGPoint(), size: iconFrame.size)) - } - var listItems: [AnyComponentWithIdentity] = [] for message in component.messages { if let author = message.author { @@ -529,8 +564,8 @@ private final class PinnedBarComponent: Component { } } - let listInsets = UIEdgeInsets(top: 0.0, left: 5.0, bottom: 0.0, right: 5.0 + 20.0) - let listFrame = CGRect(origin: CGPoint(x: toggleButtonFrame.maxX, y: 0.0), size: CGSize(width: size.width - toggleButtonFrame.maxX, height: size.height)) + let listInsets = UIEdgeInsets(top: 0.0, left: 8.0, bottom: 0.0, right: 8.0) + let listFrame = CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: size.width, height: size.height)) let _ = self.list.update( transition: transition, component: AnyComponent(AsyncListComponent( @@ -571,6 +606,7 @@ final class StoryContentLiveChatComponent: Component { let strings: PresentationStrings let theme: PresentationTheme let call: PresentationGroupCall + let storyPeerId: EnginePeer.Id let insets: UIEdgeInsets init( @@ -578,12 +614,14 @@ final class StoryContentLiveChatComponent: Component { strings: PresentationStrings, theme: PresentationTheme, call: PresentationGroupCall, + storyPeerId: EnginePeer.Id, insets: UIEdgeInsets ) { self.context = context self.strings = strings self.theme = theme self.call = call + self.storyPeerId = storyPeerId self.insets = insets } @@ -600,6 +638,9 @@ final class StoryContentLiveChatComponent: Component { if lhs.call !== rhs.call { return false } + if lhs.storyPeerId != rhs.storyPeerId { + return false + } if lhs.insets != rhs.insets { return false } @@ -624,7 +665,13 @@ final class StoryContentLiveChatComponent: Component { private var messagesState: GroupCallMessagesContext.State? private var stateDisposable: Disposable? - private var isChatExpanded: Bool = false + public var isChatEmpty: Bool { + guard let messagesState = self.messagesState else { + return true + } + return messagesState.messages.isEmpty + } + private(set) var isChatExpanded: Bool = false override init(frame: CGRect) { self.listContainer = UIView() @@ -709,6 +756,89 @@ final class StoryContentLiveChatComponent: Component { return result } + func toggleLiveChatExpanded() { + self.isChatExpanded = !self.isChatExpanded + self.state?.updated(transition: .spring(duration: 0.4)) + } + + private func openMessageContextMenu(id: Int64, gesture: ContextGesture, sourceNode: ContextExtractedContentContainingNode) { + Task { @MainActor [weak self] in + guard let self else { + return + } + guard let component = self.component else { + return + } + let presentationData = component.context.sharedContext.currentPresentationData.with({ $0 }).withUpdated(theme: component.theme) + + if let listView = self.list.view as? AsyncListComponent.View { + listView.stopScrolling() + } + + var items: [ContextMenuItem] = [] + //TODO:localize + items.append(.action(ContextMenuActionItem(text: "Copy", textColor: .primary, icon: { theme in generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Copy"), color: theme.contextMenu.primaryColor) }, action: { [weak self] c, _ in + guard let self else { + return + } + + c?.dismiss(completion: { [weak self] in + guard let self else { + return + } + if let messagesState = self.messagesState, let message = messagesState.messages.first(where: { $0.id == id }) { + UIPasteboard.general.string = message.text + } + }) + }))) + + let state = await (component.call.state |> take(1)).get() + if state.canManageCall || component.storyPeerId == component.context.account.peerId { + items.append(.action(ContextMenuActionItem(text: presentationData.strings.ChatList_Context_Delete, textColor: .destructive, icon: { theme in generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Delete"), color: theme.contextMenu.destructiveColor) }, action: { [weak self] c, _ in + guard let self else { + return + } + + c?.dismiss(completion: { [weak self] in + guard let self, let component = self.component else { + return + } + if let call = component.call as? PresentationGroupCallImpl { + call.deleteMessage(id: id) + } + }) + }))) + } + + let contextController = ContextController( + presentationData: presentationData, + source: .extracted(ItemExtractedContentSource( + sourceNode: sourceNode, + containerView: self, + keepInPlace: false + )), + items: .single(ContextController.Items(content: .list(items))), + recognizer: nil, + gesture: gesture + ) + contextController.dismissed = { [weak self] in + guard let self else { + return + } + if let listView = self.list.view { + let transition: ComponentTransition = .easeInOut(duration: 0.2) + transition.setAlpha(view: listView, alpha: 1.0) + } + } + if let listView = self.list.view { + let transition: ComponentTransition = .easeInOut(duration: 0.2) + transition.setAlpha(view: listView, alpha: 0.25) + } + + component.context.sharedContext.mainWindow?.presentInGlobalOverlay(contextController) + } + } + func update(component: StoryContentLiveChatComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { self.isUpdating = true defer { @@ -742,13 +872,22 @@ final class StoryContentLiveChatComponent: Component { var topMessageByPeerId: [EnginePeer.Id: GroupCallMessagesContext.Message] = [:] if let messagesState = self.messagesState { for message in messagesState.messages.reversed() { + let messageId = message.id listItems.append(AnyComponentWithIdentity(id: message.id, component: AnyComponent(MessageItemComponent( context: component.context, strings: component.strings, theme: component.theme, - message: message + message: message, + contextGesture: { [weak self] gesture, sourceNode in + guard let self else { + return + } + self.openMessageContextMenu(id: messageId, gesture: gesture, sourceNode: sourceNode) + } )))) - + } + + for message in messagesState.pinnedMessages.reversed() { if let author = message.author, let paidStars = message.paidStars { if let current = topMessageByPeerId[author.id] { if let currentPaidStars = current.paidStars, currentPaidStars < paidStars { @@ -790,16 +929,19 @@ final class StoryContentLiveChatComponent: Component { environment: {}, containerSize: availableSize ) - let pinnedBarFrame = CGRect(origin: CGPoint(x: 0.0, y: availableSize.height - component.insets.bottom - pinnedBarSize.height), size: pinnedBarSize) + let pinnedBarFrame = CGRect(origin: CGPoint(x: 0.0, y: availableSize.height - component.insets.bottom - pinnedBarSize.height - 4.0), size: pinnedBarSize) if let pinnedBarView = self.pinnedBar.view { if pinnedBarView.superview == nil { self.addSubview(pinnedBarView) } transition.setFrame(view: pinnedBarView, frame: pinnedBarFrame) - transition.setAlpha(view: pinnedBarView, alpha: (listItems.isEmpty && topMessages.isEmpty) ? 0.0 : 1.0) + transition.setAlpha(view: pinnedBarView, alpha: topMessages.isEmpty ? 0.0 : 1.0) } - var listInsets = UIEdgeInsets(top: availableSize.height - pinnedBarFrame.minY, left: component.insets.right, bottom: component.insets.top, right: component.insets.left) + var listInsets = UIEdgeInsets(top: component.insets.bottom + 16.0, left: component.insets.right, bottom: component.insets.top + 8.0, right: component.insets.left) + if !topMessages.isEmpty { + listInsets.top = availableSize.height - pinnedBarFrame.minY + } listInsets.top += 4.0 let _ = self.list.update( transition: transition, @@ -827,7 +969,7 @@ final class StoryContentLiveChatComponent: Component { transition.setFrame(view: self.listMaskContainer, frame: CGRect(origin: CGPoint(), size: availableSize)) let maskTopInset: CGFloat = component.insets.top - 20.0 - let maskBottomInset: CGFloat = availableSize.height - pinnedBarFrame.minY - 26.0 + let maskBottomInset: CGFloat = listInsets.top - 26.0 transition.setFrame(view: self.maskGradientView, frame: CGRect(origin: CGPoint(x: 0.0, y: maskTopInset), size: CGSize(width: availableSize.width, height: max(0.0, availableSize.height - maskTopInset - maskBottomInset)))) transition.setFrame(view: self.listShadowView, frame: CGRect(origin: CGPoint(), size: availableSize)) @@ -847,3 +989,99 @@ final class StoryContentLiveChatComponent: Component { return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) } } + +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 + let blurBackground: Bool = false + let adjustContentForSideInset: Bool = true + + private let sourceNode: ContextExtractedContentContainingNode + private weak var containerView: UIView? + + init(sourceNode: ContextExtractedContentContainingNode, containerView: UIView, keepInPlace: Bool) { + self.sourceNode = sourceNode + self.containerView = containerView + self.keepInPlace = keepInPlace + } + + func takeView() -> ContextControllerTakeViewInfo? { + var contentArea: CGRect? + if let containerView = self.containerView { + contentArea = containerView.convert(containerView.bounds, to: nil) + } + + return ContextControllerTakeViewInfo( + containingItem: .node(self.sourceNode), + contentAreaInScreenSpace: contentArea ?? UIScreen.main.bounds + ) + } + + func putBack() -> ContextControllerPutBackViewInfo? { + return ContextControllerPutBackViewInfo(contentAreaInScreenSpace: UIScreen.main.bounds) + } +} diff --git a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemContentComponent.swift b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemContentComponent.swift index 31a0c381ff..fea21ce7b7 100644 --- a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemContentComponent.swift +++ b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemContentComponent.swift @@ -134,6 +134,23 @@ final class StoryItemContentComponent: Component { private var fetchPriorityResourceId: String? private var currentFetchPriority: (isMain: Bool, disposable: Disposable)? + public var isLiveChatExpanded: Bool? { + guard let liveChatView = self.liveChat?.view as? StoryContentLiveChatComponent.View else { + return nil + } + if liveChatView.isChatEmpty { + return nil + } + return liveChatView.isChatExpanded + } + + public func toggleLiveChatExpanded() { + guard let liveChatView = self.liveChat?.view as? StoryContentLiveChatComponent.View else { + return + } + return liveChatView.toggleLiveChatExpanded() + } + override init(frame: CGRect) { self.hierarchyTrackingLayer = HierarchyTrackingLayer() self.imageView = StoryItemImageView() @@ -799,6 +816,7 @@ final class StoryItemContentComponent: Component { strings: component.strings, theme: environment.theme, call: mediaStreamCall, + storyPeerId: component.peer.id, insets: environment.containerInsets )), environment: {}, @@ -807,6 +825,7 @@ final class StoryItemContentComponent: Component { let liveChatFrame = CGRect(origin: CGPoint(), size: availableSize) if let liveChatView = liveChat.view { if liveChatView.superview == nil { + liveChat.parentState = state self.insertSubview(liveChatView, aboveSubview: self.imageView) } mediaStreamTransition.setFrame(view: liveChatView, frame: liveChatFrame) diff --git a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerComponent.swift b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerComponent.swift index ae97b0d395..aa468b1448 100644 --- a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerComponent.swift +++ b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerComponent.swift @@ -1235,7 +1235,7 @@ public final class StoryItemSetContainerComponent: Component { self.state?.updated(transition: ComponentTransition(animation: .curve(duration: 0.4, curve: .spring))) } } else { - if let visibleItemView = self.visibleItems[component.slice.item.id]?.view.view as? StoryItemContentComponent.View { + if let visibleItemView = self.visibleItems[component.slice.item.id]?.view.view as? StoryItemContentComponent.View { visibleItemView.seekEnded() } if translation.y > 200.0 || (translation.y > 5.0 && velocity.y > 200.0) { @@ -1598,6 +1598,7 @@ public final class StoryItemSetContainerComponent: Component { ) if let view = visibleItem.view.view { if visibleItem.contentContainerView.superview == nil { + visibleItem.view.parentState = self.state self.itemsContainerView.addSubview(visibleItem.contentContainerView) self.itemsContainerView.layer.addSublayer(visibleItem.contentTintLayer) self.itemsContainerView.addSubview(visibleItem.unclippedContainerView) @@ -2882,6 +2883,19 @@ public final class StoryItemSetContainerComponent: Component { var inputPanelSize: CGSize? let _ = inputNodeVisible + + var inputPanelInset: CGFloat = component.containerInsets.bottom + var inputHeight = component.inputHeight + + var needInputBackground = true + if self.viewListDisplayState != .hidden { + needInputBackground = false + } + if self.inputPanelExternalState.isEditing { + if self.sendMessageContext.currentInputMode == .media || (inputHeight.isZero && keyboardWasHidden) { + inputHeight = component.deviceMetrics.standardInputHeight(inLandscape: false) + } + } if showMessageInputPanel { var haveLikeOptions = false @@ -2906,6 +2920,11 @@ public final class StoryItemSetContainerComponent: Component { maxInputLength = GroupCallMessagesContext.getStarAmountParamMapping(value: self.sendMessageContext.currentLiveStreamMessageStars?.value ?? 0).maxLength } + var isLiveChatExpanded: Bool? + if let visibleItemView = self.visibleItems[component.slice.item.id]?.view.view as? StoryItemContentComponent.View { + isLiveChatExpanded = visibleItemView.isLiveChatExpanded + } + inputPanelSize = self.inputPanel.update( transition: inputPanelTransition, component: AnyComponent(MessageInputPanelComponent( @@ -2944,6 +2963,13 @@ public final class StoryItemSetContainerComponent: Component { guard let self else { return } + + if let visibleItemView = self.visibleItems[component.slice.item.id]?.view.view as? StoryItemContentComponent.View { + if !(visibleItemView.isLiveChatExpanded ?? true) { + visibleItemView.toggleLiveChatExpanded() + } + } + self.sendMessageContext.performSendMessageAction(view: self) }, sendMessageOptionsAction: { [weak self] sourceView, gesture in @@ -3055,7 +3081,7 @@ public final class StoryItemSetContainerComponent: Component { } self.sendMessageContext.performShareAction(view: self) } : nil, - paidMessageAction: (isLiveStream && self.sendMessageContext.currentLiveStreamMessageStars == nil) ? { [weak self] in + paidMessageAction: isLiveStream ? { [weak self] in guard let self else { return } @@ -3123,7 +3149,7 @@ public final class StoryItemSetContainerComponent: Component { timeoutValue: nil, timeoutSelected: false, displayGradient: false, - bottomInset: max(bottomContentInset, component.inputHeight), + bottomInset: max(bottomContentInset, inputHeight), isFormattingLocked: false, hideKeyboard: self.sendMessageContext.currentInputMode == .media, customInputView: nil, @@ -3132,20 +3158,22 @@ public final class StoryItemSetContainerComponent: Component { header: nil, isChannel: isChannel, storyItem: component.slice.item.storyItem, - chatLocation: nil + chatLocation: nil, + isLiveChatExpanded: isLiveChatExpanded, + toggleLiveChatExpanded: { [weak self] in + guard let self else { + return + } + if let visibleItemView = self.visibleItems[component.slice.item.id]?.view.view as? StoryItemContentComponent.View { + visibleItemView.toggleLiveChatExpanded() + } + } )), environment: {}, containerSize: CGSize(width: inputPanelAvailableWidth, height: 200.0) ) } - var inputPanelInset: CGFloat = component.containerInsets.bottom - var inputHeight = component.inputHeight - - var needInputBackground = true - if self.viewListDisplayState != .hidden { - needInputBackground = false - } if self.inputPanelExternalState.isEditing { if self.sendMessageContext.currentInputMode == .media || (inputHeight.isZero && keyboardWasHidden) { inputHeight = component.deviceMetrics.standardInputHeight(inLandscape: false) diff --git a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerViewSendMessage.swift b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerViewSendMessage.swift index 8ebbc654d0..1602ce8068 100644 --- a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerViewSendMessage.swift +++ b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerViewSendMessage.swift @@ -401,49 +401,73 @@ final class StoryItemSetContainerSendMessage { view.dismissAllTooltips() - var sendWhenOnlineAvailable = false - if let presence = component.slice.additionalPeerData.presence, case .present = presence.status { - sendWhenOnlineAvailable = true - } - let presentationData = component.context.sharedContext.currentPresentationData.with({ $0 }).withUpdated(theme: component.theme) var items: [ContextMenuItem] = [] - items.append(.action(ContextMenuActionItem(text: presentationData.strings.Conversation_SendMessage_SendSilently, icon: { theme in return generateTintedImage(image: UIImage(bundleImageName: "Chat/Input/Menu/SilentIcon"), color: theme.contextMenu.primaryColor) - }, action: { [weak self, weak view] _, a in - a(.default) - - guard let self, let view else { - return + if case .liveStream = component.slice.item.storyItem.media { + //TODO:localize + items.append(.action(ContextMenuActionItem(text: "Edit Stars", icon: { theme in return generateTintedImage(image: UIImage(bundleImageName: "Chat/Input/Text/AccessoryIconSuggestPost"), color: theme.contextMenu.primaryColor) + }, action: { [weak self, weak view] _, a in + a(.default) + + guard let self, let view else { + return + } + self.performPaidMessageAction(view: view) + }))) + if self.currentLiveStreamMessageStars != nil { + items.append(.action(ContextMenuActionItem(text: "Remove Stars", icon: { theme in return generateTintedImage(image: UIImage(bundleImageName: "Chat/Input/Text/AccessoryIconSuggestPost"), color: theme.contextMenu.primaryColor) + }, action: { [weak self, weak view] _, a in + a(.default) + + guard let self, let view else { + return + } + self.currentLiveStreamMessageStars = nil + view.state?.updated(transition: .spring(duration: 0.3)) + }))) } - self.performSendMessageAction(view: view, silentPosting: true) - }))) - - if sendWhenOnlineAvailable { - items.append(.action(ContextMenuActionItem(text: presentationData.strings.Conversation_SendMessage_SendWhenOnline, icon: { theme in return generateTintedImage(image: UIImage(bundleImageName: "Chat/Input/Menu/WhenOnlineIcon"), color: theme.contextMenu.primaryColor) + } else { + var sendWhenOnlineAvailable = false + if let presence = component.slice.additionalPeerData.presence, case .present = presence.status { + sendWhenOnlineAvailable = true + } + + items.append(.action(ContextMenuActionItem(text: presentationData.strings.Conversation_SendMessage_SendSilently, icon: { theme in return generateTintedImage(image: UIImage(bundleImageName: "Chat/Input/Menu/SilentIcon"), color: theme.contextMenu.primaryColor) }, action: { [weak self, weak view] _, a in a(.default) guard let self, let view else { return } - self.performSendMessageAction(view: view, scheduleTime: scheduleWhenOnlineTimestamp) + self.performSendMessageAction(view: view, silentPosting: true) }))) + + if sendWhenOnlineAvailable { + items.append(.action(ContextMenuActionItem(text: presentationData.strings.Conversation_SendMessage_SendWhenOnline, icon: { theme in return generateTintedImage(image: UIImage(bundleImageName: "Chat/Input/Menu/WhenOnlineIcon"), color: theme.contextMenu.primaryColor) + }, action: { [weak self, weak view] _, a in + a(.default) + + guard let self, let view else { + return + } + self.performSendMessageAction(view: view, scheduleTime: scheduleWhenOnlineTimestamp) + }))) + } + + if component.slice.additionalPeerData.sendPaidMessageStars == nil { + items.append(.action(ContextMenuActionItem(text: presentationData.strings.Conversation_SendMessage_ScheduleMessage, icon: { theme in return generateTintedImage(image: UIImage(bundleImageName: "Chat/Input/Menu/ScheduleIcon"), color: theme.contextMenu.primaryColor) + }, action: { [weak self, weak view] _, a in + a(.default) + + guard let self, let view else { + return + } + self.presentScheduleTimePicker(view: view) + }))) + } } - if component.slice.additionalPeerData.sendPaidMessageStars == nil { - items.append(.action(ContextMenuActionItem(text: presentationData.strings.Conversation_SendMessage_ScheduleMessage, icon: { theme in return generateTintedImage(image: UIImage(bundleImageName: "Chat/Input/Menu/ScheduleIcon"), color: theme.contextMenu.primaryColor) - }, action: { [weak self, weak view] _, a in - a(.default) - - guard let self, let view else { - return - } - self.presentScheduleTimePicker(view: view) - }))) - } - - let contextItems = ContextController.Items(content: .list(items)) let contextController = ContextController(presentationData: presentationData, source: .reference(HeaderContextReferenceContentSource(controller: controller, sourceView: sourceView, position: .top)), items: .single(contextItems), gesture: gesture) diff --git a/submodules/TelegramUI/Sources/Chat/ChatControllerOpenWebApp.swift b/submodules/TelegramUI/Sources/Chat/ChatControllerOpenWebApp.swift index adbc9b3266..c0b5ced09e 100644 --- a/submodules/TelegramUI/Sources/Chat/ChatControllerOpenWebApp.swift +++ b/submodules/TelegramUI/Sources/Chat/ChatControllerOpenWebApp.swift @@ -164,7 +164,18 @@ func openWebAppImpl( }, getInputContainerNode: { [weak parentController] in if let parentController = parentController as? ChatControllerImpl, let layout = parentController.validLayout, case .compact = layout.metrics.widthClass { return (parentController.chatDisplayNode.getWindowInputAccessoryHeight(), parentController.chatDisplayNode.inputPanelContainerNode, { - return parentController.chatDisplayNode.textInputPanelNode?.makeAttachmentMenuTransition(accessoryPanelNode: nil) + guard let menuTransition = parentController.chatDisplayNode.textInputPanelNode?.makeAttachmentMenuTransition(accessoryPanelNode: nil) else { + return nil + } + return AttachmentController.InputPanelTransition( + inputNode: menuTransition.inputNode, + accessoryPanelNode: menuTransition.accessoryPanelNode, + menuButtonNode: menuTransition.menuButtonNode, + menuButtonBackgroundView: menuTransition.menuButtonBackgroundView, + menuIconNode: menuTransition.menuIconNode, + menuTextNode: menuTransition.menuTextNode, + prepareForDismiss: menuTransition.prepareForDismiss + ) }) } else { return nil diff --git a/submodules/TelegramUI/Sources/ChatControllerNode.swift b/submodules/TelegramUI/Sources/ChatControllerNode.swift index 584843c3c4..2b97e40c39 100644 --- a/submodules/TelegramUI/Sources/ChatControllerNode.swift +++ b/submodules/TelegramUI/Sources/ChatControllerNode.swift @@ -230,7 +230,6 @@ class ChatControllerNode: ASDisplayNode, ASScrollViewDelegate { private(set) var feePanelNode: ChatFeePanelNode? private let titleAccessoryPanelContainer: ChatControllerTitlePanelNodeContainer - private var titleTopicsAccessoryPanelNode: ChatTopicListTitleAccessoryPanelNode? private var titleAccessoryPanelNode: ChatTitleAccessoryPanelNode? private var chatTranslationPanel: ChatTranslationPanelNode? @@ -853,7 +852,7 @@ class ChatControllerNode: ASDisplayNode, ASScrollViewDelegate { self.contentContainerNode.contentNode.addSubnode(self.navigateButtons) self.wrappingNode.contentNode.addSubnode(self.presentationContextMarker) self.contentContainerNode.contentNode.addSubnode(self.contentDimNode) - + self.navigationBar?.additionalContentNode.addSubnode(self.titleAccessoryPanelContainer) self.textInputPanelNode = ChatTextInputPanelNode(context: context, presentationInterfaceState: chatPresentationInterfaceState, presentationContext: ChatPresentationContext(context: context, backgroundNode: backgroundNode), presentController: { [weak self] controller in @@ -1317,30 +1316,6 @@ class ChatControllerNode: ASDisplayNode, ASScrollViewDelegate { previousInputPanelOrigin.y -= secondaryInputPanelNode.bounds.size.height } self.containerLayoutAndNavigationBarHeight = (layout, navigationBarHeight) - - var dismissedTitleTopicsAccessoryPanelNode: ChatTopicListTitleAccessoryPanelNode? - var immediatelyLayoutTitleTopicsAccessoryPanelNodeAndAnimateAppearance = false - var titleTopicsAccessoryPanelHeight: CGFloat? - var titleTopicsAccessoryPanelBackgroundHeight: CGFloat? - var titleTopicsAccessoryPanelHitTestSlop: CGFloat? - if let titleTopicsAccessoryPanelNode = titleTopicsPanelForChatPresentationInterfaceState(self.chatPresentationInterfaceState, context: self.context, currentPanel: self.titleTopicsAccessoryPanelNode, controllerInteraction: self.controllerInteraction, interfaceInteraction: self.interfaceInteraction, force: false) { - if self.titleTopicsAccessoryPanelNode != titleTopicsAccessoryPanelNode { - dismissedTitleTopicsAccessoryPanelNode = self.titleTopicsAccessoryPanelNode - self.titleTopicsAccessoryPanelNode = titleTopicsAccessoryPanelNode - immediatelyLayoutTitleTopicsAccessoryPanelNodeAndAnimateAppearance = true - self.titleAccessoryPanelContainer.addSubnode(titleTopicsAccessoryPanelNode) - - titleTopicsAccessoryPanelNode.clipsToBounds = true - } - - let layoutResult = titleTopicsAccessoryPanelNode.updateLayout(width: layout.size.width, leftInset: layout.safeInsets.left, rightInset: layout.safeInsets.right, transition: immediatelyLayoutTitleTopicsAccessoryPanelNodeAndAnimateAppearance ? .immediate : transition, interfaceState: self.chatPresentationInterfaceState) - titleTopicsAccessoryPanelHeight = layoutResult.insetHeight - titleTopicsAccessoryPanelBackgroundHeight = layoutResult.backgroundHeight - titleTopicsAccessoryPanelHitTestSlop = layoutResult.hitTestSlop - } else if let titleTopicsAccessoryPanelNode = self.titleTopicsAccessoryPanelNode { - dismissedTitleTopicsAccessoryPanelNode = titleTopicsAccessoryPanelNode - self.titleTopicsAccessoryPanelNode = nil - } var floatingTopicsPanelInsets = UIEdgeInsets() var dismissedFloatingTopicsPanel: (view: ComponentView, component: ChatFloatingTopicsPanel)? @@ -1860,15 +1835,6 @@ class ChatControllerNode: ASDisplayNode, ASScrollViewDelegate { var titlePanelsContentOffset: CGFloat = 0.0 - var titleTopicsAccessoryPanelFrame: CGRect? - if let _ = self.titleTopicsAccessoryPanelNode, let panelHeight = titleTopicsAccessoryPanelHeight { - titleTopicsAccessoryPanelFrame = CGRect(origin: CGPoint(x: 0.0, y: titlePanelsContentOffset), size: CGSize(width: layout.size.width, height: panelHeight)) - insets.top += panelHeight - extraNavigationBarHeight += titleTopicsAccessoryPanelBackgroundHeight ?? 0.0 - extraNavigationBarHitTestSlop = titleTopicsAccessoryPanelHitTestSlop ?? 0.0 - titlePanelsContentOffset += panelHeight - } - var titleAccessoryPanelFrame: CGRect? let titleAccessoryPanelBaseY = titlePanelsContentOffset if let _ = self.titleAccessoryPanelNode, let panelHeight = titleAccessoryPanelHeight { @@ -2076,7 +2042,7 @@ class ChatControllerNode: ASDisplayNode, ASScrollViewDelegate { transition.updatePosition(node: self.historyNode, position: CGPoint(x: contentBounds.size.width / 2.0, y: contentBounds.size.height / 2.0)) } - if didChangeFloatingTopicsPanel || dismissedFloatingTopicsPanel != nil || immediatelyLayoutTitleTopicsAccessoryPanelNodeAndAnimateAppearance || dismissedTitleTopicsAccessoryPanelNode != nil { + if didChangeFloatingTopicsPanel || dismissedFloatingTopicsPanel != nil { if transition.isAnimated { self.historyNode.resetScrolledToItem() self.historyNode.enableUnreadAlignment = false @@ -2550,21 +2516,6 @@ class ChatControllerNode: ASDisplayNode, ASScrollViewDelegate { transition.updateFrame(node: self.navigateButtons, frame: apparentNavigateButtonsFrame) self.navigateButtons.update(rect: apparentNavigateButtonsFrame, within: layout.size, transition: transition) - - if let titleTopicsAccessoryPanelNode = self.titleTopicsAccessoryPanelNode, let titleTopicsAccessoryPanelFrame, (immediatelyLayoutTitleTopicsAccessoryPanelNodeAndAnimateAppearance || !titleTopicsAccessoryPanelNode.frame.equalTo(titleTopicsAccessoryPanelFrame)) { - if immediatelyLayoutTitleTopicsAccessoryPanelNodeAndAnimateAppearance { - titleTopicsAccessoryPanelNode.frame = titleTopicsAccessoryPanelFrame.offsetBy(dx: 0.0, dy: -titleTopicsAccessoryPanelFrame.height) - - ComponentTransition(transition).setFrame(view: titleTopicsAccessoryPanelNode.view, frame: titleTopicsAccessoryPanelFrame) - } else { - let previousFrame = titleTopicsAccessoryPanelNode.frame - titleTopicsAccessoryPanelNode.frame = titleTopicsAccessoryPanelFrame - if transition.isAnimated && previousFrame.width != titleTopicsAccessoryPanelFrame.width { - } else { - transition.animatePositionAdditive(node: titleTopicsAccessoryPanelNode, offset: CGPoint(x: 0.0, y: -titleTopicsAccessoryPanelFrame.height)) - } - } - } if let titleAccessoryPanelNode = self.titleAccessoryPanelNode, let titleAccessoryPanelFrame, !titleAccessoryPanelNode.frame.equalTo(titleAccessoryPanelFrame) { let previousFrame = titleAccessoryPanelNode.frame @@ -2690,14 +2641,6 @@ class ChatControllerNode: ASDisplayNode, ASScrollViewDelegate { } } } - - if let dismissedTitleTopicsAccessoryPanelNode { - var dismissedTopPanelFrame = dismissedTitleTopicsAccessoryPanelNode.frame - dismissedTopPanelFrame.origin.y = -dismissedTopPanelFrame.size.height - transition.updateFrame(node: dismissedTitleTopicsAccessoryPanelNode, frame: dismissedTopPanelFrame, completion: { [weak dismissedTitleTopicsAccessoryPanelNode] _ in - dismissedTitleTopicsAccessoryPanelNode?.removeFromSupernode() - }) - } if let dismissedTitleAccessoryPanelNode { var dismissedPanelFrame = dismissedTitleAccessoryPanelNode.frame @@ -5145,10 +5088,7 @@ class ChatControllerNode: ASDisplayNode, ASScrollViewDelegate { func chatLocationTabSwitchDirection(from fromLocation: Int64?, to toLocation: Int64?) -> Bool? { var leftIndex: Int? var rightIndex: Int? - if let titleTopicsAccessoryPanelNode = self.titleTopicsAccessoryPanelNode { - leftIndex = titleTopicsAccessoryPanelNode.topicIndex(threadId: fromLocation) - rightIndex = titleTopicsAccessoryPanelNode.topicIndex(threadId: toLocation) - } else if let floatingTopicsPanelView = self.floatingTopicsPanel?.view.view as? ChatFloatingTopicsPanel.View { + if let floatingTopicsPanelView = self.floatingTopicsPanel?.view.view as? ChatFloatingTopicsPanel.View { leftIndex = floatingTopicsPanelView.topicIndex(threadId: fromLocation) rightIndex = floatingTopicsPanelView.topicIndex(threadId: toLocation) } diff --git a/submodules/TelegramUI/Sources/ChatInterfaceTitlePanelNodes.swift b/submodules/TelegramUI/Sources/ChatInterfaceTitlePanelNodes.swift index 3852cc6de6..4891167b25 100644 --- a/submodules/TelegramUI/Sources/ChatInterfaceTitlePanelNodes.swift +++ b/submodules/TelegramUI/Sources/ChatInterfaceTitlePanelNodes.swift @@ -229,48 +229,6 @@ func titlePanelForChatPresentationInterfaceState(_ chatPresentationInterfaceStat return nil } -func titleTopicsPanelForChatPresentationInterfaceState(_ chatPresentationInterfaceState: ChatPresentationInterfaceState, context: AccountContext, currentPanel: ChatTitleAccessoryPanelNode?, controllerInteraction: ChatControllerInteraction?, interfaceInteraction: ChatPanelInterfaceInteraction?, force: Bool) -> ChatTopicListTitleAccessoryPanelNode? { - return nil - /*if !(chatPresentationInterfaceState.subject?.isService ?? false) { - if let channel = chatPresentationInterfaceState.renderedPeer?.peer as? TelegramChannel, channel.isForumOrMonoForum, let linkedMonoforumId = channel.linkedMonoforumId, let mainChannel = chatPresentationInterfaceState.renderedPeer?.peers[linkedMonoforumId] as? TelegramChannel, mainChannel.hasPermission(.manageDirect), chatPresentationInterfaceState.search == nil { - let topicListDisplayModeOnTheSide = chatPresentationInterfaceState.persistentData.topicListPanelLocation - if !topicListDisplayModeOnTheSide, let peerId = chatPresentationInterfaceState.chatLocation.peerId { - if let currentPanel = currentPanel as? ChatTopicListTitleAccessoryPanelNode { - return currentPanel - } else { - let panel = ChatTopicListTitleAccessoryPanelNode(context: context, peerId: peerId, kind: .monoforum) - panel.interfaceInteraction = interfaceInteraction - return panel - } - } - } else if let channel = chatPresentationInterfaceState.renderedPeer?.peer as? TelegramChannel, channel.isForum, (channel.flags.contains(.displayForumAsTabs) || context.sharedContext.immediateExperimentalUISettings.allForumsHaveTabs), chatPresentationInterfaceState.search == nil { - let topicListDisplayModeOnTheSide = chatPresentationInterfaceState.persistentData.topicListPanelLocation - if !topicListDisplayModeOnTheSide, let peerId = chatPresentationInterfaceState.chatLocation.peerId { - if let currentPanel = currentPanel as? ChatTopicListTitleAccessoryPanelNode { - return currentPanel - } else { - let panel = ChatTopicListTitleAccessoryPanelNode(context: context, peerId: peerId, kind: .forum) - panel.interfaceInteraction = interfaceInteraction - return panel - } - } - } else if let user = chatPresentationInterfaceState.renderedPeer?.peer as? TelegramUser, user.isForum, chatPresentationInterfaceState.search == nil { - let topicListDisplayModeOnTheSide = chatPresentationInterfaceState.persistentData.topicListPanelLocation - if !topicListDisplayModeOnTheSide, let peerId = chatPresentationInterfaceState.chatLocation.peerId { - if let currentPanel = currentPanel as? ChatTopicListTitleAccessoryPanelNode { - return currentPanel - } else { - let panel = ChatTopicListTitleAccessoryPanelNode(context: context, peerId: peerId, kind: .botForum) - panel.interfaceInteraction = interfaceInteraction - return panel - } - } - } - } - - return nil*/ -} - func floatingTopicsPanelForChatPresentationInterfaceState(_ chatPresentationInterfaceState: ChatPresentationInterfaceState, context: AccountContext, controllerInteraction: ChatControllerInteraction?, interfaceInteraction: ChatPanelInterfaceInteraction?, force: Bool) -> ChatFloatingTopicsPanel? { guard let peerId = chatPresentationInterfaceState.chatLocation.peerId else { return nil diff --git a/submodules/TelegramUI/Sources/ChatTopicListTitleAccessoryPanelNode.swift b/submodules/TelegramUI/Sources/ChatTopicListTitleAccessoryPanelNode.swift deleted file mode 100644 index 0da1b30f5b..0000000000 --- a/submodules/TelegramUI/Sources/ChatTopicListTitleAccessoryPanelNode.swift +++ /dev/null @@ -1,160 +0,0 @@ -import Foundation -import UIKit -import Display -import AsyncDisplayKit -import TelegramPresentationData -import ChatPresentationInterfaceState -import AccountContext -import ComponentFlow -import MultilineTextComponent -import PlainButtonComponent -import TelegramCore -import Postbox -import EmojiStatusComponent -import SwiftSignalKit -import BundleIconComponent -import AvatarNode -import TextBadgeComponent -import ChatSideTopicsPanel -import ComponentDisplayAdapters - -final class ChatTopicListTitleAccessoryPanelNode: ChatTitleAccessoryPanelNode, ChatControllerCustomNavigationPanelNode { - private struct Params: Equatable { - var width: CGFloat - var leftInset: CGFloat - var rightInset: CGFloat - var interfaceState: ChatPresentationInterfaceState - - init(width: CGFloat, leftInset: CGFloat, rightInset: CGFloat, interfaceState: ChatPresentationInterfaceState) { - self.width = width - self.leftInset = leftInset - self.rightInset = rightInset - self.interfaceState = interfaceState - } - - static func ==(lhs: Params, rhs: Params) -> Bool { - if lhs.width != rhs.width { - return false - } - if lhs.leftInset != rhs.leftInset { - return false - } - if lhs.rightInset != rhs.rightInset { - return false - } - if lhs.interfaceState != rhs.interfaceState { - return false - } - return true - } - } - - private var params: Params? - - private let context: AccountContext - private let peerId: EnginePeer.Id - private let kind: ChatSideTopicsPanel.Kind - private let panel = ComponentView() - - init(context: AccountContext, peerId: EnginePeer.Id, kind: ChatSideTopicsPanel.Kind) { - self.context = context - self.peerId = peerId - self.kind = kind - - super.init() - } - - deinit { - } - - private func update(transition: ContainedViewLayoutTransition) { - if let params = self.params { - self.update(params: params, transition: transition) - } - } - - override func updateLayout(width: CGFloat, leftInset: CGFloat, rightInset: CGFloat, transition: ContainedViewLayoutTransition, interfaceState: ChatPresentationInterfaceState) -> LayoutResult { - let params = Params(width: width, leftInset: leftInset, rightInset: rightInset, interfaceState: interfaceState) - if self.params != params { - self.params = params - self.update(params: params, transition: transition) - } - - let panelHeight: CGFloat = 44.0 - - return LayoutResult(backgroundHeight: panelHeight, insetHeight: panelHeight, hitTestSlop: 0.0) - } - - func updateLayout(width: CGFloat, leftInset: CGFloat, rightInset: CGFloat, transition: ContainedViewLayoutTransition, chatController: ChatController) -> LayoutResult { - return self.updateLayout(width: width, leftInset: leftInset, rightInset: rightInset, transition: transition, interfaceState: (chatController as! ChatControllerImpl).presentationInterfaceState) - } - - private func update(params: Params, transition: ContainedViewLayoutTransition) { - let panelHeight: CGFloat = 44.0 - - let panelFrame = CGRect(origin: CGPoint(), size: CGSize(width: params.width, height: panelHeight)) - let _ = self.panel.update( - transition: ComponentTransition(transition), - component: AnyComponent(ChatSideTopicsPanel( - context: self.context, - theme: params.interfaceState.theme, - strings: params.interfaceState.strings, - location: .top, - peerId: self.peerId, - kind: self.kind, - topicId: params.interfaceState.chatLocation.threadId, - controller: { [weak self] in - return self?.interfaceInteraction?.chatController() - }, - togglePanel: { [weak self] in - guard let self else { - return - } - self.interfaceInteraction?.toggleChatSidebarMode() - }, - updateTopicId: { [weak self] topicId, direction in - guard let self else { - return - } - self.interfaceInteraction?.updateChatLocationThread(topicId, direction ? .right : .left) - }, - openDeletePeer: { [weak self] threadId in - guard let controller = self?.interfaceInteraction?.chatController() as? ChatControllerImpl else { - return - } - controller.openDeleteMonoforumPeer(peerId: PeerId(threadId)) - } - )), - environment: { - ChatSidePanelEnvironment(insets: UIEdgeInsets( - top: 0.0, - left: params.leftInset, - bottom: 0.0, - right: params.rightInset - )) - }, - containerSize: panelFrame.size - ) - if let panelView = self.panel.view { - if panelView.superview == nil { - panelView.disablesInteractiveTransitionGestureRecognizer = true - self.view.addSubview(panelView) - } - transition.updateFrame(view: panelView, frame: panelFrame) - } - } - - public func updateGlobalOffset(globalOffset: CGFloat, transition: ComponentTransition) { - if let panelView = self.panel.view as? ChatSideTopicsPanel.View { - panelView.updateGlobalOffset(globalOffset: globalOffset, transition: transition) - } - } - - public func topicIndex(threadId: Int64?) -> Int? { - if let panelView = self.panel.view as? ChatSideTopicsPanel.View { - return panelView.topicIndex(threadId: threadId) - } else { - return nil - } - } -}