mirror of
https://github.com/Swiftgram/Telegram-iOS.git
synced 2025-11-07 09:20:08 +00:00
UI improvements
This commit is contained in:
parent
55d076a494
commit
3b42f14613
@ -150,7 +150,7 @@ public struct Transition {
|
||||
//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.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)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -149,6 +149,7 @@ public enum PagerComponentPanelHideBehavior {
|
||||
case hideOnScroll
|
||||
case show
|
||||
case hide
|
||||
case disable
|
||||
}
|
||||
|
||||
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
|
||||
if let topPanel = component.topPanel {
|
||||
let effectiveTopPanelOffsetFraction = scrollingPanelOffsetFraction
|
||||
@ -616,7 +621,7 @@ public final class PagerComponent<ChildEnvironmentType: Equatable, TopPanelEnvir
|
||||
|
||||
var topPanelVisibilityFraction: CGFloat = 1.0 - effectiveTopPanelOffsetFraction
|
||||
switch component.panelHideBehavior {
|
||||
case .hide:
|
||||
case .hide, .disable:
|
||||
topPanelVisibilityFraction = 0.0
|
||||
case .show:
|
||||
topPanelVisibilityFraction = 1.0
|
||||
@ -631,12 +636,16 @@ public final class PagerComponent<ChildEnvironmentType: Equatable, TopPanelEnvir
|
||||
|
||||
if case .hide = component.panelHideBehavior {
|
||||
topPanelOffset = topPanelSize.height
|
||||
} else if case .disable = component.panelHideBehavior {
|
||||
topPanelOffset = topPanelSize.height
|
||||
}
|
||||
|
||||
if component.externalTopPanelContainer != nil {
|
||||
var visibleTopPanelHeight = max(0.0, topPanelSize.height - topPanelOffset)
|
||||
if case .hide = component.panelHideBehavior {
|
||||
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)))
|
||||
|
||||
@ -705,6 +714,8 @@ public final class PagerComponent<ChildEnvironmentType: Equatable, TopPanelEnvir
|
||||
bottomPanelOffset = bottomPanelSize.height * scrollingPanelOffsetFraction
|
||||
if case .hide = component.panelHideBehavior {
|
||||
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))
|
||||
@ -724,7 +735,7 @@ public final class PagerComponent<ChildEnvironmentType: Equatable, TopPanelEnvir
|
||||
|
||||
let effectiveTopPanelHeight: CGFloat
|
||||
switch component.panelHideBehavior {
|
||||
case .hide:
|
||||
case .hide, .disable:
|
||||
effectiveTopPanelHeight = 0.0
|
||||
case .show, .hideOnScroll:
|
||||
if component.externalTopPanelContainer != nil {
|
||||
|
||||
@ -276,7 +276,7 @@ class StickerPickerScreen: ViewController {
|
||||
openFeatured: nil,
|
||||
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 {
|
||||
return
|
||||
}
|
||||
@ -391,7 +391,7 @@ class StickerPickerScreen: ViewController {
|
||||
openStickerSettings: nil,
|
||||
openFeatured: nil,
|
||||
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 {
|
||||
return
|
||||
}
|
||||
@ -481,7 +481,7 @@ class StickerPickerScreen: ViewController {
|
||||
openFeatured: nil,
|
||||
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 {
|
||||
return
|
||||
}
|
||||
|
||||
@ -783,6 +783,8 @@
|
||||
authInfo = [authInfo withUpdatedAuthKeyAttributes:authKeyAttributes];
|
||||
[_context updateAuthInfoForDatacenterWithId:mtProto.datacenterId authInfo:authInfo selector:authInfoSelector];
|
||||
}];
|
||||
|
||||
restartRequest = true;
|
||||
} else if (rpcError.errorCode == 406) {
|
||||
if (_didReceiveSoftAuthResetError) {
|
||||
_didReceiveSoftAuthResetError();
|
||||
|
||||
@ -1264,7 +1264,7 @@ public final class ReactionContextNode: ASDisplayNode, UIScrollViewDelegate {
|
||||
},
|
||||
openSearch: {
|
||||
},
|
||||
addGroupAction: { [weak self] groupId, isPremiumLocked in
|
||||
addGroupAction: { [weak self] groupId, isPremiumLocked, _ in
|
||||
guard let strongSelf = self, let collectionId = groupId.base as? ItemCollectionId else {
|
||||
return
|
||||
}
|
||||
@ -1284,7 +1284,7 @@ public final class ReactionContextNode: ASDisplayNode, UIScrollViewDelegate {
|
||||
for featuredEmojiPack in view.items.lazy.map({ $0.contents.get(FeaturedStickerPackItem.self)! }) {
|
||||
if featuredEmojiPack.info.id == collectionId {
|
||||
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()
|
||||
|
||||
|
||||
@ -481,7 +481,7 @@ final class AvatarEditorScreenComponent: Component {
|
||||
openFeatured: nil,
|
||||
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 {
|
||||
return
|
||||
}
|
||||
@ -615,7 +615,7 @@ final class AvatarEditorScreenComponent: Component {
|
||||
openFeatured: nil,
|
||||
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 {
|
||||
return
|
||||
}
|
||||
|
||||
@ -641,25 +641,61 @@ public final class ChatEntityKeyboardInputNode: ChatInputNode {
|
||||
var premiumToastCounter = 0
|
||||
self.emojiInputInteraction = EmojiPagerContentComponent.InputInteraction(
|
||||
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 {
|
||||
return
|
||||
}
|
||||
|
||||
if groupId == AnyHashable("featuredTop"), let file = item.itemFile {
|
||||
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)
|
||||
|> deliverOnMainQueue).start(next: { [weak interfaceInteraction, weak controllerInteraction] views in
|
||||
|> deliverOnMainQueue).start(next: { [weak interfaceInteraction, weak controllerInteraction] emojiPacksView, views in
|
||||
guard let controllerInteraction = controllerInteraction else {
|
||||
return
|
||||
}
|
||||
guard let view = views.views[viewKey] as? OrderedItemListView else {
|
||||
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 }) {
|
||||
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,
|
||||
updatedPresentationData: controllerInteraction.updatedPresentationData,
|
||||
mode: .default,
|
||||
@ -675,7 +711,7 @@ public final class ChatEntityKeyboardInputNode: ChatInputNode {
|
||||
interfaceInteraction.insertText(NSAttributedString(string: text, attributes: [ChatTextInputAttributes.customEmoji: emojiAttribute]))
|
||||
}
|
||||
)
|
||||
controllerInteraction.presentController(controller, nil)
|
||||
controllerInteraction.presentController(controller, nil)*/
|
||||
|
||||
break
|
||||
}
|
||||
@ -786,7 +822,7 @@ public final class ChatEntityKeyboardInputNode: ChatInputNode {
|
||||
},
|
||||
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 {
|
||||
return
|
||||
}
|
||||
@ -815,7 +851,7 @@ public final class ChatEntityKeyboardInputNode: ChatInputNode {
|
||||
for featuredEmojiPack in view.items.lazy.map({ $0.contents.get(FeaturedStickerPackItem.self)! }) {
|
||||
if featuredEmojiPack.info.id == collectionId {
|
||||
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()
|
||||
|
||||
@ -1181,7 +1217,7 @@ public final class ChatEntityKeyboardInputNode: ChatInputNode {
|
||||
pagerView.openSearch()
|
||||
}
|
||||
},
|
||||
addGroupAction: { groupId, isPremiumLocked in
|
||||
addGroupAction: { groupId, isPremiumLocked, _ in
|
||||
guard let controllerInteraction = controllerInteraction, let collectionId = groupId.base as? ItemCollectionId else {
|
||||
return
|
||||
}
|
||||
@ -1479,7 +1515,7 @@ public final class ChatEntityKeyboardInputNode: ChatInputNode {
|
||||
guard let strongSelf = self else {
|
||||
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
|
||||
guard let strongSelf = self, let gifContext = strongSelf.gifContext else {
|
||||
@ -1697,7 +1733,7 @@ public final class ChatEntityKeyboardInputNode: ChatInputNode {
|
||||
}
|
||||
strongSelf.reorderItems(category: category, items: items)
|
||||
},
|
||||
makeSearchContainerNode: { [weak controllerInteraction] content in
|
||||
makeSearchContainerNode: { [weak self, weak controllerInteraction] content in
|
||||
guard let controllerInteraction = controllerInteraction else {
|
||||
return nil
|
||||
}
|
||||
@ -1711,7 +1747,7 @@ public final class ChatEntityKeyboardInputNode: ChatInputNode {
|
||||
}
|
||||
|
||||
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
|
||||
return PaneSearchContainerNode(
|
||||
let searchContainerNode = PaneSearchContainerNode(
|
||||
context: context,
|
||||
theme: presentationData.theme,
|
||||
strings: presentationData.strings,
|
||||
@ -1722,6 +1758,14 @@ public final class ChatEntityKeyboardInputNode: ChatInputNode {
|
||||
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 },
|
||||
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) {
|
||||
let file = item
|
||||
|
||||
private func openGifContextMenu(file: FileMediaReference, contextResult: (ChatContextResultCollection, ChatContextResult)?, sourceView: UIView, sourceRect: CGRect, gesture: ContextGesture, isSaved: Bool) {
|
||||
let canSaveGif: Bool
|
||||
if file.file.media.fileId.namespace == Namespaces.Media.CloudFile {
|
||||
if file.media.fileId.namespace == Namespaces.Media.CloudFile {
|
||||
canSaveGif = true
|
||||
} else {
|
||||
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
|
||||
guard let strongSelf = self else {
|
||||
return
|
||||
@ -1875,7 +1917,7 @@ public final class ChatEntityKeyboardInputNode: ChatInputNode {
|
||||
}
|
||||
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
|
||||
}, baseNavigationController: nil)
|
||||
@ -1887,8 +1929,8 @@ public final class ChatEntityKeyboardInputNode: ChatInputNode {
|
||||
}, action: { _, f in
|
||||
f(.default)
|
||||
if isSaved {
|
||||
let _ = self?.controllerInteraction?.sendGif(file.file, sourceView, sourceRect, false, false)
|
||||
} else if let (collection, result) = file.contextResult {
|
||||
let _ = self?.controllerInteraction?.sendGif(file, sourceView, sourceRect, false, false)
|
||||
} else if let (collection, result) = contextResult {
|
||||
let _ = self?.controllerInteraction?.sendBotContextResultAsGif(collection, result, sourceView, sourceRect, false)
|
||||
}
|
||||
})))
|
||||
@ -1908,8 +1950,8 @@ public final class ChatEntityKeyboardInputNode: ChatInputNode {
|
||||
}, action: { _, f in
|
||||
f(.default)
|
||||
if isSaved {
|
||||
let _ = self?.controllerInteraction?.sendGif(file.file, sourceView, sourceRect, true, false)
|
||||
} else if let (collection, result) = file.contextResult {
|
||||
let _ = self?.controllerInteraction?.sendGif(file, sourceView, sourceRect, true, false)
|
||||
} else if let (collection, result) = contextResult {
|
||||
let _ = self?.controllerInteraction?.sendBotContextResultAsGif(collection, result, sourceView, sourceRect, true)
|
||||
}
|
||||
})))
|
||||
@ -1921,7 +1963,7 @@ public final class ChatEntityKeyboardInputNode: ChatInputNode {
|
||||
}, action: { _, f in
|
||||
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 {
|
||||
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 {
|
||||
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 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
|
||||
guard let strongSelf = self else {
|
||||
return
|
||||
@ -2136,7 +2178,7 @@ public final class EntityInputView: UIInputView, AttachmentTextInputPanelInputVi
|
||||
},
|
||||
openSearch: {
|
||||
},
|
||||
addGroupAction: { _, _ in
|
||||
addGroupAction: { _, _, _ in
|
||||
},
|
||||
clearGroup: { [weak self] groupId in
|
||||
guard let strongSelf = self else {
|
||||
|
||||
@ -395,7 +395,7 @@ public final class EmojiStatusSelectionController: ViewController {
|
||||
},
|
||||
openSearch: {
|
||||
},
|
||||
addGroupAction: { groupId, isPremiumLocked in
|
||||
addGroupAction: { groupId, isPremiumLocked, _ in
|
||||
guard let strongSelf = self, let collectionId = groupId.base as? ItemCollectionId else {
|
||||
return
|
||||
}
|
||||
@ -410,7 +410,7 @@ public final class EmojiStatusSelectionController: ViewController {
|
||||
for featuredEmojiPack in view.items.lazy.map({ $0.contents.get(FeaturedStickerPackItem.self)! }) {
|
||||
if featuredEmojiPack.info.id == collectionId {
|
||||
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()
|
||||
|
||||
|
||||
@ -1892,7 +1892,7 @@ public final class EmojiSearchHeaderView: UIView, UITextFieldDelegate {
|
||||
component: AnyComponent(Text(
|
||||
text: strings.Common_Cancel,
|
||||
font: Font.regular(17.0),
|
||||
color: theme.chat.inputMediaPanel.panelContentVibrantOverlayColor
|
||||
color: useOpaqueTheme ? theme.list.itemAccentColor : theme.chat.inputMediaPanel.panelContentVibrantSearchOverlayColor
|
||||
)),
|
||||
environment: {},
|
||||
containerSize: CGSize(width: size.width - 32.0, height: 100.0)
|
||||
@ -2178,7 +2178,7 @@ public final class EmojiPagerContentComponent: Component {
|
||||
public enum AnimationType {
|
||||
case generic
|
||||
case groupExpanded(id: AnyHashable)
|
||||
case groupInstalled(id: AnyHashable)
|
||||
case groupInstalled(id: AnyHashable, scrollToGroup: Bool)
|
||||
case groupRemoved(id: AnyHashable)
|
||||
}
|
||||
|
||||
@ -2239,7 +2239,7 @@ public final class EmojiPagerContentComponent: Component {
|
||||
public let openStickerSettings: (() -> Void)?
|
||||
public let openFeatured: (() -> 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 pushController: (ViewController) -> Void
|
||||
public let presentController: (ViewController) -> Void
|
||||
@ -2248,6 +2248,7 @@ public final class EmojiPagerContentComponent: Component {
|
||||
public let requestUpdate: (Transition) -> Void
|
||||
public let updateSearchQuery: (EmojiPagerContentComponent.SearchQuery?) -> Void
|
||||
public let updateScrollingToItemGroup: () -> Void
|
||||
public let externalCancel: (() -> Void)?
|
||||
public let chatPeerId: PeerId?
|
||||
public let peekBehavior: EmojiContentPeekBehavior?
|
||||
public let customLayout: CustomLayout?
|
||||
@ -2262,7 +2263,7 @@ public final class EmojiPagerContentComponent: Component {
|
||||
openStickerSettings: (() -> Void)?,
|
||||
openFeatured: (() -> Void)?,
|
||||
openSearch: @escaping () -> Void,
|
||||
addGroupAction: @escaping (AnyHashable, Bool) -> Void,
|
||||
addGroupAction: @escaping (AnyHashable, Bool, Bool) -> Void,
|
||||
clearGroup: @escaping (AnyHashable) -> Void,
|
||||
pushController: @escaping (ViewController) -> Void,
|
||||
presentController: @escaping (ViewController) -> Void,
|
||||
@ -2271,6 +2272,7 @@ public final class EmojiPagerContentComponent: Component {
|
||||
requestUpdate: @escaping (Transition) -> Void,
|
||||
updateSearchQuery: @escaping (SearchQuery?) -> Void,
|
||||
updateScrollingToItemGroup: @escaping () -> Void,
|
||||
externalCancel: (() -> Void)? = nil,
|
||||
chatPeerId: PeerId?,
|
||||
peekBehavior: EmojiContentPeekBehavior?,
|
||||
customLayout: CustomLayout?,
|
||||
@ -2293,6 +2295,7 @@ public final class EmojiPagerContentComponent: Component {
|
||||
self.requestUpdate = requestUpdate
|
||||
self.updateSearchQuery = updateSearchQuery
|
||||
self.updateScrollingToItemGroup = updateScrollingToItemGroup
|
||||
self.externalCancel = externalCancel
|
||||
self.chatPeerId = chatPeerId
|
||||
self.peekBehavior = peekBehavior
|
||||
self.customLayout = customLayout
|
||||
@ -2541,6 +2544,7 @@ public final class EmojiPagerContentComponent: Component {
|
||||
public let displaySearchWithPlaceholder: String?
|
||||
public let searchCategories: EmojiSearchCategories?
|
||||
public let searchInitiallyHidden: Bool
|
||||
public let searchAlwaysActive: Bool
|
||||
public let searchIsPlaceholderOnly: Bool
|
||||
public let emptySearchResults: EmptySearchResults?
|
||||
public let enableLongPress: Bool
|
||||
@ -2561,6 +2565,7 @@ public final class EmojiPagerContentComponent: Component {
|
||||
displaySearchWithPlaceholder: String?,
|
||||
searchCategories: EmojiSearchCategories?,
|
||||
searchInitiallyHidden: Bool,
|
||||
searchAlwaysActive: Bool,
|
||||
searchIsPlaceholderOnly: Bool,
|
||||
emptySearchResults: EmptySearchResults?,
|
||||
enableLongPress: Bool,
|
||||
@ -2580,6 +2585,7 @@ public final class EmojiPagerContentComponent: Component {
|
||||
self.displaySearchWithPlaceholder = displaySearchWithPlaceholder
|
||||
self.searchCategories = searchCategories
|
||||
self.searchInitiallyHidden = searchInitiallyHidden
|
||||
self.searchAlwaysActive = searchAlwaysActive
|
||||
self.searchIsPlaceholderOnly = searchIsPlaceholderOnly
|
||||
self.emptySearchResults = emptySearchResults
|
||||
self.enableLongPress = enableLongPress
|
||||
@ -2602,6 +2608,7 @@ public final class EmojiPagerContentComponent: Component {
|
||||
displaySearchWithPlaceholder: self.displaySearchWithPlaceholder,
|
||||
searchCategories: self.searchCategories,
|
||||
searchInitiallyHidden: self.searchInitiallyHidden,
|
||||
searchAlwaysActive: self.searchAlwaysActive,
|
||||
searchIsPlaceholderOnly: self.searchIsPlaceholderOnly,
|
||||
emptySearchResults: emptySearchResults,
|
||||
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 {
|
||||
return
|
||||
}
|
||||
@ -4332,6 +4339,9 @@ public final class EmojiPagerContentComponent: Component {
|
||||
}
|
||||
|
||||
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 {
|
||||
scrollPosition = self.scrollView.contentSize.height - self.scrollView.bounds.height
|
||||
}
|
||||
@ -4339,6 +4349,26 @@ public final class EmojiPagerContentComponent: Component {
|
||||
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
|
||||
|
||||
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
|
||||
|
||||
if let commonItemOffset = commonItemOffset {
|
||||
@ -4779,6 +4809,7 @@ public final class EmojiPagerContentComponent: Component {
|
||||
return
|
||||
}
|
||||
if case .ended = recognizer.state {
|
||||
if self.scrollViewClippingView.bounds.contains(recognizer.location(in: self.scrollViewClippingView)) {
|
||||
let locationInScrollView = recognizer.location(in: self.scrollView)
|
||||
outer: for (id, groupHeader) in self.visibleGroupHeaders {
|
||||
if groupHeader.frame.insetBy(dx: -10.0, dy: -6.0).contains(locationInScrollView) {
|
||||
@ -4816,6 +4847,7 @@ public final class EmojiPagerContentComponent: Component {
|
||||
let _ = foundItem
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private let longPressDuration: Double = 0.5
|
||||
private var longPressItem: EmojiPagerContentComponent.View.ItemLayer.Key?
|
||||
@ -5006,7 +5038,7 @@ public final class EmojiPagerContentComponent: Component {
|
||||
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
|
||||
DispatchQueue.main.async {
|
||||
scrollView.isScrollEnabled = true
|
||||
@ -5118,8 +5150,10 @@ public final class EmojiPagerContentComponent: Component {
|
||||
var transitionHintExpandedGroupId: AnyHashable?
|
||||
if let contentAnimation = contentAnimation {
|
||||
switch contentAnimation.type {
|
||||
case let .groupInstalled(groupId):
|
||||
case let .groupInstalled(groupId, scrollToGroup):
|
||||
if scrollToGroup {
|
||||
transitionHintInstalledGroupId = groupId
|
||||
}
|
||||
case let .groupExpanded(groupId):
|
||||
transitionHintExpandedGroupId = groupId
|
||||
case let .groupRemoved(groupId):
|
||||
@ -5155,7 +5189,7 @@ public final class EmojiPagerContentComponent: Component {
|
||||
guard let strongSelf = self, let component = strongSelf.component else {
|
||||
return
|
||||
}
|
||||
component.inputInteractionHolder.inputInteraction?.addGroupAction(groupId, false)
|
||||
component.inputInteractionHolder.inputInteraction?.addGroupAction(groupId, false, true)
|
||||
},
|
||||
performItemAction: { [weak self] item, view, rect, layer in
|
||||
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 {
|
||||
return
|
||||
}
|
||||
component.inputInteractionHolder.inputInteraction?.addGroupAction(groupId, isPremiumLocked)
|
||||
component.inputInteractionHolder.inputInteraction?.addGroupAction(groupId, isPremiumLocked, true)
|
||||
}
|
||||
)),
|
||||
environment: {},
|
||||
@ -5967,6 +6001,10 @@ public final class EmojiPagerContentComponent: Component {
|
||||
self.component = component
|
||||
self.state = state
|
||||
|
||||
if component.searchAlwaysActive {
|
||||
self.isSearchActivated = true
|
||||
}
|
||||
|
||||
component.inputInteractionHolder.inputInteraction?.peekBehavior?.setGestureRecognizerEnabled(view: self, isEnabled: true, itemAtPoint: { [weak self] point in
|
||||
guard let strongSelf = self else {
|
||||
return nil
|
||||
@ -6019,10 +6057,15 @@ public final class EmojiPagerContentComponent: Component {
|
||||
|
||||
var transitionHintInstalledGroupId: AnyHashable?
|
||||
var transitionHintExpandedGroupId: AnyHashable?
|
||||
var keepOffset = false
|
||||
if let contentAnimation = contentAnimation {
|
||||
switch contentAnimation.type {
|
||||
case let .groupInstalled(groupId):
|
||||
case let .groupInstalled(groupId, scrollToGroup):
|
||||
if scrollToGroup {
|
||||
transitionHintInstalledGroupId = groupId
|
||||
} else {
|
||||
keepOffset = true
|
||||
}
|
||||
case let .groupExpanded(groupId):
|
||||
transitionHintExpandedGroupId = 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)
|
||||
|
||||
var animatedScrollOffset: CGFloat = 0.0
|
||||
if !anchorItems.isEmpty {
|
||||
if !anchorItems.isEmpty && !keepOffset {
|
||||
let sortedAnchorItems: [(ItemLayer.Key, CGRect)] = anchorItems.sorted(by: { lhs, rhs in
|
||||
if lhs.value.minY != rhs.value.minY {
|
||||
return lhs.value.minY < rhs.value.minY
|
||||
@ -6390,10 +6433,13 @@ public final class EmojiPagerContentComponent: Component {
|
||||
strongSelf.component?.inputInteractionHolder.inputInteraction?.requestUpdate(.immediate)
|
||||
}
|
||||
}, deactivated: { [weak self] isFirstResponder in
|
||||
guard let strongSelf = self else {
|
||||
guard let strongSelf = self, let component = strongSelf.component else {
|
||||
return
|
||||
}
|
||||
|
||||
if let externalCancel = component.inputInteractionHolder.inputInteraction?.externalCancel {
|
||||
externalCancel()
|
||||
} else {
|
||||
strongSelf.scrollToTop()
|
||||
|
||||
strongSelf.isSearchActivated = false
|
||||
@ -6405,6 +6451,7 @@ public final class EmojiPagerContentComponent: Component {
|
||||
} else {
|
||||
strongSelf.component?.inputInteractionHolder.inputInteraction?.requestUpdate(.immediate)
|
||||
}
|
||||
}
|
||||
}, updateQuery: { [weak self] query in
|
||||
guard let strongSelf = self else {
|
||||
return
|
||||
@ -7490,6 +7537,7 @@ public final class EmojiPagerContentComponent: Component {
|
||||
displaySearchWithPlaceholder: displaySearchWithPlaceholder,
|
||||
searchCategories: searchCategories,
|
||||
searchInitiallyHidden: searchInitiallyHidden,
|
||||
searchAlwaysActive: false,
|
||||
searchIsPlaceholderOnly: false,
|
||||
emptySearchResults: nil,
|
||||
enableLongPress: (isReactionSelection && !isQuickReactionSelection) || isStatusSelection,
|
||||
@ -8010,6 +8058,7 @@ public final class EmojiPagerContentComponent: Component {
|
||||
displaySearchWithPlaceholder: hasSearch ? strings.StickersSearch_SearchStickersPlaceholder : nil,
|
||||
searchCategories: searchCategories,
|
||||
searchInitiallyHidden: true,
|
||||
searchAlwaysActive: false,
|
||||
searchIsPlaceholderOnly: searchIsPlaceholderOnly,
|
||||
emptySearchResults: nil,
|
||||
enableLongPress: false,
|
||||
|
||||
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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 }) {
|
||||
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 {
|
||||
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)
|
||||
self.scrollView.setContentOffset(CGPoint(), animated: true)
|
||||
}
|
||||
|
||||
break
|
||||
@ -301,8 +320,13 @@ final class EmojiSearchSearchBarComponent: Component {
|
||||
func clearSelection(dispatchEvent: Bool) {
|
||||
if 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 {
|
||||
self.component?.searchTermUpdated(nil)
|
||||
}
|
||||
@ -419,8 +443,18 @@ final class EmojiSearchSearchBarComponent: Component {
|
||||
for (id, itemView) in self.visibleItemViews {
|
||||
if !validItemIds.contains(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 {
|
||||
@ -523,10 +557,6 @@ final class EmojiSearchSearchBarComponent: Component {
|
||||
self.isUserInteractionEnabled = false
|
||||
self.textView.view?.isHidden = hasText
|
||||
self.tintTextView.view?.isHidden = hasText
|
||||
|
||||
/*if self.scrollView.contentOffset.x != 0.0 {
|
||||
self.scrollView.setContentOffset(CGPoint(), animated: true)
|
||||
}*/
|
||||
case .inactive:
|
||||
self.isUserInteractionEnabled = true
|
||||
self.textView.view?.isHidden = false
|
||||
|
||||
@ -115,6 +115,7 @@ public final class EntityKeyboardComponent: Component {
|
||||
public let displayBottomPanel: Bool
|
||||
public let isExpanded: Bool
|
||||
public let clipContentToTopPanel: Bool
|
||||
public let hidePanels: Bool
|
||||
|
||||
public init(
|
||||
theme: PresentationTheme,
|
||||
@ -145,7 +146,8 @@ public final class EntityKeyboardComponent: Component {
|
||||
inputHeight: CGFloat,
|
||||
displayBottomPanel: Bool,
|
||||
isExpanded: Bool,
|
||||
clipContentToTopPanel: Bool
|
||||
clipContentToTopPanel: Bool,
|
||||
hidePanels: Bool = false
|
||||
) {
|
||||
self.theme = theme
|
||||
self.strings = strings
|
||||
@ -176,6 +178,7 @@ public final class EntityKeyboardComponent: Component {
|
||||
self.displayBottomPanel = displayBottomPanel
|
||||
self.isExpanded = isExpanded
|
||||
self.clipContentToTopPanel = clipContentToTopPanel
|
||||
self.hidePanels = hidePanels
|
||||
}
|
||||
|
||||
public static func ==(lhs: EntityKeyboardComponent, rhs: EntityKeyboardComponent) -> Bool {
|
||||
@ -701,6 +704,8 @@ public final class EntityKeyboardComponent: Component {
|
||||
let panelHideBehavior: PagerComponentPanelHideBehavior
|
||||
if self.searchComponent != nil {
|
||||
panelHideBehavior = .hide
|
||||
} else if component.hidePanels {
|
||||
panelHideBehavior = .disable
|
||||
} else if component.isExpanded {
|
||||
panelHideBehavior = .show
|
||||
} 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() {
|
||||
guard let component = self.component else {
|
||||
return
|
||||
@ -926,7 +950,7 @@ public final class EntityKeyboardComponent: Component {
|
||||
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 {
|
||||
return
|
||||
}
|
||||
@ -938,7 +962,7 @@ public final class EntityKeyboardComponent: Component {
|
||||
}
|
||||
self.component?.emojiContent?.inputInteractionHolder.inputInteraction?.updateScrollingToItemGroup()
|
||||
|
||||
pagerContentView.scrollToItemGroup(id: groupId, subgroupId: subgroupId)
|
||||
pagerContentView.scrollToItemGroup(id: groupId, subgroupId: subgroupId, animated: animated)
|
||||
pagerView.collapseTopPanel()
|
||||
}
|
||||
|
||||
|
||||
@ -289,7 +289,7 @@ public final class GifPagerContentComponent: Component {
|
||||
self.itemSize = floor((itemHorizontalSpace - self.horizontalSpacing * CGFloat(itemsPerRow - 1)) / CGFloat(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 {
|
||||
@ -730,13 +730,18 @@ public final class GifPagerContentComponent: Component {
|
||||
}
|
||||
|
||||
private func updateScrollingOffset(transition: Transition) {
|
||||
let isInteracting = scrollView.isDragging || scrollView.isDecelerating
|
||||
let isInteracting = self.scrollView.isDragging || self.scrollView.isDecelerating
|
||||
if let previousScrollingOffsetValue = self.previousScrollingOffset {
|
||||
let currentBounds = scrollView.bounds
|
||||
let offsetToTopEdge = max(0.0, currentBounds.minY - 0.0)
|
||||
let offsetToBottomEdge = max(0.0, scrollView.contentSize.height - currentBounds.maxY)
|
||||
let currentBounds = self.scrollView.bounds
|
||||
var offsetToTopEdge = max(0.0, currentBounds.minY - 0.0)
|
||||
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(
|
||||
relativeOffset: relativeOffset,
|
||||
absoluteOffsetToTopEdge: offsetToTopEdge,
|
||||
@ -745,19 +750,21 @@ public final class GifPagerContentComponent: Component {
|
||||
isInteracting: isInteracting,
|
||||
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 {
|
||||
guard let pagerEnvironment = self.pagerEnvironment else {
|
||||
guard let pagerEnvironment = self.pagerEnvironment, let itemLayout = self.itemLayout else {
|
||||
return proposedOffset
|
||||
}
|
||||
|
||||
var proposedOffset = proposedOffset
|
||||
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
|
||||
}
|
||||
if proposedOffset < pagerEnvironment.containerInsets.top {
|
||||
@ -775,6 +782,7 @@ public final class GifPagerContentComponent: Component {
|
||||
transition.setBounds(view: self.scrollView, bounds: currentBounds)
|
||||
|
||||
self.updateScrollingOffset(transition: transition)
|
||||
self.updateVisibleItems(attemptSynchronousLoads: false, transition: transition, fromScrolling: true)
|
||||
}
|
||||
|
||||
private func updateVisibleItems(attemptSynchronousLoads: Bool, transition: Transition, fromScrolling: Bool) {
|
||||
|
||||
@ -928,7 +928,7 @@ private final class ForumCreateTopicScreenComponent: CombinedComponent {
|
||||
},
|
||||
openSearch: {
|
||||
},
|
||||
addGroupAction: { groupId, isPremiumLocked in
|
||||
addGroupAction: { groupId, isPremiumLocked, _ in
|
||||
guard let collectionId = groupId.base as? ItemCollectionId else {
|
||||
return
|
||||
}
|
||||
|
||||
@ -8662,26 +8662,32 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
|
||||
guard let strongSelf = self else {
|
||||
return
|
||||
}
|
||||
let subjectFlags: TelegramChatBannedRightsFlags
|
||||
let subjectFlags: [TelegramChatBannedRightsFlags]
|
||||
switch subject {
|
||||
case .stickers:
|
||||
subjectFlags = .banSendStickers
|
||||
subjectFlags = [.banSendStickers]
|
||||
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 {
|
||||
bannedPermission = channel.hasBannedPermission(subjectFlags)
|
||||
for subjectFlag in subjectFlags {
|
||||
if let value = channel.hasBannedPermission(subjectFlag) {
|
||||
bannedPermission = value
|
||||
break
|
||||
}
|
||||
}
|
||||
} else if let group = strongSelf.presentationInterfaceState.renderedPeer?.peer as? TelegramGroup {
|
||||
if group.hasBannedPermission(subjectFlags) {
|
||||
for subjectFlag in subjectFlags {
|
||||
if group.hasBannedPermission(subjectFlag) {
|
||||
bannedPermission = (Int32.max, false)
|
||||
} else {
|
||||
bannedPermission = nil
|
||||
break
|
||||
}
|
||||
} else {
|
||||
bannedPermission = nil
|
||||
}
|
||||
}
|
||||
|
||||
var displayToast = false
|
||||
|
||||
if let (untilDate, personal) = bannedPermission {
|
||||
let banDescription: String
|
||||
@ -8700,7 +8706,8 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
|
||||
} else if personal {
|
||||
banDescription = strongSelf.presentationInterfaceState.strings.Conversation_RestrictedMedia
|
||||
} else {
|
||||
banDescription = strongSelf.presentationInterfaceState.strings.Conversation_DefaultRestrictedMedia
|
||||
banDescription = strongSelf.restrictedSendingContentsText()
|
||||
displayToast = true
|
||||
}
|
||||
case .premiumVoiceMessages:
|
||||
banDescription = ""
|
||||
@ -8714,6 +8721,9 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
|
||||
|
||||
switch displayType {
|
||||
case .tooltip:
|
||||
if displayToast {
|
||||
strongSelf.controllerInteraction?.displayUndo(.info(title: nil, text: banDescription))
|
||||
} else {
|
||||
var rect: CGRect?
|
||||
let isStickers: Bool = subject == .stickers
|
||||
switch subject {
|
||||
@ -8746,6 +8756,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
|
||||
return nil
|
||||
}))
|
||||
}
|
||||
}
|
||||
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))
|
||||
}
|
||||
|
||||
@ -930,7 +930,7 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate {
|
||||
return
|
||||
}
|
||||
//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 {
|
||||
strongSelf.ensureFocused()
|
||||
}
|
||||
|
||||
@ -14,6 +14,12 @@ import AnimationCache
|
||||
import MultiAnimationRenderer
|
||||
import TextFormat
|
||||
import ChatControllerInteraction
|
||||
import ContextUI
|
||||
import SwiftSignalKit
|
||||
import PremiumUI
|
||||
import StickerPeekUI
|
||||
import UndoUI
|
||||
import Pasteboard
|
||||
|
||||
private enum EmojisChatInputContextPanelEntryStableId: Hashable, Equatable {
|
||||
case symbol(String)
|
||||
@ -119,6 +125,8 @@ final class EmojisChatInputContextPanelNode: ChatInputContextPanelNode {
|
||||
private let animationCache: AnimationCache
|
||||
private let animationRenderer: MultiAnimationRenderer
|
||||
|
||||
private weak var peekController: PeekController?
|
||||
|
||||
override init(context: AccountContext, theme: PresentationTheme, strings: PresentationStrings, fontSize: PresentationFontSize, chatPresentationContext: ChatPresentationContext) {
|
||||
self.animationCache = chatPresentationContext.animationCache
|
||||
self.animationRenderer = chatPresentationContext.animationRenderer
|
||||
@ -161,6 +169,281 @@ final class EmojisChatInputContextPanelNode: ChatInputContextPanelNode {
|
||||
|
||||
self.addSubnode(self.clippingNode)
|
||||
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)]) {
|
||||
|
||||
@ -17,7 +17,7 @@ final class EmojisChatInputPanelItem: ListViewItem {
|
||||
fileprivate let theme: PresentationTheme
|
||||
fileprivate let symbol: String
|
||||
fileprivate let text: String
|
||||
fileprivate let file: TelegramMediaFile?
|
||||
let file: TelegramMediaFile?
|
||||
fileprivate let animationCache: AnimationCache
|
||||
fileprivate let animationRenderer: MultiAnimationRenderer
|
||||
private let emojiSelected: (String, TelegramMediaFile?) -> Void
|
||||
@ -94,6 +94,8 @@ final class EmojisChatInputPanelItemNode: ListViewItemNode {
|
||||
private let symbolNode: TextNode
|
||||
private var emojiView: EmojiTextAttachmentView?
|
||||
|
||||
var item: EmojisChatInputPanelItem?
|
||||
|
||||
init() {
|
||||
self.symbolNode = TextNode()
|
||||
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
|
||||
if let strongSelf = self {
|
||||
strongSelf.item = item
|
||||
|
||||
let _ = symbolApply()
|
||||
strongSelf.symbolNode.frame = CGRect(origin: CGPoint(x: floorToScreenPixels((EmojisChatInputPanelItemNode.itemSize.width - symbolLayout.size.width) / 2.0), y: 0.0), size: symbolLayout.size)
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user