[WIP] Star reactions

This commit is contained in:
Isaac 2024-08-06 15:00:29 +04:00
parent 33cb344b45
commit 3ff98fd2a8
32 changed files with 589 additions and 228 deletions

View File

@ -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))

View File

@ -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(

View File

@ -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()
})
}

View File

@ -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:

View File

@ -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 {

View File

@ -38,6 +38,7 @@ swift_library(
"//submodules/TelegramUI/Components/Utils/GenerateStickerPlaceholderImage",
"//submodules/Components/BalancedTextComponent",
"//submodules/Markdown",
"//submodules/TelegramUI/Components/Premium/PremiumStarComponent",
],
visibility = [
"//visibility:public",

View File

@ -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<Empty>()
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
}

View File

@ -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<Never, NoError> {
let starsReaction = generateStarsReaction()
let mapping: [String: KeyPath<AvailableReactions.Reaction, TelegramMediaFile>] = [
"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<AvailableReactions.Reaction, TelegramMediaFile?>] = [
"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<Never, NoError> { subscriber in
let signal: Signal<Never, NoError> = _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(

View File

@ -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

View File

@ -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 {

View File

@ -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)

View File

@ -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 {

View File

@ -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

View File

@ -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))

View File

@ -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(

View File

@ -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

View File

@ -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

View File

@ -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<Bool>()
public var ready: Signal<Bool, NoError> {
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<Empty>, transition: ComponentTransition) -> CGSize {
return view.update(component: self, availableSize: availableSize, transition: transition)
}
}

View File

@ -0,0 +1,7 @@
import Foundation
import UIKit
import Display
final class MeshLayer: CALayer {
}

View File

@ -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)
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

View File

@ -17,6 +17,9 @@ func peerMessageSelectedReactions(context: AccountContext, message: Message) ->
if !reaction.isSelected {
continue
}
if case .stars = reaction.value {
continue
}
reactions.insert(reaction.value)
switch reaction.value {
case .builtin, .stars:

View File

@ -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

View File

@ -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 {

View File

@ -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

View File

@ -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()

View File

@ -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))