From 3ff98fd2a8b0ca7246070f4150cedf9672bd18e6 Mon Sep 17 00:00:00 2001 From: Isaac <> Date: Tue, 6 Aug 2024 15:00:29 +0400 Subject: [PATCH] [WIP] Star reactions --- .../Sources/BrowserBookmarksScreen.swift | 1 + .../Sources/ReactionButtonListComponent.swift | 4 +- ...tControllerExtractedPresentationNode.swift | 5 +- .../ContainedViewLayoutTransition.swift | 11 + .../Sources/ChannelAdminsController.swift | 47 +++- submodules/ReactionSelectionNode/BUILD | 1 + .../Sources/ReactionContextNode.swift | 237 ++++++++---------- .../Sources/State/AvailableReactions.swift | 99 +++++--- .../Peers/UpdateCachedPeerData.swift | 2 +- .../Sources/Utils/MessageUtils.swift | 9 + .../Sources/ChatMessageBubbleItemNode.swift | 34 ++- .../Sources/ChatMessageItemImpl.swift | 12 +- .../ChatRecentActionsControllerNode.swift | 1 + .../ChatSendAudioMessageContextPreview.swift | 1 + .../Sources/ChatSendStarsScreen.swift | 19 +- .../Sources/ChatControllerInteraction.swift | 3 + .../Sources/PeerInfoScreen.swift | 1 + .../Sources/PremiumStarComponent.swift | 205 +++++++++++++++ .../SpaceWarpView/Sources/MeshLayer.swift | 7 + .../SpaceWarpView/Sources/SpaceWarpView.swift | 59 ++++- .../star_up/star_reaction_activate.tgs | Bin 0 -> 5659 bytes .../star_up/star_reaction_appear.tgs | Bin 0 -> 5796 bytes .../star_up/star_reaction_center.tgs | Bin 0 -> 5659 bytes .../star_up/star_reaction_effect.tgs | Bin 0 -> 22618 bytes .../star_up/star_reaction_select.tgs | Bin 0 -> 5659 bytes .../star_up/star_reaction_static_icon.webp | Bin 0 -> 2850 bytes .../Chat/PeerMessageSelectedReactions.swift | 3 + .../TelegramUI/Sources/ChatController.swift | 5 + .../Sources/ChatControllerNode.swift | 7 + ...rollerOpenMessageReactionContextMenu.swift | 42 +--- .../OverlayAudioPlayerControllerNode.swift | 1 + .../Sources/SharedAccountContext.swift | 1 + 32 files changed, 589 insertions(+), 228 deletions(-) create mode 100644 submodules/TelegramUI/Components/SpaceWarpView/Sources/MeshLayer.swift create mode 100644 submodules/TelegramUI/Resources/Animations/star_up/star_reaction_activate.tgs create mode 100644 submodules/TelegramUI/Resources/Animations/star_up/star_reaction_appear.tgs create mode 100644 submodules/TelegramUI/Resources/Animations/star_up/star_reaction_center.tgs create mode 100644 submodules/TelegramUI/Resources/Animations/star_up/star_reaction_effect.tgs create mode 100644 submodules/TelegramUI/Resources/Animations/star_up/star_reaction_select.tgs create mode 100644 submodules/TelegramUI/Resources/Animations/star_up/star_reaction_static_icon.webp diff --git a/submodules/BrowserUI/Sources/BrowserBookmarksScreen.swift b/submodules/BrowserUI/Sources/BrowserBookmarksScreen.swift index b77e831e36..cef645b788 100644 --- a/submodules/BrowserUI/Sources/BrowserBookmarksScreen.swift +++ b/submodules/BrowserUI/Sources/BrowserBookmarksScreen.swift @@ -166,6 +166,7 @@ public final class BrowserBookmarksScreen: ViewController { }, scrollToMessageId: { _ in }, navigateToStory: { _, _ in }, attemptedNavigationToPrivateQuote: { _ in + }, forceUpdateWarpContents: { }, automaticMediaDownloadSettings: MediaAutoDownloadSettings.defaultSettings, pollActionState: ChatInterfacePollActionState(), stickerSettings: ChatInterfaceStickerSettings(), presentationContext: ChatPresentationContext(context: context, backgroundNode: nil)) diff --git a/submodules/Components/ReactionButtonListComponent/Sources/ReactionButtonListComponent.swift b/submodules/Components/ReactionButtonListComponent/Sources/ReactionButtonListComponent.swift index 61746938e2..3efc953198 100644 --- a/submodules/Components/ReactionButtonListComponent/Sources/ReactionButtonListComponent.swift +++ b/submodules/Components/ReactionButtonListComponent/Sources/ReactionButtonListComponent.swift @@ -176,7 +176,7 @@ public final class ReactionIconView: PortalSourceView { animationLayer.masksToBounds = true animationLayer.cornerRadius = floor(size.width * 0.2) case .stars: - iconSize = CGSize(width: floor(size.width * 2.0), height: floor(size.height * 2.0)) + iconSize = CGSize(width: floor(size.width * 1.25), height: floor(size.height * 1.25)) animationLayer.masksToBounds = false animationLayer.cornerRadius = 0.0 } @@ -212,7 +212,7 @@ public final class ReactionIconView: PortalSourceView { case .custom: iconSize = CGSize(width: floor(size.width * 1.25), height: floor(size.height * 1.25)) case .stars: - iconSize = CGSize(width: floor(size.width * 2.0), height: floor(size.height * 2.0)) + iconSize = CGSize(width: floor(size.width * 1.25), height: floor(size.height * 1.25)) } let animationLayer = InlineStickerItemLayer( diff --git a/submodules/ContextUI/Sources/ContextControllerExtractedPresentationNode.swift b/submodules/ContextUI/Sources/ContextControllerExtractedPresentationNode.swift index 7c147f45af..f58c2993ad 100644 --- a/submodules/ContextUI/Sources/ContextControllerExtractedPresentationNode.swift +++ b/submodules/ContextUI/Sources/ContextControllerExtractedPresentationNode.swift @@ -1535,8 +1535,6 @@ final class ContextControllerExtractedPresentationNode: ASDisplayNode, ContextCo additive: true, completion: { [weak self] _ in Queue.mainQueue().after(reactionContextNodeIsAnimatingOut ? 0.2 * UIView.animationDurationFactor() : 0.0, { - contentNode.containingItem.isExtractedToContextPreview = false - contentNode.containingItem.isExtractedToContextPreviewUpdated?(false) if let strongSelf = self, let contentNode = strongSelf.itemContentNode { switch contentNode.containingItem { @@ -1547,6 +1545,9 @@ final class ContextControllerExtractedPresentationNode: ASDisplayNode, ContextCo } } + contentNode.containingItem.isExtractedToContextPreview = false + contentNode.containingItem.isExtractedToContextPreviewUpdated?(false) + completion() }) } diff --git a/submodules/Display/Source/ContainedViewLayoutTransition.swift b/submodules/Display/Source/ContainedViewLayoutTransition.swift index eea065a5f1..4ec3efdd87 100644 --- a/submodules/Display/Source/ContainedViewLayoutTransition.swift +++ b/submodules/Display/Source/ContainedViewLayoutTransition.swift @@ -538,6 +538,17 @@ public extension ContainedViewLayoutTransition { } } + func animateScaleWithKeyframes(layer: CALayer, keyframes: [CGFloat], removeOnCompletion: Bool = true, additive: Bool = false, completion: ((Bool) -> Void)? = nil) { + switch self { + case .immediate: + completion?(true) + case let .animated(duration, curve): + layer.animateKeyframes(values: keyframes.map { NSNumber(value: Float($0)) }, duration: duration, keyPath: "transform.scale", timingFunction: curve.timingFunction, mediaTimingFunction: curve.mediaTimingFunction, removeOnCompletion: removeOnCompletion, completion: { value in + completion?(value) + }) + } + } + func animateFrame(node: ASDisplayNode, from frame: CGRect, to toFrame: CGRect? = nil, removeOnCompletion: Bool = true, additive: Bool = false, completion: ((Bool) -> Void)? = nil) { switch self { case .immediate: diff --git a/submodules/PeerInfoUI/Sources/ChannelAdminsController.swift b/submodules/PeerInfoUI/Sources/ChannelAdminsController.swift index 651f17e7fa..429784f087 100644 --- a/submodules/PeerInfoUI/Sources/ChannelAdminsController.swift +++ b/submodules/PeerInfoUI/Sources/ChannelAdminsController.swift @@ -25,8 +25,9 @@ private final class ChannelAdminsControllerArguments { let openAdmin: (ChannelParticipant) -> Void let updateAntiSpamEnabled: (Bool) -> Void let updateSignMessagesEnabled: (Bool) -> Void + let updateShowAuthorProfilesEnabled: (Bool) -> Void - init(context: AccountContext, openRecentActions: @escaping () -> Void, setPeerIdWithRevealedOptions: @escaping (EnginePeer.Id?, EnginePeer.Id?) -> Void, removeAdmin: @escaping (EnginePeer.Id) -> Void, addAdmin: @escaping () -> Void, openAdmin: @escaping (ChannelParticipant) -> Void, updateAntiSpamEnabled: @escaping (Bool) -> Void, updateSignMessagesEnabled: @escaping (Bool) -> Void) { + init(context: AccountContext, openRecentActions: @escaping () -> Void, setPeerIdWithRevealedOptions: @escaping (EnginePeer.Id?, EnginePeer.Id?) -> Void, removeAdmin: @escaping (EnginePeer.Id) -> Void, addAdmin: @escaping () -> Void, openAdmin: @escaping (ChannelParticipant) -> Void, updateAntiSpamEnabled: @escaping (Bool) -> Void, updateSignMessagesEnabled: @escaping (Bool) -> Void, updateShowAuthorProfilesEnabled: @escaping (Bool) -> Void) { self.context = context self.openRecentActions = openRecentActions self.setPeerIdWithRevealedOptions = setPeerIdWithRevealedOptions @@ -35,6 +36,7 @@ private final class ChannelAdminsControllerArguments { self.openAdmin = openAdmin self.updateAntiSpamEnabled = updateAntiSpamEnabled self.updateSignMessagesEnabled = updateSignMessagesEnabled + self.updateShowAuthorProfilesEnabled = updateShowAuthorProfilesEnabled } } @@ -60,6 +62,7 @@ private enum ChannelAdminsEntry: ItemListNodeEntry { case adminsInfo(PresentationTheme, String) case signMessages(PresentationTheme, String, Bool) + case showAuthorProfiles(PresentationTheme, String, Bool) case signMessagesInfo(PresentationTheme, String) var section: ItemListSectionId { @@ -68,7 +71,7 @@ private enum ChannelAdminsEntry: ItemListNodeEntry { return ChannelAdminsSection.administration.rawValue case .adminsHeader, .adminPeerItem, .addAdmin, .adminsInfo: return ChannelAdminsSection.admins.rawValue - case .signMessages, .signMessagesInfo: + case .signMessages, .showAuthorProfiles, .signMessagesInfo: return ChannelAdminsSection.signMessages.rawValue } } @@ -91,8 +94,10 @@ private enum ChannelAdminsEntry: ItemListNodeEntry { return .peer(participant.peer.id) case .signMessages: return .index(6) - case .signMessagesInfo: + case .showAuthorProfiles: return .index(7) + case .signMessagesInfo: + return .index(9) } } @@ -176,6 +181,12 @@ private enum ChannelAdminsEntry: ItemListNodeEntry { } else { return false } + case let .showAuthorProfiles(lhsTheme, lhsText, lhsValue): + if case let .showAuthorProfiles(rhsTheme, rhsText, rhsValue) = rhs, lhsTheme === rhsTheme, lhsText == rhsText, lhsValue == rhsValue { + return true + } else { + return false + } case let .signMessagesInfo(lhsTheme, lhsText): if case let .signMessagesInfo(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText { return true @@ -240,6 +251,13 @@ private enum ChannelAdminsEntry: ItemListNodeEntry { default: return true } + case .showAuthorProfiles: + switch rhs { + case .recentActions, .antiSpam, .antiSpamInfo, .adminsHeader, .addAdmin, .adminPeerItem, .adminsInfo, .signMessages, .showAuthorProfiles: + return false + default: + return true + } case .signMessagesInfo: return false } @@ -301,6 +319,10 @@ private enum ChannelAdminsEntry: ItemListNodeEntry { return ItemListSwitchItem(presentationData: presentationData, title: text, value: value, sectionId: self.section, style: .blocks, updated: { value in arguments.updateSignMessagesEnabled(value) }) + case let .showAuthorProfiles(_, text, value): + return ItemListSwitchItem(presentationData: presentationData, title: text, value: value, sectionId: self.section, style: .blocks, updated: { value in + arguments.updateShowAuthorProfilesEnabled(value) + }) case let .signMessagesInfo(_, text): return ItemListTextItem(presentationData: presentationData, text: .plain(text), sectionId: self.section) } @@ -381,7 +403,7 @@ private struct ChannelAdminsControllerState: Equatable { } } -private func channelAdminsControllerEntries(presentationData: PresentationData, accountPeerId: EnginePeer.Id, peer: EnginePeer?, state: ChannelAdminsControllerState, participants: [RenderedChannelParticipant]?, antiSpamAvailable: Bool, antiSpamEnabled: Bool, signMessagesEnabled: Bool) -> [ChannelAdminsEntry] { +private func channelAdminsControllerEntries(presentationData: PresentationData, accountPeerId: EnginePeer.Id, peer: EnginePeer?, state: ChannelAdminsControllerState, participants: [RenderedChannelParticipant]?, antiSpamAvailable: Bool, antiSpamEnabled: Bool, signMessagesEnabled: Bool, showAuthorProfilesEnabled: Bool) -> [ChannelAdminsEntry] { if participants == nil || participants?.count == nil { return [] } @@ -476,7 +498,9 @@ private func channelAdminsControllerEntries(presentationData: PresentationData, if !isGroup && peer.hasPermission(.sendSomething) { entries.append(.signMessages(presentationData.theme, presentationData.strings.Channel_SignMessages, signMessagesEnabled)) - entries.append(.signMessagesInfo(presentationData.theme, presentationData.strings.Channel_SignMessages_Help)) + //TODO:localize + entries.append(.showAuthorProfiles(presentationData.theme, "Show Authors' Profiles", showAuthorProfilesEnabled)) + entries.append(.signMessagesInfo(presentationData.theme, "Add names and photos of admins to the messages they post, linking to their profiles.")) } } } else if case let .legacyGroup(peer) = peer { @@ -588,6 +612,9 @@ public func channelAdminsController(context: AccountContext, updatedPresentation let updateSignMessagesDisposable = MetaDisposable() actionsDisposable.add(updateSignMessagesDisposable) + let updateShowAuthorProfilesDisposable = MetaDisposable() + actionsDisposable.add(updateShowAuthorProfilesDisposable) + let adminsPromise = Promise<[RenderedChannelParticipant]?>(nil) let antiSpamConfiguration = AntiSpamBotConfiguration.with(appConfiguration: context.currentAppConfiguration.with { $0 }) @@ -790,6 +817,12 @@ public func channelAdminsController(context: AccountContext, updatedPresentation |> deliverOnMainQueue).start(next: { peerId in updateSignMessagesDisposable.set(context.engine.peers.toggleShouldChannelMessagesSignatures(peerId: peerId, enabled: value).start()) }) + }, updateShowAuthorProfilesEnabled: { value in + let _ = (currentPeerId.get() + |> take(1) + |> deliverOnMainQueue).start(next: { peerId in + updateSignMessagesDisposable.set(context.engine.peers.toggleShouldChannelMessagesSignatures(peerId: peerId, enabled: value).start()) + }) }) let membersAndLoadMoreControlValue = Atomic<(Disposable, PeerChannelMemberCategoryControl?)?>(value: nil) @@ -911,8 +944,10 @@ public func channelAdminsController(context: AccountContext, updatedPresentation } var signMessagesEnabled = false + var showAuthorProfilesEnabled = false if case let .channel(channel) = view.peer, case let .broadcast(info) = channel.info { signMessagesEnabled = info.flags.contains(.messagesShouldHaveSignatures) + showAuthorProfilesEnabled = info.flags.contains(.messagesShouldHaveSignatures) } var rightNavigationButton: ItemListNavigationButton? @@ -986,7 +1021,7 @@ public func channelAdminsController(context: AccountContext, updatedPresentation } let controllerState = ItemListControllerState(presentationData: ItemListPresentationData(presentationData), title: .text(isGroup ? presentationData.strings.ChatAdmins_Title : presentationData.strings.Channel_Management_Title), leftNavigationButton: nil, rightNavigationButton: rightNavigationButton, secondaryRightNavigationButton: secondaryRightNavigationButton, backNavigationButton: ItemListBackButton(title: presentationData.strings.Common_Back), animateChanges: true) - let listState = ItemListNodeState(presentationData: ItemListPresentationData(presentationData), entries: channelAdminsControllerEntries(presentationData: presentationData, accountPeerId: context.account.peerId, peer: view.peer, state: state, participants: admins, antiSpamAvailable: antiSpamAvailable, antiSpamEnabled: antiSpamEnabled, signMessagesEnabled: signMessagesEnabled), style: .blocks, emptyStateItem: emptyStateItem, searchItem: searchItem, animateChanges: previous != nil && admins != nil && previous!.count >= admins!.count) + let listState = ItemListNodeState(presentationData: ItemListPresentationData(presentationData), entries: channelAdminsControllerEntries(presentationData: presentationData, accountPeerId: context.account.peerId, peer: view.peer, state: state, participants: admins, antiSpamAvailable: antiSpamAvailable, antiSpamEnabled: antiSpamEnabled, signMessagesEnabled: signMessagesEnabled, showAuthorProfilesEnabled: showAuthorProfilesEnabled), style: .blocks, emptyStateItem: emptyStateItem, searchItem: searchItem, animateChanges: previous != nil && admins != nil && previous!.count >= admins!.count) return (controllerState, (listState, arguments)) } |> afterDisposed { diff --git a/submodules/ReactionSelectionNode/BUILD b/submodules/ReactionSelectionNode/BUILD index b94955dc38..fd7a395c21 100644 --- a/submodules/ReactionSelectionNode/BUILD +++ b/submodules/ReactionSelectionNode/BUILD @@ -38,6 +38,7 @@ swift_library( "//submodules/TelegramUI/Components/Utils/GenerateStickerPlaceholderImage", "//submodules/Components/BalancedTextComponent", "//submodules/Markdown", + "//submodules/TelegramUI/Components/Premium/PremiumStarComponent", ], visibility = [ "//visibility:public", diff --git a/submodules/ReactionSelectionNode/Sources/ReactionContextNode.swift b/submodules/ReactionSelectionNode/Sources/ReactionContextNode.swift index 73675e12e4..17a69a4228 100644 --- a/submodules/ReactionSelectionNode/Sources/ReactionContextNode.swift +++ b/submodules/ReactionSelectionNode/Sources/ReactionContextNode.swift @@ -24,6 +24,7 @@ import TextFormat import GZip import BalancedTextComponent import Markdown +import PremiumStarComponent public final class ReactionItem { public struct Reaction: Equatable { @@ -2672,7 +2673,7 @@ public final class ReactionContextNode: ASDisplayNode, ASScrollViewDelegate { if case .builtin = itemNode.item.reaction.rawValue { selfTargetBounds = selfTargetBounds.insetBy(dx: -selfTargetBounds.width * 0.5, dy: -selfTargetBounds.height * 0.5) } else if case .stars = itemNode.item.reaction.rawValue { - selfTargetBounds = selfTargetBounds.insetBy(dx: -selfTargetBounds.width * 0.5, dy: -selfTargetBounds.height * 0.5) + selfTargetBounds = selfTargetBounds.insetBy(dx: selfTargetBounds.width * 0.0, dy: selfTargetBounds.height * 0.0) } let selfTargetRect = self.view.convert(selfTargetBounds, from: targetView) @@ -2863,7 +2864,6 @@ public final class ReactionContextNode: ASDisplayNode, ASScrollViewDelegate { strongSelf.hapticFeedback = HapticFeedback() } strongSelf.hapticFeedback?.tap() - onHit?() if let targetView = targetView as? ReactionIconView { if switchToInlineImmediately { @@ -2886,6 +2886,8 @@ public final class ReactionContextNode: ASDisplayNode, ASScrollViewDelegate { } } + onHit?() + if switchToInlineImmediately { afterCompletion() } else { @@ -3723,7 +3725,7 @@ public final class StandaloneReactionAnimation: ASDisplayNode { if let itemNode = self.itemNode, case .builtin = itemNode.item.reaction.rawValue { selfTargetBounds = selfTargetBounds.insetBy(dx: -selfTargetBounds.width * 0.5, dy: -selfTargetBounds.height * 0.5) } else if let itemNode = self.itemNode, case .stars = itemNode.item.reaction.rawValue { - selfTargetBounds = selfTargetBounds.insetBy(dx: -selfTargetBounds.width * 0.5, dy: -selfTargetBounds.height * 0.5) + selfTargetBounds = selfTargetBounds.insetBy(dx: -selfTargetBounds.width * 0.0, dy: -selfTargetBounds.height * 0.0) } var targetFrame = self.view.convert(targetView.convert(selfTargetBounds, to: nil), from: nil) @@ -3731,7 +3733,7 @@ public final class StandaloneReactionAnimation: ASDisplayNode { if let itemNode = self.itemNode, case .builtin = itemNode.item.reaction.rawValue { targetFrame = targetFrame.insetBy(dx: -targetFrame.width * 0.5, dy: -targetFrame.height * 0.5) } else if let itemNode = self.itemNode, case .stars = itemNode.item.reaction.rawValue { - targetFrame = targetFrame.insetBy(dx: -targetFrame.width * 0.5, dy: -targetFrame.height * 0.5) + targetFrame = targetFrame.insetBy(dx: -targetFrame.width * 0.0, dy: -targetFrame.height * 0.0) } targetSnapshotView.frame = targetFrame @@ -3778,30 +3780,43 @@ public final class StandaloneReactionAnimation: ASDisplayNode { } public func animateOutToReaction(context: AccountContext, theme: PresentationTheme, item: ReactionItem, value: MessageReaction.Reaction, sourceView: UIView, targetView: UIView, hideNode: Bool, forceSwitchToInlineImmediately: Bool = false, animateTargetContainer: UIView?, addStandaloneReactionAnimation: ((StandaloneReactionAnimation) -> Void)?, onHit: (() -> Void)?, completion: @escaping () -> Void) { - let didTriggerExpandedReaction = !"".isEmpty - - let itemNode = ReactionNode(context: context, theme: theme, item: item, icon: .none, animationCache: context.animationCache, animationRenderer: context.animationRenderer, loopIdle: false, isLocked: false, useDirectRendering: true) - if let contents = sourceView.layer.contents { - itemNode.setCustomContents(contents: contents) + let star = ComponentView() + let starSize = star.update( + transition: .immediate, + component: AnyComponent(StandalonePremiumStarComponent( + theme: theme, + colors: [ + UIColor(rgb: 0xe57d02), + UIColor(rgb: 0xf09903), + UIColor(rgb: 0xf9b004), + UIColor(rgb: 0xfdd219) + ] + )), + environment: {}, + containerSize: CGSize(width: 200.0, height: 200.0) + ) + guard let starView = star.view else { + return } - self.addSubnode(itemNode) - itemNode.frame = sourceView.convert(sourceView.bounds, to: self.view) - itemNode.updateLayout(size: itemNode.frame.size, isExpanded: false, largeExpanded: false, isPreviewing: false, transition: .immediate) + + guard let sourceCloneView = sourceView.snapshotContentTree() else { + return + } + + self.view.addSubview(sourceCloneView) + self.view.addSubview(starView) + + sourceCloneView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.08, removeOnCompletion: false) + starView.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.12) + + let didTriggerExpandedReaction = "".isEmpty + + let sourceFrame = sourceView.convert(sourceView.bounds, to: self.view) + starView.bounds = CGRect(origin: CGPoint(), size: starSize) + sourceView.layer.isHidden = true - let switchToInlineImmediately: Bool - if itemNode.item.listAnimation.isVideoEmoji || itemNode.item.listAnimation.isVideoSticker || itemNode.item.listAnimation.isAnimatedSticker || itemNode.item.listAnimation.isStaticEmoji { - switch itemNode.item.reaction.rawValue { - case .builtin: - switchToInlineImmediately = forceSwitchToInlineImmediately - case .custom: - switchToInlineImmediately = !didTriggerExpandedReaction - case .stars: - switchToInlineImmediately = forceSwitchToInlineImmediately - } - } else { - switchToInlineImmediately = !didTriggerExpandedReaction - } + let switchToInlineImmediately: Bool = "".isEmpty if hideNode { if let animateTargetContainer = animateTargetContainer { @@ -3812,26 +3827,20 @@ public final class StandaloneReactionAnimation: ASDisplayNode { targetView.layer.animateAlpha(from: targetView.alpha, to: 0.0, duration: 0.2) } } + if let targetView = targetView as? ReactionIconView { + targetView.updateIsAnimationHidden(isAnimationHidden: true, transition: .immediate) + } - itemNode.isExtracted = true - let selfSourceRect = itemNode.view.convert(itemNode.view.bounds, to: self.view) + let selfSourceRect = sourceFrame var selfTargetBounds = targetView.bounds - if case .builtin = itemNode.item.reaction.rawValue { - selfTargetBounds = selfTargetBounds.insetBy(dx: -selfTargetBounds.width * 0.5, dy: -selfTargetBounds.height * 0.5) - } else if case .stars = itemNode.item.reaction.rawValue { - selfTargetBounds = selfTargetBounds.insetBy(dx: -selfTargetBounds.width * 0.5, dy: -selfTargetBounds.height * 0.5) - } + selfTargetBounds = selfTargetBounds.insetBy(dx: -selfTargetBounds.width * 0.5, dy: -selfTargetBounds.height * 0.5) let selfTargetRect = self.view.convert(selfTargetBounds, from: targetView) var expandedSize: CGSize = selfTargetRect.size if didTriggerExpandedReaction { - if itemNode.item.listAnimation.isVideoEmoji || itemNode.item.listAnimation.isVideoSticker || itemNode.item.listAnimation.isStaticEmoji { - expandedSize = CGSize(width: 80.0, height: 80.0) - } else { - expandedSize = CGSize(width: 120.0, height: 120.0) - } + expandedSize = CGSize(width: 120.0, height: 120.0) } let expandedFrame = CGRect(origin: CGPoint(x: selfTargetRect.midX - expandedSize.width / 2.0, y: selfTargetRect.midY - expandedSize.height / 2.0), size: expandedSize) @@ -3840,29 +3849,24 @@ public final class StandaloneReactionAnimation: ASDisplayNode { let incomingMessage: Bool = expandedFrame.midX < self.bounds.width / 2.0 if didTriggerExpandedReaction { let expandFactor: CGFloat = 0.5 - effectFrame = expandedFrame.insetBy(dx: -expandedFrame.width * expandFactor, dy: -expandedFrame.height * expandFactor).offsetBy(dx: incomingMessage ? (expandedFrame.width - 50.0) : (-expandedFrame.width + 50.0), dy: 0.0) + effectFrame = expandedFrame.insetBy(dx: -expandedFrame.width * expandFactor, dy: -expandedFrame.height * expandFactor)//.offsetBy(dx: incomingMessage ? (expandedFrame.width - 50.0) : (-expandedFrame.width + 50.0), dy: 0.0) } else { effectFrame = expandedFrame.insetBy(dx: -expandedSize.width, dy: -expandedSize.height) - if itemNode.item.isCustom { - effectFrame = effectFrame.insetBy(dx: -expandedSize.width, dy: -expandedSize.height) - } } - let transition: ContainedViewLayoutTransition = .animated(duration: 0.2, curve: .linear) + let transition: ContainedViewLayoutTransition = .animated(duration: 0.4, curve: .linear) - self.addSubnode(itemNode) - itemNode.position = expandedFrame.center - transition.updateBounds(node: itemNode, bounds: CGRect(origin: CGPoint(), size: expandedFrame.size)) - itemNode.updateLayout(size: expandedFrame.size, isExpanded: true, largeExpanded: didTriggerExpandedReaction, isPreviewing: false, transition: transition) + starView.center = expandedFrame.center + sourceCloneView.frame = sourceFrame let additionalAnimationNode: DefaultAnimatedStickerNodeImpl? var genericAnimationView: AnimationView? var additionalAnimation: TelegramMediaFile? if didTriggerExpandedReaction { - additionalAnimation = itemNode.item.largeApplicationAnimation + additionalAnimation = item.largeApplicationAnimation } else { - additionalAnimation = itemNode.item.applicationAnimation + additionalAnimation = item.applicationAnimation } if let additionalAnimation = additionalAnimation { @@ -3874,11 +3878,11 @@ public final class StandaloneReactionAnimation: ASDisplayNode { } } - additionalAnimationNodeValue.setup(source: AnimatedStickerResourceSource(account: itemNode.context.account, resource: additionalAnimation.resource), width: Int(effectFrame.width * 2.0), height: Int(effectFrame.height * 2.0), playbackMode: .once, mode: .direct(cachePathPrefix: context.account.postbox.mediaBox.shortLivedResourceCachePathPrefix(additionalAnimation.resource.id))) + additionalAnimationNodeValue.setup(source: AnimatedStickerResourceSource(account: context.account, resource: additionalAnimation.resource), width: Int(effectFrame.width * 2.0), height: Int(effectFrame.height * 2.0), playbackMode: .once, mode: .direct(cachePathPrefix: context.account.postbox.mediaBox.shortLivedResourceCachePathPrefix(additionalAnimation.resource.id))) additionalAnimationNodeValue.frame = effectFrame additionalAnimationNodeValue.updateLayout(size: effectFrame.size) self.addSubnode(additionalAnimationNodeValue) - } else if itemNode.item.isCustom { + } else if item.isCustom { additionalAnimationNode = nil var effectData: Data? @@ -3906,36 +3910,6 @@ public final class StandaloneReactionAnimation: ASDisplayNode { genericAnimationView = view - let animationCache = itemNode.context.animationCache - let animationRenderer = itemNode.context.animationRenderer - - for i in 1 ... 32 { - let allLayers = view.allLayers(forKeypath: AnimationKeypath(keypath: "placeholder_\(i)")) - for animationLayer in allLayers { - let baseItemLayer = InlineStickerItemLayer( - context: itemNode.context, - userLocation: .other, - attemptSynchronousLoad: false, - emoji: ChatTextInputTextCustomEmojiAttribute(interactivelySelectedFromPackId: nil, fileId: itemNode.item.listAnimation.fileId.id, file: itemNode.item.listAnimation), - file: itemNode.item.listAnimation, - cache: animationCache, - renderer: animationRenderer, - placeholderColor: UIColor(white: 0.0, alpha: 0.0), - pointSize: CGSize(width: didTriggerExpandedReaction ? 64.0 : 32.0, height: didTriggerExpandedReaction ? 64.0 : 32.0) - ) - - if let sublayers = animationLayer.sublayers { - for sublayer in sublayers { - sublayer.isHidden = true - } - } - - baseItemLayer.isVisibleForAnimations = true - baseItemLayer.frame = CGRect(origin: CGPoint(x: -0.0, y: -0.0), size: CGSize(width: 500.0, height: 500.0)) - animationLayer.addSublayer(baseItemLayer) - } - } - if didTriggerExpandedReaction { view.frame = effectFrame.insetBy(dx: -10.0, dy: -10.0).offsetBy(dx: incomingMessage ? 22.0 : -22.0, dy: 0.0) } else { @@ -3969,12 +3943,19 @@ public final class StandaloneReactionAnimation: ASDisplayNode { additionalAnimationCompleted = true } - transition.animatePositionWithKeyframes(node: itemNode, keyframes: generateParabollicMotionKeyframes(from: selfSourceRect.center, to: expandedFrame.center, elevation: 30.0), completion: { [weak itemNode, weak targetView, weak animateTargetContainer] _ in + starView.center = selfTargetRect.center + sourceCloneView.center = selfTargetRect.center + + let starSourceScale = sourceFrame.width / starSize.width + let starDestinationScale = selfTargetRect.width / starSize.width + + let keyframes = generateParabollicMotionKeyframes(from: selfSourceRect.center, to: expandedFrame.center, elevation: 40.0) + let scaleKeyframes = generateScaleKeyframes(from: starSourceScale, center: 1.0, to: starDestinationScale) + starView.layer.transform = CATransform3DMakeScale(starDestinationScale, starDestinationScale, 1.0) + transition.animateScaleWithKeyframes(layer: starView.layer, keyframes: scaleKeyframes) + transition.animatePositionWithKeyframes(layer: starView.layer, keyframes: keyframes, completion: { [weak starView, weak targetView, weak animateTargetContainer] _ in let afterCompletion: () -> Void = { - if didTriggerExpandedReaction { - return - } - guard let itemNode = itemNode else { + guard let starView else { return } if let animateTargetContainer = animateTargetContainer { @@ -3988,19 +3969,18 @@ public final class StandaloneReactionAnimation: ASDisplayNode { } HapticFeedback().tap() - onHit?() if let targetView = targetView as? ReactionIconView { if switchToInlineImmediately { targetView.updateIsAnimationHidden(isAnimationHidden: false, transition: .immediate) - itemNode.isHidden = true + starView.isHidden = true } else { targetView.updateIsAnimationHidden(isAnimationHidden: true, transition: .immediate) - targetView.addSubnode(itemNode) - itemNode.frame = selfTargetBounds + //TODO:release + //targetView.addSubnode(itemNode) } } else if let targetView = targetView as? UIImageView { - itemNode.isHidden = true + starView.isHidden = true targetView.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.12) targetView.layer.animateScale(from: 0.2, to: 1.0, duration: 0.12) } @@ -4011,14 +3991,17 @@ public final class StandaloneReactionAnimation: ASDisplayNode { } } + onHit?() + if switchToInlineImmediately { afterCompletion() } else { DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + 0.1, execute: afterCompletion) } }) + transition.animatePositionWithKeyframes(layer: sourceCloneView.layer, keyframes: keyframes) - DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + 0.15 * UIView.animationDurationFactor(), execute: { + DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + 0.3 * UIView.animationDurationFactor(), execute: { additionalAnimationNode?.visibility = true if let animateTargetContainer = animateTargetContainer { animateTargetContainer.isHidden = false @@ -4029,52 +4012,15 @@ public final class StandaloneReactionAnimation: ASDisplayNode { if !switchToInlineImmediately { DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + min(5.0, 2.0 * UIView.animationDurationFactor()), execute: { - if didTriggerExpandedReaction { - self.animateFromItemNodeToReaction(itemNode: itemNode, targetView: targetView, hideNode: hideNode, completion: { [weak self] in - if let strongSelf = self, didTriggerExpandedReaction, let addStandaloneReactionAnimation = addStandaloneReactionAnimation { - let standaloneReactionAnimation = StandaloneReactionAnimation(genericReactionEffect: strongSelf.genericReactionEffect) - - addStandaloneReactionAnimation(standaloneReactionAnimation) - - standaloneReactionAnimation.animateReactionSelection( - context: context, - theme: theme, - animationCache: context.animationCache, - reaction: itemNode.item, - avatarPeers: [], - playHaptic: false, - isLarge: false, - targetView: targetView, - addStandaloneReactionAnimation: nil, - completion: { [weak standaloneReactionAnimation] in - if let _ = standaloneReactionAnimation?.supernode { - standaloneReactionAnimation?.removeFromSupernode() - } else { - standaloneReactionAnimation?.view.removeFromSuperview() - } - } - ) - } - - mainAnimationCompleted = true - intermediateCompletion() - }) - } else { - if hideNode { - targetView.alpha = 1.0 - targetView.isHidden = false - if let targetView = targetView as? ReactionIconView { - targetView.updateIsAnimationHidden(isAnimationHidden: false, transition: .immediate) - if let _ = itemNode.supernode { - itemNode.removeFromSupernode() - } else { - itemNode.view.removeFromSuperview() - } - } + if hideNode { + targetView.alpha = 1.0 + targetView.isHidden = false + if let targetView = targetView as? ReactionIconView { + targetView.updateIsAnimationHidden(isAnimationHidden: false, transition: .immediate) } - mainAnimationCompleted = true - intermediateCompletion() } + mainAnimationCompleted = true + intermediateCompletion() }) } } @@ -4182,3 +4128,24 @@ private func generateParabollicMotionKeyframes(from sourcePoint: CGPoint, to tar return keyframes } + +private func generateScaleKeyframes(from: CGFloat, center: CGFloat, to: CGFloat) -> [CGFloat] { + var keyframes: [CGFloat] = [] + for i in 0 ..< 10 { + var k = CGFloat(i) / CGFloat(10 - 1) + let valueFrom: CGFloat + let valueTo: CGFloat + if k <= 0.5 { + k /= 0.5 + valueFrom = from + valueTo = center + } else { + k = (k - 0.5) / 0.5 + valueFrom = center + valueTo = to + } + let value = valueFrom * (1.0 - k) + valueTo * k + keyframes.append(value) + } + return keyframes +} diff --git a/submodules/TelegramCore/Sources/State/AvailableReactions.swift b/submodules/TelegramCore/Sources/State/AvailableReactions.swift index 0764d636f1..1fe767cb95 100644 --- a/submodules/TelegramCore/Sources/State/AvailableReactions.swift +++ b/submodules/TelegramCore/Sources/State/AvailableReactions.swift @@ -3,6 +3,45 @@ import TelegramApi import Postbox import SwiftSignalKit +private func generateStarsReactionFile(kind: Int, isAnimatedSticker: Bool) -> TelegramMediaFile { + let baseId: Int64 = 52343278047832950 + let fileId = baseId + Int64(kind) + + var attributes: [TelegramMediaFileAttribute] = [] + attributes.append(TelegramMediaFileAttribute.FileName(fileName: isAnimatedSticker ? "sticker.tgs" : "sticker.webp")) + if !isAnimatedSticker { + attributes.append(.CustomEmoji(isPremium: false, isSingleColor: false, alt: ".", packReference: nil)) + } + + return TelegramMediaFile( + fileId: MediaId(namespace: Namespaces.Media.CloudFile, id: fileId), + partialReference: nil, + resource: LocalFileMediaResource(fileId: fileId), + previewRepresentations: [], + videoThumbnails: [], + immediateThumbnailData: nil, + mimeType: isAnimatedSticker ? "application/x-tgsticker" : "image/webp", + size: nil, + attributes: attributes + ) +} + +private func generateStarsReaction() -> AvailableReactions.Reaction { + return AvailableReactions.Reaction( + isEnabled: false, + isPremium: false, + value: .stars, + title: "Star", + staticIcon: generateStarsReactionFile(kind: 0, isAnimatedSticker: true), + appearAnimation: generateStarsReactionFile(kind: 1, isAnimatedSticker: true), + selectAnimation: generateStarsReactionFile(kind: 2, isAnimatedSticker: true), + activateAnimation: generateStarsReactionFile(kind: 3, isAnimatedSticker: true), + effectAnimation: generateStarsReactionFile(kind: 4, isAnimatedSticker: true), + aroundAnimation: generateStarsReactionFile(kind: 5, isAnimatedSticker: true), + centerAnimation: generateStarsReactionFile(kind: 6, isAnimatedSticker: true) + ) +} + public final class AvailableReactions: Equatable, Codable { public final class Reaction: Equatable, Codable { private enum CodingKeys: String, CodingKey { @@ -181,24 +220,10 @@ public final class AvailableReactions: Equatable, Codable { ) { self.hash = hash - //TODO:release var reactions = reactions reactions.removeAll(where: { if case .stars = $0.value { return true } else { return false } }) - if let item = reactions.first(where: { if case .builtin("🤩") = $0.value { return true } else { return false } }) { - reactions.append(Reaction( - isEnabled: false, - isPremium: false, - value: .stars, - title: "Star", - staticIcon: item.staticIcon, - appearAnimation: item.appearAnimation, - selectAnimation: item.selectAnimation, - activateAnimation: item.activateAnimation, - effectAnimation: item.effectAnimation, - aroundAnimation: item.aroundAnimation, - centerAnimation: item.centerAnimation - )) - } + //TODO:release + reactions.append(generateStarsReaction()) self.reactions = reactions } @@ -220,21 +245,7 @@ public final class AvailableReactions: Equatable, Codable { //TODO:release var reactions = try container.decode([Reaction].self, forKey: .reactions) reactions.removeAll(where: { if case .stars = $0.value { return true } else { return false } }) - if let item = reactions.first(where: { if case .builtin("🤩") = $0.value { return true } else { return false } }) { - reactions.append(Reaction( - isEnabled: false, - isPremium: false, - value: .stars, - title: "Star", - staticIcon: item.staticIcon, - appearAnimation: item.appearAnimation, - selectAnimation: item.selectAnimation, - activateAnimation: item.activateAnimation, - effectAnimation: item.effectAnimation, - aroundAnimation: item.aroundAnimation, - centerAnimation: item.centerAnimation - )) - } + reactions.append(generateStarsReaction()) self.reactions = reactions } @@ -314,6 +325,31 @@ func _internal_setCachedAvailableReactions(transaction: Transaction, availableRe } func managedSynchronizeAvailableReactions(postbox: Postbox, network: Network) -> Signal { + let starsReaction = generateStarsReaction() + let mapping: [String: KeyPath] = [ + "star_reaction_activate.tgs": \.activateAnimation, + "star_reaction_appear.tgs": \.appearAnimation, + "star_reaction_effect.tgs": \.effectAnimation, + "star_reaction_select.tgs": \.selectAnimation, + "star_reaction_static_icon.webp": \.staticIcon + ] + let optionalMapping: [String: KeyPath] = [ + "star_reaction_center.tgs": \.centerAnimation, + "star_reaction_effect.tgs": \.aroundAnimation + ] + for (key, path) in mapping { + if let filePath = Bundle.main.path(forResource: key, ofType: nil), let data = try? Data(contentsOf: URL(fileURLWithPath: filePath)) { + postbox.mediaBox.storeResourceData(starsReaction[keyPath: path].resource.id, data: data) + } + } + for (key, path) in optionalMapping { + if let filePath = Bundle.main.path(forResource: key, ofType: nil), let data = try? Data(contentsOf: URL(fileURLWithPath: filePath)) { + if let file = starsReaction[keyPath: path] { + postbox.mediaBox.storeResourceData(file.resource.id, data: data) + } + } + } + let poll = Signal { subscriber in let signal: Signal = _internal_cachedAvailableReactions(postbox: postbox) |> mapToSignal { current in @@ -327,6 +363,7 @@ func managedSynchronizeAvailableReactions(postbox: Postbox, network: Network) -> guard let result = result else { return .complete() } + switch result { case let .availableReactions(hash, reactions): let availableReactions = AvailableReactions( diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Peers/UpdateCachedPeerData.swift b/submodules/TelegramCore/Sources/TelegramEngine/Peers/UpdateCachedPeerData.swift index 4a8b972ee1..119177fab8 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Peers/UpdateCachedPeerData.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Peers/UpdateCachedPeerData.swift @@ -696,7 +696,7 @@ func _internal_fetchAndUpdateCachedPeerData(accountPeerId: PeerId, peerId rawPee switch participantResult { case let .channelParticipant(participant, _, _): switch participant { - case let .channelParticipantSelf(_, _, inviterId, invitedDate, _): + case let .channelParticipantSelf(flags, _, inviterId, invitedDate, _): invitedBy = PeerId(namespace: Namespaces.Peer.CloudUser, id: PeerId.Id._internalFromInt64Value(inviterId)) if (flags & (1 << 0)) != 0 { invitedOn = invitedDate diff --git a/submodules/TelegramCore/Sources/Utils/MessageUtils.swift b/submodules/TelegramCore/Sources/Utils/MessageUtils.swift index e266dc6d11..9cbd7eaa73 100644 --- a/submodules/TelegramCore/Sources/Utils/MessageUtils.swift +++ b/submodules/TelegramCore/Sources/Utils/MessageUtils.swift @@ -530,6 +530,15 @@ public extension Message { var paidContent: TelegramMediaPaidContent? { return self.media.first(where: { $0 is TelegramMediaPaidContent }) as? TelegramMediaPaidContent } + + var authorSignatureAttribute: AuthorSignatureMessageAttribute? { + for attribute in self.attributes { + if let attribute = attribute as? AuthorSignatureMessageAttribute { + return attribute + } + } + return nil + } } public extension Message { diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageBubbleItemNode/Sources/ChatMessageBubbleItemNode.swift b/submodules/TelegramUI/Components/Chat/ChatMessageBubbleItemNode/Sources/ChatMessageBubbleItemNode.swift index 3945bc6377..0600467200 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessageBubbleItemNode/Sources/ChatMessageBubbleItemNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessageBubbleItemNode/Sources/ChatMessageBubbleItemNode.swift @@ -849,6 +849,12 @@ public class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewI for contentNode in self.contentNodes { contentNode.updateIsExtractedToContextPreview(isExtractedToContextPreview) } + + if !isExtractedToContextPreview { + if let item = self.item { + item.controllerInteraction.forceUpdateWarpContents() + } + } } self.mainContextSourceNode.updateAbsoluteRect = { [weak self] rect, size in @@ -1474,6 +1480,7 @@ public class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewI } var effectiveAuthor: Peer? + var overrideEffectiveAuthor = false var ignoreForward = false var displayAuthorInfo: Bool var ignoreNameHiding = false @@ -1542,6 +1549,31 @@ public class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewI displayAuthorInfo = false } } + + //TODO:release + if let channel = firstMessage.peers[firstMessage.id.peerId] as? TelegramChannel, case let .broadcast(info) = channel.info, info.flags.contains(.messagesShouldHaveSignatures) { + hasAvatar = true + if let authorSignatureAttribute = firstMessage.authorSignatureAttribute { + effectiveAuthor = TelegramUser(id: PeerId(namespace: Namespaces.Peer.Empty, id: PeerId.Id._internalFromInt64Value(Int64(authorSignatureAttribute.signature.persistentHashValue % 32))), accessHash: nil, firstName: authorSignatureAttribute.signature, lastName: nil, username: nil, phone: nil, photo: [], botInfo: nil, restrictionInfo: nil, flags: [], emojiStatus: nil, usernames: [], storiesHidden: nil, nameColor: nil, backgroundEmojiId: nil, profileColor: nil, profileBackgroundEmojiId: nil, subscriberCount: nil) + overrideEffectiveAuthor = true + + var allowAuthor = incoming + + if let author = firstMessage.author, author is TelegramChannel, !incoming || item.presentationData.isPreview { + allowAuthor = true + ignoreNameHiding = true + } + + if let subject = item.associatedData.subject, case let .customChatContents(contents) = subject, case .hashTagSearch = contents.kind { + ignoreNameHiding = true + } + + displayAuthorInfo = !mergedTop.merged && allowAuthor && peerId.isGroupOrChannel && effectiveAuthor != nil + if let forwardInfo = firstMessage.forwardInfo, forwardInfo.psaType != nil { + displayAuthorInfo = false + } + } + } if !peerId.isRepliesOrSavedMessages(accountPeerId: item.context.account.peerId) { if peerId.isGroupOrChannel && effectiveAuthor != nil { @@ -2089,7 +2121,7 @@ public class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewI } if initialDisplayHeader && displayAuthorInfo { - if let peer = firstMessage.peers[firstMessage.id.peerId] as? TelegramChannel, case .broadcast = peer.info, item.content.firstMessage.adAttribute == nil { + if let peer = firstMessage.peers[firstMessage.id.peerId] as? TelegramChannel, case .broadcast = peer.info, item.content.firstMessage.adAttribute == nil, !overrideEffectiveAuthor { authorNameString = EnginePeer(peer).displayTitle(strings: item.presentationData.strings, displayOrder: item.presentationData.nameDisplayOrder) let peer = (peer as Peer) diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageItemImpl/Sources/ChatMessageItemImpl.swift b/submodules/TelegramUI/Components/Chat/ChatMessageItemImpl/Sources/ChatMessageItemImpl.swift index d54a68b500..ae81587ac4 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessageItemImpl/Sources/ChatMessageItemImpl.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessageItemImpl/Sources/ChatMessageItemImpl.swift @@ -341,8 +341,16 @@ public final class ChatMessageItemImpl: ChatMessageItem, CustomStringConvertible } var hasAvatar = false - if !hasActionMedia && !isBroadcastChannel { - hasAvatar = true + if !hasActionMedia { + if !isBroadcastChannel { + hasAvatar = true + } else if let channel = message.peers[message.id.peerId] as? TelegramChannel, case let .broadcast(info) = channel.info, info.flags.contains(.messagesShouldHaveSignatures) { + //TODO:release + hasAvatar = true + if let authorSignatureAttribute = message.authorSignatureAttribute { + effectiveAuthor = TelegramUser(id: PeerId(namespace: Namespaces.Peer.Empty, id: PeerId.Id._internalFromInt64Value(Int64(authorSignatureAttribute.signature.persistentHashValue % 32))), accessHash: nil, firstName: authorSignatureAttribute.signature, lastName: nil, username: nil, phone: nil, photo: [], botInfo: nil, restrictionInfo: nil, flags: [], emojiStatus: nil, usernames: [], storiesHidden: nil, nameColor: nil, backgroundEmojiId: nil, profileColor: nil, profileBackgroundEmojiId: nil, subscriberCount: nil) + } + } } if hasAvatar { diff --git a/submodules/TelegramUI/Components/Chat/ChatRecentActionsController/Sources/ChatRecentActionsControllerNode.swift b/submodules/TelegramUI/Components/Chat/ChatRecentActionsController/Sources/ChatRecentActionsControllerNode.swift index 32d3549ad1..bbe912994c 100644 --- a/submodules/TelegramUI/Components/Chat/ChatRecentActionsController/Sources/ChatRecentActionsControllerNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatRecentActionsController/Sources/ChatRecentActionsControllerNode.swift @@ -628,6 +628,7 @@ final class ChatRecentActionsControllerNode: ViewControllerTracingNode { }, scrollToMessageId: { _ in }, navigateToStory: { _, _ in }, attemptedNavigationToPrivateQuote: { _ in + }, forceUpdateWarpContents: { }, automaticMediaDownloadSettings: self.automaticMediaDownloadSettings, pollActionState: ChatInterfacePollActionState(), stickerSettings: ChatInterfaceStickerSettings(), presentationContext: ChatPresentationContext(context: context, backgroundNode: self.backgroundNode)) self.controllerInteraction = controllerInteraction diff --git a/submodules/TelegramUI/Components/Chat/ChatSendAudioMessageContextPreview/Sources/ChatSendAudioMessageContextPreview.swift b/submodules/TelegramUI/Components/Chat/ChatSendAudioMessageContextPreview/Sources/ChatSendAudioMessageContextPreview.swift index e07264e9c3..714ce82b87 100644 --- a/submodules/TelegramUI/Components/Chat/ChatSendAudioMessageContextPreview/Sources/ChatSendAudioMessageContextPreview.swift +++ b/submodules/TelegramUI/Components/Chat/ChatSendAudioMessageContextPreview/Sources/ChatSendAudioMessageContextPreview.swift @@ -492,6 +492,7 @@ public final class ChatSendGroupMediaMessageContextPreview: UIView, ChatSendMess }, scrollToMessageId: { _ in }, navigateToStory: { _, _ in }, attemptedNavigationToPrivateQuote: { _ in + }, forceUpdateWarpContents: { }, automaticMediaDownloadSettings: MediaAutoDownloadSettings.defaultSettings, pollActionState: ChatInterfacePollActionState(), stickerSettings: ChatInterfaceStickerSettings(), presentationContext: ChatPresentationContext(context: self.context, backgroundNode: self.wallpaperBackgroundNode)) diff --git a/submodules/TelegramUI/Components/Chat/ChatSendStarsScreen/Sources/ChatSendStarsScreen.swift b/submodules/TelegramUI/Components/Chat/ChatSendStarsScreen/Sources/ChatSendStarsScreen.swift index 8b1be4cc5a..37000034f2 100644 --- a/submodules/TelegramUI/Components/Chat/ChatSendStarsScreen/Sources/ChatSendStarsScreen.swift +++ b/submodules/TelegramUI/Components/Chat/ChatSendStarsScreen/Sources/ChatSendStarsScreen.swift @@ -745,14 +745,14 @@ private final class ChatSendStarsScreenComponent: Component { let peer: EnginePeer let balance: Int64? let topPeers: [ChatSendStarsScreen.TopPeer] - let completion: (Int64, ChatSendStarsScreen.TransitionOut) -> Void + let completion: (Int64, Bool, ChatSendStarsScreen.TransitionOut) -> Void init( context: AccountContext, peer: EnginePeer, balance: Int64?, topPeers: [ChatSendStarsScreen.TopPeer], - completion: @escaping (Int64, ChatSendStarsScreen.TransitionOut) -> Void + completion: @escaping (Int64, Bool, ChatSendStarsScreen.TransitionOut) -> Void ) { self.context = context self.peer = peer @@ -1104,9 +1104,11 @@ private final class ChatSendStarsScreenComponent: Component { let progressFraction: CGFloat = CGFloat(self.amount) / CGFloat(1000 - 1) + let topCount = component.topPeers.max(by: { $0.count < $1.count })?.count + var topCutoffFraction: CGFloat? - if let maxCount = component.topPeers.max(by: { $0.count < $1.count })?.count { - let topCutoffFractionValue = CGFloat(maxCount) / CGFloat(1000 - 1) + if let topCount { + let topCutoffFractionValue = CGFloat(topCount) / CGFloat(1000 - 1) topCutoffFraction = topCutoffFractionValue let isPastCutoff = progressFraction >= topCutoffFractionValue @@ -1470,8 +1472,15 @@ private final class ChatSendStarsScreenComponent: Component { guard let badgeView = self.badge.view as? BadgeComponent.View else { return } + let isBecomingTop: Bool + if let topCount { + isBecomingTop = self.amount > topCount + } else { + isBecomingTop = true + } component.completion( self.amount, + isBecomingTop, ChatSendStarsScreen.TransitionOut( sourceView: badgeView.badgeIcon ) @@ -1617,7 +1626,7 @@ public class ChatSendStarsScreen: ViewControllerComponentContainer { private var presenceDisposable: Disposable? - public init(context: AccountContext, initialData: InitialData, completion: @escaping (Int64, TransitionOut) -> Void) { + public init(context: AccountContext, initialData: InitialData, completion: @escaping (Int64, Bool, TransitionOut) -> Void) { self.context = context super.init(context: context, component: ChatSendStarsScreenComponent( diff --git a/submodules/TelegramUI/Components/ChatControllerInteraction/Sources/ChatControllerInteraction.swift b/submodules/TelegramUI/Components/ChatControllerInteraction/Sources/ChatControllerInteraction.swift index 3fd747901f..1d555712bf 100644 --- a/submodules/TelegramUI/Components/ChatControllerInteraction/Sources/ChatControllerInteraction.swift +++ b/submodules/TelegramUI/Components/ChatControllerInteraction/Sources/ChatControllerInteraction.swift @@ -272,6 +272,7 @@ public final class ChatControllerInteraction: ChatControllerInteractionProtocol public let scrollToMessageId: (MessageIndex) -> Void public let navigateToStory: (Message, StoryId) -> Void public let attemptedNavigationToPrivateQuote: (Peer?) -> Void + public let forceUpdateWarpContents: () -> Void public var canPlayMedia: Bool = false public var hiddenMedia: [MessageId: [Media]] = [:] @@ -400,6 +401,7 @@ public final class ChatControllerInteraction: ChatControllerInteractionProtocol scrollToMessageId: @escaping (MessageIndex) -> Void, navigateToStory: @escaping (Message, StoryId) -> Void, attemptedNavigationToPrivateQuote: @escaping (Peer?) -> Void, + forceUpdateWarpContents: @escaping () -> Void, automaticMediaDownloadSettings: MediaAutoDownloadSettings, pollActionState: ChatInterfacePollActionState, stickerSettings: ChatInterfaceStickerSettings, @@ -509,6 +511,7 @@ public final class ChatControllerInteraction: ChatControllerInteractionProtocol self.scrollToMessageId = scrollToMessageId self.navigateToStory = navigateToStory self.attemptedNavigationToPrivateQuote = attemptedNavigationToPrivateQuote + self.forceUpdateWarpContents = forceUpdateWarpContents self.automaticMediaDownloadSettings = automaticMediaDownloadSettings diff --git a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreen.swift b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreen.swift index 98b14e1ed8..b6a60d4137 100644 --- a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreen.swift +++ b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreen.swift @@ -3485,6 +3485,7 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro }, scrollToMessageId: { _ in }, navigateToStory: { _, _ in }, attemptedNavigationToPrivateQuote: { _ in + }, forceUpdateWarpContents: { }, automaticMediaDownloadSettings: MediaAutoDownloadSettings.defaultSettings, pollActionState: ChatInterfacePollActionState(), stickerSettings: ChatInterfaceStickerSettings(), presentationContext: ChatPresentationContext(context: context, backgroundNode: nil)) self.hiddenMediaDisposable = context.sharedContext.mediaManager.galleryHiddenMediaManager.hiddenIds().startStrict(next: { [weak self] ids in diff --git a/submodules/TelegramUI/Components/Premium/PremiumStarComponent/Sources/PremiumStarComponent.swift b/submodules/TelegramUI/Components/Premium/PremiumStarComponent/Sources/PremiumStarComponent.swift index 495541776b..9368f7daba 100644 --- a/submodules/TelegramUI/Components/Premium/PremiumStarComponent/Sources/PremiumStarComponent.swift +++ b/submodules/TelegramUI/Components/Premium/PremiumStarComponent/Sources/PremiumStarComponent.swift @@ -684,3 +684,208 @@ public final class PremiumStarComponent: Component { return view.update(component: self, availableSize: availableSize, transition: transition) } } + +public final class StandalonePremiumStarComponent: Component { + let theme: PresentationTheme + let colors: [UIColor]? + + public init( + theme: PresentationTheme, + colors: [UIColor]? = nil + ) { + self.theme = theme + self.colors = colors + } + + public static func ==(lhs: StandalonePremiumStarComponent, rhs: StandalonePremiumStarComponent) -> Bool { + return lhs.theme === rhs.theme && lhs.colors == rhs.colors + } + + public final class View: UIView, SCNSceneRendererDelegate, ComponentTaggedView { + public final class Tag { + public init() { + } + } + + public func matches(tag: Any) -> Bool { + if let _ = tag as? Tag { + return true + } + return false + } + + private var component: StandalonePremiumStarComponent? + + private var _ready = Promise() + public var ready: Signal { + return self._ready.get() + } + + private let sceneView: SCNView + + private var timer: SwiftSignalKit.Timer? + + override init(frame: CGRect) { + self.sceneView = SCNView(frame: CGRect(origin: .zero, size: CGSize(width: 64.0, height: 64.0))) + self.sceneView.backgroundColor = .clear + self.sceneView.transform = CGAffineTransform(scaleX: 0.5, y: 0.5) + self.sceneView.isUserInteractionEnabled = false + self.sceneView.preferredFramesPerSecond = 60 + self.sceneView.isJitteringEnabled = true + + super.init(frame: frame) + + self.addSubview(self.sceneView) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + deinit { + self.timer?.invalidate() + } + + private var didSetup = false + private func setup() { + guard !self.didSetup, let scene = loadCompressedScene(name: "star2", version: sceneVersion) else { + return + } + + self.didSetup = true + self.sceneView.scene = scene + self.sceneView.delegate = self + + if let component = self.component, let node = scene.rootNode.childNode(withName: "star", recursively: false), let colors = + component.colors { + node.geometry?.materials.first?.diffuse.contents = generateDiffuseTexture(colors: colors) + } + + for node in scene.rootNode.childNodes { + if let name = node.name, name.hasPrefix("particles") { + node.isHidden = true + } + } + + self.didSetReady = true + self._ready.set(.single(true)) + self.onReady() + } + + private var didSetReady = false + public func renderer(_ renderer: SCNSceneRenderer, didRenderScene scene: SCNScene, atTime time: TimeInterval) { + if !self.didSetReady { + self.didSetReady = true + + Queue.mainQueue().justDispatch { + self._ready.set(.single(true)) + self.onReady() + } + } + } + + private func onReady() { + //self.setupScaleAnimation() + //self.setupGradientAnimation() + + self.playAppearanceAnimation(mirror: true) + } + + private func setupScaleAnimation() { + guard let scene = self.sceneView.scene, let node = scene.rootNode.childNode(withName: "star", recursively: false) else { + return + } + + let fromScale: Float = 0.1 + let toScale: Float = 0.092 + + let animation = CABasicAnimation(keyPath: "scale") + animation.duration = 2.0 + animation.fromValue = NSValue(scnVector3: SCNVector3(x: fromScale, y: fromScale, z: fromScale)) + animation.toValue = NSValue(scnVector3: SCNVector3(x: toScale, y: toScale, z: toScale)) + animation.timingFunction = CAMediaTimingFunction(name: .easeInEaseOut) + animation.autoreverses = true + animation.repeatCount = .infinity + + node.addAnimation(animation, forKey: "scale") + } + + private func setupGradientAnimation() { + guard let scene = self.sceneView.scene, let node = scene.rootNode.childNode(withName: "star", recursively: false) else { + return + } + guard let initial = node.geometry?.materials.first?.diffuse.contentsTransform else { + return + } + + let animation = CABasicAnimation(keyPath: "contentsTransform") + animation.duration = 4.5 + animation.fromValue = NSValue(scnMatrix4: initial) + animation.toValue = NSValue(scnMatrix4: SCNMatrix4Translate(initial, -0.35, 0.35, 0)) + animation.timingFunction = CAMediaTimingFunction(name: .linear) + animation.autoreverses = true + animation.repeatCount = .infinity + + node.geometry?.materials.first?.diffuse.addAnimation(animation, forKey: "gradient") + } + + private func playAppearanceAnimation(velocity: CGFloat? = nil, smallAngle: Bool = false, mirror: Bool = false) { + guard let scene = self.sceneView.scene, let node = scene.rootNode.childNode(withName: "star", recursively: false) else { + return + } + + var from = node.presentation.eulerAngles + if abs(from.y - .pi * 2.0) < 0.001 { + from.y = 0.0 + } + node.removeAnimation(forKey: "tapRotate") + + var toValue: Float = smallAngle ? 0.0 : .pi * 2.0 + if let velocity = velocity, !smallAngle && abs(velocity) > 200 && velocity < 0.0 { + toValue *= -1 + } + if mirror { + toValue *= -1 + } + let to = SCNVector3(x: 0.0, y: toValue, z: 0.0) + let distance = rad2deg(to.y - from.y) + + guard !distance.isZero else { + return + } + + let animation = CABasicAnimation(keyPath: "eulerAngles") + animation.fromValue = NSValue(scnVector3: from) + animation.toValue = NSValue(scnVector3: to) + animation.duration = 0.4 * UIView.animationDurationFactor() + animation.timingFunction = CAMediaTimingFunction(name: .easeInEaseOut) + animation.completion = { [weak node] finished in + if finished { + node?.eulerAngles = SCNVector3(x: 0.0, y: 0.0, z: 0.0) + } + } + node.addAnimation(animation, forKey: "rotate") + } + + func update(component: StandalonePremiumStarComponent, availableSize: CGSize, transition: ComponentTransition) -> CGSize { + self.component = component + + self.setup() + + self.sceneView.bounds = CGRect(origin: .zero, size: CGSize(width: availableSize.width * 2.0, height: availableSize.height * 2.0)) + if self.sceneView.superview == self { + self.sceneView.center = CGPoint(x: availableSize.width / 2.0, y: availableSize.height / 2.0) + } + + return availableSize + } + } + + public func makeView() -> View { + return View(frame: CGRect()) + } + + public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { + return view.update(component: self, availableSize: availableSize, transition: transition) + } +} diff --git a/submodules/TelegramUI/Components/SpaceWarpView/Sources/MeshLayer.swift b/submodules/TelegramUI/Components/SpaceWarpView/Sources/MeshLayer.swift new file mode 100644 index 0000000000..874704418a --- /dev/null +++ b/submodules/TelegramUI/Components/SpaceWarpView/Sources/MeshLayer.swift @@ -0,0 +1,7 @@ +import Foundation +import UIKit +import Display + +final class MeshLayer: CALayer { + +} diff --git a/submodules/TelegramUI/Components/SpaceWarpView/Sources/SpaceWarpView.swift b/submodules/TelegramUI/Components/SpaceWarpView/Sources/SpaceWarpView.swift index 99c074523e..48f18d5f66 100644 --- a/submodules/TelegramUI/Components/SpaceWarpView/Sources/SpaceWarpView.swift +++ b/submodules/TelegramUI/Components/SpaceWarpView/Sources/SpaceWarpView.swift @@ -255,7 +255,7 @@ private final class MaskGridLayer: SimpleLayer { private var resolution: (x: Int, y: Int)? - func updateGrid(resolutionX: Int, resolutionY: Int) { + func updateGrid(size: CGSize, resolutionX: Int, resolutionY: Int, cornerRadius: CGFloat) { if let resolution = self.resolution, resolution.x == resolutionX, resolution.y == resolutionY { return } @@ -266,13 +266,52 @@ private final class MaskGridLayer: SimpleLayer { } self.itemLayers.removeAll() - for _ in 0 ..< resolutionX * resolutionY { - let itemLayer = SimpleLayer() - itemLayer.backgroundColor = UIColor.black.cgColor - itemLayer.opacity = 1.0 - itemLayer.anchorPoint = CGPoint() - self.addSublayer(itemLayer) - self.itemLayers.append(itemLayer) + let itemSize = CGSize(width: size.width / CGFloat(resolutionX), height: size.height / CGFloat(resolutionY)) + + let topLeftCorner = CGRect(origin: CGPoint(), size: CGSize(width: cornerRadius, height: cornerRadius)) + let topRightCorner = CGRect(origin: CGPoint(x: size.width - cornerRadius, y: 0.0), size: CGSize(width: cornerRadius, height: cornerRadius)) + let bottomLeftCorner = CGRect(origin: CGPoint(x: 0.0, y: size.height - cornerRadius), size: CGSize(width: cornerRadius, height: cornerRadius)) + let bottomRightCorner = CGRect(origin: CGPoint(x: size.width - cornerRadius, y: size.height - cornerRadius), size: CGSize(width: cornerRadius, height: cornerRadius)) + + var cornersImage: UIImage? + if cornerRadius > 0.0 { + cornersImage = generateImage(CGSize(width: cornerRadius * 2.0 + 200.0, height: cornerRadius * 2.0 + 200.0), rotatedContext: { size, context in + context.clear(CGRect(origin: CGPoint(), size: size)) + context.setFillColor(UIColor.black.cgColor) + context.addPath(UIBezierPath(roundedRect: CGRect(origin: CGPoint(), size: size), cornerRadius: cornerRadius).cgPath) + context.fillPath() + }) + } + + for y in 0 ..< resolutionY { + for x in 0 ..< resolutionX { + let itemLayer = SimpleLayer() + itemLayer.backgroundColor = UIColor.black.cgColor + itemLayer.isOpaque = true + itemLayer.opacity = 1.0 + itemLayer.anchorPoint = CGPoint() + self.addSublayer(itemLayer) + self.itemLayers.append(itemLayer) + + if cornerRadius > 0.0, let cornersImage { + let gridPosition = CGPoint(x: CGFloat(x) / CGFloat(resolutionX), y: CGFloat(y) / CGFloat(resolutionY)) + let sourceRect = CGRect(origin: CGPoint(x: gridPosition.x * (size.width), y: gridPosition.y * (size.height)), size: itemSize) + if sourceRect.intersects(topLeftCorner) || sourceRect.intersects(topRightCorner) || sourceRect.intersects(bottomLeftCorner) || sourceRect.intersects(bottomRightCorner) { + var clippedCornersRect = sourceRect + if clippedCornersRect.maxX > cornersImage.size.width { + clippedCornersRect.origin.x -= size.width - cornersImage.size.width + } + if clippedCornersRect.maxY > cornersImage.size.height { + clippedCornersRect.origin.y -= size.height - cornersImage.size.height + } + + itemLayer.contents = cornersImage.cgImage + itemLayer.contentsRect = CGRect(origin: CGPoint(x: clippedCornersRect.minX / cornersImage.size.width, y: clippedCornersRect.minY / cornersImage.size.height), size: CGSize(width: clippedCornersRect.width / cornersImage.size.width, height: clippedCornersRect.height / cornersImage.size.height)) + itemLayer.backgroundColor = nil + itemLayer.isOpaque = false + } + } + } } } @@ -503,7 +542,7 @@ open class SpaceWarpNodeImpl: ASDisplayNode, SpaceWarpNode { let endEndPoint = CGPoint(x: (gradientLayer.startPoint.x + radius.width), y: (gradientLayer.startPoint.y + radius.height)) gradientLayer.endPoint = endEndPoint - let maxWavefrontNorm: CGFloat = 0.3 + let maxWavefrontNorm: CGFloat = 0.4 let normProgress = max(0.0, min(1.0, progress)) let interpolatedNorm: CGFloat = 1.0 * (1.0 - normProgress) + maxWavefrontNorm * normProgress @@ -587,7 +626,7 @@ open class SpaceWarpNodeImpl: ASDisplayNode, SpaceWarpNode { } if let gradientMaskLayer = self.gradientMaskLayer { - gradientMaskLayer.updateGrid(resolutionX: resolutionX, resolutionY: resolutionY) + gradientMaskLayer.updateGrid(size: size, resolutionX: resolutionX, resolutionY: resolutionY, cornerRadius: cornerRadius) gradientMaskLayer.update(positions: instancePositions, bounds: instanceBounds, transforms: instanceTransforms) } } diff --git a/submodules/TelegramUI/Resources/Animations/star_up/star_reaction_activate.tgs b/submodules/TelegramUI/Resources/Animations/star_up/star_reaction_activate.tgs new file mode 100644 index 0000000000000000000000000000000000000000..61f1538549f2aa06807dbf5c7d89f2b8f15ccef6 GIT binary patch literal 5659 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 B3rYY0 literal 0 HcmV?d00001 diff --git a/submodules/TelegramUI/Resources/Animations/star_up/star_reaction_appear.tgs b/submodules/TelegramUI/Resources/Animations/star_up/star_reaction_appear.tgs new file mode 100644 index 0000000000000000000000000000000000000000..3d4e32a67eb427151adbffaa08b57e2b6be52462 GIT binary patch literal 5796 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 B3rYY0 literal 0 HcmV?d00001 diff --git a/submodules/TelegramUI/Resources/Animations/star_up/star_reaction_effect.tgs b/submodules/TelegramUI/Resources/Animations/star_up/star_reaction_effect.tgs new file mode 100644 index 0000000000000000000000000000000000000000..f31897de51b445d2be1e18d1bbdcfea0df7385a4 GIT binary patch literal 22618 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 Tc+M^T_|N|jWg!;r|{O+M+C; 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 B3rYY0 literal 0 HcmV?d00001 diff --git a/submodules/TelegramUI/Resources/Animations/star_up/star_reaction_static_icon.webp b/submodules/TelegramUI/Resources/Animations/star_up/star_reaction_static_icon.webp new file mode 100644 index 0000000000000000000000000000000000000000..9c9b83471d98f5bdd3499ba5879b13875d8c092c GIT binary patch literal 2850 zcmV+-3*GcmNk&E*3jhFDMM6+kP&iBu3jhEwN5Byf2?cE%IZFGx&!6zZ5Yhh$;9Jo{ zmrV?a0++B8Lg@bl;K`l{ z7&!qDzz+bt{0&ATzz!A)(SuB8F{!b7>}va;MwaxK4CUejMZ?U@Oc!zTVdj|&=yWmK zXfI-Kf*B)bI^0mzRd(6ZV0X1Eo9Gt}w0j}!rcIWSl zlg|19wv)=N?B42~lAUeae$D$jl&Ebxq(@h|Dz=@BZQHi(nU!tZHrBT7Wo+9{y8BO8 z)dUa%JmLPvxAZT&2elk4K!wlr!M%iy(|5<)dJ8kYwb1*hYh}9eo?kTO%>TP5cAq{n z&+l01ecqk28HtPTmcRT1jn02=oVwfIHA*+70}`!d*cn|PU7RbDZBYSqg8Y6@%ubVNtrVPigF=|Q zul-v-U14~*w^mj9bP6{pu?F_@0*@XIHfgVjio7&&)l}X7k})crb?mi~O4=Q%iy+}w zOT3MsYX{)?y+WG%c6}(FCXqe-%Sz8_x>?9d*vIpT1fKyl*6FT8IqTdyxO|FkdBs+$ zIqMH1IvlR^A?7E2-Ur|iJD7%JcM8uuaPxbT=?RlZ4%sTBjIV_xn7u-phjzUnnihda4q2*RHe+h>2^MYP-ZJUZt)JD(r^EG%_rF$kFF=Yl{*T-&F>o!H6zDV zr+ISRQTs@u7;~sjg@0p(1Ag<4c;3Pxz5N>nxXG%F&$Tk{-U%Ww@Ev2j@>V%bEUMhm9 z@Z+ciX+ToDB0s9l$G&6BXonEx#Di$VNj$p1#|U*lNxlbH^LU=<{!*cb(SOk>cj4lW!>m^%Tl^HMpq zPAFkd#}M?;uX)&Gr|s0xHZ^S&u|~*B0YivD@aQ35`xp<%n6{e7R4f`ZZEyR;F#eau zoHQ0?J}>bFS_vEPA(_uv(spLf~GClubd;F>R=(E0yFqzWEivmoIwF+b_^ zu09{aWD_PV^1Y0C0yKpNY$`43_y0OL`Q4cGMxa5j)eWS7RjNoK87u9prvL!OyfJ_J z^k>KX<t@0BqbS%G)|k)ZQ}r}r{p8z)o{XD&QM@`N#fOcVc9;W2 zZGjK%=LL(r25XzKWWhgM8h~P3d>s{DY53azOJ8i=V^DaCtEAD1+h%koRpcF zel;sCW*z#wd4x8wFtbyf<0*W zFJgt6Y~`EBDV(xxMfV!<&z@_QW$NCG*;xUGiNs*mQSkcbNh;);%utDk6)tGb)V+ly zC_LB7W9q376orKYTR7OShov2F5dwh{c}`6KQ0}8}b$z1ju4G{%R7&R4A{bfzYae7hSkaoj(Sw@+p!M$ zRaHNUznTmE+G}xOl{U-l&>#U?363ocU>(@PF^XeuTCKLM9ru*3w>9<6p;K=5ekW?* zYHW3;PiH5t(&cbg^Hl@13T(mwUk4c002bzm{W8^g%ZX3pWBu);?Q+wexKzsMLnMuc z4o94<;i=*ng@Gn8ED8%o!ElD*(FVS>$PLd++Dn8y-bSItp?|F;g`xo%;OoHm818$} zBz7=>g->AL)yfRdi#OO;o31sZe%2*AwpcjE*8xSCDVTka0RzXdfKuU6+wiiCS~V;$ zBB~=QAwcz_hgO4O;n+HS4-8nxuz+C)PizD(maS}^7ol=k*<@TEIw(sHY~dJSSXeM% z7|sAvaNy`)*RcJV-Sfg`p05e5votwd}tXQI$B=JxH?~mN_}Hi%j`VCw`68c6QESUv+$IbV18u? zVQwzs&KT($KMAbO=7u0VGn*}F-hQT)L|Y6R2djpz)@Zz?Fj%;YBV&7#Nh49x)`+P6 zWaF9Q8Tg1TS2mg if !reaction.isSelected { continue } + if case .stars = reaction.value { + continue + } reactions.insert(reaction.value) switch reaction.value { case .builtin, .stars: diff --git a/submodules/TelegramUI/Sources/ChatController.swift b/submodules/TelegramUI/Sources/ChatController.swift index 1722b18872..b594c88667 100644 --- a/submodules/TelegramUI/Sources/ChatController.swift +++ b/submodules/TelegramUI/Sources/ChatController.swift @@ -4525,6 +4525,11 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G text = self.presentationData.strings.Chat_ToastQuoteChatUnavailbalePrivateChat } self.controllerInteraction?.displayUndo(.info(title: nil, text: text, timeout: nil, customUndoText: nil)) + }, forceUpdateWarpContents: { [weak self] in + guard let self else { + return + } + self.chatDisplayNode.forceUpdateWarpContents() }, automaticMediaDownloadSettings: self.automaticMediaDownloadSettings, pollActionState: ChatInterfacePollActionState(), stickerSettings: self.stickerSettings, presentationContext: ChatPresentationContext(context: context, backgroundNode: self.chatBackgroundNode)) controllerInteraction.enableFullTranslucency = context.sharedContext.energyUsageSettings.fullTranslucency diff --git a/submodules/TelegramUI/Sources/ChatControllerNode.swift b/submodules/TelegramUI/Sources/ChatControllerNode.swift index 4ca47e5657..a9bee54363 100644 --- a/submodules/TelegramUI/Sources/ChatControllerNode.swift +++ b/submodules/TelegramUI/Sources/ChatControllerNode.swift @@ -1076,6 +1076,13 @@ class ChatControllerNode: ASDisplayNode, ASScrollViewDelegate { return CGSize(width: layout.size.width, height: height) } + func forceUpdateWarpContents() { + guard let (layout, _) = self.validLayout else { + return + } + self.wrappingNode.update(size: layout.size, cornerRadius: layout.deviceMetrics.screenCornerRadius, transition: .immediate) + } + func containerLayoutUpdated(_ layout: ContainerViewLayout, navigationBarHeight: CGFloat, transition protoTransition: ContainedViewLayoutTransition, listViewTransaction: (ListViewUpdateSizeAndInsets, CGFloat, Bool, @escaping () -> Void) -> Void, updateExtraNavigationBarBackgroundHeight: (CGFloat, CGFloat, ContainedViewLayoutTransition) -> Void) { let transition: ContainedViewLayoutTransition if let _ = self.scheduledAnimateInAsOverlayFromNode { diff --git a/submodules/TelegramUI/Sources/ChatControllerOpenMessageReactionContextMenu.swift b/submodules/TelegramUI/Sources/ChatControllerOpenMessageReactionContextMenu.swift index fc8bd905eb..090fa6bdfc 100644 --- a/submodules/TelegramUI/Sources/ChatControllerOpenMessageReactionContextMenu.swift +++ b/submodules/TelegramUI/Sources/ChatControllerOpenMessageReactionContextMenu.swift @@ -171,7 +171,7 @@ extension ChatControllerImpl { guard let self, let initialData else { return } - self.push(ChatSendStarsScreen(context: self.context, initialData: initialData, completion: { [weak self] amount, transitionOut in + self.push(ChatSendStarsScreen(context: self.context, initialData: initialData, completion: { [weak self] amount, isBecomingTop, transitionOut in guard let self, amount > 0 else { return } @@ -216,7 +216,7 @@ extension ChatControllerImpl { self.chatDisplayNode.messageTransitionNode.addMessageStandaloneReactionAnimation(messageId: item.message.id, standaloneReactionAnimation: standaloneReactionAnimation) - self.chatDisplayNode.addSubnode(standaloneReactionAnimation) + self.view.window?.addSubview(standaloneReactionAnimation.view) standaloneReactionAnimation.frame = self.chatDisplayNode.bounds standaloneReactionAnimation.animateOutToReaction( context: self.context, @@ -240,47 +240,23 @@ extension ChatControllerImpl { guard let self else { return } + + if isBecomingTop { + self.chatDisplayNode.animateQuizCorrectOptionSelected() + } + if let itemNode, let targetView = itemNode.targetReactionView(value: .stars) { self.chatDisplayNode.wrappingNode.triggerRipple(at: targetView.convert(targetView.bounds.center, to: self.chatDisplayNode.view)) } }, completion: { [weak standaloneReactionAnimation] in - standaloneReactionAnimation?.removeFromSupernode() + standaloneReactionAnimation?.view.removeFromSuperview() } ) - /*standaloneReactionAnimation.animateReactionSelection( - context: strongSelf.context, - theme: strongSelf.presentationData.theme, - animationCache: strongSelf.controllerInteraction!.presentationContext.animationCache, - reaction: reactionItem, - avatarPeers: [], - playHaptic: false, - isLarge: false, - targetView: targetView, - addStandaloneReactionAnimation: { standaloneReactionAnimation in - guard let strongSelf = self else { - return - } - strongSelf.chatDisplayNode.messageTransitionNode.addMessageStandaloneReactionAnimation(messageId: item.message.id, standaloneReactionAnimation: standaloneReactionAnimation) - standaloneReactionAnimation.frame = strongSelf.chatDisplayNode.bounds - strongSelf.chatDisplayNode.addSubnode(standaloneReactionAnimation) - }, - onHit: { [weak itemNode] in - guard let strongSelf = self else { - return - } - if let itemNode = itemNode, let targetView = itemNode.targetReactionView(value: chosenReaction) { - strongSelf.chatDisplayNode.wrappingNode.triggerRipple(at: targetView.convert(targetView.bounds.center, to: strongSelf.chatDisplayNode.view)) - } - }, - completion: { [weak standaloneReactionAnimation] in - standaloneReactionAnimation?.removeFromSupernode() - } - )*/ } } - let _ = self.context.engine.messages.sendStarsReaction(id: message.id, count: Int(amount)) + //let _ = self.context.engine.messages.sendStarsReaction(id: message.id, count: Int(amount)) let _ = (self.context.engine.stickers.resolveInlineStickers(fileIds: [MessageReaction.starsReactionId]) |> deliverOnMainQueue).start(next: { [weak self] files in diff --git a/submodules/TelegramUI/Sources/OverlayAudioPlayerControllerNode.swift b/submodules/TelegramUI/Sources/OverlayAudioPlayerControllerNode.swift index c731e46943..98bd6c348e 100644 --- a/submodules/TelegramUI/Sources/OverlayAudioPlayerControllerNode.swift +++ b/submodules/TelegramUI/Sources/OverlayAudioPlayerControllerNode.swift @@ -184,6 +184,7 @@ final class OverlayAudioPlayerControllerNode: ViewControllerTracingNode, ASGestu }, scrollToMessageId: { _ in }, navigateToStory: { _, _ in }, attemptedNavigationToPrivateQuote: { _ in + }, forceUpdateWarpContents: { }, automaticMediaDownloadSettings: MediaAutoDownloadSettings.defaultSettings, pollActionState: ChatInterfacePollActionState(), stickerSettings: ChatInterfaceStickerSettings(), presentationContext: ChatPresentationContext(context: context, backgroundNode: nil)) self.dimNode = ASDisplayNode() diff --git a/submodules/TelegramUI/Sources/SharedAccountContext.swift b/submodules/TelegramUI/Sources/SharedAccountContext.swift index d040458dc2..01749d1680 100644 --- a/submodules/TelegramUI/Sources/SharedAccountContext.swift +++ b/submodules/TelegramUI/Sources/SharedAccountContext.swift @@ -1787,6 +1787,7 @@ public final class SharedAccountContextImpl: SharedAccountContext { }, scrollToMessageId: { _ in }, navigateToStory: { _, _ in }, attemptedNavigationToPrivateQuote: { _ in + }, forceUpdateWarpContents: { }, automaticMediaDownloadSettings: MediaAutoDownloadSettings.defaultSettings, pollActionState: ChatInterfacePollActionState(), stickerSettings: ChatInterfaceStickerSettings(), presentationContext: ChatPresentationContext(context: context, backgroundNode: backgroundNode as? WallpaperBackgroundNode))