From 624a3d0c6d09920343bb6a1e4d15a61c7b510abf Mon Sep 17 00:00:00 2001 From: Isaac <> Date: Wed, 15 May 2024 18:42:40 +0400 Subject: [PATCH 01/14] [WIP] --- .../Sources/SoftwareLottieRenderer.mm | 114 +++++++++++- .../PublicHeaders/LottieCpp/RenderTreeNode.h | 163 +++++++++--------- .../CompLayers/ShapeCompositionLayer.cpp | 9 +- 3 files changed, 194 insertions(+), 92 deletions(-) diff --git a/Tests/LottieMetalTest/SoftwareLottieRenderer/Sources/SoftwareLottieRenderer.mm b/Tests/LottieMetalTest/SoftwareLottieRenderer/Sources/SoftwareLottieRenderer.mm index a4903b4bb6..2f2189b74a 100644 --- a/Tests/LottieMetalTest/SoftwareLottieRenderer/Sources/SoftwareLottieRenderer.mm +++ b/Tests/LottieMetalTest/SoftwareLottieRenderer/Sources/SoftwareLottieRenderer.mm @@ -8,8 +8,24 @@ namespace lottie { -static void processRenderContentItem(std::shared_ptr const &contentItem, std::optional &effectiveLocalBounds, BezierPathsBoundingBoxContext &bezierPathsBoundingBoxContext) { +static void processRenderContentItem(std::shared_ptr const &contentItem, Vector2D const &globalSize, CATransform3D const &parentTransform, BezierPathsBoundingBoxContext &bezierPathsBoundingBoxContext) { + auto currentTransform = parentTransform; + + CATransform3D localTransform = contentItem->transform; + currentTransform = localTransform * currentTransform; + + if (!currentTransform.isInvertible()) { + contentItem->renderData.isValid = false; + return; + } + + std::optional effectiveLocalBounds; + + int drawContentDescendants = 0; + for (const auto &shadingVariant : contentItem->shadings) { + drawContentDescendants += 1; + CGRect shapeBounds = bezierPathsBoundingBoxParallel(bezierPathsBoundingBoxContext, shadingVariant->explicitPath.value()); if (shadingVariant->stroke) { shapeBounds = shapeBounds.insetBy(-shadingVariant->stroke->lineWidth / 2.0, -shadingVariant->stroke->lineWidth / 2.0); @@ -27,9 +43,86 @@ static void processRenderContentItem(std::shared_ptr } } - for (const auto &subItem : contentItem->subItems) { - processRenderContentItem(subItem, effectiveLocalBounds, bezierPathsBoundingBoxContext); + std::optional effectiveLocalRect; + if (effectiveLocalBounds.has_value()) { + effectiveLocalRect = effectiveLocalBounds; } + + std::optional subnodesGlobalRect; + + for (const auto &subItem : contentItem->subItems) { + processRenderContentItem(subItem, globalSize, currentTransform, bezierPathsBoundingBoxContext); + + drawContentDescendants += subItem->renderData.drawContentDescendants; + + if (!subItem->renderData.localRect.empty()) { + if (effectiveLocalRect.has_value()) { + effectiveLocalRect = effectiveLocalRect->unionWith(subItem->renderData.localRect); + } else { + effectiveLocalRect = subItem->renderData.localRect; + } + } + + if (subnodesGlobalRect) { + subnodesGlobalRect = subnodesGlobalRect->unionWith(subItem->renderData.globalRect); + } else { + subnodesGlobalRect = subItem->renderData.globalRect; + } + } + + std::optional fuzzyGlobalRect; + + if (effectiveLocalBounds) { + CGRect effectiveGlobalBounds = effectiveLocalBounds->applyingTransform(currentTransform); + if (fuzzyGlobalRect) { + fuzzyGlobalRect = fuzzyGlobalRect->unionWith(effectiveGlobalBounds); + } else { + fuzzyGlobalRect = effectiveGlobalBounds; + } + } + + if (subnodesGlobalRect) { + if (fuzzyGlobalRect) { + fuzzyGlobalRect = fuzzyGlobalRect->unionWith(subnodesGlobalRect.value()); + } else { + fuzzyGlobalRect = subnodesGlobalRect; + } + } + + if (!fuzzyGlobalRect) { + contentItem->renderData.isValid = false; + return; + } + + CGRect globalRect( + std::floor(fuzzyGlobalRect->x), + std::floor(fuzzyGlobalRect->y), + std::ceil(fuzzyGlobalRect->width + fuzzyGlobalRect->x - floor(fuzzyGlobalRect->x)), + std::ceil(fuzzyGlobalRect->height + fuzzyGlobalRect->y - floor(fuzzyGlobalRect->y)) + ); + + if (!CGRect(0.0, 0.0, globalSize.x, globalSize.y).intersects(globalRect)) { + contentItem->renderData.isValid = false; + return; + } + + CGRect localRect = effectiveLocalRect.value_or(CGRect(0.0, 0.0, 0.0, 0.0)).applyingTransform(localTransform); + + contentItem->renderData.isValid = true; + + contentItem->renderData.layer._bounds = CGRect(0.0, 0.0, 0.0, 0.0); + contentItem->renderData.layer._position = Vector2D(0.0, 0.0); + contentItem->renderData.layer._transform = contentItem->transform; + contentItem->renderData.layer._opacity = contentItem->alpha; + contentItem->renderData.layer._masksToBounds = false; + contentItem->renderData.layer._isHidden = false; + + contentItem->renderData.globalRect = globalRect; + contentItem->renderData.localRect = localRect; + contentItem->renderData.globalTransform = currentTransform; + contentItem->renderData.drawsContent = effectiveLocalBounds.has_value(); + contentItem->renderData.drawContentDescendants = drawContentDescendants; + contentItem->renderData.isInvertedMatte = false; } static void processRenderTree(std::shared_ptr const &node, Vector2D const &globalSize, CATransform3D const &parentTransform, bool isInvertedMask, BezierPathsBoundingBoxContext &bezierPathsBoundingBoxContext) { @@ -58,14 +151,12 @@ static void processRenderTree(std::shared_ptr const &node, Vecto return; } - std::optional effectiveLocalBounds; - - double alpha = node->alpha(); - if (node->_contentItem) { - processRenderContentItem(node->_contentItem, effectiveLocalBounds, bezierPathsBoundingBoxContext); + processRenderContentItem(node->_contentItem, globalSize, currentTransform, bezierPathsBoundingBoxContext); } + std::optional effectiveLocalBounds; + bool isInvertedMatte = isInvertedMask; if (isInvertedMatte) { effectiveLocalBounds = node->bounds(); @@ -179,7 +270,7 @@ static void processRenderTree(std::shared_ptr const &node, Vecto node->renderData.layer._bounds = node->bounds(); node->renderData.layer._position = node->position(); node->renderData.layer._transform = node->transform(); - node->renderData.layer._opacity = alpha; + node->renderData.layer._opacity = node->alpha(); node->renderData.layer._masksToBounds = masksToBounds; node->renderData.layer._isHidden = node->isHidden(); @@ -196,6 +287,9 @@ static void processRenderTree(std::shared_ptr const &node, Vecto namespace { static void drawLottieContentItem(std::shared_ptr context, std::shared_ptr item) { + context->saveState(); + context->concatenate(item->transform); + for (const auto &shading : item->shadings) { if (shading->explicitPath->empty()) { continue; @@ -366,6 +460,8 @@ static void drawLottieContentItem(std::shared_ptr conte for (const auto &subItem : item->subItems) { drawLottieContentItem(context, subItem); } + + context->restoreState(); } static void renderLottieRenderNode(std::shared_ptr node, std::shared_ptr parentContext, lottie::Vector2D const &globalSize, double parentAlpha) { diff --git a/submodules/TelegramUI/Components/LottieCpp/PublicHeaders/LottieCpp/RenderTreeNode.h b/submodules/TelegramUI/Components/LottieCpp/PublicHeaders/LottieCpp/RenderTreeNode.h index ee22047000..75e4f9d3fa 100644 --- a/submodules/TelegramUI/Components/LottieCpp/PublicHeaders/LottieCpp/RenderTreeNode.h +++ b/submodules/TelegramUI/Components/LottieCpp/PublicHeaders/LottieCpp/RenderTreeNode.h @@ -13,6 +13,86 @@ namespace lottie { +class ProcessedRenderTreeNodeData { +public: + struct LayerParams { + CGRect _bounds; + Vector2D _position; + CATransform3D _transform; + double _opacity; + bool _masksToBounds; + bool _isHidden; + + LayerParams( + CGRect bounds_, + Vector2D position_, + CATransform3D transform_, + double opacity_, + bool masksToBounds_, + bool isHidden_ + ) : + _bounds(bounds_), + _position(position_), + _transform(transform_), + _opacity(opacity_), + _masksToBounds(masksToBounds_), + _isHidden(isHidden_) { + } + + CGRect bounds() const { + return _bounds; + } + + Vector2D position() const { + return _position; + } + + CATransform3D transform() const { + return _transform; + } + + double opacity() const { + return _opacity; + } + + bool masksToBounds() const { + return _masksToBounds; + } + + bool isHidden() const { + return _isHidden; + } + }; + + ProcessedRenderTreeNodeData() : + isValid(false), + layer( + CGRect(0.0, 0.0, 0.0, 0.0), + Vector2D(0.0, 0.0), + CATransform3D::identity(), + 1.0, + false, + false + ), + globalRect(CGRect(0.0, 0.0, 0.0, 0.0)), + localRect(CGRect(0.0, 0.0, 0.0, 0.0)), + globalTransform(CATransform3D::identity()), + drawsContent(false), + drawContentDescendants(false), + isInvertedMatte(false) { + + } + + bool isValid = false; + LayerParams layer; + CGRect globalRect; + CGRect localRect; + CATransform3D globalTransform; + bool drawsContent; + int drawContentDescendants; + bool isInvertedMatte; +}; + class RenderableItem { public: enum class Type { @@ -345,8 +425,11 @@ public: public: bool isGroup = false; CATransform3D transform = CATransform3D::identity(); + double alpha = 0.0; std::vector> shadings; std::vector> subItems; + + ProcessedRenderTreeNodeData renderData; }; class RenderTreeNodeContentShadingVariant { @@ -362,86 +445,6 @@ public: size_t subItemLimit = 0; }; -class ProcessedRenderTreeNodeData { -public: - struct LayerParams { - CGRect _bounds; - Vector2D _position; - CATransform3D _transform; - double _opacity; - bool _masksToBounds; - bool _isHidden; - - LayerParams( - CGRect bounds_, - Vector2D position_, - CATransform3D transform_, - double opacity_, - bool masksToBounds_, - bool isHidden_ - ) : - _bounds(bounds_), - _position(position_), - _transform(transform_), - _opacity(opacity_), - _masksToBounds(masksToBounds_), - _isHidden(isHidden_) { - } - - CGRect bounds() const { - return _bounds; - } - - Vector2D position() const { - return _position; - } - - CATransform3D transform() const { - return _transform; - } - - double opacity() const { - return _opacity; - } - - bool masksToBounds() const { - return _masksToBounds; - } - - bool isHidden() const { - return _isHidden; - } - }; - - ProcessedRenderTreeNodeData() : - isValid(false), - layer( - CGRect(0.0, 0.0, 0.0, 0.0), - Vector2D(0.0, 0.0), - CATransform3D::identity(), - 1.0, - false, - false - ), - globalRect(CGRect(0.0, 0.0, 0.0, 0.0)), - localRect(CGRect(0.0, 0.0, 0.0, 0.0)), - globalTransform(CATransform3D::identity()), - drawsContent(false), - drawContentDescendants(false), - isInvertedMatte(false) { - - } - - bool isValid = false; - LayerParams layer; - CGRect globalRect; - CGRect localRect; - CATransform3D globalTransform; - bool drawsContent; - int drawContentDescendants; - bool isInvertedMatte; -}; - class RenderTreeNode { public: RenderTreeNode( diff --git a/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/MainThread/LayerContainers/CompLayers/ShapeCompositionLayer.cpp b/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/MainThread/LayerContainers/CompLayers/ShapeCompositionLayer.cpp index 1ffaff84f8..617fc87ed3 100644 --- a/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/MainThread/LayerContainers/CompLayers/ShapeCompositionLayer.cpp +++ b/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/MainThread/LayerContainers/CompLayers/ShapeCompositionLayer.cpp @@ -1034,10 +1034,10 @@ public: for (int i = (int)subItems.size() - 1; i >= 0; i--) { subItems[i]->initializeRenderChildren(); subItemNodes.push_back(subItems[i]->_renderTree); - //_renderTree->_contentItem->subItems.push_back(subItems[i]->_renderTree->_contentItem); + _renderTree->_contentItem->subItems.push_back(subItems[i]->_renderTree->_contentItem); } - if (!subItemNodes.empty()) { + /*if (!subItemNodes.empty()) { _renderTree->_subnodes.push_back(std::make_shared( CGRect(0.0, 0.0, 0.0, 0.0), Vector2D(0.0, 0.0), @@ -1049,7 +1049,7 @@ public: nullptr, false )); - } + }*/ } } @@ -1087,6 +1087,9 @@ public: containerTransform = transform->transform(); containerOpacity = transform->opacity(); } + _renderTree->_contentItem->transform = containerTransform; + _renderTree->_contentItem->alpha = containerOpacity; + _renderTree->_transform = containerTransform; _renderTree->_alpha = containerOpacity; From 4ac12021d56c12ce4bdb1b27e414832923dae607 Mon Sep 17 00:00:00 2001 From: Isaac <> Date: Wed, 15 May 2024 21:09:50 +0400 Subject: [PATCH 02/14] Request verification --- .../Sources/NotificationService.swift | 2 +- Telegram/SiriIntents/IntentHandler.swift | 2 +- .../PublicHeaders/MtProtoKit/MTContext.h | 2 + .../MtProtoKit/MTRequestContext.h | 2 - .../MtProtoKit/MTRequestErrorContext.h | 17 +++- submodules/MtProtoKit/Sources/MTContext.m | 20 +++++ .../Sources/MTRequestErrorContext.m | 12 +++ .../Sources/MTRequestMessageService.m | 84 +++++++++++++++++-- .../Sources/Network/Network.swift | 23 ++++- .../Sources/ShareExtensionContext.swift | 1 + .../TelegramUI/Sources/AppDelegate.swift | 16 +++- .../Sources/NotificationContentContext.swift | 2 +- 12 files changed, 169 insertions(+), 14 deletions(-) diff --git a/Telegram/NotificationService/Sources/NotificationService.swift b/Telegram/NotificationService/Sources/NotificationService.swift index b850243aa3..95498930c8 100644 --- a/Telegram/NotificationService/Sources/NotificationService.swift +++ b/Telegram/NotificationService/Sources/NotificationService.swift @@ -742,7 +742,7 @@ private final class NotificationServiceHandler { Logger.shared.logToConsole = loggingSettings.logToConsole Logger.shared.redactSensitiveData = loggingSettings.redactSensitiveData - let networkArguments = NetworkInitializationArguments(apiId: apiId, apiHash: apiHash, languagesCategory: languagesCategory, appVersion: appVersion, voipMaxLayer: 0, voipVersions: [], appData: .single(buildConfig.bundleData(withAppToken: nil, signatureDict: nil)), autolockDeadine: .single(nil), encryptionProvider: OpenSSLEncryptionProvider(), deviceModelName: nil, useBetaFeatures: !buildConfig.isAppStoreBuild, isICloudEnabled: false) + let networkArguments = NetworkInitializationArguments(apiId: apiId, apiHash: apiHash, languagesCategory: languagesCategory, appVersion: appVersion, voipMaxLayer: 0, voipVersions: [], appData: .single(buildConfig.bundleData(withAppToken: nil, signatureDict: nil)), externalRequestVerificationStream: .never(), autolockDeadine: .single(nil), encryptionProvider: OpenSSLEncryptionProvider(), deviceModelName: nil, useBetaFeatures: !buildConfig.isAppStoreBuild, isICloudEnabled: false) let isLockedMessage: String? if let data = try? Data(contentsOf: URL(fileURLWithPath: appLockStatePath(rootPath: rootPath))), let state = try? JSONDecoder().decode(LockState.self, from: data), isAppLocked(state: state) { diff --git a/Telegram/SiriIntents/IntentHandler.swift b/Telegram/SiriIntents/IntentHandler.swift index 09fab9493b..c3d5936b1e 100644 --- a/Telegram/SiriIntents/IntentHandler.swift +++ b/Telegram/SiriIntents/IntentHandler.swift @@ -174,7 +174,7 @@ class DefaultIntentHandler: INExtension, INSendMessageIntentHandling, INSearchFo if let accountCache = accountCache { account = .single(accountCache) } else { - account = currentAccount(allocateIfNotExists: false, networkArguments: NetworkInitializationArguments(apiId: apiId, apiHash: apiHash, languagesCategory: languagesCategory, appVersion: appVersion, voipMaxLayer: 0, voipVersions: [], appData: .single(buildConfig.bundleData(withAppToken: nil, signatureDict: nil)), autolockDeadine: .single(nil), encryptionProvider: OpenSSLEncryptionProvider(), deviceModelName: nil, useBetaFeatures: !buildConfig.isAppStoreBuild, isICloudEnabled: false), supplementary: true, manager: accountManager, rootPath: rootPath, auxiliaryMethods: accountAuxiliaryMethods, encryptionParameters: encryptionParameters) + account = currentAccount(allocateIfNotExists: false, networkArguments: NetworkInitializationArguments(apiId: apiId, apiHash: apiHash, languagesCategory: languagesCategory, appVersion: appVersion, voipMaxLayer: 0, voipVersions: [], appData: .single(buildConfig.bundleData(withAppToken: nil, signatureDict: nil)), externalRequestVerificationStream: .never(), autolockDeadine: .single(nil), encryptionProvider: OpenSSLEncryptionProvider(), deviceModelName: nil, useBetaFeatures: !buildConfig.isAppStoreBuild, isICloudEnabled: false), supplementary: true, manager: accountManager, rootPath: rootPath, auxiliaryMethods: accountAuxiliaryMethods, encryptionParameters: encryptionParameters) |> mapToSignal { account -> Signal in if let account = account { switch account { diff --git a/submodules/MtProtoKit/PublicHeaders/MtProtoKit/MTContext.h b/submodules/MtProtoKit/PublicHeaders/MtProtoKit/MTContext.h index fa26508ecb..c9b041c472 100644 --- a/submodules/MtProtoKit/PublicHeaders/MtProtoKit/MTContext.h +++ b/submodules/MtProtoKit/PublicHeaders/MtProtoKit/MTContext.h @@ -99,6 +99,8 @@ - (void)removeChangeListener:(id _Nonnull)changeListener; - (void)setDiscoverBackupAddressListSignal:(MTSignal * _Nonnull)signal; +- (void)setExternalRequestVerification:(MTSignal * _Nonnull (^ _Nonnull)(NSString * _Nonnull))externalRequestVerification; +- (MTSignal * _Nullable)performExternalRequestVerificationWithNonce:(NSString * _Nonnull)nonce; - (NSTimeInterval)globalTime; - (NSTimeInterval)globalTimeDifference; diff --git a/submodules/MtProtoKit/PublicHeaders/MtProtoKit/MTRequestContext.h b/submodules/MtProtoKit/PublicHeaders/MtProtoKit/MTRequestContext.h index 9c63163811..0ad171fe57 100644 --- a/submodules/MtProtoKit/PublicHeaders/MtProtoKit/MTRequestContext.h +++ b/submodules/MtProtoKit/PublicHeaders/MtProtoKit/MTRequestContext.h @@ -1,5 +1,3 @@ - - #import @interface MTRequestContext : NSObject diff --git a/submodules/MtProtoKit/PublicHeaders/MtProtoKit/MTRequestErrorContext.h b/submodules/MtProtoKit/PublicHeaders/MtProtoKit/MTRequestErrorContext.h index c7105cb69c..5f2db89e02 100644 --- a/submodules/MtProtoKit/PublicHeaders/MtProtoKit/MTRequestErrorContext.h +++ b/submodules/MtProtoKit/PublicHeaders/MtProtoKit/MTRequestErrorContext.h @@ -1,7 +1,18 @@ - - #import +@protocol MTDisposable; + +@interface MTRequestPendingVerificationData : NSObject + +@property (nonatomic, strong, readonly) NSString *nonce; +@property (nonatomic, strong) NSString *secret; +@property (nonatomic) bool isResolved; +@property (nonatomic, strong) id disposable; + +- (instancetype)initWithNonce:(NSString *)nonce; + +@end + @interface MTRequestErrorContext : NSObject @property (nonatomic) CFAbsoluteTime minimalExecuteTime; @@ -13,4 +24,6 @@ @property (nonatomic) bool waitingForTokenExport; @property (nonatomic, strong) id waitingForRequestToComplete; +@property (nonatomic, strong) MTRequestPendingVerificationData *pendingVerificationData; + @end diff --git a/submodules/MtProtoKit/Sources/MTContext.m b/submodules/MtProtoKit/Sources/MTContext.m index fddd5a6f84..7e506f5edc 100644 --- a/submodules/MtProtoKit/Sources/MTContext.m +++ b/submodules/MtProtoKit/Sources/MTContext.m @@ -181,6 +181,7 @@ static MTDatacenterAuthInfoMapKeyStruct parseAuthInfoMapKeyInteger(NSNumber *key NSMutableArray *_changeListeners; MTSignal *_discoverBackupAddressListSignal; + MTSignal * _Nonnull (^ _Nullable _externalRequestVerification)(NSString * _Nonnull); NSMutableDictionary *_discoverDatacenterAddressActions; NSMutableDictionary *_datacenterAuthActions; @@ -526,6 +527,25 @@ static void copyKeychainDictionaryKey(NSString * _Nonnull group, NSString * _Non } synchronous:true]; } +- (void)setExternalRequestVerification:(MTSignal * _Nonnull (^ _Nonnull)(NSString * _Nonnull))externalRequestVerification { + [[MTContext contextQueue] dispatchOnQueue:^ { + _externalRequestVerification = externalRequestVerification; + } synchronous:true]; +} + +- (MTSignal * _Nullable)performExternalRequestVerificationWithNonce:(NSString * _Nonnull)nonce { + __block MTSignal * _Nonnull (^ _Nullable externalRequestVerification)(NSString * _Nonnull); + [[MTContext contextQueue] dispatchOnQueue:^ { + externalRequestVerification = _externalRequestVerification; + } synchronous:true]; + + if (externalRequestVerification != nil) { + return externalRequestVerification(nonce); + } else { + return [MTSignal single:nil]; + } +} + - (NSTimeInterval)globalTime { return [[NSDate date] timeIntervalSince1970] + [self globalTimeDifference]; diff --git a/submodules/MtProtoKit/Sources/MTRequestErrorContext.m b/submodules/MtProtoKit/Sources/MTRequestErrorContext.m index 3196930668..85db0d3a36 100644 --- a/submodules/MtProtoKit/Sources/MTRequestErrorContext.m +++ b/submodules/MtProtoKit/Sources/MTRequestErrorContext.m @@ -1,5 +1,17 @@ #import +@implementation MTRequestPendingVerificationData + +- (instancetype)initWithNonce:(NSString *)nonce { + self = [super init]; + if (self != nil) { + _nonce = nonce; + } + return self; +} + +@end + @implementation MTRequestErrorContext @end diff --git a/submodules/MtProtoKit/Sources/MTRequestMessageService.m b/submodules/MtProtoKit/Sources/MTRequestMessageService.m index a81adedb34..202a653f24 100644 --- a/submodules/MtProtoKit/Sources/MTRequestMessageService.m +++ b/submodules/MtProtoKit/Sources/MTRequestMessageService.m @@ -17,6 +17,7 @@ #import #import #import +#import #import "MTBuffer.h" #import "MTInternalMessageParser.h" @@ -24,6 +25,26 @@ #import #import "MTDropRpcResultMessage.h" +@interface MTRequestVerificationData : NSObject + +@property (nonatomic, strong, readonly) NSString *nonce; +@property (nonatomic, strong, readonly) NSString *secret; + +@end + +@implementation MTRequestVerificationData + +- (instancetype)initWithNonce:(NSString *)nonce secret:(NSString *)secret { + self = [super init]; + if (self != nil) { + _nonce = nonce; + _secret = secret; + } + return self; +} + +@end + @interface MTRequestMessageService () { MTContext *_context; @@ -381,8 +402,8 @@ } } -- (NSData *)decorateRequestData:(MTRequest *)request initializeApi:(bool)initializeApi unresolvedDependencyOnRequestInternalId:(__autoreleasing id *)unresolvedDependencyOnRequestInternalId decoratedDebugDescription:(__autoreleasing NSString **)decoratedDebugDescription -{ +- (NSData *)decorateRequestData:(MTRequest *)request initializeApi:(bool)initializeApi requestVerificationData:(MTRequestVerificationData *)requestVerificationData unresolvedDependencyOnRequestInternalId:(__autoreleasing id *)unresolvedDependencyOnRequestInternalId decoratedDebugDescription:(__autoreleasing NSString **)decoratedDebugDescription +{ NSData *currentData = request.payload; NSString *debugDescription = @""; @@ -397,8 +418,6 @@ // invokeWithLayer [buffer appendInt32:(int32_t)0xda9b0d0d]; [buffer appendInt32:(int32_t)[_serialization currentLayer]]; - - //initConnection#c1cd5ea9 {X:Type} flags:# api_id:int device_model:string system_version:string app_version:string system_lang_code:string lang_pack:string lang_code:string proxy:flags.0?InputClientProxy query:!X = X; int32_t flags = 0; if (_apiEnvironment.socksProxySettings.secret != nil) { @@ -482,6 +501,19 @@ } } + if (requestVerificationData != nil) { + MTBuffer *buffer = [[MTBuffer alloc] init]; + + [buffer appendInt32:(int32_t)0xdae54f8]; + [buffer appendTLString:requestVerificationData.nonce]; + [buffer appendTLString:requestVerificationData.secret]; + + [buffer appendBytes:currentData.bytes length:currentData.length]; + currentData = buffer.data; + + debugDescription = [debugDescription stringByAppendingFormat:@", apnsSecret(%@, %@)", requestVerificationData.nonce, requestVerificationData.secret]; + } + if (decoratedDebugDescription != nil) { *decoratedDebugDescription = debugDescription; } @@ -511,6 +543,11 @@ if (request.errorContext.waitingForTokenExport) { continue; } + if (request.errorContext.pendingVerificationData != nil) { + if (!request.errorContext.pendingVerificationData.isResolved) { + continue; + } + } bool foundDependency = false; for (MTRequest *anotherRequest in _requests) { @@ -542,7 +579,16 @@ messageSeqNo = request.requestContext.messageSeqNo; } - NSData *decoratedRequestData = [self decorateRequestData:request initializeApi:requestsWillInitializeApi unresolvedDependencyOnRequestInternalId:&autoreleasingUnresolvedDependencyOnRequestInternalId decoratedDebugDescription:&decoratedDebugDescription]; + MTRequestVerificationData *requestVerificationData = nil; + if (request.errorContext != nil) { + if (request.errorContext.pendingVerificationData != nil) { + if (request.errorContext.pendingVerificationData.isResolved) { + requestVerificationData = [[MTRequestVerificationData alloc] initWithNonce:request.errorContext.pendingVerificationData.nonce secret:request.errorContext.pendingVerificationData.secret]; + } + } + } + + NSData *decoratedRequestData = [self decorateRequestData:request initializeApi:requestsWillInitializeApi requestVerificationData:requestVerificationData unresolvedDependencyOnRequestInternalId:&autoreleasingUnresolvedDependencyOnRequestInternalId decoratedDebugDescription:&decoratedDebugDescription]; MTOutgoingMessage *outgoingMessage = [[MTOutgoingMessage alloc] initWithData:decoratedRequestData metadata:request.metadata additionalDebugDescription:decoratedDebugDescription shortMetadata:request.shortMetadata messageId:messageId messageSeqNo:messageSeqNo]; outgoingMessage.needsQuickAck = request.acknowledgementReceived != nil; @@ -875,6 +921,34 @@ [_context updateAuthInfoForDatacenterWithId:mtProto.datacenterId authInfo:authInfo selector:authInfoSelector]; }]; + restartRequest = true; + } else if (rpcError.errorCode == 400 && [rpcError.errorDescription rangeOfString:@"APNS_VERIFY_CHECK_"].location != NSNotFound) { + if (request.errorContext == nil) { + request.errorContext = [[MTRequestErrorContext alloc] init]; + } + + NSString *nonce = [rpcError.errorDescription substringFromIndex:[@"APNS_VERIFY_CHECK_" length]]; + request.errorContext.pendingVerificationData = [[MTRequestPendingVerificationData alloc] initWithNonce:nonce]; + + __weak MTRequestMessageService *weakSelf = self; + MTQueue *queue = _queue; + id requestId = request.internalId; + request.errorContext.pendingVerificationData.disposable = [[_context performExternalRequestVerificationWithNonce:nonce] startWithNext:^(id result) { + [queue dispatchOnQueue:^{ + __strong MTRequestMessageService *strongSelf = weakSelf; + if (!strongSelf) { + return; + } + for (MTRequest *request in strongSelf->_requests) { + if (request.internalId == requestId) { + request.errorContext.pendingVerificationData.secret = result; + request.errorContext.pendingVerificationData.isResolved = true; + } + } + [strongSelf->_mtProto requestTransportTransaction]; + }]; + }]; + restartRequest = true; } else if (rpcError.errorCode == 406) { if (_didReceiveSoftAuthResetError) { diff --git a/submodules/TelegramCore/Sources/Network/Network.swift b/submodules/TelegramCore/Sources/Network/Network.swift index 99dcd097fb..c4f8561ae1 100644 --- a/submodules/TelegramCore/Sources/Network/Network.swift +++ b/submodules/TelegramCore/Sources/Network/Network.swift @@ -434,13 +434,14 @@ public struct NetworkInitializationArguments { public let voipMaxLayer: Int32 public let voipVersions: [CallSessionManagerImplementationVersion] public let appData: Signal + public let externalRequestVerificationStream: Signal<[String: String], NoError> public let autolockDeadine: Signal public let encryptionProvider: EncryptionProvider public let deviceModelName:String? public let useBetaFeatures: Bool public let isICloudEnabled: Bool - public init(apiId: Int32, apiHash: String, languagesCategory: String, appVersion: String, voipMaxLayer: Int32, voipVersions: [CallSessionManagerImplementationVersion], appData: Signal, autolockDeadine: Signal, encryptionProvider: EncryptionProvider, deviceModelName: String?, useBetaFeatures: Bool, isICloudEnabled: Bool) { + public init(apiId: Int32, apiHash: String, languagesCategory: String, appVersion: String, voipMaxLayer: Int32, voipVersions: [CallSessionManagerImplementationVersion], appData: Signal, externalRequestVerificationStream: Signal<[String: String], NoError>, autolockDeadine: Signal, encryptionProvider: EncryptionProvider, deviceModelName: String?, useBetaFeatures: Bool, isICloudEnabled: Bool) { self.apiId = apiId self.apiHash = apiHash self.languagesCategory = languagesCategory @@ -448,6 +449,7 @@ public struct NetworkInitializationArguments { self.voipMaxLayer = voipMaxLayer self.voipVersions = voipVersions self.appData = appData + self.externalRequestVerificationStream = externalRequestVerificationStream self.autolockDeadine = autolockDeadine self.encryptionProvider = encryptionProvider self.deviceModelName = deviceModelName @@ -573,6 +575,25 @@ func initializedNetwork(accountId: AccountRecordId, arguments: NetworkInitializa if !supplementary { context.setDiscoverBackupAddressListSignal(MTBackupAddressSignals.fetchBackupIps(testingEnvironment, currentContext: context, additionalSource: wrappedAdditionalSource, phoneNumber: phoneNumber, mainDatacenterId: datacenterId)) + let externalRequestVerificationStream = arguments.externalRequestVerificationStream + context.setExternalRequestVerification({ nonce in + return MTSignal(generator: { subscriber in + let disposable = (externalRequestVerificationStream + |> map { dict -> String? in + return dict[nonce] + } + |> filter { $0 != nil } + |> take(1) + |> timeout(15.0, queue: .mainQueue(), alternate: .single(nil))).start(next: { secret in + subscriber?.putNext(secret) + subscriber?.putCompletion() + }) + + return MTBlockDisposable(block: { + disposable.dispose() + }) + }) + }) } /*#if DEBUG diff --git a/submodules/TelegramUI/Components/ShareExtensionContext/Sources/ShareExtensionContext.swift b/submodules/TelegramUI/Components/ShareExtensionContext/Sources/ShareExtensionContext.swift index ef463ef0f3..f8b6cb9dc6 100644 --- a/submodules/TelegramUI/Components/ShareExtensionContext/Sources/ShareExtensionContext.swift +++ b/submodules/TelegramUI/Components/ShareExtensionContext/Sources/ShareExtensionContext.swift @@ -385,6 +385,7 @@ public class ShareRootControllerImpl { voipMaxLayer: 0, voipVersions: [], appData: .single(nil), + externalRequestVerificationStream: .never(), autolockDeadine: .single(nil), encryptionProvider: OpenSSLEncryptionProvider(), deviceModelName: nil, diff --git a/submodules/TelegramUI/Sources/AppDelegate.swift b/submodules/TelegramUI/Sources/AppDelegate.swift index 27840d2f14..e63a70fa49 100644 --- a/submodules/TelegramUI/Sources/AppDelegate.swift +++ b/submodules/TelegramUI/Sources/AppDelegate.swift @@ -274,6 +274,15 @@ private func extractAccountManagerState(records: AccountRecordsView([:]) + private var firebaseRequestVerificationSecrets: [String: String] = [:] { + didSet { + if self.firebaseRequestVerificationSecrets != oldValue { + self.firebaseRequestVerificationSecretStream.set(.single(self.firebaseRequestVerificationSecrets)) + } + } + } + private let firebaseRequestVerificationSecretStream = Promise<[String: String]>([:]) + private var urlSessions: [URLSession] = [] private func urlSession(identifier: String) -> URLSession { if let existingSession = self.urlSessions.first(where: { $0.configuration.identifier == identifier }) { @@ -491,7 +500,7 @@ private func extractAccountManagerState(records: AccountRecordsView Date: Wed, 15 May 2024 23:52:24 +0400 Subject: [PATCH 03/14] Lottie optimization --- .../Sources/SoftwareLottieRenderer.mm | 267 ++++++++---------- .../Sources/ViewController.swift | 2 +- .../PublicHeaders/LottieCpp/RenderTreeNode.h | 4 - .../Sources/LottieAnimationContainer.mm | 2 - 4 files changed, 121 insertions(+), 154 deletions(-) diff --git a/Tests/LottieMetalTest/SoftwareLottieRenderer/Sources/SoftwareLottieRenderer.mm b/Tests/LottieMetalTest/SoftwareLottieRenderer/Sources/SoftwareLottieRenderer.mm index 2f2189b74a..a0145c5d4a 100644 --- a/Tests/LottieMetalTest/SoftwareLottieRenderer/Sources/SoftwareLottieRenderer.mm +++ b/Tests/LottieMetalTest/SoftwareLottieRenderer/Sources/SoftwareLottieRenderer.mm @@ -19,94 +19,62 @@ static void processRenderContentItem(std::shared_ptr return; } - std::optional effectiveLocalBounds; + std::optional globalRect; int drawContentDescendants = 0; for (const auto &shadingVariant : contentItem->shadings) { - drawContentDescendants += 1; - CGRect shapeBounds = bezierPathsBoundingBoxParallel(bezierPathsBoundingBoxContext, shadingVariant->explicitPath.value()); if (shadingVariant->stroke) { shapeBounds = shapeBounds.insetBy(-shadingVariant->stroke->lineWidth / 2.0, -shadingVariant->stroke->lineWidth / 2.0); - if (effectiveLocalBounds) { - effectiveLocalBounds = effectiveLocalBounds->unionWith(shapeBounds); - } else { - effectiveLocalBounds = shapeBounds; - } } else if (shadingVariant->fill) { - if (effectiveLocalBounds) { - effectiveLocalBounds = effectiveLocalBounds->unionWith(shapeBounds); - } else { - effectiveLocalBounds = shapeBounds; - } + } else { + continue; + } + + drawContentDescendants += 1; + + CGRect shapeGlobalBounds = shapeBounds.applyingTransform(currentTransform); + if (globalRect) { + globalRect = globalRect->unionWith(shapeGlobalBounds); + } else { + globalRect = shapeGlobalBounds; } } - std::optional effectiveLocalRect; - if (effectiveLocalBounds.has_value()) { - effectiveLocalRect = effectiveLocalBounds; - } - - std::optional subnodesGlobalRect; - for (const auto &subItem : contentItem->subItems) { processRenderContentItem(subItem, globalSize, currentTransform, bezierPathsBoundingBoxContext); - drawContentDescendants += subItem->renderData.drawContentDescendants; - - if (!subItem->renderData.localRect.empty()) { - if (effectiveLocalRect.has_value()) { - effectiveLocalRect = effectiveLocalRect->unionWith(subItem->renderData.localRect); + if (subItem->renderData.isValid) { + drawContentDescendants += subItem->renderData.drawContentDescendants; + if (globalRect) { + globalRect = globalRect->unionWith(subItem->renderData.globalRect); } else { - effectiveLocalRect = subItem->renderData.localRect; + globalRect = subItem->renderData.globalRect; } } - - if (subnodesGlobalRect) { - subnodesGlobalRect = subnodesGlobalRect->unionWith(subItem->renderData.globalRect); - } else { - subnodesGlobalRect = subItem->renderData.globalRect; - } } - std::optional fuzzyGlobalRect; - - if (effectiveLocalBounds) { - CGRect effectiveGlobalBounds = effectiveLocalBounds->applyingTransform(currentTransform); - if (fuzzyGlobalRect) { - fuzzyGlobalRect = fuzzyGlobalRect->unionWith(effectiveGlobalBounds); - } else { - fuzzyGlobalRect = effectiveGlobalBounds; - } - } - - if (subnodesGlobalRect) { - if (fuzzyGlobalRect) { - fuzzyGlobalRect = fuzzyGlobalRect->unionWith(subnodesGlobalRect.value()); - } else { - fuzzyGlobalRect = subnodesGlobalRect; - } - } - - if (!fuzzyGlobalRect) { + if (!globalRect) { contentItem->renderData.isValid = false; return; } - CGRect globalRect( - std::floor(fuzzyGlobalRect->x), - std::floor(fuzzyGlobalRect->y), - std::ceil(fuzzyGlobalRect->width + fuzzyGlobalRect->x - floor(fuzzyGlobalRect->x)), - std::ceil(fuzzyGlobalRect->height + fuzzyGlobalRect->y - floor(fuzzyGlobalRect->y)) + CGRect integralGlobalRect( + std::floor(globalRect->x), + std::floor(globalRect->y), + std::ceil(globalRect->width + globalRect->x - floor(globalRect->x)), + std::ceil(globalRect->height + globalRect->y - floor(globalRect->y)) ); - if (!CGRect(0.0, 0.0, globalSize.x, globalSize.y).intersects(globalRect)) { + if (!CGRect(0.0, 0.0, globalSize.x, globalSize.y).intersects(integralGlobalRect)) { + contentItem->renderData.isValid = false; + return; + } + if (integralGlobalRect.width <= 0.0 || integralGlobalRect.height <= 0.0) { contentItem->renderData.isValid = false; return; } - - CGRect localRect = effectiveLocalRect.value_or(CGRect(0.0, 0.0, 0.0, 0.0)).applyingTransform(localTransform); contentItem->renderData.isValid = true; @@ -117,10 +85,8 @@ static void processRenderContentItem(std::shared_ptr contentItem->renderData.layer._masksToBounds = false; contentItem->renderData.layer._isHidden = false; - contentItem->renderData.globalRect = globalRect; - contentItem->renderData.localRect = localRect; + contentItem->renderData.globalRect = integralGlobalRect; contentItem->renderData.globalTransform = currentTransform; - contentItem->renderData.drawsContent = effectiveLocalBounds.has_value(); contentItem->renderData.drawContentDescendants = drawContentDescendants; contentItem->renderData.isInvertedMatte = false; } @@ -151,100 +117,59 @@ static void processRenderTree(std::shared_ptr const &node, Vecto return; } + int drawContentDescendants = 0; + std::optional globalRect; if (node->_contentItem) { processRenderContentItem(node->_contentItem, globalSize, currentTransform, bezierPathsBoundingBoxContext); + if (node->_contentItem->renderData.isValid) { + drawContentDescendants += node->_contentItem->renderData.drawContentDescendants; + globalRect = node->_contentItem->renderData.globalRect; + } } - std::optional effectiveLocalBounds; - bool isInvertedMatte = isInvertedMask; if (isInvertedMatte) { - effectiveLocalBounds = node->bounds(); + CGRect globalBounds = node->bounds().applyingTransform(currentTransform); + if (globalRect) { + globalRect = globalRect->unionWith(globalBounds); + } else { + globalRect = globalBounds; + } } - if (effectiveLocalBounds && effectiveLocalBounds->empty()) { - effectiveLocalBounds = std::nullopt; - } - - std::optional effectiveLocalRect; - if (effectiveLocalBounds.has_value()) { - effectiveLocalRect = effectiveLocalBounds; - } - - std::optional subnodesGlobalRect; - bool masksToBounds = node->masksToBounds(); - - int drawContentDescendants = 0; - for (const auto &item : node->subnodes()) { processRenderTree(item, globalSize, currentTransform, false, bezierPathsBoundingBoxContext); if (item->renderData.isValid) { drawContentDescendants += item->renderData.drawContentDescendants; - if (item->_contentItem) { - drawContentDescendants += 1; - } - - if (!item->renderData.localRect.empty()) { - if (effectiveLocalRect.has_value()) { - effectiveLocalRect = effectiveLocalRect->unionWith(item->renderData.localRect); - } else { - effectiveLocalRect = item->renderData.localRect; - } - } - - if (subnodesGlobalRect) { - subnodesGlobalRect = subnodesGlobalRect->unionWith(item->renderData.globalRect); + if (globalRect) { + globalRect = globalRect->unionWith(item->renderData.globalRect); } else { - subnodesGlobalRect = item->renderData.globalRect; + globalRect = item->renderData.globalRect; } } } - if (masksToBounds && effectiveLocalRect.has_value()) { - if (node->bounds().contains(effectiveLocalRect.value())) { - masksToBounds = false; - } - } - - std::optional fuzzyGlobalRect; - - if (effectiveLocalBounds) { - CGRect effectiveGlobalBounds = effectiveLocalBounds->applyingTransform(currentTransform); - if (fuzzyGlobalRect) { - fuzzyGlobalRect = fuzzyGlobalRect->unionWith(effectiveGlobalBounds); - } else { - fuzzyGlobalRect = effectiveGlobalBounds; - } - } - - if (subnodesGlobalRect) { - if (fuzzyGlobalRect) { - fuzzyGlobalRect = fuzzyGlobalRect->unionWith(subnodesGlobalRect.value()); - } else { - fuzzyGlobalRect = subnodesGlobalRect; - } - } - - if (!fuzzyGlobalRect) { + if (!globalRect) { node->renderData.isValid = false; return; } - CGRect globalRect( - std::floor(fuzzyGlobalRect->x), - std::floor(fuzzyGlobalRect->y), - std::ceil(fuzzyGlobalRect->width + fuzzyGlobalRect->x - floor(fuzzyGlobalRect->x)), - std::ceil(fuzzyGlobalRect->height + fuzzyGlobalRect->y - floor(fuzzyGlobalRect->y)) + CGRect integralGlobalRect( + std::floor(globalRect->x), + std::floor(globalRect->y), + std::ceil(globalRect->width + globalRect->x - floor(globalRect->x)), + std::ceil(globalRect->height + globalRect->y - floor(globalRect->y)) ); - if (!CGRect(0.0, 0.0, globalSize.x, globalSize.y).intersects(globalRect)) { + if (!CGRect(0.0, 0.0, globalSize.x, globalSize.y).intersects(integralGlobalRect)) { node->renderData.isValid = false; return; } - if (masksToBounds && effectiveLocalBounds) { - CGRect effectiveGlobalBounds = effectiveLocalBounds->applyingTransform(currentTransform); + bool masksToBounds = node->masksToBounds(); + if (masksToBounds) { + CGRect effectiveGlobalBounds = node->bounds().applyingTransform(currentTransform); if (effectiveGlobalBounds.contains(CGRect(0.0, 0.0, globalSize.x, globalSize.y))) { masksToBounds = false; } @@ -253,7 +178,7 @@ static void processRenderTree(std::shared_ptr const &node, Vecto if (node->mask()) { processRenderTree(node->mask(), globalSize, currentTransform, node->invertMask(), bezierPathsBoundingBoxContext); if (node->mask()->renderData.isValid) { - if (!node->mask()->renderData.globalRect.intersects(globalRect)) { + if (!node->mask()->renderData.globalRect.intersects(integralGlobalRect)) { node->renderData.isValid = false; return; } @@ -263,7 +188,10 @@ static void processRenderTree(std::shared_ptr const &node, Vecto } } - CGRect localRect = effectiveLocalRect.value_or(CGRect(0.0, 0.0, 0.0, 0.0)).applyingTransform(localTransform); + if (integralGlobalRect.width <= 0.0 || integralGlobalRect.height <= 0.0) { + node->renderData.isValid = false; + return; + } node->renderData.isValid = true; @@ -274,10 +202,8 @@ static void processRenderTree(std::shared_ptr const &node, Vecto node->renderData.layer._masksToBounds = masksToBounds; node->renderData.layer._isHidden = node->isHidden(); - node->renderData.globalRect = globalRect; - node->renderData.localRect = localRect; + node->renderData.globalRect = integralGlobalRect; node->renderData.globalTransform = currentTransform; - node->renderData.drawsContent = effectiveLocalBounds.has_value(); node->renderData.drawContentDescendants = drawContentDescendants; node->renderData.isInvertedMatte = isInvertedMatte; } @@ -286,9 +212,49 @@ static void processRenderTree(std::shared_ptr const &node, Vecto namespace { -static void drawLottieContentItem(std::shared_ptr context, std::shared_ptr item) { - context->saveState(); - context->concatenate(item->transform); +static void drawLottieContentItem(std::shared_ptr parentContext, std::shared_ptr item, double parentAlpha) { + if (!item->renderData.isValid) { + return; + } + + float normalizedOpacity = item->renderData.layer.opacity(); + double layerAlpha = ((double)normalizedOpacity) * parentAlpha; + + if (item->renderData.layer.isHidden() || normalizedOpacity == 0.0f) { + return; + } + + parentContext->saveState(); + + std::shared_ptr currentContext; + std::shared_ptr tempContext; + + bool needsTempContext = false; + needsTempContext = layerAlpha != 1.0 && item->renderData.drawContentDescendants > 1; + + if (needsTempContext) { + auto tempContextValue = parentContext->makeLayer((int)(item->renderData.globalRect.width), (int)(item->renderData.globalRect.height)); + tempContext = tempContextValue; + + currentContext = tempContextValue; + currentContext->concatenate(lottie::CATransform3D::identity().translated(lottie::Vector2D(-item->renderData.globalRect.x, -item->renderData.globalRect.y))); + + currentContext->saveState(); + currentContext->concatenate(item->renderData.globalTransform); + } else { + currentContext = parentContext; + } + + parentContext->concatenate(lottie::CATransform3D::identity().translated(lottie::Vector2D(item->renderData.layer.position().x, item->renderData.layer.position().y))); + parentContext->concatenate(lottie::CATransform3D::identity().translated(lottie::Vector2D(-item->renderData.layer.bounds().x, -item->renderData.layer.bounds().y))); + parentContext->concatenate(item->renderData.layer.transform()); + + double renderAlpha = 1.0; + if (tempContext) { + renderAlpha = 1.0; + } else { + renderAlpha = layerAlpha; + } for (const auto &shading : item->shadings) { if (shading->explicitPath->empty()) { @@ -398,7 +364,7 @@ static void drawLottieContentItem(std::shared_ptr conte dashPattern = shading->stroke->dashPattern; } - context->strokePath(path, shading->stroke->lineWidth, lineJoin, lineCap, shading->stroke->dashPhase, dashPattern, lottieRendering::Color(solidShading->color.r, solidShading->color.g, solidShading->color.b, solidShading->color.a * solidShading->opacity)); + currentContext->strokePath(path, shading->stroke->lineWidth, lineJoin, lineCap, shading->stroke->dashPhase, dashPattern, lottieRendering::Color(solidShading->color.r, solidShading->color.g, solidShading->color.b, solidShading->color.a * solidShading->opacity * renderAlpha)); } else if (shading->stroke->shading->type() == lottie::RenderTreeNodeContentItem::ShadingType::Gradient) { //TODO:gradient stroke } @@ -422,7 +388,7 @@ static void drawLottieContentItem(std::shared_ptr conte if (shading->fill->shading->type() == lottie::RenderTreeNodeContentItem::ShadingType::Solid) { lottie::RenderTreeNodeContentItem::SolidShading *solidShading = (lottie::RenderTreeNodeContentItem::SolidShading *)shading->fill->shading.get(); if (solidShading->opacity != 0.0) { - context->fillPath(path, rule, lottieRendering::Color(solidShading->color.r, solidShading->color.g, solidShading->color.b, solidShading->color.a * solidShading->opacity)); + currentContext->fillPath(path, rule, lottieRendering::Color(solidShading->color.r, solidShading->color.g, solidShading->color.b, solidShading->color.a * solidShading->opacity * renderAlpha)); } } else if (shading->fill->shading->type() == lottie::RenderTreeNodeContentItem::ShadingType::Gradient) { lottie::RenderTreeNodeContentItem::GradientShading *gradientShading = (lottie::RenderTreeNodeContentItem::GradientShading *)shading->fill->shading.get(); @@ -431,7 +397,7 @@ static void drawLottieContentItem(std::shared_ptr conte std::vector colors; std::vector locations; for (const auto &color : gradientShading->colors) { - colors.push_back(lottieRendering::Color(color.r, color.g, color.b, color.a * gradientShading->opacity)); + colors.push_back(lottieRendering::Color(color.r, color.g, color.b, color.a * gradientShading->opacity * renderAlpha)); } locations = gradientShading->locations; @@ -441,11 +407,11 @@ static void drawLottieContentItem(std::shared_ptr conte switch (gradientShading->gradientType) { case lottie::GradientType::Linear: { - context->linearGradientFillPath(path, rule, gradient, start, end); + currentContext->linearGradientFillPath(path, rule, gradient, start, end); break; } case lottie::GradientType::Radial: { - context->radialGradientFillPath(path, rule, gradient, start, 0.0, start, start.distanceTo(end)); + currentContext->radialGradientFillPath(path, rule, gradient, start, 0.0, start, start.distanceTo(end)); break; } default: { @@ -458,10 +424,19 @@ static void drawLottieContentItem(std::shared_ptr conte } for (const auto &subItem : item->subItems) { - drawLottieContentItem(context, subItem); + drawLottieContentItem(currentContext, subItem, renderAlpha); } - context->restoreState(); + if (tempContext) { + tempContext->restoreState(); + + parentContext->concatenate(item->renderData.globalTransform.inverted()); + parentContext->setAlpha(layerAlpha); + parentContext->draw(tempContext, item->renderData.globalRect); + parentContext->setAlpha(1.0); + } + + parentContext->restoreState(); } static void renderLottieRenderNode(std::shared_ptr node, std::shared_ptr parentContext, lottie::Vector2D const &globalSize, double parentAlpha) { @@ -528,10 +503,8 @@ static void renderLottieRenderNode(std::shared_ptr node, renderAlpha = layerAlpha; } - currentContext->setAlpha(renderAlpha); - if (node->_contentItem) { - drawLottieContentItem(currentContext, node->_contentItem); + drawLottieContentItem(currentContext, node->_contentItem, renderAlpha); } if (node->renderData.isInvertedMatte) { diff --git a/Tests/LottieMetalTest/Sources/ViewController.swift b/Tests/LottieMetalTest/Sources/ViewController.swift index 599c888d16..24ddbef56f 100644 --- a/Tests/LottieMetalTest/Sources/ViewController.swift +++ b/Tests/LottieMetalTest/Sources/ViewController.swift @@ -78,7 +78,7 @@ private final class ReferenceCompareTest { } var continueFromName: String? - //continueFromName = "778160933443732778.json" + continueFromName = "1391391008142393362.json" let _ = await processAnimationFolderAsync(basePath: bundlePath, path: "", stopOnFailure: true, process: { path, name, alwaysDraw in if let continueFromNameValue = continueFromName { diff --git a/submodules/TelegramUI/Components/LottieCpp/PublicHeaders/LottieCpp/RenderTreeNode.h b/submodules/TelegramUI/Components/LottieCpp/PublicHeaders/LottieCpp/RenderTreeNode.h index 75e4f9d3fa..48ce9dc944 100644 --- a/submodules/TelegramUI/Components/LottieCpp/PublicHeaders/LottieCpp/RenderTreeNode.h +++ b/submodules/TelegramUI/Components/LottieCpp/PublicHeaders/LottieCpp/RenderTreeNode.h @@ -75,9 +75,7 @@ public: false ), globalRect(CGRect(0.0, 0.0, 0.0, 0.0)), - localRect(CGRect(0.0, 0.0, 0.0, 0.0)), globalTransform(CATransform3D::identity()), - drawsContent(false), drawContentDescendants(false), isInvertedMatte(false) { @@ -86,9 +84,7 @@ public: bool isValid = false; LayerParams layer; CGRect globalRect; - CGRect localRect; CATransform3D globalTransform; - bool drawsContent; int drawContentDescendants; bool isInvertedMatte; }; diff --git a/submodules/TelegramUI/Components/LottieCpp/Sources/LottieAnimationContainer.mm b/submodules/TelegramUI/Components/LottieCpp/Sources/LottieAnimationContainer.mm index 0b874ee771..d155af122e 100644 --- a/submodules/TelegramUI/Components/LottieCpp/Sources/LottieAnimationContainer.mm +++ b/submodules/TelegramUI/Components/LottieCpp/Sources/LottieAnimationContainer.mm @@ -65,9 +65,7 @@ result.layer.isHidden = node->renderData.layer._isHidden; result.globalRect = CGRectMake(node->renderData.globalRect.x, node->renderData.globalRect.y, node->renderData.globalRect.width, node->renderData.globalRect.height); - result.localRect = CGRectMake(node->renderData.localRect.x, node->renderData.localRect.y, node->renderData.localRect.width, node->renderData.localRect.height); result.globalTransform = lottie::nativeTransform(node->renderData.globalTransform); - result.drawsContent = node->renderData.drawsContent; result.hasSimpleContents = node->renderData.drawContentDescendants <= 1; result.drawContentDescendants = node->renderData.drawContentDescendants; result.isInvertedMatte = node->renderData.isInvertedMatte; From ebb7798cffbe37aed5b24b6d67a2c537d8f26b42 Mon Sep 17 00:00:00 2001 From: Isaac <> Date: Thu, 16 May 2024 17:03:42 +0400 Subject: [PATCH 04/14] Refactoring --- .../Sources/ViewController.swift | 2 +- .../CompLayers/ShapeCompositionLayer.cpp | 79 +++++++------------ .../CompLayers/ShapeCompositionLayer.hpp | 1 + 3 files changed, 32 insertions(+), 50 deletions(-) diff --git a/Tests/LottieMetalTest/Sources/ViewController.swift b/Tests/LottieMetalTest/Sources/ViewController.swift index 24ddbef56f..0c757c03a1 100644 --- a/Tests/LottieMetalTest/Sources/ViewController.swift +++ b/Tests/LottieMetalTest/Sources/ViewController.swift @@ -78,7 +78,7 @@ private final class ReferenceCompareTest { } var continueFromName: String? - continueFromName = "1391391008142393362.json" + //continueFromName = "1391391008142393362.json" let _ = await processAnimationFolderAsync(basePath: bundlePath, path: "", stopOnFailure: true, process: { path, name, alwaysDraw in if let continueFromNameValue = continueFromName { diff --git a/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/MainThread/LayerContainers/CompLayers/ShapeCompositionLayer.cpp b/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/MainThread/LayerContainers/CompLayers/ShapeCompositionLayer.cpp index 617fc87ed3..b7eefed9b8 100644 --- a/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/MainThread/LayerContainers/CompLayers/ShapeCompositionLayer.cpp +++ b/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/MainThread/LayerContainers/CompLayers/ShapeCompositionLayer.cpp @@ -909,10 +909,6 @@ public: transform = std::move(transform_); } - std::shared_ptr const &renderTree() const { - return _renderTree; - } - private: std::unique_ptr path; std::unique_ptr transform; @@ -922,7 +918,8 @@ public: std::vector> subItems; - std::shared_ptr _renderTree; + public: + std::shared_ptr _contentItem; private: std::vector collectPaths(size_t subItemLimit, CATransform3D const &parentTransform, bool skipApplyTransform) { @@ -993,20 +990,8 @@ public: public: void initializeRenderChildren() { - _renderTree = std::make_shared( - CGRect(0.0, 0.0, 0.0, 0.0), - Vector2D(0.0, 0.0), - CATransform3D::identity(), - 1.0, - false, - false, - std::vector>(), - nullptr, - false - ); - - _renderTree->_contentItem = std::make_shared(); - _renderTree->_contentItem->isGroup = isGroup; + _contentItem = std::make_shared(); + _contentItem->isGroup = isGroup; if (!shadings.empty()) { for (int i = 0; i < shadings.size(); i++) { @@ -1025,7 +1010,7 @@ public: } itemShadingVariant->subItemLimit = shadingVariant.subItemLimit; - _renderTree->_contentItem->shadings.push_back(itemShadingVariant); + _contentItem->shadings.push_back(itemShadingVariant); } } @@ -1033,23 +1018,8 @@ public: std::vector> subItemNodes; for (int i = (int)subItems.size() - 1; i >= 0; i--) { subItems[i]->initializeRenderChildren(); - subItemNodes.push_back(subItems[i]->_renderTree); - _renderTree->_contentItem->subItems.push_back(subItems[i]->_renderTree->_contentItem); + _contentItem->subItems.push_back(subItems[i]->_contentItem); } - - /*if (!subItemNodes.empty()) { - _renderTree->_subnodes.push_back(std::make_shared( - CGRect(0.0, 0.0, 0.0, 0.0), - Vector2D(0.0, 0.0), - CATransform3D::identity(), - 1.0, - false, - false, - subItemNodes, - nullptr, - false - )); - }*/ } } @@ -1087,11 +1057,8 @@ public: containerTransform = transform->transform(); containerOpacity = transform->opacity(); } - _renderTree->_contentItem->transform = containerTransform; - _renderTree->_contentItem->alpha = containerOpacity; - - _renderTree->_transform = containerTransform; - _renderTree->_alpha = containerOpacity; + _contentItem->transform = containerTransform; + _contentItem->alpha = containerOpacity; for (int i = 0; i < shadings.size(); i++) { const auto &shadingVariant = shadings[i]; @@ -1121,7 +1088,7 @@ public: resultPaths.push_back(path); } - _renderTree->_contentItem->shadings[i]->explicitPath = resultPaths; + _contentItem->shadings[i]->explicitPath = resultPaths; } if (isGroup && !subItems.empty()) { @@ -1321,8 +1288,22 @@ std::shared_ptr ShapeCompositionLayer::renderTreeNode() { } if (!_renderTreeNode) { + _contentRenderTreeNode = std::make_shared( + CGRect(0.0, 0.0, 0.0, 0.0), + Vector2D(0.0, 0.0), + CATransform3D::identity(), + 1.0, + false, + false, + std::vector>(), + nullptr, + false + ); + _contentRenderTreeNode->_contentItem = _contentTree->itemTree->_contentItem; + std::vector> subnodes; - subnodes.push_back(_contentTree->itemTree->renderTree()); + //subnodes.push_back(_contentTree->itemTree->renderTree()); + subnodes.push_back(_contentRenderTreeNode); std::shared_ptr maskNode; bool invertMask = false; @@ -1354,12 +1335,12 @@ void ShapeCompositionLayer::updateRenderTree() { _matteLayer->updateRenderTree(); } - _contentTree->itemTree->renderTree()->_bounds = _contentsLayer->bounds(); - _contentTree->itemTree->renderTree()->_position = _contentsLayer->position(); - _contentTree->itemTree->renderTree()->_transform = _contentsLayer->transform(); - _contentTree->itemTree->renderTree()->_alpha = _contentsLayer->opacity(); - _contentTree->itemTree->renderTree()->_masksToBounds = _contentsLayer->masksToBounds(); - _contentTree->itemTree->renderTree()->_isHidden = _contentsLayer->isHidden(); + _contentRenderTreeNode->_bounds = _contentsLayer->bounds(); + _contentRenderTreeNode->_position = _contentsLayer->position(); + _contentRenderTreeNode->_transform = _contentsLayer->transform(); + _contentRenderTreeNode->_alpha = _contentsLayer->opacity(); + _contentRenderTreeNode->_masksToBounds = _contentsLayer->masksToBounds(); + _contentRenderTreeNode->_isHidden = _contentsLayer->isHidden(); assert(position() == Vector2D::Zero()); assert(transform().isIdentity()); diff --git a/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/MainThread/LayerContainers/CompLayers/ShapeCompositionLayer.hpp b/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/MainThread/LayerContainers/CompLayers/ShapeCompositionLayer.hpp index 691f926de5..f36e4a241c 100644 --- a/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/MainThread/LayerContainers/CompLayers/ShapeCompositionLayer.hpp +++ b/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/MainThread/LayerContainers/CompLayers/ShapeCompositionLayer.hpp @@ -27,6 +27,7 @@ private: bool _frameTimeInitialized = false; std::shared_ptr _renderTreeNode; + std::shared_ptr _contentRenderTreeNode; }; } From c9e42893e08b40f89f5698b980e548eb3d3b08bb Mon Sep 17 00:00:00 2001 From: Isaac <> Date: Thu, 16 May 2024 19:21:14 +0400 Subject: [PATCH 05/14] Refactor trims --- .../Sources/SoftwareLottieRenderer.mm | 92 +++++++++++++--- .../Sources/ViewController.swift | 2 +- .../PublicHeaders/LottieCpp/BezierPath.h | 5 +- .../PublicHeaders/LottieCpp/RenderTreeNode.h | 2 + .../PublicHeaders/LottieCpp/ShapeAttributes.h | 21 ++++ .../CompLayers/ShapeCompositionLayer.cpp | 100 +++++++++++------- .../Lottie/Private/Model/ShapeItems/Trim.hpp | 5 - 7 files changed, 169 insertions(+), 58 deletions(-) diff --git a/Tests/LottieMetalTest/SoftwareLottieRenderer/Sources/SoftwareLottieRenderer.mm b/Tests/LottieMetalTest/SoftwareLottieRenderer/Sources/SoftwareLottieRenderer.mm index a0145c5d4a..789c95c35a 100644 --- a/Tests/LottieMetalTest/SoftwareLottieRenderer/Sources/SoftwareLottieRenderer.mm +++ b/Tests/LottieMetalTest/SoftwareLottieRenderer/Sources/SoftwareLottieRenderer.mm @@ -6,6 +6,48 @@ #include +namespace { + +struct TransformedPath { + lottie::BezierPath path; + lottie::CATransform3D transform; + + TransformedPath(lottie::BezierPath const &path_, lottie::CATransform3D const &transform_) : + path(path_), + transform(transform_) { + } +}; + +static std::vector collectPaths(std::shared_ptr item, size_t subItemLimit, lottie::CATransform3D const &parentTransform, bool skipApplyTransform) { + std::vector mappedPaths; + + //TODO:remove skipApplyTransform + lottie::CATransform3D effectiveTransform = parentTransform; + if (!skipApplyTransform && item->isGroup) { + effectiveTransform = item->transform * effectiveTransform; + } + + size_t maxSubitem = std::min(item->subItems.size(), subItemLimit); + + if (item->path) { + mappedPaths.emplace_back(item->path.value(), effectiveTransform); + } + + for (size_t i = 0; i < maxSubitem; i++) { + auto &subItem = item->subItems[i]; + + auto subItemPaths = collectPaths(subItem, INT32_MAX, effectiveTransform, false); + + for (auto &path : subItemPaths) { + mappedPaths.emplace_back(path.path, path.transform); + } + } + + return mappedPaths; +} + +} + namespace lottie { static void processRenderContentItem(std::shared_ptr const &contentItem, Vector2D const &globalSize, CATransform3D const &parentTransform, BezierPathsBoundingBoxContext &bezierPathsBoundingBoxContext) { @@ -24,7 +66,17 @@ static void processRenderContentItem(std::shared_ptr int drawContentDescendants = 0; for (const auto &shadingVariant : contentItem->shadings) { - CGRect shapeBounds = bezierPathsBoundingBoxParallel(bezierPathsBoundingBoxContext, shadingVariant->explicitPath.value()); + std::vector itemPaths; + if (shadingVariant->explicitPath) { + itemPaths = shadingVariant->explicitPath.value(); + } else { + auto rawPaths = collectPaths(contentItem, shadingVariant->subItemLimit, lottie::CATransform3D::identity(), true); + for (const auto &rawPath : rawPaths) { + itemPaths.push_back(rawPath.path.copyUsingTransform(rawPath.transform)); + } + } + + CGRect shapeBounds = bezierPathsBoundingBoxParallel(bezierPathsBoundingBoxContext, itemPaths); if (shadingVariant->stroke) { shapeBounds = shapeBounds.insetBy(-shadingVariant->stroke->lineWidth / 2.0, -shadingVariant->stroke->lineWidth / 2.0); } else if (shadingVariant->fill) { @@ -42,17 +94,23 @@ static void processRenderContentItem(std::shared_ptr } } - for (const auto &subItem : contentItem->subItems) { - processRenderContentItem(subItem, globalSize, currentTransform, bezierPathsBoundingBoxContext); - - if (subItem->renderData.isValid) { - drawContentDescendants += subItem->renderData.drawContentDescendants; - if (globalRect) { - globalRect = globalRect->unionWith(subItem->renderData.globalRect); - } else { - globalRect = subItem->renderData.globalRect; + if (contentItem->isGroup) { + for (const auto &subItem : contentItem->subItems) { + processRenderContentItem(subItem, globalSize, currentTransform, bezierPathsBoundingBoxContext); + + if (subItem->renderData.isValid) { + drawContentDescendants += subItem->renderData.drawContentDescendants; + if (globalRect) { + globalRect = globalRect->unionWith(subItem->renderData.globalRect); + } else { + globalRect = subItem->renderData.globalRect; + } } } + } else { + for (const auto &subItem : contentItem->subItems) { + subItem->renderData.isValid = false; + } } if (!globalRect) { @@ -257,7 +315,17 @@ static void drawLottieContentItem(std::shared_ptr paren } for (const auto &shading : item->shadings) { - if (shading->explicitPath->empty()) { + std::vector itemPaths; + if (shading->explicitPath) { + itemPaths = shading->explicitPath.value(); + } else { + auto rawPaths = collectPaths(item, shading->subItemLimit, lottie::CATransform3D::identity(), true); + for (const auto &rawPath : rawPaths) { + itemPaths.push_back(rawPath.path.copyUsingTransform(rawPath.transform)); + } + } + + if (itemPaths.empty()) { continue; } @@ -288,7 +356,7 @@ static void drawLottieContentItem(std::shared_ptr paren }; LottiePathItem pathItem; - for (const auto &path : shading->explicitPath.value()) { + for (const auto &path : itemPaths) { std::optional previousElement; for (const auto &element : path.elements()) { if (previousElement.has_value()) { diff --git a/Tests/LottieMetalTest/Sources/ViewController.swift b/Tests/LottieMetalTest/Sources/ViewController.swift index 0c757c03a1..599c888d16 100644 --- a/Tests/LottieMetalTest/Sources/ViewController.swift +++ b/Tests/LottieMetalTest/Sources/ViewController.swift @@ -78,7 +78,7 @@ private final class ReferenceCompareTest { } var continueFromName: String? - //continueFromName = "1391391008142393362.json" + //continueFromName = "778160933443732778.json" let _ = await processAnimationFolderAsync(basePath: bundlePath, path: "", stopOnFailure: true, process: { path, name, alwaysDraw in if let continueFromNameValue = continueFromName { diff --git a/submodules/TelegramUI/Components/LottieCpp/PublicHeaders/LottieCpp/BezierPath.h b/submodules/TelegramUI/Components/LottieCpp/PublicHeaders/LottieCpp/BezierPath.h index f3031fa6d3..43b7018442 100644 --- a/submodules/TelegramUI/Components/LottieCpp/PublicHeaders/LottieCpp/BezierPath.h +++ b/submodules/TelegramUI/Components/LottieCpp/PublicHeaders/LottieCpp/BezierPath.h @@ -6,6 +6,7 @@ #include #include #include +#include #include @@ -126,7 +127,7 @@ public: public: BezierPath(std::shared_ptr contents); -private: +public: std::shared_ptr _contents; }; @@ -144,6 +145,8 @@ public: CGRect bezierPathsBoundingBox(std::vector const &paths); CGRect bezierPathsBoundingBoxParallel(BezierPathsBoundingBoxContext &context, std::vector const &paths); +std::vector trimBezierPaths(std::vector &sourcePaths, double start, double end, double offset, TrimType type); + } #endif diff --git a/submodules/TelegramUI/Components/LottieCpp/PublicHeaders/LottieCpp/RenderTreeNode.h b/submodules/TelegramUI/Components/LottieCpp/PublicHeaders/LottieCpp/RenderTreeNode.h index 48ce9dc944..3565fd8212 100644 --- a/submodules/TelegramUI/Components/LottieCpp/PublicHeaders/LottieCpp/RenderTreeNode.h +++ b/submodules/TelegramUI/Components/LottieCpp/PublicHeaders/LottieCpp/RenderTreeNode.h @@ -422,6 +422,8 @@ public: bool isGroup = false; CATransform3D transform = CATransform3D::identity(); double alpha = 0.0; + std::optional trimParams; + std::optional path; std::vector> shadings; std::vector> subItems; diff --git a/submodules/TelegramUI/Components/LottieCpp/PublicHeaders/LottieCpp/ShapeAttributes.h b/submodules/TelegramUI/Components/LottieCpp/PublicHeaders/LottieCpp/ShapeAttributes.h index 287f7fe491..2b707061a5 100644 --- a/submodules/TelegramUI/Components/LottieCpp/PublicHeaders/LottieCpp/ShapeAttributes.h +++ b/submodules/TelegramUI/Components/LottieCpp/PublicHeaders/LottieCpp/ShapeAttributes.h @@ -31,6 +31,27 @@ enum class GradientType: int { Radial = 2 }; +enum class TrimType: int { + Simultaneously = 1, + Individually = 2 +}; + +struct TrimParams { + double start = 0.0; + double end = 0.0; + double offset = 0.0; + TrimType type = TrimType::Simultaneously; + size_t subItemLimit = 0; + + TrimParams(double start_, double end_, double offset_, TrimType type_, size_t subItemLimit_) : + start(start_), + end(end_), + offset(offset_), + type(type_), + subItemLimit(subItemLimit_) { + } +}; + } #endif diff --git a/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/MainThread/LayerContainers/CompLayers/ShapeCompositionLayer.cpp b/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/MainThread/LayerContainers/CompLayers/ShapeCompositionLayer.cpp index b7eefed9b8..e269f7f74c 100644 --- a/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/MainThread/LayerContainers/CompLayers/ShapeCompositionLayer.cpp +++ b/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/MainThread/LayerContainers/CompLayers/ShapeCompositionLayer.cpp @@ -437,22 +437,6 @@ public: std::shared_ptr _stroke; }; - struct TrimParams { - double start = 0.0; - double end = 0.0; - double offset = 0.0; - TrimType type = TrimType::Simultaneously; - size_t subItemLimit = 0; - - TrimParams(double start_, double end_, double offset_, TrimType type_, size_t subItemLimit_) : - start(start_), - end(end_), - offset(offset_), - type(type_), - subItemLimit(subItemLimit_) { - } - }; - class TrimParamsOutput { public: TrimParamsOutput(Trim const &trim, size_t subItemLimit) : @@ -597,7 +581,7 @@ public: } if (hasUpdates) { - resolvedPath = makeRectangleBezierPath(Vector2D(positionValue.x, positionValue.y), Vector2D(sizeValue.x, sizeValue.y), cornerRadiusValue, direction); + ValueInterpolator::setInplace(makeRectangleBezierPath(Vector2D(positionValue.x, positionValue.y), Vector2D(sizeValue.x, sizeValue.y), cornerRadiusValue, direction), resolvedPath); } hasValidData = true; @@ -645,7 +629,7 @@ public: } if (hasUpdates) { - resolvedPath = makeEllipseBezierPath(Vector2D(sizeValue.x, sizeValue.y), Vector2D(positionValue.x, positionValue.y), direction); + ValueInterpolator::setInplace(makeEllipseBezierPath(Vector2D(sizeValue.x, sizeValue.y), Vector2D(positionValue.x, positionValue.y), direction), resolvedPath); } hasValidData = true; @@ -732,7 +716,7 @@ public: } if (hasUpdates) { - resolvedPath = makeStarBezierPath(Vector2D(positionValue.x, positionValue.y), outerRadiusValue, innerRadiusValue, outerRoundednessValue, innerRoundednessValue, pointsValue, rotationValue, direction); + ValueInterpolator::setInplace(makeStarBezierPath(Vector2D(positionValue.x, positionValue.y), outerRadiusValue, innerRadiusValue, outerRoundednessValue, innerRoundednessValue, pointsValue, rotationValue, direction), resolvedPath); } hasValidData = true; @@ -916,15 +900,19 @@ public: std::vector shadings; std::vector> trims; - std::vector> subItems; - public: + std::vector> subItems; std::shared_ptr _contentItem; private: - std::vector collectPaths(size_t subItemLimit, CATransform3D const &parentTransform, bool skipApplyTransform) { + bool hasTrims(size_t subItemLimit) { + return false; + } + + std::vector collectPaths(size_t subItemLimit, CATransform3D const &parentTransform, bool skipApplyTransform, bool &hasTrims) { std::vector mappedPaths; + //TODO:remove skipApplyTransform CATransform3D effectiveTransform = parentTransform; if (!skipApplyTransform && isGroup && transform) { effectiveTransform = transform->transform() * effectiveTransform; @@ -932,8 +920,8 @@ public: size_t maxSubitem = std::min(subItems.size(), subItemLimit); - if (path) { - mappedPaths.emplace_back(*(path->currentPath()), effectiveTransform); + if (_contentItem->path) { + mappedPaths.emplace_back(_contentItem->path.value(), effectiveTransform); } for (size_t i = 0; i < maxSubitem; i++) { @@ -944,9 +932,10 @@ public: currentTrim = trims[0]->trimParams(); } - auto subItemPaths = subItem->collectPaths(INT32_MAX, effectiveTransform, false); + auto subItemPaths = subItem->collectPaths(INT32_MAX, effectiveTransform, false, hasTrims); if (currentTrim) { + hasTrims = true; CompoundBezierPath tempPath; for (auto &path : subItemPaths) { tempPath.appendPath(path.path.copyUsingTransform(path.transform)); @@ -993,6 +982,10 @@ public: _contentItem = std::make_shared(); _contentItem->isGroup = isGroup; + if (path) { + _contentItem->path = *path->currentPath(); + } + if (!shadings.empty()) { for (int i = 0; i < shadings.size(); i++) { auto &shadingVariant = shadings[i]; @@ -1031,6 +1024,8 @@ public: if (path) { path->update(frameTime); + } else { + _contentItem->path = std::nullopt; } for (const auto &trim : trims) { trim->update(frameTime); @@ -1067,28 +1062,46 @@ public: continue; } - CompoundBezierPath compoundPath; - auto paths = collectPaths(shadingVariant.subItemLimit, CATransform3D::identity(), true); - for (const auto &path : paths) { - compoundPath.appendPath(path.path.copyUsingTransform(path.transform)); - } - //std::optional currentTrim = parentTrim; //TODO:investigate /*if (!trims.empty()) { currentTrim = trims[0]; }*/ + bool hasTrims = false; if (parentTrim) { + CompoundBezierPath compoundPath; + hasTrims = true; + auto paths = collectPaths(shadingVariant.subItemLimit, CATransform3D::identity(), true, hasTrims); + for (const auto &path : paths) { + compoundPath.appendPath(path.path.copyUsingTransform(path.transform)); + } + compoundPath = trimCompoundPath(compoundPath, parentTrim->start, parentTrim->end, parentTrim->offset, parentTrim->type); + + std::vector resultPaths; + for (const auto &path : compoundPath.paths) { + resultPaths.push_back(path); + } + _contentItem->shadings[i]->explicitPath = resultPaths; + } else { + CompoundBezierPath compoundPath; + auto paths = collectPaths(shadingVariant.subItemLimit, CATransform3D::identity(), true, hasTrims); + for (const auto &path : paths) { + compoundPath.appendPath(path.path.copyUsingTransform(path.transform)); + } + std::vector resultPaths; + for (const auto &path : compoundPath.paths) { + resultPaths.push_back(path); + } + + if (hasTrims) { + _contentItem->shadings[i]->explicitPath = resultPaths; + } else { + _contentItem->shadings[i]->explicitPath = std::nullopt; + _contentItem->shadings[i]->explicitPath = resultPaths; + } } - - std::vector resultPaths; - for (const auto &path : compoundPath.paths) { - resultPaths.push_back(path); - } - - _contentItem->shadings[i]->explicitPath = resultPaths; } if (isGroup && !subItems.empty()) { @@ -1199,7 +1212,16 @@ private: case ShapeType::Trim: { Trim const &trim = *((Trim *)item.get()); - itemTree->addTrim(trim); + auto groupItem = std::make_shared(); + groupItem->isGroup = true; + for (const auto &subItem : itemTree->subItems) { + groupItem->addSubItem(subItem); + } + groupItem->addTrim(trim); + itemTree->subItems.clear(); + itemTree->addSubItem(groupItem); + + //itemTree->addTrim(trim); break; } diff --git a/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/Model/ShapeItems/Trim.hpp b/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/Model/ShapeItems/Trim.hpp index e5c59daecd..e4628d604a 100644 --- a/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/Model/ShapeItems/Trim.hpp +++ b/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/Model/ShapeItems/Trim.hpp @@ -8,11 +8,6 @@ namespace lottie { -enum class TrimType: int { - Simultaneously = 1, - Individually = 2 -}; - /// An item that defines trim class Trim: public ShapeItem { public: From e9410f93153967f1fcf3858c45915a9fe1a06ef5 Mon Sep 17 00:00:00 2001 From: Isaac <> Date: Thu, 16 May 2024 19:38:11 +0400 Subject: [PATCH 06/14] Refactoring --- .../PublicHeaders/LottieCpp/ShapeAttributes.h | 6 ++--- .../CompLayers/ShapeCompositionLayer.cpp | 26 +++++++------------ 2 files changed, 12 insertions(+), 20 deletions(-) diff --git a/submodules/TelegramUI/Components/LottieCpp/PublicHeaders/LottieCpp/ShapeAttributes.h b/submodules/TelegramUI/Components/LottieCpp/PublicHeaders/LottieCpp/ShapeAttributes.h index 2b707061a5..c6f11afa4c 100644 --- a/submodules/TelegramUI/Components/LottieCpp/PublicHeaders/LottieCpp/ShapeAttributes.h +++ b/submodules/TelegramUI/Components/LottieCpp/PublicHeaders/LottieCpp/ShapeAttributes.h @@ -41,14 +41,12 @@ struct TrimParams { double end = 0.0; double offset = 0.0; TrimType type = TrimType::Simultaneously; - size_t subItemLimit = 0; - TrimParams(double start_, double end_, double offset_, TrimType type_, size_t subItemLimit_) : + TrimParams(double start_, double end_, double offset_, TrimType type_) : start(start_), end(end_), offset(offset_), - type(type_), - subItemLimit(subItemLimit_) { + type(type_) { } }; diff --git a/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/MainThread/LayerContainers/CompLayers/ShapeCompositionLayer.cpp b/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/MainThread/LayerContainers/CompLayers/ShapeCompositionLayer.cpp index e269f7f74c..64f40338d4 100644 --- a/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/MainThread/LayerContainers/CompLayers/ShapeCompositionLayer.cpp +++ b/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/MainThread/LayerContainers/CompLayers/ShapeCompositionLayer.cpp @@ -439,9 +439,8 @@ public: class TrimParamsOutput { public: - TrimParamsOutput(Trim const &trim, size_t subItemLimit) : + TrimParamsOutput(Trim const &trim) : type(trim.trimType), - subItemLimit(subItemLimit), start(trim.start.keyframes), end(trim.end.keyframes), offset(trim.offset.keyframes) { @@ -469,12 +468,11 @@ public: double resolvedOffset = fmod(offsetValue, 360.0) / 360.0; - return TrimParams(resolvedStart, resolvedEnd, resolvedOffset, type, subItemLimit); + return TrimParams(resolvedStart, resolvedEnd, resolvedOffset, type); } private: TrimType type; - size_t subItemLimit = 0; KeyframeInterpolator start; double startValue = 0.0; @@ -974,7 +972,7 @@ public: } void addTrim(Trim const &trim) { - trims.push_back(std::make_shared(trim, subItems.size())); + trims.push_back(std::make_shared(trim)); } public: @@ -1045,7 +1043,7 @@ public: } } - void updateChildren(std::optional parentTrim) { + void updateContents(std::optional parentTrim) { CATransform3D containerTransform = CATransform3D::identity(); double containerOpacity = 1.0; if (transform) { @@ -1108,14 +1106,12 @@ public: for (int i = (int)subItems.size() - 1; i >= 0; i--) { std::optional childTrim = parentTrim; for (const auto &trim : trims) { - if (i < (int)trim->trimParams().subItemLimit) { - //TODO:allow combination - //assert(!parentTrim); - childTrim = trim->trimParams(); - } + //TODO:allow combination + //assert(!parentTrim); + childTrim = trim->trimParams(); } - subItems[i]->updateChildren(childTrim); + subItems[i]->updateContents(childTrim); } } } @@ -1221,8 +1217,6 @@ private: itemTree->subItems.clear(); itemTree->addSubItem(groupItem); - //itemTree->addTrim(trim); - break; } case ShapeType::Transform: { @@ -1298,7 +1292,7 @@ void ShapeCompositionLayer::displayContentsWithFrame(double frame, bool forceUpd _frameTime = frame; _frameTimeInitialized = true; _contentTree->itemTree->updateFrame(_frameTime); - _contentTree->itemTree->updateChildren(std::nullopt); + _contentTree->itemTree->updateContents(std::nullopt); } std::shared_ptr ShapeCompositionLayer::renderTreeNode() { @@ -1306,7 +1300,7 @@ std::shared_ptr ShapeCompositionLayer::renderTreeNode() { _frameTime = 0.0; _frameTimeInitialized = true; _contentTree->itemTree->updateFrame(_frameTime); - _contentTree->itemTree->updateChildren(std::nullopt); + _contentTree->itemTree->updateContents(std::nullopt); } if (!_renderTreeNode) { From 1b64858bab9b901b560d565e5c8eb877b66946b3 Mon Sep 17 00:00:00 2001 From: Isaac <> Date: Thu, 16 May 2024 19:38:49 +0400 Subject: [PATCH 07/14] Fix check --- submodules/MtProtoKit/Sources/MTRequestMessageService.m | 2 +- submodules/TelegramCore/Sources/Network/Network.swift | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/submodules/MtProtoKit/Sources/MTRequestMessageService.m b/submodules/MtProtoKit/Sources/MTRequestMessageService.m index 202a653f24..7bfdd2f6b2 100644 --- a/submodules/MtProtoKit/Sources/MTRequestMessageService.m +++ b/submodules/MtProtoKit/Sources/MTRequestMessageService.m @@ -922,7 +922,7 @@ }]; restartRequest = true; - } else if (rpcError.errorCode == 400 && [rpcError.errorDescription rangeOfString:@"APNS_VERIFY_CHECK_"].location != NSNotFound) { + } else if (rpcError.errorCode == 403 && [rpcError.errorDescription rangeOfString:@"APNS_VERIFY_CHECK_"].location != NSNotFound) { if (request.errorContext == nil) { request.errorContext = [[MTRequestErrorContext alloc] init]; } diff --git a/submodules/TelegramCore/Sources/Network/Network.swift b/submodules/TelegramCore/Sources/Network/Network.swift index c4f8561ae1..f7e2c03889 100644 --- a/submodules/TelegramCore/Sources/Network/Network.swift +++ b/submodules/TelegramCore/Sources/Network/Network.swift @@ -584,7 +584,7 @@ func initializedNetwork(accountId: AccountRecordId, arguments: NetworkInitializa } |> filter { $0 != nil } |> take(1) - |> timeout(15.0, queue: .mainQueue(), alternate: .single(nil))).start(next: { secret in + |> timeout(15.0, queue: .mainQueue(), alternate: .single("APNS_PUSH_TIMEOUT"))).start(next: { secret in subscriber?.putNext(secret) subscriber?.putCompletion() }) From 5edecdb686cf82adc5da6c7f29c17b0dff5fe417 Mon Sep 17 00:00:00 2001 From: Isaac <> Date: Thu, 16 May 2024 23:04:02 +0400 Subject: [PATCH 08/14] [WIP] Message preview --- .../Sources/AttachmentPanel.swift | 2 + ...ChatSendMessageActionSheetController.swift | 2 + .../ChatSendMessageContextScreen.swift | 166 +++++++++++------- .../Sources/MessageItemView.swift | 153 ++++++++++++++-- 4 files changed, 249 insertions(+), 74 deletions(-) diff --git a/submodules/AttachmentUI/Sources/AttachmentPanel.swift b/submodules/AttachmentUI/Sources/AttachmentPanel.swift index a65d30ba05..b62a13ff69 100644 --- a/submodules/AttachmentUI/Sources/AttachmentPanel.swift +++ b/submodules/AttachmentUI/Sources/AttachmentPanel.swift @@ -998,6 +998,8 @@ final class AttachmentPanel: ASDisplayNode, ASScrollViewDelegate { sourceSendButton: node, textInputView: textInputNode.textView, mediaPreview: mediaPreview, + mediaCaptionIsAbove: (false, { _ in + }), emojiViewProvider: textInputPanelNode.emojiViewProvider, attachment: true, canSendWhenOnline: sendWhenOnlineAvailable, diff --git a/submodules/ChatSendMessageActionUI/Sources/ChatSendMessageActionSheetController.swift b/submodules/ChatSendMessageActionUI/Sources/ChatSendMessageActionSheetController.swift index 457bec6f6f..b60d00bfb2 100644 --- a/submodules/ChatSendMessageActionUI/Sources/ChatSendMessageActionSheetController.swift +++ b/submodules/ChatSendMessageActionUI/Sources/ChatSendMessageActionSheetController.swift @@ -185,6 +185,7 @@ public func makeChatSendMessageActionSheetController( sourceSendButton: ASDisplayNode, textInputView: UITextView, mediaPreview: ChatSendMessageContextScreenMediaPreview? = nil, + mediaCaptionIsAbove: (Bool, (Bool) -> Void)? = nil, emojiViewProvider: ((ChatTextInputTextCustomEmojiAttribute) -> UIView)?, wallpaperBackgroundNode: WallpaperBackgroundNode? = nil, attachment: Bool = false, @@ -228,6 +229,7 @@ public func makeChatSendMessageActionSheetController( sourceSendButton: sourceSendButton, textInputView: textInputView, mediaPreview: mediaPreview, + mediaCaptionIsAbove: mediaCaptionIsAbove, emojiViewProvider: emojiViewProvider, wallpaperBackgroundNode: wallpaperBackgroundNode, attachment: attachment, diff --git a/submodules/ChatSendMessageActionUI/Sources/ChatSendMessageContextScreen.swift b/submodules/ChatSendMessageActionUI/Sources/ChatSendMessageContextScreen.swift index b91868aa58..6ce29a2d88 100644 --- a/submodules/ChatSendMessageActionUI/Sources/ChatSendMessageContextScreen.swift +++ b/submodules/ChatSendMessageActionUI/Sources/ChatSendMessageContextScreen.swift @@ -63,6 +63,7 @@ final class ChatSendMessageContextScreenComponent: Component { let sourceSendButton: ASDisplayNode let textInputView: UITextView let mediaPreview: ChatSendMessageContextScreenMediaPreview? + let mediaCaptionIsAbove: (Bool, (Bool) -> Void)? let emojiViewProvider: ((ChatTextInputTextCustomEmojiAttribute) -> UIView)? let wallpaperBackgroundNode: WallpaperBackgroundNode? let attachment: Bool @@ -85,6 +86,7 @@ final class ChatSendMessageContextScreenComponent: Component { sourceSendButton: ASDisplayNode, textInputView: UITextView, mediaPreview: ChatSendMessageContextScreenMediaPreview?, + mediaCaptionIsAbove: (Bool, (Bool) -> Void)?, emojiViewProvider: ((ChatTextInputTextCustomEmojiAttribute) -> UIView)?, wallpaperBackgroundNode: WallpaperBackgroundNode?, attachment: Bool, @@ -106,6 +108,7 @@ final class ChatSendMessageContextScreenComponent: Component { self.sourceSendButton = sourceSendButton self.textInputView = textInputView self.mediaPreview = mediaPreview + self.mediaCaptionIsAbove = mediaCaptionIsAbove self.emojiViewProvider = emojiViewProvider self.wallpaperBackgroundNode = wallpaperBackgroundNode self.attachment = attachment @@ -160,6 +163,8 @@ final class ChatSendMessageContextScreenComponent: Component { private weak var state: EmptyComponentState? private var isUpdating: Bool = false + private var mediaCaptionIsAbove: Bool = false + private let messageEffectDisposable = MetaDisposable() private var selectedMessageEffect: AvailableMessageEffects.MessageEffect? private var standaloneReactionAnimation: AnimatedStickerNode? @@ -278,6 +283,8 @@ final class ChatSendMessageContextScreenComponent: Component { let themeUpdated = environment.theme !== self.environment?.theme if self.component == nil { + self.mediaCaptionIsAbove = component.mediaCaptionIsAbove?.0 ?? false + component.gesture.externalUpdated = { [weak self] view, location in guard let self, let actionsStackNode = self.actionsStackNode else { return @@ -345,9 +352,103 @@ final class ChatSendMessageContextScreenComponent: Component { sendButtonScale = 1.0 } + var reminders = false + var isSecret = false + var canSchedule = false + if let peerId = component.peerId { + reminders = peerId == component.context.account.peerId + isSecret = peerId.namespace == Namespaces.Peer.SecretChat + canSchedule = !isSecret + } + if component.isScheduledMessages { + canSchedule = false + } + + var items: [ContextMenuItem] = [] + if component.mediaCaptionIsAbove != nil { + //TODO:localize + let mediaCaptionIsAbove = self.mediaCaptionIsAbove + items.append(.action(ContextMenuActionItem( + id: AnyHashable("captionPosition"), + text: mediaCaptionIsAbove ? "Move Caption Down" : "Move Caption Up", + icon: { _ in + return nil + }, iconAnimation: ContextMenuActionItem.IconAnimation( + name: !mediaCaptionIsAbove ? "message_preview_sort_above" : "message_preview_sort_below" + ), action: { [weak self] _, _ in + guard let self, let component = self.component else { + return + } + self.mediaCaptionIsAbove = !self.mediaCaptionIsAbove + component.mediaCaptionIsAbove?.1(self.mediaCaptionIsAbove) + if !self.isUpdating { + self.state?.updated(transition: .spring(duration: 0.35)) + } + } + ))) + } + if !reminders { + items.append(.action(ContextMenuActionItem( + id: AnyHashable("silent"), + text: environment.strings.Conversation_SendMessage_SendSilently, + icon: { theme in + return generateTintedImage(image: UIImage(bundleImageName: "Chat/Input/Menu/SilentIcon"), color: theme.contextMenu.primaryColor) + }, action: { [weak self] _, _ in + guard let self, let component = self.component else { + return + } + self.animateOutToEmpty = true + component.sendMessage(.silently, self.selectedMessageEffect.flatMap({ ChatSendMessageActionSheetController.MessageEffect(id: $0.id) })) + self.environment?.controller()?.dismiss() + } + ))) + + if component.canSendWhenOnline && canSchedule { + items.append(.action(ContextMenuActionItem( + id: AnyHashable("whenOnline"), + text: environment.strings.Conversation_SendMessage_SendWhenOnline, + icon: { theme in + return generateTintedImage(image: UIImage(bundleImageName: "Chat/Input/Menu/WhenOnlineIcon"), color: theme.contextMenu.primaryColor) + }, action: { [weak self] _, _ in + guard let self, let component = self.component else { + return + } + self.animateOutToEmpty = true + component.sendMessage(.whenOnline, self.selectedMessageEffect.flatMap({ ChatSendMessageActionSheetController.MessageEffect(id: $0.id) })) + self.environment?.controller()?.dismiss() + } + ))) + } + } + if canSchedule { + items.append(.action(ContextMenuActionItem( + id: AnyHashable("schedule"), + text: reminders ? environment.strings.Conversation_SendMessage_SetReminder: environment.strings.Conversation_SendMessage_ScheduleMessage, + icon: { theme in + return generateTintedImage(image: UIImage(bundleImageName: "Chat/Input/Menu/ScheduleIcon"), color: theme.contextMenu.primaryColor) + }, action: { [weak self] _, _ in + guard let self, let component = self.component else { + return + } + self.animateOutToEmpty = true + component.schedule(self.selectedMessageEffect.flatMap({ ChatSendMessageActionSheetController.MessageEffect(id: $0.id) })) + self.environment?.controller()?.dismiss() + } + ))) + } + let actionsStackNode: ContextControllerActionsStackNode if let current = self.actionsStackNode { actionsStackNode = current + + actionsStackNode.replace(item: ContextControllerActionsListStackItem( + id: AnyHashable("items"), + items: items, + reactionItems: nil, + tip: nil, + tipSignal: .single(nil), + dismissed: nil + ), animated: !transition.animation.isImmediate) } else { actionsStackNode = ContextControllerActionsStackNode( getController: { @@ -366,69 +467,9 @@ final class ChatSendMessageContextScreenComponent: Component { ) actionsStackNode.layer.anchorPoint = CGPoint(x: 1.0, y: 0.0) - var reminders = false - var isSecret = false - var canSchedule = false - if let peerId = component.peerId { - reminders = peerId == component.context.account.peerId - isSecret = peerId.namespace == Namespaces.Peer.SecretChat - canSchedule = !isSecret - } - if component.isScheduledMessages { - canSchedule = false - } - - var items: [ContextMenuItem] = [] - if !reminders { - items.append(.action(ContextMenuActionItem( - text: environment.strings.Conversation_SendMessage_SendSilently, - icon: { theme in - return generateTintedImage(image: UIImage(bundleImageName: "Chat/Input/Menu/SilentIcon"), color: theme.contextMenu.primaryColor) - }, action: { [weak self] _, _ in - guard let self, let component = self.component else { - return - } - self.animateOutToEmpty = true - component.sendMessage(.silently, self.selectedMessageEffect.flatMap({ ChatSendMessageActionSheetController.MessageEffect(id: $0.id) })) - self.environment?.controller()?.dismiss() - } - ))) - - if component.canSendWhenOnline && canSchedule { - items.append(.action(ContextMenuActionItem( - text: environment.strings.Conversation_SendMessage_SendWhenOnline, - icon: { theme in - return generateTintedImage(image: UIImage(bundleImageName: "Chat/Input/Menu/WhenOnlineIcon"), color: theme.contextMenu.primaryColor) - }, action: { [weak self] _, _ in - guard let self, let component = self.component else { - return - } - self.animateOutToEmpty = true - component.sendMessage(.whenOnline, self.selectedMessageEffect.flatMap({ ChatSendMessageActionSheetController.MessageEffect(id: $0.id) })) - self.environment?.controller()?.dismiss() - } - ))) - } - } - if canSchedule { - items.append(.action(ContextMenuActionItem( - text: reminders ? environment.strings.Conversation_SendMessage_SetReminder: environment.strings.Conversation_SendMessage_ScheduleMessage, - icon: { theme in - return generateTintedImage(image: UIImage(bundleImageName: "Chat/Input/Menu/ScheduleIcon"), color: theme.contextMenu.primaryColor) - }, action: { [weak self] _, _ in - guard let self, let component = self.component else { - return - } - self.animateOutToEmpty = true - component.schedule(self.selectedMessageEffect.flatMap({ ChatSendMessageActionSheetController.MessageEffect(id: $0.id) })) - self.environment?.controller()?.dismiss() - } - ))) - } - actionsStackNode.push( item: ContextControllerActionsListStackItem( - id: nil, + id: AnyHashable("items"), items: items, reactionItems: nil, tip: nil, @@ -505,6 +546,7 @@ final class ChatSendMessageContextScreenComponent: Component { sourceTextInputView: component.textInputView as? ChatInputTextView, emojiViewProvider: component.emojiViewProvider, sourceMediaPreview: component.mediaPreview, + mediaCaptionIsAbove: self.mediaCaptionIsAbove, textInsets: messageTextInsets, explicitBackgroundSize: explicitMessageBackgroundSize, maxTextWidth: localSourceTextInputViewFrame.width, @@ -1093,6 +1135,7 @@ public class ChatSendMessageContextScreen: ViewControllerComponentContainer, Cha sourceSendButton: ASDisplayNode, textInputView: UITextView, mediaPreview: ChatSendMessageContextScreenMediaPreview?, + mediaCaptionIsAbove: (Bool, (Bool) -> Void)?, emojiViewProvider: ((ChatTextInputTextCustomEmojiAttribute) -> UIView)?, wallpaperBackgroundNode: WallpaperBackgroundNode?, attachment: Bool, @@ -1119,6 +1162,7 @@ public class ChatSendMessageContextScreen: ViewControllerComponentContainer, Cha sourceSendButton: sourceSendButton, textInputView: textInputView, mediaPreview: mediaPreview, + mediaCaptionIsAbove: mediaCaptionIsAbove, emojiViewProvider: emojiViewProvider, wallpaperBackgroundNode: wallpaperBackgroundNode, attachment: attachment, diff --git a/submodules/ChatSendMessageActionUI/Sources/MessageItemView.swift b/submodules/ChatSendMessageActionUI/Sources/MessageItemView.swift index 63d6ef7050..a5aee8a88a 100644 --- a/submodules/ChatSendMessageActionUI/Sources/MessageItemView.swift +++ b/submodules/ChatSendMessageActionUI/Sources/MessageItemView.swift @@ -202,6 +202,7 @@ final class MessageItemView: UIView { sourceTextInputView: ChatInputTextView?, emojiViewProvider: ((ChatTextInputTextCustomEmojiAttribute) -> UIView)?, sourceMediaPreview: ChatSendMessageContextScreenMediaPreview?, + mediaCaptionIsAbove: Bool, textInsets: UIEdgeInsets, explicitBackgroundSize: CGSize?, maxTextWidth: CGFloat, @@ -255,6 +256,16 @@ final class MessageItemView: UIView { backgroundNode: backgroundNode ) + self.backgroundNode.setType( + type: .outgoing(.None), + highlighted: false, + graphics: themeGraphics, + maskMode: true, + hasWallpaper: true, + transition: transition.containedViewLayoutTransition, + backgroundNode: backgroundNode + ) + if let sourceMediaPreview { let mediaPreviewClippingView: UIView if let current = self.mediaPreviewClippingView { @@ -281,7 +292,7 @@ final class MessageItemView: UIView { let mediaPreviewSize = sourceMediaPreview.update(containerSize: containerSize, transition: transition) var backgroundSize = CGSize(width: mediaPreviewSize.width, height: mediaPreviewSize.height) - let mediaPreviewFrame: CGRect + var mediaPreviewFrame: CGRect switch sourceMediaPreview.layoutType { case .message, .media: backgroundSize.width += 7.0 @@ -290,8 +301,135 @@ final class MessageItemView: UIView { mediaPreviewFrame = CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: mediaPreviewSize) } + let backgroundAlpha: CGFloat + switch sourceMediaPreview.layoutType { + case .media: + backgroundAlpha = explicitBackgroundSize != nil ? 0.0 : 1.0 + case .message, .videoMessage: + backgroundAlpha = 0.0 + } + + var backgroundFrame = mediaPreviewFrame.insetBy(dx: -2.0, dy: -2.0) + backgroundFrame.size.width += 6.0 + + if textString.length != 0 { + let textNode: ChatInputTextNode + if let current = self.textNode { + textNode = current + } else { + textNode = ChatInputTextNode(disableTiling: true) + textNode.textView.isScrollEnabled = false + textNode.isUserInteractionEnabled = false + self.textNode = textNode + self.textClippingContainer.addSubview(textNode.view) + + if let sourceTextInputView { + var textContainerInset = sourceTextInputView.defaultTextContainerInset + textContainerInset.right = 0.0 + textNode.textView.defaultTextContainerInset = textContainerInset + } + + let messageAttributedText = NSMutableAttributedString(attributedString: textString) + textNode.attributedText = messageAttributedText + } + + let mainColor = presentationData.theme.chat.message.outgoing.accentControlColor + let mappedLineStyle: ChatInputTextView.Theme.Quote.LineStyle + if let sourceTextInputView, let textTheme = sourceTextInputView.theme { + switch textTheme.quote.lineStyle { + case .solid: + mappedLineStyle = .solid(color: mainColor) + case .doubleDashed: + mappedLineStyle = .doubleDashed(mainColor: mainColor, secondaryColor: .clear) + case .tripleDashed: + mappedLineStyle = .tripleDashed(mainColor: mainColor, secondaryColor: .clear, tertiaryColor: .clear) + } + } else { + mappedLineStyle = .solid(color: mainColor) + } + + textNode.textView.theme = ChatInputTextView.Theme( + quote: ChatInputTextView.Theme.Quote( + background: mainColor.withMultipliedAlpha(0.1), + foreground: mainColor, + lineStyle: mappedLineStyle, + codeBackground: mainColor.withMultipliedAlpha(0.1), + codeForeground: mainColor + ) + ) + + let maxTextWidth = mediaPreviewFrame.width + + let textPositioningInsets = UIEdgeInsets(top: -5.0, left: 0.0, bottom: -4.0, right: -4.0) + + let currentRightInset: CGFloat = 0.0 + let textHeight = textNode.textHeightForWidth(maxTextWidth, rightInset: currentRightInset) + textNode.updateLayout(size: CGSize(width: maxTextWidth, height: textHeight)) + + let textBoundingRect = textNode.textView.currentTextBoundingRect().integral + let lastLineBoundingRect = textNode.textView.lastLineBoundingRect().integral + + let textWidth = textBoundingRect.width + let textSize = CGSize(width: textWidth, height: textHeight) + + var positionedTextSize = CGSize(width: textSize.width + textPositioningInsets.left + textPositioningInsets.right, height: textSize.height + textPositioningInsets.top + textPositioningInsets.bottom) + + let effectInset: CGFloat = 12.0 + if effect != nil, lastLineBoundingRect.width > textSize.width - effectInset { + if lastLineBoundingRect != textBoundingRect { + positionedTextSize.height += 11.0 + } else { + positionedTextSize.width += effectInset + } + } + let unclippedPositionedTextHeight = positionedTextSize.height - (textPositioningInsets.top + textPositioningInsets.bottom) + + positionedTextSize.height = min(positionedTextSize.height, maxTextHeight) + + let size = CGSize(width: positionedTextSize.width + textInsets.left + textInsets.right, height: positionedTextSize.height + textInsets.top + textInsets.bottom) + + var textFrame = CGRect(origin: CGPoint(x: textInsets.left - 6.0, y: backgroundFrame.height - 4.0 + textInsets.top), size: positionedTextSize) + if mediaCaptionIsAbove { + textFrame.origin.y = 5.0 + } + + backgroundFrame.size.height += textSize.height + 2.0 + if mediaCaptionIsAbove { + mediaPreviewFrame.origin.y += textSize.height + 2.0 + } + + let backgroundSize = explicitBackgroundSize ?? size + + let previousSize = self.currentSize + self.currentSize = backgroundFrame.size + let _ = previousSize + + let textClippingContainerFrame = CGRect(origin: CGPoint(x: backgroundFrame.minX + 1.0, y: backgroundFrame.minY + 1.0), size: CGSize(width: backgroundFrame.width - 1.0 - 7.0, height: backgroundFrame.height - 1.0 - 1.0)) + + var textClippingContainerBounds = CGRect(origin: CGPoint(), size: textClippingContainerFrame.size) + if explicitBackgroundSize != nil, let sourceTextInputView { + textClippingContainerBounds.origin.y = sourceTextInputView.contentOffset.y + } else { + textClippingContainerBounds.origin.y = unclippedPositionedTextHeight - backgroundSize.height + 4.0 + textClippingContainerBounds.origin.y = max(0.0, textClippingContainerBounds.origin.y) + } + + transition.setPosition(view: self.textClippingContainer, position: textClippingContainerFrame.center) + transition.setBounds(view: self.textClippingContainer, bounds: textClippingContainerBounds) + + transition.setFrame(view: textNode.view, frame: CGRect(origin: CGPoint(x: textFrame.minX + textPositioningInsets.left - textClippingContainerFrame.minX, y: textFrame.minY + textPositioningInsets.top - textClippingContainerFrame.minY), size: CGSize(width: maxTextWidth, height: textHeight))) + self.updateTextContents() + } + transition.setFrame(view: sourceMediaPreview.view, frame: mediaPreviewFrame) + transition.setFrame(view: self.backgroundWallpaperNode.view, frame: CGRect(origin: CGPoint(), size: backgroundFrame.size)) + transition.setAlpha(view: self.backgroundWallpaperNode.view, alpha: backgroundAlpha) + self.backgroundWallpaperNode.updateFrame(backgroundFrame, transition: transition.containedViewLayoutTransition) + transition.setFrame(view: self.backgroundNode.view, frame: backgroundFrame) + transition.setAlpha(view: self.backgroundNode.view, alpha: backgroundAlpha) + self.backgroundNode.updateLayout(size: backgroundFrame.size, transition: transition.containedViewLayoutTransition) + if let effectIcon = self.effectIcon, let effectIconSize { if let effectIconView = effectIcon.view { var animateIn = false @@ -367,7 +505,7 @@ final class MessageItemView: UIView { } } - return backgroundSize + return backgroundFrame.size } else { let textNode: ChatInputTextNode if let current = self.textNode { @@ -384,7 +522,6 @@ final class MessageItemView: UIView { } let messageAttributedText = NSMutableAttributedString(attributedString: textString) - //messageAttributedText.addAttribute(NSAttributedString.Key.foregroundColor, value: presentationData.theme.chat.message.outgoing.primaryTextColor, range: NSMakeRange(0, (messageAttributedText.string as NSString).length)) textNode.attributedText = messageAttributedText } @@ -446,16 +583,6 @@ final class MessageItemView: UIView { let textFrame = CGRect(origin: CGPoint(x: textInsets.left, y: textInsets.top), size: positionedTextSize) - self.backgroundNode.setType( - type: .outgoing(.None), - highlighted: false, - graphics: themeGraphics, - maskMode: true, - hasWallpaper: true, - transition: transition.containedViewLayoutTransition, - backgroundNode: backgroundNode - ) - let backgroundSize = explicitBackgroundSize ?? size let previousSize = self.currentSize From 3a5585c1fdf973d138b499da9f661d2b44177902 Mon Sep 17 00:00:00 2001 From: Isaac <> Date: Fri, 17 May 2024 00:20:40 +0400 Subject: [PATCH 09/14] Lottie --- .../Sources/SoftwareLottieRenderer.mm | 47 +++++-- .../PublicHeaders/LottieCpp/BezierPath.h | 1 + .../PublicHeaders/LottieCpp/RenderTreeNode.h | 1 + .../CompLayers/CompositionLayer.hpp | 10 +- .../CompLayers/PreCompositionLayer.hpp | 16 +-- .../CompLayers/ShapeCompositionLayer.cpp | 131 ++++++++++++------ .../CompLayers/ShapeCompositionLayer.hpp | 6 +- .../CompLayers/TextCompositionLayer.hpp | 2 +- .../MainThreadAnimationLayer.hpp | 12 +- .../Private/Utility/Primitives/BezierPath.cpp | 52 +++++++ 10 files changed, 201 insertions(+), 77 deletions(-) diff --git a/Tests/LottieMetalTest/SoftwareLottieRenderer/Sources/SoftwareLottieRenderer.mm b/Tests/LottieMetalTest/SoftwareLottieRenderer/Sources/SoftwareLottieRenderer.mm index 789c95c35a..5e5320cf47 100644 --- a/Tests/LottieMetalTest/SoftwareLottieRenderer/Sources/SoftwareLottieRenderer.mm +++ b/Tests/LottieMetalTest/SoftwareLottieRenderer/Sources/SoftwareLottieRenderer.mm @@ -18,6 +18,35 @@ struct TransformedPath { } }; +static lottie::CGRect collectPathBoundingBoxes(std::shared_ptr item, size_t subItemLimit, lottie::CATransform3D const &parentTransform, bool skipApplyTransform) { + //TODO:remove skipApplyTransform + lottie::CATransform3D effectiveTransform = parentTransform; + if (!skipApplyTransform && item->isGroup) { + effectiveTransform = item->transform * effectiveTransform; + } + + size_t maxSubitem = std::min(item->subItems.size(), subItemLimit); + + lottie::CGRect boundingBox(0.0, 0.0, 0.0, 0.0); + if (item->path) { + boundingBox = item->pathBoundingBox.applyingTransform(effectiveTransform); + } + + for (size_t i = 0; i < maxSubitem; i++) { + auto &subItem = item->subItems[i]; + + lottie::CGRect subItemBoundingBox = collectPathBoundingBoxes(subItem, INT32_MAX, effectiveTransform, false); + + if (boundingBox.empty()) { + boundingBox = subItemBoundingBox; + } else { + boundingBox = boundingBox.unionWith(subItemBoundingBox); + } + } + + return boundingBox; +} + static std::vector collectPaths(std::shared_ptr item, size_t subItemLimit, lottie::CATransform3D const &parentTransform, bool skipApplyTransform) { std::vector mappedPaths; @@ -32,6 +61,7 @@ static std::vector collectPaths(std::shared_ptrpath) { mappedPaths.emplace_back(item->path.value(), effectiveTransform); } + assert(!item->trimParams); for (size_t i = 0; i < maxSubitem; i++) { auto &subItem = item->subItems[i]; @@ -66,17 +96,8 @@ static void processRenderContentItem(std::shared_ptr int drawContentDescendants = 0; for (const auto &shadingVariant : contentItem->shadings) { - std::vector itemPaths; - if (shadingVariant->explicitPath) { - itemPaths = shadingVariant->explicitPath.value(); - } else { - auto rawPaths = collectPaths(contentItem, shadingVariant->subItemLimit, lottie::CATransform3D::identity(), true); - for (const auto &rawPath : rawPaths) { - itemPaths.push_back(rawPath.path.copyUsingTransform(rawPath.transform)); - } - } + lottie::CGRect shapeBounds = collectPathBoundingBoxes(contentItem, shadingVariant->subItemLimit, lottie::CATransform3D::identity(), true); - CGRect shapeBounds = bezierPathsBoundingBoxParallel(bezierPathsBoundingBoxContext, itemPaths); if (shadingVariant->stroke) { shapeBounds = shapeBounds.insetBy(-shadingVariant->stroke->lineWidth / 2.0, -shadingVariant->stroke->lineWidth / 2.0); } else if (shadingVariant->fill) { @@ -95,7 +116,8 @@ static void processRenderContentItem(std::shared_ptr } if (contentItem->isGroup) { - for (const auto &subItem : contentItem->subItems) { + for (auto it = contentItem->subItems.rbegin(); it != contentItem->subItems.rend(); it++) { + const auto &subItem = *it; processRenderContentItem(subItem, globalSize, currentTransform, bezierPathsBoundingBoxContext); if (subItem->renderData.isValid) { @@ -491,7 +513,8 @@ static void drawLottieContentItem(std::shared_ptr paren } } - for (const auto &subItem : item->subItems) { + for (auto it = item->subItems.rbegin(); it != item->subItems.rend(); it++) { + const auto &subItem = *it; drawLottieContentItem(currentContext, subItem, renderAlpha); } diff --git a/submodules/TelegramUI/Components/LottieCpp/PublicHeaders/LottieCpp/BezierPath.h b/submodules/TelegramUI/Components/LottieCpp/PublicHeaders/LottieCpp/BezierPath.h index 43b7018442..c9ee189b25 100644 --- a/submodules/TelegramUI/Components/LottieCpp/PublicHeaders/LottieCpp/BezierPath.h +++ b/submodules/TelegramUI/Components/LottieCpp/PublicHeaders/LottieCpp/BezierPath.h @@ -144,6 +144,7 @@ public: CGRect bezierPathsBoundingBox(std::vector const &paths); CGRect bezierPathsBoundingBoxParallel(BezierPathsBoundingBoxContext &context, std::vector const &paths); +CGRect bezierPathsBoundingBoxParallel(BezierPathsBoundingBoxContext &context, BezierPath const &path); std::vector trimBezierPaths(std::vector &sourcePaths, double start, double end, double offset, TrimType type); diff --git a/submodules/TelegramUI/Components/LottieCpp/PublicHeaders/LottieCpp/RenderTreeNode.h b/submodules/TelegramUI/Components/LottieCpp/PublicHeaders/LottieCpp/RenderTreeNode.h index 3565fd8212..2e5a8f2155 100644 --- a/submodules/TelegramUI/Components/LottieCpp/PublicHeaders/LottieCpp/RenderTreeNode.h +++ b/submodules/TelegramUI/Components/LottieCpp/PublicHeaders/LottieCpp/RenderTreeNode.h @@ -424,6 +424,7 @@ public: double alpha = 0.0; std::optional trimParams; std::optional path; + CGRect pathBoundingBox = CGRect(0.0, 0.0, 0.0, 0.0); std::vector> shadings; std::vector> subItems; diff --git a/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/MainThread/LayerContainers/CompLayers/CompositionLayer.hpp b/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/MainThread/LayerContainers/CompLayers/CompositionLayer.hpp index 3399646a54..c8a97b55dd 100644 --- a/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/MainThread/LayerContainers/CompLayers/CompositionLayer.hpp +++ b/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/MainThread/LayerContainers/CompLayers/CompositionLayer.hpp @@ -82,7 +82,7 @@ public: return _contentsLayer; } - void displayWithFrame(double frame, bool forceUpdates) { + void displayWithFrame(double frame, bool forceUpdates, BezierPathsBoundingBoxContext &boundingBoxContext) { _transformNode->updateTree(frame, forceUpdates); bool layerVisible = isInRangeOrEqual(frame, _inFrame, _outFrame); @@ -93,14 +93,14 @@ public: /// Only update contents if current time is within the layers time bounds. if (layerVisible) { - displayContentsWithFrame(frame, forceUpdates); + displayContentsWithFrame(frame, forceUpdates, boundingBoxContext); if (_maskLayer) { _maskLayer->updateWithFrame(frame, forceUpdates); } } } - virtual void displayContentsWithFrame(double frame, bool forceUpdates) { + virtual void displayContentsWithFrame(double frame, bool forceUpdates, BezierPathsBoundingBoxContext &boundingBoxContext) { /// To be overridden by subclass } @@ -151,11 +151,11 @@ public: return _timeStretch; } - virtual std::shared_ptr renderTreeNode() { + virtual std::shared_ptr renderTreeNode(BezierPathsBoundingBoxContext &boundingBoxContext) { return nullptr; } - virtual void updateRenderTree() { + virtual void updateRenderTree(BezierPathsBoundingBoxContext &boundingBoxContext) { } public: diff --git a/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/MainThread/LayerContainers/CompLayers/PreCompositionLayer.hpp b/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/MainThread/LayerContainers/CompLayers/PreCompositionLayer.hpp index 5b310c8571..aff1d21f8b 100644 --- a/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/MainThread/LayerContainers/CompLayers/PreCompositionLayer.hpp +++ b/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/MainThread/LayerContainers/CompLayers/PreCompositionLayer.hpp @@ -86,7 +86,7 @@ public: return result; } - virtual void displayContentsWithFrame(double frame, bool forceUpdates) override { + virtual void displayContentsWithFrame(double frame, bool forceUpdates, BezierPathsBoundingBoxContext &boundingBoxContext) override { double localFrame = 0.0; if (_remappingNode) { _remappingNode->update(frame); @@ -96,11 +96,11 @@ public: } for (const auto &animationLayer : _animationLayers) { - animationLayer->displayWithFrame(localFrame, forceUpdates); + animationLayer->displayWithFrame(localFrame, forceUpdates, boundingBoxContext); } } - virtual std::shared_ptr renderTreeNode() override { + virtual std::shared_ptr renderTreeNode(BezierPathsBoundingBoxContext &boundingBoxContext) override { if (!_renderTreeNode) { _contentsTreeNode = std::make_shared( CGRect(0.0, 0.0, 0.0, 0.0), @@ -120,7 +120,7 @@ public: std::shared_ptr maskNode; bool invertMask = false; if (_matteLayer) { - maskNode = _matteLayer->renderTreeNode(); + maskNode = _matteLayer->renderTreeNode(boundingBoxContext); if (maskNode && _matteType.has_value() && _matteType.value() == MatteType::Invert) { invertMask = true; } @@ -148,7 +148,7 @@ public: } } if (found) { - auto node = animationLayer->renderTreeNode(); + auto node = animationLayer->renderTreeNode(boundingBoxContext); if (node) { renderTreeSubnodes.push_back(node); } @@ -177,9 +177,9 @@ public: return _renderTreeNode; } - virtual void updateRenderTree() override { + virtual void updateRenderTree(BezierPathsBoundingBoxContext &boundingBoxContext) override { if (_matteLayer) { - _matteLayer->updateRenderTree(); + _matteLayer->updateRenderTree(boundingBoxContext); } for (const auto &animationLayer : _animationLayers) { @@ -191,7 +191,7 @@ public: } } if (found) { - animationLayer->updateRenderTree(); + animationLayer->updateRenderTree(boundingBoxContext); } } diff --git a/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/MainThread/LayerContainers/CompLayers/ShapeCompositionLayer.cpp b/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/MainThread/LayerContainers/CompLayers/ShapeCompositionLayer.cpp index 64f40338d4..02fb2d5665 100644 --- a/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/MainThread/LayerContainers/CompLayers/ShapeCompositionLayer.cpp +++ b/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/MainThread/LayerContainers/CompLayers/ShapeCompositionLayer.cpp @@ -506,8 +506,9 @@ public: } virtual ~PathOutput() = default; - virtual void update(AnimationFrameTime frameTime) = 0; + virtual void update(AnimationFrameTime frameTime, BezierPathsBoundingBoxContext &boundingBoxContext) = 0; virtual BezierPath const *currentPath() = 0; + virtual CGRect const ¤tPathBounds() = 0; }; class StaticPathOutput : public PathOutput { @@ -516,15 +517,26 @@ public: resolvedPath(path) { } - virtual void update(AnimationFrameTime frameTime) override { + virtual void update(AnimationFrameTime frameTime, BezierPathsBoundingBoxContext &boundingBoxContext) override { + if (!isPathBoundsResolved) { + resolvedPathBounds = bezierPathsBoundingBoxParallel(boundingBoxContext, resolvedPath); + isPathBoundsResolved = true; + } } virtual BezierPath const *currentPath() override { return &resolvedPath; } + virtual CGRect const ¤tPathBounds() override { + return resolvedPathBounds; + } + private: BezierPath resolvedPath; + + bool isPathBoundsResolved = false; + CGRect resolvedPathBounds = CGRect(0.0, 0.0, 0.0, 0.0); }; class ShapePathOutput : public PathOutput { @@ -533,9 +545,10 @@ public: path(shape.path.keyframes) { } - virtual void update(AnimationFrameTime frameTime) override { + virtual void update(AnimationFrameTime frameTime, BezierPathsBoundingBoxContext &boundingBoxContext) override { if (!hasValidData || path.hasUpdate(frameTime)) { path.update(frameTime, resolvedPath); + resolvedPathBounds = bezierPathsBoundingBoxParallel(boundingBoxContext, resolvedPath); } hasValidData = true; @@ -545,12 +558,17 @@ public: return &resolvedPath; } + virtual CGRect const ¤tPathBounds() override { + return resolvedPathBounds; + } + private: bool hasValidData = false; BezierPathKeyframeInterpolator path; BezierPath resolvedPath; + CGRect resolvedPathBounds = CGRect(0.0, 0.0, 0.0, 0.0); }; class RectanglePathOutput : public PathOutput { @@ -562,7 +580,7 @@ public: cornerRadius(rectangle.cornerRadius.keyframes) { } - virtual void update(AnimationFrameTime frameTime) override { + virtual void update(AnimationFrameTime frameTime, BezierPathsBoundingBoxContext &boundingBoxContext) override { bool hasUpdates = false; if (!hasValidData || position.hasUpdate(frameTime)) { @@ -580,6 +598,7 @@ public: if (hasUpdates) { ValueInterpolator::setInplace(makeRectangleBezierPath(Vector2D(positionValue.x, positionValue.y), Vector2D(sizeValue.x, sizeValue.y), cornerRadiusValue, direction), resolvedPath); + resolvedPathBounds = bezierPathsBoundingBoxParallel(boundingBoxContext, resolvedPath); } hasValidData = true; @@ -589,6 +608,10 @@ public: return &resolvedPath; } + virtual CGRect const ¤tPathBounds() override { + return resolvedPathBounds; + } + private: bool hasValidData = false; @@ -604,6 +627,7 @@ public: double cornerRadiusValue = 0.0; BezierPath resolvedPath; + CGRect resolvedPathBounds = CGRect(0.0, 0.0, 0.0, 0.0); }; class EllipsePathOutput : public PathOutput { @@ -614,7 +638,7 @@ public: size(ellipse.size.keyframes) { } - virtual void update(AnimationFrameTime frameTime) override { + virtual void update(AnimationFrameTime frameTime, BezierPathsBoundingBoxContext &boundingBoxContext) override { bool hasUpdates = false; if (!hasValidData || position.hasUpdate(frameTime)) { @@ -628,6 +652,7 @@ public: if (hasUpdates) { ValueInterpolator::setInplace(makeEllipseBezierPath(Vector2D(sizeValue.x, sizeValue.y), Vector2D(positionValue.x, positionValue.y), direction), resolvedPath); + resolvedPathBounds = bezierPathsBoundingBoxParallel(boundingBoxContext, resolvedPath); } hasValidData = true; @@ -637,6 +662,10 @@ public: return &resolvedPath; } + virtual CGRect const ¤tPathBounds() override { + return resolvedPathBounds; + } + private: bool hasValidData = false; @@ -649,6 +678,7 @@ public: Vector3D sizeValue = Vector3D(0.0, 0.0, 0.0); BezierPath resolvedPath; + CGRect resolvedPathBounds = CGRect(0.0, 0.0, 0.0, 0.0); }; class StarPathOutput : public PathOutput { @@ -673,7 +703,7 @@ public: } } - virtual void update(AnimationFrameTime frameTime) override { + virtual void update(AnimationFrameTime frameTime, BezierPathsBoundingBoxContext &boundingBoxContext) override { bool hasUpdates = false; if (!hasValidData || position.hasUpdate(frameTime)) { @@ -715,6 +745,7 @@ public: if (hasUpdates) { ValueInterpolator::setInplace(makeStarBezierPath(Vector2D(positionValue.x, positionValue.y), outerRadiusValue, innerRadiusValue, outerRoundednessValue, innerRoundednessValue, pointsValue, rotationValue, direction), resolvedPath); + resolvedPathBounds = bezierPathsBoundingBoxParallel(boundingBoxContext, resolvedPath); } hasValidData = true; @@ -724,6 +755,10 @@ public: return &resolvedPath; } + virtual CGRect const ¤tPathBounds() override { + return resolvedPathBounds; + } + private: bool hasValidData = false; @@ -751,6 +786,7 @@ public: double pointsValue = 0.0; BezierPath resolvedPath; + CGRect resolvedPathBounds = CGRect(0.0, 0.0, 0.0, 0.0); }; class TransformOutput { @@ -903,11 +939,7 @@ public: std::shared_ptr _contentItem; private: - bool hasTrims(size_t subItemLimit) { - return false; - } - - std::vector collectPaths(size_t subItemLimit, CATransform3D const &parentTransform, bool skipApplyTransform, bool &hasTrims) { + std::vector collectPaths(size_t subItemLimit, CATransform3D const &parentTransform, bool skipApplyTransform) { std::vector mappedPaths; //TODO:remove skipApplyTransform @@ -930,10 +962,9 @@ public: currentTrim = trims[0]->trimParams(); } - auto subItemPaths = subItem->collectPaths(INT32_MAX, effectiveTransform, false, hasTrims); + auto subItemPaths = subItem->collectPaths(INT32_MAX, effectiveTransform, false); if (currentTrim) { - hasTrims = true; CompoundBezierPath tempPath; for (auto &path : subItemPaths) { tempPath.appendPath(path.path.copyUsingTransform(path.transform)); @@ -1007,23 +1038,22 @@ public: if (isGroup && !subItems.empty()) { std::vector> subItemNodes; - for (int i = (int)subItems.size() - 1; i >= 0; i--) { - subItems[i]->initializeRenderChildren(); - _contentItem->subItems.push_back(subItems[i]->_contentItem); + for (const auto &subItem : subItems) { + subItem->initializeRenderChildren(); + _contentItem->subItems.push_back(subItem->_contentItem); } } } public: - void updateFrame(AnimationFrameTime frameTime) { + void updateFrame(AnimationFrameTime frameTime, BezierPathsBoundingBoxContext &boundingBoxContext) { if (transform) { transform->update(frameTime); } if (path) { - path->update(frameTime); - } else { - _contentItem->path = std::nullopt; + path->update(frameTime, boundingBoxContext); + _contentItem->pathBoundingBox = path->currentPathBounds(); } for (const auto &trim : trims) { trim->update(frameTime); @@ -1039,10 +1069,24 @@ public: } for (const auto &subItem : subItems) { - subItem->updateFrame(frameTime); + subItem->updateFrame(frameTime, boundingBoxContext); } } + bool hasTrims() { + if (!trims.empty()) { + return true; + } + + for (const auto &subItem : subItems) { + if (subItem->hasTrims()) { + return true; + } + } + + return false; + } + void updateContents(std::optional parentTrim) { CATransform3D containerTransform = CATransform3D::identity(); double containerOpacity = 1.0; @@ -1053,6 +1097,10 @@ public: _contentItem->transform = containerTransform; _contentItem->alpha = containerOpacity; + if (!trims.empty()) { + _contentItem->trimParams = trims[0]->trimParams(); + } + for (int i = 0; i < shadings.size(); i++) { const auto &shadingVariant = shadings[i]; @@ -1066,11 +1114,9 @@ public: currentTrim = trims[0]; }*/ - bool hasTrims = false; if (parentTrim) { CompoundBezierPath compoundPath; - hasTrims = true; - auto paths = collectPaths(shadingVariant.subItemLimit, CATransform3D::identity(), true, hasTrims); + auto paths = collectPaths(shadingVariant.subItemLimit, CATransform3D::identity(), true); for (const auto &path : paths) { compoundPath.appendPath(path.path.copyUsingTransform(path.transform)); } @@ -1083,21 +1129,20 @@ public: } _contentItem->shadings[i]->explicitPath = resultPaths; } else { - CompoundBezierPath compoundPath; - auto paths = collectPaths(shadingVariant.subItemLimit, CATransform3D::identity(), true, hasTrims); - for (const auto &path : paths) { - compoundPath.appendPath(path.path.copyUsingTransform(path.transform)); - } - std::vector resultPaths; - for (const auto &path : compoundPath.paths) { - resultPaths.push_back(path); - } - - if (hasTrims) { + if (hasTrims()) { + CompoundBezierPath compoundPath; + auto paths = collectPaths(shadingVariant.subItemLimit, CATransform3D::identity(), true); + for (const auto &path : paths) { + compoundPath.appendPath(path.path.copyUsingTransform(path.transform)); + } + std::vector resultPaths; + for (const auto &path : compoundPath.paths) { + resultPaths.push_back(path); + } + _contentItem->shadings[i]->explicitPath = resultPaths; } else { _contentItem->shadings[i]->explicitPath = std::nullopt; - _contentItem->shadings[i]->explicitPath = resultPaths; } } } @@ -1288,18 +1333,18 @@ CompositionLayer(solidLayer, Vector2D::Zero()) { _contentTree = std::make_shared(solidLayer); } -void ShapeCompositionLayer::displayContentsWithFrame(double frame, bool forceUpdates) { +void ShapeCompositionLayer::displayContentsWithFrame(double frame, bool forceUpdates, BezierPathsBoundingBoxContext &boundingBoxContext) { _frameTime = frame; _frameTimeInitialized = true; - _contentTree->itemTree->updateFrame(_frameTime); + _contentTree->itemTree->updateFrame(_frameTime, boundingBoxContext); _contentTree->itemTree->updateContents(std::nullopt); } -std::shared_ptr ShapeCompositionLayer::renderTreeNode() { +std::shared_ptr ShapeCompositionLayer::renderTreeNode(BezierPathsBoundingBoxContext &boundingBoxContext) { if (!_frameTimeInitialized) { _frameTime = 0.0; _frameTimeInitialized = true; - _contentTree->itemTree->updateFrame(_frameTime); + _contentTree->itemTree->updateFrame(_frameTime, boundingBoxContext); _contentTree->itemTree->updateContents(std::nullopt); } @@ -1324,7 +1369,7 @@ std::shared_ptr ShapeCompositionLayer::renderTreeNode() { std::shared_ptr maskNode; bool invertMask = false; if (_matteLayer) { - maskNode = _matteLayer->renderTreeNode(); + maskNode = _matteLayer->renderTreeNode(boundingBoxContext); if (maskNode && _matteType.has_value() && _matteType.value() == MatteType::Invert) { invertMask = true; } @@ -1346,9 +1391,9 @@ std::shared_ptr ShapeCompositionLayer::renderTreeNode() { return _renderTreeNode; } -void ShapeCompositionLayer::updateRenderTree() { +void ShapeCompositionLayer::updateRenderTree(BezierPathsBoundingBoxContext &boundingBoxContext) { if (_matteLayer) { - _matteLayer->updateRenderTree(); + _matteLayer->updateRenderTree(boundingBoxContext); } _contentRenderTreeNode->_bounds = _contentsLayer->bounds(); diff --git a/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/MainThread/LayerContainers/CompLayers/ShapeCompositionLayer.hpp b/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/MainThread/LayerContainers/CompLayers/ShapeCompositionLayer.hpp index f36e4a241c..27fed46a4e 100644 --- a/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/MainThread/LayerContainers/CompLayers/ShapeCompositionLayer.hpp +++ b/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/MainThread/LayerContainers/CompLayers/ShapeCompositionLayer.hpp @@ -16,9 +16,9 @@ public: ShapeCompositionLayer(std::shared_ptr const &shapeLayer); ShapeCompositionLayer(std::shared_ptr const &solidLayer); - virtual void displayContentsWithFrame(double frame, bool forceUpdates) override; - virtual std::shared_ptr renderTreeNode() override; - virtual void updateRenderTree() override; + virtual void displayContentsWithFrame(double frame, bool forceUpdates, BezierPathsBoundingBoxContext &boundingBoxContext) override; + virtual std::shared_ptr renderTreeNode(BezierPathsBoundingBoxContext &boundingBoxContext) override; + virtual void updateRenderTree(BezierPathsBoundingBoxContext &boundingBoxContext) override; private: std::shared_ptr _contentTree; diff --git a/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/MainThread/LayerContainers/CompLayers/TextCompositionLayer.hpp b/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/MainThread/LayerContainers/CompLayers/TextCompositionLayer.hpp index d2b9076b2d..9d7e916705 100644 --- a/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/MainThread/LayerContainers/CompLayers/TextCompositionLayer.hpp +++ b/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/MainThread/LayerContainers/CompLayers/TextCompositionLayer.hpp @@ -42,7 +42,7 @@ public: _fontProvider = fontProvider; } - virtual void displayContentsWithFrame(double frame, bool forceUpdates) override { + virtual void displayContentsWithFrame(double frame, bool forceUpdates, BezierPathsBoundingBoxContext &boundingBoxContext) override { if (!_textDocument) { return; } diff --git a/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/MainThread/LayerContainers/MainThreadAnimationLayer.hpp b/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/MainThread/LayerContainers/MainThreadAnimationLayer.hpp index c3c26a5352..3d6f999f6b 100644 --- a/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/MainThread/LayerContainers/MainThreadAnimationLayer.hpp +++ b/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/MainThread/LayerContainers/MainThreadAnimationLayer.hpp @@ -103,7 +103,7 @@ public: newFrame = floor(newFrame); } for (const auto &layer : _animationLayers) { - layer->displayWithFrame(newFrame, false); + layer->displayWithFrame(newFrame, false, _boundingBoxContext); } } @@ -118,7 +118,7 @@ public: /// Forces the view to update its drawing. void forceDisplayUpdate() { for (const auto &layer : _animationLayers) { - layer->displayWithFrame(currentFrame(), true); + layer->displayWithFrame(currentFrame(), true, _boundingBoxContext); } } @@ -193,7 +193,7 @@ public: _currentFrame = currentFrame; for (size_t i = 0; i < _animationLayers.size(); i++) { - _animationLayers[i]->displayWithFrame(_currentFrame, false); + _animationLayers[i]->displayWithFrame(_currentFrame, false, _boundingBoxContext); } } @@ -230,7 +230,7 @@ public: } } if (found) { - auto node = animationLayer->renderTreeNode(); + auto node = animationLayer->renderTreeNode(_boundingBoxContext); if (node) { subnodes.push_back(node); } @@ -264,7 +264,7 @@ public: } } if (found) { - animationLayer->updateRenderTree(); + animationLayer->updateRenderTree(_boundingBoxContext); } } } @@ -288,6 +288,8 @@ private: std::shared_ptr _layerFontProvider; std::shared_ptr _renderTreeNode; + + BezierPathsBoundingBoxContext _boundingBoxContext; }; } diff --git a/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/Utility/Primitives/BezierPath.cpp b/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/Utility/Primitives/BezierPath.cpp index 5985865ef2..b5c6065ecd 100644 --- a/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/Utility/Primitives/BezierPath.cpp +++ b/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/Utility/Primitives/BezierPath.cpp @@ -582,6 +582,58 @@ CGRect bezierPathsBoundingBoxParallel(BezierPathsBoundingBoxContext &context, st return calculateBoundingRectOpt(pointsX, pointsY, pointCount); } +CGRect bezierPathsBoundingBoxParallel(BezierPathsBoundingBoxContext &context, BezierPath const &path) { + int pointCount = 0; + + float *pointsX = context.pointsX; + float *pointsY = context.pointsY; + int pointsSize = context.pointsSize; + + PathElement const *pathElements = path.elements().data(); + int pathElementCount = (int)path.elements().size(); + + for (int i = 0; i < pathElementCount; i++) { + const auto &element = pathElements[i]; + + if (pointsSize < pointCount + 1) { + pointsSize = (pointCount + 1) * 2; + pointsX = (float *)realloc(pointsX, pointsSize * 4); + pointsY = (float *)realloc(pointsY, pointsSize * 4); + } + pointsX[pointCount] = (float)element.vertex.point.x; + pointsY[pointCount] = (float)element.vertex.point.y; + pointCount++; + + if (i != 0) { + const auto &previousElement = pathElements[i - 1]; + if (previousElement.vertex.outTangentRelative().isZero() && element.vertex.inTangentRelative().isZero()) { + } else { + if (pointsSize < pointCount + 1) { + pointsSize = (pointCount + 2) * 2; + pointsX = (float *)realloc(pointsX, pointsSize * 4); + pointsY = (float *)realloc(pointsY, pointsSize * 4); + } + pointsX[pointCount] = (float)previousElement.vertex.outTangent.x; + pointsY[pointCount] = (float)previousElement.vertex.outTangent.y; + pointCount++; + pointsX[pointCount] = (float)element.vertex.inTangent.x; + pointsY[pointCount] = (float)element.vertex.inTangent.y; + pointCount++; + } + } + } + + context.pointsX = pointsX; + context.pointsY = pointsY; + context.pointsSize = pointsSize; + + if (pointCount == 0) { + return CGRect(0.0, 0.0, 0.0, 0.0); + } + + return calculateBoundingRectOpt(pointsX, pointsY, pointCount); +} + CGRect bezierPathsBoundingBox(std::vector const &paths) { int pointCount = 0; From bbb74b6bdf49db2fd7d51d66f2273f30af5bd3ce Mon Sep 17 00:00:00 2001 From: Isaac <> Date: Fri, 17 May 2024 11:16:24 +0400 Subject: [PATCH 10/14] Lottie --- .../Sources/SoftwareLottieRenderer.mm | 4 ++++ Tests/LottieMetalTest/Sources/ViewController.swift | 4 ++-- .../LayerContainers/CompLayers/ShapeCompositionLayer.cpp | 9 --------- 3 files changed, 6 insertions(+), 11 deletions(-) diff --git a/Tests/LottieMetalTest/SoftwareLottieRenderer/Sources/SoftwareLottieRenderer.mm b/Tests/LottieMetalTest/SoftwareLottieRenderer/Sources/SoftwareLottieRenderer.mm index 5e5320cf47..90dd8f4c1e 100644 --- a/Tests/LottieMetalTest/SoftwareLottieRenderer/Sources/SoftwareLottieRenderer.mm +++ b/Tests/LottieMetalTest/SoftwareLottieRenderer/Sources/SoftwareLottieRenderer.mm @@ -657,6 +657,10 @@ CGRect getPathNativeBoundingBox(CGPathRef _Nonnull path) { return nil; } + if (!useReferenceRendering) { + return nil; + } + processRenderTree(renderNode, lottie::Vector2D((int)size.width, (int)size.height), lottie::CATransform3D::identity().scaled(lottie::Vector2D(size.width / (double)animation.size.width, size.height / (double)animation.size.height)), false, *_bezierPathsBoundingBoxContext.get()); if (useReferenceRendering) { diff --git a/Tests/LottieMetalTest/Sources/ViewController.swift b/Tests/LottieMetalTest/Sources/ViewController.swift index 599c888d16..ff9c7fe159 100644 --- a/Tests/LottieMetalTest/Sources/ViewController.swift +++ b/Tests/LottieMetalTest/Sources/ViewController.swift @@ -119,7 +119,7 @@ public final class ViewController: UIViewController { self.view.layer.addSublayer(MetalEngine.shared.rootLayer) - if "".isEmpty { + if !"".isEmpty { if #available(iOS 13.0, *) { self.test = ReferenceCompareTest(view: self.view) } @@ -167,7 +167,7 @@ public final class ViewController: UIViewController { var frameIndex = 0 while true { animationContainer.update(frameIndex) - //let _ = animationRenderer.render(for: CGSize(width: CGFloat(performanceFrameSize), height: CGFloat(performanceFrameSize)), useReferenceRendering: false) + let _ = animationRenderer.render(for: CGSize(width: CGFloat(performanceFrameSize), height: CGFloat(performanceFrameSize)), useReferenceRendering: false) frameIndex = (frameIndex + 1) % animationContainer.animation.frameCount numUpdates += 1 let timestamp = CFAbsoluteTimeGetCurrent() diff --git a/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/MainThread/LayerContainers/CompLayers/ShapeCompositionLayer.cpp b/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/MainThread/LayerContainers/CompLayers/ShapeCompositionLayer.cpp index 02fb2d5665..e9b2afb90e 100644 --- a/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/MainThread/LayerContainers/CompLayers/ShapeCompositionLayer.cpp +++ b/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Private/MainThread/LayerContainers/CompLayers/ShapeCompositionLayer.cpp @@ -1403,15 +1403,6 @@ void ShapeCompositionLayer::updateRenderTree(BezierPathsBoundingBoxContext &boun _contentRenderTreeNode->_masksToBounds = _contentsLayer->masksToBounds(); _contentRenderTreeNode->_isHidden = _contentsLayer->isHidden(); - assert(position() == Vector2D::Zero()); - assert(transform().isIdentity()); - assert(opacity() == 1.0); - assert(!masksToBounds()); - assert(!isHidden()); - assert(_contentsLayer->bounds() == CGRect(0.0, 0.0, 0.0, 0.0)); - assert(_contentsLayer->position() == Vector2D::Zero()); - assert(!_contentsLayer->masksToBounds()); - _renderTreeNode->_bounds = bounds(); _renderTreeNode->_position = position(); _renderTreeNode->_transform = transform(); From 378b7e8ed5433ef0ca1e0fd728c54faf8328a8b6 Mon Sep 17 00:00:00 2001 From: Isaac <> Date: Fri, 17 May 2024 17:10:19 +0400 Subject: [PATCH 11/14] Message preview improvements --- .../Sources/AccountContext.swift | 24 +- .../Sources/ContactSelectionController.swift | 2 +- .../Sources/PeerSelectionController.swift | 2 +- .../AttachmentTextInputPanelNode.swift | 2 +- .../Sources/AttachmentController.swift | 22 +- .../Sources/AttachmentPanel.swift | 24 +- .../Sources/CallListController.swift | 2 +- ...ChatSendMessageActionSheetController.swift | 34 +- .../ChatSendMessageContextScreen.swift | 71 +- .../Sources/MessageItemView.swift | 32 +- .../Sources/CreatePollController.swift | 15 +- .../Sources/LocationPickerController.swift | 15 +- .../Sources/MediaPickerScreen.swift | 153 +++- .../Sources/MediaPickerSelectedListNode.swift | 13 +- .../Sources/DeviceContactInfoController.swift | 2 +- .../Messages/StoryListContext.swift | 2 +- ...eBubbleContentCalclulateImageCorners.swift | 4 +- .../Sources/ChatMessageBubbleItemNode.swift | 19 +- .../ChatMessageContactBubbleContentNode.swift | 14 +- .../ChatSendAudioMessageContextPreview.swift | 174 ++++ .../Sources/PeerInfoPaneContainerNode.swift | 2 +- .../Sources/PeerInfoStoryGridScreen.swift | 1 - .../Sources/StorySearchGridScreen.swift | 261 ++++++ .../Sources/PeerInfoStoryPaneNode.swift | 771 +++++++++++------- .../Sources/PeerSelectionController.swift | 2 +- .../Sources/PeerSelectionControllerNode.swift | 2 +- .../Sources/PremiumGiftAttachmentScreen.swift | 15 +- .../Sources/ThemeColorsGridController.swift | 15 +- ...StoryItemSetContainerViewSendMessage.swift | 42 +- .../Sources/AttachmentFileController.swift | 15 +- .../Sources/Chat/ChatControllerPaste.swift | 4 +- ...ChatMessageDisplaySendMessageOptions.swift | 12 +- .../TelegramUI/Sources/ChatController.swift | 21 +- .../ChatControllerOpenAttachmentMenu.swift | 47 +- .../Sources/ComposeController.swift | 2 +- .../Sources/ContactSelectionController.swift | 51 +- .../ContactSelectionControllerNode.swift | 4 +- .../Sources/SharedAccountContext.swift | 4 + .../Sources/WebSearchController.swift | 25 +- .../WebUI/Sources/WebAppController.swift | 15 +- 40 files changed, 1459 insertions(+), 478 deletions(-) create mode 100644 submodules/TelegramUI/Components/PeerInfo/PeerInfoStoryGridScreen/Sources/StorySearchGridScreen.swift diff --git a/submodules/AccountContext/Sources/AccountContext.swift b/submodules/AccountContext/Sources/AccountContext.swift index 604caf29b5..197a61946f 100644 --- a/submodules/AccountContext/Sources/AccountContext.swift +++ b/submodules/AccountContext/Sources/AccountContext.swift @@ -969,6 +969,7 @@ public protocol SharedAccountContext: AnyObject { func makeAttachmentFileController(context: AccountContext, updatedPresentationData: (initial: PresentationData, signal: Signal)?, bannedSendMedia: (Int32, Bool)?, presentGallery: @escaping () -> Void, presentFiles: @escaping () -> Void, send: @escaping (AnyMediaReference) -> Void) -> AttachmentFileController func makeGalleryCaptionPanelView(context: AccountContext, chatLocation: ChatLocation, isScheduledMessages: Bool, isFile: Bool, customEmojiAvailable: Bool, present: @escaping (ViewController) -> Void, presentInGlobalOverlay: @escaping (ViewController) -> Void) -> NSObject? func makeHashtagSearchController(context: AccountContext, peer: EnginePeer?, query: String, all: Bool) -> ViewController + func makeStorySearchController(context: AccountContext, query: String) -> ViewController func makeMyStoriesController(context: AccountContext, isArchive: Bool) -> ViewController func makeArchiveSettingsController(context: AccountContext) -> ViewController func makeFilterSettingsController(context: AccountContext, modal: Bool, scrollToTags: Bool, dismissed: (() -> Void)?) -> ViewController @@ -1069,11 +1070,24 @@ public protocol AccountGroupCallContext: AnyObject { public protocol AccountGroupCallContextCache: AnyObject { } -public final class ChatSendMessageActionSheetControllerMessageEffect { - public let id: Int64 +public struct ChatSendMessageActionSheetControllerSendParameters { + public struct Effect { + public let id: Int64 + + public init(id: Int64) { + self.id = id + } + } - public init(id: Int64) { - self.id = id + public var effect: Effect? + public var textIsAboveMedia: Bool + + public init( + effect: Effect?, + textIsAboveMedia: Bool + ) { + self.effect = effect + self.textIsAboveMedia = textIsAboveMedia } } @@ -1089,7 +1103,7 @@ public protocol ChatSendMessageActionSheetControllerSourceSendButtonNode: ASDisp public protocol ChatSendMessageActionSheetController: ViewController { typealias SendMode = ChatSendMessageActionSheetControllerSendMode - typealias MessageEffect = ChatSendMessageActionSheetControllerMessageEffect + typealias SendParameters = ChatSendMessageActionSheetControllerSendParameters } public protocol AccountContext: AnyObject { diff --git a/submodules/AccountContext/Sources/ContactSelectionController.swift b/submodules/AccountContext/Sources/ContactSelectionController.swift index ac7907088c..19d4c5c60a 100644 --- a/submodules/AccountContext/Sources/ContactSelectionController.swift +++ b/submodules/AccountContext/Sources/ContactSelectionController.swift @@ -3,7 +3,7 @@ import Display import SwiftSignalKit public protocol ContactSelectionController: ViewController { - var result: Signal<([ContactListPeer], ContactListAction, Bool, Int32?, NSAttributedString?)?, NoError> { get } + var result: Signal<([ContactListPeer], ContactListAction, Bool, Int32?, NSAttributedString?, ChatSendMessageActionSheetController.SendParameters?)?, NoError> { get } var displayProgress: Bool { get set } var dismissed: (() -> Void)? { get set } var presentScheduleTimePicker: (@escaping (Int32) -> Void) -> Void { get set } diff --git a/submodules/AccountContext/Sources/PeerSelectionController.swift b/submodules/AccountContext/Sources/PeerSelectionController.swift index 3b29588a52..7230f902ce 100644 --- a/submodules/AccountContext/Sources/PeerSelectionController.swift +++ b/submodules/AccountContext/Sources/PeerSelectionController.swift @@ -147,7 +147,7 @@ public enum PeerSelectionControllerContext { public protocol PeerSelectionController: ViewController { var peerSelected: ((EnginePeer, Int64?) -> Void)? { get set } - var multiplePeersSelected: (([EnginePeer], [EnginePeer.Id: EnginePeer], NSAttributedString, AttachmentTextInputPanelSendMode, ChatInterfaceForwardOptionsState?, ChatSendMessageActionSheetController.MessageEffect?) -> Void)? { get set } + var multiplePeersSelected: (([EnginePeer], [EnginePeer.Id: EnginePeer], NSAttributedString, AttachmentTextInputPanelSendMode, ChatInterfaceForwardOptionsState?, ChatSendMessageActionSheetController.SendParameters?) -> Void)? { get set } var inProgress: Bool { get set } var customDismiss: (() -> Void)? { get set } } diff --git a/submodules/AttachmentTextInputPanelNode/Sources/AttachmentTextInputPanelNode.swift b/submodules/AttachmentTextInputPanelNode/Sources/AttachmentTextInputPanelNode.swift index 7f0f36aa49..4dd48fa2dd 100644 --- a/submodules/AttachmentTextInputPanelNode/Sources/AttachmentTextInputPanelNode.swift +++ b/submodules/AttachmentTextInputPanelNode/Sources/AttachmentTextInputPanelNode.swift @@ -269,7 +269,7 @@ public class AttachmentTextInputPanelNode: ASDisplayNode, TGCaptionPanelView, AS private var validLayout: (CGFloat, CGFloat, CGFloat, UIEdgeInsets, CGFloat, LayoutMetrics, Bool)? - public var sendMessage: (AttachmentTextInputPanelSendMode, ChatSendMessageActionSheetController.MessageEffect?) -> Void = { _, _ in } + public var sendMessage: (AttachmentTextInputPanelSendMode, ChatSendMessageActionSheetController.SendParameters?) -> Void = { _, _ in } public var updateHeight: (Bool) -> Void = { _ in } private var updatingInputState = false diff --git a/submodules/AttachmentUI/Sources/AttachmentController.swift b/submodules/AttachmentUI/Sources/AttachmentController.swift index 5c6ff18ef3..4043453c91 100644 --- a/submodules/AttachmentUI/Sources/AttachmentController.swift +++ b/submodules/AttachmentUI/Sources/AttachmentController.swift @@ -154,14 +154,18 @@ public protocol AttachmentMediaPickerContext { var selectionCount: Signal { get } var caption: Signal { get } + var hasCaption: Bool { get } + var captionIsAboveMedia: Signal { get } + func setCaptionIsAboveMedia(_ captionIsAboveMedia: Bool) -> Void + var loadingProgress: Signal { get } var mainButtonState: Signal { get } func mainButtonAction() func setCaption(_ caption: NSAttributedString) - func send(mode: AttachmentMediaPickerSendMode, attachmentMode: AttachmentMediaPickerAttachmentMode, messageEffect: ChatSendMessageActionSheetController.MessageEffect?) - func schedule(messageEffect: ChatSendMessageActionSheetController.MessageEffect?) + func send(mode: AttachmentMediaPickerSendMode, attachmentMode: AttachmentMediaPickerAttachmentMode, parameters: ChatSendMessageActionSheetController.SendParameters?) + func schedule(parameters: ChatSendMessageActionSheetController.SendParameters?) } private func generateShadowImage() -> UIImage? { @@ -249,7 +253,7 @@ public class AttachmentController: ViewController { private var selectionCount: Int = 0 - fileprivate var mediaPickerContext: AttachmentMediaPickerContext? { + var mediaPickerContext: AttachmentMediaPickerContext? { didSet { if let mediaPickerContext = self.mediaPickerContext { self.captionDisposable.set((mediaPickerContext.caption @@ -317,7 +321,7 @@ public class AttachmentController: ViewController { self.container = AttachmentContainer() self.container.canHaveKeyboardFocus = true - self.panel = AttachmentPanel(context: controller.context, chatLocation: controller.chatLocation, isScheduledMessages: controller.isScheduledMessages, updatedPresentationData: controller.updatedPresentationData, makeEntityInputView: makeEntityInputView) + self.panel = AttachmentPanel(controller: controller, context: controller.context, chatLocation: controller.chatLocation, isScheduledMessages: controller.isScheduledMessages, updatedPresentationData: controller.updatedPresentationData, makeEntityInputView: makeEntityInputView) self.panel.fromMenu = controller.fromMenu self.panel.isStandalone = controller.isStandalone @@ -423,17 +427,17 @@ public class AttachmentController: ViewController { } } - self.panel.sendMessagePressed = { [weak self] mode, messageEffect in + self.panel.sendMessagePressed = { [weak self] mode, parameters in if let strongSelf = self { switch mode { case .generic: - strongSelf.mediaPickerContext?.send(mode: .generic, attachmentMode: .media, messageEffect: messageEffect) + strongSelf.mediaPickerContext?.send(mode: .generic, attachmentMode: .media, parameters: parameters) case .silent: - strongSelf.mediaPickerContext?.send(mode: .silently, attachmentMode: .media, messageEffect: messageEffect) + strongSelf.mediaPickerContext?.send(mode: .silently, attachmentMode: .media, parameters: parameters) case .schedule: - strongSelf.mediaPickerContext?.schedule(messageEffect: messageEffect) + strongSelf.mediaPickerContext?.schedule(parameters: parameters) case .whenOnline: - strongSelf.mediaPickerContext?.send(mode: .whenOnline, attachmentMode: .media, messageEffect: messageEffect) + strongSelf.mediaPickerContext?.send(mode: .whenOnline, attachmentMode: .media, parameters: parameters) } } } diff --git a/submodules/AttachmentUI/Sources/AttachmentPanel.swift b/submodules/AttachmentUI/Sources/AttachmentPanel.swift index b62a13ff69..e164b62c13 100644 --- a/submodules/AttachmentUI/Sources/AttachmentPanel.swift +++ b/submodules/AttachmentUI/Sources/AttachmentPanel.swift @@ -683,6 +683,7 @@ private final class MainButtonNode: HighlightTrackingButtonNode { } final class AttachmentPanel: ASDisplayNode, ASScrollViewDelegate { + private weak var controller: AttachmentController? private let context: AccountContext private let isScheduledMessages: Bool private var presentationData: PresentationData @@ -729,7 +730,7 @@ final class AttachmentPanel: ASDisplayNode, ASScrollViewDelegate { var beganTextEditing: () -> Void = {} var textUpdated: (NSAttributedString) -> Void = { _ in } - var sendMessagePressed: (AttachmentTextInputPanelSendMode, ChatSendMessageActionSheetController.MessageEffect?) -> Void = { _, _ in } + var sendMessagePressed: (AttachmentTextInputPanelSendMode, ChatSendMessageActionSheetController.SendParameters?) -> Void = { _, _ in } var requestLayout: () -> Void = {} var present: (ViewController) -> Void = { _ in } var presentInGlobalOverlay: (ViewController) -> Void = { _ in } @@ -738,7 +739,8 @@ final class AttachmentPanel: ASDisplayNode, ASScrollViewDelegate { var mainButtonPressed: () -> Void = { } - init(context: AccountContext, chatLocation: ChatLocation?, isScheduledMessages: Bool, updatedPresentationData: (initial: PresentationData, signal: Signal)?, makeEntityInputView: @escaping () -> AttachmentTextInputPanelInputView?) { + init(controller: AttachmentController, context: AccountContext, chatLocation: ChatLocation?, isScheduledMessages: Bool, updatedPresentationData: (initial: PresentationData, signal: Signal)?, makeEntityInputView: @escaping () -> AttachmentTextInputPanelInputView?) { + self.controller = controller self.context = context self.updatedPresentationData = updatedPresentationData self.presentationData = updatedPresentationData?.initial ?? context.sharedContext.currentPresentationData.with { $0 } @@ -982,8 +984,16 @@ final class AttachmentPanel: ASDisplayNode, ASScrollViewDelegate { isReady = .single(true) } - let _ = (isReady - |> deliverOnMainQueue).start(next: { [weak strongSelf] _ in + var captionIsAboveMedia: Signal = .single(false) + if let controller = strongSelf.controller, let mediaPickerContext = controller.mediaPickerContext { + captionIsAboveMedia = mediaPickerContext.captionIsAboveMedia + } + + let _ = (combineLatest( + isReady, + captionIsAboveMedia |> take(1) + ) + |> deliverOnMainQueue).start(next: { [weak strongSelf] _, captionIsAboveMedia in guard let strongSelf else { return } @@ -998,7 +1008,11 @@ final class AttachmentPanel: ASDisplayNode, ASScrollViewDelegate { sourceSendButton: node, textInputView: textInputNode.textView, mediaPreview: mediaPreview, - mediaCaptionIsAbove: (false, { _ in + mediaCaptionIsAbove: (captionIsAboveMedia, { [weak strongSelf] value in + guard let strongSelf, let controller = strongSelf.controller, let mediaPickerContext = controller.mediaPickerContext else { + return + } + mediaPickerContext.setCaptionIsAboveMedia(value) }), emojiViewProvider: textInputPanelNode.emojiViewProvider, attachment: true, diff --git a/submodules/CallListUI/Sources/CallListController.swift b/submodules/CallListUI/Sources/CallListController.swift index 160441d7e0..45cd61a894 100644 --- a/submodules/CallListUI/Sources/CallListController.swift +++ b/submodules/CallListUI/Sources/CallListController.swift @@ -389,7 +389,7 @@ public final class CallListController: TelegramBaseController { |> take(1) |> deliverOnMainQueue).startStrict(next: { [weak controller, weak self] result in controller?.dismissSearch() - if let strongSelf = self, let (contactPeers, action, _, _, _) = result, let contactPeer = contactPeers.first, case let .peer(peer, _, _) = contactPeer { + if let strongSelf = self, let (contactPeers, action, _, _, _, _) = result, let contactPeer = contactPeers.first, case let .peer(peer, _, _) = contactPeer { strongSelf.call(peer.id, isVideo: action == .videoCall, began: { if let strongSelf = self { let _ = (strongSelf.context.sharedContext.hasOngoingCall.get() diff --git a/submodules/ChatSendMessageActionUI/Sources/ChatSendMessageActionSheetController.swift b/submodules/ChatSendMessageActionUI/Sources/ChatSendMessageActionSheetController.swift index b60d00bfb2..c15f30cdd8 100644 --- a/submodules/ChatSendMessageActionUI/Sources/ChatSendMessageActionSheetController.swift +++ b/submodules/ChatSendMessageActionUI/Sources/ChatSendMessageActionSheetController.swift @@ -29,8 +29,8 @@ private final class ChatSendMessageActionSheetControllerImpl: ViewController, Ch private let attachment: Bool private let canSendWhenOnline: Bool private let completion: () -> Void - private let sendMessage: (SendMode, MessageEffect?) -> Void - private let schedule: (MessageEffect?) -> Void + private let sendMessage: (SendMode, SendParameters?) -> Void + private let schedule: (SendParameters?) -> Void private let reactionItems: [ReactionItem]? private var presentationData: PresentationData @@ -44,7 +44,7 @@ private final class ChatSendMessageActionSheetControllerImpl: ViewController, Ch private let emojiViewProvider: ((ChatTextInputTextCustomEmojiAttribute) -> UIView)? - public init(context: AccountContext, updatedPresentationData: (initial: PresentationData, signal: Signal)? = nil, peerId: EnginePeer.Id?, isScheduledMessages: Bool = false, forwardMessageIds: [EngineMessage.Id]?, hasEntityKeyboard: Bool, gesture: ContextGesture, sourceSendButton: ASDisplayNode, textInputView: UITextView, emojiViewProvider: ((ChatTextInputTextCustomEmojiAttribute) -> UIView)?, attachment: Bool = false, canSendWhenOnline: Bool, completion: @escaping () -> Void, sendMessage: @escaping (SendMode, MessageEffect?) -> Void, schedule: @escaping (MessageEffect?) -> Void, reactionItems: [ReactionItem]? = nil) { + public init(context: AccountContext, updatedPresentationData: (initial: PresentationData, signal: Signal)? = nil, peerId: EnginePeer.Id?, isScheduledMessages: Bool = false, forwardMessageIds: [EngineMessage.Id]?, hasEntityKeyboard: Bool, gesture: ContextGesture, sourceSendButton: ASDisplayNode, textInputView: UITextView, emojiViewProvider: ((ChatTextInputTextCustomEmojiAttribute) -> UIView)?, attachment: Bool = false, canSendWhenOnline: Bool, completion: @escaping () -> Void, sendMessage: @escaping (SendMode, SendParameters?) -> Void, schedule: @escaping (SendParameters?) -> Void, reactionItems: [ReactionItem]? = nil) { self.context = context self.peerId = peerId self.isScheduledMessages = isScheduledMessages @@ -108,32 +108,16 @@ private final class ChatSendMessageActionSheetControllerImpl: ViewController, Ch } self.displayNode = ChatSendMessageActionSheetControllerNode(context: self.context, presentationData: self.presentationData, reminders: reminders, gesture: gesture, sourceSendButton: self.sourceSendButton, textInputView: self.textInputView, attachment: self.attachment, canSendWhenOnline: self.canSendWhenOnline, forwardedCount: forwardedCount, hasEntityKeyboard: self.hasEntityKeyboard, emojiViewProvider: self.emojiViewProvider, send: { [weak self] in - var messageEffect: MessageEffect? - if let selectedEffect = self?.controllerNode.selectedMessageEffect { - messageEffect = MessageEffect(id: selectedEffect.id) - } - self?.sendMessage(.generic, messageEffect) + self?.sendMessage(.generic, nil) self?.dismiss(cancel: false) }, sendSilently: { [weak self] in - var messageEffect: MessageEffect? - if let selectedEffect = self?.controllerNode.selectedMessageEffect { - messageEffect = MessageEffect(id: selectedEffect.id) - } - self?.sendMessage(.silently, messageEffect) + self?.sendMessage(.silently, nil) self?.dismiss(cancel: false) }, sendWhenOnline: { [weak self] in - var messageEffect: MessageEffect? - if let selectedEffect = self?.controllerNode.selectedMessageEffect { - messageEffect = MessageEffect(id: selectedEffect.id) - } - self?.sendMessage(.whenOnline, messageEffect) + self?.sendMessage(.whenOnline, nil) self?.dismiss(cancel: false) }, schedule: !canSchedule ? nil : { [weak self] in - var messageEffect: MessageEffect? - if let selectedEffect = self?.controllerNode.selectedMessageEffect { - messageEffect = MessageEffect(id: selectedEffect.id) - } - self?.schedule(messageEffect) + self?.schedule(nil) self?.dismiss(cancel: false) }, cancel: { [weak self] in self?.dismiss(cancel: true) @@ -191,8 +175,8 @@ public func makeChatSendMessageActionSheetController( attachment: Bool = false, canSendWhenOnline: Bool, completion: @escaping () -> Void, - sendMessage: @escaping (ChatSendMessageActionSheetController.SendMode, ChatSendMessageActionSheetController.MessageEffect?) -> Void, - schedule: @escaping (ChatSendMessageActionSheetController.MessageEffect?) -> Void, + sendMessage: @escaping (ChatSendMessageActionSheetController.SendMode, ChatSendMessageActionSheetController.SendParameters?) -> Void, + schedule: @escaping (ChatSendMessageActionSheetController.SendParameters?) -> Void, reactionItems: [ReactionItem]? = nil, availableMessageEffects: AvailableMessageEffects? = nil, isPremium: Bool = false diff --git a/submodules/ChatSendMessageActionUI/Sources/ChatSendMessageContextScreen.swift b/submodules/ChatSendMessageActionUI/Sources/ChatSendMessageContextScreen.swift index 6ce29a2d88..ab6d60a507 100644 --- a/submodules/ChatSendMessageActionUI/Sources/ChatSendMessageContextScreen.swift +++ b/submodules/ChatSendMessageActionUI/Sources/ChatSendMessageContextScreen.swift @@ -69,8 +69,8 @@ final class ChatSendMessageContextScreenComponent: Component { let attachment: Bool let canSendWhenOnline: Bool let completion: () -> Void - let sendMessage: (ChatSendMessageActionSheetController.SendMode, ChatSendMessageActionSheetController.MessageEffect?) -> Void - let schedule: (ChatSendMessageActionSheetController.MessageEffect?) -> Void + let sendMessage: (ChatSendMessageActionSheetController.SendMode, ChatSendMessageActionSheetController.SendParameters?) -> Void + let schedule: (ChatSendMessageActionSheetController.SendParameters?) -> Void let reactionItems: [ReactionItem]? let availableMessageEffects: AvailableMessageEffects? let isPremium: Bool @@ -92,8 +92,8 @@ final class ChatSendMessageContextScreenComponent: Component { attachment: Bool, canSendWhenOnline: Bool, completion: @escaping () -> Void, - sendMessage: @escaping (ChatSendMessageActionSheetController.SendMode, ChatSendMessageActionSheetController.MessageEffect?) -> Void, - schedule: @escaping (ChatSendMessageActionSheetController.MessageEffect?) -> Void, + sendMessage: @escaping (ChatSendMessageActionSheetController.SendMode, ChatSendMessageActionSheetController.SendParameters?) -> Void, + schedule: @escaping (ChatSendMessageActionSheetController.SendParameters?) -> Void, reactionItems: [ReactionItem]?, availableMessageEffects: AvailableMessageEffects?, isPremium: Bool @@ -222,7 +222,13 @@ final class ChatSendMessageContextScreenComponent: Component { return } self.animateOutToEmpty = true - component.sendMessage(.generic, self.selectedMessageEffect.flatMap({ ChatSendMessageActionSheetController.MessageEffect(id: $0.id) })) + + let sendParameters = ChatSendMessageActionSheetController.SendParameters( + effect: self.selectedMessageEffect.flatMap({ ChatSendMessageActionSheetController.SendParameters.Effect(id: $0.id) }), + textIsAboveMedia: self.mediaCaptionIsAbove + ) + + component.sendMessage(.generic, sendParameters) self.environment?.controller()?.dismiss() } @@ -327,6 +333,13 @@ final class ChatSendMessageContextScreenComponent: Component { ) } + let textString: NSAttributedString + if let attributedText = component.textInputView.attributedText { + textString = attributedText + } else { + textString = NSAttributedString(string: " ", font: Font.regular(17.0), textColor: .black) + } + let sendButton: SendButton if let current = self.sendButton { sendButton = current @@ -365,7 +378,7 @@ final class ChatSendMessageContextScreenComponent: Component { } var items: [ContextMenuItem] = [] - if component.mediaCaptionIsAbove != nil { + if component.mediaCaptionIsAbove != nil, textString.length != 0, case .media = component.mediaPreview?.layoutType { //TODO:localize let mediaCaptionIsAbove = self.mediaCaptionIsAbove items.append(.action(ContextMenuActionItem( @@ -398,7 +411,13 @@ final class ChatSendMessageContextScreenComponent: Component { return } self.animateOutToEmpty = true - component.sendMessage(.silently, self.selectedMessageEffect.flatMap({ ChatSendMessageActionSheetController.MessageEffect(id: $0.id) })) + + let sendParameters = ChatSendMessageActionSheetController.SendParameters( + effect: self.selectedMessageEffect.flatMap({ ChatSendMessageActionSheetController.SendParameters.Effect(id: $0.id) }), + textIsAboveMedia: self.mediaCaptionIsAbove + ) + + component.sendMessage(.silently, sendParameters) self.environment?.controller()?.dismiss() } ))) @@ -414,7 +433,13 @@ final class ChatSendMessageContextScreenComponent: Component { return } self.animateOutToEmpty = true - component.sendMessage(.whenOnline, self.selectedMessageEffect.flatMap({ ChatSendMessageActionSheetController.MessageEffect(id: $0.id) })) + + let sendParameters = ChatSendMessageActionSheetController.SendParameters( + effect: self.selectedMessageEffect.flatMap({ ChatSendMessageActionSheetController.SendParameters.Effect(id: $0.id) }), + textIsAboveMedia: self.mediaCaptionIsAbove + ) + + component.sendMessage(.whenOnline, sendParameters) self.environment?.controller()?.dismiss() } ))) @@ -431,7 +456,13 @@ final class ChatSendMessageContextScreenComponent: Component { return } self.animateOutToEmpty = true - component.schedule(self.selectedMessageEffect.flatMap({ ChatSendMessageActionSheetController.MessageEffect(id: $0.id) })) + + let sendParameters = ChatSendMessageActionSheetController.SendParameters( + effect: self.selectedMessageEffect.flatMap({ ChatSendMessageActionSheetController.SendParameters.Effect(id: $0.id) }), + textIsAboveMedia: self.mediaCaptionIsAbove + ) + + component.schedule(sendParameters) self.environment?.controller()?.dismiss() } ))) @@ -499,13 +530,6 @@ final class ChatSendMessageContextScreenComponent: Component { self.addSubview(messageItemView) } - let textString: NSAttributedString - if let attributedText = component.textInputView.attributedText { - textString = attributedText - } else { - textString = NSAttributedString(string: " ", font: Font.regular(17.0), textColor: .black) - } - let localSourceTextInputViewFrame = convertFrame(component.textInputView.bounds, from: component.textInputView, to: self) let sourceMessageTextInsets = UIEdgeInsets(top: 7.0, left: 12.0, bottom: 6.0, right: 20.0) @@ -952,12 +976,19 @@ final class ChatSendMessageContextScreenComponent: Component { Transition.immediate.setScale(view: actionsStackNode.view, scale: 1.0) actionsStackNode.layer.animateSpring(from: 0.001 as NSNumber, to: 1.0 as NSNumber, keyPath: "transform.scale", duration: 0.42, damping: 104.0) - messageItemView.animateIn(transition: transition) + messageItemView.animateIn( + sourceTextInputView: component.textInputView as? ChatInputTextView, + transition: transition + ) case .animatedOut: transition.setAlpha(view: actionsStackNode.view, alpha: 0.0) transition.setScale(view: actionsStackNode.view, scale: 0.001) - messageItemView.animateOut(toEmpty: self.animateOutToEmpty, transition: transition) + messageItemView.animateOut( + sourceTextInputView: component.textInputView as? ChatInputTextView, + toEmpty: self.animateOutToEmpty, + transition: transition + ) } } else { switch self.presentationAnimationState { @@ -1141,8 +1172,8 @@ public class ChatSendMessageContextScreen: ViewControllerComponentContainer, Cha attachment: Bool, canSendWhenOnline: Bool, completion: @escaping () -> Void, - sendMessage: @escaping (ChatSendMessageActionSheetController.SendMode, ChatSendMessageActionSheetController.MessageEffect?) -> Void, - schedule: @escaping (ChatSendMessageActionSheetController.MessageEffect?) -> Void, + sendMessage: @escaping (ChatSendMessageActionSheetController.SendMode, ChatSendMessageActionSheetController.SendParameters?) -> Void, + schedule: @escaping (ChatSendMessageActionSheetController.SendParameters?) -> Void, reactionItems: [ReactionItem]?, availableMessageEffects: AvailableMessageEffects?, isPremium: Bool diff --git a/submodules/ChatSendMessageActionUI/Sources/MessageItemView.swift b/submodules/ChatSendMessageActionUI/Sources/MessageItemView.swift index a5aee8a88a..d4c296ee61 100644 --- a/submodules/ChatSendMessageActionUI/Sources/MessageItemView.swift +++ b/submodules/ChatSendMessageActionUI/Sources/MessageItemView.swift @@ -155,6 +155,7 @@ final class MessageItemView: UIView { private var chatTheme: ChatPresentationThemeData? private var currentSize: CGSize? + private var currentMediaCaptionIsAbove: Bool = false override init(frame: CGRect) { self.backgroundWallpaperNode = ChatMessageBubbleBackdrop() @@ -162,6 +163,7 @@ final class MessageItemView: UIView { self.backgroundNode.backdropNode = self.backgroundWallpaperNode self.textClippingContainer = UIView() + self.textClippingContainer.layer.anchorPoint = CGPoint() self.textClippingContainer.clipsToBounds = true super.init(frame: frame) @@ -178,13 +180,20 @@ final class MessageItemView: UIView { preconditionFailure() } - func animateIn(transition: Transition) { + func animateIn( + sourceTextInputView: ChatInputTextView?, + transition: Transition + ) { if let mediaPreview = self.mediaPreview { mediaPreview.animateIn(transition: transition) } } - func animateOut(toEmpty: Bool, transition: Transition) { + func animateOut( + sourceTextInputView: ChatInputTextView?, + toEmpty: Bool, + transition: Transition + ) { if let mediaPreview = self.mediaPreview { if toEmpty { mediaPreview.animateOutOnSend(transition: transition) @@ -266,6 +275,8 @@ final class MessageItemView: UIView { backgroundNode: backgroundNode ) + let alphaTransition: Transition = transition.animation.isImmediate ? .immediate : .easeInOut(duration: 0.25) + if let sourceMediaPreview { let mediaPreviewClippingView: UIView if let current = self.mediaPreviewClippingView { @@ -304,7 +315,11 @@ final class MessageItemView: UIView { let backgroundAlpha: CGFloat switch sourceMediaPreview.layoutType { case .media: - backgroundAlpha = explicitBackgroundSize != nil ? 0.0 : 1.0 + if textString.length != 0 { + backgroundAlpha = explicitBackgroundSize != nil ? 0.0 : 1.0 + } else { + backgroundAlpha = 0.0 + } case .message, .videoMessage: backgroundAlpha = 0.0 } @@ -312,7 +327,7 @@ final class MessageItemView: UIView { var backgroundFrame = mediaPreviewFrame.insetBy(dx: -2.0, dy: -2.0) backgroundFrame.size.width += 6.0 - if textString.length != 0 { + if textString.length != 0, case .media = sourceMediaPreview.layoutType { let textNode: ChatInputTextNode if let current = self.textNode { textNode = current @@ -414,9 +429,10 @@ final class MessageItemView: UIView { textClippingContainerBounds.origin.y = max(0.0, textClippingContainerBounds.origin.y) } - transition.setPosition(view: self.textClippingContainer, position: textClippingContainerFrame.center) + transition.setPosition(view: self.textClippingContainer, position: textClippingContainerFrame.origin) transition.setBounds(view: self.textClippingContainer, bounds: textClippingContainerBounds) + alphaTransition.setAlpha(view: textNode.view, alpha: backgroundAlpha) transition.setFrame(view: textNode.view, frame: CGRect(origin: CGPoint(x: textFrame.minX + textPositioningInsets.left - textClippingContainerFrame.minX, y: textFrame.minY + textPositioningInsets.top - textClippingContainerFrame.minY), size: CGSize(width: maxTextWidth, height: textHeight))) self.updateTextContents() } @@ -424,10 +440,10 @@ final class MessageItemView: UIView { transition.setFrame(view: sourceMediaPreview.view, frame: mediaPreviewFrame) transition.setFrame(view: self.backgroundWallpaperNode.view, frame: CGRect(origin: CGPoint(), size: backgroundFrame.size)) - transition.setAlpha(view: self.backgroundWallpaperNode.view, alpha: backgroundAlpha) + alphaTransition.setAlpha(view: self.backgroundWallpaperNode.view, alpha: backgroundAlpha) self.backgroundWallpaperNode.updateFrame(backgroundFrame, transition: transition.containedViewLayoutTransition) transition.setFrame(view: self.backgroundNode.view, frame: backgroundFrame) - transition.setAlpha(view: self.backgroundNode.view, alpha: backgroundAlpha) + alphaTransition.setAlpha(view: self.backgroundNode.view, alpha: backgroundAlpha) self.backgroundNode.updateLayout(size: backgroundFrame.size, transition: transition.containedViewLayoutTransition) if let effectIcon = self.effectIcon, let effectIconSize { @@ -598,7 +614,7 @@ final class MessageItemView: UIView { textClippingContainerBounds.origin.y = max(0.0, textClippingContainerBounds.origin.y) } - transition.setPosition(view: self.textClippingContainer, position: textClippingContainerFrame.center) + transition.setPosition(view: self.textClippingContainer, position: textClippingContainerFrame.origin) transition.setBounds(view: self.textClippingContainer, bounds: textClippingContainerBounds) textNode.view.frame = CGRect(origin: CGPoint(x: textFrame.minX + textPositioningInsets.left - textClippingContainerFrame.minX, y: textFrame.minY + textPositioningInsets.top - textClippingContainerFrame.minY), size: CGSize(width: maxTextWidth, height: textHeight)) diff --git a/submodules/ComposePollUI/Sources/CreatePollController.swift b/submodules/ComposePollUI/Sources/CreatePollController.swift index 9cb0a04ed7..42b9f20a86 100644 --- a/submodules/ComposePollUI/Sources/CreatePollController.swift +++ b/submodules/ComposePollUI/Sources/CreatePollController.swift @@ -538,6 +538,17 @@ private final class CreatePollContext: AttachmentMediaPickerContext { return .single(nil) } + var captionIsAboveMedia: Signal { + return .single(false) + } + + var hasCaption: Bool { + return false + } + + func setCaptionIsAboveMedia(_ captionIsAboveMedia: Bool) -> Void { + } + public var loadingProgress: Signal { return .single(nil) } @@ -549,10 +560,10 @@ private final class CreatePollContext: AttachmentMediaPickerContext { func setCaption(_ caption: NSAttributedString) { } - func send(mode: AttachmentMediaPickerSendMode, attachmentMode: AttachmentMediaPickerAttachmentMode, messageEffect: ChatSendMessageActionSheetController.MessageEffect?) { + func send(mode: AttachmentMediaPickerSendMode, attachmentMode: AttachmentMediaPickerAttachmentMode, parameters: ChatSendMessageActionSheetController.SendParameters?) { } - func schedule(messageEffect: ChatSendMessageActionSheetController.MessageEffect?) { + func schedule(parameters: ChatSendMessageActionSheetController.SendParameters?) { } func mainButtonAction() { diff --git a/submodules/LocationUI/Sources/LocationPickerController.swift b/submodules/LocationUI/Sources/LocationPickerController.swift index cdde271e86..61f0ad4f4d 100644 --- a/submodules/LocationUI/Sources/LocationPickerController.swift +++ b/submodules/LocationUI/Sources/LocationPickerController.swift @@ -393,6 +393,17 @@ private final class LocationPickerContext: AttachmentMediaPickerContext { return .single(nil) } + var hasCaption: Bool { + return false + } + + var captionIsAboveMedia: Signal { + return .single(false) + } + + func setCaptionIsAboveMedia(_ captionIsAboveMedia: Bool) -> Void { + } + public var loadingProgress: Signal { return .single(nil) } @@ -404,10 +415,10 @@ private final class LocationPickerContext: AttachmentMediaPickerContext { func setCaption(_ caption: NSAttributedString) { } - func send(mode: AttachmentMediaPickerSendMode, attachmentMode: AttachmentMediaPickerAttachmentMode, messageEffect: ChatSendMessageActionSheetController.MessageEffect?) { + func send(mode: AttachmentMediaPickerSendMode, attachmentMode: AttachmentMediaPickerAttachmentMode, parameters: ChatSendMessageActionSheetController.SendParameters?) { } - func schedule(messageEffect: ChatSendMessageActionSheetController.MessageEffect?) { + func schedule(parameters: ChatSendMessageActionSheetController.SendParameters?) { } func mainButtonAction() { diff --git a/submodules/MediaPickerUI/Sources/MediaPickerScreen.swift b/submodules/MediaPickerUI/Sources/MediaPickerScreen.swift index e23df7541e..16b2ffffab 100644 --- a/submodules/MediaPickerUI/Sources/MediaPickerScreen.swift +++ b/submodules/MediaPickerUI/Sources/MediaPickerScreen.swift @@ -33,14 +33,23 @@ final class MediaPickerInteraction { let openSelectedMedia: (TGMediaSelectableItem, UIImage?) -> Void let openDraft: (MediaEditorDraft, UIImage?) -> Void let toggleSelection: (TGMediaSelectableItem, Bool, Bool) -> Bool - let sendSelected: (TGMediaSelectableItem?, Bool, Int32?, Bool, ChatSendMessageActionSheetController.MessageEffect?, @escaping () -> Void) -> Void - let schedule: (ChatSendMessageActionSheetController.MessageEffect?) -> Void + let sendSelected: (TGMediaSelectableItem?, Bool, Int32?, Bool, ChatSendMessageActionSheetController.SendParameters?, @escaping () -> Void) -> Void + let schedule: (ChatSendMessageActionSheetController.SendParameters?) -> Void let dismissInput: () -> Void let selectionState: TGMediaSelectionContext? let editingState: TGMediaEditingContext var hiddenMediaId: String? - init(downloadManager: AssetDownloadManager, openMedia: @escaping (PHFetchResult, Int, UIImage?) -> Void, openSelectedMedia: @escaping (TGMediaSelectableItem, UIImage?) -> Void, openDraft: @escaping (MediaEditorDraft, UIImage?) -> Void, toggleSelection: @escaping (TGMediaSelectableItem, Bool, Bool) -> Bool, sendSelected: @escaping (TGMediaSelectableItem?, Bool, Int32?, Bool, ChatSendMessageActionSheetController.MessageEffect?, @escaping () -> Void) -> Void, schedule: @escaping (ChatSendMessageActionSheetController.MessageEffect?) -> Void, dismissInput: @escaping () -> Void, selectionState: TGMediaSelectionContext?, editingState: TGMediaEditingContext) { + var captionIsAboveMedia: Bool = false { + didSet { + if self.captionIsAboveMedia != oldValue { + self.captionIsAboveMediaValue.set(self.captionIsAboveMedia) + } + } + } + let captionIsAboveMediaValue = ValuePromise(false) + + init(downloadManager: AssetDownloadManager, openMedia: @escaping (PHFetchResult, Int, UIImage?) -> Void, openSelectedMedia: @escaping (TGMediaSelectableItem, UIImage?) -> Void, openDraft: @escaping (MediaEditorDraft, UIImage?) -> Void, toggleSelection: @escaping (TGMediaSelectableItem, Bool, Bool) -> Bool, sendSelected: @escaping (TGMediaSelectableItem?, Bool, Int32?, Bool, ChatSendMessageActionSheetController.SendParameters?, @escaping () -> Void) -> Void, schedule: @escaping (ChatSendMessageActionSheetController.SendParameters?) -> Void, dismissInput: @escaping () -> Void, selectionState: TGMediaSelectionContext?, editingState: TGMediaEditingContext) { self.downloadManager = downloadManager self.openMedia = openMedia self.openSelectedMedia = openSelectedMedia @@ -200,7 +209,7 @@ public final class MediaPickerScreen: ViewController, AttachmentContainable { public var presentFilePicker: () -> Void = {} private var completed = false - public var legacyCompletion: (_ signals: [Any], _ silently: Bool, _ scheduleTime: Int32?, ChatSendMessageActionSheetController.MessageEffect?, @escaping (String) -> UIView?, @escaping () -> Void) -> Void = { _, _, _, _, _, _ in } + public var legacyCompletion: (_ signals: [Any], _ silently: Bool, _ scheduleTime: Int32?, ChatSendMessageActionSheetController.SendParameters?, @escaping (String) -> UIView?, @escaping () -> Void) -> Void = { _, _, _, _, _, _ in } public var requestAttachmentMenuExpansion: () -> Void = { } public var updateNavigationStack: (@escaping ([AttachmentContainable]) -> ([AttachmentContainable], AttachmentMediaPickerContext?)) -> Void = { _ in } @@ -1218,12 +1227,24 @@ public final class MediaPickerScreen: ViewController, AttachmentContainable { } } - fileprivate func send(asFile: Bool = false, silently: Bool, scheduleTime: Int32?, animated: Bool, messageEffect: ChatSendMessageActionSheetController.MessageEffect?, completion: @escaping () -> Void) { + fileprivate func send(asFile: Bool = false, silently: Bool, scheduleTime: Int32?, animated: Bool, parameters: ChatSendMessageActionSheetController.SendParameters?, completion: @escaping () -> Void) { guard let controller = self.controller, !controller.completed else { return } controller.dismissAllTooltips() + var parameters = parameters + if parameters == nil { + var textIsAboveMedia = false + if let interaction = controller.interaction { + textIsAboveMedia = interaction.captionIsAboveMedia + } + parameters = ChatSendMessageActionSheetController.SendParameters( + effect: nil, + textIsAboveMedia: textIsAboveMedia + ) + } + var hasHeic = false let allItems = controller.interaction?.selectionState?.selectedItems() ?? [] for item in allItems { @@ -1246,7 +1267,7 @@ public final class MediaPickerScreen: ViewController, AttachmentContainable { return } controller.completed = true - controller.legacyCompletion(signals, silently, scheduleTime, messageEffect, { [weak self] identifier in + controller.legacyCompletion(signals, silently, scheduleTime, parameters, { [weak self] identifier in return !asFile ? self?.getItemSnapshot(identifier) : nil }, { [weak self] in completion() @@ -1959,18 +1980,18 @@ public final class MediaPickerScreen: ViewController, AttachmentContainable { } else { return false } - }, sendSelected: { [weak self] currentItem, silently, scheduleTime, animated, messageEffect, completion in + }, sendSelected: { [weak self] currentItem, silently, scheduleTime, animated, parameters, completion in if let strongSelf = self, let selectionState = strongSelf.interaction?.selectionState, !strongSelf.isDismissing { strongSelf.isDismissing = true if let currentItem = currentItem { selectionState.setItem(currentItem, selected: true) } - strongSelf.controllerNode.send(silently: silently, scheduleTime: scheduleTime, animated: animated, messageEffect: messageEffect, completion: completion) + strongSelf.controllerNode.send(silently: silently, scheduleTime: scheduleTime, animated: animated, parameters: parameters, completion: completion) } - }, schedule: { [weak self] messageEffect in + }, schedule: { [weak self] parameters in if let strongSelf = self { strongSelf.presentSchedulePicker(false, { [weak self] time in - self?.interaction?.sendSelected(nil, false, time, true, messageEffect, {}) + self?.interaction?.sendSelected(nil, false, time, true, parameters, {}) }) } }, dismissInput: { [weak self] in @@ -2419,10 +2440,18 @@ public final class MediaPickerScreen: ViewController, AttachmentContainable { } } } + + var isCaptionAboveMediaAvailable: Signal = .single(false) + if let mediaPickerContext = self.mediaPickerContext { + isCaptionAboveMediaAvailable = .single(mediaPickerContext.hasCaption) + } - let items: Signal = self.groupedPromise.get() + let items: Signal = combineLatest( + self.groupedPromise.get(), + isCaptionAboveMediaAvailable + ) |> deliverOnMainQueue - |> map { [weak self] grouped -> ContextController.Items in + |> map { [weak self] grouped, isCaptionAboveMediaAvailable -> ContextController.Items in var items: [ContextMenuItem] = [] if !hasSpoilers { items.append(.action(ContextMenuActionItem(text: selectionCount > 1 ? strings.Attachment_SendAsFiles : strings.Attachment_SendAsFile, icon: { theme in @@ -2430,7 +2459,7 @@ public final class MediaPickerScreen: ViewController, AttachmentContainable { }, action: { [weak self] _, f in f(.default) - self?.controllerNode.send(asFile: true, silently: false, scheduleTime: nil, animated: true, messageEffect: nil, completion: {}) + self?.controllerNode.send(asFile: true, silently: false, scheduleTime: nil, animated: true, parameters: nil, completion: {}) }))) } if selectionCount > 1 { @@ -2458,25 +2487,48 @@ public final class MediaPickerScreen: ViewController, AttachmentContainable { self?.groupedValue = false }))) } - if isSpoilerAvailable { + if isSpoilerAvailable || (selectionCount > 0 && isCaptionAboveMediaAvailable) { if !items.isEmpty { items.append(.separator) } - items.append(.action(ContextMenuActionItem(text: hasGeneric ? strings.Attachment_EnableSpoiler : strings.Attachment_DisableSpoiler, icon: { _ in return nil }, iconAnimation: ContextMenuActionItem.IconAnimation( - name: "anim_spoiler", - loop: true - ), action: { [weak self] _, f in - f(.default) - guard let strongSelf = self else { - return + + if isCaptionAboveMediaAvailable { + var mediaCaptionIsAbove = false + if let interaction = self?.interaction { + mediaCaptionIsAbove = interaction.captionIsAboveMedia } - if let selectionContext = strongSelf.interaction?.selectionState, let editingContext = strongSelf.interaction?.editingState { - for case let item as TGMediaEditableItem in selectionContext.selectedItems() { - editingContext.setSpoiler(hasGeneric, for: item) + //TODO:localize + items.append(.action(ContextMenuActionItem(text: mediaCaptionIsAbove ? "Move Caption Down" : "Move Caption Up", icon: { _ in return nil }, iconAnimation: ContextMenuActionItem.IconAnimation( + name: !mediaCaptionIsAbove ? "message_preview_sort_above" : "message_preview_sort_below" + ), action: { [weak self] _, f in + f(.default) + guard let strongSelf = self else { + return } - } - }))) + + if let interaction = strongSelf.interaction { + interaction.captionIsAboveMedia = !interaction.captionIsAboveMedia + } + }))) + } + if isSpoilerAvailable { + items.append(.action(ContextMenuActionItem(text: hasGeneric ? strings.Attachment_EnableSpoiler : strings.Attachment_DisableSpoiler, icon: { _ in return nil }, iconAnimation: ContextMenuActionItem.IconAnimation( + name: "anim_spoiler", + loop: true + ), action: { [weak self] _, f in + f(.default) + guard let strongSelf = self else { + return + } + + if let selectionContext = strongSelf.interaction?.selectionState, let editingContext = strongSelf.interaction?.editingState { + for case let item as TGMediaEditableItem in selectionContext.selectedItems() { + editingContext.setSpoiler(hasGeneric, for: item) + } + } + }))) + } } return ContextController.Items(content: .list(items)) } @@ -2534,7 +2586,18 @@ final class MediaPickerContext: AttachmentMediaPickerContext { var caption: Signal { return Signal { [weak self] subscriber in - let disposable = self?.controller?.interaction?.editingState.forcedCaption().start(next: { caption in + guard let self else { + subscriber.putNext(nil) + subscriber.putCompletion() + return EmptyDisposable + } + guard let caption = self.controller?.interaction?.editingState.forcedCaption() else { + subscriber.putNext(nil) + subscriber.putCompletion() + return EmptyDisposable + } + + let disposable = caption.start(next: { caption in if let caption = caption as? NSAttributedString { subscriber.putNext(caption) } else { @@ -2546,6 +2609,34 @@ final class MediaPickerContext: AttachmentMediaPickerContext { } } } + + var hasCaption: Bool { + guard let isForcedCaption = self.controller?.interaction?.editingState.isForcedCaption() else { + return false + } + return isForcedCaption + } + + var captionIsAboveMedia: Signal { + return Signal { [weak self] subscriber in + guard let interaction = self?.controller?.interaction else { + subscriber.putNext(false) + subscriber.putCompletion() + + return EmptyDisposable + } + let disposable = interaction.captionIsAboveMediaValue.get().start(next: { value in + subscriber.putNext(value) + }, error: { _ in }, completed: { }) + return ActionDisposable { + disposable.dispose() + } + } + } + + func setCaptionIsAboveMedia(_ captionIsAboveMedia: Bool) -> Void { + self.controller?.interaction?.captionIsAboveMedia = captionIsAboveMedia + } public var loadingProgress: Signal { return .single(nil) @@ -2563,12 +2654,12 @@ final class MediaPickerContext: AttachmentMediaPickerContext { self.controller?.interaction?.editingState.setForcedCaption(caption, skipUpdate: true) } - func send(mode: AttachmentMediaPickerSendMode, attachmentMode: AttachmentMediaPickerAttachmentMode, messageEffect: ChatSendMessageActionSheetController.MessageEffect?) { - self.controller?.interaction?.sendSelected(nil, mode == .silently, mode == .whenOnline ? scheduleWhenOnlineTimestamp : nil, true, messageEffect, {}) + func send(mode: AttachmentMediaPickerSendMode, attachmentMode: AttachmentMediaPickerAttachmentMode, parameters: ChatSendMessageActionSheetController.SendParameters?) { + self.controller?.interaction?.sendSelected(nil, mode == .silently, mode == .whenOnline ? scheduleWhenOnlineTimestamp : nil, true, parameters, {}) } - func schedule(messageEffect: ChatSendMessageActionSheetController.MessageEffect?) { - self.controller?.interaction?.schedule(messageEffect) + func schedule(parameters: ChatSendMessageActionSheetController.SendParameters?) { + self.controller?.interaction?.schedule(parameters) } func mainButtonAction() { diff --git a/submodules/MediaPickerUI/Sources/MediaPickerSelectedListNode.swift b/submodules/MediaPickerUI/Sources/MediaPickerSelectedListNode.swift index f1105818d9..d481562cc4 100644 --- a/submodules/MediaPickerUI/Sources/MediaPickerSelectedListNode.swift +++ b/submodules/MediaPickerUI/Sources/MediaPickerSelectedListNode.swift @@ -976,7 +976,14 @@ final class MediaPickerSelectedListNode: ASDisplayNode, ASScrollViewDelegate, AS let graphics = PresentationResourcesChat.principalGraphics(theme: theme, wallpaper: wallpaper, bubbleCorners: bubbleCorners) var groupIndex = 0 + var isFirstGroup = true for (items, groupSize) in groupLayouts { + if isFirstGroup { + isFirstGroup = false + } else { + contentHeight += spacing + } + var groupRect = CGRect(origin: CGPoint(x: 0.0, y: insets.top + contentHeight), size: groupSize) if !self.isExternalPreview { groupRect.origin.x = insets.left + floorToScreenPixels((size.width - insets.left - insets.right - groupSize.width) / 2.0) @@ -1005,7 +1012,6 @@ final class MediaPickerSelectedListNode: ASDisplayNode, ASScrollViewDelegate, AS groupBackgroundNode.update(size: groupBackgroundNode.frame.size, theme: theme, wallpaper: wallpaper, graphics: graphics, wallpaperBackgroundNode: self.wallpaperBackgroundNode, transition: itemTransition) } - var isFirstGroup = true for (item, itemRect, itemPosition) in items { if let identifier = item.uniqueIdentifier, let itemNode = self.itemNodes[identifier] { var corners: CACornerMask = [] @@ -1039,11 +1045,6 @@ final class MediaPickerSelectedListNode: ASDisplayNode, ASScrollViewDelegate, AS } } - if isFirstGroup { - isFirstGroup = false - } else { - contentHeight += spacing - } contentHeight += groupSize.height contentWidth = max(contentWidth, groupSize.width) groupIndex += 1 diff --git a/submodules/PeerInfoUI/Sources/DeviceContactInfoController.swift b/submodules/PeerInfoUI/Sources/DeviceContactInfoController.swift index 67292f2528..9eeebabcdf 100644 --- a/submodules/PeerInfoUI/Sources/DeviceContactInfoController.swift +++ b/submodules/PeerInfoUI/Sources/DeviceContactInfoController.swift @@ -1415,7 +1415,7 @@ private func addContactToExisting(context: AccountContext, parentController: Vie (parentController.navigationController as? NavigationController)?.pushViewController(contactsController) let _ = (contactsController.result |> deliverOnMainQueue).start(next: { result in - if let (peers, _, _, _, _) = result, let peer = peers.first { + if let (peers, _, _, _, _, _) = result, let peer = peers.first { let dataSignal: Signal<(EnginePeer?, DeviceContactStableId?), NoError> switch peer { case let .peer(contact, _, _): diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Messages/StoryListContext.swift b/submodules/TelegramCore/Sources/TelegramEngine/Messages/StoryListContext.swift index 50645e5694..7346f5fa52 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Messages/StoryListContext.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Messages/StoryListContext.swift @@ -1175,7 +1175,7 @@ public final class PeerStoryListContext { public var hasCache: Bool public var allEntityFiles: [MediaId: TelegramMediaFile] - init( + public init( peerReference: PeerReference?, items: [EngineStoryItem], pinnedIds: Set, diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageBubbleContentNode/Sources/ChatMessageBubbleContentCalclulateImageCorners.swift b/submodules/TelegramUI/Components/Chat/ChatMessageBubbleContentNode/Sources/ChatMessageBubbleContentCalclulateImageCorners.swift index 77bf4fe086..76d65ff36a 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessageBubbleContentNode/Sources/ChatMessageBubbleContentCalclulateImageCorners.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessageBubbleContentNode/Sources/ChatMessageBubbleContentCalclulateImageCorners.swift @@ -12,8 +12,8 @@ public func chatMessageBubbleImageContentCorners(relativeContentPosition positio case let .linear(top, _): switch top { case .Neighbour: - topLeftCorner = .Corner(mergedWithAnotherContentRadius) - topRightCorner = .Corner(mergedWithAnotherContentRadius) + topLeftCorner = .Corner(normalRadius) + topRightCorner = .Corner(normalRadius) case .BubbleNeighbour: topLeftCorner = .Corner(mergedRadius) topRightCorner = .Corner(mergedRadius) diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageBubbleItemNode/Sources/ChatMessageBubbleItemNode.swift b/submodules/TelegramUI/Components/Chat/ChatMessageBubbleItemNode/Sources/ChatMessageBubbleItemNode.swift index e651de7a72..db570aef1f 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessageBubbleItemNode/Sources/ChatMessageBubbleItemNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessageBubbleItemNode/Sources/ChatMessageBubbleItemNode.swift @@ -248,7 +248,11 @@ private func contentNodeMessagesAndClassesForItem(_ item: ChatMessageItem) -> ([ messageWithCaptionToAdd = (message, itemAttributes) skipText = true } else { - result.append((message, ChatMessageTextBubbleContentNode.self, itemAttributes, BubbleItemAttributes(isAttachment: false, neighborType: .text, neighborSpacing: isFile ? .condensed : .default))) + if let _ = message.attributes.first(where: { $0 is InvertMediaMessageAttribute }) { + result.insert((message, ChatMessageTextBubbleContentNode.self, itemAttributes, BubbleItemAttributes(isAttachment: false, neighborType: .text, neighborSpacing: isFile ? .condensed : .default)), at: 0) + } else { + result.append((message, ChatMessageTextBubbleContentNode.self, itemAttributes, BubbleItemAttributes(isAttachment: false, neighborType: .text, neighborSpacing: isFile ? .condensed : .default))) + } needReactions = false } } else { @@ -293,8 +297,15 @@ private func contentNodeMessagesAndClassesForItem(_ item: ChatMessageItem) -> ([ } if let (messageWithCaptionToAdd, itemAttributes) = messageWithCaptionToAdd { - result.append((messageWithCaptionToAdd, ChatMessageTextBubbleContentNode.self, itemAttributes, BubbleItemAttributes(isAttachment: false, neighborType: .text, neighborSpacing: .default))) - needReactions = false + if let _ = messageWithCaptionToAdd.attributes.first(where: { $0 is InvertMediaMessageAttribute }) { + if result.isEmpty { + needReactions = false + } + result.insert((messageWithCaptionToAdd, ChatMessageTextBubbleContentNode.self, itemAttributes, BubbleItemAttributes(isAttachment: false, neighborType: .text, neighborSpacing: .default)), at: 0) + } else { + result.append((messageWithCaptionToAdd, ChatMessageTextBubbleContentNode.self, itemAttributes, BubbleItemAttributes(isAttachment: false, neighborType: .text, neighborSpacing: .default))) + needReactions = false + } } if let additionalContent = item.additionalContent { @@ -2760,7 +2771,7 @@ public class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewI let contentNodeFrame = framesAndPositions[mosaicIndex].0.offsetBy(dx: 0.0, dy: contentNodesHeight) contentNodeFramesPropertiesAndApply.append((contentNodeFrame, properties, true, apply)) - if mosaicIndex == mosaicRange.upperBound - 1 { + if i == mosaicRange.upperBound - 1 { contentNodesHeight += size.height totalContentNodesHeight += size.height diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageContactBubbleContentNode/Sources/ChatMessageContactBubbleContentNode.swift b/submodules/TelegramUI/Components/Chat/ChatMessageContactBubbleContentNode/Sources/ChatMessageContactBubbleContentNode.swift index 412b08e962..bde09fc878 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessageContactBubbleContentNode/Sources/ChatMessageContactBubbleContentNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessageContactBubbleContentNode/Sources/ChatMessageContactBubbleContentNode.swift @@ -254,6 +254,8 @@ public class ChatMessageContactBubbleContentNode: ChatMessageBubbleContentNode { let statusType: ChatMessageDateAndStatusType? if case .customChatContents = item.associatedData.subject { statusType = nil + } else if item.message.timestamp == 0 { + statusType = nil } else { switch position { case .linear(_, .None), .linear(_, .Neighbour(true, _, _)): @@ -274,6 +276,7 @@ public class ChatMessageContactBubbleContentNode: ChatMessageBubbleContentNode { } var statusSuggestedWidthAndContinue: (CGFloat, (CGFloat) -> (CGSize, (ListViewItemUpdateAnimation) -> Void))? + let messageEffect = item.message.messageEffect(availableMessageEffects: item.associatedData.availableMessageEffects) if let statusType = statusType { var isReplyThread = false if case .replyThread = item.chatLocation { @@ -295,7 +298,7 @@ public class ChatMessageContactBubbleContentNode: ChatMessageBubbleContentNode { reactionPeers: dateReactionsAndPeers.peers, displayAllReactionPeers: item.message.id.peerId.namespace == Namespaces.Peer.CloudUser, areReactionsTags: item.topMessage.areReactionsTags(accountPeerId: item.context.account.peerId), - messageEffect: item.message.messageEffect(availableMessageEffects: item.associatedData.availableMessageEffects), + messageEffect: messageEffect, replyCount: dateReplies, isPinned: item.message.tags.contains(.pinned) && !item.associatedData.isInPinnedListMode && isReplyThread, hasAutoremove: item.message.isSelfExpiring, @@ -447,11 +450,18 @@ public class ChatMessageContactBubbleContentNode: ChatMessageBubbleContentNode { if let forwardInfo = item.message.forwardInfo, forwardInfo.flags.contains(.isImported) { strongSelf.dateAndStatusNode.pressed = { - guard let strongSelf = self else { + guard let strongSelf = self, let item = strongSelf.item else { return } item.controllerInteraction.displayImportedMessageTooltip(strongSelf.dateAndStatusNode) } + } else if messageEffect != nil { + strongSelf.dateAndStatusNode.pressed = { + guard let strongSelf = self, let item = strongSelf.item else { + return + } + item.controllerInteraction.playMessageEffect(item.message) + } } else { strongSelf.dateAndStatusNode.pressed = nil } diff --git a/submodules/TelegramUI/Components/Chat/ChatSendAudioMessageContextPreview/Sources/ChatSendAudioMessageContextPreview.swift b/submodules/TelegramUI/Components/Chat/ChatSendAudioMessageContextPreview/Sources/ChatSendAudioMessageContextPreview.swift index 8cdbe2d773..9cc921cdf2 100644 --- a/submodules/TelegramUI/Components/Chat/ChatSendAudioMessageContextPreview/Sources/ChatSendAudioMessageContextPreview.swift +++ b/submodules/TelegramUI/Components/Chat/ChatSendAudioMessageContextPreview/Sources/ChatSendAudioMessageContextPreview.swift @@ -14,6 +14,180 @@ import WallpaperBackgroundNode import AudioWaveform import ChatMessageItemView +public final class ChatSendContactMessageContextPreview: UIView, ChatSendMessageContextScreenMediaPreview { + private let context: AccountContext + private let presentationData: PresentationData + private let wallpaperBackgroundNode: WallpaperBackgroundNode? + private let contactPeers: [ContactListPeer] + + private var messageNodes: [ListViewItemNode]? + private let messagesContainer: UIView + + public var isReady: Signal { + return .single(true) + } + + public var view: UIView { + return self + } + + public var globalClippingRect: CGRect? { + return nil + } + + public var layoutType: ChatSendMessageContextScreenMediaPreviewLayoutType { + return .message + } + + public init(context: AccountContext, presentationData: PresentationData, wallpaperBackgroundNode: WallpaperBackgroundNode?, contactPeers: [ContactListPeer]) { + self.context = context + self.presentationData = presentationData + self.wallpaperBackgroundNode = wallpaperBackgroundNode + self.contactPeers = contactPeers + + self.messagesContainer = UIView() + self.messagesContainer.layer.sublayerTransform = CATransform3DMakeScale(-1.0, -1.0, 1.0) + + super.init(frame: CGRect()) + + self.addSubview(self.messagesContainer) + } + + required public init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + deinit { + } + + public func animateIn(transition: Transition) { + transition.animateAlpha(view: self.messagesContainer, from: 0.0, to: 1.0) + transition.animateScale(view: self.messagesContainer, from: 0.001, to: 1.0) + } + + public func animateOut(transition: Transition) { + transition.setAlpha(view: self.messagesContainer, alpha: 0.0) + transition.setScale(view: self.messagesContainer, scale: 0.001) + } + + public func animateOutOnSend(transition: Transition) { + transition.setAlpha(view: self.messagesContainer, alpha: 0.0) + } + + public func update(containerSize: CGSize, transition: Transition) -> CGSize { + var contactsMedia: [TelegramMediaContact] = [] + for peer in self.contactPeers { + switch peer { + case let .peer(contact, _, _): + guard let contact = contact as? TelegramUser, let phoneNumber = contact.phone else { + continue + } + let contactData = DeviceContactExtendedData(basicData: DeviceContactBasicData(firstName: contact.firstName ?? "", lastName: contact.lastName ?? "", phoneNumbers: [DeviceContactPhoneNumberData(label: "_$!!$_", value: phoneNumber)]), middleName: "", prefix: "", suffix: "", organization: "", jobTitle: "", department: "", emailAddresses: [], urls: [], addresses: [], birthdayDate: nil, socialProfiles: [], instantMessagingProfiles: [], note: "") + + let phone = contactData.basicData.phoneNumbers[0].value + contactsMedia.append(TelegramMediaContact(firstName: contactData.basicData.firstName, lastName: contactData.basicData.lastName, phoneNumber: phone, peerId: contact.id, vCardData: nil)) + case let .deviceContact(_, basicData): + guard !basicData.phoneNumbers.isEmpty else { + continue + } + let contactData = DeviceContactExtendedData(basicData: basicData, middleName: "", prefix: "", suffix: "", organization: "", jobTitle: "", department: "", emailAddresses: [], urls: [], addresses: [], birthdayDate: nil, socialProfiles: [], instantMessagingProfiles: [], note: "") + + let phone = contactData.basicData.phoneNumbers[0].value + contactsMedia.append(TelegramMediaContact(firstName: contactData.basicData.firstName, lastName: contactData.basicData.lastName, phoneNumber: phone, peerId: nil, vCardData: nil)) + } + } + + var items: [ListViewItem] = [] + for contactMedia in contactsMedia { + let message = Message(stableId: 1, stableVersion: 0, id: MessageId(peerId: self.context.account.peerId, namespace: 0, id: 1), globallyUniqueId: nil, groupingKey: nil, groupInfo: nil, threadId: nil, timestamp: 0, flags: [.Incoming], tags: [], globalTags: [], localTags: [], customTags: [], forwardInfo: nil, author: nil, text: "", attributes: [], media: [contactMedia], peers: SimpleDictionary(), associatedMessages: SimpleDictionary(), associatedMessageIds: [], associatedMedia: [:], associatedThreadInfo: nil, associatedStories: [:]) + + let item = self.context.sharedContext.makeChatMessagePreviewItem( + context: self.context, + messages: [message], + theme: presentationData.theme, + strings: presentationData.strings, + wallpaper: presentationData.chatWallpaper, + fontSize: presentationData.chatFontSize, + chatBubbleCorners: presentationData.chatBubbleCorners, + dateTimeFormat: presentationData.dateTimeFormat, + nameOrder: presentationData.nameDisplayOrder, + forcedResourceStatus: FileMediaResourceStatus(mediaStatus: .fetchStatus(.Local), fetchStatus: .Local), + tapMessage: nil, + clickThroughMessage: nil, + backgroundNode: self.wallpaperBackgroundNode, + availableReactions: nil, + accountPeer: nil, + isCentered: false, + isPreview: true, + isStandalone: true + ) + items.append(item) + } + + let params = ListViewItemLayoutParams(width: containerSize.width, leftInset: 0.0, rightInset: 0.0, availableHeight: containerSize.height) + if let messageNodes = self.messageNodes { + for i in 0 ..< items.count { + let itemNode = messageNodes[i] + items[i].updateNode(async: { $0() }, node: { + return itemNode + }, params: params, previousItem: i == 0 ? nil : items[i - 1], nextItem: i == (items.count - 1) ? nil : items[i + 1], animation: .None, completion: { (layout, apply) in + let nodeFrame = CGRect(origin: CGPoint(x: itemNode.frame.minX, y: itemNode.frame.minY), size: CGSize(width: containerSize.width, height: layout.size.height)) + + itemNode.contentSize = layout.contentSize + itemNode.insets = layout.insets + itemNode.frame = nodeFrame + itemNode.isUserInteractionEnabled = false + + apply(ListViewItemApply(isOnScreen: true)) + }) + } + } else { + var messageNodes: [ListViewItemNode] = [] + for i in 0 ..< items.count { + var itemNode: ListViewItemNode? + items[i].nodeConfiguredForParams(async: { $0() }, params: params, synchronousLoads: false, previousItem: i == 0 ? nil : items[i - 1], nextItem: i == (items.count - 1) ? nil : items[i + 1], completion: { node, apply in + itemNode = node + apply().1(ListViewItemApply(isOnScreen: true)) + }) + itemNode!.isUserInteractionEnabled = false + messageNodes.append(itemNode!) + self.messagesContainer.addSubview(itemNode!.view) + } + self.messageNodes = messageNodes + } + + var contentSize = CGSize() + for messageNode in self.messageNodes ?? [] { + guard let messageNode = messageNode as? ChatMessageItemView else { + continue + } + if !contentSize.height.isZero { + contentSize.height += 2.0 + } + let contentFrame = messageNode.contentFrame() + contentSize.height += contentFrame.height + contentSize.width = max(contentSize.width, contentFrame.width) + } + + var contentOffsetY: CGFloat = 0.0 + for messageNode in self.messageNodes ?? [] { + guard let messageNode = messageNode as? ChatMessageItemView else { + continue + } + if !contentOffsetY.isZero { + contentOffsetY += 2.0 + } + let contentFrame = messageNode.contentFrame() + messageNode.frame = CGRect(origin: CGPoint(x: contentFrame.minX + contentSize.width - contentFrame.width + 6.0, y: 3.0 + contentOffsetY), size: CGSize(width: contentFrame.width, height: contentFrame.height)) + contentOffsetY += contentFrame.height + } + + self.messagesContainer.frame = CGRect(origin: CGPoint(x: 6.0, y: 3.0), size: CGSize(width: contentSize.width, height: contentSize.height)) + + return CGSize(width: contentSize.width - 4.0, height: contentSize.height + 2.0) + } +} + public final class ChatSendAudioMessageContextPreview: UIView, ChatSendMessageContextScreenMediaPreview { private let context: AccountContext private let presentationData: PresentationData diff --git a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoPaneContainerNode.swift b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoPaneContainerNode.swift index ca3183dc1e..5983f32a6b 100644 --- a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoPaneContainerNode.swift +++ b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoPaneContainerNode.swift @@ -419,7 +419,7 @@ private final class PeerInfoPendingPane { } } - let visualPaneNode = PeerInfoStoryPaneNode(context: context, peerId: peerId, chatLocation: chatLocation, contentType: .photoOrVideo, captureProtected: captureProtected, isSaved: false, isArchive: key == .storyArchive, isProfileEmbedded: true, canManageStories: canManage, navigationController: chatControllerInteraction.navigationController, listContext: key == .storyArchive ? data.storyArchiveListContext : data.storyListContext) + let visualPaneNode = PeerInfoStoryPaneNode(context: context, peerId: peerId, contentType: .photoOrVideo, captureProtected: captureProtected, isSaved: false, isArchive: key == .storyArchive, isProfileEmbedded: true, canManageStories: canManage, navigationController: chatControllerInteraction.navigationController, listContext: key == .storyArchive ? data.storyArchiveListContext : data.storyListContext) paneNode = visualPaneNode visualPaneNode.openCurrentDate = { openMediaCalendar() diff --git a/submodules/TelegramUI/Components/PeerInfo/PeerInfoStoryGridScreen/Sources/PeerInfoStoryGridScreen.swift b/submodules/TelegramUI/Components/PeerInfo/PeerInfoStoryGridScreen/Sources/PeerInfoStoryGridScreen.swift index 4f810d6f33..290b7a2620 100644 --- a/submodules/TelegramUI/Components/PeerInfo/PeerInfoStoryGridScreen/Sources/PeerInfoStoryGridScreen.swift +++ b/submodules/TelegramUI/Components/PeerInfo/PeerInfoStoryGridScreen/Sources/PeerInfoStoryGridScreen.swift @@ -456,7 +456,6 @@ final class PeerInfoStoryGridScreenComponent: Component { paneNode = PeerInfoStoryPaneNode( context: component.context, peerId: component.peerId, - chatLocation: .peer(id: component.peerId), contentType: .photoOrVideo, captureProtected: false, isSaved: true, diff --git a/submodules/TelegramUI/Components/PeerInfo/PeerInfoStoryGridScreen/Sources/StorySearchGridScreen.swift b/submodules/TelegramUI/Components/PeerInfo/PeerInfoStoryGridScreen/Sources/StorySearchGridScreen.swift new file mode 100644 index 0000000000..7a8cde1f26 --- /dev/null +++ b/submodules/TelegramUI/Components/PeerInfo/PeerInfoStoryGridScreen/Sources/StorySearchGridScreen.swift @@ -0,0 +1,261 @@ +import Foundation +import AsyncDisplayKit +import Display +import SwiftSignalKit +import TelegramPresentationData +import AccountContext +import ComponentFlow +import TelegramCore +import PeerInfoVisualMediaPaneNode +import ViewControllerComponent +import ChatListHeaderComponent +import ContextUI +import ChatTitleView +import BottomButtonPanelComponent +import UndoUI +import MoreHeaderButton +import MediaEditorScreen +import SaveToCameraRoll + +final class StorySearchGridScreenComponent: Component { + typealias EnvironmentType = ViewControllerComponentContainer.Environment + + let context: AccountContext + let searchQuery: String + + init( + context: AccountContext, + searchQuery: String + ) { + self.context = context + self.searchQuery = searchQuery + } + + static func ==(lhs: StorySearchGridScreenComponent, rhs: StorySearchGridScreenComponent) -> Bool { + if lhs.context !== rhs.context { + return false + } + if lhs.searchQuery != rhs.searchQuery { + return false + } + + return true + } + + final class View: UIView { + private var component: StorySearchGridScreenComponent? + private(set) weak var state: EmptyComponentState? + private var environment: EnvironmentType? + + private(set) var paneNode: PeerInfoStoryPaneNode? + private var paneStatusDisposable: Disposable? + private(set) var paneStatusText: String? + + override init(frame: CGRect) { + super.init(frame: frame) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + deinit { + self.paneStatusDisposable?.dispose() + } + + func scrollToTop() { + guard let paneNode = self.paneNode else { + return + } + let _ = paneNode.scrollToTop() + } + + private var isUpdating = false + func update(component: StorySearchGridScreenComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + self.isUpdating = true + defer { + self.isUpdating = false + } + + self.component = component + self.state = state + + let sideInset: CGFloat = 14.0 + let _ = sideInset + + let environment = environment[EnvironmentType.self].value + + let themeUpdated = self.environment?.theme !== environment.theme + + self.environment = environment + + if themeUpdated { + self.backgroundColor = environment.theme.list.plainBackgroundColor + } + + let bottomInset: CGFloat = environment.safeInsets.bottom + + let paneNode: PeerInfoStoryPaneNode + if let current = self.paneNode { + paneNode = current + } else { + paneNode = PeerInfoStoryPaneNode( + context: component.context, + peerId: nil, + searchQuery: component.searchQuery, + contentType: .photoOrVideo, + captureProtected: false, + isSaved: false, + isArchive: false, + isProfileEmbedded: false, + canManageStories: false, + navigationController: { [weak self] in + guard let self else { + return nil + } + return self.environment?.controller()?.navigationController as? NavigationController + }, + listContext: nil + ) + paneNode.isEmptyUpdated = { [weak self] _ in + guard let self else { + return + } + if !self.isUpdating { + self.state?.updated(transition: .immediate) + } + } + self.paneNode = paneNode + self.addSubview(paneNode.view) + + self.paneStatusDisposable = (paneNode.status + |> deliverOnMainQueue).start(next: { [weak self] status in + guard let self else { + return + } + if self.paneStatusText != status?.text { + self.paneStatusText = status?.text + (self.environment?.controller() as? StorySearchGridScreen)?.updateTitle() + } + }) + } + + paneNode.update( + size: availableSize, + topInset: environment.navigationHeight, + sideInset: environment.safeInsets.left, + bottomInset: bottomInset, + deviceMetrics: environment.deviceMetrics, + visibleHeight: availableSize.height, + isScrollingLockedAtTop: false, + expandProgress: 1.0, + navigationHeight: 0.0, + presentationData: component.context.sharedContext.currentPresentationData.with({ $0 }), + synchronous: false, + transition: transition.containedViewLayoutTransition + ) + transition.setFrame(view: paneNode.view, frame: CGRect(origin: CGPoint(), size: availableSize)) + + return availableSize + } + } + + func makeView() -> View { + return View() + } + + func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) + } +} + +public class StorySearchGridScreen: ViewControllerComponentContainer { + private let context: AccountContext + private let searchQuery: String + private var isDismissed: Bool = false + + private var titleView: ChatTitleView? + + public init( + context: AccountContext, + searchQuery: String + ) { + self.context = context + self.searchQuery = searchQuery + + super.init(context: context, component: StorySearchGridScreenComponent( + context: context, + searchQuery: searchQuery + ), navigationBarAppearance: .default, theme: .default) + + let presentationData = context.sharedContext.currentPresentationData.with({ $0 }) + + self.titleView = ChatTitleView( + context: context, theme: + presentationData.theme, + strings: presentationData.strings, + dateTimeFormat: presentationData.dateTimeFormat, + nameDisplayOrder: presentationData.nameDisplayOrder, + animationCache: context.animationCache, + animationRenderer: context.animationRenderer + ) + self.titleView?.disableAnimations = true + + self.navigationItem.titleView = self.titleView + + self.updateTitle() + + self.scrollToTop = { [weak self] in + guard let self, let componentView = self.node.hostView.componentView as? StorySearchGridScreenComponent.View else { + return + } + componentView.scrollToTop() + } + } + + required public init(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + deinit { + } + + func updateTitle() { + let presentationData = self.context.sharedContext.currentPresentationData.with { $0 } + let _ = presentationData + + guard let componentView = self.node.hostView.componentView as? StorySearchGridScreenComponent.View, let paneNode = componentView.paneNode else { + return + } + let _ = paneNode + + let title: String? + if let paneStatusText = componentView.paneStatusText, !paneStatusText.isEmpty { + title = paneStatusText + } else { + title = nil + } + //TODO:localize + self.titleView?.titleContent = .custom("\(self.searchQuery)", title, false) + } + + override public func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) { + super.containerLayoutUpdated(layout, transition: transition) + + self.titleView?.layout = layout + } +} + +private final class PeerInfoContextReferenceContentSource: ContextReferenceContentSource { + private let controller: ViewController + private let sourceNode: ContextReferenceContentNode + + init(controller: ViewController, sourceNode: ContextReferenceContentNode) { + self.controller = controller + self.sourceNode = sourceNode + } + + func transitionInfo() -> ContextControllerReferenceViewInfo? { + return ContextControllerReferenceViewInfo(referenceView: self.sourceNode.view, contentAreaInScreenSpace: UIScreen.main.bounds) + } +} diff --git a/submodules/TelegramUI/Components/PeerInfo/PeerInfoVisualMediaPaneNode/Sources/PeerInfoStoryPaneNode.swift b/submodules/TelegramUI/Components/PeerInfo/PeerInfoVisualMediaPaneNode/Sources/PeerInfoStoryPaneNode.swift index 67fc19cfda..3a5051bada 100644 --- a/submodules/TelegramUI/Components/PeerInfo/PeerInfoVisualMediaPaneNode/Sources/PeerInfoStoryPaneNode.swift +++ b/submodules/TelegramUI/Components/PeerInfo/PeerInfoVisualMediaPaneNode/Sources/PeerInfoStoryPaneNode.swift @@ -34,6 +34,7 @@ import UndoUI import PlainButtonComponent import ComponentDisplayAdapters import MediaEditorScreen +import AvatarNode private let mediaBadgeBackgroundColor = UIColor(white: 0.0, alpha: 0.6) private let mediaBadgeTextColor = UIColor.white @@ -88,6 +89,7 @@ private final class VisualMediaItem: SparseItemGrid.Item { let localMonthTimestamp: Int32 let peer: PeerReference let story: EngineStoryItem + let authorPeer: EnginePeer? let isPinned: Bool override var id: AnyHashable { @@ -102,10 +104,11 @@ private final class VisualMediaItem: SparseItemGrid.Item { return VisualMediaHoleAnchor(index: self.index, storyId: self.story.id, localMonthTimestamp: self.localMonthTimestamp) } - init(index: Int, peer: PeerReference, story: EngineStoryItem, isPinned: Bool, localMonthTimestamp: Int32) { + init(index: Int, peer: PeerReference, story: EngineStoryItem, authorPeer: EnginePeer?, isPinned: Bool, localMonthTimestamp: Int32) { self.indexValue = index self.peer = peer self.story = story + self.authorPeer = authorPeer self.isPinned = isPinned self.localMonthTimestamp = localMonthTimestamp } @@ -142,6 +145,10 @@ private let durationFont: UIFont = { Font.semibold(11.0) }() +private let avatarFont: UIFont = { + avatarPlaceholderFont(size: 10.0) +}() + private let minDurationImage: UIImage = { let image = generateImage(CGSize(width: 20.0, height: 20.0), rotatedContext: { size, context in context.clear(CGRect(origin: CGPoint(), size: size)) @@ -200,6 +207,22 @@ private let topRightShadowImage: UIImage = { return image! }() +private let topLeftShadowImage: UIImage = { + let baseImage = UIImage(bundleImageName: "Peer Info/MediaGridShadow")! + let image = generateImage(baseImage.size, rotatedContext: { size, context in + context.clear(CGRect(origin: CGPoint(), size: size)) + + context.translateBy(x: size.width / 2.0, y: size.height / 2.0) + context.scaleBy(x: -1.0, y: -1.0) + context.translateBy(x: -size.width / 2.0, y: -size.height / 2.0) + + UIGraphicsPushContext(context) + baseImage.draw(in: CGRect(origin: CGPoint(), size: size)) + UIGraphicsPopContext() + }) + return image! +}() + private let viewCountImage: UIImage = { let baseImage = UIImage(bundleImageName: "Peer Info/MediaGridViewCount")! let image = generateImage(baseImage.size, rotatedContext: { size, context in @@ -275,7 +298,11 @@ private enum ItemTopRightIcon { case pinned } -private final class DurationLayer: CALayer { +private final class DurationLayer: SimpleLayer { + private var authorPeerId: EnginePeer.Id? + private var avatarLayer: SimpleLayer? + private var disposable: Disposable? + override init() { super.init() @@ -286,6 +313,10 @@ private final class DurationLayer: CALayer { required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } + + deinit { + self.disposable?.dispose() + } override func action(forKey event: String) -> CAAction? { return nullAction @@ -385,34 +416,97 @@ private final class DurationLayer: CALayer { self.contents = image?.cgImage } } + + func copyAuthor(from other: DurationLayer) { + self.contents = other.contents + + let avatarLayer: SimpleLayer + if let current = self.avatarLayer { + avatarLayer = current + } else { + avatarLayer = SimpleLayer() + self.avatarLayer = avatarLayer + self.addSublayer(avatarLayer) + + avatarLayer.frame = CGRect(origin: CGPoint(x: -11.0, y: 2.0), size: CGSize(width: 13.0, height: 13.0)) + avatarLayer.cornerRadius = 13.0 * 0.5 + avatarLayer.masksToBounds = true + } + avatarLayer.contents = other.avatarLayer?.contents + } + + func update(directMediaImageCache: DirectMediaImageCache, author: EnginePeer, synchronous: SparseItemGrid.Synchronous) { + let avatarLayer: SimpleLayer + if let current = self.avatarLayer { + avatarLayer = current + } else { + avatarLayer = SimpleLayer() + self.avatarLayer = avatarLayer + self.addSublayer(avatarLayer) + + avatarLayer.frame = CGRect(origin: CGPoint(x: -11.0, y: 2.0), size: CGSize(width: 13.0, height: 13.0)) + avatarLayer.cornerRadius = 13.0 * 0.5 + avatarLayer.masksToBounds = true + } + + if self.authorPeerId != author.id { + let string = NSAttributedString(string: author.debugDisplayTitle, font: durationFont, textColor: .white) + let bounds = string.boundingRect(with: CGSize(width: 100.0, height: 100.0), options: .usesLineFragmentOrigin, context: nil) + let textSize = CGSize(width: ceil(bounds.width), height: ceil(bounds.height)) + let sideInset: CGFloat = 6.0 + let verticalInset: CGFloat = 2.0 + let image = generateImage(CGSize(width: textSize.width + sideInset * 2.0, height: textSize.height + verticalInset * 2.0), rotatedContext: { size, context in + context.clear(CGRect(origin: CGPoint(), size: size)) + + context.setBlendMode(.normal) + + context.setShadow(offset: CGSize(width: 0.0, height: 0.0), blur: 2.5, color: UIColor(rgb: 0x000000, alpha: 0.22).cgColor) + + UIGraphicsPushContext(context) + string.draw(in: bounds.offsetBy(dx: sideInset, dy: verticalInset)) + UIGraphicsPopContext() + }) + self.contents = image?.cgImage + + if let smallProfileImage = author.smallProfileImage, let peerReference = PeerReference(author._asPeer()) { + if let result = directMediaImageCache.getAvatarImage(peer: peerReference, resource: MediaResourceReference.avatar(peer: peerReference, resource: smallProfileImage.resource), immediateThumbnail: smallProfileImage.immediateThumbnailData, size: 24, includeBlurred: true, synchronous: synchronous == .full) { + if let image = result.image { + avatarLayer.contents = image.cgImage + } else if let image = result.blurredImage { + avatarLayer.contents = image.cgImage + } + if let loadSignal = result.loadSignal { + self.disposable?.dispose() + self.disposable = (loadSignal + |> deliverOnMainQueue).start(next: { [weak self] image in + guard let self else { + return + } + self.avatarLayer?.contents = image?.cgImage + }) + } + } + } else { + self.avatarLayer?.contents = generateImage(CGSize(width: 24.0, height: 24.0), rotatedContext: { size, context in + context.clear(CGRect(origin: CGPoint(), size: size)) + + drawPeerAvatarLetters(context: context, size: size, font: avatarFont, letters: author.displayLetters, peerId: author.id, nameColor: author.nameColor) + })?.cgImage + } + } + } } -private protocol ItemLayer: SparseItemGridLayer { - var item: VisualMediaItem? { get set } - var durationLayer: DurationLayer? { get set } - var minFactor: CGFloat { get set } - var selectionLayer: GridMessageSelectionLayer? { get set } - var disposable: Disposable? { get set } - - var hasContents: Bool { get set } - func setSpoilerContents(_ contents: Any?) - - func updateDuration(viewCount: Int32?, duration: Int32?, topRightIcon: ItemTopRightIcon?, isMin: Bool, minFactor: CGFloat) - func updateSelection(theme: CheckNodeTheme, isSelected: Bool?, animated: Bool) - func updateHasSpoiler(hasSpoiler: Bool) - - func bind(item: VisualMediaItem) - func unbind() -} - -private final class GenericItemLayer: CALayer, ItemLayer { +private final class ItemLayer: CALayer, SparseItemGridLayer { var item: VisualMediaItem? var viewCountLayer: DurationLayer? var durationLayer: DurationLayer? var privacyTypeLayer: DurationLayer? + var authorLayer: DurationLayer? var leftShadowLayer: SimpleLayer? var rightShadowLayer: SimpleLayer? var topRightShadowLayer: SimpleLayer? + var topLeftShadowLayer: SimpleLayer? var minFactor: CGFloat = 1.0 var selectionLayer: GridMessageSelectionLayer? var dustLayer: MediaDustLayer? @@ -458,7 +552,7 @@ private final class GenericItemLayer: CALayer, ItemLayer { self.item = item } - func updateDuration(viewCount: Int32?, duration: Int32?, topRightIcon: ItemTopRightIcon?, isMin: Bool, minFactor: CGFloat) { + func updateDuration(viewCount: Int32?, duration: Int32?, topRightIcon: ItemTopRightIcon?, author: EnginePeer?, isMin: Bool, minFactor: CGFloat, directMediaImageCache: DirectMediaImageCache, synchronous: SparseItemGrid.Synchronous) { self.minFactor = minFactor if let viewCount { @@ -511,6 +605,23 @@ private final class GenericItemLayer: CALayer, ItemLayer { privacyTypeLayer.removeFromSuperlayer() } + if let author { + if let authorLayer = self.authorLayer { + authorLayer.update(directMediaImageCache: directMediaImageCache, author: author, synchronous: synchronous) + } else { + let authorLayer = DurationLayer() + authorLayer.contentsGravity = .bottomLeft + authorLayer.update(directMediaImageCache: directMediaImageCache, author: author, synchronous: synchronous) + self.addSublayer(authorLayer) + authorLayer.frame = CGRect(origin: CGPoint(x: 17.0, y: 3.0), size: CGSize()) + authorLayer.transform = CATransform3DMakeScale(minFactor, minFactor, 1.0) + self.authorLayer = authorLayer + } + } else if let authorLayer = self.authorLayer { + self.authorLayer = nil + authorLayer.removeFromSuperlayer() + } + let size = self.bounds.size if self.viewCountLayer != nil { @@ -560,6 +671,22 @@ private final class GenericItemLayer: CALayer, ItemLayer { topRightShadowLayer.removeFromSuperlayer() } } + + if self.authorLayer != nil { + if self.topLeftShadowLayer == nil { + let topLeftShadowLayer = SimpleLayer() + self.topLeftShadowLayer = topLeftShadowLayer + self.insertSublayer(topLeftShadowLayer, at: 0) + topLeftShadowLayer.contents = topLeftShadowImage.cgImage + let shadowSize = CGSize(width: min(size.width, topLeftShadowImage.size.width), height: min(size.height, topLeftShadowImage.size.height)) + topLeftShadowLayer.frame = CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: shadowSize) + } + } else { + if let topLeftShadowLayer = self.topLeftShadowLayer { + self.topLeftShadowLayer = nil + topLeftShadowLayer.removeFromSuperlayer() + } + } } func updateSelection(theme: CheckNodeTheme, isSelected: Bool?, animated: Bool) { @@ -598,6 +725,15 @@ private final class GenericItemLayer: CALayer, ItemLayer { privacyTypeLayer.animateAlpha(from: CGFloat(previousAlpha), to: CGFloat(privacyAlpha), duration: 0.2) } } + + if let authorLayer = self.authorLayer { + let authorAlpha: Float = isSelected == nil ? 1.0 : 0.0 + if authorAlpha != authorLayer.opacity { + let previousAlpha = authorLayer.opacity + authorLayer.opacity = authorAlpha + authorLayer.animateAlpha(from: CGFloat(previousAlpha), to: CGFloat(authorAlpha), duration: 0.2) + } + } } func updateHasSpoiler(hasSpoiler: Bool) { @@ -636,6 +772,9 @@ private final class GenericItemLayer: CALayer, ItemLayer { if let privacyTypeLayer = self.privacyTypeLayer { privacyTypeLayer.frame = CGRect(origin: CGPoint(x: size.width - 2.0, y: 3.0), size: CGSize()) } + if let authorLayer = self.authorLayer { + authorLayer.frame = CGRect(origin: CGPoint(x: 17.0, y: 3.0), size: CGSize()) + } if let leftShadowLayer = self.leftShadowLayer { let shadowSize = CGSize(width: min(size.width, leftShadowImage.size.width), height: min(size.height, leftShadowImage.size.height)) @@ -652,6 +791,11 @@ private final class GenericItemLayer: CALayer, ItemLayer { topRightShadowLayer.frame = CGRect(origin: CGPoint(x: size.width - shadowSize.width, y: 0.0), size: shadowSize) } + if let topLeftShadowLayer = self.topLeftShadowLayer { + let shadowSize = CGSize(width: min(size.width, topLeftShadowImage.size.width), height: min(size.height, topLeftShadowImage.size.height)) + topLeftShadowLayer.frame = CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: shadowSize) + } + if let binding = binding as? SparseItemGridBindingImpl, let item = item as? VisualMediaItem, let previousItem = self.item { if previousItem.story.media.id != item.story.media.id { binding.bindLayers(items: [item], layers: [displayItem], size: size, insets: insets, synchronous: .none) @@ -665,7 +809,7 @@ private final class GenericItemLayer: CALayer, ItemLayer { } if let selectedMedia { - binding.updateLayerData(story: item.story, item: item, selectedMedia: selectedMedia, layer: layer) + binding.updateLayerData(story: item.story, item: item, selectedMedia: selectedMedia, layer: layer, synchronous: .none) } } } @@ -678,13 +822,16 @@ private final class ItemTransitionView: UIView { private var copyDurationLayer: SimpleLayer? private var copyViewCountLayer: SimpleLayer? private var copyPrivacyTypeLayer: SimpleLayer? + private var copyAuthorLayer: SimpleLayer? private var copyLeftShadowLayer: SimpleLayer? private var copyRightShadowLayer: SimpleLayer? private var copyTopRightShadowLayer: SimpleLayer? + private var copyTopLeftShadowLayer: SimpleLayer? private var viewCountLayerBottomLeftPosition: CGPoint? private var durationLayerBottomLeftPosition: CGPoint? private var privacyTypeLayerTopRightPosition: CGPoint? + private var authorLayerTopLeftPosition: CGPoint? var selectionLayer: GridMessageSelectionLayer? @@ -699,16 +846,20 @@ private final class ItemTransitionView: UIView { var viewCountLayer: CALayer? var durationLayer: CALayer? var privacyTypeLayer: CALayer? + var authorLayer: CALayer? var leftShadowLayer: CALayer? var rightShadowLayer: CALayer? var topRightShadowLayer: CALayer? - if let itemLayer = itemLayer as? GenericItemLayer { + var topLeftShadowLayer: CALayer? + if let itemLayer = itemLayer as? ItemLayer { viewCountLayer = itemLayer.viewCountLayer durationLayer = itemLayer.durationLayer privacyTypeLayer = itemLayer.privacyTypeLayer + authorLayer = itemLayer.authorLayer leftShadowLayer = itemLayer.leftShadowLayer rightShadowLayer = itemLayer.rightShadowLayer topRightShadowLayer = itemLayer.topRightShadowLayer + topLeftShadowLayer = itemLayer.topLeftShadowLayer self.layer.contents = itemLayer.contents } @@ -745,6 +896,17 @@ private final class ItemTransitionView: UIView { self.copyTopRightShadowLayer = copyLayer } + if let topLeftShadowLayer { + let copyLayer = SimpleLayer() + copyLayer.contents = topLeftShadowLayer.contents + copyLayer.contentsRect = topLeftShadowLayer.contentsRect + copyLayer.contentsGravity = topLeftShadowLayer.contentsGravity + copyLayer.contentsScale = topLeftShadowLayer.contentsScale + copyLayer.frame = topLeftShadowLayer.frame + self.layer.addSublayer(copyLayer) + self.copyTopLeftShadowLayer = copyLayer + } + if let viewCountLayer { let copyViewCountLayer = SimpleLayer() copyViewCountLayer.contents = viewCountLayer.contents @@ -771,6 +933,19 @@ private final class ItemTransitionView: UIView { self.privacyTypeLayerTopRightPosition = CGPoint(x: itemLayer.bounds.width - privacyTypeLayer.frame.maxX, y: privacyTypeLayer.frame.minY) } + if let authorLayer = authorLayer as? DurationLayer { + let copyAuthorLayer = DurationLayer() + copyAuthorLayer.contentsRect = authorLayer.contentsRect + copyAuthorLayer.contentsGravity = authorLayer.contentsGravity + copyAuthorLayer.contentsScale = authorLayer.contentsScale + copyAuthorLayer.frame = authorLayer.frame + copyAuthorLayer.copyAuthor(from: authorLayer) + self.layer.addSublayer(copyAuthorLayer) + self.copyAuthorLayer = copyAuthorLayer + + self.authorLayerTopLeftPosition = CGPoint(x: authorLayer.frame.minX, y: authorLayer.frame.minY) + } + if let durationLayer { let copyDurationLayer = SimpleLayer() copyDurationLayer.contents = durationLayer.contents @@ -805,6 +980,10 @@ private final class ItemTransitionView: UIView { transition.setFrame(layer: privacyTypeLayer, frame: CGRect(origin: CGPoint(x: size.width - privacyTypeLayerTopRightPosition.x, y: privacyTypeLayerTopRightPosition.y), size: privacyTypeLayer.bounds.size)) } + if let authorLayer = self.copyAuthorLayer, let authorLayerTopLeftPosition = self.authorLayerTopLeftPosition { + transition.setFrame(layer: authorLayer, frame: CGRect(origin: CGPoint(x: authorLayerTopLeftPosition.x, y: authorLayerTopLeftPosition.y), size: authorLayer.bounds.size)) + } + if let copyLeftShadowLayer = self.copyLeftShadowLayer { transition.setFrame(layer: copyLeftShadowLayer, frame: CGRect(origin: CGPoint(x: 0.0, y: size.height - copyLeftShadowLayer.bounds.height), size: copyLeftShadowLayer.bounds.size)) } @@ -816,6 +995,10 @@ private final class ItemTransitionView: UIView { if let copyTopRightShadowLayer = self.copyTopRightShadowLayer { transition.setFrame(layer: copyTopRightShadowLayer, frame: CGRect(origin: CGPoint(x: size.width - copyTopRightShadowLayer.bounds.width, y: 0.0), size: copyTopRightShadowLayer.bounds.size)) } + + if let copyTopLeftShadowLayer = self.copyTopLeftShadowLayer { + transition.setFrame(layer: copyTopLeftShadowLayer, frame: CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: copyTopLeftShadowLayer.bounds.size)) + } } func updateSelection(theme: CheckNodeTheme, isSelected: Bool?, animated: Bool) { @@ -854,12 +1037,20 @@ private final class ItemTransitionView: UIView { copyPrivacyTypeLayer.animateAlpha(from: CGFloat(previousAlpha), to: CGFloat(privacyAlpha), duration: 0.2) } } + + if let copyAuthorLayer = self.copyAuthorLayer { + let privacyAlpha: Float = isSelected == nil ? 1.0 : 0.0 + if privacyAlpha != copyAuthorLayer.opacity { + let previousAlpha = copyAuthorLayer.opacity + copyAuthorLayer.opacity = privacyAlpha + copyAuthorLayer.animateAlpha(from: CGFloat(previousAlpha), to: CGFloat(privacyAlpha), duration: 0.2) + } + } } } private final class SparseItemGridBindingImpl: SparseItemGridBinding { let context: AccountContext - let chatLocation: ChatLocation let directMediaImageCache: DirectMediaImageCache let captureProtected: Bool let displayPrivacy: Bool @@ -881,9 +1072,8 @@ private final class SparseItemGridBindingImpl: SparseItemGridBinding { private var shimmerImages: [CGFloat: UIImage] = [:] - init(context: AccountContext, chatLocation: ChatLocation, directMediaImageCache: DirectMediaImageCache, captureProtected: Bool, displayPrivacy: Bool) { + init(context: AccountContext, directMediaImageCache: DirectMediaImageCache, captureProtected: Bool, displayPrivacy: Bool) { self.context = context - self.chatLocation = chatLocation self.directMediaImageCache = directMediaImageCache self.captureProtected = false self.displayPrivacy = displayPrivacy @@ -912,11 +1102,11 @@ private final class SparseItemGridBindingImpl: SparseItemGridBinding { func createLayer(item: SparseItemGrid.Item) -> SparseItemGridLayer? { if let item = item as? VisualMediaItem, item.story.isForwardingDisabled { - let layer = GenericItemLayer() + let layer = ItemLayer() setLayerDisableScreenshots(layer, true) return layer } else { - return GenericItemLayer() + return ItemLayer() } } @@ -1014,7 +1204,7 @@ private final class SparseItemGridBindingImpl: SparseItemGridBinding { } if let contents = layer.getContents(), !synchronousValue { - let copyLayer = GenericItemLayer() + let copyLayer = ItemLayer() copyLayer.contents = contents copyLayer.contentsRect = layer.contentsRect copyLayer.frame = layer.bounds @@ -1058,7 +1248,7 @@ private final class SparseItemGridBindingImpl: SparseItemGridBinding { } } - self.updateLayerData(story: story, item: item, selectedMedia: selectedMedia, layer: layer) + self.updateLayerData(story: story, item: item, selectedMedia: selectedMedia, layer: layer, synchronous: synchronous) } var isSelected: Bool? @@ -1071,7 +1261,7 @@ private final class SparseItemGridBindingImpl: SparseItemGridBinding { } } - func updateLayerData(story: EngineStoryItem, item: VisualMediaItem, selectedMedia: Media, layer: ItemLayer) { + func updateLayerData(story: EngineStoryItem, item: VisualMediaItem, selectedMedia: Media, layer: ItemLayer, synchronous: SparseItemGrid.Synchronous) { var viewCount: Int32? if let value = story.views?.seenCount { viewCount = Int32(value) @@ -1102,7 +1292,7 @@ private final class SparseItemGridBindingImpl: SparseItemGridBinding { isMin = layer.bounds.width < 80.0 } - layer.updateDuration(viewCount: viewCount, duration: duration, topRightIcon: topRightIcon, isMin: isMin, minFactor: min(1.0, layer.bounds.height / 74.0)) + layer.updateDuration(viewCount: viewCount, duration: duration, topRightIcon: topRightIcon, author: item.authorPeer, isMin: isMin, minFactor: min(1.0, layer.bounds.height / 74.0), directMediaImageCache: self.directMediaImageCache, synchronous: synchronous) } func unbindLayer(layer: SparseItemGridLayer) { @@ -1181,8 +1371,8 @@ public final class PeerInfoStoryPaneNode: ASDisplayNode, PeerInfoPaneNode, ASScr } private let context: AccountContext - private let peerId: PeerId - private let chatLocation: ChatLocation + private let peerId: PeerId? + private let searchQuery: String? private let isSaved: Bool private let isArchive: Bool private let isProfileEmbedded: Bool @@ -1265,7 +1455,7 @@ public final class PeerInfoStoryPaneNode: ASDisplayNode, PeerInfoPaneNode, ASScr return self.itemGrid.coveringInsetOffset } - private let listDisposable = MetaDisposable() + private var listDisposable: Disposable? private var hiddenMediaDisposable: Disposable? private let updateDisposable = MetaDisposable() @@ -1301,10 +1491,10 @@ public final class PeerInfoStoryPaneNode: ASDisplayNode, PeerInfoPaneNode, ASScr private weak var contextControllerToDismissOnSelection: ContextControllerProtocol? private weak var tempContextContentItemNode: TempExtractedItemNode? - public init(context: AccountContext, peerId: PeerId, chatLocation: ChatLocation, contentType: ContentType, captureProtected: Bool, isSaved: Bool, isArchive: Bool, isProfileEmbedded: Bool, canManageStories: Bool, navigationController: @escaping () -> NavigationController?, listContext: PeerStoryListContext?) { + public init(context: AccountContext, peerId: PeerId?, searchQuery: String? = nil, contentType: ContentType, captureProtected: Bool, isSaved: Bool, isArchive: Bool, isProfileEmbedded: Bool, canManageStories: Bool, navigationController: @escaping () -> NavigationController?, listContext: PeerStoryListContext?) { self.context = context self.peerId = peerId - self.chatLocation = chatLocation + self.searchQuery = searchQuery self.contentType = contentType self.contentTypePromise = ValuePromise(contentType) self.navigationController = navigationController @@ -1323,31 +1513,32 @@ public final class PeerInfoStoryPaneNode: ASDisplayNode, PeerInfoPaneNode, ASScr self.itemGridBinding = SparseItemGridBindingImpl( context: context, - chatLocation: .peer(id: peerId), directMediaImageCache: self.directMediaImageCache, captureProtected: captureProtected, displayPrivacy: isProfileEmbedded ) - self.listSource = listContext ?? PeerStoryListContext(account: context.account, peerId: peerId, isArchived: self.isArchive) + self.listSource = listContext ?? PeerStoryListContext(account: context.account, peerId: peerId ?? context.account.peerId, isArchived: self.isArchive) self.calendarSource = nil super.init() - let _ = (ApplicationSpecificNotice.getSharedMediaScrollingTooltip(accountManager: context.sharedContext.accountManager) - |> deliverOnMainQueue).start(next: { [weak self] count in - guard let strongSelf = self else { - return - } - if count < 1 { - strongSelf.itemGrid.updateScrollingAreaTooltip(tooltip: SparseItemGridScrollingArea.DisplayTooltip(animation: "anim_infotip", text: strongSelf.itemGridBinding.chatPresentationData.strings.SharedMedia_FastScrollTooltip, completed: { - guard let strongSelf = self else { - return - } - let _ = ApplicationSpecificNotice.incrementSharedMediaScrollingTooltip(accountManager: strongSelf.context.sharedContext.accountManager, count: 1).start() - })) - } - }) + if self.peerId != nil { + let _ = (ApplicationSpecificNotice.getSharedMediaScrollingTooltip(accountManager: context.sharedContext.accountManager) + |> deliverOnMainQueue).start(next: { [weak self] count in + guard let strongSelf = self else { + return + } + if count < 1 { + strongSelf.itemGrid.updateScrollingAreaTooltip(tooltip: SparseItemGridScrollingArea.DisplayTooltip(animation: "anim_infotip", text: strongSelf.itemGridBinding.chatPresentationData.strings.SharedMedia_FastScrollTooltip, completed: { + guard let strongSelf = self else { + return + } + let _ = ApplicationSpecificNotice.incrementSharedMediaScrollingTooltip(accountManager: strongSelf.context.sharedContext.accountManager, count: 1).start() + })) + } + }) + } self.itemGridBinding.loadHoleImpl = { [weak self] hole, location in guard let strongSelf = self else { @@ -1379,10 +1570,10 @@ public final class PeerInfoStoryPaneNode: ASDisplayNode, PeerInfoPaneNode, ASScr return } - //TODO:selection + //TODO:localize let listContext = PeerStoryListContentContextImpl( context: self.context, - peerId: self.peerId, + peerId: self.peerId ?? self.context.account.peerId, listContext: self.listSource, initialId: item.story.id ) @@ -1758,12 +1949,12 @@ public final class PeerInfoStoryPaneNode: ASDisplayNode, PeerInfoPaneNode, ASScr self.requestHistoryAroundVisiblePosition(synchronous: false, reloadAtTop: false) if peerId == context.account.peerId && !isArchive { - self.preloadArchiveListContext = PeerStoryListContext(account: context.account, peerId: peerId, isArchived: true) + self.preloadArchiveListContext = PeerStoryListContext(account: context.account, peerId: context.account.peerId, isArchived: true) } } deinit { - self.listDisposable.dispose() + self.listDisposable?.dispose() self.hiddenMediaDisposable?.dispose() self.animationTimer?.invalidate() self.presentationDataDisposable?.dispose() @@ -1786,235 +1977,227 @@ public final class PeerInfoStoryPaneNode: ASDisplayNode, PeerInfoPaneNode, ASScr } private func openContextMenu(item: EngineStoryItem, itemLayer: ItemLayer, rect: CGRect, gesture: ContextGesture?) { - let _ = (self.context.engine.data.get( - TelegramEngine.EngineData.Item.Peer.Peer(id: self.peerId) - ) - |> deliverOnMainQueue).start(next: { [weak self] _ in - guard let self else { - return - } - guard let parentController = self.parentController else { - return - } + guard let parentController = self.parentController else { + return + } + + let canManage = self.canManageStories + + var items: [ContextMenuItem] = [] + + if canManage, let peerId = self.peerId { + items.append(.action(ContextMenuActionItem(text: !self.isArchive ? self.presentationData.strings.StoryList_ItemAction_Archive : self.presentationData.strings.StoryList_ItemAction_Unarchive, icon: { theme in generateTintedImage(image: UIImage(bundleImageName: self.isArchive ? "Chat/Context Menu/Archive" : "Chat/Context Menu/Unarchive"), color: theme.contextMenu.primaryColor) }, action: { [weak self] _, f in + guard let self else { + f(.default) + return + } + + if self.isArchive { + f(.default) + } else { + f(.dismissWithoutContent) + } + + let _ = self.context.engine.messages.updateStoriesArePinned(peerId: peerId, ids: [item.id: item], isPinned: self.isArchive ? true : false).startStandalone() + self.parentController?.present(UndoOverlayController(presentationData: self.presentationData, content: .actionSucceeded(title: nil, text: self.isArchive ? self.presentationData.strings.StoryList_ToastUnarchived_Text(1) : self.presentationData.strings.StoryList_ToastArchived_Text(1), cancel: nil, destructive: false), elevatedLayout: false, animateInAsReplacement: false, action: { _ in return false }), in: .current) + }))) - let canManage = self.canManageStories - - var items: [ContextMenuItem] = [] - - if canManage { - items.append(.action(ContextMenuActionItem(text: !self.isArchive ? self.presentationData.strings.StoryList_ItemAction_Archive : self.presentationData.strings.StoryList_ItemAction_Unarchive, icon: { theme in generateTintedImage(image: UIImage(bundleImageName: self.isArchive ? "Chat/Context Menu/Archive" : "Chat/Context Menu/Unarchive"), color: theme.contextMenu.primaryColor) }, action: { [weak self] _, f in + if !self.isArchive { + let isPinned = self.pinnedIds.contains(item.id) + items.append(.action(ContextMenuActionItem(text: isPinned ? self.presentationData.strings.StoryList_ItemAction_Unpin : self.presentationData.strings.StoryList_ItemAction_Pin, icon: { theme in generateTintedImage(image: UIImage(bundleImageName: isPinned ? "Chat/Context Menu/Unpin" : "Chat/Context Menu/Pin"), color: theme.contextMenu.primaryColor) }, action: { [weak self, weak itemLayer] _, f in + itemLayer?.isHidden = false guard let self else { f(.default) return } - if self.isArchive { + if !isPinned && self.pinnedIds.count >= 3 { f(.default) - } else { - f(.dismissWithoutContent) - } - - let _ = self.context.engine.messages.updateStoriesArePinned(peerId: self.peerId, ids: [item.id: item], isPinned: self.isArchive ? true : false).startStandalone() - self.parentController?.present(UndoOverlayController(presentationData: self.presentationData, content: .actionSucceeded(title: nil, text: self.isArchive ? self.presentationData.strings.StoryList_ToastUnarchived_Text(1) : self.presentationData.strings.StoryList_ToastArchived_Text(1), cancel: nil, destructive: false), elevatedLayout: false, animateInAsReplacement: false, action: { _ in return false }), in: .current) - }))) - - if !self.isArchive { - let isPinned = self.pinnedIds.contains(item.id) - items.append(.action(ContextMenuActionItem(text: isPinned ? self.presentationData.strings.StoryList_ItemAction_Unpin : self.presentationData.strings.StoryList_ItemAction_Pin, icon: { theme in generateTintedImage(image: UIImage(bundleImageName: isPinned ? "Chat/Context Menu/Unpin" : "Chat/Context Menu/Pin"), color: theme.contextMenu.primaryColor) }, action: { [weak self, weak itemLayer] _, f in - itemLayer?.isHidden = false - guard let self else { - f(.default) - return - } - - if !isPinned && self.pinnedIds.count >= 3 { - f(.default) - - let presentationData = self.presentationData - self.parentController?.present(UndoOverlayController(presentationData: presentationData, content: .info(title: nil, text: presentationData.strings.StoryList_ToastPinLimit_Text(Int32(3)), timeout: nil, customUndoText: nil), elevatedLayout: false, animateInAsReplacement: false, action: { _ in return false }), in: .current) - - return - } - - f(.dismissWithoutContent) - - var updatedPinnedIds = self.pinnedIds - if isPinned { - updatedPinnedIds.remove(item.id) - } else { - updatedPinnedIds.insert(item.id) - } - let _ = self.context.engine.messages.updatePinnedToTopStories(peerId: self.peerId, ids: Array(updatedPinnedIds)).startStandalone() let presentationData = self.presentationData + self.parentController?.present(UndoOverlayController(presentationData: presentationData, content: .info(title: nil, text: presentationData.strings.StoryList_ToastPinLimit_Text(Int32(3)), timeout: nil, customUndoText: nil), elevatedLayout: false, animateInAsReplacement: false, action: { _ in return false }), in: .current) - let toastTitle: String? - let toastText: String - if isPinned { - toastTitle = nil - toastText = presentationData.strings.StoryList_ToastUnpinned_Text(1) - } else { - toastTitle = presentationData.strings.StoryList_ToastPinned_Title(1) - toastText = presentationData.strings.StoryList_ToastPinned_Text(1) - } - self.parentController?.present(UndoOverlayController(presentationData: presentationData, content: .universal(animation: isPinned ? "anim_toastunpin" : "anim_toastpin", scale: 0.06, colors: [:], title: toastTitle, text: toastText, customUndoText: nil, timeout: 5), elevatedLayout: false, animateInAsReplacement: false, action: { _ in return false }), in: .current) - }))) - } - - items.append(.action(ContextMenuActionItem(text: self.presentationData.strings.StoryList_ItemAction_Edit, icon: { theme in generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Edit"), color: theme.contextMenu.primaryColor) }, action: { [weak self] c, _ in - c?.dismiss(completion: { - guard let self else { - return - } - let _ = (self.context.engine.data.get( - TelegramEngine.EngineData.Item.Peer.Peer(id: self.peerId) - ) - |> deliverOnMainQueue).startStandalone(next: { [weak self] peer in - guard let self, let peer else { - return - } - - var foundItemLayer: SparseItemGridLayer? - var sourceImage: UIImage? - self.itemGrid.forEachVisibleItem { gridItem in - guard let itemLayer = gridItem.layer as? ItemLayer else { - return - } - if let listItem = itemLayer.item, listItem.story.id == item.id { - foundItemLayer = itemLayer - if let contents = itemLayer.contents, CFGetTypeID(contents as CFTypeRef) == CGImage.typeID { - sourceImage = UIImage(cgImage: contents as! CGImage) - } - } - } - - guard let controller = MediaEditorScreen.makeEditStoryController( - context: self.context, - peer: peer, - storyItem: item, - videoPlaybackPosition: nil, - repost: false, - transitionIn: .gallery(MediaEditorScreen.TransitionIn.GalleryTransitionIn(sourceView: self.itemGrid.view, sourceRect: foundItemLayer?.frame ?? .zero, sourceImage: sourceImage)), - transitionOut: MediaEditorScreen.TransitionOut(destinationView: self.itemGrid.view, destinationRect: foundItemLayer?.frame ?? .zero, destinationCornerRadius: 0.0), - update: { [weak self] disposable in - guard let self else { - return - } - self.updateDisposable.set(disposable) - } - ) else { - return - } - self.parentController?.push(controller) - }) - }) - }))) - } - - if !item.isForwardingDisabled, case .everyone = item.privacy?.base { - items.append(.action(ContextMenuActionItem(text: self.presentationData.strings.StoryList_ItemAction_Forward, icon: { theme in generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Forward"), color: theme.contextMenu.primaryColor) }, action: { [weak self] c, _ in - c?.dismiss(completion: { - guard let self else { - return - } - - let _ = (self.context.engine.data.get( - TelegramEngine.EngineData.Item.Peer.Peer(id: self.peerId) - ) - |> deliverOnMainQueue).startStandalone(next: { [weak self] peer in - guard let self else { - return - } - guard let peer, let peerReference = PeerReference(peer._asPeer()) else { - return - } - - let shareController = ShareController( - context: self.context, - subject: .media(.story(peer: peerReference, id: item.id, media: TelegramMediaStory(storyId: StoryId(peerId: self.peerId, id: item.id), isMention: false))), - presetText: nil, - preferredAction: .default, - showInChat: nil, - fromForeignApp: false, - segmentedValues: nil, - externalShare: false, - immediateExternalShare: false, - switchableAccounts: [], - immediatePeerId: nil, - updatedPresentationData: nil, - forceTheme: nil, - forcedActionTitle: nil, - shareAsLink: false, - collectibleItemInfo: nil - ) - self.parentController?.present(shareController, in: .window(.root)) - }) - }) - }))) - } - - if canManage { - items.append(.action(ContextMenuActionItem(text: self.presentationData.strings.StoryList_ItemAction_Delete, textColor: .destructive, icon: { theme in generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Delete"), color: theme.contextMenu.destructiveColor) }, action: { [weak self] c, _ in - c?.dismiss(completion: { - guard let self else { - return - } - - self.presentDeleteConfirmation(ids: Set([item.id])) - }) - }))) - } - - if self.canManageStories { - if !items.isEmpty { - items.append(.separator) - } - items.append(.action(ContextMenuActionItem(text: self.presentationData.strings.Conversation_ContextMenuSelect, icon: { theme in - return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Select"), color: theme.actionSheet.primaryTextColor) - }, action: { [weak self] c, f in - guard let self, let parentController = self.parentController as? PeerInfoScreen else { - f(.default) return } - self.contextControllerToDismissOnSelection = c - parentController.toggleStorySelection(ids: [item.id], isSelected: true) + f(.dismissWithoutContent) - DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + 0.5, execute: { [weak self] in - guard let self, let contextControllerToDismissOnSelection = self.contextControllerToDismissOnSelection else { - return - } - if let contextControllerToDismissOnSelection = contextControllerToDismissOnSelection as? ContextController { - contextControllerToDismissOnSelection.dismissWithCustomTransition(transition: .animated(duration: 0.4, curve: .spring), completion: nil) - } - }) + var updatedPinnedIds = self.pinnedIds + if isPinned { + updatedPinnedIds.remove(item.id) + } else { + updatedPinnedIds.insert(item.id) + } + let _ = self.context.engine.messages.updatePinnedToTopStories(peerId: peerId, ids: Array(updatedPinnedIds)).startStandalone() + + let presentationData = self.presentationData + + let toastTitle: String? + let toastText: String + if isPinned { + toastTitle = nil + toastText = presentationData.strings.StoryList_ToastUnpinned_Text(1) + } else { + toastTitle = presentationData.strings.StoryList_ToastPinned_Title(1) + toastText = presentationData.strings.StoryList_ToastPinned_Text(1) + } + self.parentController?.present(UndoOverlayController(presentationData: presentationData, content: .universal(animation: isPinned ? "anim_toastunpin" : "anim_toastpin", scale: 0.06, colors: [:], title: toastTitle, text: toastText, customUndoText: nil, timeout: 5), elevatedLayout: false, animateInAsReplacement: false, action: { _ in return false }), in: .current) }))) } - if items.isEmpty { - return + items.append(.action(ContextMenuActionItem(text: self.presentationData.strings.StoryList_ItemAction_Edit, icon: { theme in generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Edit"), color: theme.contextMenu.primaryColor) }, action: { [weak self] c, _ in + c?.dismiss(completion: { + guard let self else { + return + } + let _ = (self.context.engine.data.get( + TelegramEngine.EngineData.Item.Peer.Peer(id: peerId) + ) + |> deliverOnMainQueue).startStandalone(next: { [weak self] peer in + guard let self, let peer else { + return + } + + var foundItemLayer: SparseItemGridLayer? + var sourceImage: UIImage? + self.itemGrid.forEachVisibleItem { gridItem in + guard let itemLayer = gridItem.layer as? ItemLayer else { + return + } + if let listItem = itemLayer.item, listItem.story.id == item.id { + foundItemLayer = itemLayer + if let contents = itemLayer.contents, CFGetTypeID(contents as CFTypeRef) == CGImage.typeID { + sourceImage = UIImage(cgImage: contents as! CGImage) + } + } + } + + guard let controller = MediaEditorScreen.makeEditStoryController( + context: self.context, + peer: peer, + storyItem: item, + videoPlaybackPosition: nil, + repost: false, + transitionIn: .gallery(MediaEditorScreen.TransitionIn.GalleryTransitionIn(sourceView: self.itemGrid.view, sourceRect: foundItemLayer?.frame ?? .zero, sourceImage: sourceImage)), + transitionOut: MediaEditorScreen.TransitionOut(destinationView: self.itemGrid.view, destinationRect: foundItemLayer?.frame ?? .zero, destinationCornerRadius: 0.0), + update: { [weak self] disposable in + guard let self else { + return + } + self.updateDisposable.set(disposable) + } + ) else { + return + } + self.parentController?.push(controller) + }) + }) + }))) + } + + if !item.isForwardingDisabled, case .everyone = item.privacy?.base { + items.append(.action(ContextMenuActionItem(text: self.presentationData.strings.StoryList_ItemAction_Forward, icon: { theme in generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Forward"), color: theme.contextMenu.primaryColor) }, action: { [weak self] c, _ in + c?.dismiss(completion: { + guard let self, let peerId = self.peerId else { + return + } + + let _ = (self.context.engine.data.get( + TelegramEngine.EngineData.Item.Peer.Peer(id: peerId) + ) + |> deliverOnMainQueue).startStandalone(next: { [weak self] peer in + guard let self else { + return + } + guard let peer, let peerReference = PeerReference(peer._asPeer()) else { + return + } + + let shareController = ShareController( + context: self.context, + subject: .media(.story(peer: peerReference, id: item.id, media: TelegramMediaStory(storyId: StoryId(peerId: peer.id, id: item.id), isMention: false))), + presetText: nil, + preferredAction: .default, + showInChat: nil, + fromForeignApp: false, + segmentedValues: nil, + externalShare: false, + immediateExternalShare: false, + switchableAccounts: [], + immediatePeerId: nil, + updatedPresentationData: nil, + forceTheme: nil, + forcedActionTitle: nil, + shareAsLink: false, + collectibleItemInfo: nil + ) + self.parentController?.present(shareController, in: .window(.root)) + }) + }) + }))) + } + + if canManage { + items.append(.action(ContextMenuActionItem(text: self.presentationData.strings.StoryList_ItemAction_Delete, textColor: .destructive, icon: { theme in generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Delete"), color: theme.contextMenu.destructiveColor) }, action: { [weak self] c, _ in + c?.dismiss(completion: { + guard let self else { + return + } + + self.presentDeleteConfirmation(ids: Set([item.id])) + }) + }))) + } + + if self.canManageStories { + if !items.isEmpty { + items.append(.separator) } - - let tempSourceNode = TempExtractedItemNode( - item: item, - itemLayer: itemLayer - ) - tempSourceNode.frame = rect - tempSourceNode.update(size: rect.size) - - let scaleSide = itemLayer.bounds.width - let minScale: CGFloat = max(0.7, (scaleSide - 15.0) / scaleSide) - let currentScale = minScale - - ContainedViewLayoutTransition.immediate.updateSublayerTransformScale(node: tempSourceNode.contextSourceNode.contentNode, scale: currentScale) - ContainedViewLayoutTransition.immediate.updateTransformScale(layer: itemLayer, scale: 1.0) - - self.tempContextContentItemNode = tempSourceNode - self.addSubnode(tempSourceNode) - - let contextController = ContextController(presentationData: self.presentationData, source: .extracted(ExtractedContentSourceImpl(controller: parentController, sourceNode: tempSourceNode.contextSourceNode, keepInPlace: false, blurBackground: true)), items: .single(ContextController.Items(content: .list(items))), gesture: gesture) - parentController.presentInGlobalOverlay(contextController) - }) + items.append(.action(ContextMenuActionItem(text: self.presentationData.strings.Conversation_ContextMenuSelect, icon: { theme in + return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Select"), color: theme.actionSheet.primaryTextColor) + }, action: { [weak self] c, f in + guard let self, let parentController = self.parentController as? PeerInfoScreen else { + f(.default) + return + } + + self.contextControllerToDismissOnSelection = c + parentController.toggleStorySelection(ids: [item.id], isSelected: true) + + DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + 0.5, execute: { [weak self] in + guard let self, let contextControllerToDismissOnSelection = self.contextControllerToDismissOnSelection else { + return + } + if let contextControllerToDismissOnSelection = contextControllerToDismissOnSelection as? ContextController { + contextControllerToDismissOnSelection.dismissWithCustomTransition(transition: .animated(duration: 0.4, curve: .spring), completion: nil) + } + }) + }))) + } + + if items.isEmpty { + return + } + + let tempSourceNode = TempExtractedItemNode( + item: item, + itemLayer: itemLayer + ) + tempSourceNode.frame = rect + tempSourceNode.update(size: rect.size) + + let scaleSide = itemLayer.bounds.width + let minScale: CGFloat = max(0.7, (scaleSide - 15.0) / scaleSide) + let currentScale = minScale + + ContainedViewLayoutTransition.immediate.updateSublayerTransformScale(node: tempSourceNode.contextSourceNode.contentNode, scale: currentScale) + ContainedViewLayoutTransition.immediate.updateTransformScale(layer: itemLayer, scale: 1.0) + + self.tempContextContentItemNode = tempSourceNode + self.addSubnode(tempSourceNode) + + let contextController = ContextController(presentationData: self.presentationData, source: .extracted(ExtractedContentSourceImpl(controller: parentController, sourceNode: tempSourceNode.contextSourceNode, keepInPlace: false, blurBackground: true)), items: .single(ContextController.Items(content: .list(items))), gesture: gesture) + parentController.presentInGlobalOverlay(contextController) } public func updateContentType(contentType: ContentType) { @@ -2053,9 +2236,38 @@ public final class PeerInfoStoryPaneNode: ASDisplayNode, PeerInfoPaneNode, ASScr self.isRequestingView = true var firstTime = true let queue = Queue() + + let authorPeer: Signal + if self.searchQuery != nil { + authorPeer = self.context.engine.data.get( + TelegramEngine.EngineData.Item.Peer.Peer(id: self.context.account.peerId) + ) + } else { + authorPeer = .single(nil) + } + + var state = self.listSource.state + if self.peerId == nil && self.listDisposable == nil { + state = .single(PeerStoryListContext.State( + peerReference: nil, + items: [], + pinnedIds: Set(), + totalCount: 0, + loadMoreToken: 0, + isCached: false, + hasCache: false, + allEntityFiles: [:] + )) |> then(state |> delay(2.0, queue: .mainQueue())) + } + + self.listDisposable?.dispose() + self.listDisposable = nil - self.listDisposable.set((self.listSource.state - |> deliverOn(queue)).start(next: { [weak self] state in + self.listDisposable = (combineLatest( + state, + authorPeer + ) + |> deliverOn(queue)).startStrict(next: { [weak self] state, authorPeer in guard let self else { return } @@ -2085,6 +2297,7 @@ public final class PeerInfoStoryPaneNode: ASDisplayNode, PeerInfoPaneNode, ASScr index: mappedItems.count, peer: peerReference, story: item, + authorPeer: authorPeer, isPinned: state.pinnedIds.contains(item.id), localMonthTimestamp: Month(localTimestamp: item.timestamp + timezoneOffset).packedValue )) @@ -2125,7 +2338,7 @@ public final class PeerInfoStoryPaneNode: ASDisplayNode, PeerInfoPaneNode, ASScr strongSelf.updateHistory(items: items, pinnedIds: state.pinnedIds, synchronous: currentSynchronous, reloadAtTop: currentReloadAtTop) strongSelf.isRequestingView = false } - })) + }) } private func updateHistory(items: SparseItemGrid.Items, pinnedIds: Set, synchronous: Bool, reloadAtTop: Bool) { @@ -2446,6 +2659,10 @@ public final class PeerInfoStoryPaneNode: ASDisplayNode, PeerInfoPaneNode, ASScr } private func presentDeleteConfirmation(ids: Set) { + guard let peerId = self.peerId else { + return + } + let presentationData = self.presentationData let controller = ActionSheetController(presentationData: presentationData) let dismissAction: () -> Void = { [weak controller] in @@ -2468,7 +2685,7 @@ public final class PeerInfoStoryPaneNode: ASDisplayNode, PeerInfoPaneNode, ASScr parentController.cancelItemSelection() } - let _ = self.context.engine.messages.deleteStories(peerId: self.peerId, ids: Array(ids)).startStandalone() + let _ = self.context.engine.messages.deleteStories(peerId: peerId, ids: Array(ids)).startStandalone() }) ]), ActionSheetItemGroup(items: [ActionSheetButtonItem(title: presentationData.strings.Common_Cancel, action: { dismissAction() })]) @@ -2480,7 +2697,7 @@ public final class PeerInfoStoryPaneNode: ASDisplayNode, PeerInfoPaneNode, ASScr self.currentParams = (size, topInset, sideInset, bottomInset, deviceMetrics, visibleHeight, isScrollingLockedAtTop, expandProgress, navigationHeight, presentationData) var bottomInset = bottomInset - if self.isProfileEmbedded, let selectedIds = self.itemInteraction.selectedIds, self.canManageStories { + if self.isProfileEmbedded, let selectedIds = self.itemInteraction.selectedIds, self.canManageStories, let peerId = self.peerId { let selectionPanel: ComponentView var selectionPanelTransition = Transition(transition) if let current = self.selectionPanel { @@ -2514,7 +2731,7 @@ public final class PeerInfoStoryPaneNode: ASDisplayNode, PeerInfoPaneNode, ASScr let toastText = presentationData.strings.StoryList_ToastPinLimit_Text(3) self.parentController?.present(UndoOverlayController(presentationData: presentationData, content: .universal(animation: "anim_infotip", scale: 1.0, colors: ["info1.info1.stroke": animationBackgroundColor, "info2.info2.Fill": animationBackgroundColor], title: nil, text: toastText, customUndoText: nil, timeout: 5), elevatedLayout: false, animateInAsReplacement: false, action: { _ in return false }), in: .current) } else { - let _ = self.context.engine.messages.updatePinnedToTopStories(peerId: self.peerId, ids: Array(updatedPinnedIds)).startStandalone() + let _ = self.context.engine.messages.updatePinnedToTopStories(peerId: peerId, ids: Array(updatedPinnedIds)).startStandalone() let presentationData = self.presentationData @@ -2532,7 +2749,7 @@ public final class PeerInfoStoryPaneNode: ASDisplayNode, PeerInfoPaneNode, ASScr for id in selectedIds { updatedPinnedIds.remove(id) } - let _ = self.context.engine.messages.updatePinnedToTopStories(peerId: self.peerId, ids: Array(updatedPinnedIds)).startStandalone() + let _ = self.context.engine.messages.updatePinnedToTopStories(peerId: peerId, ids: Array(updatedPinnedIds)).startStandalone() let presentationData = self.presentationData @@ -2563,7 +2780,7 @@ public final class PeerInfoStoryPaneNode: ASDisplayNode, PeerInfoPaneNode, ASScr parentController.cancelItemSelection() } - let _ = self.context.engine.messages.updateStoriesArePinned(peerId: self.peerId, ids: items, isPinned: self.isArchive ? true : false).startStandalone() + let _ = self.context.engine.messages.updateStoriesArePinned(peerId: peerId, ids: items, isPinned: self.isArchive ? true : false).startStandalone() let text: String if self.isArchive { diff --git a/submodules/TelegramUI/Components/PeerSelectionController/Sources/PeerSelectionController.swift b/submodules/TelegramUI/Components/PeerSelectionController/Sources/PeerSelectionController.swift index 9a9812b207..2c448b1054 100644 --- a/submodules/TelegramUI/Components/PeerSelectionController/Sources/PeerSelectionController.swift +++ b/submodules/TelegramUI/Components/PeerSelectionController/Sources/PeerSelectionController.swift @@ -19,7 +19,7 @@ public final class PeerSelectionControllerImpl: ViewController, PeerSelectionCon private var customTitle: String? public var peerSelected: ((EnginePeer, Int64?) -> Void)? - public var multiplePeersSelected: (([EnginePeer], [EnginePeer.Id: EnginePeer], NSAttributedString, AttachmentTextInputPanelSendMode, ChatInterfaceForwardOptionsState?, ChatSendMessageActionSheetController.MessageEffect?) -> Void)? + public var multiplePeersSelected: (([EnginePeer], [EnginePeer.Id: EnginePeer], NSAttributedString, AttachmentTextInputPanelSendMode, ChatInterfaceForwardOptionsState?, ChatSendMessageActionSheetController.SendParameters?) -> Void)? private let filter: ChatListNodePeersFilter private let forumPeerId: EnginePeer.Id? private let selectForumThreads: Bool diff --git a/submodules/TelegramUI/Components/PeerSelectionController/Sources/PeerSelectionControllerNode.swift b/submodules/TelegramUI/Components/PeerSelectionController/Sources/PeerSelectionControllerNode.swift index 5ae13bf4dc..3edd665584 100644 --- a/submodules/TelegramUI/Components/PeerSelectionController/Sources/PeerSelectionControllerNode.swift +++ b/submodules/TelegramUI/Components/PeerSelectionController/Sources/PeerSelectionControllerNode.swift @@ -83,7 +83,7 @@ final class PeerSelectionControllerNode: ASDisplayNode { var requestOpenDisabledPeer: ((EnginePeer, Int64?, ChatListDisabledPeerReason) -> Void)? var requestOpenPeerFromSearch: ((EnginePeer, Int64?) -> Void)? var requestOpenMessageFromSearch: ((EnginePeer, Int64?, EngineMessage.Id) -> Void)? - var requestSend: (([EnginePeer], [EnginePeer.Id: EnginePeer], NSAttributedString, AttachmentTextInputPanelSendMode, ChatInterfaceForwardOptionsState?, ChatSendMessageActionSheetController.MessageEffect?) -> Void)? + var requestSend: (([EnginePeer], [EnginePeer.Id: EnginePeer], NSAttributedString, AttachmentTextInputPanelSendMode, ChatInterfaceForwardOptionsState?, ChatSendMessageActionSheetController.SendParameters?) -> Void)? private var presentationData: PresentationData { didSet { diff --git a/submodules/TelegramUI/Components/PremiumGiftAttachmentScreen/Sources/PremiumGiftAttachmentScreen.swift b/submodules/TelegramUI/Components/PremiumGiftAttachmentScreen/Sources/PremiumGiftAttachmentScreen.swift index 092f4b8b73..102e7b1fd6 100644 --- a/submodules/TelegramUI/Components/PremiumGiftAttachmentScreen/Sources/PremiumGiftAttachmentScreen.swift +++ b/submodules/TelegramUI/Components/PremiumGiftAttachmentScreen/Sources/PremiumGiftAttachmentScreen.swift @@ -34,6 +34,17 @@ private final class PremiumGiftContext: AttachmentMediaPickerContext { return .single(nil) } + var hasCaption: Bool { + return false + } + + var captionIsAboveMedia: Signal { + return .single(false) + } + + func setCaptionIsAboveMedia(_ captionIsAboveMedia: Bool) -> Void { + } + public var loadingProgress: Signal { return .single(nil) } @@ -49,10 +60,10 @@ private final class PremiumGiftContext: AttachmentMediaPickerContext { func setCaption(_ caption: NSAttributedString) { } - func send(mode: AttachmentMediaPickerSendMode, attachmentMode: AttachmentMediaPickerAttachmentMode, messageEffect: ChatSendMessageActionSheetController.MessageEffect?) { + func send(mode: AttachmentMediaPickerSendMode, attachmentMode: AttachmentMediaPickerAttachmentMode, parameters: ChatSendMessageActionSheetController.SendParameters?) { } - func schedule(messageEffect: ChatSendMessageActionSheetController.MessageEffect?) { + func schedule(parameters: ChatSendMessageActionSheetController.SendParameters?) { } func mainButtonAction() { diff --git a/submodules/TelegramUI/Components/Settings/WallpaperGridScreen/Sources/ThemeColorsGridController.swift b/submodules/TelegramUI/Components/Settings/WallpaperGridScreen/Sources/ThemeColorsGridController.swift index de6c53c741..a6fcaa6510 100644 --- a/submodules/TelegramUI/Components/Settings/WallpaperGridScreen/Sources/ThemeColorsGridController.swift +++ b/submodules/TelegramUI/Components/Settings/WallpaperGridScreen/Sources/ThemeColorsGridController.swift @@ -362,6 +362,17 @@ private final class ThemeColorsGridContext: AttachmentMediaPickerContext { return .single(nil) } + var hasCaption: Bool { + return false + } + + var captionIsAboveMedia: Signal { + return .single(false) + } + + func setCaptionIsAboveMedia(_ captionIsAboveMedia: Bool) -> Void { + } + public var loadingProgress: Signal { return .single(nil) } @@ -377,10 +388,10 @@ private final class ThemeColorsGridContext: AttachmentMediaPickerContext { func setCaption(_ caption: NSAttributedString) { } - func send(mode: AttachmentMediaPickerSendMode, attachmentMode: AttachmentMediaPickerAttachmentMode, messageEffect: ChatSendMessageActionSheetController.MessageEffect?) { + func send(mode: AttachmentMediaPickerSendMode, attachmentMode: AttachmentMediaPickerAttachmentMode, parameters: ChatSendMessageActionSheetController.SendParameters?) { } - func schedule(messageEffect: ChatSendMessageActionSheetController.MessageEffect?) { + func schedule(parameters: ChatSendMessageActionSheetController.SendParameters?) { } func mainButtonAction() { diff --git a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerViewSendMessage.swift b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerViewSendMessage.swift index 0b1e6fbb75..61f5b9b18e 100644 --- a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerViewSendMessage.swift +++ b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerViewSendMessage.swift @@ -1556,14 +1556,14 @@ final class StoryItemSetContainerSendMessage { completion(controller, mediaPickerContext) }, updateMediaPickerContext: { [weak attachmentController] mediaPickerContext in attachmentController?.mediaPickerContext = mediaPickerContext - }, completion: { [weak self, weak view] signals, silentPosting, scheduleTime, messageEffect, getAnimatedTransitionSource, completion in + }, completion: { [weak self, weak view] signals, silentPosting, scheduleTime, parameters, getAnimatedTransitionSource, completion in guard let self, let view else { return } if !inputText.string.isEmpty { self.clearInputText(view: view) } - self.enqueueMediaMessages(view: view, peer: peer, replyToMessageId: nil, replyToStoryId: focusedStoryId, signals: signals, silentPosting: silentPosting, scheduleTime: scheduleTime, messageEffect: messageEffect, getAnimatedTransitionSource: getAnimatedTransitionSource, completion: completion) + self.enqueueMediaMessages(view: view, peer: peer, replyToMessageId: nil, replyToStoryId: focusedStoryId, signals: signals, silentPosting: silentPosting, scheduleTime: scheduleTime, parameters: parameters, getAnimatedTransitionSource: getAnimatedTransitionSource, completion: completion) } ) case .file: @@ -1658,7 +1658,7 @@ final class StoryItemSetContainerSendMessage { } self.controllerNavigationDisposable.set((contactsController.result |> deliverOnMainQueue).start(next: { [weak self, weak view] peers in - guard let self, let view, let (peers, _, silent, scheduleTime, text) = peers else { + guard let self, let view, let (peers, _, silent, scheduleTime, text, _) = peers else { return } @@ -1875,7 +1875,7 @@ final class StoryItemSetContainerSendMessage { bannedSendVideos: (Int32, Bool)?, present: @escaping (MediaPickerScreen, AttachmentMediaPickerContext?) -> Void, updateMediaPickerContext: @escaping (AttachmentMediaPickerContext?) -> Void, - completion: @escaping ([Any], Bool, Int32?, ChatSendMessageActionSheetController.MessageEffect?, @escaping (String) -> UIView?, @escaping () -> Void) -> Void + completion: @escaping ([Any], Bool, Int32?, ChatSendMessageActionSheetController.SendParameters?, @escaping (String) -> UIView?, @escaping () -> Void) -> Void ) { guard let component = view.component else { return @@ -2240,14 +2240,14 @@ final class StoryItemSetContainerSendMessage { present(controller, mediaPickerContext) }, updateMediaPickerContext: { _ in }, - completion: { [weak self, weak view] signals, silentPosting, scheduleTime, messageEffect, getAnimatedTransitionSource, completion in + completion: { [weak self, weak view] signals, silentPosting, scheduleTime, parameters, getAnimatedTransitionSource, completion in guard let self, let view else { return } if !inputText.string.isEmpty { self.clearInputText(view: view) } - self.enqueueMediaMessages(view: view, peer: peer, replyToMessageId: nil, replyToStoryId: focusedStoryId, signals: signals, silentPosting: silentPosting, scheduleTime: scheduleTime, messageEffect: messageEffect, getAnimatedTransitionSource: getAnimatedTransitionSource, completion: completion) + self.enqueueMediaMessages(view: view, peer: peer, replyToMessageId: nil, replyToStoryId: focusedStoryId, signals: signals, silentPosting: silentPosting, scheduleTime: scheduleTime, parameters: parameters, getAnimatedTransitionSource: getAnimatedTransitionSource, completion: completion) } ) } @@ -2569,7 +2569,7 @@ final class StoryItemSetContainerSendMessage { } } - private func enqueueMediaMessages(view: StoryItemSetContainerComponent.View, peer: EnginePeer, replyToMessageId: EngineMessage.Id?, replyToStoryId: StoryId?, signals: [Any]?, silentPosting: Bool, scheduleTime: Int32? = nil, messageEffect: ChatSendMessageActionSheetController.MessageEffect? = nil, getAnimatedTransitionSource: ((String) -> UIView?)? = nil, completion: @escaping () -> Void = {}) { + private func enqueueMediaMessages(view: StoryItemSetContainerComponent.View, peer: EnginePeer, replyToMessageId: EngineMessage.Id?, replyToStoryId: StoryId?, signals: [Any]?, silentPosting: Bool, scheduleTime: Int32? = nil, parameters: ChatSendMessageActionSheetController.SendParameters? = nil, getAnimatedTransitionSource: ((String) -> UIView?)? = nil, completion: @escaping () -> Void = {}) { guard let component = view.component else { return } @@ -2620,11 +2620,20 @@ final class StoryItemSetContainerSendMessage { } } } - if let messageEffect { - message = message.withUpdatedAttributes { attributes in - var attributes = attributes - attributes.append(EffectMessageAttribute(id: messageEffect.id)) - return attributes + if let parameters { + if let effect = parameters.effect { + message = message.withUpdatedAttributes { attributes in + var attributes = attributes + attributes.append(EffectMessageAttribute(id: effect.id)) + return attributes + } + } + if parameters.textIsAboveMedia { + message = message.withUpdatedAttributes { attributes in + var attributes = attributes + attributes.append(InvertMediaMessageAttribute()) + return attributes + } } } mappedMessages.append(message) @@ -2909,8 +2918,13 @@ final class StoryItemSetContainerSendMessage { return } if !hashtag.isEmpty { - let searchController = component.context.sharedContext.makeHashtagSearchController(context: component.context, peer: peer.flatMap(EnginePeer.init), query: hashtag, all: true) - navigationController.pushViewController(searchController) + if "".isEmpty { + let searchController = component.context.sharedContext.makeStorySearchController(context: component.context, query: hashtag) + navigationController.pushViewController(searchController) + } else { + let searchController = component.context.sharedContext.makeHashtagSearchController(context: component.context, peer: peer.flatMap(EnginePeer.init), query: hashtag, all: true) + navigationController.pushViewController(searchController) + } } })) } diff --git a/submodules/TelegramUI/Sources/AttachmentFileController.swift b/submodules/TelegramUI/Sources/AttachmentFileController.swift index aaecc8de2b..4ca7746f48 100644 --- a/submodules/TelegramUI/Sources/AttachmentFileController.swift +++ b/submodules/TelegramUI/Sources/AttachmentFileController.swift @@ -172,6 +172,17 @@ private final class AttachmentFileContext: AttachmentMediaPickerContext { return .single(nil) } + var hasCaption: Bool { + return false + } + + var captionIsAboveMedia: Signal { + return .single(false) + } + + func setCaptionIsAboveMedia(_ captionIsAboveMedia: Bool) -> Void { + } + public var loadingProgress: Signal { return .single(nil) } @@ -183,10 +194,10 @@ private final class AttachmentFileContext: AttachmentMediaPickerContext { func setCaption(_ caption: NSAttributedString) { } - func send(mode: AttachmentMediaPickerSendMode, attachmentMode: AttachmentMediaPickerAttachmentMode, messageEffect: ChatSendMessageActionSheetController.MessageEffect?) { + func send(mode: AttachmentMediaPickerSendMode, attachmentMode: AttachmentMediaPickerAttachmentMode, parameters: ChatSendMessageActionSheetController.SendParameters?) { } - func schedule(messageEffect: ChatSendMessageActionSheetController.MessageEffect?) { + func schedule(parameters: ChatSendMessageActionSheetController.SendParameters?) { } func mainButtonAction() { diff --git a/submodules/TelegramUI/Sources/Chat/ChatControllerPaste.swift b/submodules/TelegramUI/Sources/Chat/ChatControllerPaste.swift index 6d9f2abfe6..937c739c15 100644 --- a/submodules/TelegramUI/Sources/Chat/ChatControllerPaste.swift +++ b/submodules/TelegramUI/Sources/Chat/ChatControllerPaste.swift @@ -27,8 +27,8 @@ extension ChatControllerImpl { subjects: subjects, presentMediaPicker: { [weak self] subject, saveEditedPhotos, bannedSendPhotos, bannedSendVideos, present in if let strongSelf = self { - strongSelf.presentMediaPicker(subject: subject, saveEditedPhotos: saveEditedPhotos, bannedSendPhotos: bannedSendPhotos, bannedSendVideos: bannedSendVideos, present: present, updateMediaPickerContext: { _ in }, completion: { [weak self] signals, silentPosting, scheduleTime, messageEffect, getAnimatedTransitionSource, completion in - self?.enqueueMediaMessages(signals: signals, silentPosting: silentPosting, scheduleTime: scheduleTime, messageEffect: messageEffect, getAnimatedTransitionSource: getAnimatedTransitionSource, completion: completion) + strongSelf.presentMediaPicker(subject: subject, saveEditedPhotos: saveEditedPhotos, bannedSendPhotos: bannedSendPhotos, bannedSendVideos: bannedSendVideos, present: present, updateMediaPickerContext: { _ in }, completion: { [weak self] signals, silentPosting, scheduleTime, parameters, getAnimatedTransitionSource, completion in + self?.enqueueMediaMessages(signals: signals, silentPosting: silentPosting, scheduleTime: scheduleTime, parameters: parameters, getAnimatedTransitionSource: getAnimatedTransitionSource, completion: completion) }) } }, diff --git a/submodules/TelegramUI/Sources/Chat/ChatMessageDisplaySendMessageOptions.swift b/submodules/TelegramUI/Sources/Chat/ChatMessageDisplaySendMessageOptions.swift index 0261143f1e..eef0196e5d 100644 --- a/submodules/TelegramUI/Sources/Chat/ChatMessageDisplaySendMessageOptions.swift +++ b/submodules/TelegramUI/Sources/Chat/ChatMessageDisplaySendMessageOptions.swift @@ -15,7 +15,7 @@ import ChatControllerInteraction import ChatSendAudioMessageContextPreview extension ChatSendMessageEffect { - convenience init(_ effect: ChatSendMessageActionSheetController.MessageEffect) { + convenience init(_ effect: ChatSendMessageActionSheetController.SendParameters.Effect) { self.init(id: effect.id) } } @@ -114,17 +114,17 @@ func chatMessageDisplaySendMessageOptions(selfController: ChatControllerImpl, no } selfController.supportedOrientations = previousSupportedOrientations }, - sendMessage: { [weak selfController] mode, messageEffect in + sendMessage: { [weak selfController] mode, parameters in guard let selfController else { return } switch mode { case .generic: - selfController.controllerInteraction?.sendCurrentMessage(false, messageEffect.flatMap(ChatSendMessageEffect.init)) + selfController.controllerInteraction?.sendCurrentMessage(false, parameters?.effect.flatMap(ChatSendMessageEffect.init)) case .silently: - selfController.controllerInteraction?.sendCurrentMessage(true, messageEffect.flatMap(ChatSendMessageEffect.init)) + selfController.controllerInteraction?.sendCurrentMessage(true, parameters?.effect.flatMap(ChatSendMessageEffect.init)) case .whenOnline: - selfController.chatDisplayNode.sendCurrentMessage(scheduleTime: scheduleWhenOnlineTimestamp, messageEffect: messageEffect.flatMap(ChatSendMessageEffect.init)) { [weak selfController] in + selfController.chatDisplayNode.sendCurrentMessage(scheduleTime: scheduleWhenOnlineTimestamp, messageEffect: parameters?.effect.flatMap(ChatSendMessageEffect.init)) { [weak selfController] in guard let selfController else { return } @@ -135,7 +135,7 @@ func chatMessageDisplaySendMessageOptions(selfController: ChatControllerImpl, no } } }, - schedule: { [weak selfController] messageEffect in + schedule: { [weak selfController] effect in guard let selfController else { return } diff --git a/submodules/TelegramUI/Sources/ChatController.swift b/submodules/TelegramUI/Sources/ChatController.swift index 081f682a92..4cf2eca00a 100644 --- a/submodules/TelegramUI/Sources/ChatController.swift +++ b/submodules/TelegramUI/Sources/ChatController.swift @@ -9096,7 +9096,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G } } - func enqueueMediaMessages(signals: [Any]?, silentPosting: Bool, scheduleTime: Int32? = nil, messageEffect: ChatSendMessageActionSheetController.MessageEffect? = nil, getAnimatedTransitionSource: ((String) -> UIView?)? = nil, completion: @escaping () -> Void = {}) { + func enqueueMediaMessages(signals: [Any]?, silentPosting: Bool, scheduleTime: Int32? = nil, parameters: ChatSendMessageActionSheetController.SendParameters? = nil, getAnimatedTransitionSource: ((String) -> UIView?)? = nil, completion: @escaping () -> Void = {}) { self.enqueueMediaMessageDisposable.set((legacyAssetPickerEnqueueMessages(context: self.context, account: self.context.account, signals: signals!) |> deliverOnMainQueue).startStrict(next: { [weak self] items in if let strongSelf = self { @@ -9151,11 +9151,20 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G completionImpl = nil } - if let messageEffect { - message = message.withUpdatedAttributes { attributes in - var attributes = attributes - attributes.append(EffectMessageAttribute(id: messageEffect.id)) - return attributes + if let parameters { + if let effect = parameters.effect { + message = message.withUpdatedAttributes { attributes in + var attributes = attributes + attributes.append(EffectMessageAttribute(id: effect.id)) + return attributes + } + } + if parameters.textIsAboveMedia { + message = message.withUpdatedAttributes { attributes in + var attributes = attributes + attributes.append(InvertMediaMessageAttribute()) + return attributes + } } } diff --git a/submodules/TelegramUI/Sources/ChatControllerOpenAttachmentMenu.swift b/submodules/TelegramUI/Sources/ChatControllerOpenAttachmentMenu.swift index 7eeb7d687f..a056f61a5c 100644 --- a/submodules/TelegramUI/Sources/ChatControllerOpenAttachmentMenu.swift +++ b/submodules/TelegramUI/Sources/ChatControllerOpenAttachmentMenu.swift @@ -309,11 +309,11 @@ extension ChatControllerImpl { completion(controller, mediaPickerContext) }, updateMediaPickerContext: { [weak attachmentController] mediaPickerContext in attachmentController?.mediaPickerContext = mediaPickerContext - }, completion: { [weak self] signals, silentPosting, scheduleTime, messageEffect, getAnimatedTransitionSource, completion in + }, completion: { [weak self] signals, silentPosting, scheduleTime, parameters, getAnimatedTransitionSource, completion in if !inputText.string.isEmpty { self?.clearInputText() } - self?.enqueueMediaMessages(signals: signals, silentPosting: silentPosting, scheduleTime: scheduleTime, messageEffect: messageEffect, getAnimatedTransitionSource: getAnimatedTransitionSource, completion: completion) + self?.enqueueMediaMessages(signals: signals, silentPosting: silentPosting, scheduleTime: scheduleTime, parameters: parameters, getAnimatedTransitionSource: getAnimatedTransitionSource, completion: completion) }) case .file: strongSelf.controllerNavigationDisposable.set(nil) @@ -405,7 +405,7 @@ extension ChatControllerImpl { completion(contactsController, contactsController.mediaPickerContext) strongSelf.controllerNavigationDisposable.set((contactsController.result |> deliverOnMainQueue).startStrict(next: { [weak self] peers in - if let strongSelf = self, let (peers, _, silent, scheduleTime, text) = peers { + if let strongSelf = self, let (peers, _, silent, scheduleTime, text, parameters) = peers { var textEnqueueMessage: EnqueueMessage? if let text = text, text.length > 0 { var attributes: [MessageAttribute] = [] @@ -456,6 +456,17 @@ extension ChatControllerImpl { enqueueMessages.append(message) } } + if !enqueueMessages.isEmpty { + enqueueMessages[enqueueMessages.count - 1] = enqueueMessages[enqueueMessages.count - 1].withUpdatedAttributes { attributes in + var attributes = attributes + if let parameters { + if let effect = parameters.effect { + attributes.append(EffectMessageAttribute(id: effect.id)) + } + } + return attributes + } + } strongSelf.sendMessages(strongSelf.transformEnqueueMessages(enqueueMessages, silentPosting: silent, scheduleTime: scheduleTime)) } else if let peer = peers.first { let dataSignal: Signal<(Peer?, DeviceContactExtendedData?), NoError> @@ -471,14 +482,14 @@ extension ChatControllerImpl { |> mapToSignal { basicData -> Signal<(Peer?, DeviceContactExtendedData?), NoError> in var stableId: String? let queryPhoneNumber = formatPhoneNumber(context: context, number: phoneNumber) - outer: for (id, data) in basicData { - for phoneNumber in data.phoneNumbers { - if formatPhoneNumber(context: context, number: phoneNumber.value) == queryPhoneNumber { - stableId = id - break outer + outer: for (id, data) in basicData { + for phoneNumber in data.phoneNumbers { + if formatPhoneNumber(context: context, number: phoneNumber.value) == queryPhoneNumber { + stableId = id + break outer + } } } - } if let stableId = stableId { return (context.sharedContext.contactDataManager?.extendedData(stableId: stableId) ?? .single(nil)) @@ -498,7 +509,7 @@ extension ChatControllerImpl { } } strongSelf.controllerNavigationDisposable.set((dataSignal - |> deliverOnMainQueue).startStrict(next: { peerAndContactData in + |> deliverOnMainQueue).startStrict(next: { peerAndContactData in if let strongSelf = self, let contactData = peerAndContactData.1, contactData.basicData.phoneNumbers.count != 0 { if contactData.isPrimitive { let phone = contactData.basicData.phoneNumbers[0].value @@ -518,7 +529,13 @@ extension ChatControllerImpl { if let textEnqueueMessage = textEnqueueMessage { enqueueMessages.append(textEnqueueMessage) } - enqueueMessages.append(.message(text: "", attributes: [], inlineStickers: [:], mediaReference: .standalone(media: media), threadId: strongSelf.chatLocation.threadId, replyToMessageId: replyMessageSubject?.subjectModel, replyToStoryId: nil, localGroupingKey: nil, correlationId: nil, bubbleUpEmojiOrStickersets: [])) + var attributes: [MessageAttribute] = [] + if let parameters { + if let effect = parameters.effect { + attributes.append(EffectMessageAttribute(id: effect.id)) + } + } + enqueueMessages.append(.message(text: "", attributes: attributes, inlineStickers: [:], mediaReference: .standalone(media: media), threadId: strongSelf.chatLocation.threadId, replyToMessageId: replyMessageSubject?.subjectModel, replyToStoryId: nil, localGroupingKey: nil, correlationId: nil, bubbleUpEmojiOrStickersets: [])) strongSelf.sendMessages(strongSelf.transformEnqueueMessages(enqueueMessages, silentPosting: silent, scheduleTime: scheduleTime)) } else { let contactController = strongSelf.context.sharedContext.makeDeviceContactInfoController(context: ShareControllerAppAccountContext(context: strongSelf.context), environment: ShareControllerAppEnvironment(sharedContext: strongSelf.context.sharedContext), subject: .filter(peer: peerAndContactData.0, contactId: nil, contactData: contactData, completion: { peer, contactData in @@ -1137,7 +1154,7 @@ extension ChatControllerImpl { self.present(actionSheet, in: .window(.root)) } - func presentMediaPicker(subject: MediaPickerScreen.Subject = .assets(nil, .default), saveEditedPhotos: Bool, bannedSendPhotos: (Int32, Bool)?, bannedSendVideos: (Int32, Bool)?, present: @escaping (MediaPickerScreen, AttachmentMediaPickerContext?) -> Void, updateMediaPickerContext: @escaping (AttachmentMediaPickerContext?) -> Void, completion: @escaping ([Any], Bool, Int32?, ChatSendMessageActionSheetController.MessageEffect?, @escaping (String) -> UIView?, @escaping () -> Void) -> Void) { + func presentMediaPicker(subject: MediaPickerScreen.Subject = .assets(nil, .default), saveEditedPhotos: Bool, bannedSendPhotos: (Int32, Bool)?, bannedSendVideos: (Int32, Bool)?, present: @escaping (MediaPickerScreen, AttachmentMediaPickerContext?) -> Void, updateMediaPickerContext: @escaping (AttachmentMediaPickerContext?) -> Void, completion: @escaping ([Any], Bool, Int32?, ChatSendMessageActionSheetController.SendParameters?, @escaping (String) -> UIView?, @escaping () -> Void) -> Void) { var isScheduledMessages = false if case .scheduledMessages = self.presentationInterfaceState.subject { isScheduledMessages = true @@ -1211,8 +1228,8 @@ extension ChatControllerImpl { controller.getCaptionPanelView = { [weak self] in return self?.getCaptionPanelView(isFile: false) } - controller.legacyCompletion = { signals, silently, scheduleTime, messageEffect, getAnimatedTransitionSource, sendCompletion in - completion(signals, silently, scheduleTime, messageEffect, getAnimatedTransitionSource, sendCompletion) + controller.legacyCompletion = { signals, silently, scheduleTime, parameters, getAnimatedTransitionSource, sendCompletion in + completion(signals, silently, scheduleTime, parameters, getAnimatedTransitionSource, sendCompletion) } present(controller, mediaPickerContext) } @@ -1492,7 +1509,7 @@ extension ChatControllerImpl { self.effectiveNavigationController?.pushViewController(contactsController) self.controllerNavigationDisposable.set((contactsController.result |> deliverOnMainQueue).startStrict(next: { [weak self] peers in - if let strongSelf = self, let (peers, _, _, _, _) = peers { + if let strongSelf = self, let (peers, _, _, _, _, _) = peers { if peers.count > 1 { var enqueueMessages: [EnqueueMessage] = [] for peer in peers { diff --git a/submodules/TelegramUI/Sources/ComposeController.swift b/submodules/TelegramUI/Sources/ComposeController.swift index de5db88e8d..8700b655e6 100644 --- a/submodules/TelegramUI/Sources/ComposeController.swift +++ b/submodules/TelegramUI/Sources/ComposeController.swift @@ -158,7 +158,7 @@ public class ComposeControllerImpl: ViewController, ComposeController { strongSelf.createActionDisposable.set((controller.result |> take(1) |> deliverOnMainQueue).startStrict(next: { [weak controller] result in - if let strongSelf = self, let (contactPeers, _, _, _, _) = result, case let .peer(peer, _, _) = contactPeers.first { + if let strongSelf = self, let (contactPeers, _, _, _, _, _) = result, case let .peer(peer, _, _) = contactPeers.first { controller?.dismissSearch() controller?.displayNavigationActivity = true strongSelf.createActionDisposable.set((strongSelf.context.engine.peers.createSecretChat(peerId: peer.id) |> deliverOnMainQueue).startStrict(next: { peerId in diff --git a/submodules/TelegramUI/Sources/ContactSelectionController.swift b/submodules/TelegramUI/Sources/ContactSelectionController.swift index 0e16ad6069..7db9e4a61e 100644 --- a/submodules/TelegramUI/Sources/ContactSelectionController.swift +++ b/submodules/TelegramUI/Sources/ContactSelectionController.swift @@ -12,6 +12,8 @@ import ContactListUI import SearchUI import AttachmentUI import SearchBarNode +import ChatSendAudioMessageContextPreview +import ChatSendMessageActionUI class ContactSelectionControllerImpl: ViewController, ContactSelectionController, PresentableController, AttachmentContainable { private let context: AccountContext @@ -46,8 +48,8 @@ class ContactSelectionControllerImpl: ViewController, ContactSelectionController fileprivate var caption: NSAttributedString? - private let _result = Promise<([ContactListPeer], ContactListAction, Bool, Int32?, NSAttributedString?)?>() - var result: Signal<([ContactListPeer], ContactListAction, Bool, Int32?, NSAttributedString?)?, NoError> { + private let _result = Promise<([ContactListPeer], ContactListAction, Bool, Int32?, NSAttributedString?, ChatSendMessageActionSheetController.SendParameters?)?>() + var result: Signal<([ContactListPeer], ContactListAction, Bool, Int32?, NSAttributedString?, ChatSendMessageActionSheetController.SendParameters?)?, NoError> { return self._result.get() } @@ -87,6 +89,8 @@ class ContactSelectionControllerImpl: ViewController, ContactSelectionController var isContainerPanning: () -> Bool = { return false } var isContainerExpanded: () -> Bool = { return false } + var getCurrentSendMessageContextMediaPreview: (() -> ChatSendMessageContextScreenMediaPreview?)? + init(_ params: ContactSelectionControllerParams) { self.context = params.context self.autoDismiss = params.autoDismiss @@ -145,6 +149,24 @@ class ContactSelectionControllerImpl: ViewController, ContactSelectionController if params.multipleSelection { self.navigationItem.rightBarButtonItem = UIBarButtonItem(image: PresentationResourcesRootController.navigationCompactSearchIcon(self.presentationData.theme), style: .plain, target: self, action: #selector(self.beginSearch)) } + + self.getCurrentSendMessageContextMediaPreview = { [weak self] in + guard let self else { + return nil + } + + let selectedPeers = self.contactsNode.contactListNode.selectedPeers + if selectedPeers.isEmpty { + return nil + } + + return ChatSendContactMessageContextPreview( + context: self.context, + presentationData: self.presentationData, + wallpaperBackgroundNode: nil, + contactPeers: selectedPeers + ) + } } required init(coder aDecoder: NSCoder) { @@ -235,10 +257,10 @@ class ContactSelectionControllerImpl: ViewController, ContactSelectionController } } - self.contactsNode.requestMultipleAction = { [weak self] silent, scheduleTime in + self.contactsNode.requestMultipleAction = { [weak self] silent, scheduleTime, parameters in if let strongSelf = self { let selectedPeers = strongSelf.contactsNode.contactListNode.selectedPeers - strongSelf._result.set(.single((selectedPeers, .generic, silent, scheduleTime, strongSelf.caption))) + strongSelf._result.set(.single((selectedPeers, .generic, silent, scheduleTime, strongSelf.caption, parameters))) if strongSelf.autoDismiss { strongSelf.dismiss() } @@ -337,7 +359,7 @@ class ContactSelectionControllerImpl: ViewController, ContactSelectionController self.confirmationDisposable.set((self.confirmation(peer) |> deliverOnMainQueue).startStrict(next: { [weak self] value in if let strongSelf = self { if value { - strongSelf._result.set(.single(([peer], action, false, nil, nil))) + strongSelf._result.set(.single(([peer], action, false, nil, nil, nil))) if strongSelf.autoDismiss { strongSelf.dismiss() } @@ -435,6 +457,17 @@ final class ContactsPickerContext: AttachmentMediaPickerContext { return .single(nil) } + var hasCaption: Bool { + return false + } + + var captionIsAboveMedia: Signal { + return .single(false) + } + + func setCaptionIsAboveMedia(_ captionIsAboveMedia: Bool) -> Void { + } + public var loadingProgress: Signal { return .single(nil) } @@ -451,13 +484,13 @@ final class ContactsPickerContext: AttachmentMediaPickerContext { self.controller?.caption = caption } - func send(mode: AttachmentMediaPickerSendMode, attachmentMode: AttachmentMediaPickerAttachmentMode, messageEffect: ChatSendMessageActionSheetController.MessageEffect?) { - self.controller?.contactsNode.requestMultipleAction?(mode == .silently, mode == .whenOnline ? scheduleWhenOnlineTimestamp : nil) + func send(mode: AttachmentMediaPickerSendMode, attachmentMode: AttachmentMediaPickerAttachmentMode, parameters: ChatSendMessageActionSheetController.SendParameters?) { + self.controller?.contactsNode.requestMultipleAction?(mode == .silently, mode == .whenOnline ? scheduleWhenOnlineTimestamp : nil, parameters) } - func schedule(messageEffect: ChatSendMessageActionSheetController.MessageEffect?) { + func schedule(parameters: ChatSendMessageActionSheetController.SendParameters?) { self.controller?.presentScheduleTimePicker ({ time in - self.controller?.contactsNode.requestMultipleAction?(false, time) + self.controller?.contactsNode.requestMultipleAction?(false, time, parameters) }) } diff --git a/submodules/TelegramUI/Sources/ContactSelectionControllerNode.swift b/submodules/TelegramUI/Sources/ContactSelectionControllerNode.swift index 3285c1f824..cface4f8c9 100644 --- a/submodules/TelegramUI/Sources/ContactSelectionControllerNode.swift +++ b/submodules/TelegramUI/Sources/ContactSelectionControllerNode.swift @@ -38,7 +38,7 @@ final class ContactSelectionControllerNode: ASDisplayNode { var requestDeactivateSearch: (() -> Void)? var requestOpenPeerFromSearch: ((ContactListPeer) -> Void)? var requestOpenDisabledPeerFromSearch: ((EnginePeer, ChatListDisabledPeerReason) -> Void)? - var requestMultipleAction: ((_ silent: Bool, _ scheduleTime: Int32?) -> Void)? + var requestMultipleAction: ((_ silent: Bool, _ scheduleTime: Int32?, _ parameters: ChatSendMessageActionSheetController.SendParameters?) -> Void)? var dismiss: (() -> Void)? var cancelSearch: (() -> Void)? @@ -110,7 +110,7 @@ final class ContactSelectionControllerNode: ASDisplayNode { } shareImpl = { [weak self] in - self?.requestMultipleAction?(false, nil) + self?.requestMultipleAction?(false, nil, nil) } contextActionImpl = { [weak self] peer, node, gesture, _ in diff --git a/submodules/TelegramUI/Sources/SharedAccountContext.swift b/submodules/TelegramUI/Sources/SharedAccountContext.swift index 2b38c97041..af599b4576 100644 --- a/submodules/TelegramUI/Sources/SharedAccountContext.swift +++ b/submodules/TelegramUI/Sources/SharedAccountContext.swift @@ -1903,6 +1903,10 @@ public final class SharedAccountContextImpl: SharedAccountContext { return HashtagSearchController(context: context, peer: peer, query: query, all: all) } + public func makeStorySearchController(context: AccountContext, query: String) -> ViewController { + return StorySearchGridScreen(context: context, searchQuery: query) + } + public func makeMyStoriesController(context: AccountContext, isArchive: Bool) -> ViewController { return PeerInfoStoryGridScreen(context: context, peerId: context.account.peerId, scope: isArchive ? .archive : .saved) } diff --git a/submodules/WebSearchUI/Sources/WebSearchController.swift b/submodules/WebSearchUI/Sources/WebSearchController.swift index 896f70eaff..079887e0f8 100644 --- a/submodules/WebSearchUI/Sources/WebSearchController.swift +++ b/submodules/WebSearchUI/Sources/WebSearchController.swift @@ -35,14 +35,14 @@ final class WebSearchControllerInteraction { let setSearchQuery: (String) -> Void let deleteRecentQuery: (String) -> Void let toggleSelection: (ChatContextResult, Bool) -> Bool - let sendSelected: (ChatContextResult?, Bool, Int32?, ChatSendMessageActionSheetController.MessageEffect?) -> Void - let schedule: (ChatSendMessageActionSheetController.MessageEffect?) -> Void + let sendSelected: (ChatContextResult?, Bool, Int32?, ChatSendMessageActionSheetController.SendParameters?) -> Void + let schedule: (ChatSendMessageActionSheetController.SendParameters?) -> Void let avatarCompleted: (UIImage) -> Void let selectionState: TGMediaSelectionContext? let editingState: TGMediaEditingContext var hiddenMediaId: String? - init(openResult: @escaping (ChatContextResult) -> Void, setSearchQuery: @escaping (String) -> Void, deleteRecentQuery: @escaping (String) -> Void, toggleSelection: @escaping (ChatContextResult, Bool) -> Bool, sendSelected: @escaping (ChatContextResult?, Bool, Int32?, ChatSendMessageActionSheetController.MessageEffect?) -> Void, schedule: @escaping (ChatSendMessageActionSheetController.MessageEffect?) -> Void, avatarCompleted: @escaping (UIImage) -> Void, selectionState: TGMediaSelectionContext?, editingState: TGMediaEditingContext) { + init(openResult: @escaping (ChatContextResult) -> Void, setSearchQuery: @escaping (String) -> Void, deleteRecentQuery: @escaping (String) -> Void, toggleSelection: @escaping (ChatContextResult, Bool) -> Bool, sendSelected: @escaping (ChatContextResult?, Bool, Int32?, ChatSendMessageActionSheetController.SendParameters?) -> Void, schedule: @escaping (ChatSendMessageActionSheetController.SendParameters?) -> Void, avatarCompleted: @escaping (UIImage) -> Void, selectionState: TGMediaSelectionContext?, editingState: TGMediaEditingContext) { self.openResult = openResult self.setSearchQuery = setSearchQuery self.deleteRecentQuery = deleteRecentQuery @@ -589,6 +589,17 @@ public class WebSearchPickerContext: AttachmentMediaPickerContext { } } } + + public var hasCaption: Bool { + return false + } + + public var captionIsAboveMedia: Signal { + return .single(false) + } + + public func setCaptionIsAboveMedia(_ captionIsAboveMedia: Bool) -> Void { + } public var loadingProgress: Signal { return .single(nil) @@ -606,12 +617,12 @@ public class WebSearchPickerContext: AttachmentMediaPickerContext { self.interaction?.editingState.setForcedCaption(caption, skipUpdate: true) } - public func send(mode: AttachmentMediaPickerSendMode, attachmentMode: AttachmentMediaPickerAttachmentMode, messageEffect: ChatSendMessageActionSheetController.MessageEffect?) { - self.interaction?.sendSelected(nil, mode == .silently, nil, messageEffect) + public func send(mode: AttachmentMediaPickerSendMode, attachmentMode: AttachmentMediaPickerAttachmentMode, parameters: ChatSendMessageActionSheetController.SendParameters?) { + self.interaction?.sendSelected(nil, mode == .silently, nil, parameters) } - public func schedule(messageEffect: ChatSendMessageActionSheetController.MessageEffect?) { - self.interaction?.schedule(messageEffect) + public func schedule(parameters: ChatSendMessageActionSheetController.SendParameters?) { + self.interaction?.schedule(parameters) } public func mainButtonAction() { diff --git a/submodules/WebUI/Sources/WebAppController.swift b/submodules/WebUI/Sources/WebAppController.swift index 83aa107c7c..97629c1ff2 100644 --- a/submodules/WebUI/Sources/WebAppController.swift +++ b/submodules/WebUI/Sources/WebAppController.swift @@ -2075,6 +2075,17 @@ final class WebAppPickerContext: AttachmentMediaPickerContext { return .single(nil) } + var hasCaption: Bool { + return false + } + + var captionIsAboveMedia: Signal { + return .single(false) + } + + func setCaptionIsAboveMedia(_ captionIsAboveMedia: Bool) -> Void { + } + public var loadingProgress: Signal { return self.controller?.controllerNode.loadingProgressPromise.get() ?? .single(nil) } @@ -2090,10 +2101,10 @@ final class WebAppPickerContext: AttachmentMediaPickerContext { func setCaption(_ caption: NSAttributedString) { } - func send(mode: AttachmentMediaPickerSendMode, attachmentMode: AttachmentMediaPickerAttachmentMode, messageEffect: ChatSendMessageActionSheetController.MessageEffect?) { + func send(mode: AttachmentMediaPickerSendMode, attachmentMode: AttachmentMediaPickerAttachmentMode, parameters: ChatSendMessageActionSheetController.SendParameters?) { } - func schedule(messageEffect: ChatSendMessageActionSheetController.MessageEffect?) { + func schedule(parameters: ChatSendMessageActionSheetController.SendParameters?) { } func mainButtonAction() { From 5545bdd9789c12099e7d05add53fb56b43b50127 Mon Sep 17 00:00:00 2001 From: Isaac <> Date: Fri, 17 May 2024 18:12:37 +0400 Subject: [PATCH 12/14] Improvements --- .../ChatSendMessageContextScreen.swift | 14 +++++++++-- ...tControllerExtractedPresentationNode.swift | 2 +- .../Sources/ReactionContextNode.swift | 24 +++++++++++++++---- .../Sources/MultiAnimationRenderer.swift | 12 +++++----- 4 files changed, 38 insertions(+), 14 deletions(-) diff --git a/submodules/ChatSendMessageActionUI/Sources/ChatSendMessageContextScreen.swift b/submodules/ChatSendMessageActionUI/Sources/ChatSendMessageContextScreen.swift index ab6d60a507..c895bc1284 100644 --- a/submodules/ChatSendMessageActionUI/Sources/ChatSendMessageContextScreen.swift +++ b/submodules/ChatSendMessageActionUI/Sources/ChatSendMessageContextScreen.swift @@ -271,6 +271,18 @@ final class ChatSendMessageContextScreenComponent: Component { self.isUpdating = false } + let environment = environment[EnvironmentType.self].value + + var transition = transition + + var transitionIsImmediate = transition.animation.isImmediate + if case let .curve(duration, _) = transition.animation, duration == 0.0 { + transitionIsImmediate = true + } + if transitionIsImmediate, let previousEnvironment = self.environment, previousEnvironment.inputHeight != 0.0, environment.inputHeight != 0.0, previousEnvironment.inputHeight != environment.inputHeight { + transition = .spring(duration: 0.4) + } + let previousAnimationState = self.appliedAnimationState self.appliedAnimationState = self.presentationAnimationState @@ -284,8 +296,6 @@ final class ChatSendMessageContextScreenComponent: Component { } let _ = alphaTransition - let environment = environment[EnvironmentType.self].value - let themeUpdated = environment.theme !== self.environment?.theme if self.component == nil { diff --git a/submodules/ContextUI/Sources/ContextControllerExtractedPresentationNode.swift b/submodules/ContextUI/Sources/ContextControllerExtractedPresentationNode.swift index e1d9da2667..d26c351aa8 100644 --- a/submodules/ContextUI/Sources/ContextControllerExtractedPresentationNode.swift +++ b/submodules/ContextUI/Sources/ContextControllerExtractedPresentationNode.swift @@ -451,7 +451,7 @@ final class ContextControllerExtractedPresentationNode: ASDisplayNode, ContextCo reactionContextNode.updateIsIntersectingContent(isIntersectingContent: isIntersectingContent, transition: .animated(duration: 0.25, curve: .easeInOut)) if !reactionContextNode.isExpanded && reactionContextNode.canBeExpanded { - if topOverscroll > 30.0 && self.scroller.isDragging { + if topOverscroll > 30.0 && self.scroller.isTracking { self.scroller.panGestureRecognizer.state = .cancelled reactionContextNode.expand() } else { diff --git a/submodules/ReactionSelectionNode/Sources/ReactionContextNode.swift b/submodules/ReactionSelectionNode/Sources/ReactionContextNode.swift index e2ffc8c3ad..9f13e388c2 100644 --- a/submodules/ReactionSelectionNode/Sources/ReactionContextNode.swift +++ b/submodules/ReactionSelectionNode/Sources/ReactionContextNode.swift @@ -1943,11 +1943,13 @@ public final class ReactionContextNode: ASDisplayNode, ASScrollViewDelegate { } } } else { - let remotePacksSignal: Signal<(sets: FoundStickerSets, isFinalResult: Bool), NoError> = .single((FoundStickerSets(), false)) |> then( + let remotePacksSignal: Signal<(sets: FoundStickerSets, isFinalResult: Bool), NoError> = .single((FoundStickerSets(), false)) + |> then( context.engine.stickers.searchEmojiSetsRemotely(query: query) |> map { ($0, true) } ) + let localPacksSignal: Signal = context.engine.stickers.searchEmojiSets(query: query) resultSignal = signal |> mapToSignal { keywords -> Signal<[EmojiPagerContentComponent.ItemGroup], NoError> in @@ -1999,9 +2001,10 @@ public final class ReactionContextNode: ASDisplayNode, ASScrollViewDelegate { context.account.postbox.itemCollectionsView(orderedItemListCollectionIds: [], namespaces: [Namespaces.ItemCollection.CloudEmojiPacks], aroundIndex: nil, count: 10000000) |> take(1), context.engine.stickers.availableReactions() |> take(1), hasPremium |> take(1), - remotePacksSignal + remotePacksSignal, + localPacksSignal ) - |> map { view, availableReactions, hasPremium, foundPacks -> [EmojiPagerContentComponent.ItemGroup] in + |> map { view, availableReactions, hasPremium, foundPacks, foundLocalPacks -> [EmojiPagerContentComponent.ItemGroup] in var result: [(String, TelegramMediaFile?, String)] = [] var allEmoticons: [String: String] = [:] @@ -2072,10 +2075,21 @@ public final class ReactionContextNode: ASDisplayNode, ASScrollViewDelegate { items: items )) - for (collectionId, info, _, _) in foundPacks.sets.infos { + var combinedSets: FoundStickerSets + combinedSets = foundLocalPacks + combinedSets = combinedSets.merge(with: foundPacks.sets) + + var existingCollectionIds = Set() + for (collectionId, info, _, _) in combinedSets.infos { + if !existingCollectionIds.contains(collectionId) { + existingCollectionIds.insert(collectionId) + } else { + continue + } + if let info = info as? StickerPackCollectionInfo { var topItems: [StickerPackItem] = [] - for e in foundPacks.sets.entries { + for e in combinedSets.entries { if let item = e.item as? StickerPackItem { if e.index.collectionId == collectionId { topItems.append(item) diff --git a/submodules/TelegramUI/Components/MultiAnimationRenderer/Sources/MultiAnimationRenderer.swift b/submodules/TelegramUI/Components/MultiAnimationRenderer/Sources/MultiAnimationRenderer.swift index bde9a11206..0d010bdde4 100644 --- a/submodules/TelegramUI/Components/MultiAnimationRenderer/Sources/MultiAnimationRenderer.swift +++ b/submodules/TelegramUI/Components/MultiAnimationRenderer/Sources/MultiAnimationRenderer.swift @@ -532,7 +532,7 @@ public final class MultiAnimationRendererImpl: MultiAnimationRenderer { if itemContext.targets.isEmpty { strongSelf.itemContexts.removeValue(forKey: itemKey) } - } + }.strict() } func loadFirstFrameSynchronously(target: MultiAnimationRenderTarget, cache: AnimationCache, itemId: String, size: CGSize) -> Bool { @@ -598,7 +598,7 @@ public final class MultiAnimationRendererImpl: MultiAnimationRenderer { completion(false, true) } } - }) + }).strict() } func loadFirstFrameAsImage(cache: AnimationCache, itemId: String, size: CGSize, fetch: ((AnimationCacheFetchOptions) -> Disposable)?, completion: @escaping (CGImage?) -> Void) -> Disposable { @@ -626,7 +626,7 @@ public final class MultiAnimationRendererImpl: MultiAnimationRenderer { completion(nil) } } - }) + }).strict() } func setFrameIndex(itemId: String, size: CGSize, frameIndex: Int, placeholder: UIImage) { @@ -729,7 +729,7 @@ public final class MultiAnimationRendererImpl: MultiAnimationRenderer { return ActionDisposable { disposable.dispose() - } + }.strict() } public func loadFirstFrameSynchronously(target: MultiAnimationRenderTarget, cache: AnimationCache, itemId: String, size: CGSize) -> Bool { @@ -763,7 +763,7 @@ public final class MultiAnimationRendererImpl: MultiAnimationRenderer { self.groupContext = groupContext } - return groupContext.loadFirstFrame(target: target, cache: cache, itemId: itemId, size: size, fetch: fetch, completion: completion) + return groupContext.loadFirstFrame(target: target, cache: cache, itemId: itemId, size: size, fetch: fetch, completion: completion).strict() } public func loadFirstFrameAsImage(cache: AnimationCache, itemId: String, size: CGSize, fetch: ((AnimationCacheFetchOptions) -> Disposable)?, completion: @escaping (CGImage?) -> Void) -> Disposable { @@ -780,7 +780,7 @@ public final class MultiAnimationRendererImpl: MultiAnimationRenderer { self.groupContext = groupContext } - return groupContext.loadFirstFrameAsImage(cache: cache, itemId: itemId, size: size, fetch: fetch, completion: completion) + return groupContext.loadFirstFrameAsImage(cache: cache, itemId: itemId, size: size, fetch: fetch, completion: completion).strict() } public func setFrameIndex(itemId: String, size: CGSize, frameIndex: Int, placeholder: UIImage) { From 99b12da3bb7cd5cba74bdcbc618e61f116e4ab96 Mon Sep 17 00:00:00 2001 From: Mikhail Filimonov Date: Fri, 17 May 2024 18:14:41 +0400 Subject: [PATCH 13/14] - bugfixes --- .../Payments/BotPaymentForm.swift | 2 +- .../TelegramEngine/Payments/Stars.swift | 34 ++++++++++++++++--- 2 files changed, 30 insertions(+), 6 deletions(-) diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Payments/BotPaymentForm.swift b/submodules/TelegramCore/Sources/TelegramEngine/Payments/BotPaymentForm.swift index 17c6a4f28e..480bb8c7b1 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Payments/BotPaymentForm.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Payments/BotPaymentForm.swift @@ -323,7 +323,7 @@ func _internal_fetchBotPaymentInvoice(postbox: Postbox, network: Network, source return TelegramMediaInvoice(title: title, description: description, photo: photo.flatMap(TelegramMediaWebFile.init), receiptMessageId: nil, currency: parsedInvoice.currency, totalAmount: 0, startParam: "", extendedMedia: nil, flags: parsedFlags, version: TelegramMediaInvoice.lastVersion) case let .paymentFormStars(_, _, _, title, description, photo, invoice, _): let parsedInvoice = BotPaymentInvoice(apiInvoice: invoice) - return TelegramMediaInvoice(title: title, description: description, photo: photo.flatMap(TelegramMediaWebFile.init), receiptMessageId: nil, currency: parsedInvoice.currency, totalAmount: 0, startParam: "", extendedMedia: nil, flags: [], version: TelegramMediaInvoice.lastVersion) + return TelegramMediaInvoice(title: title, description: description, photo: photo.flatMap(TelegramMediaWebFile.init), receiptMessageId: nil, currency: parsedInvoice.currency, totalAmount: parsedInvoice.prices.reduce(0, { $0 + $1.amount }), startParam: "", extendedMedia: nil, flags: [], version: TelegramMediaInvoice.lastVersion) } } |> mapError { _ -> BotPaymentFormRequestError in } diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Payments/Stars.swift b/submodules/TelegramCore/Sources/TelegramEngine/Payments/Stars.swift index 0f654a2a25..cd8a981a71 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Payments/Stars.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Payments/Stars.swift @@ -151,7 +151,7 @@ private final class StarsContextImpl { guard let self, let state = self._state, let balance = balances[peerId] else { return } - self._state = StarsContext.State(balance: balance, transactions: state.transactions) + self._state = StarsContext.State(balance: balance, transactions: state.transactions, canLoadMore: nextOffset != nil) }) } @@ -168,7 +168,7 @@ private final class StarsContextImpl { |> deliverOnMainQueue).start(next: { [weak self] status in if let self { if let status { - self._state = StarsContext.State(balance: status.balance, transactions: status.transactions) + self._state = StarsContext.State(balance: status.balance, transactions: status.transactions, canLoadMore: status.nextOffset != nil) self.nextOffset = status.nextOffset } else { self._state = nil @@ -177,6 +177,14 @@ private final class StarsContextImpl { })) } + func add(balance: Int64) { + if let state = self._state { + var transactions = state.transactions + transactions.insert(.init(id: "\(arc4random())", count: balance, date: Int32(Date().timeIntervalSince1970), peer: .appStore), at: 0) + self._state = StarsContext.State(balance: state.balance + balance, transactions: transactions, canLoadMore: nextOffset != nil) + } + } + func loadMore() { assert(Queue.mainQueue().isCurrent()) @@ -187,7 +195,7 @@ private final class StarsContextImpl { |> deliverOnMainQueue).start(next: { [weak self] status in if let self { if let status { - self._state = StarsContext.State(balance: status.balance, transactions: currentState.transactions + status.transactions) + self._state = StarsContext.State(balance: status.balance, transactions: currentState.transactions + status.transactions, canLoadMore: status.nextOffset != nil) self.nextOffset = status.nextOffset } else { self.nextOffset = nil @@ -245,10 +253,11 @@ public final class StarsContext { public let balance: Int64 public let transactions: [Transaction] - - init(balance: Int64, transactions: [Transaction]) { + public let canLoadMore: Bool + init(balance: Int64, transactions: [Transaction], canLoadMore: Bool) { self.balance = balance self.transactions = transactions + self.canLoadMore = canLoadMore } public static func == (lhs: State, rhs: State) -> Bool { @@ -258,6 +267,9 @@ public final class StarsContext { if lhs.transactions != rhs.transactions { return false } + if lhs.canLoadMore != rhs.canLoadMore { + return false + } return true } } @@ -276,6 +288,18 @@ public final class StarsContext { } } + public func add(balance: Int64) { + self.impl.with { + $0.add(balance: balance) + } + } + + public func loadMore() { + self.impl.with { + $0.loadMore() + } + } + init(account: Account, peerId: EnginePeer.Id) { self.impl = QueueLocalObject(queue: Queue.mainQueue(), generate: { return StarsContextImpl(account: account, peerId: peerId) From d33703bc1b093dfa6e6b22059e734c5c75ba8d12 Mon Sep 17 00:00:00 2001 From: Mikhail Filimonov Date: Fri, 17 May 2024 18:57:05 +0400 Subject: [PATCH 14/14] - bugfixes --- .../TelegramEngine/Payments/Stars.swift | 30 ++++++++++++------- 1 file changed, 20 insertions(+), 10 deletions(-) diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Payments/Stars.swift b/submodules/TelegramCore/Sources/TelegramEngine/Payments/Stars.swift index cd8a981a71..7af687c12f 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Payments/Stars.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Payments/Stars.swift @@ -151,7 +151,7 @@ private final class StarsContextImpl { guard let self, let state = self._state, let balance = balances[peerId] else { return } - self._state = StarsContext.State(balance: balance, transactions: state.transactions, canLoadMore: nextOffset != nil) + self._state = StarsContext.State(balance: balance, transactions: state.transactions, canLoadMore: nextOffset != nil, isLoading: false) }) } @@ -163,12 +163,12 @@ private final class StarsContextImpl { func load() { assert(Queue.mainQueue().isCurrent()) - + self.disposable.set((requestStarsState(account: self.account, peerId: self.peerId, offset: nil) |> deliverOnMainQueue).start(next: { [weak self] status in if let self { if let status { - self._state = StarsContext.State(balance: status.balance, transactions: status.transactions, canLoadMore: status.nextOffset != nil) + self._state = StarsContext.State(balance: status.balance, transactions: status.transactions, canLoadMore: status.nextOffset != nil, isLoading: false) self.nextOffset = status.nextOffset } else { self._state = nil @@ -178,10 +178,12 @@ private final class StarsContextImpl { } func add(balance: Int64) { - if let state = self._state { + if var state = self._state { var transactions = state.transactions transactions.insert(.init(id: "\(arc4random())", count: balance, date: Int32(Date().timeIntervalSince1970), peer: .appStore), at: 0) - self._state = StarsContext.State(balance: state.balance + balance, transactions: transactions, canLoadMore: nextOffset != nil) + + state.balance = state.balance + balance + self._state = state } } @@ -191,11 +193,14 @@ private final class StarsContextImpl { guard let currentState = self._state, let nextOffset = self.nextOffset else { return } + + self._state?.isLoading = true + self.disposable.set((requestStarsState(account: self.account, peerId: self.peerId, offset: nextOffset) |> deliverOnMainQueue).start(next: { [weak self] status in if let self { if let status { - self._state = StarsContext.State(balance: status.balance, transactions: currentState.transactions + status.transactions, canLoadMore: status.nextOffset != nil) + self._state = StarsContext.State(balance: status.balance, transactions: currentState.transactions + status.transactions, canLoadMore: status.nextOffset != nil, isLoading: false) self.nextOffset = status.nextOffset } else { self.nextOffset = nil @@ -251,13 +256,15 @@ public final class StarsContext { } } - public let balance: Int64 - public let transactions: [Transaction] - public let canLoadMore: Bool - init(balance: Int64, transactions: [Transaction], canLoadMore: Bool) { + public var balance: Int64 + public var transactions: [Transaction] + public var canLoadMore: Bool + public var isLoading: Bool + init(balance: Int64, transactions: [Transaction], canLoadMore: Bool, isLoading: Bool) { self.balance = balance self.transactions = transactions self.canLoadMore = canLoadMore + self.isLoading = isLoading } public static func == (lhs: State, rhs: State) -> Bool { @@ -270,6 +277,9 @@ public final class StarsContext { if lhs.canLoadMore != rhs.canLoadMore { return false } + if lhs.isLoading != rhs.isLoading { + return false + } return true } }