diff --git a/submodules/TelegramCallsUI/Sources/PresentationGroupCall.swift b/submodules/TelegramCallsUI/Sources/PresentationGroupCall.swift index 760f476a6d..abd952c3f9 100644 --- a/submodules/TelegramCallsUI/Sources/PresentationGroupCall.swift +++ b/submodules/TelegramCallsUI/Sources/PresentationGroupCall.swift @@ -4060,15 +4060,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/TelegramEngine/Calls/GroupCalls.swift b/submodules/TelegramCore/Sources/TelegramEngine/Calls/GroupCalls.swift index 234cebeac0..bf73c77d07 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Calls/GroupCalls.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Calls/GroupCalls.swift @@ -3699,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] @@ -3706,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 @@ -3721,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, @@ -3740,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 } @@ -3921,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, @@ -3950,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, @@ -4231,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, @@ -4449,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, @@ -4462,6 +4472,7 @@ public final class GroupCallMessagesContext { state.messages.append(Message( id: Message.Id(space: .local, id: pendingSendStarsValue.messageId), stableId: stableId, + isIncoming: false, author: EnginePeer(fromPeer), text: "", entities: [], @@ -4476,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, @@ -4489,6 +4501,7 @@ public final class GroupCallMessagesContext { state.pinnedMessages.append(Message( id: Message.Id(space: .local, id: pendingSendStarsValue.messageId), stableId: stableId, + isIncoming: false, author: EnginePeer(fromPeer), text: "", entities: [], diff --git a/submodules/TelegramUI/Components/Chat/ChatSendStarsScreen/Sources/ChatSendStarsScreen.swift b/submodules/TelegramUI/Components/Chat/ChatSendStarsScreen/Sources/ChatSendStarsScreen.swift index 9b569233aa..2aef0a98f6 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: [], 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/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/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 826101a1ff..5ce7a5256f 100644 --- a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/BUILD +++ b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/BUILD @@ -111,6 +111,7 @@ swift_library( "//submodules/TelegramUI/Components/AnimatedTextComponent", "//submodules/TelegramUI/Components/AdminUserActionsSheet", "//submodules/TelegramUI/Components/Chat/ChatSendAsContextMenu", + "//submodules/Components/HierarchyTrackingLayer", ], 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..97adc3d522 --- /dev/null +++ b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/LiveChatReactionStreamView.swift @@ -0,0 +1,197 @@ +import Foundation +import UIKit +import Display +import ComponentFlow +import TelegramCore +import AvatarNode +import AppBundle +import AccountContext +import HierarchyTrackingLayer + +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 = 2.0 + let iconSize: CGFloat = 8.0 + let rightInset: CGFloat = 4.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() + + 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 { + init(image: UIImage) { + super.init() + + self.contents = image.cgImage + } + + override init(layer: Any) { + 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 + 1.0 / 30.0 { + 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) { + if "".isEmpty { + return + } + + let id = self.nextId + self.nextId += 1 + + let itemLayer = ItemLayer(image: image) + itemLayer.frame = CGRect(origin: CGPoint(x: -image.size.width - 10.0, y: -image.size.height * 0.5), size: image.size) + self.itemLayers[id] = itemLayer + self.itemLayerContainer.addSublayer(itemLayer) + + let transition = ComponentTransition(animation: .curve(duration: 2.0, curve: .linear)) + transition.setPosition(layer: itemLayer, position: CGPoint(x: itemLayer.position.x, y: -300.0)) + + itemLayer.animateAlpha(from: 1.0, to: 0.0, duration: 0.18, delay: 2.0 - 0.18, 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 + + let _ = dt + } + + 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 ff37e7c9f0..f89f062c89 100644 --- a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryContentLiveChatComponent.swift +++ b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryContentLiveChatComponent.swift @@ -476,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? @@ -826,20 +828,26 @@ 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 }) { - hasNewMessages = true - break + if let component = self.component, let previousMessagesState = self.messagesState { + if !self.isChatExpanded { + var hasNewMessages = false + for message in state.messages { + //TODO:release + //if message.author?.id != component.context.account.peerId { + do { + if !previousMessagesState.messages.contains(where: { $0.id == message.id }) { + hasNewMessages = true + break + } } } + if hasNewMessages { + component.external.hasUnseenMessages = true + } } - 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 @@ -991,6 +999,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 295eed8349..05b2c612b3 100644 --- a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerComponent.swift +++ b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerComponent.swift @@ -2987,7 +2987,20 @@ public final class StoryItemSetContainerComponent: Component { } } - sendAsConfiguration = self.sendMessageContext.currentSendAsConfiguration + 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) + } + ) + } } } diff --git a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerViewSendMessage.swift b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerViewSendMessage.swift index 9bc37ea475..f903b6fd96 100644 --- a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerViewSendMessage.swift +++ b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerViewSendMessage.swift @@ -105,9 +105,9 @@ final class StoryItemSetContainerSendMessage { var currentLiveStreamMessageStars: StarsAmount? weak var currentSendStarsUndoController: UndoOverlayController? - var sendAsData: (isPremium: Bool, availablePeers: [(peer: EnginePeer, subscriberCount: Int?, isPremiumRequired: Bool)])? - var currentSendAsConfiguration: MessageInputPanelComponent.SendAsConfiguration? - var sendAsContextPeerId: EnginePeer.Id? + var sendAsData: (isPremium: Bool, availablePeers: [SendAsPeer])? + var currentSendAsPeer: SendAsPeer? + var isSelectingSendAsPeer: Bool = false var sendAsDisposable: Disposable? private(set) var isMediaRecordingLocked: Bool = false @@ -229,21 +229,17 @@ final class StoryItemSetContainerSendMessage { let isPremium = accountPeer.isPremium - var availablePeers: [(peer: EnginePeer, subscriberCount: Int?, isPremiumRequired: Bool)] = [] - availablePeers.append(( - peer: accountPeer, - subscriberCount: nil, + 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: EnginePeer(peer.peer), - subscriberCount: peer.subscribers.flatMap(Int.init), - isPremiumRequired: peer.isPremiumRequired - )) + availablePeers.append(peer) } self.sendAsData = ( @@ -251,29 +247,16 @@ final class StoryItemSetContainerSendMessage { availablePeers: availablePeers ) - //TODO:localize - if "".isEmpty { - let sendAsConfiguration = MessageInputPanelComponent.SendAsConfiguration( - currentPeer: accountPeer, - subscriberCount: nil, - isPremiumLocked: false, - isSelecting: false, - action: { [weak self, weak view] sourceView, gesture in - guard let self, let view else { - return - } - self.openSendAsSelection(view: view, sourceView: sourceView, gesture: gesture) - } - ) - if self.currentSendAsConfiguration != sendAsConfiguration { - self.currentSendAsConfiguration = sendAsConfiguration + 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.currentSendAsConfiguration != nil { - self.currentSendAsConfiguration = nil + if self.currentSendAsPeer != nil { + self.currentSendAsPeer = nil if !view.isUpdatingComponent { view.state?.updated(transition: .spring(duration: 0.4)) } @@ -771,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) @@ -4221,11 +4204,11 @@ final class StoryItemSetContainerSendMessage { return } - call.sendStars(amount: Int64(count), delay: delay) + call.sendStars(fromId: self.currentSendAsPeer?.peer.id, amount: Int64(count), delay: delay) } - private func openSendAsSelection(view: StoryItemSetContainerComponent.View, sourceView: UIView, gesture: ContextGesture?) { - guard let component = view.component, let sendAsData = self.sendAsData, let currentSendAsConfiguration = self.currentSendAsConfiguration, let controller = component.controller() else { + 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 } @@ -4235,27 +4218,22 @@ final class StoryItemSetContainerSendMessage { } let isPremium = sendAsData.isPremium - let mappedPeers = sendAsData.availablePeers.map { peer in - return SendAsPeer( - peer: peer.peer._asPeer(), - subscribers: peer.subscriberCount.flatMap(Int32.init(clamping:)), - isPremiumRequired: peer.isPremiumRequired - ) - } - var items: [ContextMenuItem] = [] items.append(.custom(ChatSendAsPeerTitleContextItem(text: component.strings.Conversation_SendMesageAs.uppercased()), false)) items.append(.custom(ChatSendAsPeerListContextItem( context: component.context, chatPeerId: peerId, - peers: mappedPeers, - selectedPeerId: currentSendAsConfiguration.currentPeer.id, + peers: sendAsData.availablePeers, + selectedPeerId: currentSendAsPeer.peer.id, isPremium: isPremium, - action: { [weak self] peer in - guard let self else { + action: { [weak self, weak view] peer in + guard let self, let view else { return } - let _ = self + 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 { @@ -4304,11 +4282,12 @@ final class StoryItemSetContainerSendMessage { guard let self, let view else { return } - let _ = self + 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)) } }