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.HoldForAudioOnly" = "Hold to record audio.";
"Conversation.HoldForVideoOnly" = "Hold to record video.";

View File

@ -477,7 +477,7 @@ public final class AuthorizationSequenceController: NavigationController, MFMail
}
} else {
controller?.inProgress = true
strongSelf.actionDisposable.set((resendAuthorizationCode(account: strongSelf.account)
strongSelf.actionDisposable.set((resendAuthorizationCode(accountManager: strongSelf.sharedContext.accountManager, account: strongSelf.account, apiId: strongSelf.apiId, apiHash: strongSelf.apiHash, firebaseSecretStream: strongSelf.sharedContext.firebaseSecretStream)
|> deliverOnMainQueue).start(next: { result in
controller?.inProgress = false
}, error: { error in

View File

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

View File

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

View File

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

View File

@ -67,7 +67,7 @@
@property (nonatomic, assign) CGRect previewViewFrame;
- (instancetype)initWithFrame:(CGRect)frame avatar:(bool)avatar hasUltrawideCamera:(bool)hasUltrawideCamera hasTelephotoCamera:(bool)hasTelephotoCamera;
- (instancetype)initWithFrame:(CGRect)frame avatar:(bool)avatar videoModeByDefault:(bool)videoModeByDefault hasUltrawideCamera:(bool)hasUltrawideCamera hasTelephotoCamera:(bool)hasTelephotoCamera;
- (void)setDocumentFrameHidden:(bool)hidden;
- (void)setCameraMode:(PGCameraMode)mode;

View File

@ -10,6 +10,6 @@
- (void)setHidden:(bool)hidden animated:(bool)animated;
- (instancetype)initWithFrame:(CGRect)frame avatar:(bool)avatar;
- (instancetype)initWithFrame:(CGRect)frame avatar:(bool)avatar videoModeByDefault:(bool)videoModeByDefault;
@end

View File

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

View File

@ -33,7 +33,7 @@
@implementation TGAttachmentCameraView
- (instancetype)initForSelfPortrait:(bool)selfPortrait
- (instancetype)initForSelfPortrait:(bool)selfPortrait videoModeByDefault:(bool)videoModeByDefault
{
self = [super initWithFrame:CGRectZero];
if (self != nil)
@ -46,7 +46,7 @@
PGCamera *camera = nil;
if ([PGCamera cameraAvailable])
{
camera = [[PGCamera alloc] initWithMode:PGCameraModePhoto position:selfPortrait ? PGCameraPositionFront : PGCameraPositionUndefined];
camera = [[PGCamera alloc] initWithMode:videoModeByDefault ? PGCameraModeVideo : PGCameraModePhoto position:selfPortrait ? PGCameraPositionFront : PGCameraPositionUndefined];
}
_camera = camera;

View File

@ -230,7 +230,7 @@ const NSUInteger TGAttachmentDisplayedAssetLimit = 500;
if (hasCamera)
{
_cameraView = [[TGAttachmentCameraView alloc] initForSelfPortrait:selfPortrait];
_cameraView = [[TGAttachmentCameraView alloc] initForSelfPortrait:selfPortrait videoModeByDefault:false];
_cameraView.frame = CGRectMake(_smallLayout.minimumLineSpacing, 0, TGAttachmentCellSize.width, TGAttachmentCellSize.height);
[_cameraView startPreview];

View File

@ -170,8 +170,12 @@ static CGPoint TGCameraControllerClampPointToScreenSize(__unused id self, __unus
_items = [[NSMutableArray alloc] init];
if (_intent != TGCameraControllerGenericIntent)
if (_intent != TGCameraControllerGenericIntent) {
_allowCaptions = false;
}
if (_intent == TGCameraControllerGenericVideoOnlyIntent || _intent == TGCameraControllerGenericPhotoOnlyIntent) {
_allowCaptions = false;
}
_saveEditedPhotos = saveEditedPhotos;
_saveCapturedMedia = saveCapturedMedia;
@ -303,12 +307,12 @@ static CGPoint TGCameraControllerClampPointToScreenSize(__unused id self, __unus
if ([[UIDevice currentDevice] userInterfaceIdiom] == UIUserInterfaceIdiomPhone)
{
_interfaceView = [[TGCameraMainPhoneView alloc] initWithFrame:screenBounds avatar:_intent == TGCameraControllerAvatarIntent hasUltrawideCamera:_camera.hasUltrawideCamera hasTelephotoCamera:_camera.hasTelephotoCamera];
_interfaceView = [[TGCameraMainPhoneView alloc] initWithFrame:screenBounds avatar:_intent == TGCameraControllerAvatarIntent videoModeByDefault:_intent == TGCameraControllerGenericVideoOnlyIntent hasUltrawideCamera:_camera.hasUltrawideCamera hasTelephotoCamera:_camera.hasTelephotoCamera];
[_interfaceView setInterfaceOrientation:interfaceOrientation animated:false];
}
else
{
_interfaceView = [[TGCameraMainTabletView alloc] initWithFrame:screenBounds avatar:_intent == TGCameraControllerAvatarIntent hasUltrawideCamera:_camera.hasUltrawideCamera hasTelephotoCamera:_camera.hasTelephotoCamera];
_interfaceView = [[TGCameraMainTabletView alloc] initWithFrame:screenBounds avatar:_intent == TGCameraControllerAvatarIntent videoModeByDefault:_intent == TGCameraControllerGenericVideoOnlyIntent hasUltrawideCamera:_camera.hasUltrawideCamera hasTelephotoCamera:_camera.hasTelephotoCamera];
[_interfaceView setInterfaceOrientation:interfaceOrientation animated:false];
CGSize referenceSize = [self referenceViewSizeForOrientation:interfaceOrientation];
@ -451,8 +455,12 @@ static CGPoint TGCameraControllerClampPointToScreenSize(__unused id self, __unus
}
};
if (_intent != TGCameraControllerGenericIntent && _intent != TGCameraControllerAvatarIntent)
if (_intent != TGCameraControllerGenericIntent && _intent != TGCameraControllerAvatarIntent) {
[_interfaceView setHasModeControl:false];
}
if (_intent == TGCameraControllerGenericVideoOnlyIntent || _intent == TGCameraControllerGenericPhotoOnlyIntent) {
[_interfaceView setHasModeControl:false];
}
if (@available(iOS 11.0, *)) {
_backgroundView.accessibilityIgnoresInvertColors = true;
@ -1527,7 +1535,7 @@ static CGPoint TGCameraControllerClampPointToScreenSize(__unused id self, __unus
}
}];
bool hasCamera = !self.inhibitMultipleCapture && (((_intent == TGCameraControllerGenericIntent || _intent == TGCameraControllerGenericPhotoOnlyIntent) && !_shortcut) || (_intent == TGCameraControllerPassportMultipleIntent));
bool hasCamera = !self.inhibitMultipleCapture && (((_intent == TGCameraControllerGenericIntent || _intent == TGCameraControllerGenericPhotoOnlyIntent || _intent == TGCameraControllerGenericVideoOnlyIntent) && !_shortcut) || (_intent == TGCameraControllerPassportMultipleIntent));
TGMediaPickerGalleryModel *model = [[TGMediaPickerGalleryModel alloc] initWithContext:windowContext items:galleryItems focusItem:focusItem selectionContext:_items.count > 1 ? selectionContext : nil editingContext:editingContext hasCaptions:self.allowCaptions allowCaptionEntities:self.allowCaptionEntities hasTimer:self.hasTimer onlyCrop:_intent == TGCameraControllerPassportIntent || _intent == TGCameraControllerPassportIdIntent || _intent == TGCameraControllerPassportMultipleIntent inhibitDocumentCaptions:self.inhibitDocumentCaptions hasSelectionPanel:true hasCamera:hasCamera recipientName:self.recipientName];
model.inhibitMute = self.inhibitMute;
model.controller = galleryController;

View File

@ -101,7 +101,7 @@
@synthesize cancelPressed;
@synthesize actionHandle = _actionHandle;
- (instancetype)initWithFrame:(CGRect)frame avatar:(bool)avatar hasUltrawideCamera:(bool)hasUltrawideCamera hasTelephotoCamera:(bool)hasTelephotoCamera
- (instancetype)initWithFrame:(CGRect)frame avatar:(bool)avatar videoModeByDefault:(bool)videoModeByDefault hasUltrawideCamera:(bool)hasUltrawideCamera hasTelephotoCamera:(bool)hasTelephotoCamera
{
self = [super initWithFrame:frame];
if (self != nil)
@ -314,7 +314,7 @@
[_shutterButton addGestureRecognizer:shutterPanGestureRecognizer];
[_bottomPanelView addSubview:_shutterButton];
_modeControl = [[TGCameraModeControl alloc] initWithFrame:CGRectMake(0, 0, frame.size.width, _modeControlHeight) avatar:avatar];
_modeControl = [[TGCameraModeControl alloc] initWithFrame:CGRectMake(0, 0, frame.size.width, _modeControlHeight) avatar:avatar videoModeByDefault:videoModeByDefault];
[_bottomPanelView addSubview:_modeControl];
_flipButton = [[TGCameraFlipButton alloc] initWithFrame:CGRectMake(0, 0, 48, 48)];
@ -414,6 +414,12 @@
[_photoCounterButton addTarget:self action:@selector(photoCounterButtonPressed) forControlEvents:UIControlEventTouchUpInside];
_photoCounterButton.userInteractionEnabled = false;
[_bottomPanelView addSubview:_photoCounterButton];
if (videoModeByDefault) {
[UIView performWithoutAnimation:^{
[self updateForCameraModeChangeWithPreviousMode:PGCameraModePhoto];
}];
}
}
return self;
}

View File

@ -42,7 +42,7 @@ const CGFloat TGCameraTabletPanelViewWidth = 102.0f;
@synthesize shutterReleased;
@synthesize cancelPressed;
- (instancetype)initWithFrame:(CGRect)frame avatar:(bool)avatar hasUltrawideCamera:(bool)hasUltrawideCamera hasTelephotoCamera:(bool)hasTelephotoCamera
- (instancetype)initWithFrame:(CGRect)frame avatar:(bool)avatar videoModeByDefault:(bool)videoModeByDefault hasUltrawideCamera:(bool)hasUltrawideCamera hasTelephotoCamera:(bool)hasTelephotoCamera
{
self = [super initWithFrame:frame];
if (self != nil)
@ -83,7 +83,7 @@ const CGFloat TGCameraTabletPanelViewWidth = 102.0f;
[_shutterButton addTarget:self action:@selector(shutterButtonReleased) forControlEvents:UIControlEventTouchUpInside];
[_panelView addSubview:_shutterButton];
_modeControl = [[TGCameraModeControl alloc] initWithFrame:CGRectMake(0, 0, _panelView.frame.size.width, 260) avatar:avatar];
_modeControl = [[TGCameraModeControl alloc] initWithFrame:CGRectMake(0, 0, _panelView.frame.size.width, 260) avatar:avatar videoModeByDefault:videoModeByDefault];
[_panelView addSubview:_modeControl];
__weak TGCameraMainTabletView *weakSelf = self;

View File

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

View File

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

View File

@ -160,7 +160,9 @@ final class MediaPickerGridItemNode: GridItemNode {
let checkNode = InteractiveCheckNode(theme: CheckNodeTheme(theme: theme, style: .overlay))
checkNode.valueChanged = { [weak self] value in
if let strongSelf = self, let interaction = strongSelf.interaction, let selectableItem = strongSelf.selectableItem {
interaction.toggleSelection(selectableItem, value, false)
if !interaction.toggleSelection(selectableItem, value, false) {
strongSelf.checkNode?.setSelected(false, animated: false)
}
}
}
self.addSubnode(checkNode)

View File

@ -25,7 +25,7 @@ import MoreButtonNode
final class MediaPickerInteraction {
let openMedia: (PHFetchResult<PHAsset>, Int, UIImage?) -> Void
let openSelectedMedia: (TGMediaSelectableItem, UIImage?) -> Void
let toggleSelection: (TGMediaSelectableItem, Bool, Bool) -> Void
let toggleSelection: (TGMediaSelectableItem, Bool, Bool) -> Bool
let sendSelected: (TGMediaSelectableItem?, Bool, Int32?, Bool, @escaping () -> Void) -> Void
let schedule: () -> Void
let dismissInput: () -> Void
@ -33,7 +33,7 @@ final class MediaPickerInteraction {
let editingState: TGMediaEditingContext
var hiddenMediaId: String?
init(openMedia: @escaping (PHFetchResult<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.openSelectedMedia = openSelectedMedia
self.toggleSelection = toggleSelection
@ -403,7 +403,7 @@ public final class MediaPickerScreen: ViewController, AttachmentContainable {
}
if let controller = self.controller, case .assets(nil) = controller.subject {
let cameraView = TGAttachmentCameraView(forSelfPortrait: false)!
let cameraView = TGAttachmentCameraView(forSelfPortrait: false, videoModeByDefault: controller.bannedSendPhotos != nil && controller.bannedSendVideos == nil)!
cameraView.clipsToBounds = true
cameraView.removeCorners()
cameraView.pressed = { [weak self] in
@ -946,7 +946,13 @@ public final class MediaPickerScreen: ViewController, AttachmentContainable {
if cameraAccess == nil {
cameraRect = nil
}
/*if let (untilDate, personal) = self.controller?.bannedSendMedia {
var bannedSendMedia: (Int32, Bool)?
if let bannedSendPhotos = self.controller?.bannedSendPhotos, let bannedSendVideos = self.controller?.bannedSendVideos {
bannedSendMedia = (max(bannedSendPhotos.0, bannedSendVideos.0), bannedSendPhotos.1 || bannedSendVideos.1)
}
if let (untilDate, personal) = bannedSendMedia {
self.gridNode.isHidden = true
let banDescription: String
@ -973,7 +979,7 @@ public final class MediaPickerScreen: ViewController, AttachmentContainable {
placeholderTransition.updateFrame(node: placeholderNode, frame: innerBounds)
self.updateNavigation(transition: .immediate)
} else */if case .notDetermined = mediaAccess {
} else if case .notDetermined = mediaAccess {
} else {
if case .limited = mediaAccess {
let manageNode: MediaPickerManageNode
@ -1073,6 +1079,11 @@ public final class MediaPickerScreen: ViewController, AttachmentContainable {
}
}
var bannedSendMedia: (Int32, Bool)?
if let bannedSendPhotos = self.controller?.bannedSendPhotos, let bannedSendVideos = self.controller?.bannedSendVideos {
bannedSendMedia = (max(bannedSendPhotos.0, bannedSendVideos.0), bannedSendPhotos.1 || bannedSendVideos.1)
}
if case let .noAccess(cameraAccess) = self.state {
var placeholderTransition = transition
let placeholderNode: MediaPickerPlaceholderNode
@ -1099,7 +1110,7 @@ public final class MediaPickerScreen: ViewController, AttachmentContainable {
}
placeholderNode.update(layout: layout, theme: self.presentationData.theme, strings: self.presentationData.strings, hasCamera: cameraAccess == .authorized, transition: placeholderTransition)
placeholderTransition.updateFrame(node: placeholderNode, frame: innerBounds)
} else if let placeholderNode = self.placeholderNode {//, self.controller?.bannedSendMedia == nil {
} else if let placeholderNode = self.placeholderNode, bannedSendMedia == nil {
self.placeholderNode = nil
placeholderNode.removeFromSupernode()
}
@ -1161,6 +1172,45 @@ public final class MediaPickerScreen: ViewController, AttachmentContainable {
self.statusBar.statusBarStyle = .Ignore
selectionContext.attemptSelectingItem = { [weak self] item in
guard let self else {
return false
}
if let _ = item as? TGMediaPickerGalleryPhotoItem {
if self.bannedSendPhotos != nil {
//TODO:localize
self.present(standardTextAlertController(theme: AlertControllerTheme(presentationData: self.presentationData), title: nil, text: "Sending photos is not allowed", actions: [TextAlertAction(type: .defaultAction, title: self.presentationData.strings.Common_OK, action: {})]), in: .window(.root))
return false
}
} else if let _ = item as? TGMediaPickerGalleryVideoItem {
if self.bannedSendVideos != nil {
//TODO:localize
self.present(standardTextAlertController(theme: AlertControllerTheme(presentationData: self.presentationData), title: nil, text: "Sending videos is not allowed", actions: [TextAlertAction(type: .defaultAction, title: self.presentationData.strings.Common_OK, action: {})]), in: .window(.root))
return false
}
} else if let asset = item as? TGMediaAsset {
if asset.isVideo {
if self.bannedSendVideos != nil {
//TODO:localize
self.present(standardTextAlertController(theme: AlertControllerTheme(presentationData: self.presentationData), title: nil, text: "Sending videos is not allowed", actions: [TextAlertAction(type: .defaultAction, title: self.presentationData.strings.Common_OK, action: {})]), in: .window(.root))
return false
}
} else {
if self.bannedSendPhotos != nil {
//TODO:localize
self.present(standardTextAlertController(theme: AlertControllerTheme(presentationData: self.presentationData), title: nil, text: "Sending photos is not allowed", actions: [TextAlertAction(type: .defaultAction, title: self.presentationData.strings.Common_OK, action: {})]), in: .window(.root))
return false
}
}
}
return true
}
self.presentationDataDisposable = ((updatedPresentationData?.signal ?? context.sharedContext.presentationData)
|> deliverOnMainQueue).start(next: { [weak self] presentationData in
if let strongSelf = self {
@ -1223,7 +1273,39 @@ public final class MediaPickerScreen: ViewController, AttachmentContainable {
}, openSelectedMedia: { [weak self] item, immediateThumbnail in
self?.controllerNode.openSelectedMedia(item: item, immediateThumbnail: immediateThumbnail)
}, toggleSelection: { [weak self] item, value, suggestUndo in
if let strongSelf = self, let selectionState = strongSelf.interaction?.selectionState {
if let self = self, let selectionState = self.interaction?.selectionState {
if let _ = item as? TGMediaPickerGalleryPhotoItem {
if self.bannedSendPhotos != nil {
//TODO:localize
self.present(standardTextAlertController(theme: AlertControllerTheme(presentationData: self.presentationData), title: nil, text: "Sending photos is not allowed", actions: [TextAlertAction(type: .defaultAction, title: self.presentationData.strings.Common_OK, action: {})]), in: .window(.root))
return false
}
} else if let _ = item as? TGMediaPickerGalleryVideoItem {
if self.bannedSendVideos != nil {
//TODO:localize
self.present(standardTextAlertController(theme: AlertControllerTheme(presentationData: self.presentationData), title: nil, text: "Sending videos is not allowed", actions: [TextAlertAction(type: .defaultAction, title: self.presentationData.strings.Common_OK, action: {})]), in: .window(.root))
return false
}
} else if let asset = item as? TGMediaAsset {
if asset.isVideo {
if self.bannedSendVideos != nil {
//TODO:localize
self.present(standardTextAlertController(theme: AlertControllerTheme(presentationData: self.presentationData), title: nil, text: "Sending videos is not allowed", actions: [TextAlertAction(type: .defaultAction, title: self.presentationData.strings.Common_OK, action: {})]), in: .window(.root))
return false
}
} else {
if self.bannedSendPhotos != nil {
//TODO:localize
self.present(standardTextAlertController(theme: AlertControllerTheme(presentationData: self.presentationData), title: nil, text: "Sending photos is not allowed", actions: [TextAlertAction(type: .defaultAction, title: self.presentationData.strings.Common_OK, action: {})]), in: .window(.root))
return false
}
}
}
var showUndo = false
if suggestUndo {
if !value {
@ -1237,8 +1319,12 @@ public final class MediaPickerScreen: ViewController, AttachmentContainable {
selectionState.setItem(item, selected: value)
if showUndo {
strongSelf.showSelectionUndo(item: item)
self.showSelectionUndo(item: item)
}
return true
} else {
return false
}
}, sendSelected: { [weak self] currentItem, silently, scheduleTime, animated, completion in
if let strongSelf = self, let selectionState = strongSelf.interaction?.selectionState, !strongSelf.isDismissing {

View File

@ -254,7 +254,9 @@ private class MediaPickerSelectedItemNode: ASDisplayNode {
let checkNode = InteractiveCheckNode(theme: CheckNodeTheme(theme: theme, style: .overlay))
checkNode.valueChanged = { [weak self] value in
if let strongSelf = self, let interaction = strongSelf.interaction, let selectableItem = strongSelf.asset as? TGMediaSelectableItem {
interaction.toggleSelection(selectableItem, value, true)
if !interaction.toggleSelection(selectableItem, value, true) {
strongSelf.checkNode?.setSelected(false, animated: false)
}
}
}
self.addSubnode(checkNode)

View File

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

View File

@ -460,7 +460,7 @@ public final class ReactionContextNode: ASDisplayNode, UIScrollViewDelegate {
} else {
strongSelf.stableEmptyResultEmoji = nil
}
emojiContent = emojiContent.withUpdatedItemGroups(itemGroups: emojiSearchResult.groups, itemContentUniqueId: emojiSearchResult.id, emptySearchResults: emptySearchResults)
emojiContent = emojiContent.withUpdatedItemGroups(panelItemGroups: emojiContent.panelItemGroups, contentItemGroups: emojiSearchResult.groups, itemContentUniqueId: emojiSearchResult.id, emptySearchResults: emptySearchResults)
} else {
strongSelf.stableEmptyResultEmoji = nil
}

View File

@ -3,7 +3,10 @@ import Postbox
public enum TelegramChannelPermission {
case sendMessages
case sendText
case sendPhoto
case sendVideo
case sendSomething
case pinMessages
case manageTopics
case createTopics
@ -30,7 +33,7 @@ public extension TelegramChannel {
return true
}
switch permission {
case .sendMessages:
case .sendText:
if case .broadcast = self.info {
if let adminRights = self.adminRights {
return adminRights.rights.contains(.canPostMessages)
@ -41,10 +44,80 @@ public extension TelegramChannel {
if let _ = self.adminRights {
return true
}
if let bannedRights = self.bannedRights, bannedRights.flags.contains(.banSendMessages) {
if let bannedRights = self.bannedRights, bannedRights.flags.contains(.banSendText) {
return false
}
if let defaultBannedRights = self.defaultBannedRights, defaultBannedRights.flags.contains(.banSendMessages) {
if let defaultBannedRights = self.defaultBannedRights, defaultBannedRights.flags.contains(.banSendText) {
return false
}
return true
}
case .sendPhoto:
if case .broadcast = self.info {
if let adminRights = self.adminRights {
return adminRights.rights.contains(.canPostMessages)
} else {
return false
}
} else {
if let _ = self.adminRights {
return true
}
if let bannedRights = self.bannedRights, bannedRights.flags.contains(.banSendPhotos) {
return false
}
if let defaultBannedRights = self.defaultBannedRights, defaultBannedRights.flags.contains(.banSendPhotos) {
return false
}
return true
}
case .sendVideo:
if case .broadcast = self.info {
if let adminRights = self.adminRights {
return adminRights.rights.contains(.canPostMessages)
} else {
return false
}
} else {
if let _ = self.adminRights {
return true
}
if let bannedRights = self.bannedRights, bannedRights.flags.contains(.banSendVideos) {
return false
}
if let defaultBannedRights = self.defaultBannedRights, defaultBannedRights.flags.contains(.banSendVideos) {
return false
}
return true
}
case .sendSomething:
if case .broadcast = self.info {
if let adminRights = self.adminRights {
return adminRights.rights.contains(.canPostMessages)
} else {
return false
}
} else {
if let _ = self.adminRights {
return true
}
let flags: TelegramChatBannedRightsFlags = [
.banSendText,
.banSendInstantVideos,
.banSendVoice,
.banSendPhotos,
.banSendVideos,
.banSendStickers,
.banSendPolls,
.banSendFiles,
.banSendInline
]
if let bannedRights = self.bannedRights, bannedRights.flags.intersection(flags) == flags {
return false
}
if let defaultBannedRights = self.defaultBannedRights, defaultBannedRights.flags.intersection(flags) == flags {
return false
}
return true

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -194,7 +194,7 @@ final class AvatarEditorScreenComponent: Component {
private func updateData(_ data: KeyboardInputData) {
self.data = data
self.state?.selectedItem = data.emoji.itemGroups.first?.items.first
self.state?.selectedItem = data.emoji.panelItemGroups.first?.items.first
self.state?.updated(transition: .immediate)
let updateSearchQuery: (String, String) -> Void = { [weak self] rawQuery, languageCode in
@ -735,9 +735,9 @@ final class AvatarEditorScreenComponent: Component {
}
if state?.keyboardContentId == AnyHashable("emoji") {
data.emoji = data.emoji.withUpdatedItemGroups(itemGroups: searchResult.groups, itemContentUniqueId: searchResult.id, emptySearchResults: emptySearchResults)
data.emoji = data.emoji.withUpdatedItemGroups(panelItemGroups: data.emoji.panelItemGroups, contentItemGroups: searchResult.groups, itemContentUniqueId: searchResult.id, emptySearchResults: emptySearchResults)
} else {
data.stickers = data.stickers?.withUpdatedItemGroups(itemGroups: searchResult.groups, itemContentUniqueId: searchResult.id, emptySearchResults: emptySearchResults)
data.stickers = data.stickers?.withUpdatedItemGroups(panelItemGroups: data.stickers?.panelItemGroups ?? searchResult.groups, contentItemGroups: searchResult.groups, itemContentUniqueId: searchResult.id, emptySearchResults: emptySearchResults)
}
}

View File

@ -1196,7 +1196,7 @@ public final class ChatEntityKeyboardInputNode: ChatInputNode {
iconFile: nil
)
}
inputData.emoji = inputData.emoji.withUpdatedItemGroups(itemGroups: emojiSearchResult.groups, itemContentUniqueId: emojiSearchResult.id, emptySearchResults: emptySearchResults)
inputData.emoji = inputData.emoji.withUpdatedItemGroups(panelItemGroups: inputData.emoji.panelItemGroups, contentItemGroups: emojiSearchResult.groups, itemContentUniqueId: emojiSearchResult.id, emptySearchResults: emptySearchResults)
}
var transition: Transition = .immediate
@ -1586,9 +1586,9 @@ public final class ChatEntityKeyboardInputNode: ChatInputNode {
private func processInputData(inputData: InputData) -> InputData {
return InputData(
emoji: inputData.emoji.withUpdatedItemGroups(itemGroups: self.processStableItemGroupList(category: .emoji, itemGroups: inputData.emoji.itemGroups), itemContentUniqueId: inputData.emoji.itemContentUniqueId, emptySearchResults: inputData.emoji.emptySearchResults),
emoji: inputData.emoji.withUpdatedItemGroups(panelItemGroups: self.processStableItemGroupList(category: .emoji, itemGroups: inputData.emoji.panelItemGroups), contentItemGroups: self.processStableItemGroupList(category: .emoji, itemGroups: inputData.emoji.contentItemGroups), itemContentUniqueId: inputData.emoji.itemContentUniqueId, emptySearchResults: inputData.emoji.emptySearchResults),
stickers: inputData.stickers.flatMap { stickers in
return stickers.withUpdatedItemGroups(itemGroups: self.processStableItemGroupList(category: .stickers, itemGroups: stickers.itemGroups), itemContentUniqueId: nil, emptySearchResults: nil)
return stickers.withUpdatedItemGroups(panelItemGroups: self.processStableItemGroupList(category: .stickers, itemGroups: stickers.panelItemGroups), contentItemGroups: self.processStableItemGroupList(category: .stickers, itemGroups: stickers.contentItemGroups), itemContentUniqueId: nil, emptySearchResults: nil)
},
gifs: inputData.gifs,
availableGifSearchEmojies: inputData.availableGifSearchEmojies

View File

@ -371,7 +371,7 @@ public final class EmojiStatusSelectionController: ViewController {
} else {
strongSelf.stableEmptyResultEmoji = nil
}
emojiContent = emojiContent.withUpdatedItemGroups(itemGroups: emojiSearchResult.groups, itemContentUniqueId: emojiSearchResult.id, emptySearchResults: emptySearchResults)
emojiContent = emojiContent.withUpdatedItemGroups(panelItemGroups: emojiContent.panelItemGroups, contentItemGroups: emojiSearchResult.groups, itemContentUniqueId: emojiSearchResult.id, emptySearchResults: emptySearchResults)
} else {
strongSelf.stableEmptyResultEmoji = nil
}

View File

@ -1520,8 +1520,10 @@ public final class EmojiSearchHeaderView: UIView, UITextFieldDelegate {
var text: String
var useOpaqueTheme: Bool
var isActive: Bool
var hasPresetSearch: Bool
var size: CGSize
var canFocus: Bool
var hasSearchItems: Bool
static func ==(lhs: Params, rhs: Params) -> Bool {
if lhs.theme !== rhs.theme {
@ -1539,12 +1541,18 @@ public final class EmojiSearchHeaderView: UIView, UITextFieldDelegate {
if lhs.isActive != rhs.isActive {
return false
}
if lhs.hasPresetSearch != rhs.hasPresetSearch {
return false
}
if lhs.size != rhs.size {
return false
}
if lhs.canFocus != rhs.canFocus {
return false
}
if lhs.hasSearchItems != rhs.hasSearchItems {
return false
}
return true
}
}
@ -1565,6 +1573,9 @@ public final class EmojiSearchHeaderView: UIView, UITextFieldDelegate {
private let searchIconView: UIImageView
private let searchIconTintView: UIImageView
private let backIconView: UIImageView
private let backIconTintView: UIImageView
private let clearIconView: UIImageView
private let clearIconTintView: UIImageView
private let clearIconButton: HighlightTrackingButton
@ -1575,9 +1586,12 @@ public final class EmojiSearchHeaderView: UIView, UITextFieldDelegate {
private let cancelButtonTitle: ComponentView<Empty>
private let cancelButton: HighlightTrackingButton
private var suggestedItemsView: ComponentView<Empty>?
private var textField: EmojiSearchTextField?
private var tapRecognizer: UITapGestureRecognizer?
private(set) var currentPresetSearchTerm: String?
private var params: Params?
@ -1598,6 +1612,9 @@ public final class EmojiSearchHeaderView: UIView, UITextFieldDelegate {
self.searchIconView = UIImageView()
self.searchIconTintView = UIImageView()
self.backIconView = UIImageView()
self.backIconTintView = UIImageView()
self.clearIconView = UIImageView()
self.clearIconTintView = UIImageView()
self.clearIconButton = HighlightableButton()
@ -1619,6 +1636,9 @@ public final class EmojiSearchHeaderView: UIView, UITextFieldDelegate {
self.addSubview(self.searchIconView)
self.tintContainerView.addSubview(self.searchIconTintView)
self.addSubview(self.backIconView)
self.tintContainerView.addSubview(self.backIconTintView)
self.addSubview(self.clearIconView)
self.tintContainerView.addSubview(self.clearIconTintView)
self.addSubview(self.clearIconButton)
@ -1681,25 +1701,38 @@ public final class EmojiSearchHeaderView: UIView, UITextFieldDelegate {
@objc private func tapGesture(_ recognizer: UITapGestureRecognizer) {
if case .ended = recognizer.state {
if self.textField == nil, let textComponentView = self.textView.view, self.params?.canFocus == true {
let backgroundFrame = self.backgroundLayer.frame
let textFieldFrame = CGRect(origin: CGPoint(x: textComponentView.frame.minX, y: backgroundFrame.minY), size: CGSize(width: backgroundFrame.maxX - textComponentView.frame.minX, height: backgroundFrame.height))
let location = recognizer.location(in: self)
if self.backIconView.frame.contains(location) {
if let suggestedItemsView = self.suggestedItemsView?.view as? EmojiSearchSearchBarComponent.View {
suggestedItemsView.clearSelection(dispatchEvent : true)
}
} else {
if self.textField == nil, let textComponentView = self.textView.view, self.params?.canFocus == true {
let backgroundFrame = self.backgroundLayer.frame
let textFieldFrame = CGRect(origin: CGPoint(x: textComponentView.frame.minX, y: backgroundFrame.minY), size: CGSize(width: backgroundFrame.maxX - textComponentView.frame.minX, height: backgroundFrame.height))
let textField = EmojiSearchTextField(frame: textFieldFrame)
textField.autocorrectionType = .no
self.textField = textField
self.insertSubview(textField, belowSubview: self.clearIconView)
textField.delegate = self
textField.addTarget(self, action: #selector(self.textFieldChanged(_:)), for: .editingChanged)
}
let textField = EmojiSearchTextField(frame: textFieldFrame)
textField.autocorrectionType = .no
self.textField = textField
self.insertSubview(textField, belowSubview: self.clearIconView)
textField.delegate = self
textField.addTarget(self, action: #selector(self.textFieldChanged(_:)), for: .editingChanged)
self.currentPresetSearchTerm = nil
if let suggestedItemsView = self.suggestedItemsView?.view as? EmojiSearchSearchBarComponent.View {
suggestedItemsView.clearSelection(dispatchEvent: false)
}
self.activated()
self.textField?.becomeFirstResponder()
}
self.activated()
self.textField?.becomeFirstResponder()
}
}
@objc private func cancelPressed() {
self.currentPresetSearchTerm = nil
self.updateQuery("", "en")
self.clearIconView.isHidden = true
@ -1720,6 +1753,7 @@ public final class EmojiSearchHeaderView: UIView, UITextFieldDelegate {
}
@objc private func clearPressed() {
self.currentPresetSearchTerm = nil
self.updateQuery("", "en")
self.textField?.text = ""
@ -1767,6 +1801,7 @@ public final class EmojiSearchHeaderView: UIView, UITextFieldDelegate {
self.clearIconTintView.isHidden = text.isEmpty
self.clearIconButton.isHidden = text.isEmpty
self.currentPresetSearchTerm = nil
self.updateQuery(text, inputLanguage)
}
@ -1775,28 +1810,37 @@ public final class EmojiSearchHeaderView: UIView, UITextFieldDelegate {
return
}
self.params = nil
self.update(theme: params.theme, strings: params.strings, text: params.text, useOpaqueTheme: params.useOpaqueTheme, isActive: params.isActive, size: params.size, canFocus: params.canFocus, transition: transition)
self.update(theme: params.theme, strings: params.strings, text: params.text, useOpaqueTheme: params.useOpaqueTheme, isActive: params.isActive, size: params.size, canFocus: params.canFocus, hasSearchItems: params.hasSearchItems, transition: transition)
}
public func update(theme: PresentationTheme, strings: PresentationStrings, text: String, useOpaqueTheme: Bool, isActive: Bool, size: CGSize, canFocus: Bool, transition: Transition) {
public func update(theme: PresentationTheme, strings: PresentationStrings, text: String, useOpaqueTheme: Bool, isActive: Bool, size: CGSize, canFocus: Bool, hasSearchItems: Bool, transition: Transition) {
let params = Params(
theme: theme,
strings: strings,
text: text,
useOpaqueTheme: useOpaqueTheme,
isActive: isActive,
hasPresetSearch: self.currentPresetSearchTerm == nil,
size: size,
canFocus: canFocus
canFocus: canFocus,
hasSearchItems: hasSearchItems
)
if self.params == params {
return
}
let isActiveWithText = isActive && self.currentPresetSearchTerm == nil
let isLeftAligned = isActiveWithText || hasSearchItems
if self.params?.theme !== theme {
self.searchIconView.image = generateTintedImage(image: UIImage(bundleImageName: "Components/Search Bar/Loupe"), color: theme.chat.inputMediaPanel.panelContentVibrantOverlayColor)
self.searchIconTintView.image = generateTintedImage(image: UIImage(bundleImageName: "Components/Search Bar/Loupe"), color: .white)
self.backIconView.image = generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Back"), color: theme.chat.inputMediaPanel.panelContentVibrantOverlayColor)
self.backIconTintView.image = generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Back"), color: .white)
self.clearIconView.image = generateTintedImage(image: UIImage(bundleImageName: "Components/Search Bar/Clear"), color: theme.chat.inputMediaPanel.panelContentVibrantOverlayColor)
self.clearIconTintView.image = generateTintedImage(image: UIImage(bundleImageName: "Components/Search Bar/Clear"), color: .white)
}
@ -1864,7 +1908,7 @@ public final class EmojiSearchHeaderView: UIView, UITextFieldDelegate {
let cancelButtonSpacing: CGFloat = 8.0
var backgroundFrame = CGRect(origin: CGPoint(x: sideInset, y: topInset), size: CGSize(width: size.width - sideInset * 2.0, height: inputHeight))
if isActive {
if isActiveWithText {
backgroundFrame.size.width -= cancelTextSize.width + cancelButtonSpacing
}
transition.setFrame(layer: self.backgroundLayer, frame: backgroundFrame)
@ -1873,14 +1917,96 @@ public final class EmojiSearchHeaderView: UIView, UITextFieldDelegate {
transition.setFrame(view: self.cancelButton, frame: CGRect(origin: CGPoint(x: backgroundFrame.maxX, y: 0.0), size: CGSize(width: cancelButtonSpacing + cancelTextSize.width, height: size.height)))
var textFrame = CGRect(origin: CGPoint(x: backgroundFrame.minX + floor((backgroundFrame.width - textSize.width) / 2.0), y: backgroundFrame.minY + floor((backgroundFrame.height - textSize.height) / 2.0)), size: textSize)
if isActive {
if isLeftAligned {
textFrame.origin.x = backgroundFrame.minX + sideTextInset
}
if let image = self.searchIconView.image {
let iconFrame = CGRect(origin: CGPoint(x: textFrame.minX - image.size.width - 4.0, y: backgroundFrame.minY + floor((backgroundFrame.height - image.size.height) / 2.0)), size: image.size)
transition.setFrame(view: self.searchIconView, frame: iconFrame)
transition.setFrame(view: self.searchIconTintView, frame: iconFrame)
transition.setBounds(view: self.searchIconView, bounds: CGRect(origin: CGPoint(), size: iconFrame.size))
transition.setPosition(view: self.searchIconView, position: iconFrame.center)
transition.setBounds(view: self.searchIconTintView, bounds: CGRect(origin: CGPoint(), size: iconFrame.size))
transition.setPosition(view: self.searchIconTintView, position: iconFrame.center)
transition.setScale(view: self.searchIconView, scale: self.currentPresetSearchTerm == nil ? 1.0 : 0.001)
transition.setAlpha(view: self.searchIconView, alpha: self.currentPresetSearchTerm == nil ? 1.0 : 0.0)
transition.setScale(view: self.searchIconTintView, scale: self.currentPresetSearchTerm == nil ? 1.0 : 0.001)
transition.setAlpha(view: self.searchIconTintView, alpha: self.currentPresetSearchTerm == nil ? 1.0 : 0.0)
}
if let image = self.backIconView.image {
let iconFrame = CGRect(origin: CGPoint(x: textFrame.minX - image.size.width - 4.0, y: backgroundFrame.minY + floor((backgroundFrame.height - image.size.height) / 2.0)), size: image.size)
transition.setBounds(view: self.backIconView, bounds: CGRect(origin: CGPoint(), size: iconFrame.size))
transition.setPosition(view: self.backIconView, position: iconFrame.center)
transition.setBounds(view: self.backIconTintView, bounds: CGRect(origin: CGPoint(), size: iconFrame.size))
transition.setPosition(view: self.backIconTintView, position: iconFrame.center)
transition.setScale(view: self.backIconView, scale: self.currentPresetSearchTerm != nil ? 1.0 : 0.001)
transition.setAlpha(view: self.backIconView, alpha: self.currentPresetSearchTerm != nil ? 1.0 : 0.0)
transition.setScale(view: self.backIconTintView, scale: self.currentPresetSearchTerm != nil ? 1.0 : 0.001)
transition.setAlpha(view: self.backIconTintView, alpha: self.currentPresetSearchTerm != nil ? 1.0 : 0.0)
}
if hasSearchItems {
let suggestedItemsView: ComponentView<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 {
@ -2389,7 +2515,8 @@ public final class EmojiPagerContentComponent: Component {
public let animationCache: AnimationCache
public let animationRenderer: MultiAnimationRenderer
public let inputInteractionHolder: InputInteractionHolder
public let itemGroups: [ItemGroup]
public let panelItemGroups: [ItemGroup]
public let contentItemGroups: [ItemGroup]
public let itemLayoutType: ItemLayoutType
public let itemContentUniqueId: AnyHashable?
public let warpContentsOnEdges: Bool
@ -2407,7 +2534,8 @@ public final class EmojiPagerContentComponent: Component {
animationCache: AnimationCache,
animationRenderer: MultiAnimationRenderer,
inputInteractionHolder: InputInteractionHolder,
itemGroups: [ItemGroup],
panelItemGroups: [ItemGroup],
contentItemGroups: [ItemGroup],
itemLayoutType: ItemLayoutType,
itemContentUniqueId: AnyHashable?,
warpContentsOnEdges: Bool,
@ -2424,7 +2552,8 @@ public final class EmojiPagerContentComponent: Component {
self.animationCache = animationCache
self.animationRenderer = animationRenderer
self.inputInteractionHolder = inputInteractionHolder
self.itemGroups = itemGroups
self.panelItemGroups = panelItemGroups
self.contentItemGroups = contentItemGroups
self.itemLayoutType = itemLayoutType
self.itemContentUniqueId = itemContentUniqueId
self.warpContentsOnEdges = warpContentsOnEdges
@ -2436,7 +2565,7 @@ public final class EmojiPagerContentComponent: Component {
self.selectedItems = selectedItems
}
public func withUpdatedItemGroups(itemGroups: [ItemGroup], itemContentUniqueId: AnyHashable?, emptySearchResults: EmptySearchResults?) -> EmojiPagerContentComponent {
public func withUpdatedItemGroups(panelItemGroups: [ItemGroup], contentItemGroups: [ItemGroup], itemContentUniqueId: AnyHashable?, emptySearchResults: EmptySearchResults?) -> EmojiPagerContentComponent {
return EmojiPagerContentComponent(
id: self.id,
context: self.context,
@ -2444,7 +2573,8 @@ public final class EmojiPagerContentComponent: Component {
animationCache: self.animationCache,
animationRenderer: self.animationRenderer,
inputInteractionHolder: self.inputInteractionHolder,
itemGroups: itemGroups,
panelItemGroups: panelItemGroups,
contentItemGroups: contentItemGroups,
itemLayoutType: self.itemLayoutType,
itemContentUniqueId: itemContentUniqueId,
warpContentsOnEdges: self.warpContentsOnEdges,
@ -2479,7 +2609,10 @@ public final class EmojiPagerContentComponent: Component {
if lhs.inputInteractionHolder !== rhs.inputInteractionHolder {
return false
}
if lhs.itemGroups != rhs.itemGroups {
if lhs.panelItemGroups != rhs.panelItemGroups {
return false
}
if lhs.contentItemGroups != rhs.contentItemGroups {
return false
}
if lhs.itemLayoutType != rhs.itemLayoutType {
@ -4148,7 +4281,7 @@ public final class EmojiPagerContentComponent: Component {
var subgroupItemIndex: Int?
if group.supergroupId == supergroupId {
if let subgroupId = subgroupId {
inner: for itemGroup in component.itemGroups {
inner: for itemGroup in component.contentItemGroups {
if itemGroup.supergroupId == supergroupId {
for i in 0 ..< itemGroup.items.count {
if itemGroup.items[i].subgroupId == subgroupId {
@ -4858,7 +4991,7 @@ public final class EmojiPagerContentComponent: Component {
self.updateScrollingOffset(isReset: false, transition: .immediate)
if self.isSearchActivated {
if self.isSearchActivated, let visibleSearchHeader = self.visibleSearchHeader, visibleSearchHeader.currentPresetSearchTerm == nil {
self.visibleSearchHeader?.deactivate()
}
}
@ -4884,9 +5017,9 @@ public final class EmojiPagerContentComponent: Component {
return
}
let isInteracting = scrollView.isDragging || scrollView.isDecelerating
if let previousScrollingOffsetValue = self.previousScrollingOffset, !self.keepTopPanelVisibleUntilScrollingInput {
let currentBounds = scrollView.bounds
let isInteracting = self.scrollView.isDragging || self.scrollView.isDecelerating
if let previousScrollingOffsetValue = self.previousScrollingOffset, !self.keepTopPanelVisibleUntilScrollingInput, !self.isSearchActivated {
let currentBounds = self.scrollView.bounds
let offsetToTopEdge = max(0.0, currentBounds.minY - 0.0)
let offsetToBottomEdge = max(0.0, scrollView.contentSize.height - currentBounds.maxY)
@ -4968,7 +5101,7 @@ public final class EmojiPagerContentComponent: Component {
}
for groupItems in itemLayout.visibleItems(for: effectiveVisibleBounds) {
let itemGroup = component.itemGroups[groupItems.groupIndex]
let itemGroup = component.contentItemGroups[groupItems.groupIndex]
let itemGroupLayout = itemLayout.itemGroupLayouts[groupItems.groupIndex]
var assignTopVisibleSubgroupId = false
@ -5875,11 +6008,11 @@ public final class EmojiPagerContentComponent: Component {
var previousAbsoluteItemPositions: [VisualItemKey: CGPoint] = [:]
var anchorItems: [ItemLayer.Key: CGRect] = [:]
if let previousComponent = previousComponent, let previousItemLayout = self.itemLayout, previousComponent.itemGroups != component.itemGroups {
if let previousComponent = previousComponent, let previousItemLayout = self.itemLayout, previousComponent.contentItemGroups != component.contentItemGroups, previousComponent.itemContentUniqueId == component.itemContentUniqueId {
if !transition.animation.isImmediate {
var previousItemPositionsValue: [VisualItemKey: CGPoint] = [:]
for groupIndex in 0 ..< previousComponent.itemGroups.count {
let itemGroup = previousComponent.itemGroups[groupIndex]
for groupIndex in 0 ..< previousComponent.contentItemGroups.count {
let itemGroup = previousComponent.contentItemGroups[groupIndex]
for itemIndex in 0 ..< itemGroup.items.count {
let item = itemGroup.items[itemIndex]
let itemKey: ItemLayer.Key
@ -5976,7 +6109,7 @@ public final class EmojiPagerContentComponent: Component {
}
var itemGroups: [ItemGroupDescription] = []
for itemGroup in component.itemGroups {
for itemGroup in component.contentItemGroups {
itemGroups.append(ItemGroupDescription(
supergroupId: itemGroup.supergroupId,
groupId: itemGroup.groupId,
@ -6021,15 +6154,14 @@ public final class EmojiPagerContentComponent: Component {
let scrollOriginY: CGFloat = 0.0
let scrollSize = CGSize(width: availableSize.width, height: availableSize.height)
transition.setPosition(view: self.scrollView, position: CGPoint(x: 0.0, y: scrollOriginY))
transition.setFrame(view: self.scrollViewClippingView, frame: CGRect(origin: CGPoint(x: 0.0, y: self.isSearchActivated ? itemLayout.searchHeight : 0.0), size: availableSize))
transition.setBounds(view: self.scrollViewClippingView, bounds: CGRect(origin: CGPoint(x: 0.0, y: self.isSearchActivated ? itemLayout.searchHeight : 0.0), size: availableSize))
transition.setFrame(view: self.scrollViewClippingView, frame: CGRect(origin: CGPoint(x: 0.0, y: self.isSearchActivated ? itemLayout.itemInsets.top : 0.0), size: availableSize))
transition.setBounds(view: self.scrollViewClippingView, bounds: CGRect(origin: CGPoint(x: 0.0, y: self.isSearchActivated ? itemLayout.itemInsets.top : 0.0), size: availableSize))
transition.setFrame(view: self.vibrancyClippingView, frame: CGRect(origin: CGPoint(x: 0.0, y: self.isSearchActivated ? itemLayout.searchHeight : 0.0), size: availableSize))
transition.setBounds(view: self.vibrancyClippingView, bounds: CGRect(origin: CGPoint(x: 0.0, y: self.isSearchActivated ? itemLayout.searchHeight : 0.0), size: availableSize))
transition.setFrame(view: self.vibrancyClippingView, frame: CGRect(origin: CGPoint(x: 0.0, y: self.isSearchActivated ? itemLayout.itemInsets.top : 0.0), size: availableSize))
transition.setBounds(view: self.vibrancyClippingView, bounds: CGRect(origin: CGPoint(x: 0.0, y: self.isSearchActivated ? itemLayout.itemInsets.top : 0.0), size: availableSize))
let previousSize = self.scrollView.bounds.size
var resetScrolling = false
@ -6041,6 +6173,10 @@ public final class EmojiPagerContentComponent: Component {
}
self.scrollView.bounds = CGRect(origin: self.scrollView.bounds.origin, size: scrollSize)
if resetScrolling {
itemTransition = .immediate
}
let warpHeight: CGFloat = 50.0
var topWarpInset = pagerEnvironment.containerInsets.top
if self.isSearchActivated {
@ -6100,16 +6236,16 @@ public final class EmojiPagerContentComponent: Component {
}
})
outer: for i in 0 ..< component.itemGroups.count {
outer: for i in 0 ..< component.contentItemGroups.count {
for anchorItem in sortedAnchorItems {
if component.itemGroups[i].groupId != anchorItem.0.groupId {
if component.contentItemGroups[i].groupId != anchorItem.0.groupId {
continue
}
for j in 0 ..< component.itemGroups[i].items.count {
for j in 0 ..< component.contentItemGroups[i].items.count {
let itemKey: ItemLayer.Key
itemKey = ItemLayer.Key(
groupId: component.itemGroups[i].groupId,
itemId: component.itemGroups[i].items[j].content.id
groupId: component.contentItemGroups[i].groupId,
itemId: component.contentItemGroups[i].items[j].content.id
)
if itemKey == anchorItem.0 {
@ -6137,19 +6273,15 @@ public final class EmojiPagerContentComponent: Component {
}
if resetScrolling {
if component.displaySearchWithPlaceholder != nil && !self.isSearchActivated && component.searchInitiallyHidden {
self.scrollView.bounds = CGRect(origin: CGPoint(x: 0.0, y: 50.0), size: scrollSize)
} else {
self.scrollView.bounds = CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: scrollSize)
}
self.scrollView.bounds = CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: scrollSize)
}
self.ignoreScrolling = false
if calculateUpdatedItemPositions {
var updatedItemPositionsValue: [VisualItemKey: CGPoint] = [:]
for groupIndex in 0 ..< component.itemGroups.count {
let itemGroup = component.itemGroups[groupIndex]
for groupIndex in 0 ..< component.contentItemGroups.count {
let itemGroup = component.contentItemGroups[groupIndex]
let itemGroupLayout = itemLayout.itemGroupLayouts[groupIndex]
for itemIndex in 0 ..< itemGroup.items.count {
let item = itemGroup.items[itemIndex]
@ -6204,14 +6336,16 @@ public final class EmojiPagerContentComponent: Component {
}
}
} else {
/*if visibleSearchHeader.superview != self.scrollView {
self.scrollView.addSubview(visibleSearchHeader)
self.mirrorContentScrollView.addSubview(visibleSearchHeader.tintContainerView)
/*if component.inputInteractionHolder.inputInteraction?.externalBackground == nil {
if visibleSearchHeader.superview != self.scrollView {
self.scrollView.addSubview(visibleSearchHeader)
self.mirrorContentScrollView.addSubview(visibleSearchHeader.tintContainerView)
}
}*/
}
} else {
visibleSearchHeader = EmojiSearchHeaderView(activated: { [weak self] in
guard let strongSelf = self else {
guard let strongSelf = self, let visibleSearchHeader = strongSelf.visibleSearchHeader else {
return
}
@ -6219,7 +6353,9 @@ public final class EmojiPagerContentComponent: Component {
component.inputInteractionHolder.inputInteraction?.openSearch()
} else {
strongSelf.isSearchActivated = true
strongSelf.pagerEnvironment?.onWantsExclusiveModeUpdated(true)
if visibleSearchHeader.currentPresetSearchTerm == nil {
strongSelf.pagerEnvironment?.onWantsExclusiveModeUpdated(true)
}
strongSelf.component?.inputInteractionHolder.inputInteraction?.requestUpdate(.immediate)
}
}, deactivated: { [weak self] isFirstResponder in
@ -6255,8 +6391,8 @@ public final class EmojiPagerContentComponent: Component {
}
let searchHeaderFrame = CGRect(origin: CGPoint(x: itemLayout.searchInsets.left, y: itemLayout.searchInsets.top), size: CGSize(width: itemLayout.width - itemLayout.searchInsets.left - itemLayout.searchInsets.right, height: itemLayout.searchHeight))
visibleSearchHeader.update(theme: keyboardChildEnvironment.theme, strings: keyboardChildEnvironment.strings, text: displaySearchWithPlaceholder, useOpaqueTheme: useOpaqueTheme, isActive: self.isSearchActivated, size: searchHeaderFrame.size, canFocus: !component.searchIsPlaceholderOnly, transition: transition)
transition.setFrame(view: visibleSearchHeader, frame: searchHeaderFrame, completion: { [weak self] completed in
visibleSearchHeader.update(theme: keyboardChildEnvironment.theme, strings: keyboardChildEnvironment.strings, text: displaySearchWithPlaceholder, useOpaqueTheme: useOpaqueTheme, isActive: self.isSearchActivated, size: searchHeaderFrame.size, canFocus: !component.searchIsPlaceholderOnly, hasSearchItems: component.displaySearchWithPlaceholder == keyboardChildEnvironment.strings.EmojiSearch_SearchEmojiPlaceholder, transition: transition)
transition.attachAnimation(view: visibleSearchHeader, completion: { [weak self] completed in
guard let strongSelf = self, completed, let visibleSearchHeader = strongSelf.visibleSearchHeader else {
return
}
@ -6266,6 +6402,7 @@ public final class EmojiPagerContentComponent: Component {
strongSelf.mirrorContentScrollView.addSubview(visibleSearchHeader.tintContainerView)
}
})
transition.setFrame(view: visibleSearchHeader, frame: searchHeaderFrame)
} else {
if let visibleSearchHeader = self.visibleSearchHeader {
self.visibleSearchHeader = nil
@ -6307,8 +6444,30 @@ public final class EmojiPagerContentComponent: Component {
}
}
var animateContentCrossfade = false
if let previousComponent, previousComponent.itemContentUniqueId != component.itemContentUniqueId, itemTransition.animation.isImmediate, !transition.animation.isImmediate {
animateContentCrossfade = true
}
if animateContentCrossfade {
for (_, itemLayer) in self.visibleItemLayers {
if let snapshotLayer = itemLayer.snapshotContentTree() {
itemLayer.superlayer?.insertSublayer(snapshotLayer, above: itemLayer)
snapshotLayer.animateAlpha(from: CGFloat(snapshotLayer.opacity), to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { [weak snapshotLayer] _ in
snapshotLayer?.removeFromSuperlayer()
})
}
}
}
self.updateVisibleItems(transition: itemTransition, attemptSynchronousLoads: attemptSynchronousLoads, previousItemPositions: previousItemPositions, previousAbsoluteItemPositions: previousAbsoluteItemPositions, updatedItemPositions: updatedItemPositions, hintDisappearingGroupFrame: hintDisappearingGroupFrame)
if animateContentCrossfade {
for (_, itemLayer) in self.visibleItemLayers {
itemLayer.animateAlpha(from: 0.0, to: CGFloat(itemLayer.opacity), duration: 0.2)
}
}
return availableSize
}
}
@ -7127,6 +7286,41 @@ public final class EmojiPagerContentComponent: Component {
}
}
let allItemGroups = itemGroups.map { group -> EmojiPagerContentComponent.ItemGroup in
var headerItem = group.headerItem
if let groupId = group.id.base as? ItemCollectionId {
outer: for (id, info, _) in view.collectionInfos {
if id == groupId, let info = info as? StickerPackCollectionInfo {
if let thumbnailFileId = info.thumbnailFileId {
for item in group.items {
if let itemFile = item.itemFile, itemFile.fileId.id == thumbnailFileId {
headerItem = EntityKeyboardAnimationData(file: itemFile)
break outer
}
}
}
}
}
}
return EmojiPagerContentComponent.ItemGroup(
supergroupId: group.supergroupId,
groupId: group.id,
title: group.title,
subtitle: group.subtitle,
actionButtonTitle: nil,
isFeatured: group.isFeatured,
isPremiumLocked: group.isPremiumLocked,
isEmbedded: false,
hasClear: group.isClearable,
collapsedLineCount: group.collapsedLineCount,
displayPremiumBadges: false,
headerItem: headerItem,
items: group.items
)
}
return EmojiPagerContentComponent(
id: "emoji",
context: context,
@ -7134,40 +7328,8 @@ public final class EmojiPagerContentComponent: Component {
animationCache: animationCache,
animationRenderer: animationRenderer,
inputInteractionHolder: EmojiPagerContentComponent.InputInteractionHolder(),
itemGroups: itemGroups.map { group -> EmojiPagerContentComponent.ItemGroup in
var headerItem = group.headerItem
if let groupId = group.id.base as? ItemCollectionId {
outer: for (id, info, _) in view.collectionInfos {
if id == groupId, let info = info as? StickerPackCollectionInfo {
if let thumbnailFileId = info.thumbnailFileId {
for item in group.items {
if let itemFile = item.itemFile, itemFile.fileId.id == thumbnailFileId {
headerItem = EntityKeyboardAnimationData(file: itemFile)
break outer
}
}
}
}
}
}
return EmojiPagerContentComponent.ItemGroup(
supergroupId: group.supergroupId,
groupId: group.id,
title: group.title,
subtitle: group.subtitle,
actionButtonTitle: nil,
isFeatured: group.isFeatured,
isPremiumLocked: group.isPremiumLocked,
isEmbedded: false,
hasClear: group.isClearable,
collapsedLineCount: group.collapsedLineCount,
displayPremiumBadges: false,
headerItem: headerItem,
items: group.items
)
},
panelItemGroups: allItemGroups,
contentItemGroups: allItemGroups,
itemLayoutType: .compact,
itemContentUniqueId: nil,
warpContentsOnEdges: isReactionSelection || isStatusSelection,
@ -7644,6 +7806,33 @@ public final class EmojiPagerContentComponent: Component {
let isMasks = stickerNamespaces.contains(Namespaces.ItemCollection.CloudMaskPacks)
let allItemGroups = itemGroups.map { group -> EmojiPagerContentComponent.ItemGroup in
var hasClear = false
var isEmbedded = false
if group.id == AnyHashable("recent") {
hasClear = true
} else if group.id == AnyHashable("featuredTop") {
hasClear = true
isEmbedded = true
}
return EmojiPagerContentComponent.ItemGroup(
supergroupId: group.supergroupId,
groupId: group.id,
title: group.title,
subtitle: group.subtitle,
actionButtonTitle: group.actionButtonTitle,
isFeatured: group.isFeatured,
isPremiumLocked: group.isPremiumLocked,
isEmbedded: isEmbedded,
hasClear: hasClear,
collapsedLineCount: nil,
displayPremiumBadges: group.displayPremiumBadges,
headerItem: group.headerItem,
items: group.items
)
}
return EmojiPagerContentComponent(
id: isMasks ? "masks" : "stickers",
context: context,
@ -7651,32 +7840,8 @@ public final class EmojiPagerContentComponent: Component {
animationCache: animationCache,
animationRenderer: animationRenderer,
inputInteractionHolder: EmojiPagerContentComponent.InputInteractionHolder(),
itemGroups: itemGroups.map { group -> EmojiPagerContentComponent.ItemGroup in
var hasClear = false
var isEmbedded = false
if group.id == AnyHashable("recent") {
hasClear = true
} else if group.id == AnyHashable("featuredTop") {
hasClear = true
isEmbedded = true
}
return EmojiPagerContentComponent.ItemGroup(
supergroupId: group.supergroupId,
groupId: group.id,
title: group.title,
subtitle: group.subtitle,
actionButtonTitle: group.actionButtonTitle,
isFeatured: group.isFeatured,
isPremiumLocked: group.isPremiumLocked,
isEmbedded: isEmbedded,
hasClear: hasClear,
collapsedLineCount: nil,
displayPremiumBadges: group.displayPremiumBadges,
headerItem: group.headerItem,
items: group.items
)
},
panelItemGroups: allItemGroups,
contentItemGroups: allItemGroups,
itemLayoutType: .detailed,
itemContentUniqueId: nil,
warpContentsOnEdges: false,

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

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))
visibleSearchHeader.update(theme: keyboardChildEnvironment.theme, strings: keyboardChildEnvironment.strings, text: displaySearchWithPlaceholder, useOpaqueTheme: false, isActive: false, size: searchHeaderFrame.size, canFocus: false, transition: transition)
visibleSearchHeader.update(theme: keyboardChildEnvironment.theme, strings: keyboardChildEnvironment.strings, text: displaySearchWithPlaceholder, useOpaqueTheme: false, isActive: false, size: searchHeaderFrame.size, canFocus: false, hasSearchItems: true, transition: transition)
transition.setFrame(view: visibleSearchHeader, frame: searchHeaderFrame, completion: { [weak self] completed in
guard let strongSelf = self, completed, let visibleSearchHeader = strongSelf.visibleSearchHeader else {
return

View File

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

View File

@ -3855,6 +3855,19 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
}, displayUndo: { [weak self] content in
if let strongSelf = self {
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
return true
}), in: .current)
@ -4440,7 +4453,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
hasScheduledMessages = peerView.get()
|> take(1)
|> 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)
} else {
return context.account.viewTracker.scheduledMessagesViewForLocation(context.chatLocationInput(for: chatLocation, contextHolder: chatLocationContextHolder))
@ -4864,6 +4877,37 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
.updatedAutoremoveTimeout(autoremoveTimeout)
.updatedCurrentSendAsPeerId(currentSendAsPeerId)
.updatedCopyProtectionEnabled(copyProtectionEnabled)
.updatedInterfaceState { interfaceState in
var interfaceState = interfaceState
if let channel = renderedPeer?.peer as? TelegramChannel {
if channel.hasBannedPermission(.banSendVoice) != nil && channel.hasBannedPermission(.banSendInstantVideos) != nil {
interfaceState = interfaceState.withUpdatedMediaRecordingMode(.audio)
} else if channel.hasBannedPermission(.banSendVoice) != nil {
if channel.hasBannedPermission(.banSendInstantVideos) == nil {
interfaceState = interfaceState.withUpdatedMediaRecordingMode(.video)
}
} else if channel.hasBannedPermission(.banSendInstantVideos) != nil {
if channel.hasBannedPermission(.banSendVoice) == nil {
interfaceState = interfaceState.withUpdatedMediaRecordingMode(.audio)
}
}
} else if let group = renderedPeer?.peer as? TelegramGroup {
if group.hasBannedPermission(.banSendVoice) && group.hasBannedPermission(.banSendInstantVideos) {
interfaceState = interfaceState.withUpdatedMediaRecordingMode(.audio)
} else if group.hasBannedPermission(.banSendVoice) {
if !group.hasBannedPermission(.banSendInstantVideos) {
interfaceState = interfaceState.withUpdatedMediaRecordingMode(.video)
}
} else if group.hasBannedPermission(.banSendInstantVideos) {
if !group.hasBannedPermission(.banSendVoice) {
interfaceState = interfaceState.withUpdatedMediaRecordingMode(.audio)
}
}
}
return interfaceState
}
})
if case .standard(previewing: false) = mode, let channel = renderedPeer?.chatMainPeer as? TelegramChannel, case .broadcast = channel.info {
@ -4995,7 +5039,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
hasScheduledMessages = peerView
|> take(1)
|> mapToSignal { view -> Signal<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)
} else {
return context.account.viewTracker.scheduledMessagesViewForLocation(context.chatLocationInput(for: chatLocation, contextHolder: chatLocationContextHolder))
@ -5271,6 +5315,37 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
return renderedPeer
}.updatedIsNotAccessible(isNotAccessible).updatedContactStatus(contactStatus).updatedHasBots(hasBots).updatedIsArchived(isArchived).updatedPeerIsMuted(peerIsMuted).updatedPeerDiscussionId(peerDiscussionId).updatedPeerGeoLocation(peerGeoLocation).updatedExplicitelyCanPinMessages(explicitelyCanPinMessages).updatedHasScheduledMessages(hasScheduledMessages).updatedCurrentSendAsPeerId(currentSendAsPeerId)
.updatedCopyProtectionEnabled(copyProtectionEnabled)
.updatedInterfaceState { interfaceState in
var interfaceState = interfaceState
if let channel = renderedPeer?.peer as? TelegramChannel {
if channel.hasBannedPermission(.banSendVoice) != nil && channel.hasBannedPermission(.banSendInstantVideos) != nil {
interfaceState = interfaceState.withUpdatedMediaRecordingMode(.audio)
} else if channel.hasBannedPermission(.banSendVoice) != nil {
if channel.hasBannedPermission(.banSendInstantVideos) == nil {
interfaceState = interfaceState.withUpdatedMediaRecordingMode(.video)
}
} else if channel.hasBannedPermission(.banSendInstantVideos) != nil {
if channel.hasBannedPermission(.banSendVoice) == nil {
interfaceState = interfaceState.withUpdatedMediaRecordingMode(.audio)
}
}
} else if let group = renderedPeer?.peer as? TelegramGroup {
if group.hasBannedPermission(.banSendVoice) && group.hasBannedPermission(.banSendInstantVideos) {
interfaceState = interfaceState.withUpdatedMediaRecordingMode(.audio)
} else if group.hasBannedPermission(.banSendVoice) {
if !group.hasBannedPermission(.banSendInstantVideos) {
interfaceState = interfaceState.withUpdatedMediaRecordingMode(.video)
}
} else if group.hasBannedPermission(.banSendInstantVideos) {
if !group.hasBannedPermission(.banSendVoice) {
interfaceState = interfaceState.withUpdatedMediaRecordingMode(.audio)
}
}
}
return interfaceState
}
})
if !strongSelf.didSetChatLocationInfoReady {
strongSelf.didSetChatLocationInfoReady = true
@ -6312,7 +6387,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
}
if let opaqueState = (combinedInitialData.initialData?.storedInterfaceState).flatMap(_internal_decodeStoredChatInterfaceState) {
let interfaceState = ChatInterfaceState.parse(opaqueState)
var interfaceState = ChatInterfaceState.parse(opaqueState)
var pinnedMessageId: MessageId?
var peerIsBlocked: Bool = false
@ -6343,6 +6418,32 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
} else if let _ = combinedInitialData.cachedData as? CachedSecretChatData {
}
if let channel = combinedInitialData.initialData?.peer as? TelegramChannel {
if channel.hasBannedPermission(.banSendVoice) != nil && channel.hasBannedPermission(.banSendInstantVideos) != nil {
interfaceState = interfaceState.withUpdatedMediaRecordingMode(.audio)
} else if channel.hasBannedPermission(.banSendVoice) != nil {
if channel.hasBannedPermission(.banSendInstantVideos) == nil {
interfaceState = interfaceState.withUpdatedMediaRecordingMode(.video)
}
} else if channel.hasBannedPermission(.banSendInstantVideos) != nil {
if channel.hasBannedPermission(.banSendVoice) == nil {
interfaceState = interfaceState.withUpdatedMediaRecordingMode(.audio)
}
}
} else if let group = combinedInitialData.initialData?.peer as? TelegramGroup {
if group.hasBannedPermission(.banSendVoice) && group.hasBannedPermission(.banSendInstantVideos) {
interfaceState = interfaceState.withUpdatedMediaRecordingMode(.audio)
} else if group.hasBannedPermission(.banSendVoice) {
if !group.hasBannedPermission(.banSendInstantVideos) {
interfaceState = interfaceState.withUpdatedMediaRecordingMode(.video)
}
} else if group.hasBannedPermission(.banSendInstantVideos) {
if !group.hasBannedPermission(.banSendVoice) {
interfaceState = interfaceState.withUpdatedMediaRecordingMode(.audio)
}
}
}
if case let .replyThread(replyThreadMessageId) = strongSelf.chatLocation {
if let channel = combinedInitialData.initialData?.peer as? TelegramChannel, channel.flags.contains(.isForum) {
pinnedMessageId = nil
@ -8344,10 +8445,55 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
guard let strongSelf = self else {
return
}
guard let peer = strongSelf.presentationInterfaceState.renderedPeer?.peer else {
return
}
strongSelf.mediaRecordingModeTooltipController?.dismiss()
strongSelf.interfaceInteraction?.updateShowWebView { _ in
return false
}
var bannedMediaInput = false
if let channel = peer as? TelegramChannel {
if channel.hasBannedPermission(.banSendVoice) != nil && channel.hasBannedPermission(.banSendInstantVideos) != nil {
bannedMediaInput = true
} else if channel.hasBannedPermission(.banSendVoice) != nil {
if !isVideo {
//TODO:localize
strongSelf.controllerInteraction?.displayUndo(.info(title: nil, text: "The admins of this group do not allow to send voice messages."))
return
}
} else if channel.hasBannedPermission(.banSendInstantVideos) != nil {
if isVideo {
//TODO:localize
strongSelf.controllerInteraction?.displayUndo(.info(title: nil, text: "The admins of this group do not allow to send video messages."))
return
}
}
} else if let group = peer as? TelegramGroup {
if group.hasBannedPermission(.banSendVoice) && group.hasBannedPermission(.banSendInstantVideos) {
bannedMediaInput = true
} else if group.hasBannedPermission(.banSendVoice) {
if !isVideo {
//TODO:localize
strongSelf.controllerInteraction?.displayUndo(.info(title: nil, text: "The admins of this group do not allow to send voice messages."))
return
}
} else if group.hasBannedPermission(.banSendInstantVideos) {
if isVideo {
//TODO:localize
strongSelf.controllerInteraction?.displayUndo(.info(title: nil, text: "The admins of this group do not allow to send video messages."))
return
}
}
}
if bannedMediaInput {
//TODO:localize
strongSelf.controllerInteraction?.displayUndo(.info(title: nil, text: "The admins of this group do not allow to send video and voice messages."))
return
}
let requestId = strongSelf.beginMediaRecordingRequestId
let begin: () -> Void = {
@ -8607,35 +8753,77 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
tooltipController.dismissImmediately()
}
}, switchMediaRecordingMode: { [weak self] in
if let strongSelf = self {
if strongSelf.recordingModeFeedback == nil {
strongSelf.recordingModeFeedback = HapticFeedback()
strongSelf.recordingModeFeedback?.prepareImpact()
}
strongSelf.recordingModeFeedback?.impact()
var updatedMode: ChatTextInputMediaRecordingButtonMode?
strongSelf.updateChatPresentationInterfaceState(interactive: true, {
return $0.updatedInterfaceState({ current in
let mode: ChatTextInputMediaRecordingButtonMode
switch current.mediaRecordingMode {
case .audio:
mode = .video
case .video:
mode = .audio
}
updatedMode = mode
return current.withUpdatedMediaRecordingMode(mode)
}).updatedShowWebView(false)
})
if let updatedMode = updatedMode, updatedMode == .video {
let _ = ApplicationSpecificNotice.incrementChatMediaMediaRecordingTips(accountManager: strongSelf.context.sharedContext.accountManager, count: 3).start()
}
strongSelf.displayMediaRecordingTooltip()
guard let strongSelf = self else {
return
}
guard let peer = strongSelf.presentationInterfaceState.renderedPeer?.peer else {
return
}
var bannedMediaInput = false
if let channel = peer as? TelegramChannel {
if channel.hasBannedPermission(.banSendVoice) != nil && channel.hasBannedPermission(.banSendInstantVideos) != nil {
bannedMediaInput = true
} else if channel.hasBannedPermission(.banSendVoice) != nil {
if channel.hasBannedPermission(.banSendInstantVideos) == nil {
strongSelf.displayMediaRecordingTooltip()
return
}
} else if channel.hasBannedPermission(.banSendInstantVideos) != nil {
if channel.hasBannedPermission(.banSendVoice) == nil {
strongSelf.displayMediaRecordingTooltip()
return
}
}
} else if let group = peer as? TelegramGroup {
if group.hasBannedPermission(.banSendVoice) && group.hasBannedPermission(.banSendInstantVideos) {
bannedMediaInput = true
} else if group.hasBannedPermission(.banSendVoice) {
if !group.hasBannedPermission(.banSendInstantVideos) {
strongSelf.displayMediaRecordingTooltip()
return
}
} else if group.hasBannedPermission(.banSendInstantVideos) {
if !group.hasBannedPermission(.banSendVoice) {
strongSelf.displayMediaRecordingTooltip()
return
}
}
}
if bannedMediaInput {
//TODO:localize
strongSelf.controllerInteraction?.displayUndo(.info(title: nil, text: "The admins of this group do not allow to send video and voice messages."))
return
}
if strongSelf.recordingModeFeedback == nil {
strongSelf.recordingModeFeedback = HapticFeedback()
strongSelf.recordingModeFeedback?.prepareImpact()
}
strongSelf.recordingModeFeedback?.impact()
var updatedMode: ChatTextInputMediaRecordingButtonMode?
strongSelf.updateChatPresentationInterfaceState(interactive: true, {
return $0.updatedInterfaceState({ current in
let mode: ChatTextInputMediaRecordingButtonMode
switch current.mediaRecordingMode {
case .audio:
mode = .video
case .video:
mode = .audio
}
updatedMode = mode
return current.withUpdatedMediaRecordingMode(mode)
}).updatedShowWebView(false)
})
if let updatedMode = updatedMode, updatedMode == .video {
let _ = ApplicationSpecificNotice.incrementChatMediaMediaRecordingTips(accountManager: strongSelf.context.sharedContext.accountManager, count: 3).start()
}
strongSelf.displayMediaRecordingTooltip()
}, setupMessageAutoremoveTimeout: { [weak self] in
guard let strongSelf = self, case let .peer(peerId) = strongSelf.chatLocation else {
return
@ -11948,25 +12136,54 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
guard let peer = self.presentationInterfaceState.renderedPeer?.peer else {
return
}
let _ = peer
let _ = (self.context.sharedContext.accountManager.transaction { transaction -> GeneratedMediaStoreSettings in
let entry = transaction.getSharedData(ApplicationSpecificSharedDataKeys.generatedMediaStoreSettings)?.get(GeneratedMediaStoreSettings.self)
return entry ?? GeneratedMediaStoreSettings.defaultSettings
}
|> deliverOnMainQueue).start(next: { [weak self] settings in
guard let strongSelf = self else {
guard let strongSelf = self, let peer = strongSelf.presentationInterfaceState.renderedPeer?.peer else {
return
}
var photoOnly = false
var enablePhoto = true
var enableVideo = true
if let callManager = strongSelf.context.sharedContext.callManager as? PresentationCallManagerImpl, callManager.hasActiveCall {
photoOnly = true
enableVideo = false
}
var bannedSendPhotos: (Int32, Bool)?
var bannedSendVideos: (Int32, Bool)?
if let channel = peer as? TelegramChannel {
if let value = channel.hasBannedPermission(.banSendPhotos) {
bannedSendPhotos = value
}
if let value = channel.hasBannedPermission(.banSendVideos) {
bannedSendVideos = value
}
} else if let group = peer as? TelegramGroup {
if group.hasBannedPermission(.banSendPhotos) {
bannedSendPhotos = (Int32.max, false)
}
if group.hasBannedPermission(.banSendVideos) {
bannedSendVideos = (Int32.max, false)
}
}
if bannedSendPhotos != nil {
enablePhoto = false
}
if bannedSendVideos != nil {
enableVideo = false
}
let storeCapturedMedia = peer.id.namespace != Namespaces.Peer.SecretChat
let inputText = strongSelf.presentationInterfaceState.interfaceState.effectiveInputState.inputText
presentedLegacyCamera(context: strongSelf.context, peer: peer, chatLocation: strongSelf.chatLocation, cameraView: cameraView, menuController: nil, parentController: strongSelf, attachmentController: self?.attachmentController, editingMedia: false, saveCapturedPhotos: storeCapturedMedia, mediaGrouping: true, initialCaption: inputText, hasSchedule: strongSelf.presentationInterfaceState.subject != .scheduledMessages && peer.id.namespace != Namespaces.Peer.SecretChat, photoOnly: photoOnly, sendMessagesWithSignals: { [weak self] signals, silentPosting, scheduleTime in
presentedLegacyCamera(context: strongSelf.context, peer: peer, chatLocation: strongSelf.chatLocation, cameraView: cameraView, menuController: nil, parentController: strongSelf, attachmentController: self?.attachmentController, editingMedia: false, saveCapturedPhotos: storeCapturedMedia, mediaGrouping: true, initialCaption: inputText, hasSchedule: strongSelf.presentationInterfaceState.subject != .scheduledMessages && peer.id.namespace != Namespaces.Peer.SecretChat, enablePhoto: enablePhoto, enableVideo: enableVideo, sendMessagesWithSignals: { [weak self] signals, silentPosting, scheduleTime in
if let strongSelf = self {
strongSelf.enqueueMediaMessages(signals: signals, silentPosting: silentPosting, scheduleTime: scheduleTime > 0 ? scheduleTime : nil)
if !inputText.string.isEmpty {
@ -12603,12 +12820,40 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
})
}, openCamera: { [weak self] cameraView, menuController in
if let strongSelf = self, let peer = strongSelf.presentationInterfaceState.renderedPeer?.peer {
var photoOnly = false
var enablePhoto = true
var enableVideo = true
if let callManager = strongSelf.context.sharedContext.callManager as? PresentationCallManagerImpl, callManager.hasActiveCall {
photoOnly = true
enableVideo = false
}
presentedLegacyCamera(context: strongSelf.context, peer: peer, chatLocation: strongSelf.chatLocation, cameraView: cameraView, menuController: menuController, parentController: strongSelf, editingMedia: editMediaOptions != nil, saveCapturedPhotos: peer.id.namespace != Namespaces.Peer.SecretChat, mediaGrouping: true, initialCaption: inputText, hasSchedule: strongSelf.presentationInterfaceState.subject != .scheduledMessages && peer.id.namespace != Namespaces.Peer.SecretChat, photoOnly: photoOnly, sendMessagesWithSignals: { [weak self] signals, silentPosting, scheduleTime in
var bannedSendPhotos: (Int32, Bool)?
var bannedSendVideos: (Int32, Bool)?
if let channel = peer as? TelegramChannel {
if let value = channel.hasBannedPermission(.banSendPhotos) {
bannedSendPhotos = value
}
if let value = channel.hasBannedPermission(.banSendVideos) {
bannedSendVideos = value
}
} else if let group = peer as? TelegramGroup {
if group.hasBannedPermission(.banSendPhotos) {
bannedSendPhotos = (Int32.max, false)
}
if group.hasBannedPermission(.banSendVideos) {
bannedSendVideos = (Int32.max, false)
}
}
if bannedSendPhotos != nil {
enablePhoto = false
}
if bannedSendVideos != nil {
enableVideo = false
}
presentedLegacyCamera(context: strongSelf.context, peer: peer, chatLocation: strongSelf.chatLocation, cameraView: cameraView, menuController: menuController, parentController: strongSelf, editingMedia: editMediaOptions != nil, saveCapturedPhotos: peer.id.namespace != Namespaces.Peer.SecretChat, mediaGrouping: true, initialCaption: inputText, hasSchedule: strongSelf.presentationInterfaceState.subject != .scheduledMessages && peer.id.namespace != Namespaces.Peer.SecretChat, enablePhoto: enablePhoto, enableVideo: enableVideo, sendMessagesWithSignals: { [weak self] signals, silentPosting, scheduleTime in
if let strongSelf = self {
if editMediaOptions != nil {
strongSelf.editMessageMediaWithLegacySignals(signals!)
@ -13096,8 +13341,93 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
}
})
}))
controller.getCaptionPanelView = { [weak self] in
return self?.getCaptionPanelView()
controller.attemptItemSelection = { [weak strongSelf] item in
guard let strongSelf, let peer = strongSelf.presentationInterfaceState.renderedPeer?.peer else {
return false
}
enum ItemType {
case gif
case image
case video
}
var itemType: ItemType?
switch item {
case let .internalReference(reference):
if reference.type == "gif" {
itemType = .gif
} else if reference.type == "photo" {
itemType = .image
} else if reference.type == "video" {
itemType = .video
}
case let .externalReference(reference):
if reference.type == "gif" {
itemType = .gif
} else if reference.type == "photo" {
itemType = .image
} else if reference.type == "video" {
itemType = .video
}
}
var bannedSendPhotos: (Int32, Bool)?
var bannedSendVideos: (Int32, Bool)?
var bannedSendGifs: (Int32, Bool)?
if let channel = peer as? TelegramChannel {
if let value = channel.hasBannedPermission(.banSendPhotos) {
bannedSendPhotos = value
}
if let value = channel.hasBannedPermission(.banSendVideos) {
bannedSendVideos = value
}
if let value = channel.hasBannedPermission(.banSendGifs) {
bannedSendGifs = value
}
} else if let group = peer as? TelegramGroup {
if group.hasBannedPermission(.banSendPhotos) {
bannedSendPhotos = (Int32.max, false)
}
if group.hasBannedPermission(.banSendVideos) {
bannedSendVideos = (Int32.max, false)
}
if group.hasBannedPermission(.banSendGifs) {
bannedSendGifs = (Int32.max, false)
}
}
if let itemType {
switch itemType {
case .image:
if bannedSendPhotos != nil {
//TODO:localize
strongSelf.present(standardTextAlertController(theme: AlertControllerTheme(presentationData: strongSelf.presentationData), title: nil, text: "Sending photos is not allowed", actions: [TextAlertAction(type: .defaultAction, title: strongSelf.presentationData.strings.Common_OK, action: {})]), in: .window(.root))
return false
}
case .video:
if bannedSendVideos != nil {
//TODO:localize
strongSelf.present(standardTextAlertController(theme: AlertControllerTheme(presentationData: strongSelf.presentationData), title: nil, text: "Sending videos is not allowed", actions: [TextAlertAction(type: .defaultAction, title: strongSelf.presentationData.strings.Common_OK, action: {})]), in: .window(.root))
return false
}
case .gif:
if bannedSendGifs != nil {
//TODO:localize
strongSelf.present(standardTextAlertController(theme: AlertControllerTheme(presentationData: strongSelf.presentationData), title: nil, text: "Sending gifs is not allowed", actions: [TextAlertAction(type: .defaultAction, title: strongSelf.presentationData.strings.Common_OK, action: {})]), in: .window(.root))
return false
}
}
}
return true
}
controller.getCaptionPanelView = { [weak strongSelf] in
return strongSelf?.getCaptionPanelView()
}
present(controller, ViewControllerPresentationArguments(presentationAnimation: .modalSheet))
}
@ -16597,15 +16927,55 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
}
private func displayMediaRecordingTooltip() {
guard let peer = self.presentationInterfaceState.renderedPeer?.peer else {
return
}
let rect: CGRect? = self.chatDisplayNode.frameForInputActionButton()
let updatedMode: ChatTextInputMediaRecordingButtonMode = self.presentationInterfaceState.interfaceState.mediaRecordingMode
let text: String
var canSwitch = true
if let channel = peer as? TelegramChannel {
if channel.hasBannedPermission(.banSendVoice) != nil && channel.hasBannedPermission(.banSendInstantVideos) != nil {
canSwitch = false
} else if channel.hasBannedPermission(.banSendVoice) != nil {
if channel.hasBannedPermission(.banSendInstantVideos) == nil {
canSwitch = false
}
} else if channel.hasBannedPermission(.banSendInstantVideos) != nil {
if channel.hasBannedPermission(.banSendVoice) == nil {
canSwitch = false
}
}
} else if let group = peer as? TelegramGroup {
if group.hasBannedPermission(.banSendVoice) && group.hasBannedPermission(.banSendInstantVideos) {
canSwitch = false
} else if group.hasBannedPermission(.banSendVoice) {
if !group.hasBannedPermission(.banSendInstantVideos) {
canSwitch = false
}
} else if group.hasBannedPermission(.banSendInstantVideos) {
if !group.hasBannedPermission(.banSendVoice) {
canSwitch = false
}
}
}
if updatedMode == .audio {
text = self.presentationData.strings.Conversation_HoldForAudio
if canSwitch {
text = self.presentationData.strings.Conversation_HoldForAudio
} else {
text = self.presentationData.strings.Conversation_HoldForAudioOnly
}
} else {
text = self.presentationData.strings.Conversation_HoldForVideo
if canSwitch {
text = self.presentationData.strings.Conversation_HoldForVideo
} else {
text = self.presentationData.strings.Conversation_HoldForVideoOnly
}
}
self.silentPostTooltipController?.dismiss()

View File

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

View File

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

View File

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

View File

@ -660,7 +660,7 @@ struct ChatRecentActionsEntry: Comparable, Identifiable {
let order: [(TelegramChatBannedRightsFlags, String)] = [
(.banReadMessages, self.presentationData.strings.Channel_AdminLog_BanReadMessages),
(.banSendMessages, self.presentationData.strings.Channel_AdminLog_BanSendMessages),
(.banSendText, self.presentationData.strings.Channel_AdminLog_BanSendMessages),
(.banSendMedia, self.presentationData.strings.Channel_AdminLog_BanSendMedia),
(.banSendStickers, self.presentationData.strings.Channel_AdminLog_BanSendStickersAndGifs),
(.banEmbedLinks, self.presentationData.strings.Channel_AdminLog_BanEmbedLinks),
@ -1015,7 +1015,7 @@ struct ChatRecentActionsEntry: Comparable, Identifiable {
let order: [(TelegramChatBannedRightsFlags, String)] = [
(.banReadMessages, self.presentationData.strings.Channel_AdminLog_BanReadMessages),
(.banSendMessages, self.presentationData.strings.Channel_AdminLog_BanSendMessages),
(.banSendText, self.presentationData.strings.Channel_AdminLog_BanSendMessages),
(.banSendMedia, self.presentationData.strings.Channel_AdminLog_BanSendMedia),
(.banSendStickers, self.presentationData.strings.Channel_AdminLog_BanSendStickersAndGifs),
(.banEmbedLinks, self.presentationData.strings.Channel_AdminLog_BanEmbedLinks),

View File

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

View File

@ -462,6 +462,7 @@ final class ChatTextViewForOverlayContent: UIView, ChatInputPanelViewForOverlayC
class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate {
let clippingNode: ASDisplayNode
var textPlaceholderNode: ImmediateTextNode
var textLockIconNode: ASImageNode?
var contextPlaceholderNode: TextNode?
var slowmodePlaceholderNode: ChatTextInputSlowmodePlaceholderNode?
let textInputContainerBackgroundNode: ASImageNode
@ -516,6 +517,7 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate {
private var updatingInputState = false
private var currentPlaceholder: String?
private var sendingTextDisabled: Bool = false
private var presentationInterfaceState: ChatPresentationInterfaceState?
private var initializedPlaceholder = false
@ -923,7 +925,15 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate {
let recognizer = TouchDownGestureRecognizer(target: self, action: #selector(self.textInputBackgroundViewTap(_:)))
recognizer.touchDown = { [weak self] in
if let strongSelf = self {
strongSelf.ensureFocused()
if strongSelf.sendingTextDisabled {
guard let controller = strongSelf.interfaceInteraction?.chatController() as? ChatControllerImpl else {
return
}
//TODO:localize
controller.controllerInteraction?.displayUndo(.info(title: nil, text: "The admins of this group do not allow to send text messages."))
} else {
strongSelf.ensureFocused()
}
}
}
recognizer.waitForTouchUp = { [weak self] in
@ -997,6 +1007,7 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate {
textInputNode.textView.scrollIndicatorInsets = UIEdgeInsets(top: 9.0, left: 0.0, bottom: 9.0, right: -13.0)
self.textInputContainer.addSubnode(textInputNode)
textInputNode.view.disablesInteractiveTransitionGestureRecognizer = true
textInputNode.isUserInteractionEnabled = !self.sendingTextDisabled
self.textInputNode = textInputNode
var accessoryButtonsWidth: CGFloat = 0.0
@ -1024,8 +1035,8 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate {
self.updateSpoiler()
}
self.textInputBackgroundNode.isUserInteractionEnabled = false
self.textInputBackgroundNode.view.removeGestureRecognizer(self.textInputBackgroundNode.view.gestureRecognizers![0])
self.textInputBackgroundNode.isUserInteractionEnabled = !textInputNode.isUserInteractionEnabled
//self.textInputBackgroundNode.view.removeGestureRecognizer(self.textInputBackgroundNode.view.gestureRecognizers![0])
let recognizer = TouchDownGestureRecognizer(target: self, action: #selector(self.textInputBackgroundViewTap(_:)))
recognizer.touchDown = { [weak self] in
@ -1198,6 +1209,18 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate {
self.attachmentButton.accessibilityTraits = (!isSlowmodeActive || isMediaEnabled) ? [.button] : [.button, .notEnabled]
self.attachmentButtonDisabledNode.isHidden = !isSlowmodeActive || isMediaEnabled
var sendingTextDisabled = false
if let peer = interfaceState.renderedPeer?.peer {
if let channel = peer as? TelegramChannel, channel.hasBannedPermission(.banSendText) != nil {
sendingTextDisabled = true
} else if let group = peer as? TelegramGroup, group.hasBannedPermission(.banSendText) {
sendingTextDisabled = true
}
}
self.sendingTextDisabled = sendingTextDisabled
self.textInputNode?.isUserInteractionEnabled = !sendingTextDisabled
var buttonTitleUpdated = false
var menuTextSize = self.menuButtonTextNode.frame.size
if self.presentationInterfaceState != interfaceState {
@ -1347,22 +1370,30 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate {
self.initializedPlaceholder = true
var placeholder: String
if let channel = peer as? TelegramChannel, case .broadcast = channel.info {
if interfaceState.interfaceState.silentPosting {
placeholder = interfaceState.strings.Conversation_InputTextSilentBroadcastPlaceholder
} else {
placeholder = interfaceState.strings.Conversation_InputTextBroadcastPlaceholder
}
} else if let channel = peer as? TelegramChannel, case .group = channel.info, channel.hasPermission(.canBeAnonymous) {
placeholder = interfaceState.strings.Conversation_InputTextAnonymousPlaceholder
} else if case let .replyThread(replyThreadMessage) = interfaceState.chatLocation, !replyThreadMessage.isForumPost {
if replyThreadMessage.isChannelPost {
placeholder = interfaceState.strings.Conversation_InputTextPlaceholderComment
} else {
placeholder = interfaceState.strings.Conversation_InputTextPlaceholderReply
}
} else {
placeholder = interfaceState.strings.Conversation_InputTextPlaceholder
if sendingTextDisabled {
//TODO:localize
placeholder = "Text not allowed"
} else {
if let channel = peer as? TelegramChannel, case .group = channel.info, channel.hasPermission(.canBeAnonymous) {
placeholder = interfaceState.strings.Conversation_InputTextAnonymousPlaceholder
} else if case let .replyThread(replyThreadMessage) = interfaceState.chatLocation, !replyThreadMessage.isForumPost {
if replyThreadMessage.isChannelPost {
placeholder = interfaceState.strings.Conversation_InputTextPlaceholderComment
} else {
placeholder = interfaceState.strings.Conversation_InputTextPlaceholderReply
}
} else {
placeholder = interfaceState.strings.Conversation_InputTextPlaceholder
}
}
}
if let keyboardButtonsMessage = interfaceState.keyboardButtonsMessage, interfaceState.interfaceState.messageActionsState.dismissedButtonKeyboardMessageId != keyboardButtonsMessage.id {
@ -2037,7 +2068,35 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate {
transition.updateFrame(layer: self.textInputBackgroundNode.layer, frame: textInputBackgroundFrame)
transition.updateAlpha(node: self.textInputBackgroundNode, alpha: audioRecordingItemsAlpha)
transition.updateFrame(node: self.textPlaceholderNode, frame: CGRect(origin: CGPoint(x: leftInset + textFieldInsets.left + self.textInputViewInternalInsets.left, y: textFieldInsets.top + self.textInputViewInternalInsets.top + textInputViewRealInsets.top + UIScreenPixel), size: self.textPlaceholderNode.frame.size))
let textPlaceholderFrame: CGRect
if sendingTextDisabled {
textPlaceholderFrame = CGRect(origin: CGPoint(x: textInputBackgroundFrame.minX + floor((textInputBackgroundFrame.width - self.textPlaceholderNode.bounds.width) / 2.0), y: textFieldInsets.top + self.textInputViewInternalInsets.top + textInputViewRealInsets.top + UIScreenPixel), size: self.textPlaceholderNode.frame.size)
let textLockIconNode: ASImageNode
var textLockIconTransition = transition
if let current = self.textLockIconNode {
textLockIconNode = current
} else {
textLockIconTransition = .immediate
textLockIconNode = ASImageNode()
self.textLockIconNode = textLockIconNode
self.textPlaceholderNode.addSubnode(textLockIconNode)
textLockIconNode.image = generateTintedImage(image: UIImage(bundleImageName: "Chat/Input/Accessory Panels/TextLockIcon"), color: interfaceState.theme.chat.inputPanel.inputPlaceholderColor)
}
if let image = textLockIconNode.image {
textLockIconTransition.updateFrame(node: textLockIconNode, frame: CGRect(origin: CGPoint(x: -image.size.width - 4.0, y: floor((textPlaceholderFrame.height - image.size.height) / 2.0)), size: image.size))
}
} else {
textPlaceholderFrame = CGRect(origin: CGPoint(x: leftInset + textFieldInsets.left + self.textInputViewInternalInsets.left, y: textFieldInsets.top + self.textInputViewInternalInsets.top + textInputViewRealInsets.top + UIScreenPixel), size: self.textPlaceholderNode.frame.size)
if let textLockIconNode = self.textLockIconNode {
self.textLockIconNode = nil
textLockIconNode.removeFromSupernode()
}
}
transition.updateFrame(node: self.textPlaceholderNode, frame: textPlaceholderFrame)
var textPlaceholderAlpha: CGFloat = audioRecordingItemsAlpha
if self.textPlaceholderNode.frame.width > (nextButtonTopRight.x - textInputBackgroundFrame.minX) - 32.0 {
@ -3352,6 +3411,10 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate {
}
func ensureFocused() {
if self.sendingTextDisabled {
return
}
if self.textInputNode == nil {
self.loadTextInputNode()
}

View File

@ -10,7 +10,7 @@ import ShareController
import LegacyUI
import LegacyMediaPickerUI
func presentedLegacyCamera(context: AccountContext, peer: Peer, chatLocation: ChatLocation, cameraView: TGAttachmentCameraView?, menuController: TGMenuSheetController?, parentController: ViewController, attachmentController: ViewController? = nil, editingMedia: Bool, saveCapturedPhotos: Bool, mediaGrouping: Bool, initialCaption: NSAttributedString, hasSchedule: Bool, photoOnly: Bool, sendMessagesWithSignals: @escaping ([Any]?, Bool, Int32) -> Void, recognizedQRCode: @escaping (String) -> Void = { _ in }, presentSchedulePicker: @escaping (Bool, @escaping (Int32) -> Void) -> Void, presentTimerPicker: @escaping (@escaping (Int32) -> Void) -> Void, getCaptionPanelView: @escaping () -> TGCaptionPanelView?, dismissedWithResult: @escaping () -> Void = {}, finishedTransitionIn: @escaping () -> Void = {}) {
func presentedLegacyCamera(context: AccountContext, peer: Peer, chatLocation: ChatLocation, cameraView: TGAttachmentCameraView?, menuController: TGMenuSheetController?, parentController: ViewController, attachmentController: ViewController? = nil, editingMedia: Bool, saveCapturedPhotos: Bool, mediaGrouping: Bool, initialCaption: NSAttributedString, hasSchedule: Bool, enablePhoto: Bool, enableVideo: Bool, sendMessagesWithSignals: @escaping ([Any]?, Bool, Int32) -> Void, recognizedQRCode: @escaping (String) -> Void = { _ in }, presentSchedulePicker: @escaping (Bool, @escaping (Int32) -> Void) -> Void, presentTimerPicker: @escaping (@escaping (Int32) -> Void) -> Void, getCaptionPanelView: @escaping () -> TGCaptionPanelView?, dismissedWithResult: @escaping () -> Void = {}, finishedTransitionIn: @escaping () -> Void = {}) {
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
let legacyController = LegacyController(presentation: .custom, theme: presentationData.theme)
legacyController.supportedOrientations = ViewControllerSupportedOrientations(regularSize: .portrait, compactSize: .portrait)
@ -22,7 +22,16 @@ func presentedLegacyCamera(context: AccountContext, peer: Peer, chatLocation: Ch
let controller: TGCameraController
if let cameraView = cameraView, let previewView = cameraView.previewView() {
controller = TGCameraController(context: legacyController.context, saveEditedPhotos: saveCapturedPhotos && !isSecretChat, saveCapturedMedia: saveCapturedPhotos && !isSecretChat, camera: previewView.camera, previewView: previewView, intent: photoOnly ? TGCameraControllerGenericPhotoOnlyIntent : TGCameraControllerGenericIntent)
let intent: TGCameraControllerIntent
if !enableVideo {
intent = TGCameraControllerGenericPhotoOnlyIntent
} else if !enablePhoto {
intent = TGCameraControllerGenericVideoOnlyIntent
} else {
intent = TGCameraControllerGenericIntent
}
controller = TGCameraController(context: legacyController.context, saveEditedPhotos: saveCapturedPhotos && !isSecretChat, saveCapturedMedia: saveCapturedPhotos && !isSecretChat, camera: previewView.camera, previewView: previewView, intent: intent)
} else {
controller = TGCameraController(context: legacyController.context, saveEditedPhotos: saveCapturedPhotos && !isSecretChat, saveCapturedMedia: saveCapturedPhotos && !isSecretChat)
}

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

View File

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

View File

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