Permission and search UI improvements

This commit is contained in:
Ali 2023-01-17 23:02:57 +04:00
parent e67f7316ef
commit 6d8c7243e5
45 changed files with 1585 additions and 303 deletions

View File

@ -8696,3 +8696,6 @@ Sorry for the inconvenience.";
"Conversation.ViewInChannel" = "View in Channel"; "Conversation.ViewInChannel" = "View in Channel";
"Conversation.HoldForAudioOnly" = "Hold to record audio.";
"Conversation.HoldForVideoOnly" = "Hold to record video.";

View File

@ -477,7 +477,7 @@ public final class AuthorizationSequenceController: NavigationController, MFMail
} }
} else { } else {
controller?.inProgress = true 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 |> deliverOnMainQueue).start(next: { result in
controller?.inProgress = false controller?.inProgress = false
}, error: { error in }, error: { error in

View File

@ -621,11 +621,13 @@ final class ChatItemGalleryFooterContentNode: GalleryFooterContentNode, UIScroll
var canFullscreen = false var canFullscreen = false
var canEdit = false var canEdit = false
var isImage = false
var isVideo = false
for media in message.media { for media in message.media {
if media is TelegramMediaImage { if media is TelegramMediaImage {
canEdit = true canEdit = true
isImage = true
} else if let media = media as? TelegramMediaFile, !media.isAnimated { } else if let media = media as? TelegramMediaFile, !media.isAnimated {
var isVideo = false
for attribute in media.attributes { for attribute in media.attributes {
switch attribute { switch attribute {
case let .Video(_, dimensions, _): case let .Video(_, dimensions, _):
@ -666,7 +668,19 @@ final class ChatItemGalleryFooterContentNode: GalleryFooterContentNode, UIScroll
} else if let channel = peer as? TelegramChannel { } else if let channel = peer as? TelegramChannel {
if message.flags.contains(.Incoming) { if message.flags.contains(.Incoming) {
canDelete = channel.hasPermission(.deleteAllMessages) 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 { } else {
canDelete = true canDelete = true
} }

View File

@ -8,7 +8,7 @@
@property (nonatomic, copy) void (^pressed)(void); @property (nonatomic, copy) void (^pressed)(void);
@property (nonatomic, strong) TGMenuSheetPallete *pallete; @property (nonatomic, strong) TGMenuSheetPallete *pallete;
- (instancetype)initForSelfPortrait:(bool)selfPortrait; - (instancetype)initForSelfPortrait:(bool)selfPortrait videoModeByDefault:(bool)videoModeByDefault;
@property (nonatomic, readonly) bool previewViewAttached; @property (nonatomic, readonly) bool previewViewAttached;
- (void)detachPreviewView; - (void)detachPreviewView;

View File

@ -19,7 +19,8 @@ typedef enum {
TGCameraControllerPassportMultipleIntent, TGCameraControllerPassportMultipleIntent,
TGCameraControllerAvatarIntent, TGCameraControllerAvatarIntent,
TGCameraControllerSignupAvatarIntent, TGCameraControllerSignupAvatarIntent,
TGCameraControllerGenericPhotoOnlyIntent TGCameraControllerGenericPhotoOnlyIntent,
TGCameraControllerGenericVideoOnlyIntent
} TGCameraControllerIntent; } TGCameraControllerIntent;
@interface TGCameraControllerWindow : TGOverlayControllerWindow @interface TGCameraControllerWindow : TGOverlayControllerWindow

View File

@ -67,7 +67,7 @@
@property (nonatomic, assign) CGRect previewViewFrame; @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)setDocumentFrameHidden:(bool)hidden;
- (void)setCameraMode:(PGCameraMode)mode; - (void)setCameraMode:(PGCameraMode)mode;

View File

@ -10,6 +10,6 @@
- (void)setHidden:(bool)hidden animated:(bool)animated; - (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 @end

View File

@ -13,6 +13,7 @@
@property (nonatomic, readonly) bool allowGrouping; @property (nonatomic, readonly) bool allowGrouping;
@property (nonatomic, readonly) int selectionLimit; @property (nonatomic, readonly) int selectionLimit;
@property (nonatomic, copy) void (^selectionLimitExceeded)(void); @property (nonatomic, copy) void (^selectionLimitExceeded)(void);
@property (nonatomic, copy) bool (^attemptSelectingItem)(id<TGMediaSelectableItem>);
@property (nonatomic, assign) bool grouping; @property (nonatomic, assign) bool grouping;
- (SSignal *)groupingChangedSignal; - (SSignal *)groupingChangedSignal;

View File

@ -33,7 +33,7 @@
@implementation TGAttachmentCameraView @implementation TGAttachmentCameraView
- (instancetype)initForSelfPortrait:(bool)selfPortrait - (instancetype)initForSelfPortrait:(bool)selfPortrait videoModeByDefault:(bool)videoModeByDefault
{ {
self = [super initWithFrame:CGRectZero]; self = [super initWithFrame:CGRectZero];
if (self != nil) if (self != nil)
@ -46,7 +46,7 @@
PGCamera *camera = nil; PGCamera *camera = nil;
if ([PGCamera cameraAvailable]) 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; _camera = camera;

View File

@ -230,7 +230,7 @@ const NSUInteger TGAttachmentDisplayedAssetLimit = 500;
if (hasCamera) 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.frame = CGRectMake(_smallLayout.minimumLineSpacing, 0, TGAttachmentCellSize.width, TGAttachmentCellSize.height);
[_cameraView startPreview]; [_cameraView startPreview];

View File

@ -170,8 +170,12 @@ static CGPoint TGCameraControllerClampPointToScreenSize(__unused id self, __unus
_items = [[NSMutableArray alloc] init]; _items = [[NSMutableArray alloc] init];
if (_intent != TGCameraControllerGenericIntent) if (_intent != TGCameraControllerGenericIntent) {
_allowCaptions = false; _allowCaptions = false;
}
if (_intent == TGCameraControllerGenericVideoOnlyIntent || _intent == TGCameraControllerGenericPhotoOnlyIntent) {
_allowCaptions = false;
}
_saveEditedPhotos = saveEditedPhotos; _saveEditedPhotos = saveEditedPhotos;
_saveCapturedMedia = saveCapturedMedia; _saveCapturedMedia = saveCapturedMedia;
@ -303,12 +307,12 @@ static CGPoint TGCameraControllerClampPointToScreenSize(__unused id self, __unus
if ([[UIDevice currentDevice] userInterfaceIdiom] == UIUserInterfaceIdiomPhone) 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]; [_interfaceView setInterfaceOrientation:interfaceOrientation animated:false];
} }
else 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]; [_interfaceView setInterfaceOrientation:interfaceOrientation animated:false];
CGSize referenceSize = [self referenceViewSizeForOrientation:interfaceOrientation]; 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]; [_interfaceView setHasModeControl:false];
}
if (_intent == TGCameraControllerGenericVideoOnlyIntent || _intent == TGCameraControllerGenericPhotoOnlyIntent) {
[_interfaceView setHasModeControl:false];
}
if (@available(iOS 11.0, *)) { if (@available(iOS 11.0, *)) {
_backgroundView.accessibilityIgnoresInvertColors = true; _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]; 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.inhibitMute = self.inhibitMute;
model.controller = galleryController; model.controller = galleryController;

View File

@ -101,7 +101,7 @@
@synthesize cancelPressed; @synthesize cancelPressed;
@synthesize actionHandle = _actionHandle; @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]; self = [super initWithFrame:frame];
if (self != nil) if (self != nil)
@ -314,7 +314,7 @@
[_shutterButton addGestureRecognizer:shutterPanGestureRecognizer]; [_shutterButton addGestureRecognizer:shutterPanGestureRecognizer];
[_bottomPanelView addSubview:_shutterButton]; [_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]; [_bottomPanelView addSubview:_modeControl];
_flipButton = [[TGCameraFlipButton alloc] initWithFrame:CGRectMake(0, 0, 48, 48)]; _flipButton = [[TGCameraFlipButton alloc] initWithFrame:CGRectMake(0, 0, 48, 48)];
@ -414,6 +414,12 @@
[_photoCounterButton addTarget:self action:@selector(photoCounterButtonPressed) forControlEvents:UIControlEventTouchUpInside]; [_photoCounterButton addTarget:self action:@selector(photoCounterButtonPressed) forControlEvents:UIControlEventTouchUpInside];
_photoCounterButton.userInteractionEnabled = false; _photoCounterButton.userInteractionEnabled = false;
[_bottomPanelView addSubview:_photoCounterButton]; [_bottomPanelView addSubview:_photoCounterButton];
if (videoModeByDefault) {
[UIView performWithoutAnimation:^{
[self updateForCameraModeChangeWithPreviousMode:PGCameraModePhoto];
}];
}
} }
return self; return self;
} }

View File

@ -42,7 +42,7 @@ const CGFloat TGCameraTabletPanelViewWidth = 102.0f;
@synthesize shutterReleased; @synthesize shutterReleased;
@synthesize cancelPressed; @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]; self = [super initWithFrame:frame];
if (self != nil) if (self != nil)
@ -83,7 +83,7 @@ const CGFloat TGCameraTabletPanelViewWidth = 102.0f;
[_shutterButton addTarget:self action:@selector(shutterButtonReleased) forControlEvents:UIControlEventTouchUpInside]; [_shutterButton addTarget:self action:@selector(shutterButtonReleased) forControlEvents:UIControlEventTouchUpInside];
[_panelView addSubview:_shutterButton]; [_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]; [_panelView addSubview:_modeControl];
__weak TGCameraMainTabletView *weakSelf = self; __weak TGCameraMainTabletView *weakSelf = self;

View File

@ -21,7 +21,7 @@ const CGFloat TGCameraModeControlVerticalInteritemSpace = 29.0f;
@implementation TGCameraModeControl @implementation TGCameraModeControl
- (instancetype)initWithFrame:(CGRect)frame avatar:(bool)avatar - (instancetype)initWithFrame:(CGRect)frame avatar:(bool)avatar videoModeByDefault:(bool)videoModeByDefault
{ {
self = [super initWithFrame:frame]; self = [super initWithFrame:frame];
if (self != nil) if (self != nil)
@ -87,7 +87,11 @@ const CGFloat TGCameraModeControlVerticalInteritemSpace = 29.0f;
_wrapperView.frame = CGRectMake(33, 0, self.frame.size.width, topOffset - TGCameraModeControlVerticalInteritemSpace); _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; return self;
} }

View File

@ -72,6 +72,12 @@
{ {
if (![(id)item conformsToProtocol:@protocol(TGMediaSelectableItem)]) if (![(id)item conformsToProtocol:@protocol(TGMediaSelectableItem)])
return false; return false;
if (_attemptSelectingItem) {
if (!_attemptSelectingItem(item)) {
return false;
}
}
NSString *identifier = item.uniqueIdentifier; NSString *identifier = item.uniqueIdentifier;
if (selected) if (selected)

View File

@ -160,7 +160,9 @@ final class MediaPickerGridItemNode: GridItemNode {
let checkNode = InteractiveCheckNode(theme: CheckNodeTheme(theme: theme, style: .overlay)) let checkNode = InteractiveCheckNode(theme: CheckNodeTheme(theme: theme, style: .overlay))
checkNode.valueChanged = { [weak self] value in checkNode.valueChanged = { [weak self] value in
if let strongSelf = self, let interaction = strongSelf.interaction, let selectableItem = strongSelf.selectableItem { 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) self.addSubnode(checkNode)

View File

@ -25,7 +25,7 @@ import MoreButtonNode
final class MediaPickerInteraction { final class MediaPickerInteraction {
let openMedia: (PHFetchResult<PHAsset>, Int, UIImage?) -> Void let openMedia: (PHFetchResult<PHAsset>, Int, UIImage?) -> Void
let openSelectedMedia: (TGMediaSelectableItem, 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 sendSelected: (TGMediaSelectableItem?, Bool, Int32?, Bool, @escaping () -> Void) -> Void
let schedule: () -> Void let schedule: () -> Void
let dismissInput: () -> Void let dismissInput: () -> Void
@ -33,7 +33,7 @@ final class MediaPickerInteraction {
let editingState: TGMediaEditingContext let editingState: TGMediaEditingContext
var hiddenMediaId: String? var hiddenMediaId: String?
init(openMedia: @escaping (PHFetchResult<PHAsset>, 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<PHAsset>, 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.openMedia = openMedia
self.openSelectedMedia = openSelectedMedia self.openSelectedMedia = openSelectedMedia
self.toggleSelection = toggleSelection self.toggleSelection = toggleSelection
@ -403,7 +403,7 @@ public final class MediaPickerScreen: ViewController, AttachmentContainable {
} }
if let controller = self.controller, case .assets(nil) = controller.subject { 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.clipsToBounds = true
cameraView.removeCorners() cameraView.removeCorners()
cameraView.pressed = { [weak self] in cameraView.pressed = { [weak self] in
@ -946,7 +946,13 @@ public final class MediaPickerScreen: ViewController, AttachmentContainable {
if cameraAccess == nil { if cameraAccess == nil {
cameraRect = 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 self.gridNode.isHidden = true
let banDescription: String let banDescription: String
@ -973,7 +979,7 @@ public final class MediaPickerScreen: ViewController, AttachmentContainable {
placeholderTransition.updateFrame(node: placeholderNode, frame: innerBounds) placeholderTransition.updateFrame(node: placeholderNode, frame: innerBounds)
self.updateNavigation(transition: .immediate) self.updateNavigation(transition: .immediate)
} else */if case .notDetermined = mediaAccess { } else if case .notDetermined = mediaAccess {
} else { } else {
if case .limited = mediaAccess { if case .limited = mediaAccess {
let manageNode: MediaPickerManageNode 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 { if case let .noAccess(cameraAccess) = self.state {
var placeholderTransition = transition var placeholderTransition = transition
let placeholderNode: MediaPickerPlaceholderNode 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) placeholderNode.update(layout: layout, theme: self.presentationData.theme, strings: self.presentationData.strings, hasCamera: cameraAccess == .authorized, transition: placeholderTransition)
placeholderTransition.updateFrame(node: placeholderNode, frame: innerBounds) 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 self.placeholderNode = nil
placeholderNode.removeFromSupernode() placeholderNode.removeFromSupernode()
} }
@ -1161,6 +1172,45 @@ public final class MediaPickerScreen: ViewController, AttachmentContainable {
self.statusBar.statusBarStyle = .Ignore 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) self.presentationDataDisposable = ((updatedPresentationData?.signal ?? context.sharedContext.presentationData)
|> deliverOnMainQueue).start(next: { [weak self] presentationData in |> deliverOnMainQueue).start(next: { [weak self] presentationData in
if let strongSelf = self { if let strongSelf = self {
@ -1223,7 +1273,39 @@ public final class MediaPickerScreen: ViewController, AttachmentContainable {
}, openSelectedMedia: { [weak self] item, immediateThumbnail in }, openSelectedMedia: { [weak self] item, immediateThumbnail in
self?.controllerNode.openSelectedMedia(item: item, immediateThumbnail: immediateThumbnail) self?.controllerNode.openSelectedMedia(item: item, immediateThumbnail: immediateThumbnail)
}, toggleSelection: { [weak self] item, value, suggestUndo in }, 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 var showUndo = false
if suggestUndo { if suggestUndo {
if !value { if !value {
@ -1237,8 +1319,12 @@ public final class MediaPickerScreen: ViewController, AttachmentContainable {
selectionState.setItem(item, selected: value) selectionState.setItem(item, selected: value)
if showUndo { if showUndo {
strongSelf.showSelectionUndo(item: item) self.showSelectionUndo(item: item)
} }
return true
} else {
return false
} }
}, sendSelected: { [weak self] currentItem, silently, scheduleTime, animated, completion in }, sendSelected: { [weak self] currentItem, silently, scheduleTime, animated, completion in
if let strongSelf = self, let selectionState = strongSelf.interaction?.selectionState, !strongSelf.isDismissing { if let strongSelf = self, let selectionState = strongSelf.interaction?.selectionState, !strongSelf.isDismissing {

View File

@ -254,7 +254,9 @@ private class MediaPickerSelectedItemNode: ASDisplayNode {
let checkNode = InteractiveCheckNode(theme: CheckNodeTheme(theme: theme, style: .overlay)) let checkNode = InteractiveCheckNode(theme: CheckNodeTheme(theme: theme, style: .overlay))
checkNode.valueChanged = { [weak self] value in checkNode.valueChanged = { [weak self] value in
if let strongSelf = self, let interaction = strongSelf.interaction, let selectableItem = strongSelf.asset as? TGMediaSelectableItem { 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) self.addSubnode(checkNode)

View File

@ -378,7 +378,7 @@ private struct ChannelPermissionsControllerState: Equatable {
func stringForGroupPermission(strings: PresentationStrings, right: TelegramChatBannedRightsFlags, isForum: Bool) -> String { func stringForGroupPermission(strings: PresentationStrings, right: TelegramChatBannedRightsFlags, isForum: Bool) -> String {
//TODO:localize //TODO:localize
if right.contains(.banSendMessages) { if right.contains(.banSendText) {
return strings.Channel_BanUser_PermissionSendMessages return strings.Channel_BanUser_PermissionSendMessages
} else if right.contains(.banSendMedia) { } else if right.contains(.banSendMedia) {
return strings.Channel_BanUser_PermissionSendMedia return strings.Channel_BanUser_PermissionSendMedia
@ -416,7 +416,7 @@ func stringForGroupPermission(strings: PresentationStrings, right: TelegramChatB
} }
func compactStringForGroupPermission(strings: PresentationStrings, right: TelegramChatBannedRightsFlags) -> String { func compactStringForGroupPermission(strings: PresentationStrings, right: TelegramChatBannedRightsFlags) -> String {
if right.contains(.banSendMessages) { if right.contains(.banSendText) {
return strings.GroupPermission_NoSendMessages return strings.GroupPermission_NoSendMessages
} else if right.contains(.banSendMedia) { } else if right.contains(.banSendMedia) {
return strings.GroupPermission_NoSendMedia return strings.GroupPermission_NoSendMedia
@ -440,7 +440,7 @@ func compactStringForGroupPermission(strings: PresentationStrings, right: Telegr
} }
private let internal_allPossibleGroupPermissionList: [(TelegramChatBannedRightsFlags, TelegramChannelPermission)] = [ private let internal_allPossibleGroupPermissionList: [(TelegramChatBannedRightsFlags, TelegramChannelPermission)] = [
(.banSendMessages, .banMembers), (.banSendText, .banMembers),
(.banSendMedia, .banMembers), (.banSendMedia, .banMembers),
(.banSendPhotos, .banMembers), (.banSendPhotos, .banMembers),
(.banSendVideos, .banMembers), (.banSendVideos, .banMembers),
@ -460,9 +460,8 @@ private let internal_allPossibleGroupPermissionList: [(TelegramChatBannedRightsF
public func allGroupPermissionList(peer: EnginePeer) -> [(TelegramChatBannedRightsFlags, TelegramChannelPermission)] { public func allGroupPermissionList(peer: EnginePeer) -> [(TelegramChatBannedRightsFlags, TelegramChannelPermission)] {
if case let .channel(channel) = peer, channel.flags.contains(.isForum) { if case let .channel(channel) = peer, channel.flags.contains(.isForum) {
return [ return [
(.banSendMessages, .banMembers), (.banSendText, .banMembers),
(.banSendMedia, .banMembers), (.banSendMedia, .banMembers),
(.banSendPolls, .banMembers),
(.banAddMembers, .banMembers), (.banAddMembers, .banMembers),
(.banPinMessages, .pinMessages), (.banPinMessages, .pinMessages),
(.banManageTopics, .manageTopics), (.banManageTopics, .manageTopics),
@ -470,9 +469,8 @@ public func allGroupPermissionList(peer: EnginePeer) -> [(TelegramChatBannedRigh
] ]
} else { } else {
return [ return [
(.banSendMessages, .banMembers), (.banSendText, .banMembers),
(.banSendMedia, .banMembers), (.banSendMedia, .banMembers),
(.banSendPolls, .banMembers),
(.banAddMembers, .banMembers), (.banAddMembers, .banMembers),
(.banPinMessages, .pinMessages), (.banPinMessages, .pinMessages),
(.banChangeInfo, .changeInfo) (.banChangeInfo, .changeInfo)
@ -490,6 +488,7 @@ public func banSendMediaSubList() -> [(TelegramChatBannedRightsFlags, TelegramCh
(.banSendVoice, .banMembers), (.banSendVoice, .banMembers),
(.banSendInstantVideos, .banMembers), (.banSendInstantVideos, .banMembers),
(.banEmbedLinks, .banMembers), (.banEmbedLinks, .banMembers),
(.banSendPolls, .banMembers),
] ]
} }
@ -500,13 +499,13 @@ let publicGroupRestrictedPermissions: TelegramChatBannedRightsFlags = [
func groupPermissionDependencies(_ right: TelegramChatBannedRightsFlags) -> TelegramChatBannedRightsFlags { func groupPermissionDependencies(_ right: TelegramChatBannedRightsFlags) -> TelegramChatBannedRightsFlags {
if right.contains(.banSendMedia) || banSendMediaSubList().contains(where: { $0.0 == right }) { if right.contains(.banSendMedia) || banSendMediaSubList().contains(where: { $0.0 == right }) {
return [.banSendMessages] return []
} else if right.contains(.banSendGifs) { } else if right.contains(.banSendGifs) {
return [.banSendMessages] return []
} else if right.contains(.banEmbedLinks) { } else if right.contains(.banEmbedLinks) {
return [.banSendMessages] return []
} else if right.contains(.banSendPolls) { } else if right.contains(.banSendPolls) {
return [.banSendMessages] return []
} else if right.contains(.banChangeInfo) { } else if right.contains(.banChangeInfo) {
return [] return []
} else if right.contains(.banAddMembers) { } else if right.contains(.banAddMembers) {

View File

@ -460,7 +460,7 @@ public final class ReactionContextNode: ASDisplayNode, UIScrollViewDelegate {
} else { } else {
strongSelf.stableEmptyResultEmoji = nil 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 { } else {
strongSelf.stableEmptyResultEmoji = nil strongSelf.stableEmptyResultEmoji = nil
} }

View File

@ -3,7 +3,10 @@ import Postbox
public enum TelegramChannelPermission { public enum TelegramChannelPermission {
case sendMessages case sendText
case sendPhoto
case sendVideo
case sendSomething
case pinMessages case pinMessages
case manageTopics case manageTopics
case createTopics case createTopics
@ -30,7 +33,7 @@ public extension TelegramChannel {
return true return true
} }
switch permission { switch permission {
case .sendMessages: case .sendText:
if case .broadcast = self.info { if case .broadcast = self.info {
if let adminRights = self.adminRights { if let adminRights = self.adminRights {
return adminRights.rights.contains(.canPostMessages) return adminRights.rights.contains(.canPostMessages)
@ -41,10 +44,80 @@ public extension TelegramChannel {
if let _ = self.adminRights { if let _ = self.adminRights {
return true return true
} }
if let bannedRights = self.bannedRights, bannedRights.flags.contains(.banSendMessages) { if let bannedRights = self.bannedRights, bannedRights.flags.contains(.banSendText) {
return false 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 false
} }
return true return true

View File

@ -15,6 +15,7 @@ extension TelegramChatBannedRights {
var apiBannedRights: Api.ChatBannedRights { var apiBannedRights: Api.ChatBannedRights {
var effectiveFlags = self.flags var effectiveFlags = self.flags
effectiveFlags.remove(.banSendMedia) effectiveFlags.remove(.banSendMedia)
effectiveFlags.remove(TelegramChatBannedRightsFlags(rawValue: 1 << 1))
return .chatBannedRights(flags: effectiveFlags.rawValue, untilDate: self.untilDate) return .chatBannedRights(flags: effectiveFlags.rawValue, untilDate: self.untilDate)
} }

View File

@ -79,7 +79,10 @@ public enum SendAuthorizationCodeResult {
func storeFutureLoginToken(accountManager: AccountManager<TelegramAccountManagerTypes>, token: Data) { func storeFutureLoginToken(accountManager: AccountManager<TelegramAccountManagerTypes>, token: Data) {
let _ = (accountManager.transaction { transaction -> Void in let _ = (accountManager.transaction { transaction -> Void in
var tokens = transaction.getStoredLoginTokens() var tokens = transaction.getStoredLoginTokens()
tokens.insert(token, at: 0)
#if DEBUG
tokens.removeAll()
#endif
var cloudValue: [Data] = [] var cloudValue: [Data] = []
if let list = NSUbiquitousKeyValueStore.default.object(forKey: "T_SLTokens") as? [String] { if let list = NSUbiquitousKeyValueStore.default.object(forKey: "T_SLTokens") as? [String] {
@ -95,6 +98,7 @@ func storeFutureLoginToken(accountManager: AccountManager<TelegramAccountManager
tokens.insert(data, at: 0) tokens.insert(data, at: 0)
} }
} }
tokens.insert(token, at: 0)
if tokens.count > 20 { if tokens.count > 20 {
tokens.removeLast(tokens.count - 20) tokens.removeLast(tokens.count - 20)
} }
@ -143,12 +147,20 @@ public func sendAuthorizationCode(accountManager: AccountManager<TelegramAccount
return Data(base64Encoded: stringData) return Data(base64Encoded: stringData)
} }
} }
#if DEBUG
cloudValue.removeAll()
#endif
return accountManager.transaction { transaction -> [Data] in return accountManager.transaction { transaction -> [Data] in
return transaction.getStoredLoginTokens() return transaction.getStoredLoginTokens()
} }
|> castError(AuthorizationCodeRequestError.self) |> castError(AuthorizationCodeRequestError.self)
|> mapToSignal { localAuthTokens -> Signal<SendAuthorizationCodeResult, AuthorizationCodeRequestError> in |> mapToSignal { localAuthTokens -> Signal<SendAuthorizationCodeResult, AuthorizationCodeRequestError> in
var authTokens = localAuthTokens var authTokens = localAuthTokens
#if DEBUG
authTokens.removeAll()
#endif
for data in cloudValue { for data in cloudValue {
if !authTokens.contains(data) { if !authTokens.contains(data) {
authTokens.insert(data, at: 0) authTokens.insert(data, at: 0)
@ -274,7 +286,7 @@ public func sendAuthorizationCode(accountManager: AccountManager<TelegramAccount
|> castError(AuthorizationCodeRequestError.self) |> castError(AuthorizationCodeRequestError.self)
|> mapToSignal { firebaseSecret -> Signal<SendAuthorizationCodeResult, AuthorizationCodeRequestError> in |> mapToSignal { firebaseSecret -> Signal<SendAuthorizationCodeResult, AuthorizationCodeRequestError> in
guard let firebaseSecret = firebaseSecret else { 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) 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<TelegramAccount
} }
|> castError(AuthorizationCodeRequestError.self) |> castError(AuthorizationCodeRequestError.self)
} else { } 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<TelegramAccount
} }
} }
private func internalResendAuthorizationCode(account: UnauthorizedAccount, number: String, hash: String, syncContacts: Bool) -> Signal<SendAuthorizationCodeResult, AuthorizationCodeRequestError> { private func internalResendAuthorizationCode(accountManager: AccountManager<TelegramAccountManagerTypes>, account: UnauthorizedAccount, number: String, apiId: Int32, apiHash: String, hash: String, syncContacts: Bool, firebaseSecretStream: Signal<[String: String], NoError>) -> Signal<SendAuthorizationCodeResult, AuthorizationCodeRequestError> {
return account.network.request(Api.functions.auth.resendCode(phoneNumber: number, phoneCodeHash: hash), automaticFloodWait: false) return account.network.request(Api.functions.auth.resendCode(phoneNumber: number, phoneCodeHash: hash), automaticFloodWait: false)
|> mapError { error -> AuthorizationCodeRequestError in |> mapError { error -> AuthorizationCodeRequestError in
if error.errorDescription.hasPrefix("FLOOD_WAIT") { if error.errorDescription.hasPrefix("FLOOD_WAIT") {
@ -349,60 +361,155 @@ private func internalResendAuthorizationCode(account: UnauthorizedAccount, numbe
} }
} }
|> mapToSignal { sentCode -> Signal<SendAuthorizationCodeResult, AuthorizationCodeRequestError> in |> mapToSignal { sentCode -> Signal<SendAuthorizationCodeResult, AuthorizationCodeRequestError> in
return account.postbox.transaction { transaction -> SendAuthorizationCodeResult in return account.postbox.transaction { transaction -> Signal<SendAuthorizationCodeResult, AuthorizationCodeRequestError> in
switch sentCode { switch sentCode {
case let .sentCode(_, type, phoneCodeHash, nextType, timeout): case let .sentCode(_, type, phoneCodeHash, nextType, codeTimeout):
var parsedNextType: AuthorizationCodeNextType? var parsedNextType: AuthorizationCodeNextType?
if let nextType = nextType { if let nextType = nextType {
parsedNextType = AuthorizationCodeNextType(apiType: 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<SendAuthorizationCodeResult, AuthorizationCodeRequestError> 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<Bool, SendFirebaseAuthorizationCodeError> in
return .single(false)
}
|> mapError { _ -> AuthorizationCodeRequestError in
return .generic(info: nil)
}
|> mapToSignal { success -> Signal<SendAuthorizationCodeResult, AuthorizationCodeRequestError> 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: case .sentCodeSuccess:
return .loggedIn return .single(.loggedIn)
} }
} |> mapError { _ -> AuthorizationCodeRequestError in } }
|> mapError { _ -> AuthorizationCodeRequestError in }
|> switchToLatest
} }
} }
public func resendAuthorizationCode(account: UnauthorizedAccount) -> Signal<Void, AuthorizationCodeRequestError> { public func resendAuthorizationCode(accountManager: AccountManager<TelegramAccountManagerTypes>, account: UnauthorizedAccount, apiId: Int32, apiHash: String, firebaseSecretStream: Signal<[String: String], NoError>) -> Signal<Void, AuthorizationCodeRequestError> {
return account.postbox.transaction { transaction -> Signal<Void, AuthorizationCodeRequestError> in return account.postbox.transaction { transaction -> Signal<Void, AuthorizationCodeRequestError> in
if let state = transaction.getState() as? UnauthorizedAccountState { if let state = transaction.getState() as? UnauthorizedAccountState {
switch state.contents { switch state.contents {
case let .confirmationCodeEntry(number, _, hash, _, nextType, syncContacts): case let .confirmationCodeEntry(number, _, hash, _, nextType, syncContacts):
if nextType != nil { if nextType != nil {
return account.network.request(Api.functions.auth.resendCode(phoneNumber: number, phoneCodeHash: hash), automaticFloodWait: false) return account.network.request(Api.functions.auth.resendCode(phoneNumber: number, phoneCodeHash: hash), automaticFloodWait: false)
|> mapError { error -> AuthorizationCodeRequestError in |> mapError { error -> AuthorizationCodeRequestError in
if error.errorDescription.hasPrefix("FLOOD_WAIT") { if error.errorDescription.hasPrefix("FLOOD_WAIT") {
return .limitExceeded return .limitExceeded
} else if error.errorDescription == "PHONE_NUMBER_INVALID" { } else if error.errorDescription == "PHONE_NUMBER_INVALID" {
return .invalidPhoneNumber return .invalidPhoneNumber
} else if error.errorDescription == "PHONE_NUMBER_FLOOD" { } else if error.errorDescription == "PHONE_NUMBER_FLOOD" {
return .phoneLimitExceeded return .phoneLimitExceeded
} else if error.errorDescription == "PHONE_NUMBER_BANNED" { } else if error.errorDescription == "PHONE_NUMBER_BANNED" {
return .phoneBanned return .phoneBanned
} else { } else {
return .generic(info: (Int(error.errorCode), error.errorDescription)) return .generic(info: (Int(error.errorCode), error.errorDescription))
}
} }
|> mapToSignal { sentCode -> Signal<Void, AuthorizationCodeRequestError> in }
return account.postbox.transaction { transaction -> Void in |> mapToSignal { sentCode -> Signal<Void, AuthorizationCodeRequestError> in
switch sentCode { return account.postbox.transaction { transaction -> Signal<Void, AuthorizationCodeRequestError> in
case let .sentCode(_, type, phoneCodeHash, nextType, timeout): switch sentCode {
case let .sentCode(_, type, phoneCodeHash, nextType, codeTimeout):
var parsedNextType: AuthorizationCodeNextType? var parsedNextType: AuthorizationCodeNextType?
if let nextType = nextType { if let nextType = nextType {
parsedNextType = AuthorizationCodeNextType(apiType: 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
} }
} |> 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<SendAuthorizationCodeResult, AuthorizationCodeRequestError> 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<Bool, SendFirebaseAuthorizationCodeError> in
return .single(false)
}
|> mapError { _ -> AuthorizationCodeRequestError in
return .generic(info: nil)
}
|> mapToSignal { success -> Signal<SendAuthorizationCodeResult, AuthorizationCodeRequestError> 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 { } else {
return .fail(.generic(info: nil)) return .fail(.generic(info: nil))
} }

View File

@ -8,7 +8,7 @@ public enum ServerProvidedSuggestion: String {
case newcomerTicks = "NEWCOMER_TICKS" case newcomerTicks = "NEWCOMER_TICKS"
case validatePhoneNumber = "VALIDATE_PHONE_NUMBER" case validatePhoneNumber = "VALIDATE_PHONE_NUMBER"
case validatePassword = "VALIDATE_PASSWORD" case validatePassword = "VALIDATE_PASSWORD"
case setupPassword = "SETUP_2FA" case setupPassword = "SETUP_PASSWORD"
} }
private var dismissedSuggestionsPromise = ValuePromise<[AccountRecordId: Set<ServerProvidedSuggestion>]>([:]) private var dismissedSuggestionsPromise = ValuePromise<[AccountRecordId: Set<ServerProvidedSuggestion>]>([:])
@ -33,12 +33,7 @@ public func getServerProvidedSuggestions(account: Account) -> Signal<[ServerProv
return [] return []
} }
#if DEBUG
var list = listItems
list.append(ServerProvidedSuggestion.setupPassword.rawValue)
#else
let list = listItems let list = listItems
#endif
return list.compactMap { item -> ServerProvidedSuggestion? in return list.compactMap { item -> ServerProvidedSuggestion? in
return ServerProvidedSuggestion(rawValue: item) return ServerProvidedSuggestion(rawValue: item)

View File

@ -12,7 +12,6 @@ public struct TelegramChatBannedRightsFlags: OptionSet, Hashable {
} }
public static let banReadMessages = TelegramChatBannedRightsFlags(rawValue: 1 << 0) 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 banSendMedia = TelegramChatBannedRightsFlags(rawValue: 1 << 2)
public static let banSendStickers = TelegramChatBannedRightsFlags(rawValue: 1 << 3) public static let banSendStickers = TelegramChatBannedRightsFlags(rawValue: 1 << 3)
public static let banSendGifs = TelegramChatBannedRightsFlags(rawValue: 1 << 4) 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 banSendMusic = TelegramChatBannedRightsFlags(rawValue: 1 << 22)
public static let banSendVoice = TelegramChatBannedRightsFlags(rawValue: 1 << 23) public static let banSendVoice = TelegramChatBannedRightsFlags(rawValue: 1 << 23)
public static let banSendFiles = TelegramChatBannedRightsFlags(rawValue: 1 << 24) public static let banSendFiles = TelegramChatBannedRightsFlags(rawValue: 1 << 24)
public static let banSendText = TelegramChatBannedRightsFlags(rawValue: 1 << 25)
} }
public struct TelegramChatBannedRights: PostboxCoding, Equatable { public struct TelegramChatBannedRights: PostboxCoding, Equatable {

View File

@ -14,7 +14,7 @@ public func canSendMessagesToPeer(_ peer: Peer) -> Bool {
} else if let peer = peer as? TelegramSecretChat { } else if let peer = peer as? TelegramSecretChat {
return peer.embeddedState == .active return peer.embeddedState == .active
} else if let peer = peer as? TelegramChannel { } else if let peer = peer as? TelegramChannel {
return peer.hasPermission(.sendMessages) return peer.hasPermission(.sendSomething)
} else { } else {
return false return false
} }

View File

@ -194,7 +194,7 @@ final class AvatarEditorScreenComponent: Component {
private func updateData(_ data: KeyboardInputData) { private func updateData(_ data: KeyboardInputData) {
self.data = data 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) self.state?.updated(transition: .immediate)
let updateSearchQuery: (String, String) -> Void = { [weak self] rawQuery, languageCode in let updateSearchQuery: (String, String) -> Void = { [weak self] rawQuery, languageCode in
@ -735,9 +735,9 @@ final class AvatarEditorScreenComponent: Component {
} }
if state?.keyboardContentId == AnyHashable("emoji") { 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 { } 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)
} }
} }

View File

@ -1196,7 +1196,7 @@ public final class ChatEntityKeyboardInputNode: ChatInputNode {
iconFile: nil 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 var transition: Transition = .immediate
@ -1586,9 +1586,9 @@ public final class ChatEntityKeyboardInputNode: ChatInputNode {
private func processInputData(inputData: InputData) -> InputData { private func processInputData(inputData: InputData) -> InputData {
return 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 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, gifs: inputData.gifs,
availableGifSearchEmojies: inputData.availableGifSearchEmojies availableGifSearchEmojies: inputData.availableGifSearchEmojies

View File

@ -371,7 +371,7 @@ public final class EmojiStatusSelectionController: ViewController {
} else { } else {
strongSelf.stableEmptyResultEmoji = nil 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 { } else {
strongSelf.stableEmptyResultEmoji = nil strongSelf.stableEmptyResultEmoji = nil
} }

View File

@ -1520,8 +1520,10 @@ public final class EmojiSearchHeaderView: UIView, UITextFieldDelegate {
var text: String var text: String
var useOpaqueTheme: Bool var useOpaqueTheme: Bool
var isActive: Bool var isActive: Bool
var hasPresetSearch: Bool
var size: CGSize var size: CGSize
var canFocus: Bool var canFocus: Bool
var hasSearchItems: Bool
static func ==(lhs: Params, rhs: Params) -> Bool { static func ==(lhs: Params, rhs: Params) -> Bool {
if lhs.theme !== rhs.theme { if lhs.theme !== rhs.theme {
@ -1539,12 +1541,18 @@ public final class EmojiSearchHeaderView: UIView, UITextFieldDelegate {
if lhs.isActive != rhs.isActive { if lhs.isActive != rhs.isActive {
return false return false
} }
if lhs.hasPresetSearch != rhs.hasPresetSearch {
return false
}
if lhs.size != rhs.size { if lhs.size != rhs.size {
return false return false
} }
if lhs.canFocus != rhs.canFocus { if lhs.canFocus != rhs.canFocus {
return false return false
} }
if lhs.hasSearchItems != rhs.hasSearchItems {
return false
}
return true return true
} }
} }
@ -1565,6 +1573,9 @@ public final class EmojiSearchHeaderView: UIView, UITextFieldDelegate {
private let searchIconView: UIImageView private let searchIconView: UIImageView
private let searchIconTintView: UIImageView private let searchIconTintView: UIImageView
private let backIconView: UIImageView
private let backIconTintView: UIImageView
private let clearIconView: UIImageView private let clearIconView: UIImageView
private let clearIconTintView: UIImageView private let clearIconTintView: UIImageView
private let clearIconButton: HighlightTrackingButton private let clearIconButton: HighlightTrackingButton
@ -1575,9 +1586,12 @@ public final class EmojiSearchHeaderView: UIView, UITextFieldDelegate {
private let cancelButtonTitle: ComponentView<Empty> private let cancelButtonTitle: ComponentView<Empty>
private let cancelButton: HighlightTrackingButton private let cancelButton: HighlightTrackingButton
private var suggestedItemsView: ComponentView<Empty>?
private var textField: EmojiSearchTextField? private var textField: EmojiSearchTextField?
private var tapRecognizer: UITapGestureRecognizer? private var tapRecognizer: UITapGestureRecognizer?
private(set) var currentPresetSearchTerm: String?
private var params: Params? private var params: Params?
@ -1598,6 +1612,9 @@ public final class EmojiSearchHeaderView: UIView, UITextFieldDelegate {
self.searchIconView = UIImageView() self.searchIconView = UIImageView()
self.searchIconTintView = UIImageView() self.searchIconTintView = UIImageView()
self.backIconView = UIImageView()
self.backIconTintView = UIImageView()
self.clearIconView = UIImageView() self.clearIconView = UIImageView()
self.clearIconTintView = UIImageView() self.clearIconTintView = UIImageView()
self.clearIconButton = HighlightableButton() self.clearIconButton = HighlightableButton()
@ -1619,6 +1636,9 @@ public final class EmojiSearchHeaderView: UIView, UITextFieldDelegate {
self.addSubview(self.searchIconView) self.addSubview(self.searchIconView)
self.tintContainerView.addSubview(self.searchIconTintView) self.tintContainerView.addSubview(self.searchIconTintView)
self.addSubview(self.backIconView)
self.tintContainerView.addSubview(self.backIconTintView)
self.addSubview(self.clearIconView) self.addSubview(self.clearIconView)
self.tintContainerView.addSubview(self.clearIconTintView) self.tintContainerView.addSubview(self.clearIconTintView)
self.addSubview(self.clearIconButton) self.addSubview(self.clearIconButton)
@ -1681,25 +1701,38 @@ public final class EmojiSearchHeaderView: UIView, UITextFieldDelegate {
@objc private func tapGesture(_ recognizer: UITapGestureRecognizer) { @objc private func tapGesture(_ recognizer: UITapGestureRecognizer) {
if case .ended = recognizer.state { if case .ended = recognizer.state {
if self.textField == nil, let textComponentView = self.textView.view, self.params?.canFocus == true { let location = recognizer.location(in: self)
let backgroundFrame = self.backgroundLayer.frame if self.backIconView.frame.contains(location) {
let textFieldFrame = CGRect(origin: CGPoint(x: textComponentView.frame.minX, y: backgroundFrame.minY), size: CGSize(width: backgroundFrame.maxX - textComponentView.frame.minX, height: backgroundFrame.height)) 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) self.currentPresetSearchTerm = nil
textField.autocorrectionType = .no if let suggestedItemsView = self.suggestedItemsView?.view as? EmojiSearchSearchBarComponent.View {
self.textField = textField suggestedItemsView.clearSelection(dispatchEvent: false)
self.insertSubview(textField, belowSubview: self.clearIconView) }
textField.delegate = self
textField.addTarget(self, action: #selector(self.textFieldChanged(_:)), for: .editingChanged) self.activated()
self.textField?.becomeFirstResponder()
} }
self.activated()
self.textField?.becomeFirstResponder()
} }
} }
@objc private func cancelPressed() { @objc private func cancelPressed() {
self.currentPresetSearchTerm = nil
self.updateQuery("", "en") self.updateQuery("", "en")
self.clearIconView.isHidden = true self.clearIconView.isHidden = true
@ -1720,6 +1753,7 @@ public final class EmojiSearchHeaderView: UIView, UITextFieldDelegate {
} }
@objc private func clearPressed() { @objc private func clearPressed() {
self.currentPresetSearchTerm = nil
self.updateQuery("", "en") self.updateQuery("", "en")
self.textField?.text = "" self.textField?.text = ""
@ -1767,6 +1801,7 @@ public final class EmojiSearchHeaderView: UIView, UITextFieldDelegate {
self.clearIconTintView.isHidden = text.isEmpty self.clearIconTintView.isHidden = text.isEmpty
self.clearIconButton.isHidden = text.isEmpty self.clearIconButton.isHidden = text.isEmpty
self.currentPresetSearchTerm = nil
self.updateQuery(text, inputLanguage) self.updateQuery(text, inputLanguage)
} }
@ -1775,28 +1810,37 @@ public final class EmojiSearchHeaderView: UIView, UITextFieldDelegate {
return return
} }
self.params = nil 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( let params = Params(
theme: theme, theme: theme,
strings: strings, strings: strings,
text: text, text: text,
useOpaqueTheme: useOpaqueTheme, useOpaqueTheme: useOpaqueTheme,
isActive: isActive, isActive: isActive,
hasPresetSearch: self.currentPresetSearchTerm == nil,
size: size, size: size,
canFocus: canFocus canFocus: canFocus,
hasSearchItems: hasSearchItems
) )
if self.params == params { if self.params == params {
return return
} }
let isActiveWithText = isActive && self.currentPresetSearchTerm == nil
let isLeftAligned = isActiveWithText || hasSearchItems
if self.params?.theme !== theme { if self.params?.theme !== theme {
self.searchIconView.image = generateTintedImage(image: UIImage(bundleImageName: "Components/Search Bar/Loupe"), color: theme.chat.inputMediaPanel.panelContentVibrantOverlayColor) 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.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.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) 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 let cancelButtonSpacing: CGFloat = 8.0
var backgroundFrame = CGRect(origin: CGPoint(x: sideInset, y: topInset), size: CGSize(width: size.width - sideInset * 2.0, height: inputHeight)) 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 backgroundFrame.size.width -= cancelTextSize.width + cancelButtonSpacing
} }
transition.setFrame(layer: self.backgroundLayer, frame: backgroundFrame) 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))) 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) 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 textFrame.origin.x = backgroundFrame.minX + sideTextInset
} }
if let image = self.searchIconView.image { 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) 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.setBounds(view: self.searchIconView, bounds: CGRect(origin: CGPoint(), size: iconFrame.size))
transition.setFrame(view: self.searchIconTintView, frame: iconFrame) 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<Empty>
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 { if let image = self.clearIconView.image {
@ -2389,7 +2515,8 @@ public final class EmojiPagerContentComponent: Component {
public let animationCache: AnimationCache public let animationCache: AnimationCache
public let animationRenderer: MultiAnimationRenderer public let animationRenderer: MultiAnimationRenderer
public let inputInteractionHolder: InputInteractionHolder public let inputInteractionHolder: InputInteractionHolder
public let itemGroups: [ItemGroup] public let panelItemGroups: [ItemGroup]
public let contentItemGroups: [ItemGroup]
public let itemLayoutType: ItemLayoutType public let itemLayoutType: ItemLayoutType
public let itemContentUniqueId: AnyHashable? public let itemContentUniqueId: AnyHashable?
public let warpContentsOnEdges: Bool public let warpContentsOnEdges: Bool
@ -2407,7 +2534,8 @@ public final class EmojiPagerContentComponent: Component {
animationCache: AnimationCache, animationCache: AnimationCache,
animationRenderer: MultiAnimationRenderer, animationRenderer: MultiAnimationRenderer,
inputInteractionHolder: InputInteractionHolder, inputInteractionHolder: InputInteractionHolder,
itemGroups: [ItemGroup], panelItemGroups: [ItemGroup],
contentItemGroups: [ItemGroup],
itemLayoutType: ItemLayoutType, itemLayoutType: ItemLayoutType,
itemContentUniqueId: AnyHashable?, itemContentUniqueId: AnyHashable?,
warpContentsOnEdges: Bool, warpContentsOnEdges: Bool,
@ -2424,7 +2552,8 @@ public final class EmojiPagerContentComponent: Component {
self.animationCache = animationCache self.animationCache = animationCache
self.animationRenderer = animationRenderer self.animationRenderer = animationRenderer
self.inputInteractionHolder = inputInteractionHolder self.inputInteractionHolder = inputInteractionHolder
self.itemGroups = itemGroups self.panelItemGroups = panelItemGroups
self.contentItemGroups = contentItemGroups
self.itemLayoutType = itemLayoutType self.itemLayoutType = itemLayoutType
self.itemContentUniqueId = itemContentUniqueId self.itemContentUniqueId = itemContentUniqueId
self.warpContentsOnEdges = warpContentsOnEdges self.warpContentsOnEdges = warpContentsOnEdges
@ -2436,7 +2565,7 @@ public final class EmojiPagerContentComponent: Component {
self.selectedItems = selectedItems 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( return EmojiPagerContentComponent(
id: self.id, id: self.id,
context: self.context, context: self.context,
@ -2444,7 +2573,8 @@ public final class EmojiPagerContentComponent: Component {
animationCache: self.animationCache, animationCache: self.animationCache,
animationRenderer: self.animationRenderer, animationRenderer: self.animationRenderer,
inputInteractionHolder: self.inputInteractionHolder, inputInteractionHolder: self.inputInteractionHolder,
itemGroups: itemGroups, panelItemGroups: panelItemGroups,
contentItemGroups: contentItemGroups,
itemLayoutType: self.itemLayoutType, itemLayoutType: self.itemLayoutType,
itemContentUniqueId: itemContentUniqueId, itemContentUniqueId: itemContentUniqueId,
warpContentsOnEdges: self.warpContentsOnEdges, warpContentsOnEdges: self.warpContentsOnEdges,
@ -2479,7 +2609,10 @@ public final class EmojiPagerContentComponent: Component {
if lhs.inputInteractionHolder !== rhs.inputInteractionHolder { if lhs.inputInteractionHolder !== rhs.inputInteractionHolder {
return false return false
} }
if lhs.itemGroups != rhs.itemGroups { if lhs.panelItemGroups != rhs.panelItemGroups {
return false
}
if lhs.contentItemGroups != rhs.contentItemGroups {
return false return false
} }
if lhs.itemLayoutType != rhs.itemLayoutType { if lhs.itemLayoutType != rhs.itemLayoutType {
@ -4148,7 +4281,7 @@ public final class EmojiPagerContentComponent: Component {
var subgroupItemIndex: Int? var subgroupItemIndex: Int?
if group.supergroupId == supergroupId { if group.supergroupId == supergroupId {
if let subgroupId = subgroupId { if let subgroupId = subgroupId {
inner: for itemGroup in component.itemGroups { inner: for itemGroup in component.contentItemGroups {
if itemGroup.supergroupId == supergroupId { if itemGroup.supergroupId == supergroupId {
for i in 0 ..< itemGroup.items.count { for i in 0 ..< itemGroup.items.count {
if itemGroup.items[i].subgroupId == subgroupId { if itemGroup.items[i].subgroupId == subgroupId {
@ -4858,7 +4991,7 @@ public final class EmojiPagerContentComponent: Component {
self.updateScrollingOffset(isReset: false, transition: .immediate) self.updateScrollingOffset(isReset: false, transition: .immediate)
if self.isSearchActivated { if self.isSearchActivated, let visibleSearchHeader = self.visibleSearchHeader, visibleSearchHeader.currentPresetSearchTerm == nil {
self.visibleSearchHeader?.deactivate() self.visibleSearchHeader?.deactivate()
} }
} }
@ -4884,9 +5017,9 @@ public final class EmojiPagerContentComponent: Component {
return return
} }
let isInteracting = scrollView.isDragging || scrollView.isDecelerating let isInteracting = self.scrollView.isDragging || self.scrollView.isDecelerating
if let previousScrollingOffsetValue = self.previousScrollingOffset, !self.keepTopPanelVisibleUntilScrollingInput { if let previousScrollingOffsetValue = self.previousScrollingOffset, !self.keepTopPanelVisibleUntilScrollingInput, !self.isSearchActivated {
let currentBounds = scrollView.bounds let currentBounds = self.scrollView.bounds
let offsetToTopEdge = max(0.0, currentBounds.minY - 0.0) let offsetToTopEdge = max(0.0, currentBounds.minY - 0.0)
let offsetToBottomEdge = max(0.0, scrollView.contentSize.height - currentBounds.maxY) 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) { 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] let itemGroupLayout = itemLayout.itemGroupLayouts[groupItems.groupIndex]
var assignTopVisibleSubgroupId = false var assignTopVisibleSubgroupId = false
@ -5875,11 +6008,11 @@ public final class EmojiPagerContentComponent: Component {
var previousAbsoluteItemPositions: [VisualItemKey: CGPoint] = [:] var previousAbsoluteItemPositions: [VisualItemKey: CGPoint] = [:]
var anchorItems: [ItemLayer.Key: CGRect] = [:] 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 { if !transition.animation.isImmediate {
var previousItemPositionsValue: [VisualItemKey: CGPoint] = [:] var previousItemPositionsValue: [VisualItemKey: CGPoint] = [:]
for groupIndex in 0 ..< previousComponent.itemGroups.count { for groupIndex in 0 ..< previousComponent.contentItemGroups.count {
let itemGroup = previousComponent.itemGroups[groupIndex] let itemGroup = previousComponent.contentItemGroups[groupIndex]
for itemIndex in 0 ..< itemGroup.items.count { for itemIndex in 0 ..< itemGroup.items.count {
let item = itemGroup.items[itemIndex] let item = itemGroup.items[itemIndex]
let itemKey: ItemLayer.Key let itemKey: ItemLayer.Key
@ -5976,7 +6109,7 @@ public final class EmojiPagerContentComponent: Component {
} }
var itemGroups: [ItemGroupDescription] = [] var itemGroups: [ItemGroupDescription] = []
for itemGroup in component.itemGroups { for itemGroup in component.contentItemGroups {
itemGroups.append(ItemGroupDescription( itemGroups.append(ItemGroupDescription(
supergroupId: itemGroup.supergroupId, supergroupId: itemGroup.supergroupId,
groupId: itemGroup.groupId, groupId: itemGroup.groupId,
@ -6021,15 +6154,14 @@ public final class EmojiPagerContentComponent: Component {
let scrollOriginY: CGFloat = 0.0 let scrollOriginY: CGFloat = 0.0
let scrollSize = CGSize(width: availableSize.width, height: availableSize.height) let scrollSize = CGSize(width: availableSize.width, height: availableSize.height)
transition.setPosition(view: self.scrollView, position: CGPoint(x: 0.0, y: scrollOriginY)) 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.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.searchHeight : 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.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.searchHeight : 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 let previousSize = self.scrollView.bounds.size
var resetScrolling = false var resetScrolling = false
@ -6041,6 +6173,10 @@ public final class EmojiPagerContentComponent: Component {
} }
self.scrollView.bounds = CGRect(origin: self.scrollView.bounds.origin, size: scrollSize) self.scrollView.bounds = CGRect(origin: self.scrollView.bounds.origin, size: scrollSize)
if resetScrolling {
itemTransition = .immediate
}
let warpHeight: CGFloat = 50.0 let warpHeight: CGFloat = 50.0
var topWarpInset = pagerEnvironment.containerInsets.top var topWarpInset = pagerEnvironment.containerInsets.top
if self.isSearchActivated { 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 { for anchorItem in sortedAnchorItems {
if component.itemGroups[i].groupId != anchorItem.0.groupId { if component.contentItemGroups[i].groupId != anchorItem.0.groupId {
continue continue
} }
for j in 0 ..< component.itemGroups[i].items.count { for j in 0 ..< component.contentItemGroups[i].items.count {
let itemKey: ItemLayer.Key let itemKey: ItemLayer.Key
itemKey = ItemLayer.Key( itemKey = ItemLayer.Key(
groupId: component.itemGroups[i].groupId, groupId: component.contentItemGroups[i].groupId,
itemId: component.itemGroups[i].items[j].content.id itemId: component.contentItemGroups[i].items[j].content.id
) )
if itemKey == anchorItem.0 { if itemKey == anchorItem.0 {
@ -6137,19 +6273,15 @@ public final class EmojiPagerContentComponent: Component {
} }
if resetScrolling { if resetScrolling {
if component.displaySearchWithPlaceholder != nil && !self.isSearchActivated && component.searchInitiallyHidden { self.scrollView.bounds = CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: scrollSize)
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.ignoreScrolling = false self.ignoreScrolling = false
if calculateUpdatedItemPositions { if calculateUpdatedItemPositions {
var updatedItemPositionsValue: [VisualItemKey: CGPoint] = [:] var updatedItemPositionsValue: [VisualItemKey: CGPoint] = [:]
for groupIndex in 0 ..< component.itemGroups.count { for groupIndex in 0 ..< component.contentItemGroups.count {
let itemGroup = component.itemGroups[groupIndex] let itemGroup = component.contentItemGroups[groupIndex]
let itemGroupLayout = itemLayout.itemGroupLayouts[groupIndex] let itemGroupLayout = itemLayout.itemGroupLayouts[groupIndex]
for itemIndex in 0 ..< itemGroup.items.count { for itemIndex in 0 ..< itemGroup.items.count {
let item = itemGroup.items[itemIndex] let item = itemGroup.items[itemIndex]
@ -6204,14 +6336,16 @@ public final class EmojiPagerContentComponent: Component {
} }
} }
} else { } else {
/*if visibleSearchHeader.superview != self.scrollView { /*if component.inputInteractionHolder.inputInteraction?.externalBackground == nil {
self.scrollView.addSubview(visibleSearchHeader) if visibleSearchHeader.superview != self.scrollView {
self.mirrorContentScrollView.addSubview(visibleSearchHeader.tintContainerView) self.scrollView.addSubview(visibleSearchHeader)
self.mirrorContentScrollView.addSubview(visibleSearchHeader.tintContainerView)
}
}*/ }*/
} }
} else { } else {
visibleSearchHeader = EmojiSearchHeaderView(activated: { [weak self] in visibleSearchHeader = EmojiSearchHeaderView(activated: { [weak self] in
guard let strongSelf = self else { guard let strongSelf = self, let visibleSearchHeader = strongSelf.visibleSearchHeader else {
return return
} }
@ -6219,7 +6353,9 @@ public final class EmojiPagerContentComponent: Component {
component.inputInteractionHolder.inputInteraction?.openSearch() component.inputInteractionHolder.inputInteraction?.openSearch()
} else { } else {
strongSelf.isSearchActivated = true strongSelf.isSearchActivated = true
strongSelf.pagerEnvironment?.onWantsExclusiveModeUpdated(true) if visibleSearchHeader.currentPresetSearchTerm == nil {
strongSelf.pagerEnvironment?.onWantsExclusiveModeUpdated(true)
}
strongSelf.component?.inputInteractionHolder.inputInteraction?.requestUpdate(.immediate) strongSelf.component?.inputInteractionHolder.inputInteraction?.requestUpdate(.immediate)
} }
}, deactivated: { [weak self] isFirstResponder in }, 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)) 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) 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.setFrame(view: visibleSearchHeader, frame: searchHeaderFrame, completion: { [weak self] completed in transition.attachAnimation(view: visibleSearchHeader, completion: { [weak self] completed in
guard let strongSelf = self, completed, let visibleSearchHeader = strongSelf.visibleSearchHeader else { guard let strongSelf = self, completed, let visibleSearchHeader = strongSelf.visibleSearchHeader else {
return return
} }
@ -6266,6 +6402,7 @@ public final class EmojiPagerContentComponent: Component {
strongSelf.mirrorContentScrollView.addSubview(visibleSearchHeader.tintContainerView) strongSelf.mirrorContentScrollView.addSubview(visibleSearchHeader.tintContainerView)
} }
}) })
transition.setFrame(view: visibleSearchHeader, frame: searchHeaderFrame)
} else { } else {
if let visibleSearchHeader = self.visibleSearchHeader { if let visibleSearchHeader = self.visibleSearchHeader {
self.visibleSearchHeader = nil 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) 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 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( return EmojiPagerContentComponent(
id: "emoji", id: "emoji",
context: context, context: context,
@ -7134,40 +7328,8 @@ public final class EmojiPagerContentComponent: Component {
animationCache: animationCache, animationCache: animationCache,
animationRenderer: animationRenderer, animationRenderer: animationRenderer,
inputInteractionHolder: EmojiPagerContentComponent.InputInteractionHolder(), inputInteractionHolder: EmojiPagerContentComponent.InputInteractionHolder(),
itemGroups: itemGroups.map { group -> EmojiPagerContentComponent.ItemGroup in panelItemGroups: allItemGroups,
var headerItem = group.headerItem contentItemGroups: allItemGroups,
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
)
},
itemLayoutType: .compact, itemLayoutType: .compact,
itemContentUniqueId: nil, itemContentUniqueId: nil,
warpContentsOnEdges: isReactionSelection || isStatusSelection, warpContentsOnEdges: isReactionSelection || isStatusSelection,
@ -7644,6 +7806,33 @@ public final class EmojiPagerContentComponent: Component {
let isMasks = stickerNamespaces.contains(Namespaces.ItemCollection.CloudMaskPacks) 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( return EmojiPagerContentComponent(
id: isMasks ? "masks" : "stickers", id: isMasks ? "masks" : "stickers",
context: context, context: context,
@ -7651,32 +7840,8 @@ public final class EmojiPagerContentComponent: Component {
animationCache: animationCache, animationCache: animationCache,
animationRenderer: animationRenderer, animationRenderer: animationRenderer,
inputInteractionHolder: EmojiPagerContentComponent.InputInteractionHolder(), inputInteractionHolder: EmojiPagerContentComponent.InputInteractionHolder(),
itemGroups: itemGroups.map { group -> EmojiPagerContentComponent.ItemGroup in panelItemGroups: allItemGroups,
var hasClear = false contentItemGroups: allItemGroups,
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
)
},
itemLayoutType: .detailed, itemLayoutType: .detailed,
itemContentUniqueId: nil, itemContentUniqueId: nil,
warpContentsOnEdges: false, warpContentsOnEdges: false,

View File

@ -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<Int>? {
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<Empty>] = [:]
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<AnyHashable>()
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<Empty>
if let current = self.visibleItemViews[item] {
itemView = current
} else {
animateItem = animateAppearingItems
itemTransition = .immediate
itemView = ComponentView<Empty>()
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<Empty>, 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<Empty>, transition: Transition) -> CGSize {
return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition)
}
}

View File

@ -302,7 +302,7 @@ public final class EntityKeyboardComponent: Component {
if let maskContent = component.maskContent { if let maskContent = component.maskContent {
var topMaskItems: [EntityKeyboardTopPanelComponent.Item] = [] var topMaskItems: [EntityKeyboardTopPanelComponent.Item] = []
for itemGroup in maskContent.itemGroups { for itemGroup in maskContent.panelItemGroups {
if let id = itemGroup.supergroupId.base as? String { if let id = itemGroup.supergroupId.base as? String {
let iconMapping: [String: EntityKeyboardIconTopPanelComponent.Icon] = [ let iconMapping: [String: EntityKeyboardIconTopPanelComponent.Icon] = [
"saved": .saved, "saved": .saved,
@ -359,7 +359,7 @@ public final class EntityKeyboardComponent: Component {
theme: component.theme, theme: component.theme,
items: topMaskItems, items: topMaskItems,
containerSideInset: component.containerInsets.left + component.topPanelInsets.left, containerSideInset: component.containerInsets.left + component.topPanelInsets.left,
defaultActiveItemId: maskContent.itemGroups.first?.groupId, defaultActiveItemId: maskContent.panelItemGroups.first?.groupId,
activeContentItemIdUpdated: masksContentItemIdUpdated, activeContentItemIdUpdated: masksContentItemIdUpdated,
reorderItems: { [weak self] items in reorderItems: { [weak self] items in
guard let strongSelf = self else { 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 let id = itemGroup.supergroupId.base as? String {
if id == "peerSpecific" { if id == "peerSpecific" {
if let avatarPeer = stickerContent.avatarPeer { if let avatarPeer = stickerContent.avatarPeer {
@ -551,7 +551,7 @@ public final class EntityKeyboardComponent: Component {
theme: component.theme, theme: component.theme,
items: topStickerItems, items: topStickerItems,
containerSideInset: component.containerInsets.left + component.topPanelInsets.left, containerSideInset: component.containerInsets.left + component.topPanelInsets.left,
defaultActiveItemId: stickerContent.itemGroups.first?.groupId, defaultActiveItemId: stickerContent.panelItemGroups.first?.groupId,
activeContentItemIdUpdated: stickersContentItemIdUpdated, activeContentItemIdUpdated: stickersContentItemIdUpdated,
reorderItems: { [weak self] items in reorderItems: { [weak self] items in
guard let strongSelf = self else { guard let strongSelf = self else {
@ -581,7 +581,7 @@ public final class EntityKeyboardComponent: Component {
if let emojiContent = component.emojiContent { if let emojiContent = component.emojiContent {
contents.append(AnyComponentWithIdentity(id: "emoji", component: AnyComponent(emojiContent))) contents.append(AnyComponentWithIdentity(id: "emoji", component: AnyComponent(emojiContent)))
var topEmojiItems: [EntityKeyboardTopPanelComponent.Item] = [] var topEmojiItems: [EntityKeyboardTopPanelComponent.Item] = []
for itemGroup in emojiContent.itemGroups { for itemGroup in emojiContent.panelItemGroups {
if !itemGroup.items.isEmpty { if !itemGroup.items.isEmpty {
if let id = itemGroup.groupId.base as? String { if let id = itemGroup.groupId.base as? String {
if id == "recent" { if id == "recent" {

View File

@ -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)) 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 transition.setFrame(view: visibleSearchHeader, frame: searchHeaderFrame, completion: { [weak self] completed in
guard let strongSelf = self, completed, let visibleSearchHeader = strongSelf.visibleSearchHeader else { guard let strongSelf = self, completed, let visibleSearchHeader = strongSelf.visibleSearchHeader else {
return return

View File

@ -1394,6 +1394,10 @@ private func extractAccountManagerState(records: AccountRecordsView<TelegramAcco
}) })
}*/ }*/
if #available(iOS 12.0, *) {
UIApplication.shared.registerForRemoteNotifications()
}
return true return true
} }

View File

@ -3855,6 +3855,19 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
}, displayUndo: { [weak self] content in }, displayUndo: { [weak self] content in
if let strongSelf = self { if let strongSelf = self {
let presentationData = context.sharedContext.currentPresentationData.with { $0 } let presentationData = context.sharedContext.currentPresentationData.with { $0 }
strongSelf.window?.forEachController({ controller in
if let controller = controller as? UndoOverlayController {
controller.dismiss()
}
})
strongSelf.forEachController({ controller in
if let controller = controller as? UndoOverlayController {
controller.dismiss()
}
return true
})
strongSelf.present(UndoOverlayController(presentationData: presentationData, content: content, elevatedLayout: false, animateInAsReplacement: false, action: { _ in strongSelf.present(UndoOverlayController(presentationData: presentationData, content: content, elevatedLayout: false, animateInAsReplacement: false, action: { _ in
return true return true
}), in: .current) }), in: .current)
@ -4440,7 +4453,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
hasScheduledMessages = peerView.get() hasScheduledMessages = peerView.get()
|> take(1) |> take(1)
|> mapToSignal { view -> Signal<Bool, NoError> in |> mapToSignal { view -> Signal<Bool, NoError> in
if let peer = peerViewMainPeer(view) as? TelegramChannel, !peer.hasPermission(.sendMessages) { if let peer = peerViewMainPeer(view) as? TelegramChannel, !peer.hasPermission(.sendSomething) {
return .single(false) return .single(false)
} else { } else {
return context.account.viewTracker.scheduledMessagesViewForLocation(context.chatLocationInput(for: chatLocation, contextHolder: chatLocationContextHolder)) return context.account.viewTracker.scheduledMessagesViewForLocation(context.chatLocationInput(for: chatLocation, contextHolder: chatLocationContextHolder))
@ -4864,6 +4877,37 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
.updatedAutoremoveTimeout(autoremoveTimeout) .updatedAutoremoveTimeout(autoremoveTimeout)
.updatedCurrentSendAsPeerId(currentSendAsPeerId) .updatedCurrentSendAsPeerId(currentSendAsPeerId)
.updatedCopyProtectionEnabled(copyProtectionEnabled) .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 { 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 hasScheduledMessages = peerView
|> take(1) |> take(1)
|> mapToSignal { view -> Signal<Bool, NoError> in |> mapToSignal { view -> Signal<Bool, NoError> in
if let peer = peerViewMainPeer(view) as? TelegramChannel, !peer.hasPermission(.sendMessages) { if let peer = peerViewMainPeer(view) as? TelegramChannel, !peer.hasPermission(.sendSomething) {
return .single(false) return .single(false)
} else { } else {
return context.account.viewTracker.scheduledMessagesViewForLocation(context.chatLocationInput(for: chatLocation, contextHolder: chatLocationContextHolder)) return context.account.viewTracker.scheduledMessagesViewForLocation(context.chatLocationInput(for: chatLocation, contextHolder: chatLocationContextHolder))
@ -5271,6 +5315,37 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
return renderedPeer return renderedPeer
}.updatedIsNotAccessible(isNotAccessible).updatedContactStatus(contactStatus).updatedHasBots(hasBots).updatedIsArchived(isArchived).updatedPeerIsMuted(peerIsMuted).updatedPeerDiscussionId(peerDiscussionId).updatedPeerGeoLocation(peerGeoLocation).updatedExplicitelyCanPinMessages(explicitelyCanPinMessages).updatedHasScheduledMessages(hasScheduledMessages).updatedCurrentSendAsPeerId(currentSendAsPeerId) }.updatedIsNotAccessible(isNotAccessible).updatedContactStatus(contactStatus).updatedHasBots(hasBots).updatedIsArchived(isArchived).updatedPeerIsMuted(peerIsMuted).updatedPeerDiscussionId(peerDiscussionId).updatedPeerGeoLocation(peerGeoLocation).updatedExplicitelyCanPinMessages(explicitelyCanPinMessages).updatedHasScheduledMessages(hasScheduledMessages).updatedCurrentSendAsPeerId(currentSendAsPeerId)
.updatedCopyProtectionEnabled(copyProtectionEnabled) .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 { if !strongSelf.didSetChatLocationInfoReady {
strongSelf.didSetChatLocationInfoReady = true strongSelf.didSetChatLocationInfoReady = true
@ -6312,7 +6387,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
} }
if let opaqueState = (combinedInitialData.initialData?.storedInterfaceState).flatMap(_internal_decodeStoredChatInterfaceState) { if let opaqueState = (combinedInitialData.initialData?.storedInterfaceState).flatMap(_internal_decodeStoredChatInterfaceState) {
let interfaceState = ChatInterfaceState.parse(opaqueState) var interfaceState = ChatInterfaceState.parse(opaqueState)
var pinnedMessageId: MessageId? var pinnedMessageId: MessageId?
var peerIsBlocked: Bool = false var peerIsBlocked: Bool = false
@ -6343,6 +6418,32 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
} else if let _ = combinedInitialData.cachedData as? CachedSecretChatData { } 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 case let .replyThread(replyThreadMessageId) = strongSelf.chatLocation {
if let channel = combinedInitialData.initialData?.peer as? TelegramChannel, channel.flags.contains(.isForum) { if let channel = combinedInitialData.initialData?.peer as? TelegramChannel, channel.flags.contains(.isForum) {
pinnedMessageId = nil pinnedMessageId = nil
@ -8344,10 +8445,55 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
guard let strongSelf = self else { guard let strongSelf = self else {
return return
} }
guard let peer = strongSelf.presentationInterfaceState.renderedPeer?.peer else {
return
}
strongSelf.mediaRecordingModeTooltipController?.dismiss() strongSelf.mediaRecordingModeTooltipController?.dismiss()
strongSelf.interfaceInteraction?.updateShowWebView { _ in strongSelf.interfaceInteraction?.updateShowWebView { _ in
return false 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 requestId = strongSelf.beginMediaRecordingRequestId
let begin: () -> Void = { let begin: () -> Void = {
@ -8607,35 +8753,77 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
tooltipController.dismissImmediately() tooltipController.dismissImmediately()
} }
}, switchMediaRecordingMode: { [weak self] in }, switchMediaRecordingMode: { [weak self] in
if let strongSelf = self { guard let strongSelf = self else {
if strongSelf.recordingModeFeedback == nil { return
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 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 }, setupMessageAutoremoveTimeout: { [weak self] in
guard let strongSelf = self, case let .peer(peerId) = strongSelf.chatLocation else { guard let strongSelf = self, case let .peer(peerId) = strongSelf.chatLocation else {
return return
@ -11948,25 +12136,54 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
guard let peer = self.presentationInterfaceState.renderedPeer?.peer else { guard let peer = self.presentationInterfaceState.renderedPeer?.peer else {
return return
} }
let _ = peer
let _ = (self.context.sharedContext.accountManager.transaction { transaction -> GeneratedMediaStoreSettings in let _ = (self.context.sharedContext.accountManager.transaction { transaction -> GeneratedMediaStoreSettings in
let entry = transaction.getSharedData(ApplicationSpecificSharedDataKeys.generatedMediaStoreSettings)?.get(GeneratedMediaStoreSettings.self) let entry = transaction.getSharedData(ApplicationSpecificSharedDataKeys.generatedMediaStoreSettings)?.get(GeneratedMediaStoreSettings.self)
return entry ?? GeneratedMediaStoreSettings.defaultSettings return entry ?? GeneratedMediaStoreSettings.defaultSettings
} }
|> deliverOnMainQueue).start(next: { [weak self] settings in |> deliverOnMainQueue).start(next: { [weak self] settings in
guard let strongSelf = self else { guard let strongSelf = self, let peer = strongSelf.presentationInterfaceState.renderedPeer?.peer else {
return return
} }
var photoOnly = false var enablePhoto = true
var enableVideo = true
if let callManager = strongSelf.context.sharedContext.callManager as? PresentationCallManagerImpl, callManager.hasActiveCall { 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 storeCapturedMedia = peer.id.namespace != Namespaces.Peer.SecretChat
let inputText = strongSelf.presentationInterfaceState.interfaceState.effectiveInputState.inputText 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 { if let strongSelf = self {
strongSelf.enqueueMediaMessages(signals: signals, silentPosting: silentPosting, scheduleTime: scheduleTime > 0 ? scheduleTime : nil) strongSelf.enqueueMediaMessages(signals: signals, silentPosting: silentPosting, scheduleTime: scheduleTime > 0 ? scheduleTime : nil)
if !inputText.string.isEmpty { if !inputText.string.isEmpty {
@ -12603,12 +12820,40 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
}) })
}, openCamera: { [weak self] cameraView, menuController in }, openCamera: { [weak self] cameraView, menuController in
if let strongSelf = self, let peer = strongSelf.presentationInterfaceState.renderedPeer?.peer { 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 { 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 let strongSelf = self {
if editMediaOptions != nil { if editMediaOptions != nil {
strongSelf.editMessageMediaWithLegacySignals(signals!) strongSelf.editMessageMediaWithLegacySignals(signals!)
@ -13096,8 +13341,93 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
} }
}) })
})) }))
controller.getCaptionPanelView = { [weak self] in controller.attemptItemSelection = { [weak strongSelf] item in
return self?.getCaptionPanelView() 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)) present(controller, ViewControllerPresentationArguments(presentationAnimation: .modalSheet))
} }
@ -16597,15 +16927,55 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
} }
private func displayMediaRecordingTooltip() { private func displayMediaRecordingTooltip() {
guard let peer = self.presentationInterfaceState.renderedPeer?.peer else {
return
}
let rect: CGRect? = self.chatDisplayNode.frameForInputActionButton() let rect: CGRect? = self.chatDisplayNode.frameForInputActionButton()
let updatedMode: ChatTextInputMediaRecordingButtonMode = self.presentationInterfaceState.interfaceState.mediaRecordingMode let updatedMode: ChatTextInputMediaRecordingButtonMode = self.presentationInterfaceState.interfaceState.mediaRecordingMode
let text: String 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 { 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 { } 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() self.silentPostTooltipController?.dismiss()

View File

@ -280,6 +280,8 @@ func inputTextPanelStateForChatPresentationInterfaceState(_ chatPresentationInte
var currentAutoremoveTimeout: Int32? = chatPresentationInterfaceState.autoremoveTimeout var currentAutoremoveTimeout: Int32? = chatPresentationInterfaceState.autoremoveTimeout
var canSetupAutoremoveTimeout = false var canSetupAutoremoveTimeout = false
var canSendTextMessages = true
var accessoryItems: [ChatTextInputAccessoryItem] = [] var accessoryItems: [ChatTextInputAccessoryItem] = []
if let peer = chatPresentationInterfaceState.renderedPeer?.peer as? TelegramSecretChat { if let peer = chatPresentationInterfaceState.renderedPeer?.peer as? TelegramSecretChat {
var extendedSearchLayout = false var extendedSearchLayout = false
@ -298,6 +300,7 @@ func inputTextPanelStateForChatPresentationInterfaceState(_ chatPresentationInte
if !group.hasBannedPermission(.banChangeInfo) { if !group.hasBannedPermission(.banChangeInfo) {
canSetupAutoremoveTimeout = true canSetupAutoremoveTimeout = true
} }
canSendTextMessages = !group.hasBannedPermission(.banSendText)
} else if let user = chatPresentationInterfaceState.renderedPeer?.peer as? TelegramUser { } else if let user = chatPresentationInterfaceState.renderedPeer?.peer as? TelegramUser {
if user.botInfo == nil { if user.botInfo == nil {
canSetupAutoremoveTimeout = true canSetupAutoremoveTimeout = true
@ -306,6 +309,7 @@ func inputTextPanelStateForChatPresentationInterfaceState(_ chatPresentationInte
if channel.hasPermission(.changeInfo) { if channel.hasPermission(.changeInfo) {
canSetupAutoremoveTimeout = true canSetupAutoremoveTimeout = true
} }
canSendTextMessages = channel.hasBannedPermission(.banSendText) == nil
} }
if canSetupAutoremoveTimeout { if canSetupAutoremoveTimeout {
@ -375,10 +379,16 @@ func inputTextPanelStateForChatPresentationInterfaceState(_ chatPresentationInte
accessoryItems.append(.commands) accessoryItems.append(.commands)
} }
if stickersEnabled { if !canSendTextMessages {
accessoryItems.append(.input(isEnabled: true, inputMode: stickersAreEmoji ? .emoji : .stickers)) if stickersEnabled && !stickersAreEmoji {
accessoryItems.append(.input(isEnabled: true, inputMode: .stickers))
}
} else { } 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 { if isTextEmpty, let message = chatPresentationInterfaceState.keyboardButtonsMessage, let _ = message.visibleButtonKeyboardMarkup, chatPresentationInterfaceState.interfaceState.messageActionsState.dismissedButtonKeyboardMessageId != message.id {

View File

@ -283,7 +283,7 @@ func canReplyInChat(_ chatPresentationInterfaceState: ChatPresentationInterfaceS
case .peer: case .peer:
if let channel = peer as? TelegramChannel { if let channel = peer as? TelegramChannel {
if case .member = channel.participationStatus { if case .member = channel.participationStatus {
canReply = channel.hasPermission(.sendMessages) canReply = channel.hasPermission(.sendSomething)
} }
} else if let group = peer as? TelegramGroup { } else if let group = peer as? TelegramGroup {
if case .Member = group.membership { if case .Member = group.membership {

View File

@ -196,22 +196,22 @@ func inputPanelForChatPresentationIntefaceState(_ chatPresentationInterfaceState
} }
} }
if isMember && channel.hasBannedPermission(.banSendMessages) != nil && !channel.flags.contains(.isGigagroup) { if isMember && channel.hasBannedPermission(.banSendText) != nil && !channel.flags.contains(.isGigagroup) {
if let currentPanel = (currentPanel as? ChatRestrictedInputPanelNode) ?? (currentSecondaryPanel as? ChatRestrictedInputPanelNode) { /*if let currentPanel = (currentPanel as? ChatRestrictedInputPanelNode) ?? (currentSecondaryPanel as? ChatRestrictedInputPanelNode) {
return (currentPanel, nil) return (currentPanel, nil)
} else { } else {
let panel = ChatRestrictedInputPanelNode() let panel = ChatRestrictedInputPanelNode()
panel.context = context panel.context = context
panel.interfaceInteraction = interfaceInteraction panel.interfaceInteraction = interfaceInteraction
return (panel, nil) return (panel, nil)
} }*/
} }
switch channel.info { switch channel.info {
case .broadcast: case .broadcast:
if chatPresentationInterfaceState.interfaceState.editMessage != nil, channel.hasPermission(.editAllMessages) { if chatPresentationInterfaceState.interfaceState.editMessage != nil, channel.hasPermission(.editAllMessages) {
displayInputTextPanel = true displayInputTextPanel = true
} else if !channel.hasPermission(.sendMessages) || !isMember { } else if !channel.hasPermission(.sendSomething) || !isMember {
if let currentPanel = (currentPanel as? ChatChannelSubscriberInputPanelNode) ?? (currentSecondaryPanel as? ChatChannelSubscriberInputPanelNode) { if let currentPanel = (currentPanel as? ChatChannelSubscriberInputPanelNode) ?? (currentSecondaryPanel as? ChatChannelSubscriberInputPanelNode) {
return (currentPanel, nil) return (currentPanel, nil)
} else { } else {
@ -235,7 +235,7 @@ func inputPanelForChatPresentationIntefaceState(_ chatPresentationInterfaceState
} }
} }
case .member: 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) { if let currentPanel = (currentPanel as? ChatChannelSubscriberInputPanelNode) ?? (currentSecondaryPanel as? ChatChannelSubscriberInputPanelNode) {
return (currentPanel, nil) return (currentPanel, nil)
} else { } else {
@ -280,15 +280,15 @@ func inputPanelForChatPresentationIntefaceState(_ chatPresentationInterfaceState
break break
} }
if group.hasBannedPermission(.banSendMessages) { if group.hasBannedPermission(.banSendText) {
if let currentPanel = (currentPanel as? ChatRestrictedInputPanelNode) ?? (currentSecondaryPanel as? ChatRestrictedInputPanelNode) { /*if let currentPanel = (currentPanel as? ChatRestrictedInputPanelNode) ?? (currentSecondaryPanel as? ChatRestrictedInputPanelNode) {
return (currentPanel, nil) return (currentPanel, nil)
} else { } else {
let panel = ChatRestrictedInputPanelNode() let panel = ChatRestrictedInputPanelNode()
panel.context = context panel.context = context
panel.interfaceInteraction = interfaceInteraction panel.interfaceInteraction = interfaceInteraction
return (panel, nil) return (panel, nil)
} }*/
} }
} }

View File

@ -660,7 +660,7 @@ struct ChatRecentActionsEntry: Comparable, Identifiable {
let order: [(TelegramChatBannedRightsFlags, String)] = [ let order: [(TelegramChatBannedRightsFlags, String)] = [
(.banReadMessages, self.presentationData.strings.Channel_AdminLog_BanReadMessages), (.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), (.banSendMedia, self.presentationData.strings.Channel_AdminLog_BanSendMedia),
(.banSendStickers, self.presentationData.strings.Channel_AdminLog_BanSendStickersAndGifs), (.banSendStickers, self.presentationData.strings.Channel_AdminLog_BanSendStickersAndGifs),
(.banEmbedLinks, self.presentationData.strings.Channel_AdminLog_BanEmbedLinks), (.banEmbedLinks, self.presentationData.strings.Channel_AdminLog_BanEmbedLinks),
@ -1015,7 +1015,7 @@ struct ChatRecentActionsEntry: Comparable, Identifiable {
let order: [(TelegramChatBannedRightsFlags, String)] = [ let order: [(TelegramChatBannedRightsFlags, String)] = [
(.banReadMessages, self.presentationData.strings.Channel_AdminLog_BanReadMessages), (.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), (.banSendMedia, self.presentationData.strings.Channel_AdminLog_BanSendMedia),
(.banSendStickers, self.presentationData.strings.Channel_AdminLog_BanSendStickersAndGifs), (.banSendStickers, self.presentationData.strings.Channel_AdminLog_BanSendStickersAndGifs),
(.banEmbedLinks, self.presentationData.strings.Channel_AdminLog_BanEmbedLinks), (.banEmbedLinks, self.presentationData.strings.Channel_AdminLog_BanEmbedLinks),

View File

@ -32,9 +32,9 @@ final class ChatRestrictedInputPanelNode: ChatInputPanelNode {
let bannedPermission: (Int32, Bool)? let bannedPermission: (Int32, Bool)?
if let channel = interfaceState.renderedPeer?.peer as? TelegramChannel { 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 { } else if let group = interfaceState.renderedPeer?.peer as? TelegramGroup {
if group.hasBannedPermission(.banSendMessages) { if group.hasBannedPermission(.banSendText) {
bannedPermission = (Int32.max, false) bannedPermission = (Int32.max, false)
} else { } else {
bannedPermission = nil bannedPermission = nil

View File

@ -462,6 +462,7 @@ final class ChatTextViewForOverlayContent: UIView, ChatInputPanelViewForOverlayC
class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate { class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate {
let clippingNode: ASDisplayNode let clippingNode: ASDisplayNode
var textPlaceholderNode: ImmediateTextNode var textPlaceholderNode: ImmediateTextNode
var textLockIconNode: ASImageNode?
var contextPlaceholderNode: TextNode? var contextPlaceholderNode: TextNode?
var slowmodePlaceholderNode: ChatTextInputSlowmodePlaceholderNode? var slowmodePlaceholderNode: ChatTextInputSlowmodePlaceholderNode?
let textInputContainerBackgroundNode: ASImageNode let textInputContainerBackgroundNode: ASImageNode
@ -516,6 +517,7 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate {
private var updatingInputState = false private var updatingInputState = false
private var currentPlaceholder: String? private var currentPlaceholder: String?
private var sendingTextDisabled: Bool = false
private var presentationInterfaceState: ChatPresentationInterfaceState? private var presentationInterfaceState: ChatPresentationInterfaceState?
private var initializedPlaceholder = false private var initializedPlaceholder = false
@ -923,7 +925,15 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate {
let recognizer = TouchDownGestureRecognizer(target: self, action: #selector(self.textInputBackgroundViewTap(_:))) let recognizer = TouchDownGestureRecognizer(target: self, action: #selector(self.textInputBackgroundViewTap(_:)))
recognizer.touchDown = { [weak self] in recognizer.touchDown = { [weak self] in
if let strongSelf = self { 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 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) textInputNode.textView.scrollIndicatorInsets = UIEdgeInsets(top: 9.0, left: 0.0, bottom: 9.0, right: -13.0)
self.textInputContainer.addSubnode(textInputNode) self.textInputContainer.addSubnode(textInputNode)
textInputNode.view.disablesInteractiveTransitionGestureRecognizer = true textInputNode.view.disablesInteractiveTransitionGestureRecognizer = true
textInputNode.isUserInteractionEnabled = !self.sendingTextDisabled
self.textInputNode = textInputNode self.textInputNode = textInputNode
var accessoryButtonsWidth: CGFloat = 0.0 var accessoryButtonsWidth: CGFloat = 0.0
@ -1024,8 +1035,8 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate {
self.updateSpoiler() self.updateSpoiler()
} }
self.textInputBackgroundNode.isUserInteractionEnabled = false self.textInputBackgroundNode.isUserInteractionEnabled = !textInputNode.isUserInteractionEnabled
self.textInputBackgroundNode.view.removeGestureRecognizer(self.textInputBackgroundNode.view.gestureRecognizers![0]) //self.textInputBackgroundNode.view.removeGestureRecognizer(self.textInputBackgroundNode.view.gestureRecognizers![0])
let recognizer = TouchDownGestureRecognizer(target: self, action: #selector(self.textInputBackgroundViewTap(_:))) let recognizer = TouchDownGestureRecognizer(target: self, action: #selector(self.textInputBackgroundViewTap(_:)))
recognizer.touchDown = { [weak self] in recognizer.touchDown = { [weak self] in
@ -1198,6 +1209,18 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate {
self.attachmentButton.accessibilityTraits = (!isSlowmodeActive || isMediaEnabled) ? [.button] : [.button, .notEnabled] self.attachmentButton.accessibilityTraits = (!isSlowmodeActive || isMediaEnabled) ? [.button] : [.button, .notEnabled]
self.attachmentButtonDisabledNode.isHidden = !isSlowmodeActive || isMediaEnabled 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 buttonTitleUpdated = false
var menuTextSize = self.menuButtonTextNode.frame.size var menuTextSize = self.menuButtonTextNode.frame.size
if self.presentationInterfaceState != interfaceState { if self.presentationInterfaceState != interfaceState {
@ -1347,22 +1370,30 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate {
self.initializedPlaceholder = true self.initializedPlaceholder = true
var placeholder: String var placeholder: String
if let channel = peer as? TelegramChannel, case .broadcast = channel.info { if let channel = peer as? TelegramChannel, case .broadcast = channel.info {
if interfaceState.interfaceState.silentPosting { if interfaceState.interfaceState.silentPosting {
placeholder = interfaceState.strings.Conversation_InputTextSilentBroadcastPlaceholder placeholder = interfaceState.strings.Conversation_InputTextSilentBroadcastPlaceholder
} else { } else {
placeholder = interfaceState.strings.Conversation_InputTextBroadcastPlaceholder 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 { } 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 { 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.updateFrame(layer: self.textInputBackgroundNode.layer, frame: textInputBackgroundFrame)
transition.updateAlpha(node: self.textInputBackgroundNode, alpha: audioRecordingItemsAlpha) 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 var textPlaceholderAlpha: CGFloat = audioRecordingItemsAlpha
if self.textPlaceholderNode.frame.width > (nextButtonTopRight.x - textInputBackgroundFrame.minX) - 32.0 { if self.textPlaceholderNode.frame.width > (nextButtonTopRight.x - textInputBackgroundFrame.minX) - 32.0 {
@ -3352,6 +3411,10 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate {
} }
func ensureFocused() { func ensureFocused() {
if self.sendingTextDisabled {
return
}
if self.textInputNode == nil { if self.textInputNode == nil {
self.loadTextInputNode() self.loadTextInputNode()
} }

View File

@ -10,7 +10,7 @@ import ShareController
import LegacyUI import LegacyUI
import LegacyMediaPickerUI 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 presentationData = context.sharedContext.currentPresentationData.with { $0 }
let legacyController = LegacyController(presentation: .custom, theme: presentationData.theme) let legacyController = LegacyController(presentation: .custom, theme: presentationData.theme)
legacyController.supportedOrientations = ViewControllerSupportedOrientations(regularSize: .portrait, compactSize: .portrait) legacyController.supportedOrientations = ViewControllerSupportedOrientations(regularSize: .portrait, compactSize: .portrait)
@ -22,7 +22,16 @@ func presentedLegacyCamera(context: AccountContext, peer: Peer, chatLocation: Ch
let controller: TGCameraController let controller: TGCameraController
if let cameraView = cameraView, let previewView = cameraView.previewView() { 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 { } else {
controller = TGCameraController(context: legacyController.context, saveEditedPhotos: saveCapturedPhotos && !isSecretChat, saveCapturedMedia: saveCapturedPhotos && !isSecretChat) controller = TGCameraController(context: legacyController.context, saveEditedPhotos: saveCapturedPhotos && !isSecretChat, saveCapturedMedia: saveCapturedPhotos && !isSecretChat)
} }

View File

@ -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 let messagesShouldHaveSignatures: Bool
switch channel.info { switch channel.info {
case let .broadcast(info): case let .broadcast(info):

View File

@ -33,7 +33,7 @@ final class WebSearchControllerInteraction {
let openResult: (ChatContextResult) -> Void let openResult: (ChatContextResult) -> Void
let setSearchQuery: (String) -> Void let setSearchQuery: (String) -> Void
let deleteRecentQuery: (String) -> Void let deleteRecentQuery: (String) -> Void
let toggleSelection: (ChatContextResult, Bool) -> Void let toggleSelection: (ChatContextResult, Bool) -> Bool
let sendSelected: (ChatContextResult?, Bool, Int32?) -> Void let sendSelected: (ChatContextResult?, Bool, Int32?) -> Void
let schedule: () -> Void let schedule: () -> Void
let avatarCompleted: (UIImage) -> Void let avatarCompleted: (UIImage) -> Void
@ -41,7 +41,7 @@ final class WebSearchControllerInteraction {
let editingState: TGMediaEditingContext let editingState: TGMediaEditingContext
var hiddenMediaId: String? 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.openResult = openResult
self.setSearchQuery = setSearchQuery self.setSearchQuery = setSearchQuery
self.deleteRecentQuery = deleteRecentQuery self.deleteRecentQuery = deleteRecentQuery
@ -119,6 +119,8 @@ public final class WebSearchController: ViewController {
public var searchingUpdated: (Bool) -> Void = { _ in } public var searchingUpdated: (Bool) -> Void = { _ in }
public var attemptItemSelection: (ChatContextResult) -> Bool = { _ in return true }
public init(context: AccountContext, updatedPresentationData: (initial: PresentationData, signal: Signal<PresentationData, NoError>)? = nil, peer: EnginePeer?, chatLocation: ChatLocation?, configuration: EngineConfiguration.SearchBots, mode: WebSearchControllerMode) { public init(context: AccountContext, updatedPresentationData: (initial: PresentationData, signal: Signal<PresentationData, NoError>)? = nil, peer: EnginePeer?, chatLocation: ChatLocation?, configuration: EngineConfiguration.SearchBots, mode: WebSearchControllerMode) {
self.context = context self.context = context
self.mode = mode self.mode = mode
@ -233,8 +235,14 @@ public final class WebSearchController: ViewController {
} }
}, toggleSelection: { [weak self] result, value in }, toggleSelection: { [weak self] result, value in
if let strongSelf = self { if let strongSelf = self {
if !strongSelf.attemptItemSelection(result) {
return false
}
let item = LegacyWebSearchItem(result: result) let item = LegacyWebSearchItem(result: result)
strongSelf.controllerInteraction?.selectionState?.setItem(item, selected: value) strongSelf.controllerInteraction?.selectionState?.setItem(item, selected: value)
return true
} else {
return false
} }
}, sendSelected: { [weak self] current, silently, scheduleTime in }, sendSelected: { [weak self] current, silently, scheduleTime in
if let selectionState = selectionState, let results = self?.controllerNode.currentExternalResults { if let selectionState = selectionState, let results = self?.controllerNode.currentExternalResults {
@ -258,6 +266,18 @@ public final class WebSearchController: ViewController {
} }
}, selectionState: selectionState, editingState: editingState) }, 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 { if let selectionState = selectionState {
self.selectionDisposable = (selectionChangedSignal(selectionState: selectionState) self.selectionDisposable = (selectionChangedSignal(selectionState: selectionState)
|> deliverOnMainQueue).start(next: { [weak self] _ in |> deliverOnMainQueue).start(next: { [weak self] _ in

View File

@ -213,8 +213,13 @@ final class WebSearchItemNode: GridItemNode {
func updateSelectionState(animated: Bool) { func updateSelectionState(animated: Bool) {
if self.checkNode == nil, let item = self.item, let _ = item.controllerInteraction.selectionState { if self.checkNode == nil, let item = self.item, let _ = item.controllerInteraction.selectionState {
let checkNode = InteractiveCheckNode(theme: CheckNodeTheme(theme: item.theme, style: .overlay)) let checkNode = InteractiveCheckNode(theme: CheckNodeTheme(theme: item.theme, style: .overlay))
checkNode.valueChanged = { value in checkNode.valueChanged = { [weak self] value in
item.controllerInteraction.toggleSelection(item.result, value) guard let self else {
return
}
if !item.controllerInteraction.toggleSelection(item.result, value) {
self.checkNode?.setSelected(false, animated: false)
}
} }
self.addSubnode(checkNode) self.addSubnode(checkNode)
self.checkNode = checkNode self.checkNode = checkNode