diff --git a/Telegram/Telegram-iOS/en.lproj/Localizable.strings b/Telegram/Telegram-iOS/en.lproj/Localizable.strings index 2c3d25d640..e1a30a8f7a 100644 --- a/Telegram/Telegram-iOS/en.lproj/Localizable.strings +++ b/Telegram/Telegram-iOS/en.lproj/Localizable.strings @@ -8696,3 +8696,6 @@ Sorry for the inconvenience."; "Conversation.ViewInChannel" = "View in Channel"; + +"Conversation.HoldForAudioOnly" = "Hold to record audio."; +"Conversation.HoldForVideoOnly" = "Hold to record video."; diff --git a/submodules/AuthorizationUI/Sources/AuthorizationSequenceController.swift b/submodules/AuthorizationUI/Sources/AuthorizationSequenceController.swift index 27acb53b07..b9c6992c24 100644 --- a/submodules/AuthorizationUI/Sources/AuthorizationSequenceController.swift +++ b/submodules/AuthorizationUI/Sources/AuthorizationSequenceController.swift @@ -477,7 +477,7 @@ public final class AuthorizationSequenceController: NavigationController, MFMail } } else { controller?.inProgress = true - strongSelf.actionDisposable.set((resendAuthorizationCode(account: strongSelf.account) + strongSelf.actionDisposable.set((resendAuthorizationCode(accountManager: strongSelf.sharedContext.accountManager, account: strongSelf.account, apiId: strongSelf.apiId, apiHash: strongSelf.apiHash, firebaseSecretStream: strongSelf.sharedContext.firebaseSecretStream) |> deliverOnMainQueue).start(next: { result in controller?.inProgress = false }, error: { error in diff --git a/submodules/GalleryUI/Sources/ChatItemGalleryFooterContentNode.swift b/submodules/GalleryUI/Sources/ChatItemGalleryFooterContentNode.swift index 6e6f5082cf..b14b7a66e4 100644 --- a/submodules/GalleryUI/Sources/ChatItemGalleryFooterContentNode.swift +++ b/submodules/GalleryUI/Sources/ChatItemGalleryFooterContentNode.swift @@ -621,11 +621,13 @@ final class ChatItemGalleryFooterContentNode: GalleryFooterContentNode, UIScroll var canFullscreen = false var canEdit = false + var isImage = false + var isVideo = false for media in message.media { if media is TelegramMediaImage { canEdit = true + isImage = true } else if let media = media as? TelegramMediaFile, !media.isAnimated { - var isVideo = false for attribute in media.attributes { switch attribute { case let .Video(_, dimensions, _): @@ -666,7 +668,19 @@ final class ChatItemGalleryFooterContentNode: GalleryFooterContentNode, UIScroll } else if let channel = peer as? TelegramChannel { if message.flags.contains(.Incoming) { canDelete = channel.hasPermission(.deleteAllMessages) - canEdit = canEdit && channel.hasPermission(.sendMessages) + if canEdit { + if isImage { + if !channel.hasPermission(.sendPhoto) { + canEdit = false + } + } else if isVideo { + if !channel.hasPermission(.sendVideo) { + canEdit = false + } + } else { + canEdit = false + } + } } else { canDelete = true } diff --git a/submodules/LegacyComponents/PublicHeaders/LegacyComponents/TGAttachmentCameraView.h b/submodules/LegacyComponents/PublicHeaders/LegacyComponents/TGAttachmentCameraView.h index 408fbf1ccd..9c6fea4e0c 100644 --- a/submodules/LegacyComponents/PublicHeaders/LegacyComponents/TGAttachmentCameraView.h +++ b/submodules/LegacyComponents/PublicHeaders/LegacyComponents/TGAttachmentCameraView.h @@ -8,7 +8,7 @@ @property (nonatomic, copy) void (^pressed)(void); @property (nonatomic, strong) TGMenuSheetPallete *pallete; -- (instancetype)initForSelfPortrait:(bool)selfPortrait; +- (instancetype)initForSelfPortrait:(bool)selfPortrait videoModeByDefault:(bool)videoModeByDefault; @property (nonatomic, readonly) bool previewViewAttached; - (void)detachPreviewView; diff --git a/submodules/LegacyComponents/PublicHeaders/LegacyComponents/TGCameraController.h b/submodules/LegacyComponents/PublicHeaders/LegacyComponents/TGCameraController.h index cfc9c7588c..d680c7525c 100644 --- a/submodules/LegacyComponents/PublicHeaders/LegacyComponents/TGCameraController.h +++ b/submodules/LegacyComponents/PublicHeaders/LegacyComponents/TGCameraController.h @@ -19,7 +19,8 @@ typedef enum { TGCameraControllerPassportMultipleIntent, TGCameraControllerAvatarIntent, TGCameraControllerSignupAvatarIntent, - TGCameraControllerGenericPhotoOnlyIntent + TGCameraControllerGenericPhotoOnlyIntent, + TGCameraControllerGenericVideoOnlyIntent } TGCameraControllerIntent; @interface TGCameraControllerWindow : TGOverlayControllerWindow diff --git a/submodules/LegacyComponents/PublicHeaders/LegacyComponents/TGCameraMainView.h b/submodules/LegacyComponents/PublicHeaders/LegacyComponents/TGCameraMainView.h index 7e023c677a..540134b897 100644 --- a/submodules/LegacyComponents/PublicHeaders/LegacyComponents/TGCameraMainView.h +++ b/submodules/LegacyComponents/PublicHeaders/LegacyComponents/TGCameraMainView.h @@ -67,7 +67,7 @@ @property (nonatomic, assign) CGRect previewViewFrame; -- (instancetype)initWithFrame:(CGRect)frame avatar:(bool)avatar hasUltrawideCamera:(bool)hasUltrawideCamera hasTelephotoCamera:(bool)hasTelephotoCamera; +- (instancetype)initWithFrame:(CGRect)frame avatar:(bool)avatar videoModeByDefault:(bool)videoModeByDefault hasUltrawideCamera:(bool)hasUltrawideCamera hasTelephotoCamera:(bool)hasTelephotoCamera; - (void)setDocumentFrameHidden:(bool)hidden; - (void)setCameraMode:(PGCameraMode)mode; diff --git a/submodules/LegacyComponents/PublicHeaders/LegacyComponents/TGCameraModeControl.h b/submodules/LegacyComponents/PublicHeaders/LegacyComponents/TGCameraModeControl.h index 0c23e2c8d7..789c523067 100644 --- a/submodules/LegacyComponents/PublicHeaders/LegacyComponents/TGCameraModeControl.h +++ b/submodules/LegacyComponents/PublicHeaders/LegacyComponents/TGCameraModeControl.h @@ -10,6 +10,6 @@ - (void)setHidden:(bool)hidden animated:(bool)animated; -- (instancetype)initWithFrame:(CGRect)frame avatar:(bool)avatar; +- (instancetype)initWithFrame:(CGRect)frame avatar:(bool)avatar videoModeByDefault:(bool)videoModeByDefault; @end diff --git a/submodules/LegacyComponents/PublicHeaders/LegacyComponents/TGMediaSelectionContext.h b/submodules/LegacyComponents/PublicHeaders/LegacyComponents/TGMediaSelectionContext.h index 5770bf78b9..bc3c16874c 100644 --- a/submodules/LegacyComponents/PublicHeaders/LegacyComponents/TGMediaSelectionContext.h +++ b/submodules/LegacyComponents/PublicHeaders/LegacyComponents/TGMediaSelectionContext.h @@ -13,6 +13,7 @@ @property (nonatomic, readonly) bool allowGrouping; @property (nonatomic, readonly) int selectionLimit; @property (nonatomic, copy) void (^selectionLimitExceeded)(void); +@property (nonatomic, copy) bool (^attemptSelectingItem)(id); @property (nonatomic, assign) bool grouping; - (SSignal *)groupingChangedSignal; diff --git a/submodules/LegacyComponents/Sources/TGAttachmentCameraView.m b/submodules/LegacyComponents/Sources/TGAttachmentCameraView.m index ba9aeee3a7..1a7ca9334a 100644 --- a/submodules/LegacyComponents/Sources/TGAttachmentCameraView.m +++ b/submodules/LegacyComponents/Sources/TGAttachmentCameraView.m @@ -33,7 +33,7 @@ @implementation TGAttachmentCameraView -- (instancetype)initForSelfPortrait:(bool)selfPortrait +- (instancetype)initForSelfPortrait:(bool)selfPortrait videoModeByDefault:(bool)videoModeByDefault { self = [super initWithFrame:CGRectZero]; if (self != nil) @@ -46,7 +46,7 @@ PGCamera *camera = nil; if ([PGCamera cameraAvailable]) { - camera = [[PGCamera alloc] initWithMode:PGCameraModePhoto position:selfPortrait ? PGCameraPositionFront : PGCameraPositionUndefined]; + camera = [[PGCamera alloc] initWithMode:videoModeByDefault ? PGCameraModeVideo : PGCameraModePhoto position:selfPortrait ? PGCameraPositionFront : PGCameraPositionUndefined]; } _camera = camera; diff --git a/submodules/LegacyComponents/Sources/TGAttachmentCarouselItemView.m b/submodules/LegacyComponents/Sources/TGAttachmentCarouselItemView.m index 10fd7273cb..5854d03db7 100644 --- a/submodules/LegacyComponents/Sources/TGAttachmentCarouselItemView.m +++ b/submodules/LegacyComponents/Sources/TGAttachmentCarouselItemView.m @@ -230,7 +230,7 @@ const NSUInteger TGAttachmentDisplayedAssetLimit = 500; if (hasCamera) { - _cameraView = [[TGAttachmentCameraView alloc] initForSelfPortrait:selfPortrait]; + _cameraView = [[TGAttachmentCameraView alloc] initForSelfPortrait:selfPortrait videoModeByDefault:false]; _cameraView.frame = CGRectMake(_smallLayout.minimumLineSpacing, 0, TGAttachmentCellSize.width, TGAttachmentCellSize.height); [_cameraView startPreview]; diff --git a/submodules/LegacyComponents/Sources/TGCameraController.m b/submodules/LegacyComponents/Sources/TGCameraController.m index 5eed923316..b870a8017e 100644 --- a/submodules/LegacyComponents/Sources/TGCameraController.m +++ b/submodules/LegacyComponents/Sources/TGCameraController.m @@ -170,8 +170,12 @@ static CGPoint TGCameraControllerClampPointToScreenSize(__unused id self, __unus _items = [[NSMutableArray alloc] init]; - if (_intent != TGCameraControllerGenericIntent) + if (_intent != TGCameraControllerGenericIntent) { _allowCaptions = false; + } + if (_intent == TGCameraControllerGenericVideoOnlyIntent || _intent == TGCameraControllerGenericPhotoOnlyIntent) { + _allowCaptions = false; + } _saveEditedPhotos = saveEditedPhotos; _saveCapturedMedia = saveCapturedMedia; @@ -303,12 +307,12 @@ static CGPoint TGCameraControllerClampPointToScreenSize(__unused id self, __unus if ([[UIDevice currentDevice] userInterfaceIdiom] == UIUserInterfaceIdiomPhone) { - _interfaceView = [[TGCameraMainPhoneView alloc] initWithFrame:screenBounds avatar:_intent == TGCameraControllerAvatarIntent hasUltrawideCamera:_camera.hasUltrawideCamera hasTelephotoCamera:_camera.hasTelephotoCamera]; + _interfaceView = [[TGCameraMainPhoneView alloc] initWithFrame:screenBounds avatar:_intent == TGCameraControllerAvatarIntent videoModeByDefault:_intent == TGCameraControllerGenericVideoOnlyIntent hasUltrawideCamera:_camera.hasUltrawideCamera hasTelephotoCamera:_camera.hasTelephotoCamera]; [_interfaceView setInterfaceOrientation:interfaceOrientation animated:false]; } else { - _interfaceView = [[TGCameraMainTabletView alloc] initWithFrame:screenBounds avatar:_intent == TGCameraControllerAvatarIntent hasUltrawideCamera:_camera.hasUltrawideCamera hasTelephotoCamera:_camera.hasTelephotoCamera]; + _interfaceView = [[TGCameraMainTabletView alloc] initWithFrame:screenBounds avatar:_intent == TGCameraControllerAvatarIntent videoModeByDefault:_intent == TGCameraControllerGenericVideoOnlyIntent hasUltrawideCamera:_camera.hasUltrawideCamera hasTelephotoCamera:_camera.hasTelephotoCamera]; [_interfaceView setInterfaceOrientation:interfaceOrientation animated:false]; CGSize referenceSize = [self referenceViewSizeForOrientation:interfaceOrientation]; @@ -451,8 +455,12 @@ static CGPoint TGCameraControllerClampPointToScreenSize(__unused id self, __unus } }; - if (_intent != TGCameraControllerGenericIntent && _intent != TGCameraControllerAvatarIntent) + if (_intent != TGCameraControllerGenericIntent && _intent != TGCameraControllerAvatarIntent) { [_interfaceView setHasModeControl:false]; + } + if (_intent == TGCameraControllerGenericVideoOnlyIntent || _intent == TGCameraControllerGenericPhotoOnlyIntent) { + [_interfaceView setHasModeControl:false]; + } if (@available(iOS 11.0, *)) { _backgroundView.accessibilityIgnoresInvertColors = true; @@ -1527,7 +1535,7 @@ static CGPoint TGCameraControllerClampPointToScreenSize(__unused id self, __unus } }]; - bool hasCamera = !self.inhibitMultipleCapture && (((_intent == TGCameraControllerGenericIntent || _intent == TGCameraControllerGenericPhotoOnlyIntent) && !_shortcut) || (_intent == TGCameraControllerPassportMultipleIntent)); + bool hasCamera = !self.inhibitMultipleCapture && (((_intent == TGCameraControllerGenericIntent || _intent == TGCameraControllerGenericPhotoOnlyIntent || _intent == TGCameraControllerGenericVideoOnlyIntent) && !_shortcut) || (_intent == TGCameraControllerPassportMultipleIntent)); TGMediaPickerGalleryModel *model = [[TGMediaPickerGalleryModel alloc] initWithContext:windowContext items:galleryItems focusItem:focusItem selectionContext:_items.count > 1 ? selectionContext : nil editingContext:editingContext hasCaptions:self.allowCaptions allowCaptionEntities:self.allowCaptionEntities hasTimer:self.hasTimer onlyCrop:_intent == TGCameraControllerPassportIntent || _intent == TGCameraControllerPassportIdIntent || _intent == TGCameraControllerPassportMultipleIntent inhibitDocumentCaptions:self.inhibitDocumentCaptions hasSelectionPanel:true hasCamera:hasCamera recipientName:self.recipientName]; model.inhibitMute = self.inhibitMute; model.controller = galleryController; diff --git a/submodules/LegacyComponents/Sources/TGCameraMainPhoneView.m b/submodules/LegacyComponents/Sources/TGCameraMainPhoneView.m index 4cee5d2451..a8741b4b1d 100644 --- a/submodules/LegacyComponents/Sources/TGCameraMainPhoneView.m +++ b/submodules/LegacyComponents/Sources/TGCameraMainPhoneView.m @@ -101,7 +101,7 @@ @synthesize cancelPressed; @synthesize actionHandle = _actionHandle; -- (instancetype)initWithFrame:(CGRect)frame avatar:(bool)avatar hasUltrawideCamera:(bool)hasUltrawideCamera hasTelephotoCamera:(bool)hasTelephotoCamera +- (instancetype)initWithFrame:(CGRect)frame avatar:(bool)avatar videoModeByDefault:(bool)videoModeByDefault hasUltrawideCamera:(bool)hasUltrawideCamera hasTelephotoCamera:(bool)hasTelephotoCamera { self = [super initWithFrame:frame]; if (self != nil) @@ -314,7 +314,7 @@ [_shutterButton addGestureRecognizer:shutterPanGestureRecognizer]; [_bottomPanelView addSubview:_shutterButton]; - _modeControl = [[TGCameraModeControl alloc] initWithFrame:CGRectMake(0, 0, frame.size.width, _modeControlHeight) avatar:avatar]; + _modeControl = [[TGCameraModeControl alloc] initWithFrame:CGRectMake(0, 0, frame.size.width, _modeControlHeight) avatar:avatar videoModeByDefault:videoModeByDefault]; [_bottomPanelView addSubview:_modeControl]; _flipButton = [[TGCameraFlipButton alloc] initWithFrame:CGRectMake(0, 0, 48, 48)]; @@ -414,6 +414,12 @@ [_photoCounterButton addTarget:self action:@selector(photoCounterButtonPressed) forControlEvents:UIControlEventTouchUpInside]; _photoCounterButton.userInteractionEnabled = false; [_bottomPanelView addSubview:_photoCounterButton]; + + if (videoModeByDefault) { + [UIView performWithoutAnimation:^{ + [self updateForCameraModeChangeWithPreviousMode:PGCameraModePhoto]; + }]; + } } return self; } diff --git a/submodules/LegacyComponents/Sources/TGCameraMainTabletView.m b/submodules/LegacyComponents/Sources/TGCameraMainTabletView.m index 653a06ebc3..632527b333 100644 --- a/submodules/LegacyComponents/Sources/TGCameraMainTabletView.m +++ b/submodules/LegacyComponents/Sources/TGCameraMainTabletView.m @@ -42,7 +42,7 @@ const CGFloat TGCameraTabletPanelViewWidth = 102.0f; @synthesize shutterReleased; @synthesize cancelPressed; -- (instancetype)initWithFrame:(CGRect)frame avatar:(bool)avatar hasUltrawideCamera:(bool)hasUltrawideCamera hasTelephotoCamera:(bool)hasTelephotoCamera +- (instancetype)initWithFrame:(CGRect)frame avatar:(bool)avatar videoModeByDefault:(bool)videoModeByDefault hasUltrawideCamera:(bool)hasUltrawideCamera hasTelephotoCamera:(bool)hasTelephotoCamera { self = [super initWithFrame:frame]; if (self != nil) @@ -83,7 +83,7 @@ const CGFloat TGCameraTabletPanelViewWidth = 102.0f; [_shutterButton addTarget:self action:@selector(shutterButtonReleased) forControlEvents:UIControlEventTouchUpInside]; [_panelView addSubview:_shutterButton]; - _modeControl = [[TGCameraModeControl alloc] initWithFrame:CGRectMake(0, 0, _panelView.frame.size.width, 260) avatar:avatar]; + _modeControl = [[TGCameraModeControl alloc] initWithFrame:CGRectMake(0, 0, _panelView.frame.size.width, 260) avatar:avatar videoModeByDefault:videoModeByDefault]; [_panelView addSubview:_modeControl]; __weak TGCameraMainTabletView *weakSelf = self; diff --git a/submodules/LegacyComponents/Sources/TGCameraModeControl.m b/submodules/LegacyComponents/Sources/TGCameraModeControl.m index 412c8532bc..b3ec98d6fa 100644 --- a/submodules/LegacyComponents/Sources/TGCameraModeControl.m +++ b/submodules/LegacyComponents/Sources/TGCameraModeControl.m @@ -21,7 +21,7 @@ const CGFloat TGCameraModeControlVerticalInteritemSpace = 29.0f; @implementation TGCameraModeControl -- (instancetype)initWithFrame:(CGRect)frame avatar:(bool)avatar +- (instancetype)initWithFrame:(CGRect)frame avatar:(bool)avatar videoModeByDefault:(bool)videoModeByDefault { self = [super initWithFrame:frame]; if (self != nil) @@ -87,7 +87,11 @@ const CGFloat TGCameraModeControlVerticalInteritemSpace = 29.0f; _wrapperView.frame = CGRectMake(33, 0, self.frame.size.width, topOffset - TGCameraModeControlVerticalInteritemSpace); } - self.cameraMode = PGCameraModePhoto; + if (videoModeByDefault) { + self.cameraMode = PGCameraModeVideo; + } else { + self.cameraMode = PGCameraModePhoto; + } } return self; } diff --git a/submodules/LegacyComponents/Sources/TGMediaSelectionContext.m b/submodules/LegacyComponents/Sources/TGMediaSelectionContext.m index dcb967ce39..c59fc65d67 100644 --- a/submodules/LegacyComponents/Sources/TGMediaSelectionContext.m +++ b/submodules/LegacyComponents/Sources/TGMediaSelectionContext.m @@ -72,6 +72,12 @@ { if (![(id)item conformsToProtocol:@protocol(TGMediaSelectableItem)]) return false; + + if (_attemptSelectingItem) { + if (!_attemptSelectingItem(item)) { + return false; + } + } NSString *identifier = item.uniqueIdentifier; if (selected) diff --git a/submodules/MediaPickerUI/Sources/MediaPickerGridItem.swift b/submodules/MediaPickerUI/Sources/MediaPickerGridItem.swift index 0f4059fbe5..4d549e9b53 100644 --- a/submodules/MediaPickerUI/Sources/MediaPickerGridItem.swift +++ b/submodules/MediaPickerUI/Sources/MediaPickerGridItem.swift @@ -160,7 +160,9 @@ final class MediaPickerGridItemNode: GridItemNode { let checkNode = InteractiveCheckNode(theme: CheckNodeTheme(theme: theme, style: .overlay)) checkNode.valueChanged = { [weak self] value in if let strongSelf = self, let interaction = strongSelf.interaction, let selectableItem = strongSelf.selectableItem { - interaction.toggleSelection(selectableItem, value, false) + if !interaction.toggleSelection(selectableItem, value, false) { + strongSelf.checkNode?.setSelected(false, animated: false) + } } } self.addSubnode(checkNode) diff --git a/submodules/MediaPickerUI/Sources/MediaPickerScreen.swift b/submodules/MediaPickerUI/Sources/MediaPickerScreen.swift index 2b9ff18c30..944a505151 100644 --- a/submodules/MediaPickerUI/Sources/MediaPickerScreen.swift +++ b/submodules/MediaPickerUI/Sources/MediaPickerScreen.swift @@ -25,7 +25,7 @@ import MoreButtonNode final class MediaPickerInteraction { let openMedia: (PHFetchResult, Int, UIImage?) -> Void let openSelectedMedia: (TGMediaSelectableItem, UIImage?) -> Void - let toggleSelection: (TGMediaSelectableItem, Bool, Bool) -> Void + let toggleSelection: (TGMediaSelectableItem, Bool, Bool) -> Bool let sendSelected: (TGMediaSelectableItem?, Bool, Int32?, Bool, @escaping () -> Void) -> Void let schedule: () -> Void let dismissInput: () -> Void @@ -33,7 +33,7 @@ final class MediaPickerInteraction { let editingState: TGMediaEditingContext var hiddenMediaId: String? - init(openMedia: @escaping (PHFetchResult, Int, UIImage?) -> Void, openSelectedMedia: @escaping (TGMediaSelectableItem, UIImage?) -> Void, toggleSelection: @escaping (TGMediaSelectableItem, Bool, Bool) -> Void, sendSelected: @escaping (TGMediaSelectableItem?, Bool, Int32?, Bool, @escaping () -> Void) -> Void, schedule: @escaping () -> Void, dismissInput: @escaping () -> Void, selectionState: TGMediaSelectionContext?, editingState: TGMediaEditingContext) { + init(openMedia: @escaping (PHFetchResult, Int, UIImage?) -> Void, openSelectedMedia: @escaping (TGMediaSelectableItem, UIImage?) -> Void, toggleSelection: @escaping (TGMediaSelectableItem, Bool, Bool) -> Bool, sendSelected: @escaping (TGMediaSelectableItem?, Bool, Int32?, Bool, @escaping () -> Void) -> Void, schedule: @escaping () -> Void, dismissInput: @escaping () -> Void, selectionState: TGMediaSelectionContext?, editingState: TGMediaEditingContext) { self.openMedia = openMedia self.openSelectedMedia = openSelectedMedia self.toggleSelection = toggleSelection @@ -403,7 +403,7 @@ public final class MediaPickerScreen: ViewController, AttachmentContainable { } if let controller = self.controller, case .assets(nil) = controller.subject { - let cameraView = TGAttachmentCameraView(forSelfPortrait: false)! + let cameraView = TGAttachmentCameraView(forSelfPortrait: false, videoModeByDefault: controller.bannedSendPhotos != nil && controller.bannedSendVideos == nil)! cameraView.clipsToBounds = true cameraView.removeCorners() cameraView.pressed = { [weak self] in @@ -946,7 +946,13 @@ public final class MediaPickerScreen: ViewController, AttachmentContainable { if cameraAccess == nil { cameraRect = nil } - /*if let (untilDate, personal) = self.controller?.bannedSendMedia { + + var bannedSendMedia: (Int32, Bool)? + if let bannedSendPhotos = self.controller?.bannedSendPhotos, let bannedSendVideos = self.controller?.bannedSendVideos { + bannedSendMedia = (max(bannedSendPhotos.0, bannedSendVideos.0), bannedSendPhotos.1 || bannedSendVideos.1) + } + + if let (untilDate, personal) = bannedSendMedia { self.gridNode.isHidden = true let banDescription: String @@ -973,7 +979,7 @@ public final class MediaPickerScreen: ViewController, AttachmentContainable { placeholderTransition.updateFrame(node: placeholderNode, frame: innerBounds) self.updateNavigation(transition: .immediate) - } else */if case .notDetermined = mediaAccess { + } else if case .notDetermined = mediaAccess { } else { if case .limited = mediaAccess { let manageNode: MediaPickerManageNode @@ -1073,6 +1079,11 @@ public final class MediaPickerScreen: ViewController, AttachmentContainable { } } + var bannedSendMedia: (Int32, Bool)? + if let bannedSendPhotos = self.controller?.bannedSendPhotos, let bannedSendVideos = self.controller?.bannedSendVideos { + bannedSendMedia = (max(bannedSendPhotos.0, bannedSendVideos.0), bannedSendPhotos.1 || bannedSendVideos.1) + } + if case let .noAccess(cameraAccess) = self.state { var placeholderTransition = transition let placeholderNode: MediaPickerPlaceholderNode @@ -1099,7 +1110,7 @@ public final class MediaPickerScreen: ViewController, AttachmentContainable { } placeholderNode.update(layout: layout, theme: self.presentationData.theme, strings: self.presentationData.strings, hasCamera: cameraAccess == .authorized, transition: placeholderTransition) placeholderTransition.updateFrame(node: placeholderNode, frame: innerBounds) - } else if let placeholderNode = self.placeholderNode {//, self.controller?.bannedSendMedia == nil { + } else if let placeholderNode = self.placeholderNode, bannedSendMedia == nil { self.placeholderNode = nil placeholderNode.removeFromSupernode() } @@ -1161,6 +1172,45 @@ public final class MediaPickerScreen: ViewController, AttachmentContainable { self.statusBar.statusBarStyle = .Ignore + selectionContext.attemptSelectingItem = { [weak self] item in + guard let self else { + return false + } + if let _ = item as? TGMediaPickerGalleryPhotoItem { + if self.bannedSendPhotos != nil { + //TODO:localize + self.present(standardTextAlertController(theme: AlertControllerTheme(presentationData: self.presentationData), title: nil, text: "Sending photos is not allowed", actions: [TextAlertAction(type: .defaultAction, title: self.presentationData.strings.Common_OK, action: {})]), in: .window(.root)) + + return false + } + } else if let _ = item as? TGMediaPickerGalleryVideoItem { + if self.bannedSendVideos != nil { + //TODO:localize + self.present(standardTextAlertController(theme: AlertControllerTheme(presentationData: self.presentationData), title: nil, text: "Sending videos is not allowed", actions: [TextAlertAction(type: .defaultAction, title: self.presentationData.strings.Common_OK, action: {})]), in: .window(.root)) + + return false + } + } else if let asset = item as? TGMediaAsset { + if asset.isVideo { + if self.bannedSendVideos != nil { + //TODO:localize + self.present(standardTextAlertController(theme: AlertControllerTheme(presentationData: self.presentationData), title: nil, text: "Sending videos is not allowed", actions: [TextAlertAction(type: .defaultAction, title: self.presentationData.strings.Common_OK, action: {})]), in: .window(.root)) + + return false + } + } else { + if self.bannedSendPhotos != nil { + //TODO:localize + self.present(standardTextAlertController(theme: AlertControllerTheme(presentationData: self.presentationData), title: nil, text: "Sending photos is not allowed", actions: [TextAlertAction(type: .defaultAction, title: self.presentationData.strings.Common_OK, action: {})]), in: .window(.root)) + + return false + } + } + } + + return true + } + self.presentationDataDisposable = ((updatedPresentationData?.signal ?? context.sharedContext.presentationData) |> deliverOnMainQueue).start(next: { [weak self] presentationData in if let strongSelf = self { @@ -1223,7 +1273,39 @@ public final class MediaPickerScreen: ViewController, AttachmentContainable { }, openSelectedMedia: { [weak self] item, immediateThumbnail in self?.controllerNode.openSelectedMedia(item: item, immediateThumbnail: immediateThumbnail) }, toggleSelection: { [weak self] item, value, suggestUndo in - if let strongSelf = self, let selectionState = strongSelf.interaction?.selectionState { + if let self = self, let selectionState = self.interaction?.selectionState { + if let _ = item as? TGMediaPickerGalleryPhotoItem { + if self.bannedSendPhotos != nil { + //TODO:localize + self.present(standardTextAlertController(theme: AlertControllerTheme(presentationData: self.presentationData), title: nil, text: "Sending photos is not allowed", actions: [TextAlertAction(type: .defaultAction, title: self.presentationData.strings.Common_OK, action: {})]), in: .window(.root)) + + return false + } + } else if let _ = item as? TGMediaPickerGalleryVideoItem { + if self.bannedSendVideos != nil { + //TODO:localize + self.present(standardTextAlertController(theme: AlertControllerTheme(presentationData: self.presentationData), title: nil, text: "Sending videos is not allowed", actions: [TextAlertAction(type: .defaultAction, title: self.presentationData.strings.Common_OK, action: {})]), in: .window(.root)) + + return false + } + } else if let asset = item as? TGMediaAsset { + if asset.isVideo { + if self.bannedSendVideos != nil { + //TODO:localize + self.present(standardTextAlertController(theme: AlertControllerTheme(presentationData: self.presentationData), title: nil, text: "Sending videos is not allowed", actions: [TextAlertAction(type: .defaultAction, title: self.presentationData.strings.Common_OK, action: {})]), in: .window(.root)) + + return false + } + } else { + if self.bannedSendPhotos != nil { + //TODO:localize + self.present(standardTextAlertController(theme: AlertControllerTheme(presentationData: self.presentationData), title: nil, text: "Sending photos is not allowed", actions: [TextAlertAction(type: .defaultAction, title: self.presentationData.strings.Common_OK, action: {})]), in: .window(.root)) + + return false + } + } + } + var showUndo = false if suggestUndo { if !value { @@ -1237,8 +1319,12 @@ public final class MediaPickerScreen: ViewController, AttachmentContainable { selectionState.setItem(item, selected: value) if showUndo { - strongSelf.showSelectionUndo(item: item) + self.showSelectionUndo(item: item) } + + return true + } else { + return false } }, sendSelected: { [weak self] currentItem, silently, scheduleTime, animated, completion in if let strongSelf = self, let selectionState = strongSelf.interaction?.selectionState, !strongSelf.isDismissing { diff --git a/submodules/MediaPickerUI/Sources/MediaPickerSelectedListNode.swift b/submodules/MediaPickerUI/Sources/MediaPickerSelectedListNode.swift index 9035dd281e..caa9b88849 100644 --- a/submodules/MediaPickerUI/Sources/MediaPickerSelectedListNode.swift +++ b/submodules/MediaPickerUI/Sources/MediaPickerSelectedListNode.swift @@ -254,7 +254,9 @@ private class MediaPickerSelectedItemNode: ASDisplayNode { let checkNode = InteractiveCheckNode(theme: CheckNodeTheme(theme: theme, style: .overlay)) checkNode.valueChanged = { [weak self] value in if let strongSelf = self, let interaction = strongSelf.interaction, let selectableItem = strongSelf.asset as? TGMediaSelectableItem { - interaction.toggleSelection(selectableItem, value, true) + if !interaction.toggleSelection(selectableItem, value, true) { + strongSelf.checkNode?.setSelected(false, animated: false) + } } } self.addSubnode(checkNode) diff --git a/submodules/PeerInfoUI/Sources/ChannelPermissionsController.swift b/submodules/PeerInfoUI/Sources/ChannelPermissionsController.swift index 6f727e0b20..afe3d23aa2 100644 --- a/submodules/PeerInfoUI/Sources/ChannelPermissionsController.swift +++ b/submodules/PeerInfoUI/Sources/ChannelPermissionsController.swift @@ -378,7 +378,7 @@ private struct ChannelPermissionsControllerState: Equatable { func stringForGroupPermission(strings: PresentationStrings, right: TelegramChatBannedRightsFlags, isForum: Bool) -> String { //TODO:localize - if right.contains(.banSendMessages) { + if right.contains(.banSendText) { return strings.Channel_BanUser_PermissionSendMessages } else if right.contains(.banSendMedia) { return strings.Channel_BanUser_PermissionSendMedia @@ -416,7 +416,7 @@ func stringForGroupPermission(strings: PresentationStrings, right: TelegramChatB } func compactStringForGroupPermission(strings: PresentationStrings, right: TelegramChatBannedRightsFlags) -> String { - if right.contains(.banSendMessages) { + if right.contains(.banSendText) { return strings.GroupPermission_NoSendMessages } else if right.contains(.banSendMedia) { return strings.GroupPermission_NoSendMedia @@ -440,7 +440,7 @@ func compactStringForGroupPermission(strings: PresentationStrings, right: Telegr } private let internal_allPossibleGroupPermissionList: [(TelegramChatBannedRightsFlags, TelegramChannelPermission)] = [ - (.banSendMessages, .banMembers), + (.banSendText, .banMembers), (.banSendMedia, .banMembers), (.banSendPhotos, .banMembers), (.banSendVideos, .banMembers), @@ -460,9 +460,8 @@ private let internal_allPossibleGroupPermissionList: [(TelegramChatBannedRightsF public func allGroupPermissionList(peer: EnginePeer) -> [(TelegramChatBannedRightsFlags, TelegramChannelPermission)] { if case let .channel(channel) = peer, channel.flags.contains(.isForum) { return [ - (.banSendMessages, .banMembers), + (.banSendText, .banMembers), (.banSendMedia, .banMembers), - (.banSendPolls, .banMembers), (.banAddMembers, .banMembers), (.banPinMessages, .pinMessages), (.banManageTopics, .manageTopics), @@ -470,9 +469,8 @@ public func allGroupPermissionList(peer: EnginePeer) -> [(TelegramChatBannedRigh ] } else { return [ - (.banSendMessages, .banMembers), + (.banSendText, .banMembers), (.banSendMedia, .banMembers), - (.banSendPolls, .banMembers), (.banAddMembers, .banMembers), (.banPinMessages, .pinMessages), (.banChangeInfo, .changeInfo) @@ -490,6 +488,7 @@ public func banSendMediaSubList() -> [(TelegramChatBannedRightsFlags, TelegramCh (.banSendVoice, .banMembers), (.banSendInstantVideos, .banMembers), (.banEmbedLinks, .banMembers), + (.banSendPolls, .banMembers), ] } @@ -500,13 +499,13 @@ let publicGroupRestrictedPermissions: TelegramChatBannedRightsFlags = [ func groupPermissionDependencies(_ right: TelegramChatBannedRightsFlags) -> TelegramChatBannedRightsFlags { if right.contains(.banSendMedia) || banSendMediaSubList().contains(where: { $0.0 == right }) { - return [.banSendMessages] + return [] } else if right.contains(.banSendGifs) { - return [.banSendMessages] + return [] } else if right.contains(.banEmbedLinks) { - return [.banSendMessages] + return [] } else if right.contains(.banSendPolls) { - return [.banSendMessages] + return [] } else if right.contains(.banChangeInfo) { return [] } else if right.contains(.banAddMembers) { diff --git a/submodules/ReactionSelectionNode/Sources/ReactionContextNode.swift b/submodules/ReactionSelectionNode/Sources/ReactionContextNode.swift index 78726fe60f..dca2e17162 100644 --- a/submodules/ReactionSelectionNode/Sources/ReactionContextNode.swift +++ b/submodules/ReactionSelectionNode/Sources/ReactionContextNode.swift @@ -460,7 +460,7 @@ public final class ReactionContextNode: ASDisplayNode, UIScrollViewDelegate { } else { strongSelf.stableEmptyResultEmoji = nil } - emojiContent = emojiContent.withUpdatedItemGroups(itemGroups: emojiSearchResult.groups, itemContentUniqueId: emojiSearchResult.id, emptySearchResults: emptySearchResults) + emojiContent = emojiContent.withUpdatedItemGroups(panelItemGroups: emojiContent.panelItemGroups, contentItemGroups: emojiSearchResult.groups, itemContentUniqueId: emojiSearchResult.id, emptySearchResults: emptySearchResults) } else { strongSelf.stableEmptyResultEmoji = nil } diff --git a/submodules/TelegramCore/Sources/ApiUtils/TelegramChannel.swift b/submodules/TelegramCore/Sources/ApiUtils/TelegramChannel.swift index 2aa010ea94..fd2d4197ca 100644 --- a/submodules/TelegramCore/Sources/ApiUtils/TelegramChannel.swift +++ b/submodules/TelegramCore/Sources/ApiUtils/TelegramChannel.swift @@ -3,7 +3,10 @@ import Postbox public enum TelegramChannelPermission { - case sendMessages + case sendText + case sendPhoto + case sendVideo + case sendSomething case pinMessages case manageTopics case createTopics @@ -30,7 +33,7 @@ public extension TelegramChannel { return true } switch permission { - case .sendMessages: + case .sendText: if case .broadcast = self.info { if let adminRights = self.adminRights { return adminRights.rights.contains(.canPostMessages) @@ -41,10 +44,80 @@ public extension TelegramChannel { if let _ = self.adminRights { return true } - if let bannedRights = self.bannedRights, bannedRights.flags.contains(.banSendMessages) { + if let bannedRights = self.bannedRights, bannedRights.flags.contains(.banSendText) { return false } - if let defaultBannedRights = self.defaultBannedRights, defaultBannedRights.flags.contains(.banSendMessages) { + if let defaultBannedRights = self.defaultBannedRights, defaultBannedRights.flags.contains(.banSendText) { + return false + } + return true + } + case .sendPhoto: + if case .broadcast = self.info { + if let adminRights = self.adminRights { + return adminRights.rights.contains(.canPostMessages) + } else { + return false + } + } else { + if let _ = self.adminRights { + return true + } + if let bannedRights = self.bannedRights, bannedRights.flags.contains(.banSendPhotos) { + return false + } + if let defaultBannedRights = self.defaultBannedRights, defaultBannedRights.flags.contains(.banSendPhotos) { + return false + } + return true + } + case .sendVideo: + if case .broadcast = self.info { + if let adminRights = self.adminRights { + return adminRights.rights.contains(.canPostMessages) + } else { + return false + } + } else { + if let _ = self.adminRights { + return true + } + if let bannedRights = self.bannedRights, bannedRights.flags.contains(.banSendVideos) { + return false + } + if let defaultBannedRights = self.defaultBannedRights, defaultBannedRights.flags.contains(.banSendVideos) { + return false + } + return true + } + case .sendSomething: + if case .broadcast = self.info { + if let adminRights = self.adminRights { + return adminRights.rights.contains(.canPostMessages) + } else { + return false + } + } else { + if let _ = self.adminRights { + return true + } + + let flags: TelegramChatBannedRightsFlags = [ + .banSendText, + .banSendInstantVideos, + .banSendVoice, + .banSendPhotos, + .banSendVideos, + .banSendStickers, + .banSendPolls, + .banSendFiles, + .banSendInline + ] + + if let bannedRights = self.bannedRights, bannedRights.flags.intersection(flags) == flags { + return false + } + if let defaultBannedRights = self.defaultBannedRights, defaultBannedRights.flags.intersection(flags) == flags { return false } return true diff --git a/submodules/TelegramCore/Sources/ApiUtils/TelegramChannelBannedRights.swift b/submodules/TelegramCore/Sources/ApiUtils/TelegramChannelBannedRights.swift index 8de784a2cd..ec5aafc608 100644 --- a/submodules/TelegramCore/Sources/ApiUtils/TelegramChannelBannedRights.swift +++ b/submodules/TelegramCore/Sources/ApiUtils/TelegramChannelBannedRights.swift @@ -15,6 +15,7 @@ extension TelegramChatBannedRights { var apiBannedRights: Api.ChatBannedRights { var effectiveFlags = self.flags effectiveFlags.remove(.banSendMedia) + effectiveFlags.remove(TelegramChatBannedRightsFlags(rawValue: 1 << 1)) return .chatBannedRights(flags: effectiveFlags.rawValue, untilDate: self.untilDate) } diff --git a/submodules/TelegramCore/Sources/Authorization.swift b/submodules/TelegramCore/Sources/Authorization.swift index 60159c60b1..79a40d2d7c 100644 --- a/submodules/TelegramCore/Sources/Authorization.swift +++ b/submodules/TelegramCore/Sources/Authorization.swift @@ -79,7 +79,10 @@ public enum SendAuthorizationCodeResult { func storeFutureLoginToken(accountManager: AccountManager, token: Data) { let _ = (accountManager.transaction { transaction -> Void in var tokens = transaction.getStoredLoginTokens() - tokens.insert(token, at: 0) + + #if DEBUG + tokens.removeAll() + #endif var cloudValue: [Data] = [] if let list = NSUbiquitousKeyValueStore.default.object(forKey: "T_SLTokens") as? [String] { @@ -95,6 +98,7 @@ func storeFutureLoginToken(accountManager: AccountManager 20 { tokens.removeLast(tokens.count - 20) } @@ -143,12 +147,20 @@ public func sendAuthorizationCode(accountManager: AccountManager [Data] in return transaction.getStoredLoginTokens() } |> castError(AuthorizationCodeRequestError.self) |> mapToSignal { localAuthTokens -> Signal in var authTokens = localAuthTokens + + #if DEBUG + authTokens.removeAll() + #endif + for data in cloudValue { if !authTokens.contains(data) { authTokens.insert(data, at: 0) @@ -274,7 +286,7 @@ public func sendAuthorizationCode(accountManager: AccountManager castError(AuthorizationCodeRequestError.self) |> mapToSignal { firebaseSecret -> Signal in guard let firebaseSecret = firebaseSecret else { - return internalResendAuthorizationCode(account: account, number: phoneNumber, hash: phoneCodeHash, syncContacts: syncContacts) + return internalResendAuthorizationCode(accountManager: accountManager, account: account, number: phoneNumber, apiId: apiId, apiHash: apiHash, hash: phoneCodeHash, syncContacts: syncContacts, firebaseSecretStream: firebaseSecretStream) } return sendFirebaseAuthorizationCode(accountManager: accountManager, account: account, phoneNumber: phoneNumber, apiId: apiId, apiHash: apiHash, phoneCodeHash: phoneCodeHash, timeout: codeTimeout, firebaseSecret: firebaseSecret, syncContacts: syncContacts) @@ -293,7 +305,7 @@ public func sendAuthorizationCode(accountManager: AccountManager castError(AuthorizationCodeRequestError.self) } else { - return internalResendAuthorizationCode(account: account, number: phoneNumber, hash: phoneCodeHash, syncContacts: syncContacts) + return internalResendAuthorizationCode(accountManager: accountManager, account: account, number: phoneNumber, apiId: apiId, apiHash: apiHash, hash: phoneCodeHash, syncContacts: syncContacts, firebaseSecretStream: firebaseSecretStream) } } } @@ -333,7 +345,7 @@ public func sendAuthorizationCode(accountManager: AccountManager Signal { +private func internalResendAuthorizationCode(accountManager: AccountManager, account: UnauthorizedAccount, number: String, apiId: Int32, apiHash: String, hash: String, syncContacts: Bool, firebaseSecretStream: Signal<[String: String], NoError>) -> Signal { return account.network.request(Api.functions.auth.resendCode(phoneNumber: number, phoneCodeHash: hash), automaticFloodWait: false) |> mapError { error -> AuthorizationCodeRequestError in if error.errorDescription.hasPrefix("FLOOD_WAIT") { @@ -349,60 +361,155 @@ private func internalResendAuthorizationCode(account: UnauthorizedAccount, numbe } } |> mapToSignal { sentCode -> Signal in - return account.postbox.transaction { transaction -> SendAuthorizationCodeResult in + return account.postbox.transaction { transaction -> Signal in switch sentCode { - case let .sentCode(_, type, phoneCodeHash, nextType, timeout): + case let .sentCode(_, type, phoneCodeHash, nextType, codeTimeout): var parsedNextType: AuthorizationCodeNextType? if let nextType = nextType { parsedNextType = AuthorizationCodeNextType(apiType: nextType) } - transaction.setState(UnauthorizedAccountState(isTestingEnvironment: account.testingEnvironment, masterDatacenterId: account.masterDatacenterId, contents: .confirmationCodeEntry(number: number, type: SentAuthorizationCodeType(apiType: type), hash: phoneCodeHash, timeout: timeout, nextType: parsedNextType, syncContacts: syncContacts))) + if case let .sentCodeTypeFirebaseSms(_, _, receipt, pushTimeout, _) = type { + return firebaseSecretStream + |> map { mapping -> String? in + guard let receipt = receipt else { + return nil + } + if let value = mapping[receipt] { + return value + } + if receipt == "" && mapping.count == 1 { + return mapping.first?.value + } + return nil + } + |> filter { $0 != nil } + |> take(1) + |> timeout(Double(pushTimeout ?? 15), queue: .mainQueue(), alternate: .single(nil)) + |> castError(AuthorizationCodeRequestError.self) + |> mapToSignal { firebaseSecret -> Signal in + guard let firebaseSecret = firebaseSecret else { + return internalResendAuthorizationCode(accountManager: accountManager, account: account, number: number, apiId: apiId, apiHash: apiHash, hash: phoneCodeHash, syncContacts: syncContacts, firebaseSecretStream: firebaseSecretStream) + } + + return sendFirebaseAuthorizationCode(accountManager: accountManager, account: account, phoneNumber: number, apiId: apiId, apiHash: apiHash, phoneCodeHash: phoneCodeHash, timeout: codeTimeout, firebaseSecret: firebaseSecret, syncContacts: syncContacts) + |> `catch` { _ -> Signal in + return .single(false) + } + |> mapError { _ -> AuthorizationCodeRequestError in + return .generic(info: nil) + } + |> mapToSignal { success -> Signal in + if success { + return account.postbox.transaction { transaction -> SendAuthorizationCodeResult in + transaction.setState(UnauthorizedAccountState(isTestingEnvironment: account.testingEnvironment, masterDatacenterId: account.masterDatacenterId, contents: .confirmationCodeEntry(number: number, type: SentAuthorizationCodeType(apiType: type), hash: phoneCodeHash, timeout: codeTimeout, nextType: parsedNextType, syncContacts: syncContacts))) + + return .sentCode(account) + } + |> castError(AuthorizationCodeRequestError.self) + } else { + return internalResendAuthorizationCode(accountManager: accountManager, account: account, number: number, apiId: apiId, apiHash: apiHash, hash: phoneCodeHash, syncContacts: syncContacts, firebaseSecretStream: firebaseSecretStream) + } + } + } + } - return .sentCode(account) + transaction.setState(UnauthorizedAccountState(isTestingEnvironment: account.testingEnvironment, masterDatacenterId: account.masterDatacenterId, contents: .confirmationCodeEntry(number: number, type: SentAuthorizationCodeType(apiType: type), hash: phoneCodeHash, timeout: codeTimeout, nextType: parsedNextType, syncContacts: syncContacts))) + + return .single(.sentCode(account)) case .sentCodeSuccess: - return .loggedIn + return .single(.loggedIn) } - } |> mapError { _ -> AuthorizationCodeRequestError in } + } + |> mapError { _ -> AuthorizationCodeRequestError in } + |> switchToLatest } } -public func resendAuthorizationCode(account: UnauthorizedAccount) -> Signal { +public func resendAuthorizationCode(accountManager: AccountManager, account: UnauthorizedAccount, apiId: Int32, apiHash: String, firebaseSecretStream: Signal<[String: String], NoError>) -> Signal { return account.postbox.transaction { transaction -> Signal in if let state = transaction.getState() as? UnauthorizedAccountState { switch state.contents { case let .confirmationCodeEntry(number, _, hash, _, nextType, syncContacts): if nextType != nil { return account.network.request(Api.functions.auth.resendCode(phoneNumber: number, phoneCodeHash: hash), automaticFloodWait: false) - |> mapError { error -> AuthorizationCodeRequestError in - if error.errorDescription.hasPrefix("FLOOD_WAIT") { - return .limitExceeded - } else if error.errorDescription == "PHONE_NUMBER_INVALID" { - return .invalidPhoneNumber - } else if error.errorDescription == "PHONE_NUMBER_FLOOD" { - return .phoneLimitExceeded - } else if error.errorDescription == "PHONE_NUMBER_BANNED" { - return .phoneBanned - } else { - return .generic(info: (Int(error.errorCode), error.errorDescription)) - } + |> mapError { error -> AuthorizationCodeRequestError in + if error.errorDescription.hasPrefix("FLOOD_WAIT") { + return .limitExceeded + } else if error.errorDescription == "PHONE_NUMBER_INVALID" { + return .invalidPhoneNumber + } else if error.errorDescription == "PHONE_NUMBER_FLOOD" { + return .phoneLimitExceeded + } else if error.errorDescription == "PHONE_NUMBER_BANNED" { + return .phoneBanned + } else { + return .generic(info: (Int(error.errorCode), error.errorDescription)) } - |> mapToSignal { sentCode -> Signal in - return account.postbox.transaction { transaction -> Void in - switch sentCode { - case let .sentCode(_, type, phoneCodeHash, nextType, timeout): - - var parsedNextType: AuthorizationCodeNextType? - if let nextType = nextType { - parsedNextType = AuthorizationCodeNextType(apiType: nextType) - } - - transaction.setState(UnauthorizedAccountState(isTestingEnvironment: account.testingEnvironment, masterDatacenterId: account.masterDatacenterId, contents: .confirmationCodeEntry(number: number, type: SentAuthorizationCodeType(apiType: type), hash: phoneCodeHash, timeout: timeout, nextType: parsedNextType, syncContacts: syncContacts))) - case .sentCodeSuccess: - break + } + |> mapToSignal { sentCode -> Signal in + return account.postbox.transaction { transaction -> Signal in + switch sentCode { + case let .sentCode(_, type, phoneCodeHash, nextType, codeTimeout): + var parsedNextType: AuthorizationCodeNextType? + if let nextType = nextType { + parsedNextType = AuthorizationCodeNextType(apiType: nextType) } - } |> mapError { _ -> AuthorizationCodeRequestError in } + + if case let .sentCodeTypeFirebaseSms(_, _, receipt, pushTimeout, _) = type { + return firebaseSecretStream + |> map { mapping -> String? in + guard let receipt = receipt else { + return nil + } + if let value = mapping[receipt] { + return value + } + if receipt == "" && mapping.count == 1 { + return mapping.first?.value + } + return nil + } + |> filter { $0 != nil } + |> take(1) + |> timeout(Double(pushTimeout ?? 15), queue: .mainQueue(), alternate: .single(nil)) + |> castError(AuthorizationCodeRequestError.self) + |> mapToSignal { firebaseSecret -> Signal in + guard let firebaseSecret = firebaseSecret else { + return internalResendAuthorizationCode(accountManager: accountManager, account: account, number: number, apiId: apiId, apiHash: apiHash, hash: phoneCodeHash, syncContacts: syncContacts, firebaseSecretStream: firebaseSecretStream) + } + + return sendFirebaseAuthorizationCode(accountManager: accountManager, account: account, phoneNumber: number, apiId: apiId, apiHash: apiHash, phoneCodeHash: phoneCodeHash, timeout: codeTimeout, firebaseSecret: firebaseSecret, syncContacts: syncContacts) + |> `catch` { _ -> Signal in + return .single(false) + } + |> mapError { _ -> AuthorizationCodeRequestError in + return .generic(info: nil) + } + |> mapToSignal { success -> Signal in + if success { + return account.postbox.transaction { transaction -> SendAuthorizationCodeResult in + transaction.setState(UnauthorizedAccountState(isTestingEnvironment: account.testingEnvironment, masterDatacenterId: account.masterDatacenterId, contents: .confirmationCodeEntry(number: number, type: SentAuthorizationCodeType(apiType: type), hash: phoneCodeHash, timeout: codeTimeout, nextType: parsedNextType, syncContacts: syncContacts))) + + return .sentCode(account) + } + |> castError(AuthorizationCodeRequestError.self) + } else { + return internalResendAuthorizationCode(accountManager: accountManager, account: account, number: number, apiId: apiId, apiHash: apiHash, hash: phoneCodeHash, syncContacts: syncContacts, firebaseSecretStream: firebaseSecretStream) + } + } + } + |> map { _ -> Void in return Void() } + } + + transaction.setState(UnauthorizedAccountState(isTestingEnvironment: account.testingEnvironment, masterDatacenterId: account.masterDatacenterId, contents: .confirmationCodeEntry(number: number, type: SentAuthorizationCodeType(apiType: type), hash: phoneCodeHash, timeout: codeTimeout, nextType: parsedNextType, syncContacts: syncContacts))) + case .sentCodeSuccess: + break + } + return .single(Void()) } + |> mapError { _ -> AuthorizationCodeRequestError in } + |> switchToLatest + } } else { return .fail(.generic(info: nil)) } diff --git a/submodules/TelegramCore/Sources/Suggestions.swift b/submodules/TelegramCore/Sources/Suggestions.swift index 523dd5cb09..ba0125474b 100644 --- a/submodules/TelegramCore/Sources/Suggestions.swift +++ b/submodules/TelegramCore/Sources/Suggestions.swift @@ -8,7 +8,7 @@ public enum ServerProvidedSuggestion: String { case newcomerTicks = "NEWCOMER_TICKS" case validatePhoneNumber = "VALIDATE_PHONE_NUMBER" case validatePassword = "VALIDATE_PASSWORD" - case setupPassword = "SETUP_2FA" + case setupPassword = "SETUP_PASSWORD" } private var dismissedSuggestionsPromise = ValuePromise<[AccountRecordId: Set]>([:]) @@ -33,12 +33,7 @@ public func getServerProvidedSuggestions(account: Account) -> Signal<[ServerProv return [] } - #if DEBUG - var list = listItems - list.append(ServerProvidedSuggestion.setupPassword.rawValue) - #else let list = listItems - #endif return list.compactMap { item -> ServerProvidedSuggestion? in return ServerProvidedSuggestion(rawValue: item) diff --git a/submodules/TelegramCore/Sources/SyncCore/SyncCore_TelegramChatBannedRights.swift b/submodules/TelegramCore/Sources/SyncCore/SyncCore_TelegramChatBannedRights.swift index d7be95d4f2..884351ce23 100644 --- a/submodules/TelegramCore/Sources/SyncCore/SyncCore_TelegramChatBannedRights.swift +++ b/submodules/TelegramCore/Sources/SyncCore/SyncCore_TelegramChatBannedRights.swift @@ -12,7 +12,6 @@ public struct TelegramChatBannedRightsFlags: OptionSet, Hashable { } public static let banReadMessages = TelegramChatBannedRightsFlags(rawValue: 1 << 0) - public static let banSendMessages = TelegramChatBannedRightsFlags(rawValue: 1 << 1) public static let banSendMedia = TelegramChatBannedRightsFlags(rawValue: 1 << 2) public static let banSendStickers = TelegramChatBannedRightsFlags(rawValue: 1 << 3) public static let banSendGifs = TelegramChatBannedRightsFlags(rawValue: 1 << 4) @@ -30,6 +29,7 @@ public struct TelegramChatBannedRightsFlags: OptionSet, Hashable { public static let banSendMusic = TelegramChatBannedRightsFlags(rawValue: 1 << 22) public static let banSendVoice = TelegramChatBannedRightsFlags(rawValue: 1 << 23) public static let banSendFiles = TelegramChatBannedRightsFlags(rawValue: 1 << 24) + public static let banSendText = TelegramChatBannedRightsFlags(rawValue: 1 << 25) } public struct TelegramChatBannedRights: PostboxCoding, Equatable { diff --git a/submodules/TelegramCore/Sources/Utils/CanSendMessagesToPeer.swift b/submodules/TelegramCore/Sources/Utils/CanSendMessagesToPeer.swift index 3ef0fc377f..50ca3c53a0 100644 --- a/submodules/TelegramCore/Sources/Utils/CanSendMessagesToPeer.swift +++ b/submodules/TelegramCore/Sources/Utils/CanSendMessagesToPeer.swift @@ -14,7 +14,7 @@ public func canSendMessagesToPeer(_ peer: Peer) -> Bool { } else if let peer = peer as? TelegramSecretChat { return peer.embeddedState == .active } else if let peer = peer as? TelegramChannel { - return peer.hasPermission(.sendMessages) + return peer.hasPermission(.sendSomething) } else { return false } diff --git a/submodules/TelegramUI/Components/AvatarEditorScreen/Sources/AvatarEditorScreen.swift b/submodules/TelegramUI/Components/AvatarEditorScreen/Sources/AvatarEditorScreen.swift index 4083491d5f..9b4dd67c51 100644 --- a/submodules/TelegramUI/Components/AvatarEditorScreen/Sources/AvatarEditorScreen.swift +++ b/submodules/TelegramUI/Components/AvatarEditorScreen/Sources/AvatarEditorScreen.swift @@ -194,7 +194,7 @@ final class AvatarEditorScreenComponent: Component { private func updateData(_ data: KeyboardInputData) { self.data = data - self.state?.selectedItem = data.emoji.itemGroups.first?.items.first + self.state?.selectedItem = data.emoji.panelItemGroups.first?.items.first self.state?.updated(transition: .immediate) let updateSearchQuery: (String, String) -> Void = { [weak self] rawQuery, languageCode in @@ -735,9 +735,9 @@ final class AvatarEditorScreenComponent: Component { } if state?.keyboardContentId == AnyHashable("emoji") { - data.emoji = data.emoji.withUpdatedItemGroups(itemGroups: searchResult.groups, itemContentUniqueId: searchResult.id, emptySearchResults: emptySearchResults) + data.emoji = data.emoji.withUpdatedItemGroups(panelItemGroups: data.emoji.panelItemGroups, contentItemGroups: searchResult.groups, itemContentUniqueId: searchResult.id, emptySearchResults: emptySearchResults) } else { - data.stickers = data.stickers?.withUpdatedItemGroups(itemGroups: searchResult.groups, itemContentUniqueId: searchResult.id, emptySearchResults: emptySearchResults) + data.stickers = data.stickers?.withUpdatedItemGroups(panelItemGroups: data.stickers?.panelItemGroups ?? searchResult.groups, contentItemGroups: searchResult.groups, itemContentUniqueId: searchResult.id, emptySearchResults: emptySearchResults) } } diff --git a/submodules/TelegramUI/Components/ChatEntityKeyboardInputNode/Sources/ChatEntityKeyboardInputNode.swift b/submodules/TelegramUI/Components/ChatEntityKeyboardInputNode/Sources/ChatEntityKeyboardInputNode.swift index 4a147aa594..d78a8ffeb4 100644 --- a/submodules/TelegramUI/Components/ChatEntityKeyboardInputNode/Sources/ChatEntityKeyboardInputNode.swift +++ b/submodules/TelegramUI/Components/ChatEntityKeyboardInputNode/Sources/ChatEntityKeyboardInputNode.swift @@ -1196,7 +1196,7 @@ public final class ChatEntityKeyboardInputNode: ChatInputNode { iconFile: nil ) } - inputData.emoji = inputData.emoji.withUpdatedItemGroups(itemGroups: emojiSearchResult.groups, itemContentUniqueId: emojiSearchResult.id, emptySearchResults: emptySearchResults) + inputData.emoji = inputData.emoji.withUpdatedItemGroups(panelItemGroups: inputData.emoji.panelItemGroups, contentItemGroups: emojiSearchResult.groups, itemContentUniqueId: emojiSearchResult.id, emptySearchResults: emptySearchResults) } var transition: Transition = .immediate @@ -1586,9 +1586,9 @@ public final class ChatEntityKeyboardInputNode: ChatInputNode { private func processInputData(inputData: InputData) -> InputData { return InputData( - emoji: inputData.emoji.withUpdatedItemGroups(itemGroups: self.processStableItemGroupList(category: .emoji, itemGroups: inputData.emoji.itemGroups), itemContentUniqueId: inputData.emoji.itemContentUniqueId, emptySearchResults: inputData.emoji.emptySearchResults), + emoji: inputData.emoji.withUpdatedItemGroups(panelItemGroups: self.processStableItemGroupList(category: .emoji, itemGroups: inputData.emoji.panelItemGroups), contentItemGroups: self.processStableItemGroupList(category: .emoji, itemGroups: inputData.emoji.contentItemGroups), itemContentUniqueId: inputData.emoji.itemContentUniqueId, emptySearchResults: inputData.emoji.emptySearchResults), stickers: inputData.stickers.flatMap { stickers in - return stickers.withUpdatedItemGroups(itemGroups: self.processStableItemGroupList(category: .stickers, itemGroups: stickers.itemGroups), itemContentUniqueId: nil, emptySearchResults: nil) + return stickers.withUpdatedItemGroups(panelItemGroups: self.processStableItemGroupList(category: .stickers, itemGroups: stickers.panelItemGroups), contentItemGroups: self.processStableItemGroupList(category: .stickers, itemGroups: stickers.contentItemGroups), itemContentUniqueId: nil, emptySearchResults: nil) }, gifs: inputData.gifs, availableGifSearchEmojies: inputData.availableGifSearchEmojies diff --git a/submodules/TelegramUI/Components/EmojiStatusSelectionComponent/Sources/EmojiStatusSelectionComponent.swift b/submodules/TelegramUI/Components/EmojiStatusSelectionComponent/Sources/EmojiStatusSelectionComponent.swift index 2b40665c5b..627f28145a 100644 --- a/submodules/TelegramUI/Components/EmojiStatusSelectionComponent/Sources/EmojiStatusSelectionComponent.swift +++ b/submodules/TelegramUI/Components/EmojiStatusSelectionComponent/Sources/EmojiStatusSelectionComponent.swift @@ -371,7 +371,7 @@ public final class EmojiStatusSelectionController: ViewController { } else { strongSelf.stableEmptyResultEmoji = nil } - emojiContent = emojiContent.withUpdatedItemGroups(itemGroups: emojiSearchResult.groups, itemContentUniqueId: emojiSearchResult.id, emptySearchResults: emptySearchResults) + emojiContent = emojiContent.withUpdatedItemGroups(panelItemGroups: emojiContent.panelItemGroups, contentItemGroups: emojiSearchResult.groups, itemContentUniqueId: emojiSearchResult.id, emptySearchResults: emptySearchResults) } else { strongSelf.stableEmptyResultEmoji = nil } diff --git a/submodules/TelegramUI/Components/EntityKeyboard/Sources/EmojiPagerContentComponent.swift b/submodules/TelegramUI/Components/EntityKeyboard/Sources/EmojiPagerContentComponent.swift index f5a2aa0b4f..29ff9d6a3c 100644 --- a/submodules/TelegramUI/Components/EntityKeyboard/Sources/EmojiPagerContentComponent.swift +++ b/submodules/TelegramUI/Components/EntityKeyboard/Sources/EmojiPagerContentComponent.swift @@ -1520,8 +1520,10 @@ public final class EmojiSearchHeaderView: UIView, UITextFieldDelegate { var text: String var useOpaqueTheme: Bool var isActive: Bool + var hasPresetSearch: Bool var size: CGSize var canFocus: Bool + var hasSearchItems: Bool static func ==(lhs: Params, rhs: Params) -> Bool { if lhs.theme !== rhs.theme { @@ -1539,12 +1541,18 @@ public final class EmojiSearchHeaderView: UIView, UITextFieldDelegate { if lhs.isActive != rhs.isActive { return false } + if lhs.hasPresetSearch != rhs.hasPresetSearch { + return false + } if lhs.size != rhs.size { return false } if lhs.canFocus != rhs.canFocus { return false } + if lhs.hasSearchItems != rhs.hasSearchItems { + return false + } return true } } @@ -1565,6 +1573,9 @@ public final class EmojiSearchHeaderView: UIView, UITextFieldDelegate { private let searchIconView: UIImageView private let searchIconTintView: UIImageView + private let backIconView: UIImageView + private let backIconTintView: UIImageView + private let clearIconView: UIImageView private let clearIconTintView: UIImageView private let clearIconButton: HighlightTrackingButton @@ -1575,9 +1586,12 @@ public final class EmojiSearchHeaderView: UIView, UITextFieldDelegate { private let cancelButtonTitle: ComponentView private let cancelButton: HighlightTrackingButton + private var suggestedItemsView: ComponentView? + private var textField: EmojiSearchTextField? private var tapRecognizer: UITapGestureRecognizer? + private(set) var currentPresetSearchTerm: String? private var params: Params? @@ -1598,6 +1612,9 @@ public final class EmojiSearchHeaderView: UIView, UITextFieldDelegate { self.searchIconView = UIImageView() self.searchIconTintView = UIImageView() + self.backIconView = UIImageView() + self.backIconTintView = UIImageView() + self.clearIconView = UIImageView() self.clearIconTintView = UIImageView() self.clearIconButton = HighlightableButton() @@ -1619,6 +1636,9 @@ public final class EmojiSearchHeaderView: UIView, UITextFieldDelegate { self.addSubview(self.searchIconView) self.tintContainerView.addSubview(self.searchIconTintView) + self.addSubview(self.backIconView) + self.tintContainerView.addSubview(self.backIconTintView) + self.addSubview(self.clearIconView) self.tintContainerView.addSubview(self.clearIconTintView) self.addSubview(self.clearIconButton) @@ -1681,25 +1701,38 @@ public final class EmojiSearchHeaderView: UIView, UITextFieldDelegate { @objc private func tapGesture(_ recognizer: UITapGestureRecognizer) { if case .ended = recognizer.state { - if self.textField == nil, let textComponentView = self.textView.view, self.params?.canFocus == true { - let backgroundFrame = self.backgroundLayer.frame - let textFieldFrame = CGRect(origin: CGPoint(x: textComponentView.frame.minX, y: backgroundFrame.minY), size: CGSize(width: backgroundFrame.maxX - textComponentView.frame.minX, height: backgroundFrame.height)) + let location = recognizer.location(in: self) + if self.backIconView.frame.contains(location) { + if let suggestedItemsView = self.suggestedItemsView?.view as? EmojiSearchSearchBarComponent.View { + suggestedItemsView.clearSelection(dispatchEvent : true) + } + } else { + if self.textField == nil, let textComponentView = self.textView.view, self.params?.canFocus == true { + let backgroundFrame = self.backgroundLayer.frame + let textFieldFrame = CGRect(origin: CGPoint(x: textComponentView.frame.minX, y: backgroundFrame.minY), size: CGSize(width: backgroundFrame.maxX - textComponentView.frame.minX, height: backgroundFrame.height)) + + let textField = EmojiSearchTextField(frame: textFieldFrame) + textField.autocorrectionType = .no + self.textField = textField + self.insertSubview(textField, belowSubview: self.clearIconView) + textField.delegate = self + textField.addTarget(self, action: #selector(self.textFieldChanged(_:)), for: .editingChanged) + } - let textField = EmojiSearchTextField(frame: textFieldFrame) - textField.autocorrectionType = .no - self.textField = textField - self.insertSubview(textField, belowSubview: self.clearIconView) - textField.delegate = self - textField.addTarget(self, action: #selector(self.textFieldChanged(_:)), for: .editingChanged) + self.currentPresetSearchTerm = nil + if let suggestedItemsView = self.suggestedItemsView?.view as? EmojiSearchSearchBarComponent.View { + suggestedItemsView.clearSelection(dispatchEvent: false) + } + + self.activated() + + self.textField?.becomeFirstResponder() } - - self.activated() - - self.textField?.becomeFirstResponder() } } @objc private func cancelPressed() { + self.currentPresetSearchTerm = nil self.updateQuery("", "en") self.clearIconView.isHidden = true @@ -1720,6 +1753,7 @@ public final class EmojiSearchHeaderView: UIView, UITextFieldDelegate { } @objc private func clearPressed() { + self.currentPresetSearchTerm = nil self.updateQuery("", "en") self.textField?.text = "" @@ -1767,6 +1801,7 @@ public final class EmojiSearchHeaderView: UIView, UITextFieldDelegate { self.clearIconTintView.isHidden = text.isEmpty self.clearIconButton.isHidden = text.isEmpty + self.currentPresetSearchTerm = nil self.updateQuery(text, inputLanguage) } @@ -1775,28 +1810,37 @@ public final class EmojiSearchHeaderView: UIView, UITextFieldDelegate { return } self.params = nil - self.update(theme: params.theme, strings: params.strings, text: params.text, useOpaqueTheme: params.useOpaqueTheme, isActive: params.isActive, size: params.size, canFocus: params.canFocus, transition: transition) + self.update(theme: params.theme, strings: params.strings, text: params.text, useOpaqueTheme: params.useOpaqueTheme, isActive: params.isActive, size: params.size, canFocus: params.canFocus, hasSearchItems: params.hasSearchItems, transition: transition) } - public func update(theme: PresentationTheme, strings: PresentationStrings, text: String, useOpaqueTheme: Bool, isActive: Bool, size: CGSize, canFocus: Bool, transition: Transition) { + public func update(theme: PresentationTheme, strings: PresentationStrings, text: String, useOpaqueTheme: Bool, isActive: Bool, size: CGSize, canFocus: Bool, hasSearchItems: Bool, transition: Transition) { let params = Params( theme: theme, strings: strings, text: text, useOpaqueTheme: useOpaqueTheme, isActive: isActive, + hasPresetSearch: self.currentPresetSearchTerm == nil, size: size, - canFocus: canFocus + canFocus: canFocus, + hasSearchItems: hasSearchItems ) if self.params == params { return } + let isActiveWithText = isActive && self.currentPresetSearchTerm == nil + + let isLeftAligned = isActiveWithText || hasSearchItems + if self.params?.theme !== theme { self.searchIconView.image = generateTintedImage(image: UIImage(bundleImageName: "Components/Search Bar/Loupe"), color: theme.chat.inputMediaPanel.panelContentVibrantOverlayColor) self.searchIconTintView.image = generateTintedImage(image: UIImage(bundleImageName: "Components/Search Bar/Loupe"), color: .white) + self.backIconView.image = generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Back"), color: theme.chat.inputMediaPanel.panelContentVibrantOverlayColor) + self.backIconTintView.image = generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Back"), color: .white) + self.clearIconView.image = generateTintedImage(image: UIImage(bundleImageName: "Components/Search Bar/Clear"), color: theme.chat.inputMediaPanel.panelContentVibrantOverlayColor) self.clearIconTintView.image = generateTintedImage(image: UIImage(bundleImageName: "Components/Search Bar/Clear"), color: .white) } @@ -1864,7 +1908,7 @@ public final class EmojiSearchHeaderView: UIView, UITextFieldDelegate { let cancelButtonSpacing: CGFloat = 8.0 var backgroundFrame = CGRect(origin: CGPoint(x: sideInset, y: topInset), size: CGSize(width: size.width - sideInset * 2.0, height: inputHeight)) - if isActive { + if isActiveWithText { backgroundFrame.size.width -= cancelTextSize.width + cancelButtonSpacing } transition.setFrame(layer: self.backgroundLayer, frame: backgroundFrame) @@ -1873,14 +1917,96 @@ public final class EmojiSearchHeaderView: UIView, UITextFieldDelegate { transition.setFrame(view: self.cancelButton, frame: CGRect(origin: CGPoint(x: backgroundFrame.maxX, y: 0.0), size: CGSize(width: cancelButtonSpacing + cancelTextSize.width, height: size.height))) var textFrame = CGRect(origin: CGPoint(x: backgroundFrame.minX + floor((backgroundFrame.width - textSize.width) / 2.0), y: backgroundFrame.minY + floor((backgroundFrame.height - textSize.height) / 2.0)), size: textSize) - if isActive { + if isLeftAligned { textFrame.origin.x = backgroundFrame.minX + sideTextInset } if let image = self.searchIconView.image { let iconFrame = CGRect(origin: CGPoint(x: textFrame.minX - image.size.width - 4.0, y: backgroundFrame.minY + floor((backgroundFrame.height - image.size.height) / 2.0)), size: image.size) - transition.setFrame(view: self.searchIconView, frame: iconFrame) - transition.setFrame(view: self.searchIconTintView, frame: iconFrame) + transition.setBounds(view: self.searchIconView, bounds: CGRect(origin: CGPoint(), size: iconFrame.size)) + transition.setPosition(view: self.searchIconView, position: iconFrame.center) + transition.setBounds(view: self.searchIconTintView, bounds: CGRect(origin: CGPoint(), size: iconFrame.size)) + transition.setPosition(view: self.searchIconTintView, position: iconFrame.center) + transition.setScale(view: self.searchIconView, scale: self.currentPresetSearchTerm == nil ? 1.0 : 0.001) + transition.setAlpha(view: self.searchIconView, alpha: self.currentPresetSearchTerm == nil ? 1.0 : 0.0) + transition.setScale(view: self.searchIconTintView, scale: self.currentPresetSearchTerm == nil ? 1.0 : 0.001) + transition.setAlpha(view: self.searchIconTintView, alpha: self.currentPresetSearchTerm == nil ? 1.0 : 0.0) + } + + if let image = self.backIconView.image { + let iconFrame = CGRect(origin: CGPoint(x: textFrame.minX - image.size.width - 4.0, y: backgroundFrame.minY + floor((backgroundFrame.height - image.size.height) / 2.0)), size: image.size) + transition.setBounds(view: self.backIconView, bounds: CGRect(origin: CGPoint(), size: iconFrame.size)) + transition.setPosition(view: self.backIconView, position: iconFrame.center) + transition.setBounds(view: self.backIconTintView, bounds: CGRect(origin: CGPoint(), size: iconFrame.size)) + transition.setPosition(view: self.backIconTintView, position: iconFrame.center) + transition.setScale(view: self.backIconView, scale: self.currentPresetSearchTerm != nil ? 1.0 : 0.001) + transition.setAlpha(view: self.backIconView, alpha: self.currentPresetSearchTerm != nil ? 1.0 : 0.0) + transition.setScale(view: self.backIconTintView, scale: self.currentPresetSearchTerm != nil ? 1.0 : 0.001) + transition.setAlpha(view: self.backIconTintView, alpha: self.currentPresetSearchTerm != nil ? 1.0 : 0.0) + } + + if hasSearchItems { + let suggestedItemsView: ComponentView + var suggestedItemsTransition = transition + if let current = self.suggestedItemsView { + suggestedItemsView = current + } else { + suggestedItemsTransition = .immediate + suggestedItemsView = ComponentView() + self.suggestedItemsView = suggestedItemsView + } + + let itemsX: CGFloat = textFrame.maxX + 8.0 + let suggestedItemsFrame = CGRect(origin: CGPoint(x: itemsX, y: backgroundFrame.minY), size: CGSize(width: backgroundFrame.maxX - itemsX, height: backgroundFrame.height)) + let _ = suggestedItemsView.update( + transition: suggestedItemsTransition, + component: AnyComponent(EmojiSearchSearchBarComponent( + theme: theme, + strings: strings, + searchTermUpdated: { [weak self] term in + guard let self else { + return + } + var shouldChangeActivation = false + if (self.currentPresetSearchTerm == nil) != (term == nil) { + shouldChangeActivation = true + } + self.currentPresetSearchTerm = term + + if shouldChangeActivation { + self.update(transition: Transition(animation: .curve(duration: 0.4, curve: .spring))) + + if term == nil { + self.deactivated(self.textField?.isFirstResponder ?? false) + self.updateQuery(term ?? "", "en") + } else { + self.updateQuery(term ?? "", "en") + self.activated() + } + } else { + self.updateQuery(term ?? "", "en") + } + } + )), + environment: {}, + containerSize: suggestedItemsFrame.size + ) + if let suggestedItemsComponentView = suggestedItemsView.view { + if suggestedItemsComponentView.superview == nil { + self.addSubview(suggestedItemsComponentView) + } + suggestedItemsTransition.setFrame(view: suggestedItemsComponentView, frame: suggestedItemsFrame) + suggestedItemsTransition.setAlpha(view: suggestedItemsComponentView, alpha: isActiveWithText ? 0.0 : 1.0) + } + } else { + if let suggestedItemsView = self.suggestedItemsView { + self.suggestedItemsView = nil + if let suggestedItemsComponentView = suggestedItemsView.view { + transition.setAlpha(view: suggestedItemsComponentView, alpha: 0.0, completion: { [weak suggestedItemsComponentView] _ in + suggestedItemsComponentView?.removeFromSuperview() + }) + } + } } if let image = self.clearIconView.image { @@ -2389,7 +2515,8 @@ public final class EmojiPagerContentComponent: Component { public let animationCache: AnimationCache public let animationRenderer: MultiAnimationRenderer public let inputInteractionHolder: InputInteractionHolder - public let itemGroups: [ItemGroup] + public let panelItemGroups: [ItemGroup] + public let contentItemGroups: [ItemGroup] public let itemLayoutType: ItemLayoutType public let itemContentUniqueId: AnyHashable? public let warpContentsOnEdges: Bool @@ -2407,7 +2534,8 @@ public final class EmojiPagerContentComponent: Component { animationCache: AnimationCache, animationRenderer: MultiAnimationRenderer, inputInteractionHolder: InputInteractionHolder, - itemGroups: [ItemGroup], + panelItemGroups: [ItemGroup], + contentItemGroups: [ItemGroup], itemLayoutType: ItemLayoutType, itemContentUniqueId: AnyHashable?, warpContentsOnEdges: Bool, @@ -2424,7 +2552,8 @@ public final class EmojiPagerContentComponent: Component { self.animationCache = animationCache self.animationRenderer = animationRenderer self.inputInteractionHolder = inputInteractionHolder - self.itemGroups = itemGroups + self.panelItemGroups = panelItemGroups + self.contentItemGroups = contentItemGroups self.itemLayoutType = itemLayoutType self.itemContentUniqueId = itemContentUniqueId self.warpContentsOnEdges = warpContentsOnEdges @@ -2436,7 +2565,7 @@ public final class EmojiPagerContentComponent: Component { self.selectedItems = selectedItems } - public func withUpdatedItemGroups(itemGroups: [ItemGroup], itemContentUniqueId: AnyHashable?, emptySearchResults: EmptySearchResults?) -> EmojiPagerContentComponent { + public func withUpdatedItemGroups(panelItemGroups: [ItemGroup], contentItemGroups: [ItemGroup], itemContentUniqueId: AnyHashable?, emptySearchResults: EmptySearchResults?) -> EmojiPagerContentComponent { return EmojiPagerContentComponent( id: self.id, context: self.context, @@ -2444,7 +2573,8 @@ public final class EmojiPagerContentComponent: Component { animationCache: self.animationCache, animationRenderer: self.animationRenderer, inputInteractionHolder: self.inputInteractionHolder, - itemGroups: itemGroups, + panelItemGroups: panelItemGroups, + contentItemGroups: contentItemGroups, itemLayoutType: self.itemLayoutType, itemContentUniqueId: itemContentUniqueId, warpContentsOnEdges: self.warpContentsOnEdges, @@ -2479,7 +2609,10 @@ public final class EmojiPagerContentComponent: Component { if lhs.inputInteractionHolder !== rhs.inputInteractionHolder { return false } - if lhs.itemGroups != rhs.itemGroups { + if lhs.panelItemGroups != rhs.panelItemGroups { + return false + } + if lhs.contentItemGroups != rhs.contentItemGroups { return false } if lhs.itemLayoutType != rhs.itemLayoutType { @@ -4148,7 +4281,7 @@ public final class EmojiPagerContentComponent: Component { var subgroupItemIndex: Int? if group.supergroupId == supergroupId { if let subgroupId = subgroupId { - inner: for itemGroup in component.itemGroups { + inner: for itemGroup in component.contentItemGroups { if itemGroup.supergroupId == supergroupId { for i in 0 ..< itemGroup.items.count { if itemGroup.items[i].subgroupId == subgroupId { @@ -4858,7 +4991,7 @@ public final class EmojiPagerContentComponent: Component { self.updateScrollingOffset(isReset: false, transition: .immediate) - if self.isSearchActivated { + if self.isSearchActivated, let visibleSearchHeader = self.visibleSearchHeader, visibleSearchHeader.currentPresetSearchTerm == nil { self.visibleSearchHeader?.deactivate() } } @@ -4884,9 +5017,9 @@ public final class EmojiPagerContentComponent: Component { return } - let isInteracting = scrollView.isDragging || scrollView.isDecelerating - if let previousScrollingOffsetValue = self.previousScrollingOffset, !self.keepTopPanelVisibleUntilScrollingInput { - let currentBounds = scrollView.bounds + let isInteracting = self.scrollView.isDragging || self.scrollView.isDecelerating + if let previousScrollingOffsetValue = self.previousScrollingOffset, !self.keepTopPanelVisibleUntilScrollingInput, !self.isSearchActivated { + let currentBounds = self.scrollView.bounds let offsetToTopEdge = max(0.0, currentBounds.minY - 0.0) let offsetToBottomEdge = max(0.0, scrollView.contentSize.height - currentBounds.maxY) @@ -4968,7 +5101,7 @@ public final class EmojiPagerContentComponent: Component { } for groupItems in itemLayout.visibleItems(for: effectiveVisibleBounds) { - let itemGroup = component.itemGroups[groupItems.groupIndex] + let itemGroup = component.contentItemGroups[groupItems.groupIndex] let itemGroupLayout = itemLayout.itemGroupLayouts[groupItems.groupIndex] var assignTopVisibleSubgroupId = false @@ -5875,11 +6008,11 @@ public final class EmojiPagerContentComponent: Component { var previousAbsoluteItemPositions: [VisualItemKey: CGPoint] = [:] var anchorItems: [ItemLayer.Key: CGRect] = [:] - if let previousComponent = previousComponent, let previousItemLayout = self.itemLayout, previousComponent.itemGroups != component.itemGroups { + if let previousComponent = previousComponent, let previousItemLayout = self.itemLayout, previousComponent.contentItemGroups != component.contentItemGroups, previousComponent.itemContentUniqueId == component.itemContentUniqueId { if !transition.animation.isImmediate { var previousItemPositionsValue: [VisualItemKey: CGPoint] = [:] - for groupIndex in 0 ..< previousComponent.itemGroups.count { - let itemGroup = previousComponent.itemGroups[groupIndex] + for groupIndex in 0 ..< previousComponent.contentItemGroups.count { + let itemGroup = previousComponent.contentItemGroups[groupIndex] for itemIndex in 0 ..< itemGroup.items.count { let item = itemGroup.items[itemIndex] let itemKey: ItemLayer.Key @@ -5976,7 +6109,7 @@ public final class EmojiPagerContentComponent: Component { } var itemGroups: [ItemGroupDescription] = [] - for itemGroup in component.itemGroups { + for itemGroup in component.contentItemGroups { itemGroups.append(ItemGroupDescription( supergroupId: itemGroup.supergroupId, groupId: itemGroup.groupId, @@ -6021,15 +6154,14 @@ public final class EmojiPagerContentComponent: Component { let scrollOriginY: CGFloat = 0.0 - let scrollSize = CGSize(width: availableSize.width, height: availableSize.height) transition.setPosition(view: self.scrollView, position: CGPoint(x: 0.0, y: scrollOriginY)) - transition.setFrame(view: self.scrollViewClippingView, frame: CGRect(origin: CGPoint(x: 0.0, y: self.isSearchActivated ? itemLayout.searchHeight : 0.0), size: availableSize)) - transition.setBounds(view: self.scrollViewClippingView, bounds: CGRect(origin: CGPoint(x: 0.0, y: self.isSearchActivated ? itemLayout.searchHeight : 0.0), size: availableSize)) + transition.setFrame(view: self.scrollViewClippingView, frame: CGRect(origin: CGPoint(x: 0.0, y: self.isSearchActivated ? itemLayout.itemInsets.top : 0.0), size: availableSize)) + transition.setBounds(view: self.scrollViewClippingView, bounds: CGRect(origin: CGPoint(x: 0.0, y: self.isSearchActivated ? itemLayout.itemInsets.top : 0.0), size: availableSize)) - transition.setFrame(view: self.vibrancyClippingView, frame: CGRect(origin: CGPoint(x: 0.0, y: self.isSearchActivated ? itemLayout.searchHeight : 0.0), size: availableSize)) - transition.setBounds(view: self.vibrancyClippingView, bounds: CGRect(origin: CGPoint(x: 0.0, y: self.isSearchActivated ? itemLayout.searchHeight : 0.0), size: availableSize)) + transition.setFrame(view: self.vibrancyClippingView, frame: CGRect(origin: CGPoint(x: 0.0, y: self.isSearchActivated ? itemLayout.itemInsets.top : 0.0), size: availableSize)) + transition.setBounds(view: self.vibrancyClippingView, bounds: CGRect(origin: CGPoint(x: 0.0, y: self.isSearchActivated ? itemLayout.itemInsets.top : 0.0), size: availableSize)) let previousSize = self.scrollView.bounds.size var resetScrolling = false @@ -6041,6 +6173,10 @@ public final class EmojiPagerContentComponent: Component { } self.scrollView.bounds = CGRect(origin: self.scrollView.bounds.origin, size: scrollSize) + if resetScrolling { + itemTransition = .immediate + } + let warpHeight: CGFloat = 50.0 var topWarpInset = pagerEnvironment.containerInsets.top if self.isSearchActivated { @@ -6100,16 +6236,16 @@ public final class EmojiPagerContentComponent: Component { } }) - outer: for i in 0 ..< component.itemGroups.count { + outer: for i in 0 ..< component.contentItemGroups.count { for anchorItem in sortedAnchorItems { - if component.itemGroups[i].groupId != anchorItem.0.groupId { + if component.contentItemGroups[i].groupId != anchorItem.0.groupId { continue } - for j in 0 ..< component.itemGroups[i].items.count { + for j in 0 ..< component.contentItemGroups[i].items.count { let itemKey: ItemLayer.Key itemKey = ItemLayer.Key( - groupId: component.itemGroups[i].groupId, - itemId: component.itemGroups[i].items[j].content.id + groupId: component.contentItemGroups[i].groupId, + itemId: component.contentItemGroups[i].items[j].content.id ) if itemKey == anchorItem.0 { @@ -6137,19 +6273,15 @@ public final class EmojiPagerContentComponent: Component { } if resetScrolling { - if component.displaySearchWithPlaceholder != nil && !self.isSearchActivated && component.searchInitiallyHidden { - self.scrollView.bounds = CGRect(origin: CGPoint(x: 0.0, y: 50.0), size: scrollSize) - } else { - self.scrollView.bounds = CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: scrollSize) - } + self.scrollView.bounds = CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: scrollSize) } self.ignoreScrolling = false if calculateUpdatedItemPositions { var updatedItemPositionsValue: [VisualItemKey: CGPoint] = [:] - for groupIndex in 0 ..< component.itemGroups.count { - let itemGroup = component.itemGroups[groupIndex] + for groupIndex in 0 ..< component.contentItemGroups.count { + let itemGroup = component.contentItemGroups[groupIndex] let itemGroupLayout = itemLayout.itemGroupLayouts[groupIndex] for itemIndex in 0 ..< itemGroup.items.count { let item = itemGroup.items[itemIndex] @@ -6204,14 +6336,16 @@ public final class EmojiPagerContentComponent: Component { } } } else { - /*if visibleSearchHeader.superview != self.scrollView { - self.scrollView.addSubview(visibleSearchHeader) - self.mirrorContentScrollView.addSubview(visibleSearchHeader.tintContainerView) + /*if component.inputInteractionHolder.inputInteraction?.externalBackground == nil { + if visibleSearchHeader.superview != self.scrollView { + self.scrollView.addSubview(visibleSearchHeader) + self.mirrorContentScrollView.addSubview(visibleSearchHeader.tintContainerView) + } }*/ } } else { visibleSearchHeader = EmojiSearchHeaderView(activated: { [weak self] in - guard let strongSelf = self else { + guard let strongSelf = self, let visibleSearchHeader = strongSelf.visibleSearchHeader else { return } @@ -6219,7 +6353,9 @@ public final class EmojiPagerContentComponent: Component { component.inputInteractionHolder.inputInteraction?.openSearch() } else { strongSelf.isSearchActivated = true - strongSelf.pagerEnvironment?.onWantsExclusiveModeUpdated(true) + if visibleSearchHeader.currentPresetSearchTerm == nil { + strongSelf.pagerEnvironment?.onWantsExclusiveModeUpdated(true) + } strongSelf.component?.inputInteractionHolder.inputInteraction?.requestUpdate(.immediate) } }, deactivated: { [weak self] isFirstResponder in @@ -6255,8 +6391,8 @@ public final class EmojiPagerContentComponent: Component { } let searchHeaderFrame = CGRect(origin: CGPoint(x: itemLayout.searchInsets.left, y: itemLayout.searchInsets.top), size: CGSize(width: itemLayout.width - itemLayout.searchInsets.left - itemLayout.searchInsets.right, height: itemLayout.searchHeight)) - visibleSearchHeader.update(theme: keyboardChildEnvironment.theme, strings: keyboardChildEnvironment.strings, text: displaySearchWithPlaceholder, useOpaqueTheme: useOpaqueTheme, isActive: self.isSearchActivated, size: searchHeaderFrame.size, canFocus: !component.searchIsPlaceholderOnly, transition: transition) - transition.setFrame(view: visibleSearchHeader, frame: searchHeaderFrame, completion: { [weak self] completed in + visibleSearchHeader.update(theme: keyboardChildEnvironment.theme, strings: keyboardChildEnvironment.strings, text: displaySearchWithPlaceholder, useOpaqueTheme: useOpaqueTheme, isActive: self.isSearchActivated, size: searchHeaderFrame.size, canFocus: !component.searchIsPlaceholderOnly, hasSearchItems: component.displaySearchWithPlaceholder == keyboardChildEnvironment.strings.EmojiSearch_SearchEmojiPlaceholder, transition: transition) + transition.attachAnimation(view: visibleSearchHeader, completion: { [weak self] completed in guard let strongSelf = self, completed, let visibleSearchHeader = strongSelf.visibleSearchHeader else { return } @@ -6266,6 +6402,7 @@ public final class EmojiPagerContentComponent: Component { strongSelf.mirrorContentScrollView.addSubview(visibleSearchHeader.tintContainerView) } }) + transition.setFrame(view: visibleSearchHeader, frame: searchHeaderFrame) } else { if let visibleSearchHeader = self.visibleSearchHeader { self.visibleSearchHeader = nil @@ -6307,8 +6444,30 @@ public final class EmojiPagerContentComponent: Component { } } + var animateContentCrossfade = false + if let previousComponent, previousComponent.itemContentUniqueId != component.itemContentUniqueId, itemTransition.animation.isImmediate, !transition.animation.isImmediate { + animateContentCrossfade = true + } + + if animateContentCrossfade { + for (_, itemLayer) in self.visibleItemLayers { + if let snapshotLayer = itemLayer.snapshotContentTree() { + itemLayer.superlayer?.insertSublayer(snapshotLayer, above: itemLayer) + snapshotLayer.animateAlpha(from: CGFloat(snapshotLayer.opacity), to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { [weak snapshotLayer] _ in + snapshotLayer?.removeFromSuperlayer() + }) + } + } + } + self.updateVisibleItems(transition: itemTransition, attemptSynchronousLoads: attemptSynchronousLoads, previousItemPositions: previousItemPositions, previousAbsoluteItemPositions: previousAbsoluteItemPositions, updatedItemPositions: updatedItemPositions, hintDisappearingGroupFrame: hintDisappearingGroupFrame) + if animateContentCrossfade { + for (_, itemLayer) in self.visibleItemLayers { + itemLayer.animateAlpha(from: 0.0, to: CGFloat(itemLayer.opacity), duration: 0.2) + } + } + return availableSize } } @@ -7127,6 +7286,41 @@ public final class EmojiPagerContentComponent: Component { } } + let allItemGroups = itemGroups.map { group -> EmojiPagerContentComponent.ItemGroup in + var headerItem = group.headerItem + + if let groupId = group.id.base as? ItemCollectionId { + outer: for (id, info, _) in view.collectionInfos { + if id == groupId, let info = info as? StickerPackCollectionInfo { + if let thumbnailFileId = info.thumbnailFileId { + for item in group.items { + if let itemFile = item.itemFile, itemFile.fileId.id == thumbnailFileId { + headerItem = EntityKeyboardAnimationData(file: itemFile) + break outer + } + } + } + } + } + } + + return EmojiPagerContentComponent.ItemGroup( + supergroupId: group.supergroupId, + groupId: group.id, + title: group.title, + subtitle: group.subtitle, + actionButtonTitle: nil, + isFeatured: group.isFeatured, + isPremiumLocked: group.isPremiumLocked, + isEmbedded: false, + hasClear: group.isClearable, + collapsedLineCount: group.collapsedLineCount, + displayPremiumBadges: false, + headerItem: headerItem, + items: group.items + ) + } + return EmojiPagerContentComponent( id: "emoji", context: context, @@ -7134,40 +7328,8 @@ public final class EmojiPagerContentComponent: Component { animationCache: animationCache, animationRenderer: animationRenderer, inputInteractionHolder: EmojiPagerContentComponent.InputInteractionHolder(), - itemGroups: itemGroups.map { group -> EmojiPagerContentComponent.ItemGroup in - var headerItem = group.headerItem - - if let groupId = group.id.base as? ItemCollectionId { - outer: for (id, info, _) in view.collectionInfos { - if id == groupId, let info = info as? StickerPackCollectionInfo { - if let thumbnailFileId = info.thumbnailFileId { - for item in group.items { - if let itemFile = item.itemFile, itemFile.fileId.id == thumbnailFileId { - headerItem = EntityKeyboardAnimationData(file: itemFile) - break outer - } - } - } - } - } - } - - return EmojiPagerContentComponent.ItemGroup( - supergroupId: group.supergroupId, - groupId: group.id, - title: group.title, - subtitle: group.subtitle, - actionButtonTitle: nil, - isFeatured: group.isFeatured, - isPremiumLocked: group.isPremiumLocked, - isEmbedded: false, - hasClear: group.isClearable, - collapsedLineCount: group.collapsedLineCount, - displayPremiumBadges: false, - headerItem: headerItem, - items: group.items - ) - }, + panelItemGroups: allItemGroups, + contentItemGroups: allItemGroups, itemLayoutType: .compact, itemContentUniqueId: nil, warpContentsOnEdges: isReactionSelection || isStatusSelection, @@ -7644,6 +7806,33 @@ public final class EmojiPagerContentComponent: Component { let isMasks = stickerNamespaces.contains(Namespaces.ItemCollection.CloudMaskPacks) + let allItemGroups = itemGroups.map { group -> EmojiPagerContentComponent.ItemGroup in + var hasClear = false + var isEmbedded = false + if group.id == AnyHashable("recent") { + hasClear = true + } else if group.id == AnyHashable("featuredTop") { + hasClear = true + isEmbedded = true + } + + return EmojiPagerContentComponent.ItemGroup( + supergroupId: group.supergroupId, + groupId: group.id, + title: group.title, + subtitle: group.subtitle, + actionButtonTitle: group.actionButtonTitle, + isFeatured: group.isFeatured, + isPremiumLocked: group.isPremiumLocked, + isEmbedded: isEmbedded, + hasClear: hasClear, + collapsedLineCount: nil, + displayPremiumBadges: group.displayPremiumBadges, + headerItem: group.headerItem, + items: group.items + ) + } + return EmojiPagerContentComponent( id: isMasks ? "masks" : "stickers", context: context, @@ -7651,32 +7840,8 @@ public final class EmojiPagerContentComponent: Component { animationCache: animationCache, animationRenderer: animationRenderer, inputInteractionHolder: EmojiPagerContentComponent.InputInteractionHolder(), - itemGroups: itemGroups.map { group -> EmojiPagerContentComponent.ItemGroup in - var hasClear = false - var isEmbedded = false - if group.id == AnyHashable("recent") { - hasClear = true - } else if group.id == AnyHashable("featuredTop") { - hasClear = true - isEmbedded = true - } - - return EmojiPagerContentComponent.ItemGroup( - supergroupId: group.supergroupId, - groupId: group.id, - title: group.title, - subtitle: group.subtitle, - actionButtonTitle: group.actionButtonTitle, - isFeatured: group.isFeatured, - isPremiumLocked: group.isPremiumLocked, - isEmbedded: isEmbedded, - hasClear: hasClear, - collapsedLineCount: nil, - displayPremiumBadges: group.displayPremiumBadges, - headerItem: group.headerItem, - items: group.items - ) - }, + panelItemGroups: allItemGroups, + contentItemGroups: allItemGroups, itemLayoutType: .detailed, itemContentUniqueId: nil, warpContentsOnEdges: false, diff --git a/submodules/TelegramUI/Components/EntityKeyboard/Sources/EmojiSearchSearchBarComponent.swift b/submodules/TelegramUI/Components/EntityKeyboard/Sources/EmojiSearchSearchBarComponent.swift new file mode 100644 index 0000000000..ca359ddf2a --- /dev/null +++ b/submodules/TelegramUI/Components/EntityKeyboard/Sources/EmojiSearchSearchBarComponent.swift @@ -0,0 +1,328 @@ +import Foundation +import UIKit +import Display +import ComponentFlow +import PagerComponent +import TelegramPresentationData +import TelegramCore +import Postbox +import AnimationCache +import MultiAnimationRenderer +import AccountContext +import AsyncDisplayKit +import ComponentDisplayAdapters +import LottieAnimationComponent + +final class EmojiSearchSearchBarComponent: Component { + let theme: PresentationTheme + let strings: PresentationStrings + let searchTermUpdated: (String?) -> Void + + init( + theme: PresentationTheme, + strings: PresentationStrings, + searchTermUpdated: @escaping (String?) -> Void + ) { + self.theme = theme + self.strings = strings + self.searchTermUpdated = searchTermUpdated + } + + static func ==(lhs: EmojiSearchSearchBarComponent, rhs: EmojiSearchSearchBarComponent) -> Bool { + if lhs.theme !== rhs.theme { + return false + } + if lhs.strings !== rhs.strings { + return false + } + return true + } + + private struct ItemLayout { + let containerSize: CGSize + let itemCount: Int + let itemSize: CGSize + let itemSpacing: CGFloat + let contentSize: CGSize + let sideInset: CGFloat + + init(containerSize: CGSize, itemCount: Int) { + self.containerSize = containerSize + self.itemCount = itemCount + self.itemSpacing = 8.0 + self.sideInset = 8.0 + self.itemSize = CGSize(width: 24.0, height: 24.0) + + self.contentSize = CGSize(width: self.sideInset * 2.0 + self.itemSize.width * CGFloat(self.itemCount) + self.itemSpacing * CGFloat(max(0, self.itemCount - 1)), height: containerSize.height) + } + + func visibleItems(for rect: CGRect) -> Range? { + let offsetRect = rect.offsetBy(dx: -self.sideInset, dy: 0.0) + var minVisibleIndex = Int(floor((offsetRect.minX - self.itemSpacing) / (self.itemSize.width + self.itemSpacing))) + minVisibleIndex = max(0, minVisibleIndex) + var maxVisibleIndex = Int(ceil((offsetRect.maxX - self.itemSpacing) / (self.itemSize.height + self.itemSpacing))) + maxVisibleIndex = min(maxVisibleIndex, self.itemCount - 1) + + if minVisibleIndex <= maxVisibleIndex { + return minVisibleIndex ..< (maxVisibleIndex + 1) + } else { + return nil + } + } + + func frame(at index: Int) -> CGRect { + return CGRect(origin: CGPoint(x: self.sideInset + CGFloat(index) * (self.itemSize.width + self.itemSpacing), y: floor((self.containerSize.height - self.itemSize.height) * 0.5)), size: self.itemSize) + } + } + + final class View: UIView, UIScrollViewDelegate { + private let scrollView: UIScrollView + + private var visibleItemViews: [AnyHashable: ComponentView] = [:] + private let selectedItemBackground: SimpleLayer + + private var items: [String] = [] + + private var component: EmojiSearchSearchBarComponent? + private weak var state: EmptyComponentState? + + private var itemLayout: ItemLayout? + private var ignoreScrolling: Bool = false + + private let maskLayer: SimpleLayer + + private var selectedItem: String? + + override init(frame: CGRect) { + self.scrollView = UIScrollView() + self.maskLayer = SimpleLayer() + + self.selectedItemBackground = SimpleLayer() + + super.init(frame: frame) + + self.scrollView.delaysContentTouches = false + if #available(iOSApplicationExtension 11.0, iOS 11.0, *) { + self.scrollView.contentInsetAdjustmentBehavior = .never + } + if #available(iOS 13.0, *) { + self.scrollView.automaticallyAdjustsScrollIndicatorInsets = false + } + self.scrollView.showsVerticalScrollIndicator = true + self.scrollView.showsHorizontalScrollIndicator = false + self.scrollView.delegate = self + self.scrollView.clipsToBounds = false + self.scrollView.scrollsToTop = false + + self.addSubview(self.scrollView) + + //self.layer.mask = self.maskLayer + self.layer.addSublayer(self.maskLayer) + self.layer.masksToBounds = true + + self.scrollView.layer.addSublayer(self.selectedItemBackground) + + self.scrollView.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.tapGesture(_:)))) + + self.items = ["Smile", "🤔", "😝", "😡", "😐", "🏌️‍♀️", "🎉", "😨", "❤️", "😄", "👍", "☹️", "👎", "⛔", "💤", "💼", "🍔", "🏠", "🛁", "🏖", "⚽️", "🕔"] + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + @objc private func tapGesture(_ recognizer: UITapGestureRecognizer) { + if case .ended = recognizer.state { + let location = recognizer.location(in: self.scrollView) + for (id, itemView) in self.visibleItemViews { + if let itemComponentView = itemView.view, itemComponentView.frame.contains(location), let item = id.base as? String { + if self.selectedItem == item { + self.selectedItem = nil + } else { + self.selectedItem = item + } + self.state?.updated(transition: .immediate) + self.component?.searchTermUpdated(self.selectedItem) + + break + } + } + } + } + + func clearSelection(dispatchEvent: Bool) { + if self.selectedItem != nil { + self.selectedItem = nil + self.state?.updated(transition: .immediate) + if dispatchEvent { + self.component?.searchTermUpdated(self.selectedItem) + } + } + } + + func scrollViewDidScroll(_ scrollView: UIScrollView) { + if !self.ignoreScrolling { + self.updateScrolling(transition: .immediate, fromScrolling: true) + } + } + + private func updateScrolling(transition: Transition, fromScrolling: Bool) { + guard let component = self.component, let itemLayout = self.itemLayout else { + return + } + + var validItemIds = Set() + let visibleBounds = self.scrollView.bounds + + var animateAppearingItems = false + if fromScrolling { + animateAppearingItems = true + } + + let items = self.items + + for i in 0 ..< items.count { + let itemFrame = itemLayout.frame(at: i) + if visibleBounds.intersects(itemFrame) { + let item = items[i] + validItemIds.insert(AnyHashable(item)) + + var animateItem = false + var itemTransition = transition + let itemView: ComponentView + if let current = self.visibleItemViews[item] { + itemView = current + } else { + animateItem = animateAppearingItems + itemTransition = .immediate + itemView = ComponentView() + self.visibleItemViews[item] = itemView + } + + let animationName: String + + switch EmojiPagerContentComponent.StaticEmojiSegment.allCases[i % EmojiPagerContentComponent.StaticEmojiSegment.allCases.count] { + case .people: + animationName = "emojicat_smiles" + case .animalsAndNature: + animationName = "emojicat_animals" + case .foodAndDrink: + animationName = "emojicat_food" + case .activityAndSport: + animationName = "emojicat_activity" + case .travelAndPlaces: + animationName = "emojicat_places" + case .objects: + animationName = "emojicat_objects" + case .symbols: + animationName = "emojicat_symbols" + case .flags: + animationName = "emojicat_flags" + } + + let baseColor: UIColor + baseColor = component.theme.chat.inputMediaPanel.panelIconColor + + let baseHighlightedColor = component.theme.chat.inputMediaPanel.panelHighlightedIconBackgroundColor.blitOver(component.theme.chat.inputPanel.panelBackgroundColor, alpha: 1.0) + let color = baseColor.blitOver(baseHighlightedColor, alpha: 1.0) + + let _ = itemTransition + let _ = itemView.update( + transition: .immediate, + component: AnyComponent(LottieAnimationComponent( + animation: LottieAnimationComponent.AnimationItem( + name: animationName, + mode: .still(position: .end) + ), + colors: ["__allcolors__": color], + size: itemLayout.itemSize + )), + environment: {}, + containerSize: itemLayout.itemSize + ) + if let view = itemView.view { + if view.superview == nil { + self.scrollView.addSubview(view) + } + + itemTransition.setPosition(view: view, position: CGPoint(x: itemFrame.midX, y: itemFrame.midY)) + itemTransition.setBounds(view: view, bounds: CGRect(origin: CGPoint(), size: CGSize(width: itemLayout.itemSize.width, height: itemLayout.itemSize.height))) + let scaleFactor = itemFrame.width / itemLayout.itemSize.width + itemTransition.setSublayerTransform(view: view, transform: CATransform3DMakeScale(scaleFactor, scaleFactor, 1.0)) + + let isHidden = !visibleBounds.intersects(itemFrame) + if isHidden != view.isHidden { + view.isHidden = isHidden + + if !isHidden { + if let view = view as? LottieAnimationComponent.View { + view.playOnce() + } + } + } else if animateItem { + if let view = view as? LottieAnimationComponent.View { + view.playOnce() + } + } + } + } + } + + var removedItemIds: [AnyHashable] = [] + for (id, itemView) in self.visibleItemViews { + if !validItemIds.contains(id) { + removedItemIds.append(id) + itemView.view?.removeFromSuperview() + } + } + for id in removedItemIds { + self.visibleItemViews.removeValue(forKey: id) + } + + if let selectedItem = self.selectedItem, let index = self.items.firstIndex(of: selectedItem) { + self.selectedItemBackground.isHidden = false + + let selectedItemCenter = itemLayout.frame(at: index).center + let selectionSize = CGSize(width: 28.0, height: 28.0) + + self.selectedItemBackground.backgroundColor = component.theme.chat.inputMediaPanel.panelContentControlOpaqueSelectionColor.cgColor + self.selectedItemBackground.cornerRadius = selectionSize.height * 0.5 + + self.selectedItemBackground.frame = CGRect(origin: CGPoint(x: floor(selectedItemCenter.x - selectionSize.width * 0.5), y: floor(selectedItemCenter.y - selectionSize.height * 0.5)), size: selectionSize) + } else { + self.selectedItemBackground.isHidden = true + } + } + + func update(component: EmojiSearchSearchBarComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + self.component = component + self.state = state + + transition.setCornerRadius(layer: self.layer, cornerRadius: availableSize.height * 0.5) + + let itemLayout = ItemLayout(containerSize: availableSize, itemCount: self.items.count) + self.itemLayout = itemLayout + + self.ignoreScrolling = true + if self.scrollView.bounds.size != availableSize { + transition.setFrame(view: self.scrollView, frame: CGRect(origin: CGPoint(), size: availableSize)) + } + if self.scrollView.contentSize != itemLayout.contentSize { + self.scrollView.contentSize = itemLayout.contentSize + } + self.ignoreScrolling = false + + self.updateScrolling(transition: transition, fromScrolling: false) + + return availableSize + } + } + + func makeView() -> View { + return View(frame: CGRect()) + } + + 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) + } +} diff --git a/submodules/TelegramUI/Components/EntityKeyboard/Sources/EntityKeyboard.swift b/submodules/TelegramUI/Components/EntityKeyboard/Sources/EntityKeyboard.swift index 1b9356f34a..3a6986eddb 100644 --- a/submodules/TelegramUI/Components/EntityKeyboard/Sources/EntityKeyboard.swift +++ b/submodules/TelegramUI/Components/EntityKeyboard/Sources/EntityKeyboard.swift @@ -302,7 +302,7 @@ public final class EntityKeyboardComponent: Component { if let maskContent = component.maskContent { var topMaskItems: [EntityKeyboardTopPanelComponent.Item] = [] - for itemGroup in maskContent.itemGroups { + for itemGroup in maskContent.panelItemGroups { if let id = itemGroup.supergroupId.base as? String { let iconMapping: [String: EntityKeyboardIconTopPanelComponent.Icon] = [ "saved": .saved, @@ -359,7 +359,7 @@ public final class EntityKeyboardComponent: Component { theme: component.theme, items: topMaskItems, containerSideInset: component.containerInsets.left + component.topPanelInsets.left, - defaultActiveItemId: maskContent.itemGroups.first?.groupId, + defaultActiveItemId: maskContent.panelItemGroups.first?.groupId, activeContentItemIdUpdated: masksContentItemIdUpdated, reorderItems: { [weak self] items in guard let strongSelf = self else { @@ -476,7 +476,7 @@ public final class EntityKeyboardComponent: Component { )) } - for itemGroup in stickerContent.itemGroups { + for itemGroup in stickerContent.panelItemGroups { if let id = itemGroup.supergroupId.base as? String { if id == "peerSpecific" { if let avatarPeer = stickerContent.avatarPeer { @@ -551,7 +551,7 @@ public final class EntityKeyboardComponent: Component { theme: component.theme, items: topStickerItems, containerSideInset: component.containerInsets.left + component.topPanelInsets.left, - defaultActiveItemId: stickerContent.itemGroups.first?.groupId, + defaultActiveItemId: stickerContent.panelItemGroups.first?.groupId, activeContentItemIdUpdated: stickersContentItemIdUpdated, reorderItems: { [weak self] items in guard let strongSelf = self else { @@ -581,7 +581,7 @@ public final class EntityKeyboardComponent: Component { if let emojiContent = component.emojiContent { contents.append(AnyComponentWithIdentity(id: "emoji", component: AnyComponent(emojiContent))) var topEmojiItems: [EntityKeyboardTopPanelComponent.Item] = [] - for itemGroup in emojiContent.itemGroups { + for itemGroup in emojiContent.panelItemGroups { if !itemGroup.items.isEmpty { if let id = itemGroup.groupId.base as? String { if id == "recent" { diff --git a/submodules/TelegramUI/Components/EntityKeyboard/Sources/GifPagerContentComponent.swift b/submodules/TelegramUI/Components/EntityKeyboard/Sources/GifPagerContentComponent.swift index 84da5e9505..651b556585 100644 --- a/submodules/TelegramUI/Components/EntityKeyboard/Sources/GifPagerContentComponent.swift +++ b/submodules/TelegramUI/Components/EntityKeyboard/Sources/GifPagerContentComponent.swift @@ -979,7 +979,7 @@ public final class GifPagerContentComponent: Component { } let searchHeaderFrame = CGRect(origin: CGPoint(x: itemLayout.searchInsets.left, y: itemLayout.searchInsets.top), size: CGSize(width: itemLayout.width - itemLayout.searchInsets.left - itemLayout.searchInsets.right, height: itemLayout.searchHeight)) - visibleSearchHeader.update(theme: keyboardChildEnvironment.theme, strings: keyboardChildEnvironment.strings, text: displaySearchWithPlaceholder, useOpaqueTheme: false, isActive: false, size: searchHeaderFrame.size, canFocus: false, transition: transition) + visibleSearchHeader.update(theme: keyboardChildEnvironment.theme, strings: keyboardChildEnvironment.strings, text: displaySearchWithPlaceholder, useOpaqueTheme: false, isActive: false, size: searchHeaderFrame.size, canFocus: false, hasSearchItems: true, transition: transition) transition.setFrame(view: visibleSearchHeader, frame: searchHeaderFrame, completion: { [weak self] completed in guard let strongSelf = self, completed, let visibleSearchHeader = strongSelf.visibleSearchHeader else { return diff --git a/submodules/TelegramUI/Sources/AppDelegate.swift b/submodules/TelegramUI/Sources/AppDelegate.swift index 3d13b9b814..c868643634 100644 --- a/submodules/TelegramUI/Sources/AppDelegate.swift +++ b/submodules/TelegramUI/Sources/AppDelegate.swift @@ -1394,6 +1394,10 @@ private func extractAccountManagerState(records: AccountRecordsView take(1) |> mapToSignal { view -> Signal in - if let peer = peerViewMainPeer(view) as? TelegramChannel, !peer.hasPermission(.sendMessages) { + if let peer = peerViewMainPeer(view) as? TelegramChannel, !peer.hasPermission(.sendSomething) { return .single(false) } else { return context.account.viewTracker.scheduledMessagesViewForLocation(context.chatLocationInput(for: chatLocation, contextHolder: chatLocationContextHolder)) @@ -4864,6 +4877,37 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G .updatedAutoremoveTimeout(autoremoveTimeout) .updatedCurrentSendAsPeerId(currentSendAsPeerId) .updatedCopyProtectionEnabled(copyProtectionEnabled) + .updatedInterfaceState { interfaceState in + var interfaceState = interfaceState + + if let channel = renderedPeer?.peer as? TelegramChannel { + if channel.hasBannedPermission(.banSendVoice) != nil && channel.hasBannedPermission(.banSendInstantVideos) != nil { + interfaceState = interfaceState.withUpdatedMediaRecordingMode(.audio) + } else if channel.hasBannedPermission(.banSendVoice) != nil { + if channel.hasBannedPermission(.banSendInstantVideos) == nil { + interfaceState = interfaceState.withUpdatedMediaRecordingMode(.video) + } + } else if channel.hasBannedPermission(.banSendInstantVideos) != nil { + if channel.hasBannedPermission(.banSendVoice) == nil { + interfaceState = interfaceState.withUpdatedMediaRecordingMode(.audio) + } + } + } else if let group = renderedPeer?.peer as? TelegramGroup { + if group.hasBannedPermission(.banSendVoice) && group.hasBannedPermission(.banSendInstantVideos) { + interfaceState = interfaceState.withUpdatedMediaRecordingMode(.audio) + } else if group.hasBannedPermission(.banSendVoice) { + if !group.hasBannedPermission(.banSendInstantVideos) { + interfaceState = interfaceState.withUpdatedMediaRecordingMode(.video) + } + } else if group.hasBannedPermission(.banSendInstantVideos) { + if !group.hasBannedPermission(.banSendVoice) { + interfaceState = interfaceState.withUpdatedMediaRecordingMode(.audio) + } + } + } + + return interfaceState + } }) if case .standard(previewing: false) = mode, let channel = renderedPeer?.chatMainPeer as? TelegramChannel, case .broadcast = channel.info { @@ -4995,7 +5039,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G hasScheduledMessages = peerView |> take(1) |> mapToSignal { view -> Signal in - if let peer = peerViewMainPeer(view) as? TelegramChannel, !peer.hasPermission(.sendMessages) { + if let peer = peerViewMainPeer(view) as? TelegramChannel, !peer.hasPermission(.sendSomething) { return .single(false) } else { return context.account.viewTracker.scheduledMessagesViewForLocation(context.chatLocationInput(for: chatLocation, contextHolder: chatLocationContextHolder)) @@ -5271,6 +5315,37 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G return renderedPeer }.updatedIsNotAccessible(isNotAccessible).updatedContactStatus(contactStatus).updatedHasBots(hasBots).updatedIsArchived(isArchived).updatedPeerIsMuted(peerIsMuted).updatedPeerDiscussionId(peerDiscussionId).updatedPeerGeoLocation(peerGeoLocation).updatedExplicitelyCanPinMessages(explicitelyCanPinMessages).updatedHasScheduledMessages(hasScheduledMessages).updatedCurrentSendAsPeerId(currentSendAsPeerId) .updatedCopyProtectionEnabled(copyProtectionEnabled) + .updatedInterfaceState { interfaceState in + var interfaceState = interfaceState + + if let channel = renderedPeer?.peer as? TelegramChannel { + if channel.hasBannedPermission(.banSendVoice) != nil && channel.hasBannedPermission(.banSendInstantVideos) != nil { + interfaceState = interfaceState.withUpdatedMediaRecordingMode(.audio) + } else if channel.hasBannedPermission(.banSendVoice) != nil { + if channel.hasBannedPermission(.banSendInstantVideos) == nil { + interfaceState = interfaceState.withUpdatedMediaRecordingMode(.video) + } + } else if channel.hasBannedPermission(.banSendInstantVideos) != nil { + if channel.hasBannedPermission(.banSendVoice) == nil { + interfaceState = interfaceState.withUpdatedMediaRecordingMode(.audio) + } + } + } else if let group = renderedPeer?.peer as? TelegramGroup { + if group.hasBannedPermission(.banSendVoice) && group.hasBannedPermission(.banSendInstantVideos) { + interfaceState = interfaceState.withUpdatedMediaRecordingMode(.audio) + } else if group.hasBannedPermission(.banSendVoice) { + if !group.hasBannedPermission(.banSendInstantVideos) { + interfaceState = interfaceState.withUpdatedMediaRecordingMode(.video) + } + } else if group.hasBannedPermission(.banSendInstantVideos) { + if !group.hasBannedPermission(.banSendVoice) { + interfaceState = interfaceState.withUpdatedMediaRecordingMode(.audio) + } + } + } + + return interfaceState + } }) if !strongSelf.didSetChatLocationInfoReady { strongSelf.didSetChatLocationInfoReady = true @@ -6312,7 +6387,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G } if let opaqueState = (combinedInitialData.initialData?.storedInterfaceState).flatMap(_internal_decodeStoredChatInterfaceState) { - let interfaceState = ChatInterfaceState.parse(opaqueState) + var interfaceState = ChatInterfaceState.parse(opaqueState) var pinnedMessageId: MessageId? var peerIsBlocked: Bool = false @@ -6343,6 +6418,32 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G } else if let _ = combinedInitialData.cachedData as? CachedSecretChatData { } + if let channel = combinedInitialData.initialData?.peer as? TelegramChannel { + if channel.hasBannedPermission(.banSendVoice) != nil && channel.hasBannedPermission(.banSendInstantVideos) != nil { + interfaceState = interfaceState.withUpdatedMediaRecordingMode(.audio) + } else if channel.hasBannedPermission(.banSendVoice) != nil { + if channel.hasBannedPermission(.banSendInstantVideos) == nil { + interfaceState = interfaceState.withUpdatedMediaRecordingMode(.video) + } + } else if channel.hasBannedPermission(.banSendInstantVideos) != nil { + if channel.hasBannedPermission(.banSendVoice) == nil { + interfaceState = interfaceState.withUpdatedMediaRecordingMode(.audio) + } + } + } else if let group = combinedInitialData.initialData?.peer as? TelegramGroup { + if group.hasBannedPermission(.banSendVoice) && group.hasBannedPermission(.banSendInstantVideos) { + interfaceState = interfaceState.withUpdatedMediaRecordingMode(.audio) + } else if group.hasBannedPermission(.banSendVoice) { + if !group.hasBannedPermission(.banSendInstantVideos) { + interfaceState = interfaceState.withUpdatedMediaRecordingMode(.video) + } + } else if group.hasBannedPermission(.banSendInstantVideos) { + if !group.hasBannedPermission(.banSendVoice) { + interfaceState = interfaceState.withUpdatedMediaRecordingMode(.audio) + } + } + } + if case let .replyThread(replyThreadMessageId) = strongSelf.chatLocation { if let channel = combinedInitialData.initialData?.peer as? TelegramChannel, channel.flags.contains(.isForum) { pinnedMessageId = nil @@ -8344,10 +8445,55 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G guard let strongSelf = self else { return } + guard let peer = strongSelf.presentationInterfaceState.renderedPeer?.peer else { + return + } + strongSelf.mediaRecordingModeTooltipController?.dismiss() strongSelf.interfaceInteraction?.updateShowWebView { _ in return false } + + var bannedMediaInput = false + if let channel = peer as? TelegramChannel { + if channel.hasBannedPermission(.banSendVoice) != nil && channel.hasBannedPermission(.banSendInstantVideos) != nil { + bannedMediaInput = true + } else if channel.hasBannedPermission(.banSendVoice) != nil { + if !isVideo { + //TODO:localize + strongSelf.controllerInteraction?.displayUndo(.info(title: nil, text: "The admins of this group do not allow to send voice messages.")) + return + } + } else if channel.hasBannedPermission(.banSendInstantVideos) != nil { + if isVideo { + //TODO:localize + strongSelf.controllerInteraction?.displayUndo(.info(title: nil, text: "The admins of this group do not allow to send video messages.")) + return + } + } + } else if let group = peer as? TelegramGroup { + if group.hasBannedPermission(.banSendVoice) && group.hasBannedPermission(.banSendInstantVideos) { + bannedMediaInput = true + } else if group.hasBannedPermission(.banSendVoice) { + if !isVideo { + //TODO:localize + strongSelf.controllerInteraction?.displayUndo(.info(title: nil, text: "The admins of this group do not allow to send voice messages.")) + return + } + } else if group.hasBannedPermission(.banSendInstantVideos) { + if isVideo { + //TODO:localize + strongSelf.controllerInteraction?.displayUndo(.info(title: nil, text: "The admins of this group do not allow to send video messages.")) + return + } + } + } + + if bannedMediaInput { + //TODO:localize + strongSelf.controllerInteraction?.displayUndo(.info(title: nil, text: "The admins of this group do not allow to send video and voice messages.")) + return + } let requestId = strongSelf.beginMediaRecordingRequestId let begin: () -> Void = { @@ -8607,35 +8753,77 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G tooltipController.dismissImmediately() } }, switchMediaRecordingMode: { [weak self] in - if let strongSelf = self { - if strongSelf.recordingModeFeedback == nil { - strongSelf.recordingModeFeedback = HapticFeedback() - strongSelf.recordingModeFeedback?.prepareImpact() - } - - strongSelf.recordingModeFeedback?.impact() - var updatedMode: ChatTextInputMediaRecordingButtonMode? - - strongSelf.updateChatPresentationInterfaceState(interactive: true, { - return $0.updatedInterfaceState({ current in - let mode: ChatTextInputMediaRecordingButtonMode - switch current.mediaRecordingMode { - case .audio: - mode = .video - case .video: - mode = .audio - } - updatedMode = mode - return current.withUpdatedMediaRecordingMode(mode) - }).updatedShowWebView(false) - }) - - if let updatedMode = updatedMode, updatedMode == .video { - let _ = ApplicationSpecificNotice.incrementChatMediaMediaRecordingTips(accountManager: strongSelf.context.sharedContext.accountManager, count: 3).start() - } - - strongSelf.displayMediaRecordingTooltip() + guard let strongSelf = self else { + return } + guard let peer = strongSelf.presentationInterfaceState.renderedPeer?.peer else { + return + } + + var bannedMediaInput = false + if let channel = peer as? TelegramChannel { + if channel.hasBannedPermission(.banSendVoice) != nil && channel.hasBannedPermission(.banSendInstantVideos) != nil { + bannedMediaInput = true + } else if channel.hasBannedPermission(.banSendVoice) != nil { + if channel.hasBannedPermission(.banSendInstantVideos) == nil { + strongSelf.displayMediaRecordingTooltip() + return + } + } else if channel.hasBannedPermission(.banSendInstantVideos) != nil { + if channel.hasBannedPermission(.banSendVoice) == nil { + strongSelf.displayMediaRecordingTooltip() + return + } + } + } else if let group = peer as? TelegramGroup { + if group.hasBannedPermission(.banSendVoice) && group.hasBannedPermission(.banSendInstantVideos) { + bannedMediaInput = true + } else if group.hasBannedPermission(.banSendVoice) { + if !group.hasBannedPermission(.banSendInstantVideos) { + strongSelf.displayMediaRecordingTooltip() + return + } + } else if group.hasBannedPermission(.banSendInstantVideos) { + if !group.hasBannedPermission(.banSendVoice) { + strongSelf.displayMediaRecordingTooltip() + return + } + } + } + + if bannedMediaInput { + //TODO:localize + strongSelf.controllerInteraction?.displayUndo(.info(title: nil, text: "The admins of this group do not allow to send video and voice messages.")) + return + } + + if strongSelf.recordingModeFeedback == nil { + strongSelf.recordingModeFeedback = HapticFeedback() + strongSelf.recordingModeFeedback?.prepareImpact() + } + + strongSelf.recordingModeFeedback?.impact() + var updatedMode: ChatTextInputMediaRecordingButtonMode? + + strongSelf.updateChatPresentationInterfaceState(interactive: true, { + return $0.updatedInterfaceState({ current in + let mode: ChatTextInputMediaRecordingButtonMode + switch current.mediaRecordingMode { + case .audio: + mode = .video + case .video: + mode = .audio + } + updatedMode = mode + return current.withUpdatedMediaRecordingMode(mode) + }).updatedShowWebView(false) + }) + + if let updatedMode = updatedMode, updatedMode == .video { + let _ = ApplicationSpecificNotice.incrementChatMediaMediaRecordingTips(accountManager: strongSelf.context.sharedContext.accountManager, count: 3).start() + } + + strongSelf.displayMediaRecordingTooltip() }, setupMessageAutoremoveTimeout: { [weak self] in guard let strongSelf = self, case let .peer(peerId) = strongSelf.chatLocation else { return @@ -11948,25 +12136,54 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G guard let peer = self.presentationInterfaceState.renderedPeer?.peer else { return } + let _ = peer let _ = (self.context.sharedContext.accountManager.transaction { transaction -> GeneratedMediaStoreSettings in let entry = transaction.getSharedData(ApplicationSpecificSharedDataKeys.generatedMediaStoreSettings)?.get(GeneratedMediaStoreSettings.self) return entry ?? GeneratedMediaStoreSettings.defaultSettings } |> deliverOnMainQueue).start(next: { [weak self] settings in - guard let strongSelf = self else { + guard let strongSelf = self, let peer = strongSelf.presentationInterfaceState.renderedPeer?.peer else { return } - var photoOnly = false + var enablePhoto = true + var enableVideo = true + if let callManager = strongSelf.context.sharedContext.callManager as? PresentationCallManagerImpl, callManager.hasActiveCall { - photoOnly = true + enableVideo = false + } + + var bannedSendPhotos: (Int32, Bool)? + var bannedSendVideos: (Int32, Bool)? + + if let channel = peer as? TelegramChannel { + if let value = channel.hasBannedPermission(.banSendPhotos) { + bannedSendPhotos = value + } + if let value = channel.hasBannedPermission(.banSendVideos) { + bannedSendVideos = value + } + } else if let group = peer as? TelegramGroup { + if group.hasBannedPermission(.banSendPhotos) { + bannedSendPhotos = (Int32.max, false) + } + if group.hasBannedPermission(.banSendVideos) { + bannedSendVideos = (Int32.max, false) + } + } + + if bannedSendPhotos != nil { + enablePhoto = false + } + if bannedSendVideos != nil { + enableVideo = false } let storeCapturedMedia = peer.id.namespace != Namespaces.Peer.SecretChat let inputText = strongSelf.presentationInterfaceState.interfaceState.effectiveInputState.inputText - presentedLegacyCamera(context: strongSelf.context, peer: peer, chatLocation: strongSelf.chatLocation, cameraView: cameraView, menuController: nil, parentController: strongSelf, attachmentController: self?.attachmentController, editingMedia: false, saveCapturedPhotos: storeCapturedMedia, mediaGrouping: true, initialCaption: inputText, hasSchedule: strongSelf.presentationInterfaceState.subject != .scheduledMessages && peer.id.namespace != Namespaces.Peer.SecretChat, photoOnly: photoOnly, sendMessagesWithSignals: { [weak self] signals, silentPosting, scheduleTime in + presentedLegacyCamera(context: strongSelf.context, peer: peer, chatLocation: strongSelf.chatLocation, cameraView: cameraView, menuController: nil, parentController: strongSelf, attachmentController: self?.attachmentController, editingMedia: false, saveCapturedPhotos: storeCapturedMedia, mediaGrouping: true, initialCaption: inputText, hasSchedule: strongSelf.presentationInterfaceState.subject != .scheduledMessages && peer.id.namespace != Namespaces.Peer.SecretChat, enablePhoto: enablePhoto, enableVideo: enableVideo, sendMessagesWithSignals: { [weak self] signals, silentPosting, scheduleTime in if let strongSelf = self { strongSelf.enqueueMediaMessages(signals: signals, silentPosting: silentPosting, scheduleTime: scheduleTime > 0 ? scheduleTime : nil) if !inputText.string.isEmpty { @@ -12603,12 +12820,40 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G }) }, openCamera: { [weak self] cameraView, menuController in if let strongSelf = self, let peer = strongSelf.presentationInterfaceState.renderedPeer?.peer { - var photoOnly = false + var enablePhoto = true + var enableVideo = true + if let callManager = strongSelf.context.sharedContext.callManager as? PresentationCallManagerImpl, callManager.hasActiveCall { - photoOnly = true + enableVideo = false } - presentedLegacyCamera(context: strongSelf.context, peer: peer, chatLocation: strongSelf.chatLocation, cameraView: cameraView, menuController: menuController, parentController: strongSelf, editingMedia: editMediaOptions != nil, saveCapturedPhotos: peer.id.namespace != Namespaces.Peer.SecretChat, mediaGrouping: true, initialCaption: inputText, hasSchedule: strongSelf.presentationInterfaceState.subject != .scheduledMessages && peer.id.namespace != Namespaces.Peer.SecretChat, photoOnly: photoOnly, sendMessagesWithSignals: { [weak self] signals, silentPosting, scheduleTime in + var bannedSendPhotos: (Int32, Bool)? + var bannedSendVideos: (Int32, Bool)? + + if let channel = peer as? TelegramChannel { + if let value = channel.hasBannedPermission(.banSendPhotos) { + bannedSendPhotos = value + } + if let value = channel.hasBannedPermission(.banSendVideos) { + bannedSendVideos = value + } + } else if let group = peer as? TelegramGroup { + if group.hasBannedPermission(.banSendPhotos) { + bannedSendPhotos = (Int32.max, false) + } + if group.hasBannedPermission(.banSendVideos) { + bannedSendVideos = (Int32.max, false) + } + } + + if bannedSendPhotos != nil { + enablePhoto = false + } + if bannedSendVideos != nil { + enableVideo = false + } + + presentedLegacyCamera(context: strongSelf.context, peer: peer, chatLocation: strongSelf.chatLocation, cameraView: cameraView, menuController: menuController, parentController: strongSelf, editingMedia: editMediaOptions != nil, saveCapturedPhotos: peer.id.namespace != Namespaces.Peer.SecretChat, mediaGrouping: true, initialCaption: inputText, hasSchedule: strongSelf.presentationInterfaceState.subject != .scheduledMessages && peer.id.namespace != Namespaces.Peer.SecretChat, enablePhoto: enablePhoto, enableVideo: enableVideo, sendMessagesWithSignals: { [weak self] signals, silentPosting, scheduleTime in if let strongSelf = self { if editMediaOptions != nil { strongSelf.editMessageMediaWithLegacySignals(signals!) @@ -13096,8 +13341,93 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G } }) })) - controller.getCaptionPanelView = { [weak self] in - return self?.getCaptionPanelView() + controller.attemptItemSelection = { [weak strongSelf] item in + guard let strongSelf, let peer = strongSelf.presentationInterfaceState.renderedPeer?.peer else { + return false + } + + enum ItemType { + case gif + case image + case video + } + + var itemType: ItemType? + switch item { + case let .internalReference(reference): + if reference.type == "gif" { + itemType = .gif + } else if reference.type == "photo" { + itemType = .image + } else if reference.type == "video" { + itemType = .video + } + case let .externalReference(reference): + if reference.type == "gif" { + itemType = .gif + } else if reference.type == "photo" { + itemType = .image + } else if reference.type == "video" { + itemType = .video + } + } + + var bannedSendPhotos: (Int32, Bool)? + var bannedSendVideos: (Int32, Bool)? + var bannedSendGifs: (Int32, Bool)? + + if let channel = peer as? TelegramChannel { + if let value = channel.hasBannedPermission(.banSendPhotos) { + bannedSendPhotos = value + } + if let value = channel.hasBannedPermission(.banSendVideos) { + bannedSendVideos = value + } + if let value = channel.hasBannedPermission(.banSendGifs) { + bannedSendGifs = value + } + } else if let group = peer as? TelegramGroup { + if group.hasBannedPermission(.banSendPhotos) { + bannedSendPhotos = (Int32.max, false) + } + if group.hasBannedPermission(.banSendVideos) { + bannedSendVideos = (Int32.max, false) + } + if group.hasBannedPermission(.banSendGifs) { + bannedSendGifs = (Int32.max, false) + } + } + + if let itemType { + switch itemType { + case .image: + if bannedSendPhotos != nil { + //TODO:localize + strongSelf.present(standardTextAlertController(theme: AlertControllerTheme(presentationData: strongSelf.presentationData), title: nil, text: "Sending photos is not allowed", actions: [TextAlertAction(type: .defaultAction, title: strongSelf.presentationData.strings.Common_OK, action: {})]), in: .window(.root)) + + return false + } + case .video: + if bannedSendVideos != nil { + //TODO:localize + strongSelf.present(standardTextAlertController(theme: AlertControllerTheme(presentationData: strongSelf.presentationData), title: nil, text: "Sending videos is not allowed", actions: [TextAlertAction(type: .defaultAction, title: strongSelf.presentationData.strings.Common_OK, action: {})]), in: .window(.root)) + + return false + } + case .gif: + if bannedSendGifs != nil { + //TODO:localize + strongSelf.present(standardTextAlertController(theme: AlertControllerTheme(presentationData: strongSelf.presentationData), title: nil, text: "Sending gifs is not allowed", actions: [TextAlertAction(type: .defaultAction, title: strongSelf.presentationData.strings.Common_OK, action: {})]), in: .window(.root)) + + return false + } + } + } + + return true + } + controller.getCaptionPanelView = { [weak strongSelf] in + return strongSelf?.getCaptionPanelView() } present(controller, ViewControllerPresentationArguments(presentationAnimation: .modalSheet)) } @@ -16597,15 +16927,55 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G } private func displayMediaRecordingTooltip() { + guard let peer = self.presentationInterfaceState.renderedPeer?.peer else { + return + } + let rect: CGRect? = self.chatDisplayNode.frameForInputActionButton() let updatedMode: ChatTextInputMediaRecordingButtonMode = self.presentationInterfaceState.interfaceState.mediaRecordingMode let text: String + + var canSwitch = true + if let channel = peer as? TelegramChannel { + if channel.hasBannedPermission(.banSendVoice) != nil && channel.hasBannedPermission(.banSendInstantVideos) != nil { + canSwitch = false + } else if channel.hasBannedPermission(.banSendVoice) != nil { + if channel.hasBannedPermission(.banSendInstantVideos) == nil { + canSwitch = false + } + } else if channel.hasBannedPermission(.banSendInstantVideos) != nil { + if channel.hasBannedPermission(.banSendVoice) == nil { + canSwitch = false + } + } + } else if let group = peer as? TelegramGroup { + if group.hasBannedPermission(.banSendVoice) && group.hasBannedPermission(.banSendInstantVideos) { + canSwitch = false + } else if group.hasBannedPermission(.banSendVoice) { + if !group.hasBannedPermission(.banSendInstantVideos) { + canSwitch = false + } + } else if group.hasBannedPermission(.banSendInstantVideos) { + if !group.hasBannedPermission(.banSendVoice) { + canSwitch = false + } + } + } + if updatedMode == .audio { - text = self.presentationData.strings.Conversation_HoldForAudio + if canSwitch { + text = self.presentationData.strings.Conversation_HoldForAudio + } else { + text = self.presentationData.strings.Conversation_HoldForAudioOnly + } } else { - text = self.presentationData.strings.Conversation_HoldForVideo + if canSwitch { + text = self.presentationData.strings.Conversation_HoldForVideo + } else { + text = self.presentationData.strings.Conversation_HoldForVideoOnly + } } self.silentPostTooltipController?.dismiss() diff --git a/submodules/TelegramUI/Sources/ChatInterfaceInputContexts.swift b/submodules/TelegramUI/Sources/ChatInterfaceInputContexts.swift index 7f748a8b56..c492f5ebf0 100644 --- a/submodules/TelegramUI/Sources/ChatInterfaceInputContexts.swift +++ b/submodules/TelegramUI/Sources/ChatInterfaceInputContexts.swift @@ -280,6 +280,8 @@ func inputTextPanelStateForChatPresentationInterfaceState(_ chatPresentationInte var currentAutoremoveTimeout: Int32? = chatPresentationInterfaceState.autoremoveTimeout var canSetupAutoremoveTimeout = false + var canSendTextMessages = true + var accessoryItems: [ChatTextInputAccessoryItem] = [] if let peer = chatPresentationInterfaceState.renderedPeer?.peer as? TelegramSecretChat { var extendedSearchLayout = false @@ -298,6 +300,7 @@ func inputTextPanelStateForChatPresentationInterfaceState(_ chatPresentationInte if !group.hasBannedPermission(.banChangeInfo) { canSetupAutoremoveTimeout = true } + canSendTextMessages = !group.hasBannedPermission(.banSendText) } else if let user = chatPresentationInterfaceState.renderedPeer?.peer as? TelegramUser { if user.botInfo == nil { canSetupAutoremoveTimeout = true @@ -306,6 +309,7 @@ func inputTextPanelStateForChatPresentationInterfaceState(_ chatPresentationInte if channel.hasPermission(.changeInfo) { canSetupAutoremoveTimeout = true } + canSendTextMessages = channel.hasBannedPermission(.banSendText) == nil } if canSetupAutoremoveTimeout { @@ -375,10 +379,16 @@ func inputTextPanelStateForChatPresentationInterfaceState(_ chatPresentationInte accessoryItems.append(.commands) } - if stickersEnabled { - accessoryItems.append(.input(isEnabled: true, inputMode: stickersAreEmoji ? .emoji : .stickers)) + if !canSendTextMessages { + if stickersEnabled && !stickersAreEmoji { + accessoryItems.append(.input(isEnabled: true, inputMode: .stickers)) + } } else { - accessoryItems.append(.input(isEnabled: true, inputMode: .emoji)) + if stickersEnabled { + accessoryItems.append(.input(isEnabled: true, inputMode: stickersAreEmoji ? .emoji : .stickers)) + } else { + accessoryItems.append(.input(isEnabled: true, inputMode: .emoji)) + } } if isTextEmpty, let message = chatPresentationInterfaceState.keyboardButtonsMessage, let _ = message.visibleButtonKeyboardMarkup, chatPresentationInterfaceState.interfaceState.messageActionsState.dismissedButtonKeyboardMessageId != message.id { diff --git a/submodules/TelegramUI/Sources/ChatInterfaceStateContextMenus.swift b/submodules/TelegramUI/Sources/ChatInterfaceStateContextMenus.swift index 758a7cfe33..cd382d2274 100644 --- a/submodules/TelegramUI/Sources/ChatInterfaceStateContextMenus.swift +++ b/submodules/TelegramUI/Sources/ChatInterfaceStateContextMenus.swift @@ -283,7 +283,7 @@ func canReplyInChat(_ chatPresentationInterfaceState: ChatPresentationInterfaceS case .peer: if let channel = peer as? TelegramChannel { if case .member = channel.participationStatus { - canReply = channel.hasPermission(.sendMessages) + canReply = channel.hasPermission(.sendSomething) } } else if let group = peer as? TelegramGroup { if case .Member = group.membership { diff --git a/submodules/TelegramUI/Sources/ChatInterfaceStateInputPanels.swift b/submodules/TelegramUI/Sources/ChatInterfaceStateInputPanels.swift index 3e26beda6e..30a7776995 100644 --- a/submodules/TelegramUI/Sources/ChatInterfaceStateInputPanels.swift +++ b/submodules/TelegramUI/Sources/ChatInterfaceStateInputPanels.swift @@ -196,22 +196,22 @@ func inputPanelForChatPresentationIntefaceState(_ chatPresentationInterfaceState } } - if isMember && channel.hasBannedPermission(.banSendMessages) != nil && !channel.flags.contains(.isGigagroup) { - if let currentPanel = (currentPanel as? ChatRestrictedInputPanelNode) ?? (currentSecondaryPanel as? ChatRestrictedInputPanelNode) { + if isMember && channel.hasBannedPermission(.banSendText) != nil && !channel.flags.contains(.isGigagroup) { + /*if let currentPanel = (currentPanel as? ChatRestrictedInputPanelNode) ?? (currentSecondaryPanel as? ChatRestrictedInputPanelNode) { return (currentPanel, nil) } else { let panel = ChatRestrictedInputPanelNode() panel.context = context panel.interfaceInteraction = interfaceInteraction return (panel, nil) - } + }*/ } switch channel.info { case .broadcast: if chatPresentationInterfaceState.interfaceState.editMessage != nil, channel.hasPermission(.editAllMessages) { displayInputTextPanel = true - } else if !channel.hasPermission(.sendMessages) || !isMember { + } else if !channel.hasPermission(.sendSomething) || !isMember { if let currentPanel = (currentPanel as? ChatChannelSubscriberInputPanelNode) ?? (currentSecondaryPanel as? ChatChannelSubscriberInputPanelNode) { return (currentPanel, nil) } else { @@ -235,7 +235,7 @@ func inputPanelForChatPresentationIntefaceState(_ chatPresentationInterfaceState } } case .member: - if channel.flags.contains(.isGigagroup) && !channel.hasPermission(.sendMessages) { + if channel.flags.contains(.isGigagroup) && !channel.hasPermission(.sendSomething) { if let currentPanel = (currentPanel as? ChatChannelSubscriberInputPanelNode) ?? (currentSecondaryPanel as? ChatChannelSubscriberInputPanelNode) { return (currentPanel, nil) } else { @@ -280,15 +280,15 @@ func inputPanelForChatPresentationIntefaceState(_ chatPresentationInterfaceState break } - if group.hasBannedPermission(.banSendMessages) { - if let currentPanel = (currentPanel as? ChatRestrictedInputPanelNode) ?? (currentSecondaryPanel as? ChatRestrictedInputPanelNode) { + if group.hasBannedPermission(.banSendText) { + /*if let currentPanel = (currentPanel as? ChatRestrictedInputPanelNode) ?? (currentSecondaryPanel as? ChatRestrictedInputPanelNode) { return (currentPanel, nil) } else { let panel = ChatRestrictedInputPanelNode() panel.context = context panel.interfaceInteraction = interfaceInteraction return (panel, nil) - } + }*/ } } diff --git a/submodules/TelegramUI/Sources/ChatRecentActionsHistoryTransition.swift b/submodules/TelegramUI/Sources/ChatRecentActionsHistoryTransition.swift index 7e649d5279..84eb66b1d2 100644 --- a/submodules/TelegramUI/Sources/ChatRecentActionsHistoryTransition.swift +++ b/submodules/TelegramUI/Sources/ChatRecentActionsHistoryTransition.swift @@ -660,7 +660,7 @@ struct ChatRecentActionsEntry: Comparable, Identifiable { let order: [(TelegramChatBannedRightsFlags, String)] = [ (.banReadMessages, self.presentationData.strings.Channel_AdminLog_BanReadMessages), - (.banSendMessages, self.presentationData.strings.Channel_AdminLog_BanSendMessages), + (.banSendText, self.presentationData.strings.Channel_AdminLog_BanSendMessages), (.banSendMedia, self.presentationData.strings.Channel_AdminLog_BanSendMedia), (.banSendStickers, self.presentationData.strings.Channel_AdminLog_BanSendStickersAndGifs), (.banEmbedLinks, self.presentationData.strings.Channel_AdminLog_BanEmbedLinks), @@ -1015,7 +1015,7 @@ struct ChatRecentActionsEntry: Comparable, Identifiable { let order: [(TelegramChatBannedRightsFlags, String)] = [ (.banReadMessages, self.presentationData.strings.Channel_AdminLog_BanReadMessages), - (.banSendMessages, self.presentationData.strings.Channel_AdminLog_BanSendMessages), + (.banSendText, self.presentationData.strings.Channel_AdminLog_BanSendMessages), (.banSendMedia, self.presentationData.strings.Channel_AdminLog_BanSendMedia), (.banSendStickers, self.presentationData.strings.Channel_AdminLog_BanSendStickersAndGifs), (.banEmbedLinks, self.presentationData.strings.Channel_AdminLog_BanEmbedLinks), diff --git a/submodules/TelegramUI/Sources/ChatRestrictedInputPanelNode.swift b/submodules/TelegramUI/Sources/ChatRestrictedInputPanelNode.swift index 247a175925..2b88e19118 100644 --- a/submodules/TelegramUI/Sources/ChatRestrictedInputPanelNode.swift +++ b/submodules/TelegramUI/Sources/ChatRestrictedInputPanelNode.swift @@ -32,9 +32,9 @@ final class ChatRestrictedInputPanelNode: ChatInputPanelNode { let bannedPermission: (Int32, Bool)? if let channel = interfaceState.renderedPeer?.peer as? TelegramChannel { - bannedPermission = channel.hasBannedPermission(.banSendMessages) + bannedPermission = channel.hasBannedPermission(.banSendText) } else if let group = interfaceState.renderedPeer?.peer as? TelegramGroup { - if group.hasBannedPermission(.banSendMessages) { + if group.hasBannedPermission(.banSendText) { bannedPermission = (Int32.max, false) } else { bannedPermission = nil diff --git a/submodules/TelegramUI/Sources/ChatTextInputPanelNode.swift b/submodules/TelegramUI/Sources/ChatTextInputPanelNode.swift index 17c279aa47..975672f11a 100644 --- a/submodules/TelegramUI/Sources/ChatTextInputPanelNode.swift +++ b/submodules/TelegramUI/Sources/ChatTextInputPanelNode.swift @@ -462,6 +462,7 @@ final class ChatTextViewForOverlayContent: UIView, ChatInputPanelViewForOverlayC class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate { let clippingNode: ASDisplayNode var textPlaceholderNode: ImmediateTextNode + var textLockIconNode: ASImageNode? var contextPlaceholderNode: TextNode? var slowmodePlaceholderNode: ChatTextInputSlowmodePlaceholderNode? let textInputContainerBackgroundNode: ASImageNode @@ -516,6 +517,7 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate { private var updatingInputState = false private var currentPlaceholder: String? + private var sendingTextDisabled: Bool = false private var presentationInterfaceState: ChatPresentationInterfaceState? private var initializedPlaceholder = false @@ -923,7 +925,15 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate { let recognizer = TouchDownGestureRecognizer(target: self, action: #selector(self.textInputBackgroundViewTap(_:))) recognizer.touchDown = { [weak self] in if let strongSelf = self { - strongSelf.ensureFocused() + if strongSelf.sendingTextDisabled { + guard let controller = strongSelf.interfaceInteraction?.chatController() as? ChatControllerImpl else { + return + } + //TODO:localize + controller.controllerInteraction?.displayUndo(.info(title: nil, text: "The admins of this group do not allow to send text messages.")) + } else { + strongSelf.ensureFocused() + } } } recognizer.waitForTouchUp = { [weak self] in @@ -997,6 +1007,7 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate { textInputNode.textView.scrollIndicatorInsets = UIEdgeInsets(top: 9.0, left: 0.0, bottom: 9.0, right: -13.0) self.textInputContainer.addSubnode(textInputNode) textInputNode.view.disablesInteractiveTransitionGestureRecognizer = true + textInputNode.isUserInteractionEnabled = !self.sendingTextDisabled self.textInputNode = textInputNode var accessoryButtonsWidth: CGFloat = 0.0 @@ -1024,8 +1035,8 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate { self.updateSpoiler() } - self.textInputBackgroundNode.isUserInteractionEnabled = false - self.textInputBackgroundNode.view.removeGestureRecognizer(self.textInputBackgroundNode.view.gestureRecognizers![0]) + self.textInputBackgroundNode.isUserInteractionEnabled = !textInputNode.isUserInteractionEnabled + //self.textInputBackgroundNode.view.removeGestureRecognizer(self.textInputBackgroundNode.view.gestureRecognizers![0]) let recognizer = TouchDownGestureRecognizer(target: self, action: #selector(self.textInputBackgroundViewTap(_:))) recognizer.touchDown = { [weak self] in @@ -1198,6 +1209,18 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate { self.attachmentButton.accessibilityTraits = (!isSlowmodeActive || isMediaEnabled) ? [.button] : [.button, .notEnabled] self.attachmentButtonDisabledNode.isHidden = !isSlowmodeActive || isMediaEnabled + var sendingTextDisabled = false + if let peer = interfaceState.renderedPeer?.peer { + if let channel = peer as? TelegramChannel, channel.hasBannedPermission(.banSendText) != nil { + sendingTextDisabled = true + } else if let group = peer as? TelegramGroup, group.hasBannedPermission(.banSendText) { + sendingTextDisabled = true + } + } + self.sendingTextDisabled = sendingTextDisabled + + self.textInputNode?.isUserInteractionEnabled = !sendingTextDisabled + var buttonTitleUpdated = false var menuTextSize = self.menuButtonTextNode.frame.size if self.presentationInterfaceState != interfaceState { @@ -1347,22 +1370,30 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate { self.initializedPlaceholder = true var placeholder: String + if let channel = peer as? TelegramChannel, case .broadcast = channel.info { if interfaceState.interfaceState.silentPosting { placeholder = interfaceState.strings.Conversation_InputTextSilentBroadcastPlaceholder } else { placeholder = interfaceState.strings.Conversation_InputTextBroadcastPlaceholder } - } else if let channel = peer as? TelegramChannel, case .group = channel.info, channel.hasPermission(.canBeAnonymous) { - placeholder = interfaceState.strings.Conversation_InputTextAnonymousPlaceholder - } else if case let .replyThread(replyThreadMessage) = interfaceState.chatLocation, !replyThreadMessage.isForumPost { - if replyThreadMessage.isChannelPost { - placeholder = interfaceState.strings.Conversation_InputTextPlaceholderComment - } else { - placeholder = interfaceState.strings.Conversation_InputTextPlaceholderReply - } } else { - placeholder = interfaceState.strings.Conversation_InputTextPlaceholder + if sendingTextDisabled { + //TODO:localize + placeholder = "Text not allowed" + } else { + if let channel = peer as? TelegramChannel, case .group = channel.info, channel.hasPermission(.canBeAnonymous) { + placeholder = interfaceState.strings.Conversation_InputTextAnonymousPlaceholder + } else if case let .replyThread(replyThreadMessage) = interfaceState.chatLocation, !replyThreadMessage.isForumPost { + if replyThreadMessage.isChannelPost { + placeholder = interfaceState.strings.Conversation_InputTextPlaceholderComment + } else { + placeholder = interfaceState.strings.Conversation_InputTextPlaceholderReply + } + } else { + placeholder = interfaceState.strings.Conversation_InputTextPlaceholder + } + } } if let keyboardButtonsMessage = interfaceState.keyboardButtonsMessage, interfaceState.interfaceState.messageActionsState.dismissedButtonKeyboardMessageId != keyboardButtonsMessage.id { @@ -2037,7 +2068,35 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate { transition.updateFrame(layer: self.textInputBackgroundNode.layer, frame: textInputBackgroundFrame) transition.updateAlpha(node: self.textInputBackgroundNode, alpha: audioRecordingItemsAlpha) - transition.updateFrame(node: self.textPlaceholderNode, frame: CGRect(origin: CGPoint(x: leftInset + textFieldInsets.left + self.textInputViewInternalInsets.left, y: textFieldInsets.top + self.textInputViewInternalInsets.top + textInputViewRealInsets.top + UIScreenPixel), size: self.textPlaceholderNode.frame.size)) + let textPlaceholderFrame: CGRect + if sendingTextDisabled { + textPlaceholderFrame = CGRect(origin: CGPoint(x: textInputBackgroundFrame.minX + floor((textInputBackgroundFrame.width - self.textPlaceholderNode.bounds.width) / 2.0), y: textFieldInsets.top + self.textInputViewInternalInsets.top + textInputViewRealInsets.top + UIScreenPixel), size: self.textPlaceholderNode.frame.size) + + let textLockIconNode: ASImageNode + var textLockIconTransition = transition + if let current = self.textLockIconNode { + textLockIconNode = current + } else { + textLockIconTransition = .immediate + textLockIconNode = ASImageNode() + self.textLockIconNode = textLockIconNode + self.textPlaceholderNode.addSubnode(textLockIconNode) + + textLockIconNode.image = generateTintedImage(image: UIImage(bundleImageName: "Chat/Input/Accessory Panels/TextLockIcon"), color: interfaceState.theme.chat.inputPanel.inputPlaceholderColor) + } + + if let image = textLockIconNode.image { + textLockIconTransition.updateFrame(node: textLockIconNode, frame: CGRect(origin: CGPoint(x: -image.size.width - 4.0, y: floor((textPlaceholderFrame.height - image.size.height) / 2.0)), size: image.size)) + } + } else { + textPlaceholderFrame = CGRect(origin: CGPoint(x: leftInset + textFieldInsets.left + self.textInputViewInternalInsets.left, y: textFieldInsets.top + self.textInputViewInternalInsets.top + textInputViewRealInsets.top + UIScreenPixel), size: self.textPlaceholderNode.frame.size) + + if let textLockIconNode = self.textLockIconNode { + self.textLockIconNode = nil + textLockIconNode.removeFromSupernode() + } + } + transition.updateFrame(node: self.textPlaceholderNode, frame: textPlaceholderFrame) var textPlaceholderAlpha: CGFloat = audioRecordingItemsAlpha if self.textPlaceholderNode.frame.width > (nextButtonTopRight.x - textInputBackgroundFrame.minX) - 32.0 { @@ -3352,6 +3411,10 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate { } func ensureFocused() { + if self.sendingTextDisabled { + return + } + if self.textInputNode == nil { self.loadTextInputNode() } diff --git a/submodules/TelegramUI/Sources/LegacyCamera.swift b/submodules/TelegramUI/Sources/LegacyCamera.swift index 1efc0e4076..1178495c2b 100644 --- a/submodules/TelegramUI/Sources/LegacyCamera.swift +++ b/submodules/TelegramUI/Sources/LegacyCamera.swift @@ -10,7 +10,7 @@ import ShareController import LegacyUI import LegacyMediaPickerUI -func presentedLegacyCamera(context: AccountContext, peer: Peer, chatLocation: ChatLocation, cameraView: TGAttachmentCameraView?, menuController: TGMenuSheetController?, parentController: ViewController, attachmentController: ViewController? = nil, editingMedia: Bool, saveCapturedPhotos: Bool, mediaGrouping: Bool, initialCaption: NSAttributedString, hasSchedule: Bool, photoOnly: Bool, sendMessagesWithSignals: @escaping ([Any]?, Bool, Int32) -> Void, recognizedQRCode: @escaping (String) -> Void = { _ in }, presentSchedulePicker: @escaping (Bool, @escaping (Int32) -> Void) -> Void, presentTimerPicker: @escaping (@escaping (Int32) -> Void) -> Void, getCaptionPanelView: @escaping () -> TGCaptionPanelView?, dismissedWithResult: @escaping () -> Void = {}, finishedTransitionIn: @escaping () -> Void = {}) { +func presentedLegacyCamera(context: AccountContext, peer: Peer, chatLocation: ChatLocation, cameraView: TGAttachmentCameraView?, menuController: TGMenuSheetController?, parentController: ViewController, attachmentController: ViewController? = nil, editingMedia: Bool, saveCapturedPhotos: Bool, mediaGrouping: Bool, initialCaption: NSAttributedString, hasSchedule: Bool, enablePhoto: Bool, enableVideo: Bool, sendMessagesWithSignals: @escaping ([Any]?, Bool, Int32) -> Void, recognizedQRCode: @escaping (String) -> Void = { _ in }, presentSchedulePicker: @escaping (Bool, @escaping (Int32) -> Void) -> Void, presentTimerPicker: @escaping (@escaping (Int32) -> Void) -> Void, getCaptionPanelView: @escaping () -> TGCaptionPanelView?, dismissedWithResult: @escaping () -> Void = {}, finishedTransitionIn: @escaping () -> Void = {}) { let presentationData = context.sharedContext.currentPresentationData.with { $0 } let legacyController = LegacyController(presentation: .custom, theme: presentationData.theme) legacyController.supportedOrientations = ViewControllerSupportedOrientations(regularSize: .portrait, compactSize: .portrait) @@ -22,7 +22,16 @@ func presentedLegacyCamera(context: AccountContext, peer: Peer, chatLocation: Ch let controller: TGCameraController if let cameraView = cameraView, let previewView = cameraView.previewView() { - controller = TGCameraController(context: legacyController.context, saveEditedPhotos: saveCapturedPhotos && !isSecretChat, saveCapturedMedia: saveCapturedPhotos && !isSecretChat, camera: previewView.camera, previewView: previewView, intent: photoOnly ? TGCameraControllerGenericPhotoOnlyIntent : TGCameraControllerGenericIntent) + let intent: TGCameraControllerIntent + if !enableVideo { + intent = TGCameraControllerGenericPhotoOnlyIntent + } else if !enablePhoto { + intent = TGCameraControllerGenericVideoOnlyIntent + } else { + intent = TGCameraControllerGenericIntent + } + + controller = TGCameraController(context: legacyController.context, saveEditedPhotos: saveCapturedPhotos && !isSecretChat, saveCapturedMedia: saveCapturedPhotos && !isSecretChat, camera: previewView.camera, previewView: previewView, intent: intent) } else { controller = TGCameraController(context: legacyController.context, saveEditedPhotos: saveCapturedPhotos && !isSecretChat, saveCapturedMedia: saveCapturedPhotos && !isSecretChat) } diff --git a/submodules/TelegramUI/Sources/PeerInfo/PeerInfoScreen.swift b/submodules/TelegramUI/Sources/PeerInfo/PeerInfoScreen.swift index 1fb590520c..06a5eb54f4 100644 --- a/submodules/TelegramUI/Sources/PeerInfo/PeerInfoScreen.swift +++ b/submodules/TelegramUI/Sources/PeerInfo/PeerInfoScreen.swift @@ -1516,7 +1516,7 @@ private func editingItems(data: PeerInfoScreenData?, state: PeerInfoState, chatL })) } - if isCreator || (channel.adminRights != nil && channel.hasPermission(.sendMessages)) { + if isCreator || (channel.adminRights != nil && channel.hasPermission(.sendSomething)) { let messagesShouldHaveSignatures: Bool switch channel.info { case let .broadcast(info): diff --git a/submodules/WebSearchUI/Sources/WebSearchController.swift b/submodules/WebSearchUI/Sources/WebSearchController.swift index 96f8cd1134..b8a63649ea 100644 --- a/submodules/WebSearchUI/Sources/WebSearchController.swift +++ b/submodules/WebSearchUI/Sources/WebSearchController.swift @@ -33,7 +33,7 @@ final class WebSearchControllerInteraction { let openResult: (ChatContextResult) -> Void let setSearchQuery: (String) -> Void let deleteRecentQuery: (String) -> Void - let toggleSelection: (ChatContextResult, Bool) -> Void + let toggleSelection: (ChatContextResult, Bool) -> Bool let sendSelected: (ChatContextResult?, Bool, Int32?) -> Void let schedule: () -> Void let avatarCompleted: (UIImage) -> Void @@ -41,7 +41,7 @@ final class WebSearchControllerInteraction { let editingState: TGMediaEditingContext var hiddenMediaId: String? - init(openResult: @escaping (ChatContextResult) -> Void, setSearchQuery: @escaping (String) -> Void, deleteRecentQuery: @escaping (String) -> Void, toggleSelection: @escaping (ChatContextResult, Bool) -> Void, sendSelected: @escaping (ChatContextResult?, Bool, Int32?) -> Void, schedule: @escaping () -> 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?) -> Void, schedule: @escaping () -> Void, avatarCompleted: @escaping (UIImage) -> Void, selectionState: TGMediaSelectionContext?, editingState: TGMediaEditingContext) { self.openResult = openResult self.setSearchQuery = setSearchQuery self.deleteRecentQuery = deleteRecentQuery @@ -119,6 +119,8 @@ public final class WebSearchController: ViewController { public var searchingUpdated: (Bool) -> Void = { _ in } + public var attemptItemSelection: (ChatContextResult) -> Bool = { _ in return true } + public init(context: AccountContext, updatedPresentationData: (initial: PresentationData, signal: Signal)? = nil, peer: EnginePeer?, chatLocation: ChatLocation?, configuration: EngineConfiguration.SearchBots, mode: WebSearchControllerMode) { self.context = context self.mode = mode @@ -233,8 +235,14 @@ public final class WebSearchController: ViewController { } }, toggleSelection: { [weak self] result, value in if let strongSelf = self { + if !strongSelf.attemptItemSelection(result) { + return false + } let item = LegacyWebSearchItem(result: result) strongSelf.controllerInteraction?.selectionState?.setItem(item, selected: value) + return true + } else { + return false } }, sendSelected: { [weak self] current, silently, scheduleTime in if let selectionState = selectionState, let results = self?.controllerNode.currentExternalResults { @@ -258,6 +266,18 @@ public final class WebSearchController: ViewController { } }, selectionState: selectionState, editingState: editingState) + selectionState?.attemptSelectingItem = { [weak self] item in + guard let self else { + return false + } + + if let item = item as? LegacyWebSearchItem { + return self.attemptItemSelection(item.result) + } + + return true + } + if let selectionState = selectionState { self.selectionDisposable = (selectionChangedSignal(selectionState: selectionState) |> deliverOnMainQueue).start(next: { [weak self] _ in diff --git a/submodules/WebSearchUI/Sources/WebSearchItem.swift b/submodules/WebSearchUI/Sources/WebSearchItem.swift index 21d7506613..98df5e9c31 100644 --- a/submodules/WebSearchUI/Sources/WebSearchItem.swift +++ b/submodules/WebSearchUI/Sources/WebSearchItem.swift @@ -213,8 +213,13 @@ final class WebSearchItemNode: GridItemNode { func updateSelectionState(animated: Bool) { if self.checkNode == nil, let item = self.item, let _ = item.controllerInteraction.selectionState { let checkNode = InteractiveCheckNode(theme: CheckNodeTheme(theme: item.theme, style: .overlay)) - checkNode.valueChanged = { value in - item.controllerInteraction.toggleSelection(item.result, value) + checkNode.valueChanged = { [weak self] value in + guard let self else { + return + } + if !item.controllerInteraction.toggleSelection(item.result, value) { + self.checkNode?.setSelected(false, animated: false) + } } self.addSubnode(checkNode) self.checkNode = checkNode