UI improvements

This commit is contained in:
Ali 2023-01-23 14:36:47 +04:00
parent 55d076a494
commit 3b42f14613
18 changed files with 1085 additions and 155 deletions

View File

@ -150,7 +150,7 @@ public struct Transition {
//view.center = CGPoint(x: frame.midX, y: frame.midY) //view.center = CGPoint(x: frame.midX, y: frame.midY)
self.animatePosition(view: view, from: CGPoint(x: previousFrame.midX, y: previousFrame.midY), to: CGPoint(x: frame.midX, y: frame.midY), completion: completion) self.animatePosition(view: view, from: CGPoint(x: previousFrame.midX, y: previousFrame.midY), to: CGPoint(x: frame.midX, y: frame.midY), completion: completion)
self.animateBounds(view: view, from: CGRect(origin: view.bounds.origin, size: previousFrame.size), to: CGRect(origin: view.bounds.origin, size: frame.size)) self.animateBoundsSize(view: view, from: previousFrame.size, to: frame.size)
} }
} }

View File

@ -149,6 +149,7 @@ public enum PagerComponentPanelHideBehavior {
case hideOnScroll case hideOnScroll
case show case show
case hide case hide
case disable
} }
public final class PagerComponentContentIcon: Equatable { public final class PagerComponentContentIcon: Equatable {
@ -559,6 +560,10 @@ public final class PagerComponent<ChildEnvironmentType: Equatable, TopPanelEnvir
} }
} }
if case .disable = component.panelHideBehavior {
topPanelVisibility = 0.0
}
var topPanelHeight: CGFloat = 0.0 var topPanelHeight: CGFloat = 0.0
if let topPanel = component.topPanel { if let topPanel = component.topPanel {
let effectiveTopPanelOffsetFraction = scrollingPanelOffsetFraction let effectiveTopPanelOffsetFraction = scrollingPanelOffsetFraction
@ -616,7 +621,7 @@ public final class PagerComponent<ChildEnvironmentType: Equatable, TopPanelEnvir
var topPanelVisibilityFraction: CGFloat = 1.0 - effectiveTopPanelOffsetFraction var topPanelVisibilityFraction: CGFloat = 1.0 - effectiveTopPanelOffsetFraction
switch component.panelHideBehavior { switch component.panelHideBehavior {
case .hide: case .hide, .disable:
topPanelVisibilityFraction = 0.0 topPanelVisibilityFraction = 0.0
case .show: case .show:
topPanelVisibilityFraction = 1.0 topPanelVisibilityFraction = 1.0
@ -631,12 +636,16 @@ public final class PagerComponent<ChildEnvironmentType: Equatable, TopPanelEnvir
if case .hide = component.panelHideBehavior { if case .hide = component.panelHideBehavior {
topPanelOffset = topPanelSize.height topPanelOffset = topPanelSize.height
} else if case .disable = component.panelHideBehavior {
topPanelOffset = topPanelSize.height
} }
if component.externalTopPanelContainer != nil { if component.externalTopPanelContainer != nil {
var visibleTopPanelHeight = max(0.0, topPanelSize.height - topPanelOffset) var visibleTopPanelHeight = max(0.0, topPanelSize.height - topPanelOffset)
if case .hide = component.panelHideBehavior { if case .hide = component.panelHideBehavior {
visibleTopPanelHeight = 0.0 visibleTopPanelHeight = 0.0
} else if case .disable = component.panelHideBehavior {
visibleTopPanelHeight = 0.0
} }
panelStateTransition.setFrame(view: topPanelView, frame: CGRect(origin: CGPoint(), size: CGSize(width: topPanelSize.width, height: visibleTopPanelHeight))) panelStateTransition.setFrame(view: topPanelView, frame: CGRect(origin: CGPoint(), size: CGSize(width: topPanelSize.width, height: visibleTopPanelHeight)))
@ -705,6 +714,8 @@ public final class PagerComponent<ChildEnvironmentType: Equatable, TopPanelEnvir
bottomPanelOffset = bottomPanelSize.height * scrollingPanelOffsetFraction bottomPanelOffset = bottomPanelSize.height * scrollingPanelOffsetFraction
if case .hide = component.panelHideBehavior { if case .hide = component.panelHideBehavior {
bottomPanelOffset = bottomPanelSize.height bottomPanelOffset = bottomPanelSize.height
} else if case .disable = component.panelHideBehavior {
bottomPanelOffset = bottomPanelSize.height
} }
panelStateTransition.setFrame(view: bottomPanelView, frame: CGRect(origin: CGPoint(x: 0.0, y: availableSize.height - bottomPanelSize.height + bottomPanelOffset), size: bottomPanelSize)) panelStateTransition.setFrame(view: bottomPanelView, frame: CGRect(origin: CGPoint(x: 0.0, y: availableSize.height - bottomPanelSize.height + bottomPanelOffset), size: bottomPanelSize))
@ -724,7 +735,7 @@ public final class PagerComponent<ChildEnvironmentType: Equatable, TopPanelEnvir
let effectiveTopPanelHeight: CGFloat let effectiveTopPanelHeight: CGFloat
switch component.panelHideBehavior { switch component.panelHideBehavior {
case .hide: case .hide, .disable:
effectiveTopPanelHeight = 0.0 effectiveTopPanelHeight = 0.0
case .show, .hideOnScroll: case .show, .hideOnScroll:
if component.externalTopPanelContainer != nil { if component.externalTopPanelContainer != nil {

View File

@ -276,7 +276,7 @@ class StickerPickerScreen: ViewController {
openFeatured: nil, openFeatured: nil,
openSearch: { openSearch: {
}, },
addGroupAction: { [weak self] groupId, isPremiumLocked in addGroupAction: { [weak self] groupId, isPremiumLocked, _ in
guard let strongSelf = self, let controller = strongSelf.controller, let collectionId = groupId.base as? ItemCollectionId else { guard let strongSelf = self, let controller = strongSelf.controller, let collectionId = groupId.base as? ItemCollectionId else {
return return
} }
@ -391,7 +391,7 @@ class StickerPickerScreen: ViewController {
openStickerSettings: nil, openStickerSettings: nil,
openFeatured: nil, openFeatured: nil,
openSearch: {}, openSearch: {},
addGroupAction: { [weak self] groupId, isPremiumLocked in addGroupAction: { [weak self] groupId, isPremiumLocked, _ in
guard let strongSelf = self, let controller = strongSelf.controller, let collectionId = groupId.base as? ItemCollectionId else { guard let strongSelf = self, let controller = strongSelf.controller, let collectionId = groupId.base as? ItemCollectionId else {
return return
} }
@ -481,7 +481,7 @@ class StickerPickerScreen: ViewController {
openFeatured: nil, openFeatured: nil,
openSearch: { openSearch: {
}, },
addGroupAction: { [weak self] groupId, isPremiumLocked in addGroupAction: { [weak self] groupId, isPremiumLocked, _ in
guard let strongSelf = self, let controller = strongSelf.controller, let collectionId = groupId.base as? ItemCollectionId else { guard let strongSelf = self, let controller = strongSelf.controller, let collectionId = groupId.base as? ItemCollectionId else {
return return
} }

View File

@ -783,6 +783,8 @@
authInfo = [authInfo withUpdatedAuthKeyAttributes:authKeyAttributes]; authInfo = [authInfo withUpdatedAuthKeyAttributes:authKeyAttributes];
[_context updateAuthInfoForDatacenterWithId:mtProto.datacenterId authInfo:authInfo selector:authInfoSelector]; [_context updateAuthInfoForDatacenterWithId:mtProto.datacenterId authInfo:authInfo selector:authInfoSelector];
}]; }];
restartRequest = true;
} else if (rpcError.errorCode == 406) { } else if (rpcError.errorCode == 406) {
if (_didReceiveSoftAuthResetError) { if (_didReceiveSoftAuthResetError) {
_didReceiveSoftAuthResetError(); _didReceiveSoftAuthResetError();

View File

@ -1264,7 +1264,7 @@ public final class ReactionContextNode: ASDisplayNode, UIScrollViewDelegate {
}, },
openSearch: { openSearch: {
}, },
addGroupAction: { [weak self] groupId, isPremiumLocked in addGroupAction: { [weak self] groupId, isPremiumLocked, _ in
guard let strongSelf = self, let collectionId = groupId.base as? ItemCollectionId else { guard let strongSelf = self, let collectionId = groupId.base as? ItemCollectionId else {
return return
} }
@ -1284,7 +1284,7 @@ public final class ReactionContextNode: ASDisplayNode, UIScrollViewDelegate {
for featuredEmojiPack in view.items.lazy.map({ $0.contents.get(FeaturedStickerPackItem.self)! }) { for featuredEmojiPack in view.items.lazy.map({ $0.contents.get(FeaturedStickerPackItem.self)! }) {
if featuredEmojiPack.info.id == collectionId { if featuredEmojiPack.info.id == collectionId {
if let strongSelf = self { if let strongSelf = self {
strongSelf.scheduledEmojiContentAnimationHint = EmojiPagerContentComponent.ContentAnimation(type: .groupInstalled(id: collectionId)) strongSelf.scheduledEmojiContentAnimationHint = EmojiPagerContentComponent.ContentAnimation(type: .groupInstalled(id: collectionId, scrollToGroup: true))
} }
let _ = strongSelf.context.engine.stickers.addStickerPackInteractively(info: featuredEmojiPack.info, items: featuredEmojiPack.topItems).start() let _ = strongSelf.context.engine.stickers.addStickerPackInteractively(info: featuredEmojiPack.info, items: featuredEmojiPack.topItems).start()

View File

@ -481,7 +481,7 @@ final class AvatarEditorScreenComponent: Component {
openFeatured: nil, openFeatured: nil,
openSearch: { openSearch: {
}, },
addGroupAction: { [weak self] groupId, isPremiumLocked in addGroupAction: { [weak self] groupId, isPremiumLocked, _ in
guard let strongSelf = self, let controller = strongSelf.controller?(), let collectionId = groupId.base as? ItemCollectionId else { guard let strongSelf = self, let controller = strongSelf.controller?(), let collectionId = groupId.base as? ItemCollectionId else {
return return
} }
@ -615,7 +615,7 @@ final class AvatarEditorScreenComponent: Component {
openFeatured: nil, openFeatured: nil,
openSearch: { openSearch: {
}, },
addGroupAction: { [weak self] groupId, isPremiumLocked in addGroupAction: { [weak self] groupId, isPremiumLocked, _ in
guard let strongSelf = self, let controller = strongSelf.controller?(), let collectionId = groupId.base as? ItemCollectionId else { guard let strongSelf = self, let controller = strongSelf.controller?(), let collectionId = groupId.base as? ItemCollectionId else {
return return
} }

View File

@ -641,25 +641,61 @@ public final class ChatEntityKeyboardInputNode: ChatInputNode {
var premiumToastCounter = 0 var premiumToastCounter = 0
self.emojiInputInteraction = EmojiPagerContentComponent.InputInteraction( self.emojiInputInteraction = EmojiPagerContentComponent.InputInteraction(
performItemAction: { [weak self, weak interfaceInteraction, weak controllerInteraction] groupId, item, _, _, _, _ in performItemAction: { [weak self, weak interfaceInteraction, weak controllerInteraction] groupId, item, _, _, _, _ in
let _ = (ChatEntityKeyboardInputNode.hasPremium(context: context, chatPeerId: chatPeerId, premiumIfSavedMessages: true) |> take(1) |> deliverOnMainQueue).start(next: { hasPremium in let _ = (
combineLatest(
ChatEntityKeyboardInputNode.hasPremium(context: context, chatPeerId: chatPeerId, premiumIfSavedMessages: true),
ChatEntityKeyboardInputNode.hasPremium(context: context, chatPeerId: chatPeerId, premiumIfSavedMessages: false)
)
|> take(1)
|> deliverOnMainQueue).start(next: { hasPremium, hasGlobalPremium in
guard let strongSelf = self, let controllerInteraction = controllerInteraction, let interfaceInteraction = interfaceInteraction else { guard let strongSelf = self, let controllerInteraction = controllerInteraction, let interfaceInteraction = interfaceInteraction else {
return return
} }
if groupId == AnyHashable("featuredTop"), let file = item.itemFile { if groupId == AnyHashable("featuredTop"), let file = item.itemFile {
let viewKey = PostboxViewKey.orderedItemList(id: Namespaces.OrderedItemList.CloudFeaturedEmojiPacks) let viewKey = PostboxViewKey.orderedItemList(id: Namespaces.OrderedItemList.CloudFeaturedEmojiPacks)
let _ = (context.account.postbox.combinedView(keys: [viewKey]) let _ = (combineLatest(
context.account.postbox.itemCollectionsView(orderedItemListCollectionIds: [], namespaces: [Namespaces.ItemCollection.CloudEmojiPacks], aroundIndex: nil, count: 10000000),
context.account.postbox.combinedView(keys: [viewKey])
)
|> take(1) |> take(1)
|> deliverOnMainQueue).start(next: { [weak interfaceInteraction, weak controllerInteraction] views in |> deliverOnMainQueue).start(next: { [weak interfaceInteraction, weak controllerInteraction] emojiPacksView, views in
guard let controllerInteraction = controllerInteraction else { guard let controllerInteraction = controllerInteraction else {
return return
} }
guard let view = views.views[viewKey] as? OrderedItemListView else { guard let view = views.views[viewKey] as? OrderedItemListView else {
return return
} }
for featuredStickerPack in view.items.lazy.map({ $0.contents.get(FeaturedStickerPackItem.self)! }) { guard let self else {
return
}
let _ = interfaceInteraction
let _ = controllerInteraction
var installedCollectionIds = Set<ItemCollectionId>()
for (id, _, _) in emojiPacksView.collectionInfos {
installedCollectionIds.insert(id)
}
let stickerPacks = view.items.map({ $0.contents.get(FeaturedStickerPackItem.self)! }).filter({
!installedCollectionIds.contains($0.info.id)
})
for featuredStickerPack in stickerPacks {
if featuredStickerPack.topItems.contains(where: { $0.file.fileId == file.fileId }) { if featuredStickerPack.topItems.contains(where: { $0.file.fileId == file.fileId }) {
let controller = StickerPackScreen( if let pagerView = self.entityKeyboardView.componentView as? EntityKeyboardComponent.View, let emojiInputInteraction = self.emojiInputInteraction {
pagerView.openCustomSearch(content: EmojiSearchContent(
context: self.context,
items: stickerPacks,
initialFocusId: featuredStickerPack.info.id,
hasPremiumForUse: hasPremium,
hasPremiumForInstallation: hasGlobalPremium,
parentInputInteraction: emojiInputInteraction
))
}
/*let controller = StickerPackScreen(
context: context, context: context,
updatedPresentationData: controllerInteraction.updatedPresentationData, updatedPresentationData: controllerInteraction.updatedPresentationData,
mode: .default, mode: .default,
@ -675,7 +711,7 @@ public final class ChatEntityKeyboardInputNode: ChatInputNode {
interfaceInteraction.insertText(NSAttributedString(string: text, attributes: [ChatTextInputAttributes.customEmoji: emojiAttribute])) interfaceInteraction.insertText(NSAttributedString(string: text, attributes: [ChatTextInputAttributes.customEmoji: emojiAttribute]))
} }
) )
controllerInteraction.presentController(controller, nil) controllerInteraction.presentController(controller, nil)*/
break break
} }
@ -786,7 +822,7 @@ public final class ChatEntityKeyboardInputNode: ChatInputNode {
}, },
openSearch: { openSearch: {
}, },
addGroupAction: { [weak self, weak controllerInteraction] groupId, isPremiumLocked in addGroupAction: { [weak self, weak controllerInteraction] groupId, isPremiumLocked, scrollToGroup in
guard let controllerInteraction = controllerInteraction, let collectionId = groupId.base as? ItemCollectionId else { guard let controllerInteraction = controllerInteraction, let collectionId = groupId.base as? ItemCollectionId else {
return return
} }
@ -815,7 +851,7 @@ public final class ChatEntityKeyboardInputNode: ChatInputNode {
for featuredEmojiPack in view.items.lazy.map({ $0.contents.get(FeaturedStickerPackItem.self)! }) { for featuredEmojiPack in view.items.lazy.map({ $0.contents.get(FeaturedStickerPackItem.self)! }) {
if featuredEmojiPack.info.id == collectionId { if featuredEmojiPack.info.id == collectionId {
if let strongSelf = self { if let strongSelf = self {
strongSelf.scheduledContentAnimationHint = EmojiPagerContentComponent.ContentAnimation(type: .groupInstalled(id: collectionId)) strongSelf.scheduledContentAnimationHint = EmojiPagerContentComponent.ContentAnimation(type: .groupInstalled(id: collectionId, scrollToGroup: scrollToGroup))
} }
let _ = context.engine.stickers.addStickerPackInteractively(info: featuredEmojiPack.info, items: featuredEmojiPack.topItems).start() let _ = context.engine.stickers.addStickerPackInteractively(info: featuredEmojiPack.info, items: featuredEmojiPack.topItems).start()
@ -1181,7 +1217,7 @@ public final class ChatEntityKeyboardInputNode: ChatInputNode {
pagerView.openSearch() pagerView.openSearch()
} }
}, },
addGroupAction: { groupId, isPremiumLocked in addGroupAction: { groupId, isPremiumLocked, _ in
guard let controllerInteraction = controllerInteraction, let collectionId = groupId.base as? ItemCollectionId else { guard let controllerInteraction = controllerInteraction, let collectionId = groupId.base as? ItemCollectionId else {
return return
} }
@ -1479,7 +1515,7 @@ public final class ChatEntityKeyboardInputNode: ChatInputNode {
guard let strongSelf = self else { guard let strongSelf = self else {
return return
} }
strongSelf.openGifContextMenu(item: item, sourceView: sourceView, sourceRect: sourceRect, gesture: gesture, isSaved: isSaved) strongSelf.openGifContextMenu(file: item.file, contextResult: item.contextResult, sourceView: sourceView, sourceRect: sourceRect, gesture: gesture, isSaved: isSaved)
}, },
loadMore: { [weak self] token in loadMore: { [weak self] token in
guard let strongSelf = self, let gifContext = strongSelf.gifContext else { guard let strongSelf = self, let gifContext = strongSelf.gifContext else {
@ -1697,7 +1733,7 @@ public final class ChatEntityKeyboardInputNode: ChatInputNode {
} }
strongSelf.reorderItems(category: category, items: items) strongSelf.reorderItems(category: category, items: items)
}, },
makeSearchContainerNode: { [weak controllerInteraction] content in makeSearchContainerNode: { [weak self, weak controllerInteraction] content in
guard let controllerInteraction = controllerInteraction else { guard let controllerInteraction = controllerInteraction else {
return nil return nil
} }
@ -1711,7 +1747,7 @@ public final class ChatEntityKeyboardInputNode: ChatInputNode {
} }
let presentationData = context.sharedContext.currentPresentationData.with { $0 } let presentationData = context.sharedContext.currentPresentationData.with { $0 }
return PaneSearchContainerNode( let searchContainerNode = PaneSearchContainerNode(
context: context, context: context,
theme: presentationData.theme, theme: presentationData.theme,
strings: presentationData.strings, strings: presentationData.strings,
@ -1722,6 +1758,14 @@ public final class ChatEntityKeyboardInputNode: ChatInputNode {
cancel: { cancel: {
} }
) )
searchContainerNode.openGifContextMenu = { item, sourceNode, sourceRect, gesture, isSaved in
guard let self else {
return
}
self.openGifContextMenu(file: item.file, contextResult: item.contextResult, sourceView: sourceNode.view, sourceRect: sourceRect, gesture: gesture, isSaved: isSaved)
}
return searchContainerNode
}, },
contentIdUpdated: { _ in }, contentIdUpdated: { _ in },
deviceMetrics: deviceMetrics, deviceMetrics: deviceMetrics,
@ -1854,17 +1898,15 @@ public final class ChatEntityKeyboardInputNode: ChatInputNode {
}) })
} }
private func openGifContextMenu(item: GifPagerContentComponent.Item, sourceView: UIView, sourceRect: CGRect, gesture: ContextGesture, isSaved: Bool) { private func openGifContextMenu(file: FileMediaReference, contextResult: (ChatContextResultCollection, ChatContextResult)?, sourceView: UIView, sourceRect: CGRect, gesture: ContextGesture, isSaved: Bool) {
let file = item
let canSaveGif: Bool let canSaveGif: Bool
if file.file.media.fileId.namespace == Namespaces.Media.CloudFile { if file.media.fileId.namespace == Namespaces.Media.CloudFile {
canSaveGif = true canSaveGif = true
} else { } else {
canSaveGif = false canSaveGif = false
} }
let _ = (self.context.engine.stickers.isGifSaved(id: file.file.media.fileId) let _ = (self.context.engine.stickers.isGifSaved(id: file.media.fileId)
|> deliverOnMainQueue).start(next: { [weak self] isGifSaved in |> deliverOnMainQueue).start(next: { [weak self] isGifSaved in
guard let strongSelf = self else { guard let strongSelf = self else {
return return
@ -1875,7 +1917,7 @@ public final class ChatEntityKeyboardInputNode: ChatInputNode {
} }
let presentationData = strongSelf.context.sharedContext.currentPresentationData.with { $0 } let presentationData = strongSelf.context.sharedContext.currentPresentationData.with { $0 }
let message = Message(stableId: 0, stableVersion: 0, id: MessageId(peerId: PeerId(0), namespace: Namespaces.Message.Local, id: 0), globallyUniqueId: nil, groupingKey: nil, groupInfo: nil, threadId: nil, timestamp: 0, flags: [], tags: [], globalTags: [], localTags: [], forwardInfo: nil, author: nil, text: "", attributes: [], media: [file.file.media], peers: SimpleDictionary(), associatedMessages: SimpleDictionary(), associatedMessageIds: [], associatedMedia: [:], associatedThreadInfo: nil) let message = Message(stableId: 0, stableVersion: 0, id: MessageId(peerId: PeerId(0), namespace: Namespaces.Message.Local, id: 0), globallyUniqueId: nil, groupingKey: nil, groupInfo: nil, threadId: nil, timestamp: 0, flags: [], tags: [], globalTags: [], localTags: [], forwardInfo: nil, author: nil, text: "", attributes: [], media: [file.media], peers: SimpleDictionary(), associatedMessages: SimpleDictionary(), associatedMessageIds: [], associatedMedia: [:], associatedThreadInfo: nil)
let gallery = GalleryController(context: strongSelf.context, source: .standaloneMessage(message), streamSingleVideo: true, replaceRootController: { _, _ in let gallery = GalleryController(context: strongSelf.context, source: .standaloneMessage(message), streamSingleVideo: true, replaceRootController: { _, _ in
}, baseNavigationController: nil) }, baseNavigationController: nil)
@ -1887,8 +1929,8 @@ public final class ChatEntityKeyboardInputNode: ChatInputNode {
}, action: { _, f in }, action: { _, f in
f(.default) f(.default)
if isSaved { if isSaved {
let _ = self?.controllerInteraction?.sendGif(file.file, sourceView, sourceRect, false, false) let _ = self?.controllerInteraction?.sendGif(file, sourceView, sourceRect, false, false)
} else if let (collection, result) = file.contextResult { } else if let (collection, result) = contextResult {
let _ = self?.controllerInteraction?.sendBotContextResultAsGif(collection, result, sourceView, sourceRect, false) let _ = self?.controllerInteraction?.sendBotContextResultAsGif(collection, result, sourceView, sourceRect, false)
} }
}))) })))
@ -1908,8 +1950,8 @@ public final class ChatEntityKeyboardInputNode: ChatInputNode {
}, action: { _, f in }, action: { _, f in
f(.default) f(.default)
if isSaved { if isSaved {
let _ = self?.controllerInteraction?.sendGif(file.file, sourceView, sourceRect, true, false) let _ = self?.controllerInteraction?.sendGif(file, sourceView, sourceRect, true, false)
} else if let (collection, result) = file.contextResult { } else if let (collection, result) = contextResult {
let _ = self?.controllerInteraction?.sendBotContextResultAsGif(collection, result, sourceView, sourceRect, true) let _ = self?.controllerInteraction?.sendBotContextResultAsGif(collection, result, sourceView, sourceRect, true)
} }
}))) })))
@ -1921,7 +1963,7 @@ public final class ChatEntityKeyboardInputNode: ChatInputNode {
}, action: { _, f in }, action: { _, f in
f(.default) f(.default)
let _ = self?.controllerInteraction?.sendGif(file.file, sourceView, sourceRect, false, true) let _ = self?.controllerInteraction?.sendGif(file, sourceView, sourceRect, false, true)
}))) })))
} }
} }
@ -1937,7 +1979,7 @@ public final class ChatEntityKeyboardInputNode: ChatInputNode {
guard let strongSelf = self else { guard let strongSelf = self else {
return return
} }
let _ = removeSavedGif(postbox: strongSelf.context.account.postbox, mediaId: file.file.media.fileId).start() let _ = removeSavedGif(postbox: strongSelf.context.account.postbox, mediaId: file.media.fileId).start()
}))) })))
} else if canSaveGif && !isGifSaved { } else if canSaveGif && !isGifSaved {
items.append(.action(ContextMenuActionItem(text: presentationData.strings.Preview_SaveGif, icon: { theme in items.append(.action(ContextMenuActionItem(text: presentationData.strings.Preview_SaveGif, icon: { theme in
@ -1951,7 +1993,7 @@ public final class ChatEntityKeyboardInputNode: ChatInputNode {
let context = strongSelf.context let context = strongSelf.context
let presentationData = context.sharedContext.currentPresentationData.with { $0 } let presentationData = context.sharedContext.currentPresentationData.with { $0 }
let _ = (toggleGifSaved(account: context.account, fileReference: file.file, saved: true) let _ = (toggleGifSaved(account: context.account, fileReference: file, saved: true)
|> deliverOnMainQueue).start(next: { result in |> deliverOnMainQueue).start(next: { result in
guard let strongSelf = self else { guard let strongSelf = self else {
return return
@ -2136,7 +2178,7 @@ public final class EntityInputView: UIInputView, AttachmentTextInputPanelInputVi
}, },
openSearch: { openSearch: {
}, },
addGroupAction: { _, _ in addGroupAction: { _, _, _ in
}, },
clearGroup: { [weak self] groupId in clearGroup: { [weak self] groupId in
guard let strongSelf = self else { guard let strongSelf = self else {

View File

@ -395,7 +395,7 @@ public final class EmojiStatusSelectionController: ViewController {
}, },
openSearch: { openSearch: {
}, },
addGroupAction: { groupId, isPremiumLocked in addGroupAction: { groupId, isPremiumLocked, _ in
guard let strongSelf = self, let collectionId = groupId.base as? ItemCollectionId else { guard let strongSelf = self, let collectionId = groupId.base as? ItemCollectionId else {
return return
} }
@ -410,7 +410,7 @@ public final class EmojiStatusSelectionController: ViewController {
for featuredEmojiPack in view.items.lazy.map({ $0.contents.get(FeaturedStickerPackItem.self)! }) { for featuredEmojiPack in view.items.lazy.map({ $0.contents.get(FeaturedStickerPackItem.self)! }) {
if featuredEmojiPack.info.id == collectionId { if featuredEmojiPack.info.id == collectionId {
if let strongSelf = self { if let strongSelf = self {
strongSelf.scheduledEmojiContentAnimationHint = EmojiPagerContentComponent.ContentAnimation(type: .groupInstalled(id: collectionId)) strongSelf.scheduledEmojiContentAnimationHint = EmojiPagerContentComponent.ContentAnimation(type: .groupInstalled(id: collectionId, scrollToGroup: true))
} }
let _ = strongSelf.context.engine.stickers.addStickerPackInteractively(info: featuredEmojiPack.info, items: featuredEmojiPack.topItems).start() let _ = strongSelf.context.engine.stickers.addStickerPackInteractively(info: featuredEmojiPack.info, items: featuredEmojiPack.topItems).start()

View File

@ -1892,7 +1892,7 @@ public final class EmojiSearchHeaderView: UIView, UITextFieldDelegate {
component: AnyComponent(Text( component: AnyComponent(Text(
text: strings.Common_Cancel, text: strings.Common_Cancel,
font: Font.regular(17.0), font: Font.regular(17.0),
color: theme.chat.inputMediaPanel.panelContentVibrantOverlayColor color: useOpaqueTheme ? theme.list.itemAccentColor : theme.chat.inputMediaPanel.panelContentVibrantSearchOverlayColor
)), )),
environment: {}, environment: {},
containerSize: CGSize(width: size.width - 32.0, height: 100.0) containerSize: CGSize(width: size.width - 32.0, height: 100.0)
@ -2178,7 +2178,7 @@ public final class EmojiPagerContentComponent: Component {
public enum AnimationType { public enum AnimationType {
case generic case generic
case groupExpanded(id: AnyHashable) case groupExpanded(id: AnyHashable)
case groupInstalled(id: AnyHashable) case groupInstalled(id: AnyHashable, scrollToGroup: Bool)
case groupRemoved(id: AnyHashable) case groupRemoved(id: AnyHashable)
} }
@ -2239,7 +2239,7 @@ public final class EmojiPagerContentComponent: Component {
public let openStickerSettings: (() -> Void)? public let openStickerSettings: (() -> Void)?
public let openFeatured: (() -> Void)? public let openFeatured: (() -> Void)?
public let openSearch: () -> Void public let openSearch: () -> Void
public let addGroupAction: (AnyHashable, Bool) -> Void public let addGroupAction: (AnyHashable, Bool, Bool) -> Void
public let clearGroup: (AnyHashable) -> Void public let clearGroup: (AnyHashable) -> Void
public let pushController: (ViewController) -> Void public let pushController: (ViewController) -> Void
public let presentController: (ViewController) -> Void public let presentController: (ViewController) -> Void
@ -2248,6 +2248,7 @@ public final class EmojiPagerContentComponent: Component {
public let requestUpdate: (Transition) -> Void public let requestUpdate: (Transition) -> Void
public let updateSearchQuery: (EmojiPagerContentComponent.SearchQuery?) -> Void public let updateSearchQuery: (EmojiPagerContentComponent.SearchQuery?) -> Void
public let updateScrollingToItemGroup: () -> Void public let updateScrollingToItemGroup: () -> Void
public let externalCancel: (() -> Void)?
public let chatPeerId: PeerId? public let chatPeerId: PeerId?
public let peekBehavior: EmojiContentPeekBehavior? public let peekBehavior: EmojiContentPeekBehavior?
public let customLayout: CustomLayout? public let customLayout: CustomLayout?
@ -2262,7 +2263,7 @@ public final class EmojiPagerContentComponent: Component {
openStickerSettings: (() -> Void)?, openStickerSettings: (() -> Void)?,
openFeatured: (() -> Void)?, openFeatured: (() -> Void)?,
openSearch: @escaping () -> Void, openSearch: @escaping () -> Void,
addGroupAction: @escaping (AnyHashable, Bool) -> Void, addGroupAction: @escaping (AnyHashable, Bool, Bool) -> Void,
clearGroup: @escaping (AnyHashable) -> Void, clearGroup: @escaping (AnyHashable) -> Void,
pushController: @escaping (ViewController) -> Void, pushController: @escaping (ViewController) -> Void,
presentController: @escaping (ViewController) -> Void, presentController: @escaping (ViewController) -> Void,
@ -2271,6 +2272,7 @@ public final class EmojiPagerContentComponent: Component {
requestUpdate: @escaping (Transition) -> Void, requestUpdate: @escaping (Transition) -> Void,
updateSearchQuery: @escaping (SearchQuery?) -> Void, updateSearchQuery: @escaping (SearchQuery?) -> Void,
updateScrollingToItemGroup: @escaping () -> Void, updateScrollingToItemGroup: @escaping () -> Void,
externalCancel: (() -> Void)? = nil,
chatPeerId: PeerId?, chatPeerId: PeerId?,
peekBehavior: EmojiContentPeekBehavior?, peekBehavior: EmojiContentPeekBehavior?,
customLayout: CustomLayout?, customLayout: CustomLayout?,
@ -2293,6 +2295,7 @@ public final class EmojiPagerContentComponent: Component {
self.requestUpdate = requestUpdate self.requestUpdate = requestUpdate
self.updateSearchQuery = updateSearchQuery self.updateSearchQuery = updateSearchQuery
self.updateScrollingToItemGroup = updateScrollingToItemGroup self.updateScrollingToItemGroup = updateScrollingToItemGroup
self.externalCancel = externalCancel
self.chatPeerId = chatPeerId self.chatPeerId = chatPeerId
self.peekBehavior = peekBehavior self.peekBehavior = peekBehavior
self.customLayout = customLayout self.customLayout = customLayout
@ -2541,6 +2544,7 @@ public final class EmojiPagerContentComponent: Component {
public let displaySearchWithPlaceholder: String? public let displaySearchWithPlaceholder: String?
public let searchCategories: EmojiSearchCategories? public let searchCategories: EmojiSearchCategories?
public let searchInitiallyHidden: Bool public let searchInitiallyHidden: Bool
public let searchAlwaysActive: Bool
public let searchIsPlaceholderOnly: Bool public let searchIsPlaceholderOnly: Bool
public let emptySearchResults: EmptySearchResults? public let emptySearchResults: EmptySearchResults?
public let enableLongPress: Bool public let enableLongPress: Bool
@ -2561,6 +2565,7 @@ public final class EmojiPagerContentComponent: Component {
displaySearchWithPlaceholder: String?, displaySearchWithPlaceholder: String?,
searchCategories: EmojiSearchCategories?, searchCategories: EmojiSearchCategories?,
searchInitiallyHidden: Bool, searchInitiallyHidden: Bool,
searchAlwaysActive: Bool,
searchIsPlaceholderOnly: Bool, searchIsPlaceholderOnly: Bool,
emptySearchResults: EmptySearchResults?, emptySearchResults: EmptySearchResults?,
enableLongPress: Bool, enableLongPress: Bool,
@ -2580,6 +2585,7 @@ public final class EmojiPagerContentComponent: Component {
self.displaySearchWithPlaceholder = displaySearchWithPlaceholder self.displaySearchWithPlaceholder = displaySearchWithPlaceholder
self.searchCategories = searchCategories self.searchCategories = searchCategories
self.searchInitiallyHidden = searchInitiallyHidden self.searchInitiallyHidden = searchInitiallyHidden
self.searchAlwaysActive = searchAlwaysActive
self.searchIsPlaceholderOnly = searchIsPlaceholderOnly self.searchIsPlaceholderOnly = searchIsPlaceholderOnly
self.emptySearchResults = emptySearchResults self.emptySearchResults = emptySearchResults
self.enableLongPress = enableLongPress self.enableLongPress = enableLongPress
@ -2602,6 +2608,7 @@ public final class EmojiPagerContentComponent: Component {
displaySearchWithPlaceholder: self.displaySearchWithPlaceholder, displaySearchWithPlaceholder: self.displaySearchWithPlaceholder,
searchCategories: self.searchCategories, searchCategories: self.searchCategories,
searchInitiallyHidden: self.searchInitiallyHidden, searchInitiallyHidden: self.searchInitiallyHidden,
searchAlwaysActive: self.searchAlwaysActive,
searchIsPlaceholderOnly: self.searchIsPlaceholderOnly, searchIsPlaceholderOnly: self.searchIsPlaceholderOnly,
emptySearchResults: emptySearchResults, emptySearchResults: emptySearchResults,
enableLongPress: self.enableLongPress, enableLongPress: self.enableLongPress,
@ -4296,7 +4303,7 @@ public final class EmojiPagerContentComponent: Component {
} }
} }
public func scrollToItemGroup(id supergroupId: AnyHashable, subgroupId: Int32?) { public func scrollToItemGroup(id supergroupId: AnyHashable, subgroupId: Int32?, animated: Bool) {
guard let component = self.component, let pagerEnvironment = self.pagerEnvironment, let itemLayout = self.itemLayout else { guard let component = self.component, let pagerEnvironment = self.pagerEnvironment, let itemLayout = self.itemLayout else {
return return
} }
@ -4332,6 +4339,9 @@ public final class EmojiPagerContentComponent: Component {
} }
var scrollPosition = anchorFrame.minY + floor(-itemLayout.verticalGroupDefaultSpacing / 2.0) - pagerEnvironment.containerInsets.top var scrollPosition = anchorFrame.minY + floor(-itemLayout.verticalGroupDefaultSpacing / 2.0) - pagerEnvironment.containerInsets.top
if !animated {
scrollPosition = floor(anchorFrame.midY - self.scrollView.bounds.height * 0.5)
}
if scrollPosition > self.scrollView.contentSize.height - self.scrollView.bounds.height { if scrollPosition > self.scrollView.contentSize.height - self.scrollView.bounds.height {
scrollPosition = self.scrollView.contentSize.height - self.scrollView.bounds.height scrollPosition = self.scrollView.contentSize.height - self.scrollView.bounds.height
} }
@ -4339,6 +4349,26 @@ public final class EmojiPagerContentComponent: Component {
scrollPosition = 0.0 scrollPosition = 0.0
} }
if !animated, let keyboardChildEnvironment = self.keyboardChildEnvironment, let inputInteraction = component.inputInteractionHolder.inputInteraction, inputInteraction.useOpaqueTheme {
let highlightLayer = SimpleLayer()
highlightLayer.backgroundColor = keyboardChildEnvironment.theme.list.itemAccentColor.withMultipliedAlpha(0.1).cgColor
highlightLayer.cornerRadius = 20.0
var highlightFrame = group.frame
if highlightFrame.origin.x < 4.0 {
highlightFrame.size.width += (4.0 - highlightFrame.origin.x)
highlightFrame.origin.x = 4.0
}
if highlightFrame.minX + highlightFrame.size.width > self.scrollView.bounds.width - 4.0 {
highlightFrame.size.width = self.scrollView.bounds.width - 4.0 - highlightFrame.minX
}
highlightLayer.frame = highlightFrame
self.scrollView.layer.insertSublayer(highlightLayer, at: 0)
highlightLayer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3, delay: 0.8, removeOnCompletion: false, completion: { [weak highlightLayer] _ in
highlightLayer?.removeFromSuperlayer()
})
}
let offsetDirectionSign: Double = scrollPosition < self.scrollView.bounds.minY ? -1.0 : 1.0 let offsetDirectionSign: Double = scrollPosition < self.scrollView.bounds.minY ? -1.0 : 1.0
var previousVisibleLayers: [ItemLayer.Key: (CALayer, CGRect)] = [:] var previousVisibleLayers: [ItemLayer.Key: (CALayer, CGRect)] = [:]
@ -4462,7 +4492,7 @@ public final class EmojiPagerContentComponent: Component {
} }
} }
let duration = 0.4 let duration: Double = animated ? 0.4 : 0.0
let timingFunction = kCAMediaTimingFunctionSpring let timingFunction = kCAMediaTimingFunctionSpring
if let commonItemOffset = commonItemOffset { if let commonItemOffset = commonItemOffset {
@ -4779,41 +4809,43 @@ public final class EmojiPagerContentComponent: Component {
return return
} }
if case .ended = recognizer.state { if case .ended = recognizer.state {
let locationInScrollView = recognizer.location(in: self.scrollView) if self.scrollViewClippingView.bounds.contains(recognizer.location(in: self.scrollViewClippingView)) {
outer: for (id, groupHeader) in self.visibleGroupHeaders { let locationInScrollView = recognizer.location(in: self.scrollView)
if groupHeader.frame.insetBy(dx: -10.0, dy: -6.0).contains(locationInScrollView) { outer: for (id, groupHeader) in self.visibleGroupHeaders {
let groupHeaderPoint = self.scrollView.convert(locationInScrollView, to: groupHeader) if groupHeader.frame.insetBy(dx: -10.0, dy: -6.0).contains(locationInScrollView) {
if let clearIconLayer = groupHeader.clearIconLayer, clearIconLayer.frame.insetBy(dx: -4.0, dy: -4.0).contains(groupHeaderPoint) { let groupHeaderPoint = self.scrollView.convert(locationInScrollView, to: groupHeader)
component.inputInteractionHolder.inputInteraction?.clearGroup(id) if let clearIconLayer = groupHeader.clearIconLayer, clearIconLayer.frame.insetBy(dx: -4.0, dy: -4.0).contains(groupHeaderPoint) {
return component.inputInteractionHolder.inputInteraction?.clearGroup(id)
} else {
if groupHeader.tapGesture(point: recognizer.location(in: groupHeader)) {
return return
} else {
if groupHeader.tapGesture(point: recognizer.location(in: groupHeader)) {
return
}
} }
} }
} }
}
var foundItem = false
var foundItem = false var foundExactItem = false
var foundExactItem = false if let (item, itemKey) = self.item(atPoint: recognizer.location(in: self)), let itemLayer = self.visibleItemLayers[itemKey] {
if let (item, itemKey) = self.item(atPoint: recognizer.location(in: self)), let itemLayer = self.visibleItemLayers[itemKey] { foundExactItem = true
foundExactItem = true
foundItem = true
if !itemLayer.displayPlaceholder {
component.inputInteractionHolder.inputInteraction?.performItemAction(itemKey.groupId, item, self, self.scrollView.convert(itemLayer.frame, to: self), itemLayer, false)
}
}
if !foundExactItem {
if let (item, itemKey) = self.item(atPoint: recognizer.location(in: self), extendedHitRange: true), let itemLayer = self.visibleItemLayers[itemKey] {
foundItem = true foundItem = true
if !itemLayer.displayPlaceholder { if !itemLayer.displayPlaceholder {
component.inputInteractionHolder.inputInteraction?.performItemAction(itemKey.groupId, item, self, self.scrollView.convert(itemLayer.frame, to: self), itemLayer, false) component.inputInteractionHolder.inputInteraction?.performItemAction(itemKey.groupId, item, self, self.scrollView.convert(itemLayer.frame, to: self), itemLayer, false)
} }
} }
if !foundExactItem {
if let (item, itemKey) = self.item(atPoint: recognizer.location(in: self), extendedHitRange: true), let itemLayer = self.visibleItemLayers[itemKey] {
foundItem = true
if !itemLayer.displayPlaceholder {
component.inputInteractionHolder.inputInteraction?.performItemAction(itemKey.groupId, item, self, self.scrollView.convert(itemLayer.frame, to: self), itemLayer, false)
}
}
}
let _ = foundItem
} }
let _ = foundItem
} }
} }
@ -5006,7 +5038,7 @@ public final class EmojiPagerContentComponent: Component {
scrollView.layer.removeAllAnimations() scrollView.layer.removeAllAnimations()
} }
if self.isSearchActivated, let visibleSearchHeader = self.visibleSearchHeader, visibleSearchHeader.currentPresetSearchTerm == nil { if self.isSearchActivated, let component = self.component, !component.searchAlwaysActive, let visibleSearchHeader = self.visibleSearchHeader, visibleSearchHeader.currentPresetSearchTerm == nil {
scrollView.isScrollEnabled = false scrollView.isScrollEnabled = false
DispatchQueue.main.async { DispatchQueue.main.async {
scrollView.isScrollEnabled = true scrollView.isScrollEnabled = true
@ -5118,8 +5150,10 @@ public final class EmojiPagerContentComponent: Component {
var transitionHintExpandedGroupId: AnyHashable? var transitionHintExpandedGroupId: AnyHashable?
if let contentAnimation = contentAnimation { if let contentAnimation = contentAnimation {
switch contentAnimation.type { switch contentAnimation.type {
case let .groupInstalled(groupId): case let .groupInstalled(groupId, scrollToGroup):
transitionHintInstalledGroupId = groupId if scrollToGroup {
transitionHintInstalledGroupId = groupId
}
case let .groupExpanded(groupId): case let .groupExpanded(groupId):
transitionHintExpandedGroupId = groupId transitionHintExpandedGroupId = groupId
case let .groupRemoved(groupId): case let .groupRemoved(groupId):
@ -5155,7 +5189,7 @@ public final class EmojiPagerContentComponent: Component {
guard let strongSelf = self, let component = strongSelf.component else { guard let strongSelf = self, let component = strongSelf.component else {
return return
} }
component.inputInteractionHolder.inputInteraction?.addGroupAction(groupId, false) component.inputInteractionHolder.inputInteraction?.addGroupAction(groupId, false, true)
}, },
performItemAction: { [weak self] item, view, rect, layer in performItemAction: { [weak self] item, view, rect, layer in
guard let strongSelf = self, let component = strongSelf.component else { guard let strongSelf = self, let component = strongSelf.component else {
@ -5337,7 +5371,7 @@ public final class EmojiPagerContentComponent: Component {
guard let strongSelf = self, let component = strongSelf.component else { guard let strongSelf = self, let component = strongSelf.component else {
return return
} }
component.inputInteractionHolder.inputInteraction?.addGroupAction(groupId, isPremiumLocked) component.inputInteractionHolder.inputInteraction?.addGroupAction(groupId, isPremiumLocked, true)
} }
)), )),
environment: {}, environment: {},
@ -5967,6 +6001,10 @@ public final class EmojiPagerContentComponent: Component {
self.component = component self.component = component
self.state = state self.state = state
if component.searchAlwaysActive {
self.isSearchActivated = true
}
component.inputInteractionHolder.inputInteraction?.peekBehavior?.setGestureRecognizerEnabled(view: self, isEnabled: true, itemAtPoint: { [weak self] point in component.inputInteractionHolder.inputInteraction?.peekBehavior?.setGestureRecognizerEnabled(view: self, isEnabled: true, itemAtPoint: { [weak self] point in
guard let strongSelf = self else { guard let strongSelf = self else {
return nil return nil
@ -6019,10 +6057,15 @@ public final class EmojiPagerContentComponent: Component {
var transitionHintInstalledGroupId: AnyHashable? var transitionHintInstalledGroupId: AnyHashable?
var transitionHintExpandedGroupId: AnyHashable? var transitionHintExpandedGroupId: AnyHashable?
var keepOffset = false
if let contentAnimation = contentAnimation { if let contentAnimation = contentAnimation {
switch contentAnimation.type { switch contentAnimation.type {
case let .groupInstalled(groupId): case let .groupInstalled(groupId, scrollToGroup):
transitionHintInstalledGroupId = groupId if scrollToGroup {
transitionHintInstalledGroupId = groupId
} else {
keepOffset = true
}
case let .groupExpanded(groupId): case let .groupExpanded(groupId):
transitionHintExpandedGroupId = groupId transitionHintExpandedGroupId = groupId
case let .groupRemoved(groupId): case let .groupRemoved(groupId):
@ -6258,7 +6301,7 @@ public final class EmojiPagerContentComponent: Component {
self.previousScrollingOffset = ScrollingOffsetState(value: scrollView.contentOffset.y, isDraggingOrDecelerating: scrollView.isDragging || scrollView.isDecelerating) self.previousScrollingOffset = ScrollingOffsetState(value: scrollView.contentOffset.y, isDraggingOrDecelerating: scrollView.isDragging || scrollView.isDecelerating)
var animatedScrollOffset: CGFloat = 0.0 var animatedScrollOffset: CGFloat = 0.0
if !anchorItems.isEmpty { if !anchorItems.isEmpty && !keepOffset {
let sortedAnchorItems: [(ItemLayer.Key, CGRect)] = anchorItems.sorted(by: { lhs, rhs in let sortedAnchorItems: [(ItemLayer.Key, CGRect)] = anchorItems.sorted(by: { lhs, rhs in
if lhs.value.minY != rhs.value.minY { if lhs.value.minY != rhs.value.minY {
return lhs.value.minY < rhs.value.minY return lhs.value.minY < rhs.value.minY
@ -6390,20 +6433,24 @@ public final class EmojiPagerContentComponent: Component {
strongSelf.component?.inputInteractionHolder.inputInteraction?.requestUpdate(.immediate) strongSelf.component?.inputInteractionHolder.inputInteraction?.requestUpdate(.immediate)
} }
}, deactivated: { [weak self] isFirstResponder in }, deactivated: { [weak self] isFirstResponder in
guard let strongSelf = self else { guard let strongSelf = self, let component = strongSelf.component else {
return return
} }
strongSelf.scrollToTop() if let externalCancel = component.inputInteractionHolder.inputInteraction?.externalCancel {
externalCancel()
strongSelf.isSearchActivated = false
strongSelf.pagerEnvironment?.onWantsExclusiveModeUpdated(false)
if strongSelf.component?.searchInitiallyHidden == false {
if !isFirstResponder {
strongSelf.component?.inputInteractionHolder.inputInteraction?.requestUpdate(.easeInOut(duration: 0.2))
}
} else { } else {
strongSelf.component?.inputInteractionHolder.inputInteraction?.requestUpdate(.immediate) strongSelf.scrollToTop()
strongSelf.isSearchActivated = false
strongSelf.pagerEnvironment?.onWantsExclusiveModeUpdated(false)
if strongSelf.component?.searchInitiallyHidden == false {
if !isFirstResponder {
strongSelf.component?.inputInteractionHolder.inputInteraction?.requestUpdate(.easeInOut(duration: 0.2))
}
} else {
strongSelf.component?.inputInteractionHolder.inputInteraction?.requestUpdate(.immediate)
}
} }
}, updateQuery: { [weak self] query in }, updateQuery: { [weak self] query in
guard let strongSelf = self else { guard let strongSelf = self else {
@ -7490,6 +7537,7 @@ public final class EmojiPagerContentComponent: Component {
displaySearchWithPlaceholder: displaySearchWithPlaceholder, displaySearchWithPlaceholder: displaySearchWithPlaceholder,
searchCategories: searchCategories, searchCategories: searchCategories,
searchInitiallyHidden: searchInitiallyHidden, searchInitiallyHidden: searchInitiallyHidden,
searchAlwaysActive: false,
searchIsPlaceholderOnly: false, searchIsPlaceholderOnly: false,
emptySearchResults: nil, emptySearchResults: nil,
enableLongPress: (isReactionSelection && !isQuickReactionSelection) || isStatusSelection, enableLongPress: (isReactionSelection && !isQuickReactionSelection) || isStatusSelection,
@ -8010,6 +8058,7 @@ public final class EmojiPagerContentComponent: Component {
displaySearchWithPlaceholder: hasSearch ? strings.StickersSearch_SearchStickersPlaceholder : nil, displaySearchWithPlaceholder: hasSearch ? strings.StickersSearch_SearchStickersPlaceholder : nil,
searchCategories: searchCategories, searchCategories: searchCategories,
searchInitiallyHidden: true, searchInitiallyHidden: true,
searchAlwaysActive: false,
searchIsPlaceholderOnly: searchIsPlaceholderOnly, searchIsPlaceholderOnly: searchIsPlaceholderOnly,
emptySearchResults: nil, emptySearchResults: nil,
enableLongPress: false, enableLongPress: false,

View File

@ -0,0 +1,466 @@
import Foundation
import UIKit
import Display
import ComponentFlow
import TelegramPresentationData
import TelegramCore
import Postbox
import AnimationCache
import MultiAnimationRenderer
import AccountContext
import AsyncDisplayKit
import ComponentDisplayAdapters
import PagerComponent
import SwiftSignalKit
public final class EmojiSearchContent: ASDisplayNode, EntitySearchContainerNode {
private struct Params: Equatable {
var size: CGSize
var leftInset: CGFloat
var rightInset: CGFloat
var bottomInset: CGFloat
var inputHeight: CGFloat
var deviceMetrics: DeviceMetrics
}
private let context: AccountContext
private var initialFocusId: ItemCollectionId?
private let hasPremiumForUse: Bool
private let hasPremiumForInstallation: Bool
private let parentInputInteraction: EmojiPagerContentComponent.InputInteraction
private var presentationData: PresentationData
private let keyboardView = ComponentView<Empty>()
private let panelHostView: PagerExternalTopPanelContainer
private let inputInteractionHolder: EmojiPagerContentComponent.InputInteractionHolder
private var params: Params?
private var itemGroups: [EmojiPagerContentComponent.ItemGroup] = []
public var onCancel: (() -> Void)?
private let emojiSearchDisposable = MetaDisposable()
private let emojiSearchResult = Promise<(groups: [EmojiPagerContentComponent.ItemGroup], id: AnyHashable)?>(nil)
private var emojiSearchResultValue: (groups: [EmojiPagerContentComponent.ItemGroup], id: AnyHashable)?
private var dataDisposable: Disposable?
public init(
context: AccountContext,
items: [FeaturedStickerPackItem],
initialFocusId: ItemCollectionId?,
hasPremiumForUse: Bool,
hasPremiumForInstallation: Bool,
parentInputInteraction: EmojiPagerContentComponent.InputInteraction
) {
self.context = context
self.initialFocusId = initialFocusId
self.hasPremiumForUse = hasPremiumForUse
self.hasPremiumForInstallation = hasPremiumForInstallation
self.parentInputInteraction = parentInputInteraction
self.presentationData = context.sharedContext.currentPresentationData.with { $0 }
self.panelHostView = PagerExternalTopPanelContainer()
self.inputInteractionHolder = EmojiPagerContentComponent.InputInteractionHolder()
super.init()
for groupItem in items {
var groupItems: [EmojiPagerContentComponent.Item] = []
for item in groupItem.topItems {
var tintMode: EmojiPagerContentComponent.Item.TintMode = .none
if item.file.isCustomTemplateEmoji {
tintMode = .primary
}
let animationData = EntityKeyboardAnimationData(file: item.file)
let resultItem = EmojiPagerContentComponent.Item(
animationData: animationData,
content: .animation(animationData),
itemFile: item.file,
subgroupId: nil,
icon: .none,
tintMode: tintMode
)
groupItems.append(resultItem)
}
//TODO:localize
self.itemGroups.append(EmojiPagerContentComponent.ItemGroup(
supergroupId: AnyHashable(groupItem.info.id),
groupId: AnyHashable(groupItem.info.id),
title: groupItem.info.title,
subtitle: nil,
actionButtonTitle: "Add \(groupItem.info.title)",
isFeatured: true,
isPremiumLocked: !self.hasPremiumForInstallation,
isEmbedded: false,
hasClear: false,
collapsedLineCount: 3,
displayPremiumBadges: false,
headerItem: nil,
items: groupItems
))
}
self.inputInteractionHolder.inputInteraction = EmojiPagerContentComponent.InputInteraction(
performItemAction: { [weak self] groupId, item, sourceView, sourceRect, sourceLayer, isPreview in
guard let self else {
return
}
self.parentInputInteraction.performItemAction(groupId, item, sourceView, sourceRect, sourceLayer, isPreview)
if self.hasPremiumForUse {
self.onCancel?()
}
},
deleteBackwards: {
},
openStickerSettings: {
},
openFeatured: {
},
openSearch: {
},
addGroupAction: { [weak self] groupId, isPremiumLocked, _ in
guard let self else {
return
}
self.parentInputInteraction.addGroupAction(groupId, isPremiumLocked, false)
if !isPremiumLocked {
if self.itemGroups.count == 1 {
self.onCancel?()
} else {
self.itemGroups.removeAll(where: { $0.groupId == groupId })
self.update(transition: Transition(animation: .curve(duration: 0.4, curve: .spring)).withUserData(EmojiPagerContentComponent.ContentAnimation(type: .groupRemoved(id: groupId))))
}
}
},
clearGroup: { _ in
},
pushController: { _ in
},
presentController: { _ in
},
presentGlobalOverlayController: { _ in
},
navigationController: {
return nil
},
requestUpdate: { _ in
},
updateSearchQuery: { [weak self] query in
guard let self else {
return
}
switch query {
case .none:
self.emojiSearchDisposable.set(nil)
self.emojiSearchResult.set(.single(nil))
case let .text(rawQuery, languageCode):
let query = rawQuery.trimmingCharacters(in: .whitespacesAndNewlines)
if query.isEmpty {
self.emojiSearchDisposable.set(nil)
self.emojiSearchResult.set(.single(nil))
} else {
let context = self.context
var signal = context.engine.stickers.searchEmojiKeywords(inputLanguageCode: languageCode, query: query, completeMatch: false)
if !languageCode.lowercased().hasPrefix("en") {
signal = signal
|> mapToSignal { keywords in
return .single(keywords)
|> then(
context.engine.stickers.searchEmojiKeywords(inputLanguageCode: "en-US", query: query, completeMatch: query.count < 3)
|> map { englishKeywords in
return keywords + englishKeywords
}
)
}
}
let hasPremium = context.engine.data.subscribe(TelegramEngine.EngineData.Item.Peer.Peer(id: context.account.peerId))
|> map { peer -> Bool in
guard case let .user(user) = peer else {
return false
}
return user.isPremium
}
|> distinctUntilChanged
let resultSignal = signal
|> mapToSignal { keywords -> Signal<[EmojiPagerContentComponent.ItemGroup], NoError> in
return combineLatest(
context.account.postbox.itemCollectionsView(orderedItemListCollectionIds: [], namespaces: [Namespaces.ItemCollection.CloudEmojiPacks], aroundIndex: nil, count: 10000000),
context.engine.stickers.availableReactions(),
hasPremium
)
|> take(1)
|> map { view, availableReactions, hasPremium -> [EmojiPagerContentComponent.ItemGroup] in
var result: [(String, TelegramMediaFile?, String)] = []
var allEmoticons: [String: String] = [:]
for keyword in keywords {
for emoticon in keyword.emoticons {
allEmoticons[emoticon] = keyword.keyword
}
}
for entry in view.entries {
guard let item = entry.item as? StickerPackItem else {
continue
}
for attribute in item.file.attributes {
switch attribute {
case let .CustomEmoji(_, _, alt, _):
if !item.file.isPremiumEmoji || hasPremium {
if !alt.isEmpty, let keyword = allEmoticons[alt] {
result.append((alt, item.file, keyword))
} else if alt == query {
result.append((alt, item.file, alt))
}
}
default:
break
}
}
}
var items: [EmojiPagerContentComponent.Item] = []
var existingIds = Set<MediaId>()
for item in result {
if let itemFile = item.1 {
if existingIds.contains(itemFile.fileId) {
continue
}
existingIds.insert(itemFile.fileId)
let animationData = EntityKeyboardAnimationData(file: itemFile)
let item = EmojiPagerContentComponent.Item(
animationData: animationData,
content: .animation(animationData),
itemFile: itemFile, subgroupId: nil,
icon: .none,
tintMode: animationData.isTemplate ? .primary : .none
)
items.append(item)
}
}
return [EmojiPagerContentComponent.ItemGroup(
supergroupId: "search",
groupId: "search",
title: nil,
subtitle: nil,
actionButtonTitle: nil,
isFeatured: false,
isPremiumLocked: false,
isEmbedded: false,
hasClear: false,
collapsedLineCount: nil,
displayPremiumBadges: false,
headerItem: nil,
items: items
)]
}
}
self.emojiSearchDisposable.set((resultSignal
|> delay(0.15, queue: .mainQueue())
|> deliverOnMainQueue).start(next: { [weak self] result in
guard let self else {
return
}
self.emojiSearchResult.set(.single((result, AnyHashable(query))))
}))
}
case let .category(value):
let resultSignal = self.context.engine.stickers.searchEmoji(emojiString: value)
|> mapToSignal { files -> Signal<[EmojiPagerContentComponent.ItemGroup], NoError> in
var items: [EmojiPagerContentComponent.Item] = []
var existingIds = Set<MediaId>()
for itemFile in files {
if existingIds.contains(itemFile.fileId) {
continue
}
existingIds.insert(itemFile.fileId)
let animationData = EntityKeyboardAnimationData(file: itemFile)
let item = EmojiPagerContentComponent.Item(
animationData: animationData,
content: .animation(animationData),
itemFile: itemFile, subgroupId: nil,
icon: .none,
tintMode: animationData.isTemplate ? .primary : .none
)
items.append(item)
}
return .single([EmojiPagerContentComponent.ItemGroup(
supergroupId: "search",
groupId: "search",
title: nil,
subtitle: nil,
actionButtonTitle: nil,
isFeatured: false,
isPremiumLocked: false,
isEmbedded: false,
hasClear: false,
collapsedLineCount: nil,
displayPremiumBadges: false,
headerItem: nil,
items: items
)])
}
self.emojiSearchDisposable.set((resultSignal
|> delay(0.15, queue: .mainQueue())
|> deliverOnMainQueue).start(next: { [weak self] result in
guard let self else {
return
}
self.emojiSearchResult.set(.single((result, AnyHashable(value))))
}))
}
},
updateScrollingToItemGroup: {
},
externalCancel: { [weak self] in
guard let self else {
return
}
self.onCancel?()
},
chatPeerId: nil,
peekBehavior: nil,
customLayout: nil,
externalBackground: nil,
externalExpansionView: nil,
useOpaqueTheme: true,
hideBackground: false
)
self.dataDisposable = (
self.emojiSearchResult.get()
|> deliverOnMainQueue
).start(next: { [weak self] emojiSearchResult in
guard let self else {
return
}
self.emojiSearchResultValue = emojiSearchResult
self.update(transition: .immediate)
})
}
deinit {
self.emojiSearchDisposable.dispose()
self.dataDisposable?.dispose()
}
private func update(transition: Transition) {
if let params = self.params {
self.update(size: params.size, leftInset: params.leftInset, rightInset: params.rightInset, bottomInset: params.bottomInset, inputHeight: params.inputHeight, deviceMetrics: params.deviceMetrics, transition: transition)
}
}
public func updateLayout(size: CGSize, leftInset: CGFloat, rightInset: CGFloat, bottomInset: CGFloat, inputHeight: CGFloat, deviceMetrics: DeviceMetrics, transition: ContainedViewLayoutTransition) {
self.update(size: size, leftInset: leftInset, rightInset: rightInset, bottomInset: bottomInset, inputHeight: inputHeight, deviceMetrics: deviceMetrics, transition: Transition(transition))
}
private func update(size: CGSize, leftInset: CGFloat, rightInset: CGFloat, bottomInset: CGFloat, inputHeight: CGFloat, deviceMetrics: DeviceMetrics, transition: Transition) {
self.backgroundColor = self.presentationData.theme.list.plainBackgroundColor
let params = Params(size: size, leftInset: leftInset, rightInset: rightInset, bottomInset: bottomInset, inputHeight: inputHeight, deviceMetrics: deviceMetrics)
self.params = params
//TODO:localize
var emojiContent = EmojiPagerContentComponent(
id: "emoji",
context: self.context,
avatarPeer: nil,
animationCache: self.context.animationCache,
animationRenderer: self.context.animationRenderer,
inputInteractionHolder: self.inputInteractionHolder,
panelItemGroups: [],
contentItemGroups: self.itemGroups,
itemLayoutType: .compact,
itemContentUniqueId: "main",
warpContentsOnEdges: false,
displaySearchWithPlaceholder: "Search Emoji",
searchCategories: nil,
searchInitiallyHidden: false,
searchAlwaysActive: true,
searchIsPlaceholderOnly: false,
emptySearchResults: nil,
enableLongPress: false,
selectedItems: Set()
)
if let emojiSearchResult = self.emojiSearchResultValue {
var emptySearchResults: EmojiPagerContentComponent.EmptySearchResults?
if !emojiSearchResult.groups.contains(where: { !$0.items.isEmpty }) {
emptySearchResults = EmojiPagerContentComponent.EmptySearchResults(
text: self.presentationData.strings.EmojiSearch_SearchEmojiEmptyResult,
iconFile: nil
)
}
emojiContent = emojiContent.withUpdatedItemGroups(panelItemGroups: emojiContent.panelItemGroups, contentItemGroups: emojiSearchResult.groups, itemContentUniqueId: emojiSearchResult.id, emptySearchResults: emptySearchResults)
}
let _ = self.keyboardView.update(
transition: transition.withUserData(EmojiPagerContentComponent.SynchronousLoadBehavior(isDisabled: true)),
component: AnyComponent(EntityKeyboardComponent(
theme: self.presentationData.theme,
strings: self.presentationData.strings,
isContentInFocus: true,
containerInsets: UIEdgeInsets(top: 0.0, left: leftInset, bottom: bottomInset, right: rightInset),
topPanelInsets: UIEdgeInsets(top: 0.0, left: 4.0, bottom: 0.0, right: 4.0),
emojiContent: emojiContent,
stickerContent: nil,
maskContent: nil,
gifContent: nil,
hasRecentGifs: false,
availableGifSearchEmojies: [],
defaultToEmojiTab: true,
externalTopPanelContainer: self.panelHostView,
externalBottomPanelContainer: nil,
displayTopPanelBackground: true,
topPanelExtensionUpdated: { _, _ in },
hideInputUpdated: { _, _, _ in },
hideTopPanelUpdated: { _, _ in
},
switchToTextInput: {},
switchToGifSubject: { _ in },
reorderItems: { _, _ in },
makeSearchContainerNode: { _ in return nil },
contentIdUpdated: { _ in },
deviceMetrics: deviceMetrics,
hiddenInputHeight: 0.0,
inputHeight: 0.0,
displayBottomPanel: false,
isExpanded: false,
clipContentToTopPanel: false,
hidePanels: true
)),
environment: {},
containerSize: size
)
if let keyboardComponentView = self.keyboardView.view as? EntityKeyboardComponent.View {
if keyboardComponentView.superview == nil {
self.view.addSubview(keyboardComponentView)
}
transition.setFrame(view: keyboardComponentView, frame: CGRect(origin: CGPoint(), size: size))
if let initialFocusId = self.initialFocusId {
self.initialFocusId = nil
keyboardComponentView.scrollToItemGroup(contentId: "emoji", groupId: AnyHashable(initialFocusId), subgroupId: nil, animated: false)
}
}
}
}

View File

@ -286,9 +286,28 @@ final class EmojiSearchSearchBarComponent: Component {
if let _ = self.selectedItem, let categories = component.categories, let group = categories.groups.first(where: { $0.id == itemId }) { if let _ = self.selectedItem, let categories = component.categories, let group = categories.groups.first(where: { $0.id == itemId }) {
component.searchTermUpdated(group.identifiers.joined(separator: "")) component.searchTermUpdated(group.identifiers.joined(separator: ""))
if let itemComponentView = itemView.view.view {
var offset = self.scrollView.contentOffset.x
let maxDistance: CGFloat = 44.0
if itemComponentView.frame.maxX - offset > self.scrollView.bounds.width - maxDistance {
offset = itemComponentView.frame.maxX - (self.scrollView.bounds.width - maxDistance)
}
if itemComponentView.frame.minX - offset < maxDistance {
offset = itemComponentView.frame.minX - maxDistance
}
offset = max(0.0, min(offset, self.scrollView.contentSize.width - self.scrollView.bounds.width))
if offset != self.scrollView.contentOffset.x {
self.scrollView.setContentOffset(CGPoint(x: offset, y: 0.0), animated: true)
}
}
} else { } else {
let transition = Transition(animation: .curve(duration: 0.4, curve: .spring))
transition.setBounds(view: self.scrollView, bounds: CGRect(origin: CGPoint(), size: self.scrollView.bounds.size))
self.updateScrolling(transition: transition, fromScrolling: false)
//self.scrollView.setContentOffset(CGPoint(), animated: true)
component.searchTermUpdated(nil) component.searchTermUpdated(nil)
self.scrollView.setContentOffset(CGPoint(), animated: true)
} }
break break
@ -301,8 +320,13 @@ final class EmojiSearchSearchBarComponent: Component {
func clearSelection(dispatchEvent: Bool) { func clearSelection(dispatchEvent: Bool) {
if self.selectedItem != nil { if self.selectedItem != nil {
self.selectedItem = nil self.selectedItem = nil
self.state?.updated(transition: .immediate)
self.scrollView.setContentOffset(CGPoint(), animated: true) let transition = Transition(animation: .curve(duration: 0.4, curve: .spring))
transition.setBounds(view: self.scrollView, bounds: CGRect(origin: CGPoint(), size: self.scrollView.bounds.size))
self.updateScrolling(transition: transition, fromScrolling: false)
self.state?.updated(transition: transition)
if dispatchEvent { if dispatchEvent {
self.component?.searchTermUpdated(nil) self.component?.searchTermUpdated(nil)
} }
@ -419,8 +443,18 @@ final class EmojiSearchSearchBarComponent: Component {
for (id, itemView) in self.visibleItemViews { for (id, itemView) in self.visibleItemViews {
if !validItemIds.contains(id) { if !validItemIds.contains(id) {
removedItemIds.append(id) removedItemIds.append(id)
itemView.view.view?.removeFromSuperview()
itemView.tintView.removeFromSuperview() if let itemComponentView = itemView.view.view {
transition.attachAnimation(view: itemComponentView, id: "remove", completion: { [weak itemComponentView] _ in
itemComponentView?.removeFromSuperview()
})
}
let tintView = itemView.tintView
transition.attachAnimation(view: tintView, id: "remove", completion: { [weak tintView] _ in
tintView?.removeFromSuperview()
})
//itemView.view.view?.removeFromSuperview()
//itemView.tintView.removeFromSuperview()
} }
} }
for id in removedItemIds { for id in removedItemIds {
@ -523,10 +557,6 @@ final class EmojiSearchSearchBarComponent: Component {
self.isUserInteractionEnabled = false self.isUserInteractionEnabled = false
self.textView.view?.isHidden = hasText self.textView.view?.isHidden = hasText
self.tintTextView.view?.isHidden = hasText self.tintTextView.view?.isHidden = hasText
/*if self.scrollView.contentOffset.x != 0.0 {
self.scrollView.setContentOffset(CGPoint(), animated: true)
}*/
case .inactive: case .inactive:
self.isUserInteractionEnabled = true self.isUserInteractionEnabled = true
self.textView.view?.isHidden = false self.textView.view?.isHidden = false

View File

@ -115,6 +115,7 @@ public final class EntityKeyboardComponent: Component {
public let displayBottomPanel: Bool public let displayBottomPanel: Bool
public let isExpanded: Bool public let isExpanded: Bool
public let clipContentToTopPanel: Bool public let clipContentToTopPanel: Bool
public let hidePanels: Bool
public init( public init(
theme: PresentationTheme, theme: PresentationTheme,
@ -145,7 +146,8 @@ public final class EntityKeyboardComponent: Component {
inputHeight: CGFloat, inputHeight: CGFloat,
displayBottomPanel: Bool, displayBottomPanel: Bool,
isExpanded: Bool, isExpanded: Bool,
clipContentToTopPanel: Bool clipContentToTopPanel: Bool,
hidePanels: Bool = false
) { ) {
self.theme = theme self.theme = theme
self.strings = strings self.strings = strings
@ -176,6 +178,7 @@ public final class EntityKeyboardComponent: Component {
self.displayBottomPanel = displayBottomPanel self.displayBottomPanel = displayBottomPanel
self.isExpanded = isExpanded self.isExpanded = isExpanded
self.clipContentToTopPanel = clipContentToTopPanel self.clipContentToTopPanel = clipContentToTopPanel
self.hidePanels = hidePanels
} }
public static func ==(lhs: EntityKeyboardComponent, rhs: EntityKeyboardComponent) -> Bool { public static func ==(lhs: EntityKeyboardComponent, rhs: EntityKeyboardComponent) -> Bool {
@ -701,6 +704,8 @@ public final class EntityKeyboardComponent: Component {
let panelHideBehavior: PagerComponentPanelHideBehavior let panelHideBehavior: PagerComponentPanelHideBehavior
if self.searchComponent != nil { if self.searchComponent != nil {
panelHideBehavior = .hide panelHideBehavior = .hide
} else if component.hidePanels {
panelHideBehavior = .disable
} else if component.isExpanded { } else if component.isExpanded {
panelHideBehavior = .show panelHideBehavior = .show
} else { } else {
@ -914,6 +919,25 @@ public final class EntityKeyboardComponent: Component {
} }
} }
public func openCustomSearch(content: EntitySearchContainerNode) {
guard let component = self.component else {
return
}
if self.searchComponent != nil {
return
}
self.searchComponent = EntitySearchContentComponent(
makeContainerNode: {
return content
},
dismissSearch: { [weak self] in
self?.closeSearch()
}
)
component.hideInputUpdated(true, true, Transition(animation: .curve(duration: 0.3, curve: .spring)))
}
private func closeSearch() { private func closeSearch() {
guard let component = self.component else { guard let component = self.component else {
return return
@ -926,7 +950,7 @@ public final class EntityKeyboardComponent: Component {
component.hideInputUpdated(false, false, Transition(animation: .curve(duration: 0.4, curve: .spring))) component.hideInputUpdated(false, false, Transition(animation: .curve(duration: 0.4, curve: .spring)))
} }
private func scrollToItemGroup(contentId: String, groupId: AnyHashable, subgroupId: Int32?) { public func scrollToItemGroup(contentId: String, groupId: AnyHashable, subgroupId: Int32?, animated: Bool = true) {
guard let pagerView = self.pagerView.findTaggedView(tag: PagerComponentViewTag()) as? PagerComponent<EntityKeyboardChildEnvironment, EntityKeyboardTopContainerPanelEnvironment>.View else { guard let pagerView = self.pagerView.findTaggedView(tag: PagerComponentViewTag()) as? PagerComponent<EntityKeyboardChildEnvironment, EntityKeyboardTopContainerPanelEnvironment>.View else {
return return
} }
@ -938,7 +962,7 @@ public final class EntityKeyboardComponent: Component {
} }
self.component?.emojiContent?.inputInteractionHolder.inputInteraction?.updateScrollingToItemGroup() self.component?.emojiContent?.inputInteractionHolder.inputInteraction?.updateScrollingToItemGroup()
pagerContentView.scrollToItemGroup(id: groupId, subgroupId: subgroupId) pagerContentView.scrollToItemGroup(id: groupId, subgroupId: subgroupId, animated: animated)
pagerView.collapseTopPanel() pagerView.collapseTopPanel()
} }

View File

@ -289,7 +289,7 @@ public final class GifPagerContentComponent: Component {
self.itemSize = floor((itemHorizontalSpace - self.horizontalSpacing * CGFloat(itemsPerRow - 1)) / CGFloat(itemsPerRow)) self.itemSize = floor((itemHorizontalSpace - self.horizontalSpacing * CGFloat(itemsPerRow - 1)) / CGFloat(itemsPerRow))
let numRowsInGroup = (itemCount + (self.itemsPerRow - 1)) / self.itemsPerRow let numRowsInGroup = (itemCount + (self.itemsPerRow - 1)) / self.itemsPerRow
self.contentSize = CGSize(width: width, height: self.containerInsets.top + self.containerInsets.bottom + CGFloat(numRowsInGroup) * self.itemSize + CGFloat(max(0, numRowsInGroup - 1)) * self.verticalSpacing) self.contentSize = CGSize(width: width, height: self.searchInsets.top + self.searchHeight + self.containerInsets.top + self.containerInsets.bottom + CGFloat(numRowsInGroup) * self.itemSize + CGFloat(max(0, numRowsInGroup - 1)) * self.verticalSpacing)
} }
func frame(at index: Int) -> CGRect { func frame(at index: Int) -> CGRect {
@ -730,13 +730,18 @@ public final class GifPagerContentComponent: Component {
} }
private func updateScrollingOffset(transition: Transition) { private func updateScrollingOffset(transition: Transition) {
let isInteracting = scrollView.isDragging || scrollView.isDecelerating let isInteracting = self.scrollView.isDragging || self.scrollView.isDecelerating
if let previousScrollingOffsetValue = self.previousScrollingOffset { if let previousScrollingOffsetValue = self.previousScrollingOffset {
let currentBounds = scrollView.bounds let currentBounds = self.scrollView.bounds
let offsetToTopEdge = max(0.0, currentBounds.minY - 0.0) var offsetToTopEdge = max(0.0, currentBounds.minY - 0.0)
let offsetToBottomEdge = max(0.0, scrollView.contentSize.height - currentBounds.maxY) var offsetToBottomEdge = max(0.0, self.scrollView.contentSize.height - currentBounds.maxY)
let relativeOffset = scrollView.contentOffset.y - previousScrollingOffsetValue if self.scrollView.contentSize.height < self.scrollView.bounds.height * 2.0 {
offsetToTopEdge = 0.0
offsetToBottomEdge = self.scrollView.contentSize.height
}
let relativeOffset = self.scrollView.contentOffset.y - previousScrollingOffsetValue
self.pagerEnvironment?.onChildScrollingUpdate(PagerComponentChildEnvironment.ContentScrollingUpdate( self.pagerEnvironment?.onChildScrollingUpdate(PagerComponentChildEnvironment.ContentScrollingUpdate(
relativeOffset: relativeOffset, relativeOffset: relativeOffset,
absoluteOffsetToTopEdge: offsetToTopEdge, absoluteOffsetToTopEdge: offsetToTopEdge,
@ -745,19 +750,21 @@ public final class GifPagerContentComponent: Component {
isInteracting: isInteracting, isInteracting: isInteracting,
transition: transition transition: transition
)) ))
self.previousScrollingOffset = scrollView.contentOffset.y self.previousScrollingOffset = self.scrollView.contentOffset.y
} }
self.previousScrollingOffset = scrollView.contentOffset.y self.previousScrollingOffset = self.scrollView.contentOffset.y
} }
private func snappedContentOffset(proposedOffset: CGFloat) -> CGFloat { private func snappedContentOffset(proposedOffset: CGFloat) -> CGFloat {
guard let pagerEnvironment = self.pagerEnvironment else { guard let pagerEnvironment = self.pagerEnvironment, let itemLayout = self.itemLayout else {
return proposedOffset return proposedOffset
} }
var proposedOffset = proposedOffset var proposedOffset = proposedOffset
let bounds = self.bounds let bounds = self.bounds
if proposedOffset + bounds.height > self.scrollView.contentSize.height - pagerEnvironment.containerInsets.bottom { if proposedOffset <= itemLayout.searchInsets.top + itemLayout.searchHeight * 0.5 {
proposedOffset = 0.0
} else if proposedOffset + bounds.height > self.scrollView.contentSize.height - pagerEnvironment.containerInsets.bottom {
proposedOffset = self.scrollView.contentSize.height - bounds.height proposedOffset = self.scrollView.contentSize.height - bounds.height
} }
if proposedOffset < pagerEnvironment.containerInsets.top { if proposedOffset < pagerEnvironment.containerInsets.top {
@ -775,6 +782,7 @@ public final class GifPagerContentComponent: Component {
transition.setBounds(view: self.scrollView, bounds: currentBounds) transition.setBounds(view: self.scrollView, bounds: currentBounds)
self.updateScrollingOffset(transition: transition) self.updateScrollingOffset(transition: transition)
self.updateVisibleItems(attemptSynchronousLoads: false, transition: transition, fromScrolling: true)
} }
private func updateVisibleItems(attemptSynchronousLoads: Bool, transition: Transition, fromScrolling: Bool) { private func updateVisibleItems(attemptSynchronousLoads: Bool, transition: Transition, fromScrolling: Bool) {

View File

@ -928,7 +928,7 @@ private final class ForumCreateTopicScreenComponent: CombinedComponent {
}, },
openSearch: { openSearch: {
}, },
addGroupAction: { groupId, isPremiumLocked in addGroupAction: { groupId, isPremiumLocked, _ in
guard let collectionId = groupId.base as? ItemCollectionId else { guard let collectionId = groupId.base as? ItemCollectionId else {
return return
} }

View File

@ -8662,27 +8662,33 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
guard let strongSelf = self else { guard let strongSelf = self else {
return return
} }
let subjectFlags: TelegramChatBannedRightsFlags let subjectFlags: [TelegramChatBannedRightsFlags]
switch subject { switch subject {
case .stickers: case .stickers:
subjectFlags = .banSendStickers subjectFlags = [.banSendStickers]
case .mediaRecording, .premiumVoiceMessages: case .mediaRecording, .premiumVoiceMessages:
subjectFlags = .banSendMedia subjectFlags = [.banSendVoice, .banSendInstantVideos]
} }
let bannedPermission: (Int32, Bool)? var bannedPermission: (Int32, Bool)? = nil
if let channel = strongSelf.presentationInterfaceState.renderedPeer?.peer as? TelegramChannel { if let channel = strongSelf.presentationInterfaceState.renderedPeer?.peer as? TelegramChannel {
bannedPermission = channel.hasBannedPermission(subjectFlags) for subjectFlag in subjectFlags {
} else if let group = strongSelf.presentationInterfaceState.renderedPeer?.peer as? TelegramGroup { if let value = channel.hasBannedPermission(subjectFlag) {
if group.hasBannedPermission(subjectFlags) { bannedPermission = value
bannedPermission = (Int32.max, false) break
} else { }
bannedPermission = nil }
} else if let group = strongSelf.presentationInterfaceState.renderedPeer?.peer as? TelegramGroup {
for subjectFlag in subjectFlags {
if group.hasBannedPermission(subjectFlag) {
bannedPermission = (Int32.max, false)
break
}
} }
} else {
bannedPermission = nil
} }
var displayToast = false
if let (untilDate, personal) = bannedPermission { if let (untilDate, personal) = bannedPermission {
let banDescription: String let banDescription: String
switch subject { switch subject {
@ -8700,7 +8706,8 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
} else if personal { } else if personal {
banDescription = strongSelf.presentationInterfaceState.strings.Conversation_RestrictedMedia banDescription = strongSelf.presentationInterfaceState.strings.Conversation_RestrictedMedia
} else { } else {
banDescription = strongSelf.presentationInterfaceState.strings.Conversation_DefaultRestrictedMedia banDescription = strongSelf.restrictedSendingContentsText()
displayToast = true
} }
case .premiumVoiceMessages: case .premiumVoiceMessages:
banDescription = "" banDescription = ""
@ -8714,37 +8721,41 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
switch displayType { switch displayType {
case .tooltip: case .tooltip:
var rect: CGRect? if displayToast {
let isStickers: Bool = subject == .stickers strongSelf.controllerInteraction?.displayUndo(.info(title: nil, text: banDescription))
switch subject { } else {
case .stickers: var rect: CGRect?
rect = strongSelf.chatDisplayNode.frameForStickersButton() let isStickers: Bool = subject == .stickers
if var rectValue = rect, let actionRect = strongSelf.chatDisplayNode.frameForInputActionButton() { switch subject {
rectValue.origin.y = actionRect.minY case .stickers:
rect = rectValue rect = strongSelf.chatDisplayNode.frameForStickersButton()
} if var rectValue = rect, let actionRect = strongSelf.chatDisplayNode.frameForInputActionButton() {
case .mediaRecording, .premiumVoiceMessages: rectValue.origin.y = actionRect.minY
rect = strongSelf.chatDisplayNode.frameForInputActionButton() rect = rectValue
}
if let tooltipController = strongSelf.mediaRestrictedTooltipController, strongSelf.mediaRestrictedTooltipControllerMode == isStickers {
tooltipController.updateContent(.text(banDescription), animated: true, extendTimer: true)
} else if let rect = rect {
strongSelf.mediaRestrictedTooltipController?.dismiss()
let tooltipController = TooltipController(content: .text(banDescription), baseFontSize: strongSelf.presentationData.listsFontSize.baseDisplaySize)
strongSelf.mediaRestrictedTooltipController = tooltipController
strongSelf.mediaRestrictedTooltipControllerMode = isStickers
tooltipController.dismissed = { [weak tooltipController] _ in
if let strongSelf = self, let tooltipController = tooltipController, strongSelf.mediaRestrictedTooltipController === tooltipController {
strongSelf.mediaRestrictedTooltipController = nil
} }
case .mediaRecording, .premiumVoiceMessages:
rect = strongSelf.chatDisplayNode.frameForInputActionButton()
} }
strongSelf.present(tooltipController, in: .window(.root), with: TooltipControllerPresentationArguments(sourceNodeAndRect: {
if let strongSelf = self { if let tooltipController = strongSelf.mediaRestrictedTooltipController, strongSelf.mediaRestrictedTooltipControllerMode == isStickers {
return (strongSelf.chatDisplayNode, rect) tooltipController.updateContent(.text(banDescription), animated: true, extendTimer: true)
} else if let rect = rect {
strongSelf.mediaRestrictedTooltipController?.dismiss()
let tooltipController = TooltipController(content: .text(banDescription), baseFontSize: strongSelf.presentationData.listsFontSize.baseDisplaySize)
strongSelf.mediaRestrictedTooltipController = tooltipController
strongSelf.mediaRestrictedTooltipControllerMode = isStickers
tooltipController.dismissed = { [weak tooltipController] _ in
if let strongSelf = self, let tooltipController = tooltipController, strongSelf.mediaRestrictedTooltipController === tooltipController {
strongSelf.mediaRestrictedTooltipController = nil
}
} }
return nil strongSelf.present(tooltipController, in: .window(.root), with: TooltipControllerPresentationArguments(sourceNodeAndRect: {
})) if let strongSelf = self {
return (strongSelf.chatDisplayNode, rect)
}
return nil
}))
}
} }
case .alert: case .alert:
strongSelf.present(textAlertController(context: strongSelf.context, updatedPresentationData: strongSelf.updatedPresentationData, title: nil, text: banDescription, actions: [TextAlertAction(type: .defaultAction, title: strongSelf.presentationData.strings.Common_OK, action: {})]), in: .window(.root)) strongSelf.present(textAlertController(context: strongSelf.context, updatedPresentationData: strongSelf.updatedPresentationData, title: nil, text: banDescription, actions: [TextAlertAction(type: .defaultAction, title: strongSelf.presentationData.strings.Common_OK, action: {})]), in: .window(.root))

View File

@ -930,7 +930,7 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate {
return return
} }
//TODO:localize //TODO:localize
controller.controllerInteraction?.displayUndo(.info(title: nil, text: "The admins of this group do not allow to send text messages.")) controller.controllerInteraction?.displayUndo(.info(title: nil, text: controller.restrictedSendingContentsText()))
} else { } else {
strongSelf.ensureFocused() strongSelf.ensureFocused()
} }

View File

@ -14,6 +14,12 @@ import AnimationCache
import MultiAnimationRenderer import MultiAnimationRenderer
import TextFormat import TextFormat
import ChatControllerInteraction import ChatControllerInteraction
import ContextUI
import SwiftSignalKit
import PremiumUI
import StickerPeekUI
import UndoUI
import Pasteboard
private enum EmojisChatInputContextPanelEntryStableId: Hashable, Equatable { private enum EmojisChatInputContextPanelEntryStableId: Hashable, Equatable {
case symbol(String) case symbol(String)
@ -119,6 +125,8 @@ final class EmojisChatInputContextPanelNode: ChatInputContextPanelNode {
private let animationCache: AnimationCache private let animationCache: AnimationCache
private let animationRenderer: MultiAnimationRenderer private let animationRenderer: MultiAnimationRenderer
private weak var peekController: PeekController?
override init(context: AccountContext, theme: PresentationTheme, strings: PresentationStrings, fontSize: PresentationFontSize, chatPresentationContext: ChatPresentationContext) { override init(context: AccountContext, theme: PresentationTheme, strings: PresentationStrings, fontSize: PresentationFontSize, chatPresentationContext: ChatPresentationContext) {
self.animationCache = chatPresentationContext.animationCache self.animationCache = chatPresentationContext.animationCache
self.animationRenderer = chatPresentationContext.animationRenderer self.animationRenderer = chatPresentationContext.animationRenderer
@ -161,6 +169,281 @@ final class EmojisChatInputContextPanelNode: ChatInputContextPanelNode {
self.addSubnode(self.clippingNode) self.addSubnode(self.clippingNode)
self.clippingNode.addSubnode(self.listView) self.clippingNode.addSubnode(self.listView)
let peekRecognizer = PeekControllerGestureRecognizer(contentAtPoint: { [weak self] point in
guard let self else {
return nil
}
return self.peekContentAtPoint(point: point)
}, present: { [weak self] content, sourceView, sourceRect in
guard let strongSelf = self else {
return nil
}
let presentationData = strongSelf.context.sharedContext.currentPresentationData.with { $0 }
let controller = PeekController(presentationData: presentationData, content: content, sourceView: {
return (sourceView, sourceRect)
})
/*controller.visibilityUpdated = { [weak self] visible in
self?.previewingStickersPromise.set(visible)
self?.requestDisableStickerAnimations?(visible)
self?.simulateUpdateLayout(isVisible: !visible)
}*/
strongSelf.peekController = controller
strongSelf.interfaceInteraction?.presentController(controller, nil)
return controller
}, updateContent: { [weak self] content in
guard let strongSelf = self else {
return
}
let _ = strongSelf
})
self.view.addGestureRecognizer(peekRecognizer)
}
private func peekContentAtPoint(point: CGPoint) -> Signal<(UIView, CGRect, PeekControllerContent)?, NoError>? {
guard let presentationInterfaceState = self.presentationInterfaceState else {
return nil
}
guard let chatPeerId = presentationInterfaceState.renderedPeer?.peer?.id else {
return nil
}
var maybeFile: TelegramMediaFile?
var maybeItemLayer: CALayer?
self.listView.forEachItemNode { itemNode in
if let itemNode = itemNode as? EmojisChatInputPanelItemNode, let item = itemNode.item {
let localPoint = self.view.convert(point, to: itemNode.view)
if itemNode.view.bounds.contains(localPoint) {
maybeFile = item.file
maybeItemLayer = itemNode.layer
}
}
}
guard let file = maybeFile else {
return nil
}
guard let itemLayer = maybeItemLayer else {
return nil
}
let _ = chatPeerId
let _ = file
let _ = itemLayer
var collectionId: ItemCollectionId?
for attribute in file.attributes {
if case let .CustomEmoji(_, _, _, packReference) = attribute {
switch packReference {
case let .id(id, _):
collectionId = ItemCollectionId(namespace: Namespaces.ItemCollection.CloudEmojiPacks, id: id)
default:
break
}
}
}
var bubbleUpEmojiOrStickersets: [ItemCollectionId] = []
if let collectionId {
bubbleUpEmojiOrStickersets.append(collectionId)
}
let context = self.context
let accountPeerId = context.account.peerId
let _ = bubbleUpEmojiOrStickersets
let _ = context
let _ = accountPeerId
return context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: accountPeerId))
|> map { peer -> Bool in
var hasPremium = false
if case let .user(user) = peer, user.isPremium {
hasPremium = true
}
return hasPremium
}
|> deliverOnMainQueue
|> map { [weak self, weak itemLayer] hasPremium -> (UIView, CGRect, PeekControllerContent)? in
guard let strongSelf = self, let itemLayer = itemLayer else {
return nil
}
let _ = strongSelf
let _ = itemLayer
var menuItems: [ContextMenuItem] = []
menuItems.removeAll()
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
let _ = presentationData
var isLocked = false
if !hasPremium {
isLocked = file.isPremiumEmoji
if isLocked && chatPeerId == context.account.peerId {
isLocked = false
}
}
if let interaction = strongSelf.interfaceInteraction {
let _ = interaction
let sendEmoji: (TelegramMediaFile) -> Void = { file in
guard let self else {
return
}
guard let controller = (self.interfaceInteraction?.chatController() as? ChatControllerImpl) else {
return
}
var text = "."
var emojiAttribute: ChatTextInputTextCustomEmojiAttribute?
loop: for attribute in file.attributes {
switch attribute {
case let .CustomEmoji(_, _, displayText, stickerPackReference):
text = displayText
var packId: ItemCollectionId?
if case let .id(id, _) = stickerPackReference {
packId = ItemCollectionId(namespace: Namespaces.ItemCollection.CloudEmojiPacks, id: id)
}
emojiAttribute = ChatTextInputTextCustomEmojiAttribute(interactivelySelectedFromPackId: packId, fileId: file.fileId.id, file: file)
break loop
default:
break
}
}
if let emojiAttribute {
controller.controllerInteraction?.sendEmoji(text, emojiAttribute, true)
}
}
let setStatus: (TelegramMediaFile) -> Void = { file in
guard let self else {
return
}
guard let controller = (self.interfaceInteraction?.chatController() as? ChatControllerImpl) else {
return
}
let _ = self.context.engine.accountData.setEmojiStatus(file: file, expirationDate: nil).start()
var animateInAsReplacement = false
animateInAsReplacement = false
/*if let currentUndoOverlayController = strongSelf.currentUndoOverlayController {
currentUndoOverlayController.dismissWithCommitActionAndReplacementAnimation()
strongSelf.currentUndoOverlayController = nil
animateInAsReplacement = true
}*/
let presentationData = self.context.sharedContext.currentPresentationData.with { $0 }
//TODO:localize
let undoController = UndoOverlayController(presentationData: presentationData, content: .sticker(context: self.context, file: file, title: nil, text: "Your emoji status has been updated.", undoText: nil, customAction: nil), elevatedLayout: false, animateInAsReplacement: animateInAsReplacement, action: { _ in return false })
//strongSelf.currentUndoOverlayController = controller
controller.controllerInteraction?.presentController(undoController, nil)
}
let copyEmoji: (TelegramMediaFile) -> Void = { file in
var text = "."
var emojiAttribute: ChatTextInputTextCustomEmojiAttribute?
loop: for attribute in file.attributes {
switch attribute {
case let .CustomEmoji(_, _, displayText, _):
text = displayText
emojiAttribute = ChatTextInputTextCustomEmojiAttribute(interactivelySelectedFromPackId: nil, fileId: file.fileId.id, file: file)
break loop
default:
break
}
}
if let _ = emojiAttribute {
storeMessageTextInPasteboard(text, entities: [MessageTextEntity(range: 0 ..< (text as NSString).length, type: .CustomEmoji(stickerPack: nil, fileId: file.fileId.id))])
}
}
//TODO:localize
menuItems.append(.action(ContextMenuActionItem(text: "Send Emoji", icon: { theme in
if let image = generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Download"), color: theme.actionSheet.primaryTextColor) {
return generateImage(image.size, rotatedContext: { size, context in
context.clear(CGRect(origin: CGPoint(), size: size))
if let cgImage = image.cgImage {
context.draw(cgImage, in: CGRect(origin: CGPoint(), size: size))
}
})
} else {
return nil
}
}, action: { _, f in
sendEmoji(file)
f(.default)
})))
//TODO:localize
menuItems.append(.action(ContextMenuActionItem(text: "Set as Status", icon: { theme in
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Smile"), color: theme.actionSheet.primaryTextColor)
}, action: { _, f in
f(.default)
guard let strongSelf = self else {
return
}
if hasPremium {
setStatus(file)
} else {
var replaceImpl: ((ViewController) -> Void)?
let controller = PremiumDemoScreen(context: context, subject: .animatedEmoji, action: {
let controller = PremiumIntroScreen(context: context, source: .animatedEmoji)
replaceImpl?(controller)
})
replaceImpl = { [weak controller] c in
controller?.replace(with: c)
}
strongSelf.interfaceInteraction?.getNavigationController()?.pushViewController(controller)
}
})))
//TODO:localize
menuItems.append(.action(ContextMenuActionItem(text: "Copy Emoji", icon: { theme in
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Copy"), color: theme.actionSheet.primaryTextColor)
}, action: { _, f in
copyEmoji(file)
f(.default)
})))
}
if menuItems.isEmpty {
return nil
}
let content = StickerPreviewPeekContent(account: context.account, theme: presentationData.theme, strings: presentationData.strings, item: .pack(file), isLocked: isLocked, menu: menuItems, openPremiumIntro: { [weak self] in
guard let self else {
return
}
guard let interfaceInteraction = self.interfaceInteraction else {
return
}
let _ = self
let _ = interfaceInteraction
let controller = PremiumIntroScreen(context: context, source: .stickers)
//let _ = controller
interfaceInteraction.getNavigationController()?.pushViewController(controller)
})
let _ = content
//return nil
return (strongSelf.view, itemLayer.convert(itemLayer.bounds, to: strongSelf.view.layer), content)
}
} }
func updateResults(_ results: [(String, TelegramMediaFile?, String)]) { func updateResults(_ results: [(String, TelegramMediaFile?, String)]) {

View File

@ -17,7 +17,7 @@ final class EmojisChatInputPanelItem: ListViewItem {
fileprivate let theme: PresentationTheme fileprivate let theme: PresentationTheme
fileprivate let symbol: String fileprivate let symbol: String
fileprivate let text: String fileprivate let text: String
fileprivate let file: TelegramMediaFile? let file: TelegramMediaFile?
fileprivate let animationCache: AnimationCache fileprivate let animationCache: AnimationCache
fileprivate let animationRenderer: MultiAnimationRenderer fileprivate let animationRenderer: MultiAnimationRenderer
private let emojiSelected: (String, TelegramMediaFile?) -> Void private let emojiSelected: (String, TelegramMediaFile?) -> Void
@ -94,6 +94,8 @@ final class EmojisChatInputPanelItemNode: ListViewItemNode {
private let symbolNode: TextNode private let symbolNode: TextNode
private var emojiView: EmojiTextAttachmentView? private var emojiView: EmojiTextAttachmentView?
var item: EmojisChatInputPanelItem?
init() { init() {
self.symbolNode = TextNode() self.symbolNode = TextNode()
self.symbolNode.transform = CATransform3DMakeRotation(CGFloat.pi / 2.0, 0.0, 0.0, 1.0) self.symbolNode.transform = CATransform3DMakeRotation(CGFloat.pi / 2.0, 0.0, 0.0, 1.0)
@ -123,6 +125,8 @@ final class EmojisChatInputPanelItemNode: ListViewItemNode {
return (nodeLayout, { _ in return (nodeLayout, { _ in
if let strongSelf = self { if let strongSelf = self {
strongSelf.item = item
let _ = symbolApply() let _ = symbolApply()
strongSelf.symbolNode.frame = CGRect(origin: CGPoint(x: floorToScreenPixels((EmojisChatInputPanelItemNode.itemSize.width - symbolLayout.size.width) / 2.0), y: 0.0), size: symbolLayout.size) strongSelf.symbolNode.frame = CGRect(origin: CGPoint(x: floorToScreenPixels((EmojisChatInputPanelItemNode.itemSize.width - symbolLayout.size.width) / 2.0), y: 0.0), size: symbolLayout.size)