From 21121e1924cb6133965447631a3193ec4dbeb31d Mon Sep 17 00:00:00 2001 From: Ilya Laktyushin Date: Mon, 12 Dec 2022 04:46:21 +0400 Subject: [PATCH] Avatar setup improvements --- .../Telegram-iOS/en.lproj/Localizable.strings | 6 ++ .../GalleryData/Sources/GalleryData.swift | 11 ++- .../TGPhotoEditorController.h | 3 +- .../TGPhotoPaintStickersContext.h | 7 ++ .../LegacyComponents/TGPhotoToolbarView.h | 1 + .../LegacyComponents/TGPhotoVideoEditor.h | 2 +- .../Sources/TGPhotoAvatarPreviewController.h | 7 +- .../Sources/TGPhotoAvatarPreviewController.m | 84 ++++++++++++++++++- .../Sources/TGPhotoEditorController.m | 26 +++++- .../Sources/TGPhotoToolbarView.m | 50 +++++++++++ .../Sources/TGPhotoVideoEditor.m | 55 +++++++++++- submodules/LegacyMediaPickerUI/BUILD | 1 + .../Sources/LegacyAvatarPicker.swift | 4 +- .../Sources/LegacyPaintStickersContext.swift | 14 ++++ .../Sources/MediaPickerGridItem.swift | 4 +- .../Sources/MediaPickerScreen.swift | 2 +- .../Sources/MediaPickerSelectedListNode.swift | 30 +++++-- .../Sources/PeerAvatarImageGalleryItem.swift | 14 +++- .../Network/FetchedMediaResource.swift | 4 + .../TelegramUI/Sources/ChatController.swift | 38 ++++++--- ...ageProfilePhotoSuggestionContentNode.swift | 56 ++++++++++++- 21 files changed, 379 insertions(+), 40 deletions(-) diff --git a/Telegram/Telegram-iOS/en.lproj/Localizable.strings b/Telegram/Telegram-iOS/en.lproj/Localizable.strings index 4269b4ad29..782cf7a55e 100644 --- a/Telegram/Telegram-iOS/en.lproj/Localizable.strings +++ b/Telegram/Telegram-iOS/en.lproj/Localizable.strings @@ -8485,3 +8485,9 @@ Sorry for the inconvenience."; "Notification.SuggestedProfilePhoto" = "Suggested Profile Photo"; "Notification.SuggestedProfileVideo" = "Suggested Profile Video"; + +"PhotoEditor.SetProfilePhoto" = "Set Profile Photo"; +"PhotoEditor.SetProfileVideo" = "Set Profile Video"; + +"PhotoEditor.SetAsMyPhoto" = "Set as My Photo"; +"PhotoEditor.SetAsMyVideo" = "Set as My Video"; diff --git a/submodules/GalleryData/Sources/GalleryData.swift b/submodules/GalleryData/Sources/GalleryData.swift index 448bc0dbe8..44d4244548 100644 --- a/submodules/GalleryData/Sources/GalleryData.swift +++ b/submodules/GalleryData/Sources/GalleryData.swift @@ -114,10 +114,17 @@ public func chatMessageGalleryControllerData(context: AccountContext, chatLocati galleryMedia = fullMedia } else if let action = media as? TelegramMediaAction { switch action.action { - case let .photoUpdated(image): + case let .photoUpdated(image), let .suggestedProfilePhoto(image): if let peer = messageMainPeer(EngineMessage(message)), let image = image { let promise: Promise<[AvatarGalleryEntry]> = Promise([AvatarGalleryEntry.image(image.imageId, image.reference, image.representations.map({ ImageRepresentationWithReference(representation: $0, reference: .media(media: .message(message: MessageReference(message), media: media), resource: $0.resource)) }), image.videoRepresentations.map({ VideoRepresentationWithReference(representation: $0, reference: .media(media: .message(message: MessageReference(message), media: media), resource: $0.resource)) }), peer._asPeer(), message.timestamp, nil, message.id, image.immediateThumbnailData, "action")]) - let galleryController = AvatarGalleryController(context: context, peer: peer._asPeer(), sourceCorners: .roundRect(15.5), remoteEntries: promise, skipInitial: true, replaceRootController: { controller, ready in + + let sourceCorners: AvatarGalleryController.SourceCorners + if case .photoUpdated = action.action { + sourceCorners = .roundRect(15.5) + } else { + sourceCorners = .round + } + let galleryController = AvatarGalleryController(context: context, peer: peer._asPeer(), sourceCorners: sourceCorners, remoteEntries: promise, skipInitial: true, replaceRootController: { controller, ready in }) return .chatAvatars(galleryController, image) diff --git a/submodules/LegacyComponents/PublicHeaders/LegacyComponents/TGPhotoEditorController.h b/submodules/LegacyComponents/PublicHeaders/LegacyComponents/TGPhotoEditorController.h index 99853c039a..2c5c7df125 100644 --- a/submodules/LegacyComponents/PublicHeaders/LegacyComponents/TGPhotoEditorController.h +++ b/submodules/LegacyComponents/PublicHeaders/LegacyComponents/TGPhotoEditorController.h @@ -21,7 +21,8 @@ typedef enum { TGPhotoEditorControllerFromCameraIntent = (1 << 2), TGPhotoEditorControllerWebIntent = (1 << 3), TGPhotoEditorControllerVideoIntent = (1 << 4), - TGPhotoEditorControllerForumAvatarIntent = (1 << 5) + TGPhotoEditorControllerForumAvatarIntent = (1 << 5), + TGPhotoEditorControllerSuggestedAvatarIntent = (1 << 6) } TGPhotoEditorControllerIntent; @interface TGPhotoEditorController : TGOverlayController diff --git a/submodules/LegacyComponents/PublicHeaders/LegacyComponents/TGPhotoPaintStickersContext.h b/submodules/LegacyComponents/PublicHeaders/LegacyComponents/TGPhotoPaintStickersContext.h index a5a523c8ac..7e8cdb437d 100644 --- a/submodules/LegacyComponents/PublicHeaders/LegacyComponents/TGPhotoPaintStickersContext.h +++ b/submodules/LegacyComponents/PublicHeaders/LegacyComponents/TGPhotoPaintStickersContext.h @@ -11,6 +11,11 @@ @end +@protocol TGPhotoSolidRoundedButtonView + +- (void)updateWidth:(CGFloat)width; + +@end @protocol TGPhotoPaintStickerRenderView @@ -127,6 +132,8 @@ @property (nonatomic, copy) id(^captionPanelView)(void); + +- (UIView *)solidRoundedButton:(NSString *)title action:(void(^)(void))action; - (id)drawingAdapter:(CGSize)size; @end diff --git a/submodules/LegacyComponents/PublicHeaders/LegacyComponents/TGPhotoToolbarView.h b/submodules/LegacyComponents/PublicHeaders/LegacyComponents/TGPhotoToolbarView.h index 0d275e10d8..084868682b 100644 --- a/submodules/LegacyComponents/PublicHeaders/LegacyComponents/TGPhotoToolbarView.h +++ b/submodules/LegacyComponents/PublicHeaders/LegacyComponents/TGPhotoToolbarView.h @@ -66,6 +66,7 @@ typedef enum - (void)setEditButtonsDisabled:(TGPhotoEditorTab)buttons; - (void)setAllButtonsHidden:(bool)hidden animated:(bool)animated; +- (void)setCancelDoneButtonsHidden:(bool)hidden animated:(bool)animated; @property (nonatomic, readonly) TGPhotoEditorTab currentTabs; - (void)setToolbarTabs:(TGPhotoEditorTab)tabs animated:(bool)animated; diff --git a/submodules/LegacyComponents/PublicHeaders/LegacyComponents/TGPhotoVideoEditor.h b/submodules/LegacyComponents/PublicHeaders/LegacyComponents/TGPhotoVideoEditor.h index 624ff74ea8..7b60e1429c 100644 --- a/submodules/LegacyComponents/PublicHeaders/LegacyComponents/TGPhotoVideoEditor.h +++ b/submodules/LegacyComponents/PublicHeaders/LegacyComponents/TGPhotoVideoEditor.h @@ -2,7 +2,7 @@ @interface TGPhotoVideoEditor : NSObject -+ (void)presentWithContext:(id)context parentController:(TGViewController *)parentController image:(UIImage *)image video:(NSURL *)video stickersContext:(id)stickersContext didFinishWithImage:(void (^)(UIImage *image))didFinishWithImage didFinishWithVideo:(void (^)(UIImage *image, NSURL *url, TGVideoEditAdjustments *adjustments))didFinishWithVideo dismissed:(void (^)(void))dismissed; ++ (void)presentWithContext:(id)context parentController:(TGViewController *)parentController image:(UIImage *)image video:(NSURL *)video stickersContext:(id)stickersContext transitionView:(UIView *)transitionView didFinishWithImage:(void (^)(UIImage *image))didFinishWithImage didFinishWithVideo:(void (^)(UIImage *image, NSURL *url, TGVideoEditAdjustments *adjustments))didFinishWithVideo dismissed:(void (^)(void))dismissed; + (void)presentWithContext:(id)context controller:(TGViewController *)controller caption:(NSAttributedString *)caption withItem:(id)item paint:(bool)paint recipientName:(NSString *)recipientName stickersContext:(id)stickersContext snapshots:(NSArray *)snapshots immediate:(bool)immediate appeared:(void (^)(void))appeared completion:(void (^)(id, TGMediaEditingContext *))completion dismissed:(void (^)())dismissed; diff --git a/submodules/LegacyComponents/Sources/TGPhotoAvatarPreviewController.h b/submodules/LegacyComponents/Sources/TGPhotoAvatarPreviewController.h index d9ed3bee57..5a72f91f0e 100644 --- a/submodules/LegacyComponents/Sources/TGPhotoAvatarPreviewController.h +++ b/submodules/LegacyComponents/Sources/TGPhotoAvatarPreviewController.h @@ -13,6 +13,9 @@ @property (nonatomic, assign) bool skipTransitionIn; @property (nonatomic, assign) bool fromCamera; +@property (nonatomic, copy) void (^cancelPressed)(void); +@property (nonatomic, copy) void (^donePressed)(void); + @property (nonatomic, copy) void (^croppingChanged)(void); @property (nonatomic, copy) void (^togglePlayback)(void); @@ -23,7 +26,9 @@ @property (nonatomic, weak) TGPhotoEntitiesContainerView *fullEntitiesView; @property (nonatomic, weak) TGMediaPickerGalleryVideoScrubber *scrubberView; -- (instancetype)initWithContext:(id)context photoEditor:(PGPhotoEditor *)photoEditor previewView:(TGPhotoEditorPreviewView *)previewView isForum:(bool)isForum; +@property (nonatomic, strong) id stickersContext; + +- (instancetype)initWithContext:(id)context photoEditor:(PGPhotoEditor *)photoEditor previewView:(TGPhotoEditorPreviewView *)previewView isForum:(bool)isForum isSuggestion:(bool)isSuggestion; - (void)setImage:(UIImage *)image; - (void)setSnapshotImage:(UIImage *)snapshotImage; diff --git a/submodules/LegacyComponents/Sources/TGPhotoAvatarPreviewController.m b/submodules/LegacyComponents/Sources/TGPhotoAvatarPreviewController.m index a3a92bd16c..83e8c2cd7a 100644 --- a/submodules/LegacyComponents/Sources/TGPhotoAvatarPreviewController.m +++ b/submodules/LegacyComponents/Sources/TGPhotoAvatarPreviewController.m @@ -18,6 +18,8 @@ #import "TGModernGalleryVideoView.h" #import "TGPhotoEntitiesContainerView.h" +#import "TGPhotoPaintStickersContext.h" + #import "TGPhotoPaintController.h" const CGFloat TGPhotoAvatarPreviewPanelSize = 96.0f; @@ -34,7 +36,7 @@ const CGFloat TGPhotoAvatarPreviewLandscapePanelSize = TGPhotoAvatarPreviewPanel UIView *_wrapperView; __weak TGPhotoAvatarCropView *_cropView; - + UIView *_portraitToolsWrapperView; UIView *_landscapeToolsWrapperView; UIView *_portraitWrapperBackgroundView; @@ -44,11 +46,16 @@ const CGFloat TGPhotoAvatarPreviewLandscapePanelSize = TGPhotoAvatarPreviewPanel UIView *_landscapeToolControlView; UILabel *_coverLabel; + TGModernButton *_cancelButton; + UILabel *_titleLabel; + UIView *_doneButton; + bool _wasPlayingBeforeCropping; bool _scheduledTransitionIn; bool _isForum; + bool _isSuggestion; } @property (nonatomic, weak) PGPhotoEditor *photoEditor; @@ -58,13 +65,14 @@ const CGFloat TGPhotoAvatarPreviewLandscapePanelSize = TGPhotoAvatarPreviewPanel @implementation TGPhotoAvatarPreviewController -- (instancetype)initWithContext:(id)context photoEditor:(PGPhotoEditor *)photoEditor previewView:(TGPhotoEditorPreviewView *)previewView isForum:(bool)isForum { +- (instancetype)initWithContext:(id)context photoEditor:(PGPhotoEditor *)photoEditor previewView:(TGPhotoEditorPreviewView *)previewView isForum:(bool)isForum isSuggestion:(bool)isSuggestion { self = [super initWithContext:context]; if (self != nil) { self.photoEditor = photoEditor; self.previewView = previewView; _isForum = isForum; + _isSuggestion = isSuggestion; } return self; } @@ -179,6 +187,42 @@ const CGFloat TGPhotoAvatarPreviewLandscapePanelSize = TGPhotoAvatarPreviewPanel [_wrapperView addSubview:_dotImageView]; } + + if (_isSuggestion) { + _titleLabel = [[UILabel alloc] init]; + _titleLabel.backgroundColor = [UIColor clearColor]; + _titleLabel.font = TGBoldSystemFontOfSize(17.0f); + _titleLabel.textColor = [UIColor whiteColor]; + _titleLabel.text = self.item.isVideo ? TGLocalized(@"PhotoEditor.SetProfileVideo") : TGLocalized(@"PhotoEditor.SetProfilePhoto"); + [_titleLabel sizeToFit]; + [_wrapperView addSubview:_titleLabel]; + + if (!self.item.isVideo) { + _cancelButton = [[TGModernButton alloc] init]; + [_cancelButton setTitle:TGLocalized(@"Common.Cancel") forState:UIControlStateNormal]; + _cancelButton.titleLabel.font = TGSystemFontOfSize(17.0); + [_cancelButton sizeToFit]; + [_cancelButton addTarget:self action:@selector(cancelButtonPressed) forControlEvents:UIControlEventTouchUpInside]; + [_wrapperView addSubview:_cancelButton]; + + if (_stickersContext != nil) { + _doneButton = [_stickersContext solidRoundedButton:self.item.isVideo ? TGLocalized(@"PhotoEditor.SetAsMyVideo") : TGLocalized(@"PhotoEditor.SetAsMyPhoto") action:^{ + __strong TGPhotoAvatarPreviewController *strongSelf = weakSelf; + if (strongSelf == nil) + return; + + if (strongSelf.donePressed != nil) + strongSelf.donePressed(); + }]; + [_wrapperView addSubview:_doneButton]; + } + } + } +} + +- (void)cancelButtonPressed { + if (self.cancelPressed != nil) + self.cancelPressed(); } - (void)viewWillAppear:(BOOL)animated @@ -325,8 +369,16 @@ const CGFloat TGPhotoAvatarPreviewLandscapePanelSize = TGPhotoAvatarPreviewPanel [_cropView animateTransitionIn]; + _cancelButton.alpha = 0.0; + _titleLabel.alpha = 0.0; + _doneButton.alpha = 0.0; + [UIView animateWithDuration:0.3f animations:^ { + _cancelButton.alpha = 1.0; + _titleLabel.alpha = 1.0; + _doneButton.alpha = 1.0; + _portraitToolsWrapperView.alpha = 1.0f; _landscapeToolsWrapperView.alpha = 1.0f; _dotImageView.alpha = 1.0f; @@ -499,6 +551,9 @@ const CGFloat TGPhotoAvatarPreviewLandscapePanelSize = TGPhotoAvatarPreviewPanel _landscapeToolsWrapperView.alpha = 0.0f; _dotImageView.alpha = 0.0f; _dotMarkerView.alpha = 0.0f; + _cancelButton.alpha = 0.0; + _titleLabel.alpha = 0.0; + _doneButton.alpha = 0.0; } completion:^(__unused BOOL finished) { if (!switching) { @@ -614,6 +669,9 @@ const CGFloat TGPhotoAvatarPreviewLandscapePanelSize = TGPhotoAvatarPreviewPanel _portraitToolsWrapperView.alpha = 0.0f; _landscapeToolsWrapperView.alpha = 0.0f; _dotImageView.alpha = 0.0f; + _titleLabel.alpha = 0.0f; + _cancelButton.alpha = 0.0f; + _doneButton.alpha = 0.0; } completion:nil]; } @@ -742,6 +800,9 @@ const CGFloat TGPhotoAvatarPreviewLandscapePanelSize = TGPhotoAvatarPreviewPanel screenEdges.left += safeAreaInset.left; screenEdges.bottom -= safeAreaInset.bottom; screenEdges.right -= safeAreaInset.right; + + CGSize buttonSize = CGSizeMake(MIN(referenceSize.width, referenceSize.height) - 16.0 * 2.0, 50.0f); + [_doneButton updateWidth:buttonSize.width]; switch (orientation) { @@ -757,6 +818,12 @@ const CGFloat TGPhotoAvatarPreviewLandscapePanelSize = TGPhotoAvatarPreviewPanel _portraitToolsWrapperView.frame = CGRectMake(screenEdges.left, screenSide - panelToolbarPortraitSize, referenceSize.width, panelToolbarPortraitSize); _portraitToolsWrapperView.frame = CGRectMake((screenSide - referenceSize.width) / 2, screenSide - panelToolbarPortraitSize, referenceSize.width, panelToolbarPortraitSize); + + _titleLabel.frame = CGRectMake(screenEdges.left + floor((referenceSize.width - _titleLabel.frame.size.width) / 2.0), 0.0, _titleLabel.frame.size.width, _titleLabel.frame.size.height); + + _cancelButton.frame = CGRectMake(-_cancelButton.frame.size.width, screenEdges.top + floor((44.0 - _cancelButton.frame.size.height) / 2.0), _cancelButton.frame.size.width, _cancelButton.frame.size.height); + + _doneButton.frame = CGRectMake(floor((_wrapperView.frame.size.width - buttonSize.width) / 2.0), screenEdges.bottom + safeAreaInset.bottom, buttonSize.width, buttonSize.height); } break; @@ -768,11 +835,16 @@ const CGFloat TGPhotoAvatarPreviewLandscapePanelSize = TGPhotoAvatarPreviewPanel }]; _landscapeToolsWrapperView.frame = CGRectMake(screenEdges.right - panelToolbarLandscapeSize, screenEdges.top, panelToolbarLandscapeSize, referenceSize.height); - _portraitToolsWrapperView.frame = CGRectMake(screenEdges.top, screenSide - panelToolbarPortraitSize, referenceSize.width, panelToolbarPortraitSize); _portraitToolsWrapperView.frame = CGRectMake((screenSide - referenceSize.width) / 2, screenSide - panelToolbarPortraitSize, referenceSize.width, panelToolbarPortraitSize); + + _titleLabel.frame = CGRectMake(screenEdges.left + floor((referenceSize.width - _titleLabel.frame.size.width) / 2.0), 0.0, _titleLabel.frame.size.width, _titleLabel.frame.size.height); + + _cancelButton.frame = CGRectMake(-_cancelButton.frame.size.width, screenEdges.top + floor((44.0 - _cancelButton.frame.size.height) / 2.0), _cancelButton.frame.size.width, _cancelButton.frame.size.height); + + _doneButton.frame = CGRectMake(floor((_wrapperView.frame.size.width - buttonSize.width) / 2.0), screenEdges.bottom + safeAreaInset.bottom, buttonSize.width, buttonSize.height); } break; @@ -793,6 +865,12 @@ const CGFloat TGPhotoAvatarPreviewLandscapePanelSize = TGPhotoAvatarPreviewPanel _portraitToolsWrapperView.frame = CGRectMake(screenEdges.left, screenEdges.bottom - panelToolbarPortraitSize, referenceSize.width, panelToolbarPortraitSize); _coverLabel.frame = CGRectMake(floor((_portraitToolsWrapperView.frame.size.width - _coverLabel.frame.size.width) / 2.0), CGRectGetMaxY(_scrubberView.frame) + 6.0, _coverLabel.frame.size.width, _coverLabel.frame.size.height); + + _titleLabel.frame = CGRectMake(screenEdges.left + floor((referenceSize.width - _titleLabel.frame.size.width) / 2.0), screenEdges.top + floor((44.0 - _titleLabel.frame.size.height) / 2.0), _titleLabel.frame.size.width, _titleLabel.frame.size.height); + + _cancelButton.frame = CGRectMake(screenEdges.left + 16.0, screenEdges.top + floor((44.0 - _cancelButton.frame.size.height) / 2.0), _cancelButton.frame.size.width, _cancelButton.frame.size.height); + + _doneButton.frame = CGRectMake(screenEdges.left + floor((referenceSize.width - buttonSize.width) / 2.0), screenEdges.bottom - 56.0 - buttonSize.height, buttonSize.width, buttonSize.height); } break; } diff --git a/submodules/LegacyComponents/Sources/TGPhotoEditorController.m b/submodules/LegacyComponents/Sources/TGPhotoEditorController.m index 17554ac1dc..199e6f620d 100644 --- a/submodules/LegacyComponents/Sources/TGPhotoEditorController.m +++ b/submodules/LegacyComponents/Sources/TGPhotoEditorController.m @@ -1145,6 +1145,11 @@ return _intent & (TGPhotoEditorControllerForumAvatarIntent); } +- (bool)presentedForSuggestedAvatar +{ + return _intent & (TGPhotoEditorControllerSuggestedAvatarIntent); +} + #pragma mark - Transition - (void)transitionIn @@ -1317,6 +1322,10 @@ TGPhotoEditorBackButton backButtonType = TGPhotoEditorBackButtonCancel; TGPhotoEditorDoneButton doneButtonType = TGPhotoEditorDoneButtonCheck; + if ([self presentedForSuggestedAvatar]) { + [_portraitToolbarView setCancelDoneButtonsHidden:tab == TGPhotoEditorCropTab animated:!isInitialAppearance]; + } + __weak TGPhotoEditorController *weakSelf = self; TGPhotoEditorTabController *controller = nil; switch (tab) @@ -1332,7 +1341,8 @@ { bool skipInitialTransition = (![self presentedFromCamera] && self.navigationController != nil) || self.skipInitialTransition; - TGPhotoAvatarPreviewController *cropController = [[TGPhotoAvatarPreviewController alloc] initWithContext:_context photoEditor:_photoEditor previewView:_previewView isForum:[self presentedForForumAvatarCreation]]; + TGPhotoAvatarPreviewController *cropController = [[TGPhotoAvatarPreviewController alloc] initWithContext:_context photoEditor:_photoEditor previewView:_previewView isForum:[self presentedForForumAvatarCreation] isSuggestion:[self presentedForSuggestedAvatar]]; + cropController.stickersContext = _stickersContext; cropController.scrubberView = _scrubberView; cropController.dotImageView = _dotImageView; cropController.dotMarkerView = _dotMarkerView; @@ -1460,6 +1470,20 @@ [strongSelf returnFullPreviewView]; }; + cropController.cancelPressed = ^{ + __strong TGPhotoEditorController *strongSelf = weakSelf; + if (strongSelf == nil) + return; + + strongSelf->_portraitToolbarView.cancelPressed(); + }; + cropController.donePressed = ^{ + __strong TGPhotoEditorController *strongSelf = weakSelf; + if (strongSelf == nil) + return; + + strongSelf->_portraitToolbarView.donePressed(); + }; controller = cropController; doneButtonType = TGPhotoEditorDoneButtonDone; diff --git a/submodules/LegacyComponents/Sources/TGPhotoToolbarView.m b/submodules/LegacyComponents/Sources/TGPhotoToolbarView.m index f652cccb86..66145b71ec 100644 --- a/submodules/LegacyComponents/Sources/TGPhotoToolbarView.m +++ b/submodules/LegacyComponents/Sources/TGPhotoToolbarView.m @@ -25,6 +25,8 @@ UILongPressGestureRecognizer *_longPressGestureRecognizer; bool _transitionedOut; + + bool _animatingCancelDoneButtons; } @end @@ -71,6 +73,9 @@ - (void)setBackButtonType:(TGPhotoEditorBackButton)backButtonType { _backButtonType = backButtonType; + if (_animatingCancelDoneButtons) + return; + UIImage *cancelImage = nil; switch (backButtonType) { @@ -88,6 +93,9 @@ - (void)setDoneButtonType:(TGPhotoEditorDoneButton)doneButtonType { _doneButtonType = doneButtonType; + if (_animatingCancelDoneButtons) + return; + TGMediaAssetsPallete *pallete = nil; if ([_context respondsToSelector:@selector(mediaAssetsPallete)]) pallete = [_context mediaAssetsPallete]; @@ -528,6 +536,48 @@ } } +- (void)setCancelDoneButtonsHidden:(bool)hidden animated:(bool)animated { + CGFloat targetAlpha = hidden ? 0.0f : 1.0f; + + if (animated) + { + _animatingCancelDoneButtons = hidden; + if (hidden) { + _cancelButton.modernHighlight = false; + } + _cancelButton.hidden = false; + _doneButton.hidden = false; + + [UIView animateWithDuration:0.2f + animations:^ + { + _cancelButton.alpha = targetAlpha; + _doneButton.alpha = targetAlpha; + } completion:^(__unused BOOL finished) + { + _animatingCancelDoneButtons = false; + _cancelButton.hidden = hidden; + _doneButton.hidden = hidden; + + if (hidden) { + _cancelButton.modernHighlight = true; + } + + if (hidden) { + [self setBackButtonType:_backButtonType]; + [self setDoneButtonType:_doneButtonType]; + } + }]; + } + else + { + _cancelButton.alpha = targetAlpha; + _doneButton.alpha = targetAlpha; + _cancelButton.hidden = hidden; + _doneButton.hidden = hidden; + } +} + - (TGPhotoEditorButton *)buttonForTab:(TGPhotoEditorTab)tab { for (TGPhotoEditorButton *button in _buttonsWrapperView.subviews) diff --git a/submodules/LegacyComponents/Sources/TGPhotoVideoEditor.m b/submodules/LegacyComponents/Sources/TGPhotoVideoEditor.m index 09600fb6a7..febe7151b2 100644 --- a/submodules/LegacyComponents/Sources/TGPhotoVideoEditor.m +++ b/submodules/LegacyComponents/Sources/TGPhotoVideoEditor.m @@ -12,7 +12,7 @@ @implementation TGPhotoVideoEditor -+ (void)presentWithContext:(id)context parentController:(TGViewController *)parentController image:(UIImage *)image video:(NSURL *)video stickersContext:(id)stickersContext didFinishWithImage:(void (^)(UIImage *image))didFinishWithImage didFinishWithVideo:(void (^)(UIImage *image, NSURL *url, TGVideoEditAdjustments *adjustments))didFinishWithVideo dismissed:(void (^)(void))dismissed ++ (void)presentWithContext:(id)context parentController:(TGViewController *)parentController image:(UIImage *)image video:(NSURL *)video stickersContext:(id)stickersContext transitionView:(UIView *)transitionView didFinishWithImage:(void (^)(UIImage *image))didFinishWithImage didFinishWithVideo:(void (^)(UIImage *image, NSURL *url, TGVideoEditAdjustments *adjustments))didFinishWithVideo dismissed:(void (^)(void))dismissed { id windowManager = [context makeOverlayWindowManager]; @@ -34,10 +34,17 @@ } void (^present)(UIImage *) = ^(UIImage *screenImage) { - TGPhotoEditorController *controller = [[TGPhotoEditorController alloc] initWithContext:[windowManager context] item:editableItem intent:TGPhotoEditorControllerAvatarIntent adjustments:nil caption:nil screenImage:screenImage availableTabs:[TGPhotoEditorController defaultTabsForAvatarIntent] selectedTab:TGPhotoEditorCropTab]; + TGPhotoEditorController *controller = [[TGPhotoEditorController alloc] initWithContext:[windowManager context] item:editableItem intent:TGPhotoEditorControllerAvatarIntent | TGPhotoEditorControllerSuggestedAvatarIntent adjustments:nil caption:nil screenImage:screenImage availableTabs:[TGPhotoEditorController defaultTabsForAvatarIntent] selectedTab:TGPhotoEditorCropTab]; controller.stickersContext = stickersContext; - controller.skipInitialTransition = true; - controller.dontHideStatusBar = true; + + TGMediaAvatarEditorTransition *transition; + if (transitionView != nil) { + transition = [[TGMediaAvatarEditorTransition alloc] initWithController:controller fromView:transitionView]; + } else { + controller.skipInitialTransition = true; + controller.dontHideStatusBar = true; + } + controller.didFinishEditing = ^(__unused id adjustments, UIImage *resultImage, __unused UIImage *thumbnailImage, __unused bool hasChanges, void(^commit)(void)) { if (didFinishWithImage != nil) @@ -84,6 +91,46 @@ TGOverlayControllerWindow *controllerWindow = [[TGOverlayControllerWindow alloc] initWithManager:windowManager parentController:controller contentController:controller]; controllerWindow.hidden = false; controller.view.clipsToBounds = true; + + if (transitionView != nil) { + transition.referenceFrame = ^CGRect + { + UIView *referenceView = transitionView; + return [referenceView.superview convertRect:referenceView.frame toView:nil]; + }; + transition.referenceImageSize = ^CGSize + { + return image.size; + }; + transition.referenceScreenImageSignal = ^SSignal * + { + return [SSignal single:image]; + }; + [transition presentAnimated:true]; + + transitionView.alpha = 0.0; + TGDispatchAfter(0.4, dispatch_get_main_queue(), ^{ + transitionView.alpha = 1.0; + }); + + controller.beginCustomTransitionOut = ^(CGRect outReferenceFrame, UIView *repView, void (^completion)(void)) + { + transition.outReferenceFrame = outReferenceFrame; + transition.repView = repView; + + transitionView.alpha = 0.0; + [transition dismissAnimated:true completion:^ + { + transitionView.alpha = 1.0; + dispatch_async(dispatch_get_main_queue(), ^ + { + if (completion != nil) + completion(); + dismissed(); + }); + }]; + }; + } }; if (image != nil) { diff --git a/submodules/LegacyMediaPickerUI/BUILD b/submodules/LegacyMediaPickerUI/BUILD index 53d93d060d..65aae17021 100644 --- a/submodules/LegacyMediaPickerUI/BUILD +++ b/submodules/LegacyMediaPickerUI/BUILD @@ -29,6 +29,7 @@ swift_library( "//submodules/TextFormat:TextFormat", "//submodules/AttachmentUI:AttachmentUI", "//submodules/DrawingUI:DrawingUI", + "//submodules/SolidRoundedButtonNode:SolidRoundedButtonNode", ], visibility = [ "//visibility:public", diff --git a/submodules/LegacyMediaPickerUI/Sources/LegacyAvatarPicker.swift b/submodules/LegacyMediaPickerUI/Sources/LegacyAvatarPicker.swift index 2f72cb2f5e..8e97fffc79 100644 --- a/submodules/LegacyMediaPickerUI/Sources/LegacyAvatarPicker.swift +++ b/submodules/LegacyMediaPickerUI/Sources/LegacyAvatarPicker.swift @@ -53,7 +53,7 @@ public func presentLegacyAvatarPicker(holder: Atomic, signup: Bool, t } -public func legacyAvatarEditor(context: AccountContext, media: AnyMediaReference, present: @escaping (ViewController, Any?) -> Void, imageCompletion: @escaping (UIImage) -> Void, videoCompletion: @escaping (UIImage, URL, TGVideoEditAdjustments) -> Void) { +public func legacyAvatarEditor(context: AccountContext, media: AnyMediaReference, transitionView: UIView?, present: @escaping (ViewController, Any?) -> Void, imageCompletion: @escaping (UIImage) -> Void, videoCompletion: @escaping (UIImage, URL, TGVideoEditAdjustments) -> Void) { let _ = (fetchMediaData(context: context, postbox: context.account.postbox, mediaReference: media) |> deliverOnMainQueue).start(next: { (value, isImage) in guard case let .data(data) = value, data.complete else { @@ -93,7 +93,7 @@ public func legacyAvatarEditor(context: AccountContext, media: AnyMediaReference present(legacyController, nil) - TGPhotoVideoEditor.present(with: legacyController.context, parentController: emptyController, image: image, video: url, stickersContext: paintStickersContext, didFinishWithImage: { image in + TGPhotoVideoEditor.present(with: legacyController.context, parentController: emptyController, image: image, video: url, stickersContext: paintStickersContext, transitionView: transitionView, didFinishWithImage: { image in if let image = image { imageCompletion(image) } diff --git a/submodules/LegacyMediaPickerUI/Sources/LegacyPaintStickersContext.swift b/submodules/LegacyMediaPickerUI/Sources/LegacyPaintStickersContext.swift index 560aeed2aa..24e00747b3 100644 --- a/submodules/LegacyMediaPickerUI/Sources/LegacyPaintStickersContext.swift +++ b/submodules/LegacyMediaPickerUI/Sources/LegacyPaintStickersContext.swift @@ -9,6 +9,7 @@ import TelegramAnimatedStickerNode import YuvConversion import StickerResources import DrawingUI +import SolidRoundedButtonNode protocol LegacyPaintEntity { var position: CGPoint { get } @@ -496,4 +497,17 @@ public final class LegacyPaintStickersContext: NSObject, TGPhotoPaintStickersCon public func drawingAdapter(_ size: CGSize) -> TGPhotoDrawingAdapter! { return LegacyDrawingAdapter(context: self.context, size: size) } + + public func solidRoundedButton(_ title: String!, action: (() -> Void)!) -> (UIView & TGPhotoSolidRoundedButtonView)! { + let theme = SolidRoundedButtonTheme(theme: self.context.sharedContext.currentPresentationData.with { $0 }.theme) + let button = SolidRoundedButtonView(title: title, theme: theme, height: 50.0, cornerRadius: 10.0) + button.pressed = action + return button + } +} + +extension SolidRoundedButtonView: TGPhotoSolidRoundedButtonView { + public func updateWidth(_ width: CGFloat) { + let _ = self.updateLayout(width: width, transition: .immediate) + } } diff --git a/submodules/MediaPickerUI/Sources/MediaPickerGridItem.swift b/submodules/MediaPickerUI/Sources/MediaPickerGridItem.swift index f4862c3bd8..71e44ef289 100644 --- a/submodules/MediaPickerUI/Sources/MediaPickerGridItem.swift +++ b/submodules/MediaPickerUI/Sources/MediaPickerGridItem.swift @@ -91,7 +91,7 @@ final class MediaPickerGridItemNode: GridItemNode { private var theme: PresentationTheme? private let spoilerDisposable = MetaDisposable() - private var spoilerNode: SpoilerOverlayNode? + var spoilerNode: SpoilerOverlayNode? private var currentIsPreviewing = false @@ -440,7 +440,7 @@ final class MediaPickerGridItemNode: GridItemNode { class SpoilerOverlayNode: ASDisplayNode { private let blurNode: ASImageNode - private let dustNode: MediaDustNode + let dustNode: MediaDustNode private var maskView: UIView? private var maskLayer: CAShapeLayer? diff --git a/submodules/MediaPickerUI/Sources/MediaPickerScreen.swift b/submodules/MediaPickerUI/Sources/MediaPickerScreen.swift index cc59c4475e..8ec3804ee2 100644 --- a/submodules/MediaPickerUI/Sources/MediaPickerScreen.swift +++ b/submodules/MediaPickerUI/Sources/MediaPickerScreen.swift @@ -599,7 +599,7 @@ public final class MediaPickerScreen: ViewController, AttachmentContainable { } } if let node = node { - return (node.view, { [weak node] animateCheckNode in + return (node.view, node.spoilerNode?.dustNode, { [weak node] animateCheckNode in node?.animateFadeIn(animateCheckNode: animateCheckNode, animateSpoilerNode: false) }) } else { diff --git a/submodules/MediaPickerUI/Sources/MediaPickerSelectedListNode.swift b/submodules/MediaPickerUI/Sources/MediaPickerSelectedListNode.swift index 7eb23af421..d07169ce38 100644 --- a/submodules/MediaPickerUI/Sources/MediaPickerSelectedListNode.swift +++ b/submodules/MediaPickerUI/Sources/MediaPickerSelectedListNode.swift @@ -394,7 +394,7 @@ private class MediaPickerSelectedItemNode: ASDisplayNode { }) } - func animateTo(_ view: UIView, completion: @escaping (Bool) -> Void) { + func animateTo(_ view: UIView, dustNode: ASDisplayNode?, completion: @escaping (Bool) -> Void) { view.alpha = 0.0 let frame = self.frame @@ -405,6 +405,20 @@ private class MediaPickerSelectedItemNode: ASDisplayNode { self.durationTextNode?.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false) self.durationBackgroundNode?.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false) + var dustSupernode: ASDisplayNode? + var dustPosition: CGPoint? + if let dustNode = dustNode { + dustSupernode = dustNode.supernode + dustPosition = dustNode.position + + self.addSubnode(dustNode) + dustNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.25) + + dustNode.layer.animatePosition(from: CGPoint(x: frame.width / 2.0, y: frame.height / 2.0), to: dustNode.position, duration: 0.25, timingFunction: kCAMediaTimingFunctionSpring) + + self.spoilerNode?.dustNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.25, removeOnCompletion: false) + } + self.corners = [] self.updateLayout(size: targetFrame.size, transition: .animated(duration: 0.25, curve: .spring)) self.layer.animateFrame(from: frame, to: targetFrame, duration: 0.25, timingFunction: kCAMediaTimingFunctionSpring, removeOnCompletion: false, completion: { [weak view, weak self] _ in @@ -413,6 +427,11 @@ private class MediaPickerSelectedItemNode: ASDisplayNode { self?.durationTextNode?.layer.removeAllAnimations() self?.durationBackgroundNode?.layer.removeAllAnimations() + if let dustNode = dustNode { + dustSupernode?.addSubnode(dustNode) + dustNode.position = dustPosition ?? dustNode.position + } + var animateCheckNode = false if let strongSelf = self, let checkNode = strongSelf.checkNode, checkNode.alpha.isZero { animateCheckNode = true @@ -424,6 +443,7 @@ private class MediaPickerSelectedItemNode: ASDisplayNode { Queue.mainQueue().after(0.01) { self?.layer.removeAllAnimations() + self?.spoilerNode?.dustNode.layer.removeAllAnimations() } }) } @@ -553,7 +573,7 @@ final class MediaPickerSelectedListNode: ASDisplayNode, UIScrollViewDelegate, UI }) } - var getTransitionView: (String ) -> (UIView, (Bool) -> Void)? = { _ in return nil } + var getTransitionView: (String) -> (UIView, ASDisplayNode?, (Bool) -> Void)? = { _ in return nil } func animateIn(initiated: @escaping () -> Void, completion: @escaping () -> Void = {}) { let _ = (self.ready.get() @@ -576,7 +596,7 @@ final class MediaPickerSelectedListNode: ASDisplayNode, UIScrollViewDelegate, UI } for (identifier, itemNode) in strongSelf.itemNodes { - if let (transitionView, _) = strongSelf.getTransitionView(identifier) { + if let (transitionView, _, _) = strongSelf.getTransitionView(identifier) { itemNode.animateFrom(transitionView) } else { itemNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.1) @@ -624,8 +644,8 @@ final class MediaPickerSelectedListNode: ASDisplayNode, UIScrollViewDelegate, UI } for (identifier, itemNode) in self.itemNodes { - if let (transitionView, completion) = self.getTransitionView(identifier) { - itemNode.animateTo(transitionView, completion: completion) + if let (transitionView, maybeDustNode, completion) = self.getTransitionView(identifier) { + itemNode.animateTo(transitionView, dustNode: maybeDustNode, completion: completion) } else { itemNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.1, removeOnCompletion: false) } diff --git a/submodules/PeerAvatarGalleryUI/Sources/PeerAvatarImageGalleryItem.swift b/submodules/PeerAvatarGalleryUI/Sources/PeerAvatarImageGalleryItem.swift index 3554ab1dab..9b021ea44c 100644 --- a/submodules/PeerAvatarGalleryUI/Sources/PeerAvatarImageGalleryItem.swift +++ b/submodules/PeerAvatarGalleryUI/Sources/PeerAvatarImageGalleryItem.swift @@ -195,10 +195,16 @@ final class PeerAvatarImageGalleryItemNode: ZoomableContentGalleryItemNode { subject = .image(entry.representations) actionCompletionText = strongSelf.presentationData.strings.Gallery_ImageSaved } - let shareController = ShareController(context: strongSelf.context, subject: subject, preferredAction: .saveToCameraRoll) - shareController.actionCompleted = { [weak self] in - if let strongSelf = self, let actionCompletionText = actionCompletionText { - let presentationData = strongSelf.context.sharedContext.currentPresentationData.with { $0 } + + let presentationData = strongSelf.context.sharedContext.currentPresentationData.with { $0 } + var forceTheme: PresentationTheme? + if !presentationData.theme.overallDarkAppearance { + forceTheme = defaultDarkColorPresentationTheme + } + + let shareController = ShareController(context: strongSelf.context, subject: subject, preferredAction: .saveToCameraRoll, forceTheme: forceTheme) + shareController.actionCompleted = { + if let actionCompletionText = actionCompletionText { interaction.presentController(UndoOverlayController(presentationData: presentationData, content: .mediaSaved(text: actionCompletionText), elevatedLayout: true, animateInAsReplacement: false, action: { _ in return true }), nil) } } diff --git a/submodules/TelegramCore/Sources/Network/FetchedMediaResource.swift b/submodules/TelegramCore/Sources/Network/FetchedMediaResource.swift index b15de96770..d13287e4ce 100644 --- a/submodules/TelegramCore/Sources/Network/FetchedMediaResource.swift +++ b/submodules/TelegramCore/Sources/Network/FetchedMediaResource.swift @@ -165,6 +165,10 @@ private func findMediaResource(media: Media, previousMedia: Media?, resource: Me if let image = image, let result = findMediaResource(media: image, previousMedia: previousMedia, resource: resource) { return result } + case let .suggestedProfilePhoto(image): + if let image = image, let result = findMediaResource(media: image, previousMedia: previousMedia, resource: resource) { + return result + } default: break } diff --git a/submodules/TelegramUI/Sources/ChatController.swift b/submodules/TelegramUI/Sources/ChatController.swift index 156613b17a..dc08ea4b2d 100644 --- a/submodules/TelegramUI/Sources/ChatController.swift +++ b/submodules/TelegramUI/Sources/ChatController.swift @@ -822,21 +822,35 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G return true case let .suggestedProfilePhoto(image): if let image = image { - legacyAvatarEditor(context: strongSelf.context, media: .message(message: MessageReference(message), media: image), present: { [weak self] c, a in - self?.present(c, in: .window(.root), with: a) - }, imageCompletion: { [weak self] image in - if let strongSelf = self { - if let rootController = strongSelf.effectiveNavigationController as? TelegramRootController, let settingsController = rootController.accountSettingsController as? PeerInfoScreenImpl { - settingsController.updateProfilePhoto(image) + if message.effectivelyIncoming(strongSelf.context.account.peerId) { + var selectedNode: (ASDisplayNode, CGRect, () -> (UIView?, UIView?))? + strongSelf.chatDisplayNode.historyNode.forEachItemNode { itemNode in + if let itemNode = itemNode as? ChatMessageItemView { + if let result = itemNode.transitionNode(id: message.id, media: image) { + selectedNode = result + } } } - }, videoCompletion: { [weak self] image, url, adjustments in - if let strongSelf = self { - if let rootController = strongSelf.effectiveNavigationController as? TelegramRootController, let settingsController = rootController.accountSettingsController as? PeerInfoScreenImpl { - settingsController.updateProfileVideo(image, asset: AVURLAsset(url: url), adjustments: adjustments) + let transitionView = selectedNode?.0.view + + legacyAvatarEditor(context: strongSelf.context, media: .message(message: MessageReference(message), media: image), transitionView: transitionView, present: { [weak self] c, a in + self?.present(c, in: .window(.root), with: a) + }, imageCompletion: { [weak self] image in + if let strongSelf = self { + if let rootController = strongSelf.effectiveNavigationController as? TelegramRootController, let settingsController = rootController.accountSettingsController as? PeerInfoScreenImpl { + settingsController.updateProfilePhoto(image) + } } - } - }) + }, videoCompletion: { [weak self] image, url, adjustments in + if let strongSelf = self { + if let rootController = strongSelf.effectiveNavigationController as? TelegramRootController, let settingsController = rootController.accountSettingsController as? PeerInfoScreenImpl { + settingsController.updateProfileVideo(image, asset: AVURLAsset(url: url), adjustments: adjustments) + } + } + }) + } else { + openMessageByAction = true + } } default: break diff --git a/submodules/TelegramUI/Sources/ChatMessageProfilePhotoSuggestionContentNode.swift b/submodules/TelegramUI/Sources/ChatMessageProfilePhotoSuggestionContentNode.swift index 5cf99cccbd..2f9d92be17 100644 --- a/submodules/TelegramUI/Sources/ChatMessageProfilePhotoSuggestionContentNode.swift +++ b/submodules/TelegramUI/Sources/ChatMessageProfilePhotoSuggestionContentNode.swift @@ -93,6 +93,50 @@ class ChatMessageProfilePhotoSuggestionContentNode: ChatMessageBubbleContentNode self.fetchDisposable.dispose() } + override func transitionNode(messageId: MessageId, media: Media) -> (ASDisplayNode, CGRect, () -> (UIView?, UIView?))? { + if self.item?.message.id == messageId { + return (self.imageNode, self.imageNode.bounds, { [weak self] in + guard let strongSelf = self else { + return (nil, nil) + } + + let resultView = strongSelf.imageNode.view.snapshotContentTree(unhide: true) + return (resultView, nil) + }) + } else { + return nil + } + } + + override func updateHiddenMedia(_ media: [Media]?) -> Bool { + var mediaHidden = false + var currentMedia: Media? + if let item = item { + mediaLoop: for media in item.message.media { + if let media = media as? TelegramMediaAction { + switch media.action { + case let .suggestedProfilePhoto(image): + currentMedia = image + break mediaLoop + default: + break + } + } + } + } + if let currentMedia = currentMedia, let media = media { + for item in media { + if item.isSemanticallyEqual(to: currentMedia) { + mediaHidden = true + break + } + } + } + + self.imageNode.isHidden = mediaHidden + return mediaHidden + } + @objc private func buttonPressed() { guard let item = self.item else { return @@ -105,6 +149,8 @@ class ChatMessageProfilePhotoSuggestionContentNode: ChatMessageBubbleContentNode let makeImageLayout = self.imageNode.asyncLayout() let makeSubtitleLayout = TextNode.asyncLayout(self.subtitleNode) let makeButtonTitleLayout = TextNode.asyncLayout(self.buttonTitleNode) + + let currentItem = self.item return { item, layoutConstants, _, _, _, _ in let contentProperties = ChatMessageBubbleContentProperties(hidesSimpleAuthorHeader: true, headerSpacing: 0.0, hidesBackground: .always, forceFullCorners: false, forceAlignment: .center) @@ -119,6 +165,12 @@ class ChatMessageProfilePhotoSuggestionContentNode: ChatMessageBubbleContentNode if let media = item.message.media.first(where: { $0 is TelegramMediaAction }) as? TelegramMediaAction, case let .suggestedProfilePhoto(image) = media.action { photo = image } + + var mediaUpdated = true + if let photo = photo, let media = currentItem?.message.media.first(where: { $0 is TelegramMediaAction }) as? TelegramMediaAction, case let .suggestedProfilePhoto(maybeCurrentPhoto) = media.action, let currentPhoto = maybeCurrentPhoto { + mediaUpdated = !photo.isSemanticallyEqual(to: currentPhoto) + } + let isVideo = !(photo?.videoRepresentations.isEmpty ?? true) let fromYou = item.message.author?.id == item.context.account.peerId @@ -144,7 +196,9 @@ class ChatMessageProfilePhotoSuggestionContentNode: ChatMessageBubbleContentNode strongSelf.item = item if let photo = photo { - strongSelf.fetchDisposable.set(chatMessagePhotoInteractiveFetched(context: item.context, photoReference: .message(message: MessageReference(item.message), media: photo), displayAtSize: nil, storeToDownloadsPeerType: nil).start()) + if mediaUpdated { + strongSelf.fetchDisposable.set(chatMessagePhotoInteractiveFetched(context: item.context, photoReference: .message(message: MessageReference(item.message), media: photo), displayAtSize: nil, storeToDownloadsPeerType: nil).start()) + } let updateImageSignal = chatMessagePhoto(postbox: item.context.account.postbox, photoReference: .message(message: MessageReference(item.message), media: photo), synchronousLoad: synchronousLoads) strongSelf.imageNode.setSignal(updateImageSignal, attemptSynchronously: synchronousLoads)