diff --git a/Random.txt b/Random.txt index ee6acb4db0..275fbd7cbd 100644 --- a/Random.txt +++ b/Random.txt @@ -1 +1 @@ -eb0ad702db3f1d1c0afe18fdddf9fd4ba4562edd550a0b414b087d26caac1bda +0e50d6a10bc89732f2e475cc35994ebf5085443bef2d88cf3dbcb5d913d2128a diff --git a/submodules/ContextUI/Sources/ContextController.swift b/submodules/ContextUI/Sources/ContextController.swift index a1344158eb..dd6506d277 100644 --- a/submodules/ContextUI/Sources/ContextController.swift +++ b/submodules/ContextUI/Sources/ContextController.swift @@ -684,11 +684,12 @@ final class ContextControllerNode: ViewControllerTracingNode, ASScrollViewDelega self.contentReady.set(.single(true)) let transitionInfo = source.transitionInfo() - if let transitionInfo = transitionInfo { + if let transitionInfo { let referenceView = transitionInfo.referenceView self.contentContainerNode.contentNode = .reference(view: referenceView) self.contentAreaInScreenSpace = transitionInfo.contentAreaInScreenSpace self.customPosition = transitionInfo.customPosition + var projectedFrame = convertFrame(referenceView.bounds, from: referenceView, to: self.view) projectedFrame.origin.x += transitionInfo.insets.left projectedFrame.size.width -= transitionInfo.insets.left + transitionInfo.insets.right diff --git a/submodules/ContextUI/Sources/ContextControllerActionsStackNode.swift b/submodules/ContextUI/Sources/ContextControllerActionsStackNode.swift index 728e56f64e..dbf823676a 100644 --- a/submodules/ContextUI/Sources/ContextControllerActionsStackNode.swift +++ b/submodules/ContextUI/Sources/ContextControllerActionsStackNode.swift @@ -749,7 +749,7 @@ private final class ContextControllerActionsListCustomItemNode: ASDisplayNode, C getController: self.getController, actionSelected: { result in switch result { - case .dismissWithoutContent: + case .default, .dismissWithoutContent: self.requestDismiss(result) default: break diff --git a/submodules/TelegramCallsUI/Sources/AccountGroupCallContextImpl.swift b/submodules/TelegramCallsUI/Sources/AccountGroupCallContextImpl.swift index bbf2e28b75..5b348c93da 100644 --- a/submodules/TelegramCallsUI/Sources/AccountGroupCallContextImpl.swift +++ b/submodules/TelegramCallsUI/Sources/AccountGroupCallContextImpl.swift @@ -100,7 +100,8 @@ public final class AccountGroupCallContextImpl: AccountGroupCallContext { isVideoEnabled: state.isVideoEnabled, unmutedVideoLimit: state.unmutedVideoLimit, isStream: state.isStream, - isCreator: state.isCreator + isCreator: state.isCreator, + defaultSendAs: state.defaultSendAs ), topParticipants: topParticipants, participantCount: state.totalCount, diff --git a/submodules/TelegramCallsUI/Sources/PresentationGroupCall.swift b/submodules/TelegramCallsUI/Sources/PresentationGroupCall.swift index e3ba01b24a..86fd6ff154 100644 --- a/submodules/TelegramCallsUI/Sources/PresentationGroupCall.swift +++ b/submodules/TelegramCallsUI/Sources/PresentationGroupCall.swift @@ -934,21 +934,6 @@ public final class PresentationGroupCallImpl: PresentationGroupCall { if isStream { messageLifetime = Int32.max - - if self.isStream { - var allowLiveChat = false - if let data = self.accountContext.currentAppConfiguration.with({ $0 }).data { - if let dev = data["dev"] as? Double, dev != 0.0 { - allowLiveChat = true - } - if data["ios_can_join_streams"] != nil { - allowLiveChat = true - } - } - if !allowLiveChat { - preconditionFailure() - } - } } self.messagesContext = accountContext.engine.messages.groupCallMessages( @@ -1689,7 +1674,8 @@ public final class PresentationGroupCallImpl: PresentationGroupCall { isVideoEnabled: callInfo.isVideoEnabled, unmutedVideoLimit: callInfo.unmutedVideoLimit, isStream: callInfo.isStream, - isCreator: callInfo.isCreator + isCreator: callInfo.isCreator, + defaultSendAs: callInfo.defaultSendAs )), audioSessionControl: self.audioSessionControl) } else { self.summaryInfoState.set(.single(SummaryInfoState(info: GroupCallInfo( @@ -1707,7 +1693,8 @@ public final class PresentationGroupCallImpl: PresentationGroupCall { isVideoEnabled: state.isVideoEnabled, unmutedVideoLimit: state.unmutedVideoLimit, isStream: callInfo.isStream, - isCreator: callInfo.isCreator + isCreator: callInfo.isCreator, + defaultSendAs: callInfo.defaultSendAs )))) self.summaryParticipantsState.set(.single(SummaryParticipantsState( @@ -1787,7 +1774,8 @@ public final class PresentationGroupCallImpl: PresentationGroupCall { isVideoEnabled: false, unmutedVideoLimit: 0, isStream: true, - isCreator: false + isCreator: false, + defaultSendAs: nil ) } else { activeCallInfo = nil @@ -2691,7 +2679,8 @@ public final class PresentationGroupCallImpl: PresentationGroupCall { isVideoEnabled: state.isVideoEnabled, unmutedVideoLimit: state.unmutedVideoLimit, isStream: callInfo.isStream, - isCreator: callInfo.isCreator + isCreator: callInfo.isCreator, + defaultSendAs: callInfo.defaultSendAs )))) self.summaryParticipantsState.set(.single(SummaryParticipantsState( @@ -4060,15 +4049,15 @@ public final class PresentationGroupCallImpl: PresentationGroupCall { }) } - public func sendMessage(randomId: Int64? = nil, text: String, entities: [MessageTextEntity], paidStars: Int64?) { + public func sendMessage(fromId: PeerId?, randomId: Int64? = nil, text: String, entities: [MessageTextEntity], paidStars: Int64?) { if let messagesContext = self.messagesContext { - messagesContext.send(fromId: self.joinAsPeerId, randomId: randomId, text: text, entities: entities, paidStars: paidStars) + messagesContext.send(fromId: fromId ?? self.joinAsPeerId, randomId: randomId, text: text, entities: entities, paidStars: paidStars) } } - public func sendStars(amount: Int64, delay: Bool) { + public func sendStars(fromId: PeerId?, amount: Int64, delay: Bool) { if let messagesContext = self.messagesContext { - messagesContext.sendStars(fromId: self.joinAsPeerId, amount: amount, delay: delay) + messagesContext.sendStars(fromId: fromId ?? self.joinAsPeerId, amount: amount, delay: delay) } } diff --git a/submodules/TelegramCallsUI/Sources/VideoChatScreen.swift b/submodules/TelegramCallsUI/Sources/VideoChatScreen.swift index 3d37f0610e..58bfb14a9e 100644 --- a/submodules/TelegramCallsUI/Sources/VideoChatScreen.swift +++ b/submodules/TelegramCallsUI/Sources/VideoChatScreen.swift @@ -1416,7 +1416,7 @@ final class VideoChatScreenComponent: Component { return } let entities = generateTextEntities(text.string, enabledTypes: [.mention, .hashtag], currentEntities: generateChatInputTextEntities(text)) - call.sendMessage(randomId: randomId, text: text.string, entities: entities, paidStars: nil) + call.sendMessage(fromId: nil, randomId: randomId, text: text.string, entities: entities, paidStars: nil) } inputPanelView.clearSendMessageInput(updateState: true) @@ -3836,7 +3836,7 @@ final class VideoChatScreenComponent: Component { guard case let .group(groupCall) = self.currentCall, let call = groupCall as? PresentationGroupCallImpl else { return } - call.sendMessage(text: text, entities: entities, paidStars: nil) + call.sendMessage(fromId: nil, text: text, entities: entities, paidStars: nil) }) } diff --git a/submodules/TelegramCore/Sources/SyncCore/SyncCore_Namespaces.swift b/submodules/TelegramCore/Sources/SyncCore/SyncCore_Namespaces.swift index d9284622d8..c81fbbf369 100644 --- a/submodules/TelegramCore/Sources/SyncCore/SyncCore_Namespaces.swift +++ b/submodules/TelegramCore/Sources/SyncCore/SyncCore_Namespaces.swift @@ -147,6 +147,7 @@ public struct Namespaces { public static let cachedProfileGiftsCollections: Int8 = 48 public static let cachedProfileSavedMusic: Int8 = 49 public static let cachedChatThemes: Int8 = 50 + public static let cachedLiveStorySendAsPeers: Int8 = 51 } public struct UnorderedItemList { diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Calls/GroupCalls.swift b/submodules/TelegramCore/Sources/TelegramEngine/Calls/GroupCalls.swift index f9e6dfab35..bf73c77d07 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Calls/GroupCalls.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Calls/GroupCalls.swift @@ -99,6 +99,7 @@ public struct GroupCallInfo: Equatable { public var unmutedVideoLimit: Int public var isStream: Bool public var isCreator: Bool + public var defaultSendAs: EnginePeer.Id? public init( id: Int64, @@ -115,7 +116,8 @@ public struct GroupCallInfo: Equatable { isVideoEnabled: Bool, unmutedVideoLimit: Int, isStream: Bool, - isCreator: Bool + isCreator: Bool, + defaultSendAs: EnginePeer.Id? ) { self.id = id self.accessHash = accessHash @@ -132,6 +134,7 @@ public struct GroupCallInfo: Equatable { self.unmutedVideoLimit = unmutedVideoLimit self.isStream = isStream self.isCreator = isCreator + self.defaultSendAs = defaultSendAs } } @@ -143,7 +146,7 @@ public struct GroupCallSummary: Equatable { extension GroupCallInfo { init?(_ call: Api.GroupCall) { switch call { - case let .groupCall(flags, id, accessHash, participantsCount, title, streamDcId, recordStartDate, scheduleDate, _, unmutedVideoLimit, _, _, sendPaidMessagesStars, _): + case let .groupCall(flags, id, accessHash, participantsCount, title, streamDcId, recordStartDate, scheduleDate, _, unmutedVideoLimit, _, _, sendPaidMessagesStars, defaultSendAs): self.init( id: id, accessHash: accessHash, @@ -159,7 +162,8 @@ extension GroupCallInfo { isVideoEnabled: (flags & (1 << 9)) != 0, unmutedVideoLimit: Int(unmutedVideoLimit), isStream: (flags & (1 << 12)) != 0, - isCreator: (flags & (1 << 15)) != 0 + isCreator: (flags & (1 << 15)) != 0, + defaultSendAs: defaultSendAs?.peerId ) case .groupCallDiscarded: return nil @@ -773,7 +777,7 @@ func _internal_joinGroupCall(account: Account, peerId: PeerId?, joinAs: PeerId?, maybeParsedCall = GroupCallInfo(call) switch call { - case let .groupCall(flags, _, _, _, title, _, recordStartDate, scheduleDate, _, unmutedVideoLimit, _, _, sendPaidMessagesStars, _): + case let .groupCall(flags, _, _, _, title, _, recordStartDate, scheduleDate, _, unmutedVideoLimit, _, _, sendPaidMessagesStars, defaultSendAs): let isMin = (flags & (1 << 19)) != 0 let isMuted = (flags & (1 << 1)) != 0 let canChange = (flags & (1 << 2)) != 0 @@ -787,6 +791,7 @@ func _internal_joinGroupCall(account: Account, peerId: PeerId?, joinAs: PeerId?, state.scheduleTimestamp = scheduleDate state.isVideoEnabled = isMin ? state.isVideoEnabled : isVideoEnabled state.unmutedVideoLimit = Int(unmutedVideoLimit) + state.defaultSendAs = defaultSendAs?.peerId default: break } @@ -1429,6 +1434,7 @@ public final class GroupCallParticipantsContext { public var unmutedVideoLimit: Int public var isStream: Bool public var sendPaidMessagesStars: Int64? + public var defaultSendAs: PeerId? public var version: Int32 public mutating func mergeActivity(from other: State, myPeerId: PeerId?, previousMyPeerId: PeerId?, mergeActivityTimestamps: Bool) { @@ -3693,6 +3699,7 @@ public final class GroupCallMessagesContext { public let id: Id public let stableId: Int + public let isIncoming: Bool public let author: EnginePeer? public let text: String public let entities: [MessageTextEntity] @@ -3700,9 +3707,10 @@ public final class GroupCallMessagesContext { public let lifetime: Int32 public let paidStars: Int64? - public init(id: Id, stableId: Int, author: EnginePeer?, text: String, entities: [MessageTextEntity], date: Int32, lifetime: Int32, paidStars: Int64?) { + public init(id: Id, stableId: Int, isIncoming: Bool, author: EnginePeer?, text: String, entities: [MessageTextEntity], date: Int32, lifetime: Int32, paidStars: Int64?) { self.id = id self.stableId = stableId + self.isIncoming = isIncoming self.author = author self.text = text self.entities = entities @@ -3715,6 +3723,7 @@ public final class GroupCallMessagesContext { return Message( id: id, stableId: self.stableId, + isIncoming: self.isIncoming, author: self.author, text: self.text, entities: self.entities, @@ -3734,6 +3743,9 @@ public final class GroupCallMessagesContext { if lhs.stableId != rhs.stableId { return false } + if lhs.isIncoming != rhs.isIncoming { + return false + } if lhs.author != rhs.author { return false } @@ -3836,7 +3848,7 @@ public final class GroupCallMessagesContext { private var messageLifeTimer: SwiftSignalKit.Timer? - private var pendingSendStars: (fromId: PeerId, messageId: Int64, amount: Int64)? + private var pendingSendStars: (fromPeer: Peer, messageId: Int64, amount: Int64)? private var pendingSendStarsTimer: SwiftSignalKit.Timer? init(queue: Queue, account: Account, callId: Int64, reference: InternalGroupCallReference, e2eContext: ConferenceCallE2EContext?, messageLifetime: Int32, isLiveStream: Bool) { @@ -3915,6 +3927,7 @@ public final class GroupCallMessagesContext { messages.append(Message( id: Message.Id(space: .remote, id: randomId), stableId: allocatedStableIds[messages.count], + isIncoming: addedOpaqueMessage.authorId != accountPeerId, author: transaction.getPeer(addedOpaqueMessage.authorId).flatMap(EnginePeer.init), text: text, entities: entities, @@ -3944,6 +3957,7 @@ public final class GroupCallMessagesContext { messages.append(Message( id: Message.Id(space: .remote, id: Int64(addedMessage.messageId)), stableId: allocatedStableIds[messages.count], + isIncoming: addedMessage.authorId != accountPeerId, author: transaction.getPeer(addedMessage.authorId).flatMap(EnginePeer.init), text: addedMessage.text, entities: addedMessage.entities, @@ -4225,6 +4239,7 @@ public final class GroupCallMessagesContext { let message = Message( id: Message.Id(space: .local, id: randomId), stableId: stableId, + isIncoming: false, author: fromPeer.flatMap(EnginePeer.init), text: text, entities: entities, @@ -4269,6 +4284,16 @@ public final class GroupCallMessagesContext { if paidStars != nil { flags |= 1 << 0 } + var sendAs: Api.InputPeer? + if fromId != self.account.peerId { + guard let fromPeer else { + return + } + sendAs = apiInputPeer(fromPeer) + } + if sendAs != nil { + flags |= 1 << 1 + } self.sendMessageDisposables.add((self.account.network.request(Api.functions.phone.sendGroupCallMessage( flags: flags, call: self.reference.apiInputGroupCall, @@ -4278,7 +4303,7 @@ public final class GroupCallMessagesContext { entities: apiEntitiesFromMessageTextEntities(entities, associatedPeers: SimpleDictionary()) ), allowPaidStars: paidStars, - sendAs: nil + sendAs: sendAs )) |> deliverOn(self.queue)).startStrict(next: { [weak self] updates in guard let self else { return @@ -4323,6 +4348,13 @@ public final class GroupCallMessagesContext { var flags: Int32 = 0 flags |= 1 << 0 + var sendAs: Api.InputPeer? + if pendingSendStars.fromPeer.id != self.account.peerId { + sendAs = apiInputPeer(pendingSendStars.fromPeer) + } + if sendAs != nil { + flags |= 1 << 1 + } self.sendMessageDisposables.add((self.account.network.request(Api.functions.phone.sendGroupCallMessage( flags: flags, call: self.reference.apiInputGroupCall, @@ -4332,7 +4364,7 @@ public final class GroupCallMessagesContext { entities: [] ), allowPaidStars: pendingSendStars.amount, - sendAs: nil + sendAs: sendAs )) |> deliverOn(self.queue)).startStrict(next: { [weak self] updates in guard let self else { return @@ -4349,7 +4381,7 @@ public final class GroupCallMessagesContext { if let index = state.pinnedMessages.firstIndex(where: { $0.id == Message.Id(space: .local, id: pendingSendStars.messageId) }) { state.pinnedMessages[index] = state.pinnedMessages[index].withId(Message.Id(space: .remote, id: Int64(id))) } - Impl.addStateStars(state: &state, peerId: pendingSendStars.fromId, isMy: true, amount: pendingSendStars.amount) + Impl.addStateStars(state: &state, peerId: pendingSendStars.fromPeer.id, isMy: true, amount: pendingSendStars.amount) state.pendingMyStars = 0 self.state = state break @@ -4386,7 +4418,7 @@ public final class GroupCallMessagesContext { return transaction.getPeer(fromId) } |> deliverOn(self.queue)).startStandalone(next: { [weak self] fromPeer in - guard let self else { + guard let self, let fromPeer else { return } @@ -4397,7 +4429,7 @@ public final class GroupCallMessagesContext { totalAmount = pendingSendStarsValue.amount + amount self.pendingSendStars = ( - fromId: fromId, + fromPeer: fromPeer, messageId: pendingSendStarsValue.messageId, amount: totalAmount ) @@ -4408,7 +4440,7 @@ public final class GroupCallMessagesContext { arc4random_buf(&randomId, 8) self.pendingSendStars = ( - fromId: fromId, + fromPeer: fromPeer, messageId: randomId, amount: amount ) @@ -4426,6 +4458,7 @@ public final class GroupCallMessagesContext { state.messages.append(Message( id: message.id, stableId: message.stableId, + isIncoming: false, author: message.author, text: message.text, entities: message.entities, @@ -4439,7 +4472,8 @@ public final class GroupCallMessagesContext { state.messages.append(Message( id: Message.Id(space: .local, id: pendingSendStarsValue.messageId), stableId: stableId, - author: fromPeer.flatMap(EnginePeer.init), + isIncoming: false, + author: EnginePeer(fromPeer), text: "", entities: [], date: currentTime, @@ -4453,6 +4487,7 @@ public final class GroupCallMessagesContext { state.pinnedMessages.append(Message( id: message.id, stableId: message.stableId, + isIncoming: message.isIncoming, author: message.author, text: message.text, entities: message.entities, @@ -4466,7 +4501,8 @@ public final class GroupCallMessagesContext { state.pinnedMessages.append(Message( id: Message.Id(space: .local, id: pendingSendStarsValue.messageId), stableId: stableId, - author: fromPeer.flatMap(EnginePeer.init), + isIncoming: false, + author: EnginePeer(fromPeer), text: "", entities: [], date: currentTime, diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Messages/SendAsPeers.swift b/submodules/TelegramCore/Sources/TelegramEngine/Messages/SendAsPeers.swift index 63405e9f01..0f3320a497 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Messages/SendAsPeers.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Messages/SendAsPeers.swift @@ -96,7 +96,6 @@ func _internal_cachedPeerSendAsAvailablePeers(account: Account, peerId: PeerId) } } - func _internal_peerSendAsAvailablePeers(accountPeerId: PeerId, network: Network, postbox: Postbox, peerId: PeerId) -> Signal<[SendAsPeer], NoError> { return postbox.transaction { transaction -> Peer? in return transaction.getPeer(peerId) @@ -216,3 +215,113 @@ func _internal_updatePeerSendAsPeer(account: Account, peerId: PeerId, sendAs: Pe } } } + +func _internal_cachedLiveStorySendAsAvailablePeers(account: Account, peerId: PeerId) -> Signal<[SendAsPeer], NoError> { + let key = ValueBoxKey(length: 8) + key.setInt64(0, value: peerId.toInt64()) + return account.postbox.transaction { transaction -> ([SendAsPeer], Int32)? in + let cached = transaction.retrieveItemCacheEntry(id: ItemCacheEntryId(collectionId: Namespaces.CachedItemCollection.cachedLiveStorySendAsPeers, key: key))?.get(CachedSendAsPeers.self) + if let cached = cached { + var peers: [SendAsPeer] = [] + for peerId in cached.peerIds { + if let peer = transaction.getPeer(peerId) { + var subscribers: Int32? + if let cachedData = transaction.getPeerCachedData(peerId: peerId) as? CachedChannelData { + subscribers = cachedData.participantsSummary.memberCount + } + peers.append(SendAsPeer(peer: peer, subscribers: subscribers, isPremiumRequired: cached.premiumRequiredPeerIds.contains(peer.id))) + } + } + return (peers, cached.timestamp) + } else { + return nil + } + } + |> mapToSignal { cachedPeersAndTimestamp -> Signal<[SendAsPeer], NoError> in + let initialSignal: Signal<[SendAsPeer], NoError> + if let (cachedPeers, _) = cachedPeersAndTimestamp { + initialSignal = .single(cachedPeers) + } else { + initialSignal = .complete() + } + return initialSignal + |> then(_internal_liveStorySendAsAvailablePeers(account: account, peerId: peerId) + |> mapToSignal { peers -> Signal<[SendAsPeer], NoError> in + return account.postbox.transaction { transaction -> [SendAsPeer] in + let currentTimestamp = Int32(CFAbsoluteTimeGetCurrent() + kCFAbsoluteTimeIntervalSince1970) + var premiumRequiredPeerIds = Set() + for peer in peers { + if peer.isPremiumRequired { + premiumRequiredPeerIds.insert(peer.peer.id) + } + } + if let entry = CodableEntry(CachedSendAsPeers(peerIds: peers.map { $0.peer.id }, premiumRequiredPeerIds: premiumRequiredPeerIds, timestamp: currentTimestamp)) { + transaction.putItemCacheEntry(id: ItemCacheEntryId(collectionId: Namespaces.CachedItemCollection.cachedLiveStorySendAsPeers, key: key), entry: entry) + } + return peers + } + }) + } +} + +func _internal_liveStorySendAsAvailablePeers(account: Account, peerId: PeerId) -> Signal<[SendAsPeer], NoError> { + return account.postbox.transaction { transaction -> Peer? in + return transaction.getPeer(peerId) + } + |> mapToSignal { peer -> Signal<[SendAsPeer], NoError> in + guard let peer else { + return .single([]) + } + guard let inputPeer = apiInputPeer(peer) else { + return .single([]) + } + + return account.network.request(Api.functions.channels.getSendAs(flags: 1 << 1, peer: inputPeer)) + |> map(Optional.init) + |> `catch` { _ -> Signal in + return .single(nil) + } + |> mapToSignal { result -> Signal<[SendAsPeer], NoError> in + guard let result = result else { + return .single([]) + } + switch result { + case let .sendAsPeers(sendAsPeers, chats, _): + return account.postbox.transaction { transaction -> [SendAsPeer] in + var subscribers: [PeerId: Int32] = [:] + let parsedPeers = AccumulatedPeers(transaction: transaction, chats: chats, users: []) + + var premiumRequiredPeerIds = Set() + for sendAsPeer in sendAsPeers { + if case let .sendAsPeer(flags, peer) = sendAsPeer, (flags & (1 << 0)) != 0 { + premiumRequiredPeerIds.insert(peer.peerId) + } + } + for chat in chats { + if let groupOrChannel = parsedPeers.get(chat.peerId) { + switch chat { + case let .channel(_, _, _, _, _, _, _, _, _, _, _, _, participantsCount, _, _, _, _, _, _, _, _, _, _): + if let participantsCount = participantsCount { + subscribers[groupOrChannel.id] = participantsCount + } + case let .chat(_, _, _, _, participantsCount, _, _, _, _, _): + subscribers[groupOrChannel.id] = participantsCount + default: + break + } + } + } + updatePeers(transaction: transaction, accountPeerId: account.peerId, peers: parsedPeers) + + var peers: [Peer] = [] + for chat in chats { + if let peer = transaction.getPeer(chat.peerId) { + peers.append(peer) + } + } + return peers.map { SendAsPeer(peer: $0, subscribers: subscribers[$0.id], isPremiumRequired: premiumRequiredPeerIds.contains($0.id)) } + } + } + } + } +} diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Messages/Stories.swift b/submodules/TelegramCore/Sources/TelegramEngine/Messages/Stories.swift index 8fc0db0404..66cc3d4e41 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Messages/Stories.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Messages/Stories.swift @@ -520,23 +520,27 @@ public enum Stories { case timestamp case expirationTimestamp case isCloseFriends = "clf" + case isLiveItem = "liv" } public let id: Int32 public let timestamp: Int32 public let expirationTimestamp: Int32 public let isCloseFriends: Bool + public let isLiveItem: Bool public init( id: Int32, timestamp: Int32, expirationTimestamp: Int32, - isCloseFriends: Bool + isCloseFriends: Bool, + isLiveItem: Bool ) { self.id = id self.timestamp = timestamp self.expirationTimestamp = expirationTimestamp self.isCloseFriends = isCloseFriends + self.isLiveItem = isLiveItem } public init(from decoder: Decoder) throws { @@ -546,6 +550,7 @@ public enum Stories { self.timestamp = try container.decode(Int32.self, forKey: .timestamp) self.expirationTimestamp = try container.decode(Int32.self, forKey: .expirationTimestamp) self.isCloseFriends = try container.decodeIfPresent(Bool.self, forKey: .isCloseFriends) ?? false + self.isLiveItem = try container.decodeIfPresent(Bool.self, forKey: .isLiveItem) ?? false } public func encode(to encoder: Encoder) throws { @@ -555,6 +560,7 @@ public enum Stories { try container.encode(self.timestamp, forKey: .timestamp) try container.encode(self.expirationTimestamp, forKey: .expirationTimestamp) try container.encode(self.isCloseFriends, forKey: .isCloseFriends) + try container.encode(self.isLiveItem, forKey: .isLiveItem) } public static func ==(lhs: Placeholder, rhs: Placeholder) -> Bool { @@ -570,6 +576,9 @@ public enum Stories { if lhs.isCloseFriends != rhs.isCloseFriends { return false } + if lhs.isLiveItem != rhs.isLiveItem { + return false + } return true } } @@ -628,8 +637,8 @@ public enum Stories { switch self { case let .item(item): return item.media is TelegramMediaLiveStream - case .placeholder: - return false + case let .placeholder(placeholder): + return placeholder.isLiveItem } } @@ -2326,7 +2335,8 @@ extension Stories.StoredItem { } case let .storyItemSkipped(flags, id, date, expireDate): let isCloseFriends = (flags & (1 << 8)) != 0 - self = .placeholder(Stories.Placeholder(id: id, timestamp: date, expirationTimestamp: expireDate, isCloseFriends: isCloseFriends)) + let isLiveItem = (flags & (1 << 9)) != 0 + self = .placeholder(Stories.Placeholder(id: id, timestamp: date, expirationTimestamp: expireDate, isCloseFriends: isCloseFriends, isLiveItem: isLiveItem)) case .storyItemDeleted: return nil } diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Peers/TelegramEnginePeers.swift b/submodules/TelegramCore/Sources/TelegramEngine/Peers/TelegramEnginePeers.swift index 064db9b593..2c246af27d 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Peers/TelegramEnginePeers.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Peers/TelegramEnginePeers.swift @@ -1175,6 +1175,10 @@ public extension TelegramEngine { return _internal_updatePeerSendAsPeer(account: self.account, peerId: peerId, sendAs: sendAs) } + public func liveStorySendAsAvailablePeers(peerId: PeerId) -> Signal<[SendAsPeer], NoError> { + return _internal_cachedLiveStorySendAsAvailablePeers(account: self.account, peerId: peerId) + } + public func updatePeerReactionSettings(peerId: PeerId, reactionSettings: PeerReactionSettings) -> Signal { return _internal_updatePeerReactionSettings(account: account, peerId: peerId, reactionSettings: reactionSettings) } diff --git a/submodules/TelegramUI/BUILD b/submodules/TelegramUI/BUILD index ec8fff1b06..601bf2830e 100644 --- a/submodules/TelegramUI/BUILD +++ b/submodules/TelegramUI/BUILD @@ -495,6 +495,7 @@ swift_library( "//submodules/TelegramUI/Components/EdgeEffect", "//submodules/TelegramUI/Components/AttachmentFileController", "//submodules/TelegramUI/Components/Contacts/NewContactScreen", + "//submodules/TelegramUI/Components/Chat/ChatSendAsContextMenu", ] + select({ "@build_bazel_rules_apple//apple:ios_arm64": appcenter_targets, "//build-system:ios_sim_arm64": [], diff --git a/submodules/TelegramUI/Components/Chat/ChatSendAsContextMenu/BUILD b/submodules/TelegramUI/Components/Chat/ChatSendAsContextMenu/BUILD new file mode 100644 index 0000000000..1ade4c0550 --- /dev/null +++ b/submodules/TelegramUI/Components/Chat/ChatSendAsContextMenu/BUILD @@ -0,0 +1,29 @@ +load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library") + +swift_library( + name = "ChatSendAsContextMenu", + module_name = "ChatSendAsContextMenu", + srcs = glob([ + "Sources/**/*.swift", + ]), + copts = [ + "-warnings-as-errors", + ], + deps = [ + "//submodules/Display", + "//submodules/AsyncDisplayKit", + "//submodules/SSignalKit/SwiftSignalKit", + "//submodules/TelegramPresentationData", + "//submodules/Postbox", + "//submodules/TelegramCore", + "//submodules/AppBundle", + "//submodules/ContextUI", + "//submodules/TelegramStringFormatting", + "//submodules/AvatarNode", + "//submodules/AccountContext", + "//submodules/UndoUI", + ], + visibility = [ + "//visibility:public", + ], +) diff --git a/submodules/TelegramUI/Sources/ChatSendAsPeerListContextItem.swift b/submodules/TelegramUI/Components/Chat/ChatSendAsContextMenu/Sources/ChatSendAsPeerListContextItem.swift similarity index 94% rename from submodules/TelegramUI/Sources/ChatSendAsPeerListContextItem.swift rename to submodules/TelegramUI/Components/Chat/ChatSendAsContextMenu/Sources/ChatSendAsPeerListContextItem.swift index 0dc33acc28..a6a877ccea 100644 --- a/submodules/TelegramUI/Sources/ChatSendAsPeerListContextItem.swift +++ b/submodules/TelegramUI/Components/Chat/ChatSendAsContextMenu/Sources/ChatSendAsPeerListContextItem.swift @@ -13,24 +13,26 @@ import AvatarNode import AccountContext import UndoUI -final class ChatSendAsPeerListContextItem: ContextMenuCustomItem { +public final class ChatSendAsPeerListContextItem: ContextMenuCustomItem { let context: AccountContext let chatPeerId: PeerId let peers: [SendAsPeer] let selectedPeerId: PeerId? let isPremium: Bool + let action: (EnginePeer) -> Void let presentToast: (EnginePeer) -> Void - init(context: AccountContext, chatPeerId: PeerId, peers: [SendAsPeer], selectedPeerId: PeerId?, isPremium: Bool, presentToast: @escaping (EnginePeer) -> Void) { + public init(context: AccountContext, chatPeerId: PeerId, peers: [SendAsPeer], selectedPeerId: PeerId?, isPremium: Bool, action: @escaping (EnginePeer) -> Void, presentToast: @escaping (EnginePeer) -> Void) { self.context = context self.chatPeerId = chatPeerId self.peers = peers self.selectedPeerId = selectedPeerId self.isPremium = isPremium + self.action = action self.presentToast = presentToast } - func node(presentationData: PresentationData, getController: @escaping () -> ContextControllerProtocol?, actionSelected: @escaping (ContextMenuActionResult) -> Void) -> ContextMenuCustomNode { + public func node(presentationData: PresentationData, getController: @escaping () -> ContextControllerProtocol?, actionSelected: @escaping (ContextMenuActionResult) -> Void) -> ContextMenuCustomNode { return ChatSendAsPeerListContextItemNode(presentationData: presentationData, item: self, getController: getController, actionSelected: actionSelected) } } @@ -115,7 +117,7 @@ private final class ChatSendAsPeerListContextItemNode: ASDisplayNode, ContextMen } if peer.peer.id != item.selectedPeerId { - let _ = item.context.engine.peers.updatePeerSendAsPeer(peerId: item.chatPeerId, sendAs: peer.peer.id).startStandalone() + item.action(EnginePeer(peer.peer)) } }) let actionNode = ContextActionNode(presentationData: presentationData, action: action, getController: getController, actionSelected: actionSelected, requestLayout: {}, requestUpdateAction: { _, _ in diff --git a/submodules/TelegramUI/Sources/ChatSendAsPeerTitleContextItem.swift b/submodules/TelegramUI/Components/Chat/ChatSendAsContextMenu/Sources/ChatSendAsPeerTitleContextItem.swift similarity index 100% rename from submodules/TelegramUI/Sources/ChatSendAsPeerTitleContextItem.swift rename to submodules/TelegramUI/Components/Chat/ChatSendAsContextMenu/Sources/ChatSendAsPeerTitleContextItem.swift diff --git a/submodules/TelegramUI/Components/Chat/ChatSendStarsScreen/Sources/ChatSendStarsScreen.swift b/submodules/TelegramUI/Components/Chat/ChatSendStarsScreen/Sources/ChatSendStarsScreen.swift index 9b569233aa..46661824be 100644 --- a/submodules/TelegramUI/Components/Chat/ChatSendStarsScreen/Sources/ChatSendStarsScreen.swift +++ b/submodules/TelegramUI/Components/Chat/ChatSendStarsScreen/Sources/ChatSendStarsScreen.swift @@ -1755,35 +1755,37 @@ private final class ChatSendStarsScreenComponent: Component { switch component.initialData.subjectInitialData { case let .react(reactData): - var sendAsPeers: [EnginePeer] = [reactData.myPeer] - sendAsPeers.append(contentsOf: self.channelsForPublicReaction) - - let currentMyPeer = self.currentMyPeer ?? reactData.myPeer - - let peerSelectorButtonSize = self.peerSelectorButton.update( - transition: transition, - component: AnyComponent(PeerSelectorBadgeComponent( - context: component.context, - theme: environment.theme, - strings: environment.strings, - peer: currentMyPeer, - action: { [weak self] sourceView in - guard let self else { - return + if case .message = reactData.reactSubject { + var sendAsPeers: [EnginePeer] = [reactData.myPeer] + sendAsPeers.append(contentsOf: self.channelsForPublicReaction) + + let currentMyPeer = self.currentMyPeer ?? reactData.myPeer + + let peerSelectorButtonSize = self.peerSelectorButton.update( + transition: transition, + component: AnyComponent(PeerSelectorBadgeComponent( + context: component.context, + theme: environment.theme, + strings: environment.strings, + peer: currentMyPeer, + action: { [weak self] sourceView in + guard let self else { + return + } + self.displayTargetSelectionMenu(sourceView: sourceView) } - self.displayTargetSelectionMenu(sourceView: sourceView) + )), + environment: {}, + containerSize: CGSize(width: 120.0, height: 100.0) + ) + let peerSelectorButtonFrame = CGRect(origin: CGPoint(x: availableSize.width - sideInset - peerSelectorButtonSize.width, y: floor((78.0 - peerSelectorButtonSize.height) * 0.5)), size: peerSelectorButtonSize) + if let peerSelectorButtonView = self.peerSelectorButton.view { + if peerSelectorButtonView.superview == nil { + self.navigationBarContainer.addSubview(peerSelectorButtonView) } - )), - environment: {}, - containerSize: CGSize(width: 120.0, height: 100.0) - ) - let peerSelectorButtonFrame = CGRect(origin: CGPoint(x: availableSize.width - sideInset - peerSelectorButtonSize.width, y: floor((78.0 - peerSelectorButtonSize.height) * 0.5)), size: peerSelectorButtonSize) - if let peerSelectorButtonView = self.peerSelectorButton.view { - if peerSelectorButtonView.superview == nil { - self.navigationBarContainer.addSubview(peerSelectorButtonView) + transition.setFrame(view: peerSelectorButtonView, frame: peerSelectorButtonFrame) + peerSelectorButtonView.isHidden = sendAsPeers.count <= 1 } - transition.setFrame(view: peerSelectorButtonView, frame: peerSelectorButtonFrame) - peerSelectorButtonView.isHidden = sendAsPeers.count <= 1 } case .liveStreamMessage: break @@ -1967,6 +1969,7 @@ private final class ChatSendStarsScreenComponent: Component { id: 1 ), stableId: 0, + isIncoming: false, author: liveStreamMessage.myPeer, text: liveStreamMessage.text.string, entities: entities, @@ -1981,6 +1984,7 @@ private final class ChatSendStarsScreenComponent: Component { id: 1 ), stableId: 0, + isIncoming: false, author: reactData.myPeer, text: "", entities: [], @@ -2799,7 +2803,7 @@ public class ChatSendStarsScreen: ViewControllerComponentContainer { } } - public static func initialData(context: AccountContext, peerId: EnginePeer.Id, reactSubject: ReactSubject, topPeers: [ReactionsMessageAttribute.TopPeer], completion: @escaping (Int64, TelegramPaidReactionPrivacy, Bool, TransitionOut) -> Void) -> Signal { + public static func initialData(context: AccountContext, peerId: EnginePeer.Id, myPeer: EnginePeer? = nil, reactSubject: ReactSubject, topPeers: [ReactionsMessageAttribute.TopPeer], completion: @escaping (Int64, TelegramPaidReactionPrivacy, Bool, TransitionOut) -> Void) -> Signal { let balance: Signal if let starsContext = context.starsContext { balance = starsContext.state @@ -2871,7 +2875,8 @@ public class ChatSendStarsScreen: ViewControllerComponentContainer { defaultPrivacyPeer ) |> map { peerAndTopPeerMap, balance, channelsForPublicReaction, defaultPrivacyPeer -> InitialData? in - let (peer, myPeer, topPeerMap) = peerAndTopPeerMap + let (peer, myPeerValue, topPeerMap) = peerAndTopPeerMap + let myPeer = myPeer ?? myPeerValue guard let peer, let myPeer else { return nil } diff --git a/submodules/TelegramUI/Components/Chat/ChatTextInputPanelNode/BUILD b/submodules/TelegramUI/Components/Chat/ChatTextInputPanelNode/BUILD index c2ce20425f..46eb4b449b 100644 --- a/submodules/TelegramUI/Components/Chat/ChatTextInputPanelNode/BUILD +++ b/submodules/TelegramUI/Components/Chat/ChatTextInputPanelNode/BUILD @@ -65,6 +65,7 @@ swift_library( "//submodules/TelegramUI/Components/Chat/ChatInputContextPanelNode", "//submodules/TelegramUI/Components/AnimatedTextComponent", "//submodules/TelegramUI/Components/RasterizedCompositionComponent", + "//submodules/TelegramUI/Components/StarsParticleEffect", ], visibility = [ "//visibility:public", diff --git a/submodules/TelegramUI/Components/Chat/ChatTextInputPanelNode/Sources/ChatTextInputPanelComponent.swift b/submodules/TelegramUI/Components/Chat/ChatTextInputPanelNode/Sources/ChatTextInputPanelComponent.swift index a17ba719e3..fc624f6476 100644 --- a/submodules/TelegramUI/Components/Chat/ChatTextInputPanelNode/Sources/ChatTextInputPanelComponent.swift +++ b/submodules/TelegramUI/Components/Chat/ChatTextInputPanelNode/Sources/ChatTextInputPanelComponent.swift @@ -103,6 +103,38 @@ public final class ChatTextInputPanelComponent: Component { } } + public final class SendAsConfiguration: Equatable { + public let currentPeer: EnginePeer + public let subscriberCount: Int? + public let isPremiumLocked: Bool + public let isSelecting: Bool + public let action: (UIView, ContextGesture?) -> Void + + public init(currentPeer: EnginePeer, subscriberCount: Int?, isPremiumLocked: Bool, isSelecting: Bool, action: @escaping (UIView, ContextGesture?) -> Void) { + self.currentPeer = currentPeer + self.subscriberCount = subscriberCount + self.isPremiumLocked = isPremiumLocked + self.isSelecting = isSelecting + self.action = action + } + + public static func ==(lhs: SendAsConfiguration, rhs: SendAsConfiguration) -> Bool { + if lhs.currentPeer != rhs.currentPeer { + return false + } + if lhs.subscriberCount != rhs.subscriberCount { + return false + } + if lhs.isPremiumLocked != rhs.isPremiumLocked { + return false + } + if lhs.isSelecting != rhs.isSelecting { + return false + } + return true + } + } + let externalState: ExternalState let context: AccountContext let theme: PresentationTheme @@ -111,6 +143,7 @@ public final class ChatTextInputPanelComponent: Component { let inlineActions: [InlineAction] let leftAction: LeftAction? let rightAction: RightAction? + let sendAsConfiguration: SendAsConfiguration? let placeholder: String let paidMessagePrice: StarsAmount? let sendColor: UIColor? @@ -131,6 +164,7 @@ public final class ChatTextInputPanelComponent: Component { inlineActions: [InlineAction], leftAction: LeftAction?, rightAction: RightAction?, + sendAsConfiguration: SendAsConfiguration?, placeholder: String, paidMessagePrice: StarsAmount?, sendColor: UIColor?, @@ -150,6 +184,7 @@ public final class ChatTextInputPanelComponent: Component { self.inlineActions = inlineActions self.leftAction = leftAction self.rightAction = rightAction + self.sendAsConfiguration = sendAsConfiguration self.placeholder = placeholder self.paidMessagePrice = paidMessagePrice self.sendColor = sendColor @@ -187,6 +222,9 @@ public final class ChatTextInputPanelComponent: Component { if lhs.rightAction != rhs.rightAction { return false } + if lhs.sendAsConfiguration != rhs.sendAsConfiguration { + return false + } if lhs.placeholder != rhs.placeholder { return false } @@ -582,7 +620,11 @@ public final class ChatTextInputPanelComponent: Component { }, openInviteRequests: { }, - openSendAsPeer: { _, _ in + openSendAsPeer: { [weak self] sourceNode, gesture in + guard let self, let component = self.component, let sendAsConfiguration = component.sendAsConfiguration else { + return + } + sendAsConfiguration.action(sourceNode.view, gesture) }, presentChatRequestAdminInfo: { }, @@ -722,6 +764,14 @@ public final class ChatTextInputPanelComponent: Component { } presentationInterfaceState = presentationInterfaceState.updatedSendPaidMessageStars(component.paidMessagePrice) + if let sendAsConfiguration = component.sendAsConfiguration { + presentationInterfaceState = presentationInterfaceState.updatedSendAsPeers([SendAsPeer( + peer: sendAsConfiguration.currentPeer._asPeer(), + subscribers: sendAsConfiguration.subscriberCount.flatMap(Int32.init(clamping:)), + isPremiumRequired: sendAsConfiguration.isPremiumLocked + )]).updatedShowSendAsPeers(sendAsConfiguration.isSelecting).updatedCurrentSendAsPeerId(sendAsConfiguration.currentPeer.id) + } + let panelNode: ChatTextInputPanelNode if let current = self.panelNode { panelNode = current diff --git a/submodules/TelegramUI/Components/Chat/ChatTextInputPanelNode/Sources/StarReactionButtonComponent.swift b/submodules/TelegramUI/Components/Chat/ChatTextInputPanelNode/Sources/StarReactionButtonComponent.swift index 594976a7d7..2e9ac46f63 100644 --- a/submodules/TelegramUI/Components/Chat/ChatTextInputPanelNode/Sources/StarReactionButtonComponent.swift +++ b/submodules/TelegramUI/Components/Chat/ChatTextInputPanelNode/Sources/StarReactionButtonComponent.swift @@ -5,6 +5,109 @@ import TelegramPresentationData import ComponentFlow import GlassBackgroundComponent import AnimatedTextComponent +import StarsParticleEffect + +final class StarReactionButtonBadgeComponent: Component { + let theme: PresentationTheme + let count: Int + let isFilled: Bool + + init( + theme: PresentationTheme, + count: Int, + isFilled: Bool + ) { + self.theme = theme + self.count = count + self.isFilled = isFilled + } + + static func ==(lhs: StarReactionButtonBadgeComponent, rhs: StarReactionButtonBadgeComponent) -> 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 text = ComponentView() + + private var component: StarReactionButtonBadgeComponent? + private weak var state: EmptyComponentState? + + override init(frame: CGRect) { + self.backgroundView = GlassBackgroundView() + + super.init(frame: frame) + + self.addSubview(self.backgroundView) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + func update(component: StarReactionButtonBadgeComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { + self.component = component + self.state = state + + let height: CGFloat = 15.0 + let sideInset: CGFloat = 4.0 + + let textSize = self.text.update( + transition: transition, + component: AnyComponent(AnimatedTextComponent( + font: Font.semibold(10.0), + color: component.theme.chat.inputPanel.panelControlColor, + items: [AnimatedTextComponent.Item(id: AnyHashable(0), content: .text(countString(Int64(component.count))))], + noDelay: true + )), + environment: {}, + containerSize: CGSize(width: 100.0, height: 100.0) + ) + + let size = CGSize(width: textSize.width + sideInset * 2.0, height: height) + let backgroundFrame = CGRect(origin: CGPoint(), size: size) + + let backgroundTintColor: GlassBackgroundView.TintColor + if component.isFilled { + backgroundTintColor = .init(kind: .custom, color: UIColor(rgb: 0xFFB10D)) + } else { + backgroundTintColor = .init(kind: .panel, color: component.theme.chat.inputPanel.inputBackgroundColor.withMultipliedAlpha(0.7)) + } + + self.backgroundView.update(size: backgroundFrame.size, cornerRadius: backgroundFrame.height * 0.5, isDark: component.theme.overallDarkAppearance, tintColor: backgroundTintColor, isInteractive: true, transition: transition) + + if let textView = self.text.view { + let textFrame = textSize.centered(in: CGRect(origin: CGPoint(), size: size)) + + if textView.superview == nil { + textView.isUserInteractionEnabled = false + self.backgroundView.contentView.addSubview(textView) + } + transition.setFrame(view: textView, frame: textFrame) + } + + 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) + } +} + final class StarReactionButtonComponent: Component { let theme: PresentationTheme @@ -45,8 +148,11 @@ final class StarReactionButtonComponent: Component { final class View: UIView { private let backgroundView: GlassBackgroundView + private let backgroundEffectLayer: StarsParticleEffectLayer + private let backgroundMaskView: UIView + private let backgroundBadgeMask: UIImageView private let iconView: UIImageView - private var text: ComponentView? + private var badge: ComponentView? private var longTapRecognizer: TapLongTapOrDoubleTapGestureRecognizer? @@ -55,6 +161,20 @@ final class StarReactionButtonComponent: Component { override init(frame: CGRect) { self.backgroundView = GlassBackgroundView() + self.backgroundMaskView = UIView() + self.backgroundBadgeMask = UIImageView() + self.backgroundMaskView.addSubview(self.backgroundBadgeMask) + + self.backgroundEffectLayer = StarsParticleEffectLayer() + self.backgroundView.contentView.layer.addSublayer(self.backgroundEffectLayer) + + //self.backgroundView.mask = self.backgroundMaskView + + self.backgroundMaskView.backgroundColor = .white + if let filter = CALayer.luminanceToAlpha() { + self.backgroundMaskView.layer.filters = [filter] + } + self.iconView = UIImageView() super.init(frame: frame) @@ -96,48 +216,53 @@ final class StarReactionButtonComponent: Component { self.component = component self.state = state - let leftInset: CGFloat = 12.0 - let rightInset: CGFloat = 12.0 - let textSpacing: CGFloat = 2.0 - - var size = CGSize(width: 40.0, height: 40.0) - var textSize: CGSize? + let size = CGSize(width: 40.0, height: 40.0) if self.iconView.image == nil { self.iconView.image = UIImage(bundleImageName: "Premium/Stars/ButtonStar")?.withRenderingMode(.alwaysTemplate) } if component.count != 0 { - let text: ComponentView - var textTransition = transition - if let current = self.text { - text = current + let badge: ComponentView + var badgeTransition = transition + if let current = self.badge { + badge = current } else { - textTransition = textTransition.withAnimation(.none) - text = ComponentView() - self.text = text + badgeTransition = badgeTransition.withAnimation(.none) + badge = ComponentView() + self.badge = badge } - let textSizeValue = text.update( - transition: textTransition, - component: AnyComponent(AnimatedTextComponent( - font: Font.regular(17.0), - color: component.theme.chat.inputPanel.panelControlColor, - items: [AnimatedTextComponent.Item(id: AnyHashable(0), content: .number(component.count, minDigits: 1))], - noDelay: true + let badgeSize = badge.update( + transition: badgeTransition, + component: AnyComponent(StarReactionButtonBadgeComponent( + theme: component.theme, + count: component.count, + isFilled: component.isFilled )), environment: {}, containerSize: CGSize(width: 100.0, height: 100.0) ) - textSize = textSizeValue - if let image = self.iconView.image { - size.width = leftInset + image.size.width + textSpacing + textSizeValue.width + rightInset + if let badgeView = badge.view { + var badgeFrame = CGRect(origin: CGPoint(x: min(size.width + 6.0 - badgeSize.width, floorToScreenPixels(size.width - 4.0 - badgeSize.width * 0.5)), y: -3.0), size: badgeSize) + if badgeSize.width > size.width * 0.8 { + badgeFrame.origin.x = floor((size.width - badgeSize.width) * 0.5) + } + + if badgeView.superview == nil { + badgeView.isUserInteractionEnabled = false + self.backgroundView.contentView.addSubview(badgeView) + badgeView.frame = badgeFrame + transition.animateScale(view: badgeView, from: 0.001, to: 1.0) + transition.animateAlpha(view: badgeView, from: 0.0, to: 1.0) + } + transition.setFrame(view: badgeView, frame: badgeFrame) } - } else if let text = self.text { - self.text = nil - if let textView = text.view { - transition.setScale(view: textView, scale: 0.001) - transition.setAlpha(view: textView, alpha: 0.0, completion: { [weak textView] _ in - textView?.removeFromSuperview() + } else if let badge = self.badge { + self.badge = nil + if let badgeView = badge.view { + transition.setScale(view: badgeView, scale: 0.001) + transition.setAlpha(view: badgeView, alpha: 0.0, completion: { [weak badgeView] _ in + badgeView?.removeFromSuperview() }) } } @@ -153,30 +278,24 @@ final class StarReactionButtonComponent: Component { self.backgroundView.update(size: backgroundFrame.size, cornerRadius: backgroundFrame.height * 0.5, isDark: component.theme.overallDarkAppearance, tintColor: backgroundTintColor, isInteractive: true, transition: transition) transition.setFrame(view: self.backgroundView, frame: backgroundFrame) + transition.setFrame(view: self.backgroundMaskView, frame: CGRect(origin: CGPoint(), size: backgroundFrame.size)) + + transition.setFrame(layer: self.backgroundEffectLayer, frame: CGRect(origin: CGPoint(), size: backgroundFrame.size)) + self.backgroundEffectLayer.update(color: UIColor(white: 1.0, alpha: 0.25), rate: 10.0, size: backgroundFrame.size, cornerRadius: backgroundFrame.height * 0.5, transition: transition) + + let badgeDiameter: CGFloat = 15.0 + if self.backgroundBadgeMask.image == nil { + self.backgroundBadgeMask.image = generateStretchableFilledCircleImage(diameter: badgeDiameter + 1.0 * 2.0, color: .black) + } + let badgeWidth: CGFloat = 20.0 + let badgeFrame = CGRect(origin: CGPoint(x: backgroundFrame.width - badgeWidth, y: 0.0), size: CGSize(width: badgeWidth, height: badgeDiameter)) + transition.setFrame(view: self.backgroundBadgeMask, frame: badgeFrame.insetBy(dx: -1.0, dy: -1.0)) self.iconView.tintColor = component.theme.chat.inputPanel.panelControlColor if let image = self.iconView.image { - let iconFrame: CGRect - if textSize == nil { - iconFrame = image.size.centered(in: CGRect(origin: CGPoint(), size: backgroundFrame.size)) - } else { - iconFrame = CGRect(origin: CGPoint(x: leftInset, y: floor((backgroundFrame.height - image.size.height) * 0.5)), size: image.size) - } + let iconFrame = image.size.centered(in: CGRect(origin: CGPoint(), size: backgroundFrame.size)) transition.setFrame(view: self.iconView, frame: iconFrame) - - if let textView = self.text?.view, let textSize { - let textFrame = CGRect(origin: CGPoint(x: iconFrame.maxX + textSpacing, y: floor((backgroundFrame.height - textSize.height) * 0.5)), size: textSize) - - if textView.superview == nil { - textView.isUserInteractionEnabled = false - self.backgroundView.contentView.addSubview(textView) - textView.frame = textFrame - transition.animateScale(view: textView, from: 0.001, to: 1.0) - transition.animateAlpha(view: textView, from: 0.0, to: 1.0) - } - transition.setFrame(view: textView, frame: textFrame) - } } return size diff --git a/submodules/TelegramUI/Components/GlassBackgroundComponent/Sources/GlassBackgroundComponent.swift b/submodules/TelegramUI/Components/GlassBackgroundComponent/Sources/GlassBackgroundComponent.swift index c0d0710189..fb7e2f54cb 100644 --- a/submodules/TelegramUI/Components/GlassBackgroundComponent/Sources/GlassBackgroundComponent.swift +++ b/submodules/TelegramUI/Components/GlassBackgroundComponent/Sources/GlassBackgroundComponent.swift @@ -383,7 +383,6 @@ public class GlassBackgroundView: UIView { nativeView.layer.cornerRadius = cornerRadius nativeView.frame = CGRect(origin: CGPoint(), size: size) } else { - //nativeView.layer.cornerRadius = cornerRadius transition.setCornerRadius(layer: nativeView.layer, cornerRadius: cornerRadius) let nativeFrame = CGRect(origin: CGPoint(), size: size) diff --git a/submodules/TelegramUI/Components/MessageInputPanelComponent/Sources/MessageInputPanelComponent.swift b/submodules/TelegramUI/Components/MessageInputPanelComponent/Sources/MessageInputPanelComponent.swift index 7b491e959d..ec7b716d98 100644 --- a/submodules/TelegramUI/Components/MessageInputPanelComponent/Sources/MessageInputPanelComponent.swift +++ b/submodules/TelegramUI/Components/MessageInputPanelComponent/Sources/MessageInputPanelComponent.swift @@ -192,6 +192,38 @@ public final class MessageInputPanelComponent: Component { } } + public final class SendAsConfiguration: Equatable { + public let currentPeer: EnginePeer + public let subscriberCount: Int? + public let isPremiumLocked: Bool + public let isSelecting: Bool + public let action: (UIView, ContextGesture?) -> Void + + public init(currentPeer: EnginePeer, subscriberCount: Int?, isPremiumLocked: Bool, isSelecting: Bool, action: @escaping (UIView, ContextGesture?) -> Void) { + self.currentPeer = currentPeer + self.subscriberCount = subscriberCount + self.isPremiumLocked = isPremiumLocked + self.isSelecting = isSelecting + self.action = action + } + + public static func ==(lhs: SendAsConfiguration, rhs: SendAsConfiguration) -> Bool { + if lhs.currentPeer != rhs.currentPeer { + return false + } + if lhs.subscriberCount != rhs.subscriberCount { + return false + } + if lhs.isPremiumLocked != rhs.isPremiumLocked { + return false + } + if lhs.isSelecting != rhs.isSelecting { + return false + } + return true + } + } + public let externalState: ExternalState public let context: AccountContext public let theme: PresentationTheme @@ -255,6 +287,7 @@ public final class MessageInputPanelComponent: Component { public let toggleLiveChatExpanded: (() -> Void)? public let sendStarsAction: ((UIView, Bool) -> Void)? public let starStars: StarStats? + public let sendAsConfiguration: SendAsConfiguration? public init( externalState: ExternalState, @@ -319,7 +352,8 @@ public final class MessageInputPanelComponent: Component { liveChatState: LiveChatState? = nil, toggleLiveChatExpanded: (() -> Void)? = nil, sendStarsAction: ((UIView, Bool) -> Void)? = nil, - starStars: StarStats? = nil + starStars: StarStats? = nil, + sendAsConfiguration: SendAsConfiguration? = nil ) { self.externalState = externalState self.context = context @@ -384,6 +418,7 @@ public final class MessageInputPanelComponent: Component { self.toggleLiveChatExpanded = toggleLiveChatExpanded self.sendStarsAction = sendStarsAction self.starStars = starStars + self.sendAsConfiguration = sendAsConfiguration } public static func ==(lhs: MessageInputPanelComponent, rhs: MessageInputPanelComponent) -> Bool { @@ -519,6 +554,9 @@ public final class MessageInputPanelComponent: Component { if lhs.starStars != rhs.starStars { return false } + if lhs.sendAsConfiguration != rhs.sendAsConfiguration { + return false + } return true } @@ -968,6 +1006,16 @@ public final class MessageInputPanelComponent: Component { } } + let sendAsConfiguration = component.sendAsConfiguration.flatMap { value in + return ChatTextInputPanelComponent.SendAsConfiguration( + currentPeer: value.currentPeer, + subscriberCount: value.subscriberCount, + isPremiumLocked: value.isPremiumLocked, + isSelecting: value.isSelecting, + action: value.action + ) + } + let inputPanelSize = inputPanel.update( transition: transition, component: AnyComponent(ChatTextInputPanelComponent( @@ -994,6 +1042,7 @@ public final class MessageInputPanelComponent: Component { } component.sendStarsAction?(sourceView, true) }), + sendAsConfiguration: sendAsConfiguration, placeholder: placeholder, paidMessagePrice: component.sendPaidMessageStars, sendColor: component.sendPaidMessageStars.flatMap { value in diff --git a/submodules/TelegramUI/Components/StarsParticleEffect/Sources/ActionPanelComponent.swift b/submodules/TelegramUI/Components/StarsParticleEffect/Sources/ActionPanelComponent.swift index 1d211b19f8..def26465ca 100644 --- a/submodules/TelegramUI/Components/StarsParticleEffect/Sources/ActionPanelComponent.swift +++ b/submodules/TelegramUI/Components/StarsParticleEffect/Sources/ActionPanelComponent.swift @@ -23,7 +23,7 @@ public final class StarsParticleEffectLayer: SimpleLayer { fatalError("init(coder:) has not been implemented") } - private func setup() { + private func setup(rate: CGFloat) { guard let currentColor = self.currentColor else { return } @@ -32,7 +32,7 @@ public final class StarsParticleEffectLayer: SimpleLayer { let emitter = CAEmitterCell() emitter.name = "emitter" emitter.contents = UIImage(bundleImageName: "Premium/Stars/Particle")?.cgImage - emitter.birthRate = 25.0 + emitter.birthRate = Float(rate) emitter.lifetime = 2.0 emitter.velocity = 12.0 emitter.velocityRange = 3 @@ -56,10 +56,10 @@ public final class StarsParticleEffectLayer: SimpleLayer { self.emitterLayer.emitterCells = [emitter] } - public func update(color: UIColor, size: CGSize, cornerRadius: CGFloat, transition: ComponentTransition) { + public func update(color: UIColor, rate: CGFloat = 25.0, size: CGSize, cornerRadius: CGFloat, transition: ComponentTransition) { if self.emitterLayer.emitterCells == nil || self.currentColor != color { self.currentColor = color - self.setup() + self.setup(rate: rate) } self.emitterLayer.emitterShape = .circle self.emitterLayer.emitterSize = CGSize(width: size.width * 0.7, height: size.height * 0.7) diff --git a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/BUILD b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/BUILD index 7333c3e16d..8d049bc116 100644 --- a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/BUILD +++ b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/BUILD @@ -110,6 +110,9 @@ swift_library( "//submodules/TelegramUI/Components/StarsParticleEffect", "//submodules/TelegramUI/Components/AnimatedTextComponent", "//submodules/TelegramUI/Components/AdminUserActionsSheet", + "//submodules/TelegramUI/Components/Chat/ChatSendAsContextMenu", + "//submodules/Components/HierarchyTrackingLayer", + "//submodules/Utils/LokiRng", ], visibility = [ "//visibility:public", diff --git a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/LiveChatReactionStreamView.swift b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/LiveChatReactionStreamView.swift new file mode 100644 index 0000000000..bafe924698 --- /dev/null +++ b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/LiveChatReactionStreamView.swift @@ -0,0 +1,320 @@ +import Foundation +import UIKit +import Display +import ComponentFlow +import TelegramCore +import AvatarNode +import AppBundle +import AccountContext +import HierarchyTrackingLayer +import LokiRng + +private let gradientColors: [NSArray] = [ + [UIColor(rgb: 0xff516a).cgColor, UIColor(rgb: 0xff885e).cgColor], + [UIColor(rgb: 0xffa85c).cgColor, UIColor(rgb: 0xffcd6a).cgColor], + [UIColor(rgb: 0x665fff).cgColor, UIColor(rgb: 0x82b1ff).cgColor], + [UIColor(rgb: 0x54cb68).cgColor, UIColor(rgb: 0xa0de7e).cgColor], + [UIColor(rgb: 0x4acccd).cgColor, UIColor(rgb: 0x00fcfd).cgColor], + [UIColor(rgb: 0x2a9ef1).cgColor, UIColor(rgb: 0x72d5fd).cgColor], + [UIColor(rgb: 0xd669ed).cgColor, UIColor(rgb: 0xe0a2f3).cgColor], +] + +private func avatarViewLettersImage(size: CGSize, peerId: EnginePeer.Id, letters: [String], isStory: Bool) -> UIImage? { + UIGraphicsBeginImageContextWithOptions(size, false, 2.0) + let context = UIGraphicsGetCurrentContext() + + context?.beginPath() + if isStory { + context?.addEllipse(in: CGRect(x: 0.0, y: 0.0, width: size.width, height: size.height).insetBy(dx: 4.0, dy: 4.0)) + } else { + context?.addEllipse(in: CGRect(x: 0.0, y: 0.0, width: size.width, height: size.height)) + } + context?.clip() + + let colorIndex: Int + if peerId.namespace == .max { + colorIndex = 0 + } else { + colorIndex = abs(Int(clamping: peerId.id._internalGetInt64Value())) + } + + let colorsArray = gradientColors[colorIndex % gradientColors.count] + var locations: [CGFloat] = [1.0, 0.0] + let gradient = CGGradient(colorsSpace: deviceColorSpace, colors: colorsArray, locations: &locations)! + + context?.drawLinearGradient(gradient, start: CGPoint(), end: CGPoint(x: 0.0, y: size.height), options: CGGradientDrawingOptions()) + + context?.setBlendMode(.normal) + + let string = letters.count == 0 ? "" : (letters[0] + (letters.count == 1 ? "" : letters[1])) + let attributedString = NSAttributedString(string: string, attributes: [NSAttributedString.Key.font: UIFont.systemFont(ofSize: 8.0), NSAttributedString.Key.foregroundColor: UIColor.white]) + + let line = CTLineCreateWithAttributedString(attributedString) + let lineBounds = CTLineGetBoundsWithOptions(line, .useGlyphPathBounds) + + let lineOffset = CGPoint(x: string == "B" ? 1.0 : 0.0, y: 0.0) + let lineOrigin = CGPoint(x: floor(-lineBounds.origin.x + (size.width - lineBounds.size.width) / 2.0) + lineOffset.x, y: floor(-lineBounds.origin.y + (size.height - lineBounds.size.height) / 2.0)) + + context?.translateBy(x: size.width / 2.0, y: size.height / 2.0) + context?.scaleBy(x: 1.0, y: -1.0) + context?.translateBy(x: -size.width / 2.0, y: -size.height / 2.0) + + context?.translateBy(x: lineOrigin.x, y: lineOrigin.y) + if let context = context { + CTLineDraw(line, context) + } + context?.translateBy(x: -lineOrigin.x, y: -lineOrigin.y) + + if isStory { + context?.resetClip() + + let lineWidth: CGFloat = 2.0 + context?.setLineWidth(lineWidth) + context?.addEllipse(in: CGRect(origin: CGPoint(x: size.width * 0.5, y: size.height * 0.5), size: CGSize(width: size.width, height: size.height)).insetBy(dx: lineWidth * 0.5, dy: lineWidth * 0.5)) + context?.replacePathWithStrokedPath() + context?.clip() + + let colors: [CGColor] = [ + UIColor(rgb: 0x34C76F).cgColor, + UIColor(rgb: 0x3DA1FD).cgColor + ] + var locations: [CGFloat] = [0.0, 1.0] + + let colorSpace = CGColorSpaceCreateDeviceRGB() + let gradient = CGGradient(colorsSpace: colorSpace, colors: colors as CFArray, locations: &locations)! + + context?.drawLinearGradient(gradient, start: CGPoint(x: 0.0, y: 0.0), end: CGPoint(x: 0.0, y: size.height), options: CGGradientDrawingOptions()) + } + + let image = UIGraphicsGetImageFromCurrentImageContext() + UIGraphicsEndImageContext() + return image +} + +private func makePeerBadgeImage(engine: TelegramEngine, peer: EnginePeer, count: Int) async -> UIImage { + let avatarSize: CGFloat = 16.0 + let avatarInset: CGFloat = 2.0 + let avatarIconSpacing: CGFloat = 2.0 + let iconTextSpacing: CGFloat = 1.0 + let iconSize: CGFloat = 10.0 + let rightInset: CGFloat = 2.0 + + let text = NSAttributedString(string: "\(count)", font: Font.semibold(10.0), textColor: .white) + var textSize = text.boundingRect(with: CGSize(width: 100.0, height: 100.0), options: .usesLineFragmentOrigin, context: nil).size + textSize.width = ceil(textSize.width) + textSize.height = ceil(textSize.height) + + let size = CGSize(width: avatarInset + avatarSize + avatarIconSpacing + iconSize + iconTextSpacing + textSize.height + rightInset, height: avatarSize + avatarInset * 2.0) + return generateImage(size, rotatedContext: { size, context in + UIGraphicsPushContext(context) + defer { + UIGraphicsPopContext() + } + + context.clear(CGRect(origin: CGPoint(), size: size)) + context.setFillColor(UIColor(rgb: 0xFFB10D).cgColor) + context.addPath(UIBezierPath(roundedRect: CGRect(origin: CGPoint(), size: size), cornerRadius: size.height * 0.5).cgPath) + context.fillPath() + + if let image = avatarViewLettersImage(size: CGSize(width: avatarSize, height: avatarSize), peerId: peer.id, letters: peer.displayLetters, isStory: false) { + image.draw(in: CGRect(origin: CGPoint(x: avatarInset, y: avatarInset), size: CGSize(width: avatarSize, height: avatarSize))) + } + + if let image = generateTintedImage(image: UIImage(bundleImageName: "Premium/Stars/ButtonStar"), color: .white) { + let iconFrame = CGRect(origin: CGPoint(x: avatarInset + avatarSize + avatarIconSpacing, y: floorToScreenPixels((size.height - iconSize) * 0.5) + 1.0), size: CGSize(width: iconSize, height: iconSize)) + image.draw(in: iconFrame) + } + + text.draw(at: CGPoint(x: avatarInset + avatarSize + avatarIconSpacing + iconSize + iconTextSpacing, y: floorToScreenPixels((size.height - textSize.height) * 0.5))) + })! +} + +private actor LiveChatReactionItemTaskQueue { + private final class PeerTask { + let peer: EnginePeer + let count: Int + let completion: (UIImage) -> Void + + init(peer: EnginePeer, count: Int, completion: @escaping (UIImage) -> Void) { + self.peer = peer + self.count = count + self.completion = completion + } + } + + private let engine: TelegramEngine + private var tasks: [PeerTask] = [] + + init(engine: TelegramEngine) { + self.engine = engine + } + + func add(peer: EnginePeer, count: Int, completion: @escaping (UIImage) -> Void) { + self.tasks.append(PeerTask(peer: peer, count: count, completion: completion)) + if self.tasks.count == 1 { + Task { + await processTasks() + } + } + } + + private func processTasks() async { + while !self.tasks.isEmpty { + let task = self.tasks.removeFirst() + let image = await makePeerBadgeImage(engine: self.engine, peer: task.peer, count: task.count) + task.completion(image) + } + } +} + +final class LiveChatReactionStreamView: UIView { + private final class ItemLayer: SimpleLayer { + let amplitude: CGFloat + let period: CGFloat + let phaseOffset: CGFloat + let baseX: CGFloat + var timeValue: CGFloat = 0.0 + + init(image: UIImage, amplitude: CGFloat, period: CGFloat, phaseOffset: CGFloat, baseX: CGFloat) { + self.amplitude = amplitude + self.period = period + self.phaseOffset = phaseOffset + self.baseX = baseX + + super.init() + + self.contents = image.cgImage + self.allowsEdgeAntialiasing = true + } + + override init(layer: Any) { + self.amplitude = 0.0 + self.period = 0.0 + self.phaseOffset = 0.0 + self.baseX = 0.0 + + super.init(layer: layer) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + } + + private var nextId: Int = 0 + private var itemLayers: [Int: ItemLayer] = [:] + private let itemLayerContainer: SimpleLayer + private let hierarchyTracker: HierarchyTrackingLayer + private var previousTimestamp: Double = 0.0 + private var displayLink: SharedDisplayLinkDriver.Link? + private var previousPhysicsTimestamp: Double = 0.0 + + private let taskQueue: LiveChatReactionItemTaskQueue + + init(context: AccountContext) { + self.itemLayerContainer = SimpleLayer() + self.hierarchyTracker = HierarchyTrackingLayer() + self.taskQueue = LiveChatReactionItemTaskQueue(engine: context.engine) + + super.init(frame: CGRect()) + + self.layer.addSublayer(self.itemLayerContainer) + + self.layer.addSublayer(self.hierarchyTracker) + self.hierarchyTracker.isInHierarchyUpdated = { [weak self] inHierarchy in + guard let self else { + return + } + if inHierarchy { + if self.displayLink == nil { + self.displayLink = SharedDisplayLinkDriver.shared.add(framesPerSecond: .max, { [weak self] _ in + guard let self else { + return + } + self.updatePhysics() + }) + } + } else { + self.displayLink = nil + } + } + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + func add(peer: EnginePeer, count: Int) { + if !self.hierarchyTracker.isInHierarchy { + return + } + let timestamp = CFAbsoluteTimeGetCurrent() + if timestamp < self.previousTimestamp + 0.2 { + return + } + self.previousTimestamp = timestamp + Task { + await self.taskQueue.add(peer: peer, count: count, completion: { [weak self] image in + Task { @MainActor in + guard let self else { + return + } + self.addRenderedItem(image: image) + } + }) + } + } + + private func addRenderedItem(image: UIImage) { + let id = self.nextId + self.nextId += 1 + + let random = LokiRng(seed0: UInt(id), seed1: 1, seed2: 0) + let itemX: CGFloat = -image.size.width - 8.0 + 20.0 * CGFloat(LokiRng.random(withSeed0: UInt(id), seed1: 0, seed2: 0) - 0.5) + let phaseOffset: CGFloat = CGFloat(random.next()) + let itemLayer = ItemLayer(image: image, amplitude: 0.0 + CGFloat(random.next()) * 6.0, period: 1.0 + CGFloat(random.next()) * 1.0, phaseOffset: phaseOffset, baseX: itemX) + itemLayer.frame = CGRect(origin: CGPoint(x: itemX, y: -image.size.height * 0.5), size: image.size) + itemLayer.transform = CATransform3DMakeRotation(CGFloat(random.next() - 0.5) * CGFloat.pi * 0.2, 0.0, 0.0, 1.0) + self.itemLayers[id] = itemLayer + self.itemLayerContainer.addSublayer(itemLayer) + + itemLayer.animatePosition(from: CGPoint(), to: CGPoint(x: 0.0, y: -200.0), duration: 2.0, timingFunction: CAMediaTimingFunctionName.linear.rawValue, removeOnCompletion: false, additive: true) + + itemLayer.animateScale(from: 0.001, to: 1.0, duration: 0.2) + itemLayer.animateAlpha(from: 0.0, to: 1.0, duration: 0.1, completion: { [weak self, weak itemLayer] _ in + guard let itemLayer else { + return + } + + let delay: Double = 2.0 - 0.1 - 0.18 + + itemLayer.animateScale(from: 1.0, to: 0.001, duration: 0.18, delay: delay, removeOnCompletion: false) + itemLayer.animateAlpha(from: 1.0, to: 0.0, duration: 0.18, delay: delay, removeOnCompletion: false, completion: { [weak self] _ in + guard let self else { + return + } + if let itemLayer = self.itemLayers[id] { + self.itemLayers.removeValue(forKey: id) + itemLayer.removeFromSuperlayer() + } + }) + }) + } + + private func updatePhysics() { + let timestamp = CACurrentMediaTime() + let dt = max(1.0 / 120.0, min(1.0 / 30.0, timestamp - self.previousPhysicsTimestamp)) + self.previousPhysicsTimestamp = timestamp + + for (_, itemLayer) in self.itemLayers { + itemLayer.timeValue += dt + let itemPhase = (itemLayer.timeValue.truncatingRemainder(dividingBy: itemLayer.period) / itemLayer.period + itemLayer.phaseOffset).truncatingRemainder(dividingBy: 1.0) + let phaseFraction = sin(itemPhase * CGFloat.pi * 2.0) + itemLayer.position.x = itemLayer.baseX + phaseFraction * itemLayer.amplitude + } + } + + func update(size: CGSize, sourcePoint: CGPoint, transition: ComponentTransition) { + transition.setFrame(layer: self.itemLayerContainer, frame: CGRect(origin: sourcePoint, size: CGSize())) + } +} diff --git a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryContentLiveChatComponent.swift b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryContentLiveChatComponent.swift index 1218816e63..7fdddb25a8 100644 --- a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryContentLiveChatComponent.swift +++ b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryContentLiveChatComponent.swift @@ -302,6 +302,7 @@ private final class PinnedBarComponent: Component { self.isUpdating = false } + let previousComponent = self.component self.component = component self.state = state @@ -325,8 +326,21 @@ private final class PinnedBarComponent: Component { let listInsets = UIEdgeInsets(top: 0.0, left: insets.left, bottom: 0.0, right: insets.right) let listFrame = CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: size.width, height: size.height)) + + var listTransition = transition + var animateIn = false + if let previousComponent { + if previousComponent.messages.isEmpty { + listTransition = listTransition.withAnimation(.none) + animateIn = true + } + } else { + listTransition = listTransition.withAnimation(.none) + animateIn = true + } + let _ = self.list.update( - transition: transition, + transition: listTransition, component: AnyComponent(AsyncListComponent( externalState: self.listState, items: listItems, @@ -343,6 +357,10 @@ private final class PinnedBarComponent: Component { } transition.setPosition(view: listView, position: CGRect(origin: CGPoint(), size: listFrame.size).center) transition.setBounds(view: listView, bounds: CGRect(origin: CGPoint(), size: listFrame.size)) + + if animateIn { + transition.animateAlpha(view: listView, from: 0.0, to: 1.0) + } } transition.setFrame(view: self.listContainer, frame: listFrame) @@ -458,6 +476,8 @@ final class StoryContentLiveChatComponent: Component { private let listState = AsyncListComponent.ExternalState() private let list = ComponentView() private let listShadowView: UIView + + private var reactionStreamView: LiveChatReactionStreamView? private var component: StoryContentLiveChatComponent? private weak var state: EmptyComponentState? @@ -808,20 +828,25 @@ final class StoryContentLiveChatComponent: Component { updateTransition = .immediate } - if let component = self.component, let previousMessagesState = self.messagesState, !self.isChatExpanded { - var hasNewMessages = false - for message in state.messages { - //TODO:release - //if message.author?.id != component.context.account.peerId { - do { - if !previousMessagesState.messages.contains(where: { $0.id == message.id }) { + if let component = self.component, let previousMessagesState = self.messagesState { + if !self.isChatExpanded { + var hasNewMessages = false + for message in state.messages { + if message.isIncoming && !previousMessagesState.messages.contains(where: { $0.id == message.id }) { hasNewMessages = true - break + + if message.isIncoming, let paidStars = message.paidStars, let author = message.author { + self.reactionStreamView?.add(peer: author, count: Int(paidStars)) + } } } + if hasNewMessages { + component.external.hasUnseenMessages = true + } } - if hasNewMessages { - component.external.hasUnseenMessages = true + + if state.pendingMyStars > previousMessagesState.pendingMyStars, let message = state.messages.first(where: { $0.paidStars != nil && !$0.isIncoming }), let peer = message.author { + self.reactionStreamView?.add(peer: peer, count: Int(state.pendingMyStars - previousMessagesState.pendingMyStars)) } } self.messagesState = state @@ -973,6 +998,16 @@ final class StoryContentLiveChatComponent: Component { self.listShadowView.backgroundColor = UIColor(white: 0.0, alpha: 0.3) transition.setAlpha(view: self.listShadowView, alpha: self.isChatExpanded ? 1.0 : 0.0) + let reactionStreamView: LiveChatReactionStreamView + if let current = self.reactionStreamView { + reactionStreamView = current + } else { + reactionStreamView = LiveChatReactionStreamView(context: component.context) + self.reactionStreamView = reactionStreamView + self.addSubview(reactionStreamView) + } + reactionStreamView.update(size: availableSize, sourcePoint: CGPoint(x: availableSize.width, y: availableSize.height), transition: transition) + return availableSize } } diff --git a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerComponent.swift b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerComponent.swift index e876c3d7ed..27882ef6d3 100644 --- a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerComponent.swift +++ b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerComponent.swift @@ -2628,7 +2628,7 @@ public final class StoryItemSetContainerComponent: Component { let isFirstTime = self.component == nil if self.component == nil { - self.sendMessageContext.setup(context: component.context, view: self, inputPanelExternalState: self.inputPanelExternalState, keyboardInputData: component.keyboardInputData) + self.sendMessageContext.setup(component: component, view: self, inputPanelExternalState: self.inputPanelExternalState, keyboardInputData: component.keyboardInputData) /*#if DEBUG class Target: NSObject { @@ -2963,6 +2963,7 @@ public final class StoryItemSetContainerComponent: Component { var liveChatState: MessageInputPanelComponent.LiveChatState? var starStats: MessageInputPanelComponent.StarStats? + var sendAsConfiguration: MessageInputPanelComponent.SendAsConfiguration? var sendPaidMessageStars = isLiveStream ? self.sendMessageContext.currentLiveStreamMessageStars : component.slice.additionalPeerData.sendPaidMessageStars if let visibleItemView = self.visibleItems[component.slice.item.id]?.view.view as? StoryItemContentComponent.View { if let liveChatStateValue = visibleItemView.liveChatState { @@ -2985,6 +2986,21 @@ public final class StoryItemSetContainerComponent: Component { sendPaidMessageStars = StarsAmount(value: minMessagePrice, nanos: 0) } } + + sendAsConfiguration = self.sendMessageContext.currentSendAsPeer.flatMap { value in + return MessageInputPanelComponent.SendAsConfiguration( + currentPeer: EnginePeer(value.peer), + subscriberCount: value.subscribers.flatMap(Int.init), + isPremiumLocked: value.isPremiumRequired, + isSelecting: self.sendMessageContext.isSelectingSendAsPeer, + action: { [weak self] sourceView, gesture in + guard let self else { + return + } + self.sendMessageContext.openSendAsSelection(view: self, sourceView: sourceView, gesture: gesture) + } + ) + } } } @@ -3244,7 +3260,8 @@ public final class StoryItemSetContainerComponent: Component { self.sendMessageContext.performSendStars(view: self, buttonView: sourceView, count: 1, isFromExpandedView: false) } } : nil, - starStars: starStats + starStars: starStats, + sendAsConfiguration: sendAsConfiguration )), environment: {}, containerSize: CGSize(width: inputPanelAvailableWidth, height: 200.0) diff --git a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerViewSendMessage.swift b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerViewSendMessage.swift index 0ced021abd..719bad7ac6 100644 --- a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerViewSendMessage.swift +++ b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerViewSendMessage.swift @@ -37,7 +37,6 @@ import TextFieldComponent import StickerPackPreviewUI import OpenInExternalAppUI import SafariServices -//import MediaPasteboardUI import WebPBinding import ContextUI import ChatScheduleTimeController @@ -53,6 +52,7 @@ import AudioWaveform import ChatMessagePaymentAlertController import ChatSendStarsScreen import AnimatedTextComponent +import ChatSendAsContextMenu private var ObjCKey_DeinitWatcher: Int? @@ -105,6 +105,11 @@ final class StoryItemSetContainerSendMessage { var currentLiveStreamMessageStars: StarsAmount? weak var currentSendStarsUndoController: UndoOverlayController? + var sendAsData: (isPremium: Bool, availablePeers: [SendAsPeer])? + var currentSendAsPeer: SendAsPeer? + var isSelectingSendAsPeer: Bool = false + var sendAsDisposable: Disposable? + private(set) var isMediaRecordingLocked: Bool = false var wasRecordingDismissed: Bool = false @@ -118,10 +123,11 @@ final class StoryItemSetContainerSendMessage { self.resolvePeerByNameDisposable.dispose() self.inputMediaNodeDataDisposable?.dispose() self.currentTooltipUpdateTimer?.invalidate() + self.sendAsDisposable?.dispose() } - func setup(context: AccountContext, view: StoryItemSetContainerComponent.View, inputPanelExternalState: MessageInputPanelComponent.ExternalState, keyboardInputData: Signal) { - self.context = context + func setup(component: StoryItemSetContainerComponent, view: StoryItemSetContainerComponent.View, inputPanelExternalState: MessageInputPanelComponent.ExternalState, keyboardInputData: Signal) { + self.context = component.context self.inputPanelExternalState = inputPanelExternalState self.view = view @@ -208,6 +214,56 @@ final class StoryItemSetContainerSendMessage { } ) self.inputMediaInteraction?.forceTheme = defaultDarkColorPresentationTheme + + if let peerId = component.slice.item.peerId { + self.sendAsDisposable = combineLatest( + queue: Queue.mainQueue(), + component.context.engine.data.subscribe( + TelegramEngine.EngineData.Item.Peer.Peer(id: component.context.account.peerId) + ), + component.context.engine.peers.liveStorySendAsAvailablePeers(peerId: peerId) + ).startStrict(next: { [weak self, weak view] accountPeer, peers in + guard let self, let view, let accountPeer else { + return + } + + let isPremium = accountPeer.isPremium + + var availablePeers: [SendAsPeer] = [] + availablePeers.append(SendAsPeer( + peer: accountPeer._asPeer(), + subscribers: nil, + isPremiumRequired: false + )) + for peer in peers { + if peer.peer.id == accountPeer.id { + continue + } + availablePeers.append(peer) + } + + self.sendAsData = ( + isPremium: isPremium, + availablePeers: availablePeers + ) + + if availablePeers.count > 1 { + if self.currentSendAsPeer == nil { + self.currentSendAsPeer = availablePeers.first + if !view.isUpdatingComponent { + view.state?.updated(transition: .spring(duration: 0.4)) + } + } + } else { + if self.currentSendAsPeer != nil { + self.currentSendAsPeer = nil + if !view.isUpdatingComponent { + view.state?.updated(transition: .spring(duration: 0.4)) + } + } + } + }) + } } func toggleInputMode() { @@ -281,7 +337,7 @@ final class StoryItemSetContainerSendMessage { self.inputMediaNode = inputMediaNode } - let presentationData = context.sharedContext.currentPresentationData.with { $0 }.withUpdated(theme: defaultDarkPresentationTheme) + let presentationData = context.sharedContext.currentPresentationData.with { $0 }.withUpdated(theme: defaultDarkColorPresentationTheme) let presentationInterfaceState = ChatPresentationInterfaceState( chatWallpaper: .builtin(WallpaperSettings()), theme: presentationData.theme, @@ -698,7 +754,7 @@ final class StoryItemSetContainerSendMessage { let entities = generateChatInputTextEntities(text) - call.sendMessage(text: text.string, entities: entities, paidStars: sendPaidMessageStars?.value) + call.sendMessage(fromId: self.currentSendAsPeer?.peer.id, text: text.string, entities: entities, paidStars: sendPaidMessageStars?.value) component.storyItemSharedState.replyDrafts.removeValue(forKey: StoryId(peerId: peerId, id: focusedItem.storyItem.id)) inputPanelView.clearSendMessageInput(updateState: true) @@ -3952,13 +4008,14 @@ final class StoryItemSetContainerSendMessage { let initialData = await ChatSendStarsScreen.initialData( context: component.context, peerId: peerId, + myPeer: (self.currentSendAsPeer?.peer).flatMap(EnginePeer.init), reactSubject: .liveStream(peerId: peerId, storyId: focusedItem.storyItem.id, minAmount: Int(minAmount)), topPeers: topPeers, - completion: { [weak view] amount, privacy, isBecomingTop, transitionOut in - guard let view, let component = view.component else { + completion: { [weak self, weak view] amount, _, _, _ in + guard let self, let view else { return } - let _ = component.context.engine.messages.sendStoryStars(peerId: component.slice.effectivePeer.id, id: component.slice.item.storyItem.id, count: Int(amount)).startStandalone() + self.performSendStars(view: view, buttonView: nil, count: Int(amount), isFromExpandedView: true) }).get() if let initialData { controller.push(ChatSendStarsScreen( @@ -3970,12 +4027,17 @@ final class StoryItemSetContainerSendMessage { } } - func performSendStars(view: StoryItemSetContainerComponent.View, buttonView: UIView, count: Int, isFromExpandedView: Bool) { + func performSendStars(view: StoryItemSetContainerComponent.View, buttonView: UIView?, count: Int, isFromExpandedView: Bool) { guard let component = view.component else { return } if isFromExpandedView { + if let currentSendStarsUndoController = self.currentSendStarsUndoController { + self.currentSendStarsUndoController = nil + currentSendStarsUndoController.dismiss() + } + self.commitSendStars(view: view, count: count, delay: false) } else { Task { @MainActor [weak view] in @@ -4009,7 +4071,7 @@ final class StoryItemSetContainerSendMessage { } } - if let reactionItem { + if let reactionItem, let buttonView { let targetFrame = buttonView.convert(buttonView.bounds, to: view) let targetView = UIView(frame: targetFrame) @@ -4143,7 +4205,91 @@ final class StoryItemSetContainerSendMessage { return } - call.sendStars(amount: Int64(count), delay: delay) + call.sendStars(fromId: self.currentSendAsPeer?.peer.id, amount: Int64(count), delay: delay) + } + + func openSendAsSelection(view: StoryItemSetContainerComponent.View, sourceView: UIView, gesture: ContextGesture?) { + guard let component = view.component, let sendAsData = self.sendAsData, let currentSendAsPeer = self.currentSendAsPeer, let controller = component.controller() else { + return + } + + let focusedItem = component.slice.item + guard let peerId = focusedItem.peerId else { + return + } + let isPremium = sendAsData.isPremium + + var items: [ContextMenuItem] = [] + items.append(.custom(ChatSendAsPeerTitleContextItem(text: component.strings.Conversation_SendMesageAs.uppercased()), false)) + items.append(.custom(ChatSendAsPeerListContextItem( + context: component.context, + chatPeerId: peerId, + peers: sendAsData.availablePeers, + selectedPeerId: currentSendAsPeer.peer.id, + isPremium: isPremium, + action: { [weak self, weak view] peer in + guard let self, let view else { + return + } + if let foundPeer = self.sendAsData?.availablePeers.first(where: { $0.peer.id == peer.id }) { + self.currentSendAsPeer = foundPeer + view.state?.updated(transition: .spring(duration: 0.4)) + } + }, + presentToast: { [weak view] peer in + guard let view, let component = view.component, let controller = component.controller() else { + return + } + HapticFeedback().impact() + + let presentationData = component.context.sharedContext.currentPresentationData.with { $0 }.withUpdated(theme: defaultDarkColorPresentationTheme) + controller.present(UndoOverlayController(presentationData: presentationData, content: .invitedToVoiceChat(context: component.context, peer: peer, title: nil, text: presentationData.strings.Conversation_SendMesageAsPremiumInfo, action: presentationData.strings.EmojiInput_PremiumEmojiToast_Action, duration: 3), elevatedLayout: false, action: { [weak view] action in + guard let view, let component = view.component, let controller = component.controller() else { + return true + } + if case .undo = action { + view.endEditing(true) + + let introScreen = PremiumIntroScreen(context: component.context, source: .settings) + controller.push(introScreen) + } + return true + }), in: .current) + }), + false + )) + + final class ReferenceSource: ContextReferenceContentSource { + let controller: ViewController + let sourceView: UIView + let insets: UIEdgeInsets + let contentInsets: UIEdgeInsets + + init(controller: ViewController, sourceView: UIView, insets: UIEdgeInsets, contentInsets: UIEdgeInsets = UIEdgeInsets()) { + self.controller = controller + self.sourceView = sourceView + self.insets = insets + self.contentInsets = contentInsets + } + + func transitionInfo() -> ContextControllerReferenceViewInfo? { + return ContextControllerReferenceViewInfo(referenceView: self.sourceView, contentAreaInScreenSpace: UIScreen.main.bounds.inset(by: self.insets), insets: self.contentInsets, actionsPosition: .top) + } + } + + let presentationData = component.context.sharedContext.currentPresentationData.with { $0 }.withUpdated(theme: defaultDarkColorPresentationTheme) + let contextController = ContextController(presentationData: presentationData, source: .reference(ReferenceSource(controller: controller, sourceView: sourceView, insets: UIEdgeInsets(top: 0.0, left: 0.0, bottom: 0.0, right: 0.0), contentInsets: UIEdgeInsets(top: 0.0, left: 0.0, bottom: 0.0, right: 0.0))), items: .single(ContextController.Items(content: .list(items))), gesture: gesture, workaroundUseLegacyImplementation: false) + contextController.dismissed = { [weak self, weak view] in + guard let self, let view else { + return + } + self.isSelectingSendAsPeer = false + view.state?.updated(transition: .spring(duration: 0.4)) + } + controller.presentInGlobalOverlay(contextController) + + self.isSelectingSendAsPeer = true + view.state?.updated(transition: .spring(duration: 0.4)) } } diff --git a/submodules/TelegramUI/Sources/Chat/ChatControllerLoadDisplayNode.swift b/submodules/TelegramUI/Sources/Chat/ChatControllerLoadDisplayNode.swift index be52c49e1d..b90870ac87 100644 --- a/submodules/TelegramUI/Sources/Chat/ChatControllerLoadDisplayNode.swift +++ b/submodules/TelegramUI/Sources/Chat/ChatControllerLoadDisplayNode.swift @@ -123,6 +123,7 @@ import ChatMediaInputStickerGridItem import AdsInfoScreen import PostSuggestionsSettingsScreen import ChatSendStarsScreen +import ChatSendAsContextMenu extension ChatControllerImpl { func reloadChatLocation(chatLocation: ChatLocation, chatLocationContextHolder: Atomic, historyNode: ChatHistoryListNodeImpl, apply: @escaping ((ContainedViewLayoutTransition?) -> Void) -> Void) { @@ -3896,7 +3897,12 @@ extension ChatControllerImpl { var items: [ContextMenuItem] = [] items.append(.custom(ChatSendAsPeerTitleContextItem(text: strongSelf.presentationInterfaceState.strings.Conversation_SendMesageAs.uppercased()), false)) - items.append(.custom(ChatSendAsPeerListContextItem(context: strongSelf.context, chatPeerId: peerId, peers: peers, selectedPeerId: myPeerId, isPremium: isPremium, presentToast: { [weak self] peer in + items.append(.custom(ChatSendAsPeerListContextItem(context: strongSelf.context, chatPeerId: peerId, peers: peers, selectedPeerId: myPeerId, isPremium: isPremium, action: { [weak self] peer in + guard let self else { + return + } + let _ = self.context.engine.peers.updatePeerSendAsPeer(peerId: peerId, sendAs: peer.id).startStandalone() + }, presentToast: { [weak self] peer in if let strongSelf = self { HapticFeedback().impact() diff --git a/submodules/Utils/LokiRng/PublicHeaders/LokiRng/LokiRng.h b/submodules/Utils/LokiRng/PublicHeaders/LokiRng/LokiRng.h index e1c0abb7c3..73b623d0f9 100644 --- a/submodules/Utils/LokiRng/PublicHeaders/LokiRng/LokiRng.h +++ b/submodules/Utils/LokiRng/PublicHeaders/LokiRng/LokiRng.h @@ -8,6 +8,7 @@ - (instancetype _Nonnull)initWithSeed0:(NSUInteger)seed0 seed1:(NSUInteger)seed1 seed2:(NSUInteger)seed2; - (float)next; ++ (float)randomWithSeed0:(NSUInteger)seed0 seed1:(NSUInteger)seed1 seed2:(NSUInteger)seed2; @end diff --git a/submodules/Utils/LokiRng/Sources/LokiRng.mm b/submodules/Utils/LokiRng/Sources/LokiRng.mm index 3f8240d361..957af6d795 100644 --- a/submodules/Utils/LokiRng/Sources/LokiRng.mm +++ b/submodules/Utils/LokiRng/Sources/LokiRng.mm @@ -61,4 +61,34 @@ static uint32_t tausStep(const uint32_t z, const int32_t s1, const int32_t s2, c return old_seed; } ++ (float)randomWithSeed0:(NSUInteger)seed0 seed1:(NSUInteger)seed1 seed2:(NSUInteger)seed2 { + uint32_t seed = ((uint32_t)seed0) * 1099087573U; + uint32_t seedb = ((uint32_t)seed1) * 1099087573U; + uint32_t seedc = ((uint32_t)seed2) * 1099087573U; + + // Round 1: Randomise seed + uint32_t z1 = tausStep(seed,13,19,12,429496729U); + uint32_t z2 = tausStep(seed,2,25,4,4294967288U); + uint32_t z3 = tausStep(seed,3,11,17,429496280U); + uint32_t z4 = (1664525*seed + 1013904223U); + + // Round 2: Randomise seed again using second seed + uint32_t r1 = (z1^z2^z3^z4^seedb); + + z1 = tausStep(r1,13,19,12,429496729U); + z2 = tausStep(r1,2,25,4,4294967288U); + z3 = tausStep(r1,3,11,17,429496280U); + z4 = (1664525*r1 + 1013904223U); + + // Round 3: Randomise seed again using third seed + r1 = (z1^z2^z3^z4^seedc); + + z1 = tausStep(r1,13,19,12,429496729U); + z2 = tausStep(r1,2,25,4,4294967288U); + z3 = tausStep(r1,3,11,17,429496280U); + z4 = (1664525*r1 + 1013904223U); + + return (z1^z2^z3^z4) * 2.3283064365387e-10f; +} + @end