From dac58e50b32f8b06ad8217f89aef7da0695d0f33 Mon Sep 17 00:00:00 2001 From: Ali <> Date: Thu, 19 Jan 2023 17:50:17 +0400 Subject: [PATCH] UI fixes --- .../Sources/StickerPickerScreen.swift | 6 +- .../Items/ItemListExpandableSwitchItem.swift | 9 +- .../Sources/MediaPickerScreen.swift | 10 +- .../ChannelBannedMemberController.swift | 105 +++++- .../ChannelPermissionsController.swift | 18 +- .../Sources/ReactionContextNode.swift | 247 +++++++----- .../SaveIncomingMediaController.swift | 36 +- submodules/TelegramApi/Sources/Api0.swift | 9 +- submodules/TelegramApi/Sources/Api26.swift | 156 +++----- submodules/TelegramApi/Sources/Api27.swift | 130 +++++-- submodules/TelegramApi/Sources/Api30.swift | 66 +++- submodules/TelegramApi/Sources/Api5.swift | 94 ++--- submodules/TelegramApi/Sources/Api6.swift | 44 +++ .../Sources/Account/Account.swift | 2 + .../TelegramChannelBannedRights.swift | 1 + .../Sources/State/EmojiSearchCategories.swift | 168 +++++++++ .../Sources/State/ManagedRecentStickers.swift | 1 + .../SyncCore/SyncCore_Namespaces.swift | 3 + .../TelegramEngine/Messages/Translate.swift | 5 + .../Stickers/TelegramEngineStickers.swift | 32 ++ .../Sources/AvatarEditorScreen.swift | 355 ++++++++++-------- .../Sources/ChatEntityKeyboardInputNode.swift | 285 ++++++++------ .../EmojiStatusSelectionComponent.swift | 316 +++++++++++----- .../Components/EntityKeyboard/BUILD | 2 + .../Sources/EmojiPagerContentComponent.swift | 305 ++++++++------- .../EmojiSearchSearchBarComponent.swift | 345 ++++++++++++----- .../Sources/GifPagerContentComponent.swift | 4 +- .../Sources/ForumCreateTopicScreen.swift | 2 +- .../Components/LottieComponent/BUILD | 22 ++ .../Sources/LottieComponent.swift | 246 ++++++++++++ .../LottieComponentEmojiContent/BUILD | 22 ++ .../Sources/LottieComponentEmojiContent.swift | 67 ++++ .../TelegramUI/Sources/ChatController.swift | 132 +++++-- .../Sources/ChatControllerNode.swift | 2 +- 34 files changed, 2317 insertions(+), 930 deletions(-) create mode 100644 submodules/TelegramCore/Sources/State/EmojiSearchCategories.swift create mode 100644 submodules/TelegramUI/Components/LottieComponent/BUILD create mode 100644 submodules/TelegramUI/Components/LottieComponent/Sources/LottieComponent.swift create mode 100644 submodules/TelegramUI/Components/LottieComponentEmojiContent/BUILD create mode 100644 submodules/TelegramUI/Components/LottieComponentEmojiContent/Sources/LottieComponentEmojiContent.swift diff --git a/submodules/DrawingUI/Sources/StickerPickerScreen.swift b/submodules/DrawingUI/Sources/StickerPickerScreen.swift index fc76d525f9..f9f0ca4b49 100644 --- a/submodules/DrawingUI/Sources/StickerPickerScreen.swift +++ b/submodules/DrawingUI/Sources/StickerPickerScreen.swift @@ -352,7 +352,7 @@ class StickerPickerScreen: ViewController { }, requestUpdate: { _ in }, - updateSearchQuery: { _, _ in + updateSearchQuery: { _ in }, updateScrollingToItemGroup: { [weak self] in self?.update(isExpanded: true, transition: .animated(duration: 0.4, curve: .spring)) @@ -429,7 +429,7 @@ class StickerPickerScreen: ViewController { }, requestUpdate: { _ in }, - updateSearchQuery: { _, _ in + updateSearchQuery: { _ in }, updateScrollingToItemGroup: { [weak self] in self?.update(isExpanded: true, transition: .animated(duration: 0.4, curve: .spring)) @@ -553,7 +553,7 @@ class StickerPickerScreen: ViewController { }, requestUpdate: { _ in }, - updateSearchQuery: { _, _ in + updateSearchQuery: { _ in }, updateScrollingToItemGroup: { [weak self] in self?.update(isExpanded: true, transition: .animated(duration: 0.4, curve: .spring)) diff --git a/submodules/ItemListUI/Sources/Items/ItemListExpandableSwitchItem.swift b/submodules/ItemListUI/Sources/Items/ItemListExpandableSwitchItem.swift index 5b768d8522..e933571d3c 100644 --- a/submodules/ItemListUI/Sources/Items/ItemListExpandableSwitchItem.swift +++ b/submodules/ItemListUI/Sources/Items/ItemListExpandableSwitchItem.swift @@ -18,15 +18,18 @@ public class ItemListExpandableSwitchItem: ListViewItem, ItemListItem { public var id: AnyHashable public var title: String public var isSelected: Bool + public var isEnabled: Bool public init( id: AnyHashable, title: String, - isSelected: Bool + isSelected: Bool, + isEnabled: Bool ) { self.id = id self.title = title self.isSelected = isSelected + self.isEnabled = isEnabled } } @@ -140,7 +143,7 @@ private final class SubItemNode: HighlightTrackingButtonNode { } @objc private func pressed() { - guard let item = self.item, let action = self.action else { + guard let item = self.item, item.isEnabled, let action = self.action else { return } action(item) @@ -180,6 +183,8 @@ private final class SubItemNode: HighlightTrackingButtonNode { self.textNode.attributedText = NSAttributedString(string: item.title, font: Font.regular(17.0), textColor: presentationData.theme.list.itemPrimaryTextColor) let titleSize = self.textNode.updateLayout(CGSize(width: size.width - leftInset, height: 100.0)) self.textNode.frame = CGRect(origin: CGPoint(x: leftInset, y: floor((size.height - titleSize.height) / 2.0)), size: titleSize) + + self.alpha = item.isEnabled ? 1.0 : 0.5 } } diff --git a/submodules/MediaPickerUI/Sources/MediaPickerScreen.swift b/submodules/MediaPickerUI/Sources/MediaPickerScreen.swift index 944a505151..cd1d08772c 100644 --- a/submodules/MediaPickerUI/Sources/MediaPickerScreen.swift +++ b/submodules/MediaPickerUI/Sources/MediaPickerScreen.swift @@ -1237,9 +1237,13 @@ public final class MediaPickerScreen: ViewController, AttachmentContainable { self.navigationItem.leftBarButtonItem = UIBarButtonItem(backButtonAppearanceWithTitle: self.presentationData.strings.Common_Back, target: self, action: #selector(self.backPressed)) } else { self.navigationItem.leftBarButtonItem = UIBarButtonItem(title: self.presentationData.strings.Common_Cancel, style: .plain, target: self, action: #selector(self.cancelPressed)) - self.navigationItem.rightBarButtonItem = UIBarButtonItem(customDisplayNode: self.moreButtonNode) - self.navigationItem.rightBarButtonItem?.action = #selector(self.rightButtonPressed) - self.navigationItem.rightBarButtonItem?.target = self + + if self.bannedSendPhotos != nil && self.bannedSendVideos != nil { + } else { + self.navigationItem.rightBarButtonItem = UIBarButtonItem(customDisplayNode: self.moreButtonNode) + self.navigationItem.rightBarButtonItem?.action = #selector(self.rightButtonPressed) + self.navigationItem.rightBarButtonItem?.target = self + } } self.moreButtonNode.action = { [weak self] _, gesture in diff --git a/submodules/PeerInfoUI/Sources/ChannelBannedMemberController.swift b/submodules/PeerInfoUI/Sources/ChannelBannedMemberController.swift index 9d7fc65b1b..fdb9c55b32 100644 --- a/submodules/PeerInfoUI/Sources/ChannelBannedMemberController.swift +++ b/submodules/PeerInfoUI/Sources/ChannelBannedMemberController.swift @@ -18,13 +18,15 @@ private final class ChannelBannedMemberControllerArguments { let context: AccountContext let toggleRight: (TelegramChatBannedRightsFlags, Bool) -> Void let toggleRightWhileDisabled: (TelegramChatBannedRightsFlags) -> Void + let toggleIsOptionExpanded: (TelegramChatBannedRightsFlags) -> Void let openTimeout: () -> Void let delete: () -> Void - init(context: AccountContext, toggleRight: @escaping (TelegramChatBannedRightsFlags, Bool) -> Void, toggleRightWhileDisabled: @escaping (TelegramChatBannedRightsFlags) -> Void, openTimeout: @escaping () -> Void, delete: @escaping () -> Void) { + init(context: AccountContext, toggleRight: @escaping (TelegramChatBannedRightsFlags, Bool) -> Void, toggleRightWhileDisabled: @escaping (TelegramChatBannedRightsFlags) -> Void, toggleIsOptionExpanded: @escaping (TelegramChatBannedRightsFlags) -> Void, openTimeout: @escaping () -> Void, delete: @escaping () -> Void) { self.context = context self.toggleRight = toggleRight self.toggleRightWhileDisabled = toggleRightWhileDisabled + self.toggleIsOptionExpanded = toggleIsOptionExpanded self.openTimeout = openTimeout self.delete = delete } @@ -49,7 +51,7 @@ private enum ChannelBannedMemberEntryStableId: Hashable { private enum ChannelBannedMemberEntry: ItemListNodeEntry { case info(PresentationTheme, PresentationStrings, PresentationDateTimeFormat, EnginePeer, EnginePeer.Presence?) case rightsHeader(PresentationTheme, String) - case rightItem(PresentationTheme, Int, String, TelegramChatBannedRightsFlags, Bool, Bool) + case rightItem(PresentationTheme, Int, String, TelegramChatBannedRightsFlags, Bool, Bool, [SubPermission], Bool) case timeout(PresentationTheme, String, String) case exceptionInfo(PresentationTheme, String) case delete(PresentationTheme, String) @@ -73,7 +75,7 @@ private enum ChannelBannedMemberEntry: ItemListNodeEntry { return .info case .rightsHeader: return .rightsHeader - case let .rightItem(_, _, _, right, _, _): + case let .rightItem(_, _, _, right, _, _, _, _): return .right(right) case .timeout: return .timeout @@ -114,8 +116,8 @@ private enum ChannelBannedMemberEntry: ItemListNodeEntry { } else { return false } - case let .rightItem(lhsTheme, lhsIndex, lhsText, lhsRight, lhsValue, lhsEnabled): - if case let .rightItem(rhsTheme, rhsIndex, rhsText, rhsRight, rhsValue, rhsEnabled) = rhs { + case let .rightItem(lhsTheme, lhsIndex, lhsText, lhsRight, lhsValue, lhsEnabled, lhsSubItems, lhsIsExpanded): + if case let .rightItem(rhsTheme, rhsIndex, rhsText, rhsRight, rhsValue, rhsEnabled, rhsSubItems, rhsIsExpanded) = rhs { if lhsTheme !== rhsTheme { return false } @@ -134,6 +136,12 @@ private enum ChannelBannedMemberEntry: ItemListNodeEntry { if lhsEnabled != rhsEnabled { return false } + if lhsSubItems != rhsSubItems { + return false + } + if lhsIsExpanded != rhsIsExpanded { + return false + } return true } else { return false @@ -175,11 +183,11 @@ private enum ChannelBannedMemberEntry: ItemListNodeEntry { default: return true } - case let .rightItem(_, lhsIndex, _, _, _, _): + case let .rightItem(_, lhsIndex, _, _, _, _, _, _): switch rhs { case .info, .rightsHeader: return false - case let .rightItem(_, rhsIndex, _, _, _, _): + case let .rightItem(_, rhsIndex, _, _, _, _, _, _): return lhsIndex < rhsIndex default: return true @@ -212,12 +220,40 @@ private enum ChannelBannedMemberEntry: ItemListNodeEntry { }) case let .rightsHeader(_, text): return ItemListSectionHeaderItem(presentationData: presentationData, text: text, sectionId: self.section) - case let .rightItem(_, _, text, right, value, enabled): - return ItemListSwitchItem(presentationData: presentationData, title: text, value: value, type: .icon, enableInteractiveChanges: enabled, enabled: enabled, sectionId: self.section, style: .blocks, updated: { value in - arguments.toggleRight(right, value) - }, activatedWhileDisabled: { - arguments.toggleRightWhileDisabled(right) - }) + case let .rightItem(_, _, text, right, value, enabled, subPermissions, isExpanded): + if !subPermissions.isEmpty { + return ItemListExpandableSwitchItem(presentationData: presentationData, title: text, value: value, isExpanded: isExpanded, subItems: subPermissions.map { item in + return ItemListExpandableSwitchItem.SubItem( + id: AnyHashable(item.flags.rawValue), + title: item.title, + isSelected: item.isSelected, + isEnabled: item.isEnabled + ) + }, type: .icon, enableInteractiveChanges: enabled, enabled: enabled, sectionId: self.section, style: .blocks, updated: { value in + arguments.toggleRight(right, value) + }, activatedWhileDisabled: { + arguments.toggleRightWhileDisabled(right) + }, selectAction: { + arguments.toggleIsOptionExpanded(right) + }, subAction: { item in + guard let value = item.id.base as? Int32 else { + return + } + let subRights = TelegramChatBannedRightsFlags(rawValue: value) + + if item.isEnabled { + arguments.toggleRight(subRights, !item.isSelected) + } else { + arguments.toggleIsOptionExpanded(subRights) + } + }) + } else { + return ItemListSwitchItem(presentationData: presentationData, title: text, value: value, type: .icon, enableInteractiveChanges: enabled, enabled: enabled, sectionId: self.section, style: .blocks, updated: { value in + arguments.toggleRight(right, value) + }, activatedWhileDisabled: { + arguments.toggleRightWhileDisabled(right) + }) + } case let .timeout(_, text, value): return ItemListDisclosureItem(presentationData: presentationData, title: text, label: value, sectionId: self.section, style: .blocks, action: { arguments.openTimeout() @@ -237,6 +273,7 @@ private struct ChannelBannedMemberControllerState: Equatable { var updatedFlags: TelegramChatBannedRightsFlags? var updatedTimeout: Int32? var updating: Bool = false + var expandedPermissions = Set() } func completeRights(_ flags: TelegramChatBannedRightsFlags) -> TelegramChatBannedRightsFlags { @@ -293,7 +330,21 @@ private func channelBannedMemberControllerEntries(presentationData: Presentation var index = 0 for (right, _) in allGroupPermissionList(peer: .channel(channel)) { let defaultEnabled = !defaultBannedRights.flags.contains(right) && channel.hasPermission(.banMembers) - entries.append(.rightItem(presentationData.theme, index, stringForGroupPermission(strings: presentationData.strings, right: right, isForum: channel.flags.contains(.isForum)), right, defaultEnabled && !currentRightsFlags.contains(right), defaultEnabled && !state.updating)) + + var isSelected = defaultEnabled && !currentRightsFlags.contains(right) + + var subItems: [SubPermission] = [] + if right == .banSendMedia { + isSelected = banSendMediaSubList().allSatisfy({ defaultEnabled && !currentRightsFlags.contains($0.0) }) + + for (subRight, _) in banSendMediaSubList() { + let subItemEnabled = defaultEnabled && !state.updating && !defaultBannedRights.flags.contains(subRight) && channel.hasPermission(.banMembers) + + subItems.append(SubPermission(title: stringForGroupPermission(strings: presentationData.strings, right: subRight, isForum: channel.isForum), flags: subRight, isSelected: defaultEnabled && !currentRightsFlags.contains(subRight), isEnabled: subItemEnabled)) + } + } + + entries.append(.rightItem(presentationData.theme, index, stringForGroupPermission(strings: presentationData.strings, right: right, isForum: channel.flags.contains(.isForum)), right, isSelected, defaultEnabled && !state.updating, subItems, state.expandedPermissions.contains(right))) index += 1 } @@ -339,7 +390,21 @@ private func channelBannedMemberControllerEntries(presentationData: Presentation var index = 0 for (right, _) in allGroupPermissionList(peer: .legacyGroup(group)) { let defaultEnabled = !defaultBannedRightsFlags.contains(right) - entries.append(.rightItem(presentationData.theme, index, stringForGroupPermission(strings: presentationData.strings, right: right, isForum: false), right, defaultEnabled && !currentRightsFlags.contains(right), defaultEnabled && !state.updating)) + + var isSelected = defaultEnabled && !currentRightsFlags.contains(right) + + var subItems: [SubPermission] = [] + if right == .banSendMedia { + isSelected = banSendMediaSubList().allSatisfy({ defaultEnabled && !currentRightsFlags.contains($0.0) }) + + for (subRight, _) in banSendMediaSubList() { + let subItemEnabled = defaultEnabled && !state.updating && !defaultBannedRightsFlags.contains(subRight) + + subItems.append(SubPermission(title: stringForGroupPermission(strings: presentationData.strings, right: subRight, isForum: false), flags: subRight, isSelected: defaultEnabled && !currentRightsFlags.contains(subRight), isEnabled: subItemEnabled)) + } + } + + entries.append(.rightItem(presentationData.theme, index, stringForGroupPermission(strings: presentationData.strings, right: right, isForum: false), right, isSelected, defaultEnabled && !state.updating, subItems, state.expandedPermissions.contains(right))) index += 1 } @@ -439,6 +504,16 @@ public func channelBannedMemberController(context: AccountContext, updatedPresen } presentControllerImpl?(textAlertController(context: context, updatedPresentationData: updatedPresentationData, title: nil, text: text, actions: [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_OK, action: {})]), nil) }) + }, toggleIsOptionExpanded: { flags in + updateState { state in + var state = state + if state.expandedPermissions.contains(flags) { + state.expandedPermissions.remove(flags) + } else { + state.expandedPermissions.insert(flags) + } + return state + } }, openTimeout: { let presentationData = updatedPresentationData?.initial ?? context.sharedContext.currentPresentationData.with { $0 } let actionSheet = ActionSheetController(presentationData: presentationData) diff --git a/submodules/PeerInfoUI/Sources/ChannelPermissionsController.swift b/submodules/PeerInfoUI/Sources/ChannelPermissionsController.swift index afe3d23aa2..96f6177d24 100644 --- a/submodules/PeerInfoUI/Sources/ChannelPermissionsController.swift +++ b/submodules/PeerInfoUI/Sources/ChannelPermissionsController.swift @@ -65,10 +65,11 @@ private enum ChannelPermissionsEntryStableId: Hashable { case peer(PeerId) } -private struct SubPermission: Equatable { +struct SubPermission: Equatable { var title: String var flags: TelegramChatBannedRightsFlags var isSelected: Bool + var isEnabled: Bool } private enum ChannelPermissionsEntry: ItemListNodeEntry { @@ -270,7 +271,8 @@ private enum ChannelPermissionsEntry: ItemListNodeEntry { return ItemListExpandableSwitchItem.SubItem( id: AnyHashable(item.flags.rawValue), title: item.title, - isSelected: item.isSelected + isSelected: item.isSelected, + isEnabled: item.isEnabled ) }, type: .icon, enableInteractiveChanges: enabled != nil, enabled: enabled ?? true, sectionId: self.section, style: .blocks, updated: { value in if let _ = enabled { @@ -533,7 +535,7 @@ private func channelPermissionsControllerEntries(context: AccountContext, presen entries.append(.permissionsHeader(presentationData.theme, presentationData.strings.GroupInfo_Permissions_SectionTitle)) var rightIndex: Int = 0 for (rights, correspondingAdminRight) in allGroupPermissionList(peer: .channel(channel)) { - var enabled: Bool? = true + var enabled = true if channel.addressName != nil && publicGroupRestrictedPermissions.contains(rights) { enabled = false } @@ -552,7 +554,9 @@ private func channelPermissionsControllerEntries(context: AccountContext, presen isSelected = banSendMediaSubList().allSatisfy({ !effectiveRightsFlags.contains($0.0) }) for (subRight, _) in banSendMediaSubList() { - subItems.append(SubPermission(title: stringForGroupPermission(strings: presentationData.strings, right: subRight, isForum: channel.isForum), flags: subRight, isSelected: !effectiveRightsFlags.contains(subRight))) + let subRightEnabled = true + + subItems.append(SubPermission(title: stringForGroupPermission(strings: presentationData.strings, right: subRight, isForum: channel.isForum), flags: subRight, isSelected: !effectiveRightsFlags.contains(subRight), isEnabled: enabled && subRightEnabled)) } } @@ -596,7 +600,9 @@ private func channelPermissionsControllerEntries(context: AccountContext, presen var subItems: [SubPermission] = [] if rights == .banSendMedia { for (subRight, _) in banSendMediaSubList() { - subItems.append(SubPermission(title: stringForGroupPermission(strings: presentationData.strings, right: subRight, isForum: false), flags: subRight, isSelected: !effectiveRightsFlags.contains(subRight))) + let subRightEnabled = true + + subItems.append(SubPermission(title: stringForGroupPermission(strings: presentationData.strings, right: subRight, isForum: false), flags: subRight, isSelected: !effectiveRightsFlags.contains(subRight), isEnabled: subRightEnabled)) } } @@ -793,7 +799,7 @@ public func channelPermissionsController(context: AccountContext, updatedPresent |> take(1) |> deliverOnMainQueue).start(next: { peerId, _ in var dismissController: (() -> Void)? - let controller = ChannelMembersSearchController(context: context, peerId: peerId, mode: .ban, openPeer: { peer, participant in + let controller = ChannelMembersSearchController(context: context, peerId: peerId, mode: .ban, filters: [.disable([context.account.peerId])], openPeer: { peer, participant in if let participant = participant { let presentationData = updatedPresentationData?.initial ?? context.sharedContext.currentPresentationData.with { $0 } switch participant.participant { diff --git a/submodules/ReactionSelectionNode/Sources/ReactionContextNode.swift b/submodules/ReactionSelectionNode/Sources/ReactionContextNode.swift index dca2e17162..e918c15efb 100644 --- a/submodules/ReactionSelectionNode/Sources/ReactionContextNode.swift +++ b/submodules/ReactionSelectionNode/Sources/ReactionContextNode.swift @@ -1335,117 +1335,170 @@ public final class ReactionContextNode: ASDisplayNode, UIScrollViewDelegate { } strongSelf.requestUpdateOverlayWantsToBeBelowKeyboard(transition.containedViewLayoutTransition) }, - updateSearchQuery: { [weak self] rawQuery, languageCode in + updateSearchQuery: { [weak self] query in guard let strongSelf = self else { return } - let query = rawQuery.trimmingCharacters(in: .whitespacesAndNewlines) - - if query.isEmpty { + switch query { + case .none: strongSelf.emojiSearchDisposable.set(nil) strongSelf.emojiSearchResult.set(.single(nil)) - } else { - let context = strongSelf.context + case let .text(rawQuery, languageCode): + let query = rawQuery.trimmingCharacters(in: .whitespacesAndNewlines) - 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 - } + if query.isEmpty { + strongSelf.emojiSearchDisposable.set(nil) + strongSelf.emojiSearchResult.set(.single(nil)) + } else { + let context = strongSelf.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 + } + ) } - - 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 + } + + 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 } } - } - - var items: [EmojiPagerContentComponent.Item] = [] - - var existingIds = Set() - for item in result { - if let itemFile = item.1 { - if existingIds.contains(itemFile.fileId) { + + for entry in view.entries { + guard let item = entry.item as? StickerPackItem else { 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) + 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() + 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 + )] } - - 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 - )] } + + strongSelf.emojiSearchDisposable.set((resultSignal + |> delay(0.15, queue: .mainQueue()) + |> deliverOnMainQueue).start(next: { result in + guard let strongSelf = self else { + return + } + strongSelf.emojiSearchResult.set(.single((result, AnyHashable(query)))) + })) + } + case let .category(value): + let resultSignal = strongSelf.context.engine.stickers.searchEmoji(emojiString: value) + |> mapToSignal { files -> Signal<[EmojiPagerContentComponent.ItemGroup], NoError> in + var items: [EmojiPagerContentComponent.Item] = [] + + var existingIds = Set() + 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 + )]) } strongSelf.emojiSearchDisposable.set((resultSignal @@ -1454,7 +1507,7 @@ public final class ReactionContextNode: ASDisplayNode, UIScrollViewDelegate { guard let strongSelf = self else { return } - strongSelf.emojiSearchResult.set(.single((result, AnyHashable(query)))) + strongSelf.emojiSearchResult.set(.single((result, AnyHashable(value)))) })) } }, diff --git a/submodules/SettingsUI/Sources/Data and Storage/SaveIncomingMediaController.swift b/submodules/SettingsUI/Sources/Data and Storage/SaveIncomingMediaController.swift index db83a1a327..a253a6fcce 100644 --- a/submodules/SettingsUI/Sources/Data and Storage/SaveIncomingMediaController.swift +++ b/submodules/SettingsUI/Sources/Data and Storage/SaveIncomingMediaController.swift @@ -277,7 +277,7 @@ private func saveIncomingMediaControllerEntries(presentationData: PresentationDa entries.append(.videoSizeHeader("MAXIMUM VIDEO SIZE")) entries.append(.videoSize(decimalSeparator: presentationData.dateTimeFormat.decimalSeparator, text: text, value: configuration.maximumVideoSize)) - entries.append(.videoInfo("All downloaded videos in private chats less than 100 MB will be saved to Cameral Roll.")) + entries.append(.videoInfo("All downloaded videos in private chats less than \(sizeText) will be saved to Cameral Roll.")) } if case let .peerType(peerType) = scope { @@ -332,7 +332,7 @@ private func saveIncomingMediaControllerEntries(presentationData: PresentationDa if !label.isEmpty { label.append(", ") } - label.append("Videos up to \(dataSizeString(Int(configuration.maximumVideoSize), formatting: DataSizeStringFormatting(presentationData: presentationData)))") + label.append("Videos up to \(dataSizeString(Int(exceptionConfiguration.maximumVideoSize), formatting: DataSizeStringFormatting(presentationData: presentationData)))") } else { if !label.isEmpty { label.append(", ") @@ -381,6 +381,7 @@ func saveIncomingMediaController(context: AccountContext, scope: SaveIncomingMed } var pushController: ((ViewController) -> Void)? + var presentControllerImpl: ((ViewController) -> Void)? var dismiss: (() -> Void)? let arguments = SaveIncomingMediaControllerArguments( @@ -584,13 +585,27 @@ func saveIncomingMediaController(context: AccountContext, scope: SaveIncomingMed }).start() }, deleteAllExceptions: { - let _ = updateMediaAutoSaveSettingsInteractively(account: context.account, { settings in - var settings = settings - - settings.exceptions.removeAll() - - return settings - }).start() + let presentationData = context.sharedContext.currentPresentationData.with { $0 } + let actionSheet = ActionSheetController(presentationData: presentationData) + actionSheet.setItemGroups([ActionSheetItemGroup(items: [ + //ActionSheetTextItem(title: presentationData.strings.AutoDownloadSettings_ResetHelp), + ActionSheetButtonItem(title: "Delete All Exceptions", color: .destructive, action: { [weak actionSheet] in + actionSheet?.dismissAnimated() + + let _ = updateMediaAutoSaveSettingsInteractively(account: context.account, { settings in + var settings = settings + + settings.exceptions.removeAll() + + return settings + }).start() + }) + ]), ActionSheetItemGroup(items: [ + ActionSheetButtonItem(title: presentationData.strings.Common_Cancel, color: .accent, font: .bold, action: { [weak actionSheet] in + actionSheet?.dismissAnimated() + }) + ])]) + presentControllerImpl?(actionSheet) } ) @@ -723,6 +738,9 @@ func saveIncomingMediaController(context: AccountContext, scope: SaveIncomingMed pushController = { [weak controller] c in controller?.push(c) } + presentControllerImpl = { [weak controller] c in + controller?.present(c, in: .window(.root)) + } dismiss = { [weak controller] in controller?.dismiss() } diff --git a/submodules/TelegramApi/Sources/Api0.swift b/submodules/TelegramApi/Sources/Api0.swift index cee0bc09a6..54c8f2eed5 100644 --- a/submodules/TelegramApi/Sources/Api0.swift +++ b/submodules/TelegramApi/Sources/Api0.swift @@ -207,6 +207,7 @@ fileprivate let parsers: [Int32 : (BufferReader) -> Any?] = { dict[1383932651] = { return Api.EmailVerifyPurpose.parse_emailVerifyPurposeLoginChange($0) } dict[1128644211] = { return Api.EmailVerifyPurpose.parse_emailVerifyPurposeLoginSetup($0) } dict[-1141565819] = { return Api.EmailVerifyPurpose.parse_emailVerifyPurposePassport($0) } + dict[2056961449] = { return Api.EmojiGroup.parse_emojiGroup($0) } dict[-709641735] = { return Api.EmojiKeyword.parse_emojiKeyword($0) } dict[594408994] = { return Api.EmojiKeyword.parse_emojiKeywordDeleted($0) } dict[1556570557] = { return Api.EmojiKeywordsDifference.parse_emojiKeywordsDifference($0) } @@ -1031,6 +1032,8 @@ fileprivate let parsers: [Int32 : (BufferReader) -> Any?] = { dict[-253500010] = { return Api.messages.Dialogs.parse_dialogsNotModified($0) } dict[1910543603] = { return Api.messages.Dialogs.parse_dialogsSlice($0) } dict[-1506535550] = { return Api.messages.DiscussionMessage.parse_discussionMessage($0) } + dict[-2011186869] = { return Api.messages.EmojiGroups.parse_emojiGroups($0) } + dict[1874111879] = { return Api.messages.EmojiGroups.parse_emojiGroupsNotModified($0) } dict[410107472] = { return Api.messages.ExportedChatInvite.parse_exportedChatInvite($0) } dict[572915951] = { return Api.messages.ExportedChatInvite.parse_exportedChatInviteReplaced($0) } dict[-1111085620] = { return Api.messages.ExportedChatInvites.parse_exportedChatInvites($0) } @@ -1074,9 +1077,7 @@ fileprivate let parsers: [Int32 : (BufferReader) -> Any?] = { dict[816245886] = { return Api.messages.Stickers.parse_stickers($0) } dict[-244016606] = { return Api.messages.Stickers.parse_stickersNotModified($0) } dict[-1821037486] = { return Api.messages.TranscribedAudio.parse_transcribedAudio($0) } - dict[1741309751] = { return Api.messages.TranslatedText.parse_translateNoResult($0) } dict[870003448] = { return Api.messages.TranslatedText.parse_translateResult($0) } - dict[-1575684144] = { return Api.messages.TranslatedText.parse_translateResultText($0) } dict[136574537] = { return Api.messages.VotesList.parse_votesList($0) } dict[1042605427] = { return Api.payments.BankCardData.parse_bankCardData($0) } dict[-1362048039] = { return Api.payments.ExportedInvoice.parse_exportedInvoice($0) } @@ -1295,6 +1296,8 @@ public extension Api { _1.serialize(buffer, boxed) case let _1 as Api.EmailVerifyPurpose: _1.serialize(buffer, boxed) + case let _1 as Api.EmojiGroup: + _1.serialize(buffer, boxed) case let _1 as Api.EmojiKeyword: _1.serialize(buffer, boxed) case let _1 as Api.EmojiKeywordsDifference: @@ -1823,6 +1826,8 @@ public extension Api { _1.serialize(buffer, boxed) case let _1 as Api.messages.DiscussionMessage: _1.serialize(buffer, boxed) + case let _1 as Api.messages.EmojiGroups: + _1.serialize(buffer, boxed) case let _1 as Api.messages.ExportedChatInvite: _1.serialize(buffer, boxed) case let _1 as Api.messages.ExportedChatInvites: diff --git a/submodules/TelegramApi/Sources/Api26.swift b/submodules/TelegramApi/Sources/Api26.swift index f7e40833c8..8ad3ca293a 100644 --- a/submodules/TelegramApi/Sources/Api26.swift +++ b/submodules/TelegramApi/Sources/Api26.swift @@ -800,6 +800,64 @@ public extension Api.messages { } } +public extension Api.messages { + enum EmojiGroups: TypeConstructorDescription { + case emojiGroups(hash: Int32, groups: [Api.EmojiGroup]) + case emojiGroupsNotModified + + public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { + switch self { + case .emojiGroups(let hash, let groups): + if boxed { + buffer.appendInt32(-2011186869) + } + serializeInt32(hash, buffer: buffer, boxed: false) + buffer.appendInt32(481674261) + buffer.appendInt32(Int32(groups.count)) + for item in groups { + item.serialize(buffer, true) + } + break + case .emojiGroupsNotModified: + if boxed { + buffer.appendInt32(1874111879) + } + + break + } + } + + public func descriptionFields() -> (String, [(String, Any)]) { + switch self { + case .emojiGroups(let hash, let groups): + return ("emojiGroups", [("hash", hash as Any), ("groups", groups as Any)]) + case .emojiGroupsNotModified: + return ("emojiGroupsNotModified", []) + } + } + + public static func parse_emojiGroups(_ reader: BufferReader) -> EmojiGroups? { + var _1: Int32? + _1 = reader.readInt32() + var _2: [Api.EmojiGroup]? + if let _ = reader.readInt32() { + _2 = Api.parseVector(reader, elementSignature: 0, elementType: Api.EmojiGroup.self) + } + let _c1 = _1 != nil + let _c2 = _2 != nil + if _c1 && _c2 { + return Api.messages.EmojiGroups.emojiGroups(hash: _1!, groups: _2!) + } + else { + return nil + } + } + public static func parse_emojiGroupsNotModified(_ reader: BufferReader) -> EmojiGroups? { + return Api.messages.EmojiGroups.emojiGroupsNotModified + } + + } +} public extension Api.messages { enum ExportedChatInvite: TypeConstructorDescription { case exportedChatInvite(invite: Api.ExportedChatInvite, users: [Api.User]) @@ -1364,101 +1422,3 @@ public extension Api.messages { } } -public extension Api.messages { - enum InactiveChats: TypeConstructorDescription { - case inactiveChats(dates: [Int32], chats: [Api.Chat], users: [Api.User]) - - public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { - switch self { - case .inactiveChats(let dates, let chats, let users): - if boxed { - buffer.appendInt32(-1456996667) - } - buffer.appendInt32(481674261) - buffer.appendInt32(Int32(dates.count)) - for item in dates { - serializeInt32(item, buffer: buffer, boxed: false) - } - buffer.appendInt32(481674261) - buffer.appendInt32(Int32(chats.count)) - for item in chats { - item.serialize(buffer, true) - } - buffer.appendInt32(481674261) - buffer.appendInt32(Int32(users.count)) - for item in users { - item.serialize(buffer, true) - } - break - } - } - - public func descriptionFields() -> (String, [(String, Any)]) { - switch self { - case .inactiveChats(let dates, let chats, let users): - return ("inactiveChats", [("dates", dates as Any), ("chats", chats as Any), ("users", users as Any)]) - } - } - - public static func parse_inactiveChats(_ reader: BufferReader) -> InactiveChats? { - var _1: [Int32]? - if let _ = reader.readInt32() { - _1 = Api.parseVector(reader, elementSignature: -1471112230, elementType: Int32.self) - } - var _2: [Api.Chat]? - if let _ = reader.readInt32() { - _2 = Api.parseVector(reader, elementSignature: 0, elementType: Api.Chat.self) - } - var _3: [Api.User]? - if let _ = reader.readInt32() { - _3 = Api.parseVector(reader, elementSignature: 0, elementType: Api.User.self) - } - let _c1 = _1 != nil - let _c2 = _2 != nil - let _c3 = _3 != nil - if _c1 && _c2 && _c3 { - return Api.messages.InactiveChats.inactiveChats(dates: _1!, chats: _2!, users: _3!) - } - else { - return nil - } - } - - } -} -public extension Api.messages { - enum MessageEditData: TypeConstructorDescription { - case messageEditData(flags: Int32) - - public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { - switch self { - case .messageEditData(let flags): - if boxed { - buffer.appendInt32(649453030) - } - serializeInt32(flags, buffer: buffer, boxed: false) - break - } - } - - public func descriptionFields() -> (String, [(String, Any)]) { - switch self { - case .messageEditData(let flags): - return ("messageEditData", [("flags", flags as Any)]) - } - } - - public static func parse_messageEditData(_ reader: BufferReader) -> MessageEditData? { - var _1: Int32? - _1 = reader.readInt32() - let _c1 = _1 != nil - if _c1 { - return Api.messages.MessageEditData.messageEditData(flags: _1!) - } - else { - return nil - } - } - - } -} diff --git a/submodules/TelegramApi/Sources/Api27.swift b/submodules/TelegramApi/Sources/Api27.swift index ba46e29edf..3713973863 100644 --- a/submodules/TelegramApi/Sources/Api27.swift +++ b/submodules/TelegramApi/Sources/Api27.swift @@ -1,3 +1,101 @@ +public extension Api.messages { + enum InactiveChats: TypeConstructorDescription { + case inactiveChats(dates: [Int32], chats: [Api.Chat], users: [Api.User]) + + public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { + switch self { + case .inactiveChats(let dates, let chats, let users): + if boxed { + buffer.appendInt32(-1456996667) + } + buffer.appendInt32(481674261) + buffer.appendInt32(Int32(dates.count)) + for item in dates { + serializeInt32(item, buffer: buffer, boxed: false) + } + buffer.appendInt32(481674261) + buffer.appendInt32(Int32(chats.count)) + for item in chats { + item.serialize(buffer, true) + } + buffer.appendInt32(481674261) + buffer.appendInt32(Int32(users.count)) + for item in users { + item.serialize(buffer, true) + } + break + } + } + + public func descriptionFields() -> (String, [(String, Any)]) { + switch self { + case .inactiveChats(let dates, let chats, let users): + return ("inactiveChats", [("dates", dates as Any), ("chats", chats as Any), ("users", users as Any)]) + } + } + + public static func parse_inactiveChats(_ reader: BufferReader) -> InactiveChats? { + var _1: [Int32]? + if let _ = reader.readInt32() { + _1 = Api.parseVector(reader, elementSignature: -1471112230, elementType: Int32.self) + } + var _2: [Api.Chat]? + if let _ = reader.readInt32() { + _2 = Api.parseVector(reader, elementSignature: 0, elementType: Api.Chat.self) + } + var _3: [Api.User]? + if let _ = reader.readInt32() { + _3 = Api.parseVector(reader, elementSignature: 0, elementType: Api.User.self) + } + let _c1 = _1 != nil + let _c2 = _2 != nil + let _c3 = _3 != nil + if _c1 && _c2 && _c3 { + return Api.messages.InactiveChats.inactiveChats(dates: _1!, chats: _2!, users: _3!) + } + else { + return nil + } + } + + } +} +public extension Api.messages { + enum MessageEditData: TypeConstructorDescription { + case messageEditData(flags: Int32) + + public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { + switch self { + case .messageEditData(let flags): + if boxed { + buffer.appendInt32(649453030) + } + serializeInt32(flags, buffer: buffer, boxed: false) + break + } + } + + public func descriptionFields() -> (String, [(String, Any)]) { + switch self { + case .messageEditData(let flags): + return ("messageEditData", [("flags", flags as Any)]) + } + } + + public static func parse_messageEditData(_ reader: BufferReader) -> MessageEditData? { + var _1: Int32? + _1 = reader.readInt32() + let _c1 = _1 != nil + if _c1 { + return Api.messages.MessageEditData.messageEditData(flags: _1!) + } + else { + return nil + } + } + + } +} public extension Api.messages { enum MessageReactionsList: TypeConstructorDescription { case messageReactionsList(flags: Int32, count: Int32, reactions: [Api.MessagePeerReaction], chats: [Api.Chat], users: [Api.User], nextOffset: String?) @@ -1246,18 +1344,10 @@ public extension Api.messages { } public extension Api.messages { enum TranslatedText: TypeConstructorDescription { - case translateNoResult case translateResult(result: [Api.TextWithEntities]) - case translateResultText(text: String) public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { switch self { - case .translateNoResult: - if boxed { - buffer.appendInt32(1741309751) - } - - break case .translateResult(let result): if boxed { buffer.appendInt32(870003448) @@ -1268,29 +1358,16 @@ public extension Api.messages { item.serialize(buffer, true) } break - case .translateResultText(let text): - if boxed { - buffer.appendInt32(-1575684144) - } - serializeString(text, buffer: buffer, boxed: false) - break } } public func descriptionFields() -> (String, [(String, Any)]) { switch self { - case .translateNoResult: - return ("translateNoResult", []) case .translateResult(let result): return ("translateResult", [("result", result as Any)]) - case .translateResultText(let text): - return ("translateResultText", [("text", text as Any)]) } } - public static func parse_translateNoResult(_ reader: BufferReader) -> TranslatedText? { - return Api.messages.TranslatedText.translateNoResult - } public static func parse_translateResult(_ reader: BufferReader) -> TranslatedText? { var _1: [Api.TextWithEntities]? if let _ = reader.readInt32() { @@ -1304,17 +1381,6 @@ public extension Api.messages { return nil } } - public static func parse_translateResultText(_ reader: BufferReader) -> TranslatedText? { - var _1: String? - _1 = parseString(reader) - let _c1 = _1 != nil - if _c1 { - return Api.messages.TranslatedText.translateResultText(text: _1!) - } - else { - return nil - } - } } } diff --git a/submodules/TelegramApi/Sources/Api30.swift b/submodules/TelegramApi/Sources/Api30.swift index a34000dc6e..de241641ea 100644 --- a/submodules/TelegramApi/Sources/Api30.swift +++ b/submodules/TelegramApi/Sources/Api30.swift @@ -4564,6 +4564,21 @@ public extension Api.functions.messages { }) } } +public extension Api.functions.messages { + static func getEmojiGroups(hash: Int32) -> (FunctionDescription, Buffer, DeserializeFunctionResponse) { + let buffer = Buffer() + buffer.appendInt32(1955122779) + serializeInt32(hash, buffer: buffer, boxed: false) + return (FunctionDescription(name: "messages.getEmojiGroups", parameters: [("hash", String(describing: hash))]), buffer, DeserializeFunctionResponse { (buffer: Buffer) -> Api.messages.EmojiGroups? in + let reader = BufferReader(buffer) + var result: Api.messages.EmojiGroups? + if let signature = reader.readInt32() { + result = Api.parse(reader, signature: signature) as? Api.messages.EmojiGroups + } + return result + }) + } +} public extension Api.functions.messages { static func getEmojiKeywords(langCode: String) -> (FunctionDescription, Buffer, DeserializeFunctionResponse) { let buffer = Buffer() @@ -4614,6 +4629,21 @@ public extension Api.functions.messages { }) } } +public extension Api.functions.messages { + static func getEmojiStatusGroups(hash: Int32) -> (FunctionDescription, Buffer, DeserializeFunctionResponse) { + let buffer = Buffer() + buffer.appendInt32(785209037) + serializeInt32(hash, buffer: buffer, boxed: false) + return (FunctionDescription(name: "messages.getEmojiStatusGroups", parameters: [("hash", String(describing: hash))]), buffer, DeserializeFunctionResponse { (buffer: Buffer) -> Api.messages.EmojiGroups? in + let reader = BufferReader(buffer) + var result: Api.messages.EmojiGroups? + if let signature = reader.readInt32() { + result = Api.parse(reader, signature: signature) as? Api.messages.EmojiGroups + } + return result + }) + } +} public extension Api.functions.messages { static func getEmojiStickers(hash: Int64) -> (FunctionDescription, Buffer, DeserializeFunctionResponse) { let buffer = Buffer() @@ -6027,6 +6057,22 @@ public extension Api.functions.messages { }) } } +public extension Api.functions.messages { + static func searchCustomEmoji(emoticon: String, hash: Int64) -> (FunctionDescription, Buffer, DeserializeFunctionResponse) { + let buffer = Buffer() + buffer.appendInt32(739360983) + serializeString(emoticon, buffer: buffer, boxed: false) + serializeInt64(hash, buffer: buffer, boxed: false) + return (FunctionDescription(name: "messages.searchCustomEmoji", parameters: [("emoticon", String(describing: emoticon)), ("hash", String(describing: hash))]), buffer, DeserializeFunctionResponse { (buffer: Buffer) -> Api.EmojiList? in + let reader = BufferReader(buffer) + var result: Api.EmojiList? + if let signature = reader.readInt32() { + result = Api.parse(reader, signature: signature) as? Api.EmojiList + } + return result + }) + } +} public extension Api.functions.messages { static func searchGlobal(flags: Int32, folderId: Int32?, q: String, filter: Api.MessagesFilter, minDate: Int32, maxDate: Int32, offsetRate: Int32, offsetPeer: Api.InputPeer, offsetId: Int32, limit: Int32) -> (FunctionDescription, Buffer, DeserializeFunctionResponse) { let buffer = Buffer() @@ -6690,6 +6736,22 @@ public extension Api.functions.messages { }) } } +public extension Api.functions.messages { + static func togglePeerTranslations(flags: Int32, peer: Api.InputPeer) -> (FunctionDescription, Buffer, DeserializeFunctionResponse) { + let buffer = Buffer() + buffer.appendInt32(-461589127) + serializeInt32(flags, buffer: buffer, boxed: false) + peer.serialize(buffer, true) + return (FunctionDescription(name: "messages.togglePeerTranslations", parameters: [("flags", String(describing: flags)), ("peer", String(describing: peer))]), buffer, DeserializeFunctionResponse { (buffer: Buffer) -> Api.Bool? in + let reader = BufferReader(buffer) + var result: Api.Bool? + if let signature = reader.readInt32() { + result = Api.parse(reader, signature: signature) as? Api.Bool + } + return result + }) + } +} public extension Api.functions.messages { static func toggleStickerSets(flags: Int32, stickersets: [Api.InputStickerSet]) -> (FunctionDescription, Buffer, DeserializeFunctionResponse) { let buffer = Buffer() @@ -7676,12 +7738,12 @@ public extension Api.functions.photos { public extension Api.functions.photos { static func uploadProfilePhoto(flags: Int32, file: Api.InputFile?, video: Api.InputFile?, videoStartTs: Double?, videoEmojiMarkup: Api.VideoSize?) -> (FunctionDescription, Buffer, DeserializeFunctionResponse) { let buffer = Buffer() - buffer.appendInt32(-771759753) + buffer.appendInt32(154966609) serializeInt32(flags, buffer: buffer, boxed: false) if Int(flags) & Int(1 << 0) != 0 {file!.serialize(buffer, true)} if Int(flags) & Int(1 << 1) != 0 {video!.serialize(buffer, true)} if Int(flags) & Int(1 << 2) != 0 {serializeDouble(videoStartTs!, buffer: buffer, boxed: false)} - if Int(flags) & Int(1 << 3) != 0 {videoEmojiMarkup!.serialize(buffer, true)} + if Int(flags) & Int(1 << 4) != 0 {videoEmojiMarkup!.serialize(buffer, true)} return (FunctionDescription(name: "photos.uploadProfilePhoto", parameters: [("flags", String(describing: flags)), ("file", String(describing: file)), ("video", String(describing: video)), ("videoStartTs", String(describing: videoStartTs)), ("videoEmojiMarkup", String(describing: videoEmojiMarkup))]), buffer, DeserializeFunctionResponse { (buffer: Buffer) -> Api.photos.Photo? in let reader = BufferReader(buffer) var result: Api.photos.Photo? diff --git a/submodules/TelegramApi/Sources/Api5.swift b/submodules/TelegramApi/Sources/Api5.swift index b3effb2ab1..1d327a9163 100644 --- a/submodules/TelegramApi/Sources/Api5.swift +++ b/submodules/TelegramApi/Sources/Api5.swift @@ -220,6 +220,56 @@ public extension Api { } } +public extension Api { + enum EmojiGroup: TypeConstructorDescription { + case emojiGroup(title: String, iconEmojiId: Int64, emoticons: [String]) + + public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { + switch self { + case .emojiGroup(let title, let iconEmojiId, let emoticons): + if boxed { + buffer.appendInt32(2056961449) + } + serializeString(title, buffer: buffer, boxed: false) + serializeInt64(iconEmojiId, buffer: buffer, boxed: false) + buffer.appendInt32(481674261) + buffer.appendInt32(Int32(emoticons.count)) + for item in emoticons { + serializeString(item, buffer: buffer, boxed: false) + } + break + } + } + + public func descriptionFields() -> (String, [(String, Any)]) { + switch self { + case .emojiGroup(let title, let iconEmojiId, let emoticons): + return ("emojiGroup", [("title", title as Any), ("iconEmojiId", iconEmojiId as Any), ("emoticons", emoticons as Any)]) + } + } + + public static func parse_emojiGroup(_ reader: BufferReader) -> EmojiGroup? { + var _1: String? + _1 = parseString(reader) + var _2: Int64? + _2 = reader.readInt64() + var _3: [String]? + if let _ = reader.readInt32() { + _3 = Api.parseVector(reader, elementSignature: -1255641564, elementType: String.self) + } + let _c1 = _1 != nil + let _c2 = _2 != nil + let _c3 = _3 != nil + if _c1 && _c2 && _c3 { + return Api.EmojiGroup.emojiGroup(title: _1!, iconEmojiId: _2!, emoticons: _3!) + } + else { + return nil + } + } + + } +} public extension Api { enum EmojiKeyword: TypeConstructorDescription { case emojiKeyword(keyword: String, emoticons: [String]) @@ -1054,47 +1104,3 @@ public extension Api { } } -public extension Api { - enum FileHash: TypeConstructorDescription { - case fileHash(offset: Int64, limit: Int32, hash: Buffer) - - public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { - switch self { - case .fileHash(let offset, let limit, let hash): - if boxed { - buffer.appendInt32(-207944868) - } - serializeInt64(offset, buffer: buffer, boxed: false) - serializeInt32(limit, buffer: buffer, boxed: false) - serializeBytes(hash, buffer: buffer, boxed: false) - break - } - } - - public func descriptionFields() -> (String, [(String, Any)]) { - switch self { - case .fileHash(let offset, let limit, let hash): - return ("fileHash", [("offset", offset as Any), ("limit", limit as Any), ("hash", hash as Any)]) - } - } - - public static func parse_fileHash(_ reader: BufferReader) -> FileHash? { - var _1: Int64? - _1 = reader.readInt64() - var _2: Int32? - _2 = reader.readInt32() - var _3: Buffer? - _3 = parseBytes(reader) - let _c1 = _1 != nil - let _c2 = _2 != nil - let _c3 = _3 != nil - if _c1 && _c2 && _c3 { - return Api.FileHash.fileHash(offset: _1!, limit: _2!, hash: _3!) - } - else { - return nil - } - } - - } -} diff --git a/submodules/TelegramApi/Sources/Api6.swift b/submodules/TelegramApi/Sources/Api6.swift index 69adbb6013..226f3df1a4 100644 --- a/submodules/TelegramApi/Sources/Api6.swift +++ b/submodules/TelegramApi/Sources/Api6.swift @@ -1,3 +1,47 @@ +public extension Api { + enum FileHash: TypeConstructorDescription { + case fileHash(offset: Int64, limit: Int32, hash: Buffer) + + public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { + switch self { + case .fileHash(let offset, let limit, let hash): + if boxed { + buffer.appendInt32(-207944868) + } + serializeInt64(offset, buffer: buffer, boxed: false) + serializeInt32(limit, buffer: buffer, boxed: false) + serializeBytes(hash, buffer: buffer, boxed: false) + break + } + } + + public func descriptionFields() -> (String, [(String, Any)]) { + switch self { + case .fileHash(let offset, let limit, let hash): + return ("fileHash", [("offset", offset as Any), ("limit", limit as Any), ("hash", hash as Any)]) + } + } + + public static func parse_fileHash(_ reader: BufferReader) -> FileHash? { + var _1: Int64? + _1 = reader.readInt64() + var _2: Int32? + _2 = reader.readInt32() + var _3: Buffer? + _3 = parseBytes(reader) + let _c1 = _1 != nil + let _c2 = _2 != nil + let _c3 = _3 != nil + if _c1 && _c2 && _c3 { + return Api.FileHash.fileHash(offset: _1!, limit: _2!, hash: _3!) + } + else { + return nil + } + } + + } +} public extension Api { enum Folder: TypeConstructorDescription { case folder(flags: Int32, id: Int32, title: String, photo: Api.ChatPhoto?) diff --git a/submodules/TelegramCore/Sources/Account/Account.swift b/submodules/TelegramCore/Sources/Account/Account.swift index bdc35ed874..d50de0ed3a 100644 --- a/submodules/TelegramCore/Sources/Account/Account.swift +++ b/submodules/TelegramCore/Sources/Account/Account.swift @@ -1111,6 +1111,8 @@ public class Account { self.managedOperationsDisposable.add(managedSynchronizeEmojiKeywordsOperations(postbox: self.postbox, network: self.network).start()) self.managedOperationsDisposable.add(managedApplyPendingScheduledMessagesActions(postbox: self.postbox, network: self.network, stateManager: self.stateManager).start()) self.managedOperationsDisposable.add(managedSynchronizeAvailableReactions(postbox: self.postbox, network: self.network).start()) + self.managedOperationsDisposable.add(managedSynchronizeEmojiSearchCategories(postbox: self.postbox, network: self.network, kind: .emoji).start()) + self.managedOperationsDisposable.add(managedSynchronizeEmojiSearchCategories(postbox: self.postbox, network: self.network, kind: .status).start()) self.managedOperationsDisposable.add(managedSynchronizeAttachMenuBots(postbox: self.postbox, network: self.network, force: true).start()) self.managedOperationsDisposable.add(managedSynchronizeNotificationSoundList(postbox: self.postbox, network: self.network).start()) diff --git a/submodules/TelegramCore/Sources/ApiUtils/TelegramChannelBannedRights.swift b/submodules/TelegramCore/Sources/ApiUtils/TelegramChannelBannedRights.swift index ec5aafc608..8e2a362c9b 100644 --- a/submodules/TelegramCore/Sources/ApiUtils/TelegramChannelBannedRights.swift +++ b/submodules/TelegramCore/Sources/ApiUtils/TelegramChannelBannedRights.swift @@ -8,6 +8,7 @@ extension TelegramChatBannedRights { case let .chatBannedRights(flags, untilDate): var effectiveFlags = TelegramChatBannedRightsFlags(rawValue: flags) effectiveFlags.remove(.banSendMedia) + effectiveFlags.remove(TelegramChatBannedRightsFlags(rawValue: 1 << 1)) self.init(flags: effectiveFlags, untilDate: untilDate) } } diff --git a/submodules/TelegramCore/Sources/State/EmojiSearchCategories.swift b/submodules/TelegramCore/Sources/State/EmojiSearchCategories.swift new file mode 100644 index 0000000000..06a108d61b --- /dev/null +++ b/submodules/TelegramCore/Sources/State/EmojiSearchCategories.swift @@ -0,0 +1,168 @@ +import Foundation +import TelegramApi +import Postbox +import SwiftSignalKit + +public final class EmojiSearchCategories: Equatable, Codable { + public enum Kind: Int64 { + case emoji = 0 + case status = 1 + } + + public struct Group: Codable, Equatable { + enum CodingKeys: String, CodingKey { + case id + case title + case identifiers + } + + public var id: Int64 + public var title: String + public var identifiers: [String] + + public init(id: Int64, title: String, identifiers: [String]) { + self.id = id + self.title = title + self.identifiers = identifiers + } + + public init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + + self.id = try container.decode(Int64.self, forKey: .id) + self.title = try container.decode(String.self, forKey: .title) + self.identifiers = try container.decode([String].self, forKey: .identifiers) + } + } + + private enum CodingKeys: String, CodingKey { + case newHash + case groups + } + + public let hash: Int32 + public let groups: [Group] + + public init( + hash: Int32, + groups: [Group] + ) { + self.hash = hash + self.groups = groups + } + + public static func ==(lhs: EmojiSearchCategories, rhs: EmojiSearchCategories) -> Bool { + if lhs === rhs { + return true + } + if lhs.hash != rhs.hash { + return false + } + if lhs.groups != rhs.groups { + return false + } + return true + } + + public init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + + self.hash = try container.decodeIfPresent(Int32.self, forKey: .newHash) ?? 0 + self.groups = try container.decode([Group].self, forKey: .groups) + } + + public func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + + try container.encode(self.hash, forKey: .newHash) + try container.encode(self.groups, forKey: .groups) + } +} + +func _internal_cachedEmojiSearchCategories(postbox: Postbox, kind: EmojiSearchCategories.Kind) -> Signal { + return postbox.transaction { transaction -> EmojiSearchCategories? in + return _internal_cachedEmojiSearchCategories(transaction: transaction, kind: kind) + } +} + +func _internal_cachedEmojiSearchCategories(transaction: Transaction, kind: EmojiSearchCategories.Kind) -> EmojiSearchCategories? { + let key = ValueBoxKey(length: 8) + key.setInt64(0, value: kind.rawValue) + + let cached = transaction.retrieveItemCacheEntry(id: ItemCacheEntryId(collectionId: Namespaces.CachedItemCollection.emojiSearchCategories, key: key))?.get(EmojiSearchCategories.self) + if let cached = cached { + return cached + } else { + return nil + } +} + +func _internal_setCachedEmojiSearchCategories(transaction: Transaction, categories: EmojiSearchCategories, kind: EmojiSearchCategories.Kind) { + let key = ValueBoxKey(length: 8) + key.setInt64(0, value: kind.rawValue) + + if let entry = CodableEntry(categories) { + transaction.putItemCacheEntry(id: ItemCacheEntryId(collectionId: Namespaces.CachedItemCollection.emojiSearchCategories, key: key), entry: entry) + } +} + +func managedSynchronizeEmojiSearchCategories(postbox: Postbox, network: Network, kind: EmojiSearchCategories.Kind) -> Signal { + let poll = Signal { subscriber in + let signal: Signal = _internal_cachedEmojiSearchCategories(postbox: postbox, kind: kind) + |> mapToSignal { current in + let signal: Signal + switch kind { + case .emoji: + signal = network.request(Api.functions.messages.getEmojiGroups(hash: current?.hash ?? 0)) + |> `catch` { _ -> Signal in + return .single(.emojiGroupsNotModified) + } + case .status: + signal = network.request(Api.functions.messages.getEmojiStatusGroups(hash: current?.hash ?? 0)) + |> `catch` { _ -> Signal in + return .single(.emojiGroupsNotModified) + } + } + + return signal + |> mapToSignal { result -> Signal in + return postbox.transaction { transaction -> Signal in + switch result { + case let .emojiGroups(hash, groups): + let categories = EmojiSearchCategories( + hash: hash, + groups: groups.map { item -> EmojiSearchCategories.Group in + switch item { + case let .emojiGroup(title, iconEmojiId, emoticons): + return EmojiSearchCategories.Group( + id: iconEmojiId, + title: title, identifiers: emoticons + ) + } + } + ) + _internal_setCachedEmojiSearchCategories(transaction: transaction, categories: categories, kind: kind) + case .emojiGroupsNotModified: + break + } + + return .complete() + } + |> switchToLatest + } + } + + return signal.start(completed: { + subscriber.putCompletion() + }) + } + + return ( + poll + |> then( + .complete() + |> suspendAwareDelay(1.0 * 60.0 * 60.0, queue: Queue.concurrentDefaultQueue()) + ) + ) + |> restart +} diff --git a/submodules/TelegramCore/Sources/State/ManagedRecentStickers.swift b/submodules/TelegramCore/Sources/State/ManagedRecentStickers.swift index d6a2c1bce7..7681b4461c 100644 --- a/submodules/TelegramCore/Sources/State/ManagedRecentStickers.swift +++ b/submodules/TelegramCore/Sources/State/ManagedRecentStickers.swift @@ -2,6 +2,7 @@ import Foundation import Postbox import TelegramApi import SwiftSignalKit +import MtProtoKit private func hashForIds(_ ids: [Int64]) -> Int64 { var acc: UInt64 = 0 diff --git a/submodules/TelegramCore/Sources/SyncCore/SyncCore_Namespaces.swift b/submodules/TelegramCore/Sources/SyncCore/SyncCore_Namespaces.swift index 95d0072217..3e9ac4dad7 100644 --- a/submodules/TelegramCore/Sources/SyncCore/SyncCore_Namespaces.swift +++ b/submodules/TelegramCore/Sources/SyncCore/SyncCore_Namespaces.swift @@ -75,6 +75,8 @@ public struct Namespaces { public static let CloudFeaturedStatusEmoji: Int32 = 18 public static let CloudRecentReactions: Int32 = 19 public static let CloudTopReactions: Int32 = 20 + public static let CloudEmojiCategories: Int32 = 21 + public static let CloudEmojiStatusCategories: Int32 = 22 } public struct CachedItemCollection { @@ -100,6 +102,7 @@ public struct Namespaces { public static let notificationSoundList: Int8 = 22 public static let attachMenuBots: Int8 = 23 public static let featuredStickersConfiguration: Int8 = 24 + public static let emojiSearchCategories: Int8 = 25 } public struct UnorderedItemList { diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Messages/Translate.swift b/submodules/TelegramCore/Sources/TelegramEngine/Messages/Translate.swift index f5c1e4f113..8388bd2e9f 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Messages/Translate.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Messages/Translate.swift @@ -5,6 +5,10 @@ import TelegramApi import MtProtoKit func _internal_translate(network: Network, text: String, fromLang: String?, toLang: String) -> Signal { + #if DEBUG + return .single(nil) + #else + var flags: Int32 = 0 flags |= (1 << 1) @@ -30,6 +34,7 @@ func _internal_translate(network: Network, text: String, fromLang: String?, toLa } } } + #endif } func _internal_translateMessages(postbox: Postbox, network: Network, messageIds: [EngineMessage.Id], toLang: String) -> Signal { diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Stickers/TelegramEngineStickers.swift b/submodules/TelegramCore/Sources/TelegramEngine/Stickers/TelegramEngineStickers.swift index f596f4015c..52aa9951ed 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Stickers/TelegramEngineStickers.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Stickers/TelegramEngineStickers.swift @@ -105,6 +105,10 @@ public extension TelegramEngine { return _internal_cachedAvailableReactions(postbox: self.account.postbox) } + public func emojiSearchCategories(kind: EmojiSearchCategories.Kind) -> Signal { + return _internal_cachedEmojiSearchCategories(postbox: self.account.postbox, kind: kind) + } + public func updateQuickReaction(reaction: MessageReaction.Reaction) -> Signal { let _ = updateReactionSettingsInteractively(postbox: self.account.postbox, { settings in var settings = settings @@ -179,6 +183,34 @@ public extension TelegramEngine { public func resolveInlineStickers(fileIds: [Int64]) -> Signal<[Int64: TelegramMediaFile], NoError> { return _internal_resolveInlineStickers(postbox: self.account.postbox, network: self.account.network, fileIds: fileIds) } + + public func searchEmoji(emojiString: String) -> Signal<[TelegramMediaFile], NoError> { + return self.account.network.request(Api.functions.messages.searchCustomEmoji(emoticon: emojiString, hash: 0)) + |> map(Optional.init) + |> `catch` { _ -> Signal in + return .single(nil) + } + |> mapToSignal { result -> Signal<[TelegramMediaFile], NoError> in + guard let result = result else { + return .single([]) + } + switch result { + case let .emojiList(_, documentIds): + return self.resolveInlineStickers(fileIds: documentIds) + |> map { result -> [TelegramMediaFile] in + var files: [TelegramMediaFile] = [] + for id in documentIds { + if let file = result[id] { + files.append(file) + } + } + return files + } + default: + return .single([]) + } + } + } } } diff --git a/submodules/TelegramUI/Components/AvatarEditorScreen/Sources/AvatarEditorScreen.swift b/submodules/TelegramUI/Components/AvatarEditorScreen/Sources/AvatarEditorScreen.swift index 3c1876b3be..516f7d1fc7 100644 --- a/submodules/TelegramUI/Components/AvatarEditorScreen/Sources/AvatarEditorScreen.swift +++ b/submodules/TelegramUI/Components/AvatarEditorScreen/Sources/AvatarEditorScreen.swift @@ -232,166 +232,219 @@ final class AvatarEditorScreenComponent: Component { self.state?.selectedFile = data.emoji.panelItemGroups.first?.items.first?.itemFile self.state?.updated(transition: .immediate) - let updateSearchQuery: (String, String) -> Void = { [weak self] rawQuery, languageCode in + let updateSearchQuery: (EmojiPagerContentComponent.SearchQuery?) -> Void = { [weak self] query in guard let strongSelf = self, let context = strongSelf.state?.context else { return } - let query = rawQuery.trimmingCharacters(in: .whitespacesAndNewlines) - - if query.isEmpty { + switch query { + case .none: strongSelf.emojiSearchDisposable.set(nil) strongSelf.emojiSearchResult.set(.single(nil)) - } else { - 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 resultSignal = signal - |> mapToSignal { keywords -> Signal<[EmojiPagerContentComponent.ItemGroup], NoError> in - return combineLatest( - context.account.postbox.itemCollectionsView(orderedItemListCollectionIds: [], namespaces: [Namespaces.ItemCollection.CloudEmojiPacks], aroundIndex: nil, count: 10000000) |> take(1), - combineLatest(keywords.map { context.engine.stickers.searchStickers(query: $0.emoticons.first!) }) - ) - |> map { view, stickers -> [EmojiPagerContentComponent.ItemGroup] in - let hasPremium = true - - var emojis: [(String, TelegramMediaFile?, String)] = [] - - var existingEmoticons = Set() - var allEmoticons: [String: String] = [:] - for keyword in keywords { - for emoticon in keyword.emoticons { - allEmoticons[emoticon] = keyword.keyword - - if !existingEmoticons.contains(emoticon) { - existingEmoticons.insert(emoticon) - } - } - } - - 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] { - emojis.append((alt, item.file, keyword)) - } else if alt == query { - emojis.append((alt, item.file, alt)) - } - } - default: - break - } - } - } - - var emojiItems: [EmojiPagerContentComponent.Item] = [] - var existingIds = Set() - for item in emojis { - 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 - ) - emojiItems.append(item) - } - } - - var stickerItems: [EmojiPagerContentComponent.Item] = [] - for stickerResult in stickers { - for sticker in stickerResult { - if existingIds.contains(sticker.file.fileId) { - continue - } - - existingIds.insert(sticker.file.fileId) - let animationData = EntityKeyboardAnimationData(file: sticker.file) - let item = EmojiPagerContentComponent.Item( - animationData: animationData, - content: .animation(animationData), - itemFile: sticker.file, - subgroupId: nil, - icon: .none, - tintMode: .none - ) - stickerItems.append(item) - } - } - - var result: [EmojiPagerContentComponent.ItemGroup] = [] - if !emojiItems.isEmpty { - result.append( - EmojiPagerContentComponent.ItemGroup( - supergroupId: "search", - groupId: "emoji", - title: "Emoji", - subtitle: nil, - actionButtonTitle: nil, - isFeatured: false, - isPremiumLocked: false, - isEmbedded: false, - hasClear: false, - collapsedLineCount: nil, - displayPremiumBadges: false, - headerItem: nil, - items: emojiItems - ) - ) - } - if !stickerItems.isEmpty { - result.append( - EmojiPagerContentComponent.ItemGroup( - supergroupId: "search", - groupId: "stickers", - title: "Stickers", - subtitle: nil, - actionButtonTitle: nil, - isFeatured: false, - isPremiumLocked: false, - isEmbedded: false, - hasClear: false, - collapsedLineCount: nil, - displayPremiumBadges: false, - headerItem: nil, - items: stickerItems - ) - ) - } - return result - } - } + case let .text(rawQuery, languageCode): + let query = rawQuery.trimmingCharacters(in: .whitespacesAndNewlines) + if query.isEmpty { + strongSelf.emojiSearchDisposable.set(nil) + strongSelf.emojiSearchResult.set(.single(nil)) + } else { + 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 resultSignal = signal + |> mapToSignal { keywords -> Signal<[EmojiPagerContentComponent.ItemGroup], NoError> in + return combineLatest( + context.account.postbox.itemCollectionsView(orderedItemListCollectionIds: [], namespaces: [Namespaces.ItemCollection.CloudEmojiPacks], aroundIndex: nil, count: 10000000) |> take(1), + combineLatest(keywords.map { context.engine.stickers.searchStickers(query: $0.emoticons.first!) }) + ) + |> map { view, stickers -> [EmojiPagerContentComponent.ItemGroup] in + let hasPremium = true + + var emojis: [(String, TelegramMediaFile?, String)] = [] + + var existingEmoticons = Set() + var allEmoticons: [String: String] = [:] + for keyword in keywords { + for emoticon in keyword.emoticons { + allEmoticons[emoticon] = keyword.keyword + + if !existingEmoticons.contains(emoticon) { + existingEmoticons.insert(emoticon) + } + } + } + + 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] { + emojis.append((alt, item.file, keyword)) + } else if alt == query { + emojis.append((alt, item.file, alt)) + } + } + default: + break + } + } + } + + var emojiItems: [EmojiPagerContentComponent.Item] = [] + var existingIds = Set() + for item in emojis { + 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 + ) + emojiItems.append(item) + } + } + + var stickerItems: [EmojiPagerContentComponent.Item] = [] + for stickerResult in stickers { + for sticker in stickerResult { + if existingIds.contains(sticker.file.fileId) { + continue + } + + existingIds.insert(sticker.file.fileId) + let animationData = EntityKeyboardAnimationData(file: sticker.file) + let item = EmojiPagerContentComponent.Item( + animationData: animationData, + content: .animation(animationData), + itemFile: sticker.file, + subgroupId: nil, + icon: .none, + tintMode: .none + ) + stickerItems.append(item) + } + } + + var result: [EmojiPagerContentComponent.ItemGroup] = [] + if !emojiItems.isEmpty { + result.append( + EmojiPagerContentComponent.ItemGroup( + supergroupId: "search", + groupId: "emoji", + title: "Emoji", + subtitle: nil, + actionButtonTitle: nil, + isFeatured: false, + isPremiumLocked: false, + isEmbedded: false, + hasClear: false, + collapsedLineCount: nil, + displayPremiumBadges: false, + headerItem: nil, + items: emojiItems + ) + ) + } + if !stickerItems.isEmpty { + result.append( + EmojiPagerContentComponent.ItemGroup( + supergroupId: "search", + groupId: "stickers", + title: "Stickers", + subtitle: nil, + actionButtonTitle: nil, + isFeatured: false, + isPremiumLocked: false, + isEmbedded: false, + hasClear: false, + collapsedLineCount: nil, + displayPremiumBadges: false, + headerItem: nil, + items: stickerItems + ) + ) + } + return result + } + } + + strongSelf.emojiSearchDisposable.set((resultSignal + |> delay(0.15, queue: .mainQueue()) + |> deliverOnMainQueue).start(next: { [weak self] result in + guard let strongSelf = self else { + return + } + strongSelf.emojiSearchResult.set(.single((result, AnyHashable(query)))) + })) + } + case let .category(value): + let resultSignal = context.engine.stickers.searchEmoji(emojiString: value) + |> mapToSignal { files -> Signal<[EmojiPagerContentComponent.ItemGroup], NoError> in + var items: [EmojiPagerContentComponent.Item] = [] + + var existingIds = Set() + 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 + )]) + } + strongSelf.emojiSearchDisposable.set((resultSignal |> delay(0.15, queue: .mainQueue()) |> deliverOnMainQueue).start(next: { [weak self] result in guard let strongSelf = self else { return } - strongSelf.emojiSearchResult.set(.single((result, AnyHashable(query)))) + strongSelf.emojiSearchResult.set(.single((result, AnyHashable(value)))) })) } } @@ -491,8 +544,8 @@ final class AvatarEditorScreenComponent: Component { strongSelf.state?.updated(transition: transition) } }, - updateSearchQuery: { rawQuery, languageCode in - updateSearchQuery(rawQuery, languageCode) + updateSearchQuery: { query in + updateSearchQuery(query) }, updateScrollingToItemGroup: { }, @@ -621,8 +674,8 @@ final class AvatarEditorScreenComponent: Component { strongSelf.state?.updated(transition: transition) } }, - updateSearchQuery: { rawQuery, languageCode in - updateSearchQuery(rawQuery, languageCode) + updateSearchQuery: { query in + updateSearchQuery(query) }, updateScrollingToItemGroup: { }, diff --git a/submodules/TelegramUI/Components/ChatEntityKeyboardInputNode/Sources/ChatEntityKeyboardInputNode.swift b/submodules/TelegramUI/Components/ChatEntityKeyboardInputNode/Sources/ChatEntityKeyboardInputNode.swift index d78a8ffeb4..faf7f4f604 100644 --- a/submodules/TelegramUI/Components/ChatEntityKeyboardInputNode/Sources/ChatEntityKeyboardInputNode.swift +++ b/submodules/TelegramUI/Components/ChatEntityKeyboardInputNode/Sources/ChatEntityKeyboardInputNode.swift @@ -816,145 +816,198 @@ public final class ChatEntityKeyboardInputNode: ChatInputNode { strongSelf.interfaceInteraction?.requestLayout(transition.containedViewLayoutTransition) } }, - updateSearchQuery: { [weak self] rawQuery, languageCode in + updateSearchQuery: { [weak self] query in guard let strongSelf = self else { return } - let query = rawQuery.trimmingCharacters(in: .whitespacesAndNewlines) - - if query.isEmpty { + switch query { + case .none: strongSelf.emojiSearchDisposable.set(nil) strongSelf.emojiSearchResult.set(.single(nil)) - } else { - let context = strongSelf.context + case let .text(rawQuery, languageCode): + let query = rawQuery.trimmingCharacters(in: .whitespacesAndNewlines) - 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 - } + if query.isEmpty { + strongSelf.emojiSearchDisposable.set(nil) + strongSelf.emojiSearchResult.set(.single(nil)) + } else { + let context = strongSelf.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 ) - } - } - - 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 existingEmoticons = Set() - var allEmoticonsList: [String] = [] - var allEmoticons: [String: String] = [:] - for keyword in keywords { - for emoticon in keyword.emoticons { - allEmoticons[emoticon] = keyword.keyword - - if !existingEmoticons.contains(emoticon) { - allEmoticonsList.append(emoticon) - existingEmoticons.insert(emoticon) - } - } - } - - 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)) - } + |> take(1) + |> map { view, availableReactions, hasPremium -> [EmojiPagerContentComponent.ItemGroup] in + var result: [(String, TelegramMediaFile?, String)] = [] + + var existingEmoticons = Set() + var allEmoticonsList: [String] = [] + var allEmoticons: [String: String] = [:] + for keyword in keywords { + for emoticon in keyword.emoticons { + allEmoticons[emoticon] = keyword.keyword + + if !existingEmoticons.contains(emoticon) { + allEmoticonsList.append(emoticon) + existingEmoticons.insert(emoticon) } - default: - break } } - } - - var items: [EmojiPagerContentComponent.Item] = [] - - var existingIds = Set() - for item in result { - if let itemFile = item.1 { - if existingIds.contains(itemFile.fileId) { + + for entry in view.entries { + guard let item = entry.item as? StickerPackItem else { continue } - existingIds.insert(itemFile.fileId) - let animationData = EntityKeyboardAnimationData(file: itemFile) - let item = EmojiPagerContentComponent.Item( - animationData: animationData, - content: .animation(animationData), - itemFile: itemFile, + 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() + 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) + } + } + + for emoji in allEmoticonsList { + items.append(EmojiPagerContentComponent.Item( + animationData: nil, + content: .staticEmoji(emoji), + itemFile: nil, subgroupId: nil, icon: .none, - tintMode: animationData.isTemplate ? .primary : .none - ) - items.append(item) + tintMode: .none + )) } + + 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 + )] } - - for emoji in allEmoticonsList { - items.append(EmojiPagerContentComponent.Item( - animationData: nil, - content: .staticEmoji(emoji), - itemFile: nil, - subgroupId: nil, - icon: .none, - tintMode: .none - )) - } - - 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 - )] } + + strongSelf.emojiSearchDisposable.set((resultSignal + |> delay(0.15, queue: .mainQueue()) + |> deliverOnMainQueue).start(next: { [weak self] result in + guard let strongSelf = self else { + return + } + strongSelf.emojiSearchResult.set(.single((result, AnyHashable(query)))) + })) } - + case let .category(value): + let resultSignal = strongSelf.context.engine.stickers.searchEmoji(emojiString: value) + |> mapToSignal { files -> Signal<[EmojiPagerContentComponent.ItemGroup], NoError> in + var items: [EmojiPagerContentComponent.Item] = [] + + var existingIds = Set() + 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 + )]) + } + strongSelf.emojiSearchDisposable.set((resultSignal |> delay(0.15, queue: .mainQueue()) |> deliverOnMainQueue).start(next: { [weak self] result in guard let strongSelf = self else { return } - strongSelf.emojiSearchResult.set(.single((result, AnyHashable(query)))) + strongSelf.emojiSearchResult.set(.single((result, AnyHashable(value)))) })) } }, @@ -1161,7 +1214,7 @@ public final class ChatEntityKeyboardInputNode: ChatInputNode { }, requestUpdate: { _ in }, - updateSearchQuery: { _, _ in + updateSearchQuery: { _ in }, updateScrollingToItemGroup: { }, @@ -1937,7 +1990,7 @@ public final class EntityInputView: UIInputView, AttachmentTextInputPanelInputVi }, requestUpdate: { _ in }, - updateSearchQuery: { _, _ in + updateSearchQuery: { _ in }, updateScrollingToItemGroup: { }, diff --git a/submodules/TelegramUI/Components/EmojiStatusSelectionComponent/Sources/EmojiStatusSelectionComponent.swift b/submodules/TelegramUI/Components/EmojiStatusSelectionComponent/Sources/EmojiStatusSelectionComponent.swift index 627f28145a..5a20d1904a 100644 --- a/submodules/TelegramUI/Components/EmojiStatusSelectionComponent/Sources/EmojiStatusSelectionComponent.swift +++ b/submodules/TelegramUI/Components/EmojiStatusSelectionComponent/Sources/EmojiStatusSelectionComponent.swift @@ -432,127 +432,237 @@ public final class EmojiStatusSelectionController: ViewController { }, requestUpdate: { _ in }, - updateSearchQuery: { rawQuery, languageCode in + updateSearchQuery: { query in guard let strongSelf = self else { return } - let query = rawQuery.trimmingCharacters(in: .whitespacesAndNewlines) - - if query.isEmpty { + switch query { + case .none: strongSelf.emojiSearchDisposable.set(nil) strongSelf.emojiSearchResult.set(.single(nil)) - } else { - let context = strongSelf.context + case let .text(rawQuery, languageCode): + let query = rawQuery.trimmingCharacters(in: .whitespacesAndNewlines) - 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 - } + if query.isEmpty { + strongSelf.emojiSearchDisposable.set(nil) + strongSelf.emojiSearchResult.set(.single(nil)) + } else { + let context = strongSelf.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 + } + ) } - - 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 + } + + 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 } } - } - - var items: [EmojiPagerContentComponent.Item] = [] - - var existingIds = Set() - for item in result { - if let itemFile = item.1 { - if existingIds.contains(itemFile.fileId) { + + for entry in view.entries { + guard let item = entry.item as? StickerPackItem else { 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) + 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() + 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 + )] } - - 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 - )] } + + strongSelf.emojiSearchDisposable.set((resultSignal + |> delay(0.15, queue: .mainQueue()) + |> deliverOnMainQueue).start(next: { result in + guard let strongSelf = self else { + return + } + strongSelf.emojiSearchResult.set(.single((result, AnyHashable(query)))) + })) } + case let .category(query): + if query.isEmpty { + strongSelf.emojiSearchDisposable.set(nil) + strongSelf.emojiSearchResult.set(.single(nil)) + } else { + let context = strongSelf.context + + let signal = context.engine.stickers.searchEmojiKeywords(inputLanguageCode: "en", query: query, completeMatch: false) - strongSelf.emojiSearchDisposable.set((resultSignal - |> delay(0.15, queue: .mainQueue()) - |> deliverOnMainQueue).start(next: { result in - guard let strongSelf = self else { - return + 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 } - strongSelf.emojiSearchResult.set(.single((result, AnyHashable(query)))) - })) + |> 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() + 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 + )] + } + } + + strongSelf.emojiSearchDisposable.set((resultSignal + |> delay(0.15, queue: .mainQueue()) + |> deliverOnMainQueue).start(next: { result in + guard let strongSelf = self else { + return + } + strongSelf.emojiSearchResult.set(.single((result, AnyHashable(query)))) + })) + } } }, updateScrollingToItemGroup: { diff --git a/submodules/TelegramUI/Components/EntityKeyboard/BUILD b/submodules/TelegramUI/Components/EntityKeyboard/BUILD index da95fdb49f..153632f6fb 100644 --- a/submodules/TelegramUI/Components/EntityKeyboard/BUILD +++ b/submodules/TelegramUI/Components/EntityKeyboard/BUILD @@ -30,6 +30,8 @@ swift_library( "//submodules/TelegramUI/Components/MultiAnimationRenderer:MultiAnimationRenderer", "//submodules/TelegramUI/Components/EmojiTextAttachmentView:EmojiTextAttachmentView", "//submodules/TelegramUI/Components/EmojiStatusComponent:EmojiStatusComponent", + "//submodules/TelegramUI/Components/LottieComponent", + "//submodules/TelegramUI/Components/LottieComponentEmojiContent", "//submodules/SoftwareVideo:SoftwareVideo", "//submodules/ShimmerEffect:ShimmerEffect", "//submodules/PhotoResources:PhotoResources", diff --git a/submodules/TelegramUI/Components/EntityKeyboard/Sources/EmojiPagerContentComponent.swift b/submodules/TelegramUI/Components/EntityKeyboard/Sources/EmojiPagerContentComponent.swift index 2f55bb2d3d..4c230710ba 100644 --- a/submodules/TelegramUI/Components/EntityKeyboard/Sources/EmojiPagerContentComponent.swift +++ b/submodules/TelegramUI/Components/EntityKeyboard/Sources/EmojiPagerContentComponent.swift @@ -1515,17 +1515,22 @@ public final class EmojiSearchHeaderView: UIView, UITextFieldDelegate { } private struct Params: Equatable { + var context: AccountContext var theme: PresentationTheme var strings: PresentationStrings var text: String var useOpaqueTheme: Bool var isActive: Bool var hasPresetSearch: Bool + var textInputState: EmojiSearchSearchBarComponent.TextInputState var size: CGSize var canFocus: Bool - var hasSearchItems: Bool + var searchCategories: EmojiSearchCategories? static func ==(lhs: Params, rhs: Params) -> Bool { + if lhs.context !== rhs.context { + return false + } if lhs.theme !== rhs.theme { return false } @@ -1544,13 +1549,16 @@ public final class EmojiSearchHeaderView: UIView, UITextFieldDelegate { if lhs.hasPresetSearch != rhs.hasPresetSearch { return false } + if lhs.textInputState != rhs.textInputState { + return false + } if lhs.size != rhs.size { return false } if lhs.canFocus != rhs.canFocus { return false } - if lhs.hasSearchItems != rhs.hasSearchItems { + if lhs.searchCategories != rhs.searchCategories { return false } return true @@ -1563,7 +1571,7 @@ public final class EmojiSearchHeaderView: UIView, UITextFieldDelegate { private let activated: () -> Void private let deactivated: (Bool) -> Void - private let updateQuery: (String, String) -> Void + private let updateQuery: (EmojiPagerContentComponent.SearchQuery?) -> Void let tintContainerView: UIView @@ -1580,14 +1588,13 @@ public final class EmojiSearchHeaderView: UIView, UITextFieldDelegate { private let clearIconTintView: UIImageView private let clearIconButton: HighlightTrackingButton - private let tintTextView: ComponentView - private let textView: ComponentView private let cancelButtonTintTitle: ComponentView private let cancelButtonTitle: ComponentView private let cancelButton: HighlightTrackingButton - private var suggestedItemsView: ComponentView? + private var placeholderContent = ComponentView() + private var textFrame: CGRect? private var textField: EmojiSearchTextField? private var tapRecognizer: UITapGestureRecognizer? @@ -1599,7 +1606,7 @@ public final class EmojiSearchHeaderView: UIView, UITextFieldDelegate { return self.textField != nil } - init(activated: @escaping () -> Void, deactivated: @escaping (Bool) -> Void, updateQuery: @escaping (String, String) -> Void) { + init(activated: @escaping () -> Void, deactivated: @escaping (Bool) -> Void, updateQuery: @escaping (EmojiPagerContentComponent.SearchQuery?) -> Void) { self.activated = activated self.deactivated = deactivated self.updateQuery = updateQuery @@ -1622,8 +1629,6 @@ public final class EmojiSearchHeaderView: UIView, UITextFieldDelegate { self.clearIconTintView.isHidden = true self.clearIconButton.isHidden = true - self.tintTextView = ComponentView() - self.textView = ComponentView() self.cancelButtonTintTitle = ComponentView() self.cancelButtonTitle = ComponentView() self.cancelButton = HighlightTrackingButton() @@ -1703,66 +1708,71 @@ public final class EmojiSearchHeaderView: UIView, UITextFieldDelegate { if case .ended = recognizer.state { let location = recognizer.location(in: self) if self.backIconView.frame.contains(location) { - if let suggestedItemsView = self.suggestedItemsView?.view as? EmojiSearchSearchBarComponent.View { - suggestedItemsView.clearSelection(dispatchEvent : true) + if let placeholderContentView = self.placeholderContent.view as? EmojiSearchSearchBarComponent.View { + placeholderContentView.clearSelection(dispatchEvent : true) } } else { - if self.textField == nil, let textComponentView = self.textView.view, self.params?.canFocus == true { - let backgroundFrame = self.backgroundLayer.frame - let textFieldFrame = CGRect(origin: CGPoint(x: textComponentView.frame.minX, y: backgroundFrame.minY), size: CGSize(width: backgroundFrame.maxX - textComponentView.frame.minX, height: backgroundFrame.height)) - - let textField = EmojiSearchTextField(frame: textFieldFrame) - textField.autocorrectionType = .no - self.textField = textField - self.insertSubview(textField, belowSubview: self.clearIconView) - textField.delegate = self - textField.addTarget(self, action: #selector(self.textFieldChanged(_:)), for: .editingChanged) - } - - self.currentPresetSearchTerm = nil - if let suggestedItemsView = self.suggestedItemsView?.view as? EmojiSearchSearchBarComponent.View { - suggestedItemsView.clearSelection(dispatchEvent: false) - } - - self.activated() - - self.textField?.becomeFirstResponder() + self.activateTextInput() } } } + private func activateTextInput() { + if self.textField == nil, let textFrame = self.textFrame, self.params?.canFocus == true { + let backgroundFrame = self.backgroundLayer.frame + let textFieldFrame = CGRect(origin: CGPoint(x: textFrame.minX, y: backgroundFrame.minY), size: CGSize(width: backgroundFrame.maxX - textFrame.minX, height: backgroundFrame.height)) + + let textField = EmojiSearchTextField(frame: textFieldFrame) + textField.autocorrectionType = .no + self.textField = textField + self.insertSubview(textField, belowSubview: self.clearIconView) + textField.delegate = self + textField.addTarget(self, action: #selector(self.textFieldChanged(_:)), for: .editingChanged) + } + + self.currentPresetSearchTerm = nil + if let placeholderContentView = self.placeholderContent.view as? EmojiSearchSearchBarComponent.View { + placeholderContentView.clearSelection(dispatchEvent: false) + } + + self.activated() + + self.textField?.becomeFirstResponder() + } + @objc private func cancelPressed() { self.currentPresetSearchTerm = nil - self.updateQuery("", "en") + self.updateQuery(nil) self.clearIconView.isHidden = true self.clearIconTintView.isHidden = true self.clearIconButton.isHidden = true - - self.deactivated(self.textField?.isFirstResponder ?? false) - - if let textField = self.textField { - self.textField = nil + let textField = self.textField + self.textField = nil + + self.deactivated(textField?.isFirstResponder ?? false) + + if let textField { textField.resignFirstResponder() textField.removeFromSuperview() } - self.tintTextView.view?.isHidden = false - self.textView.view?.isHidden = false + /*self.tintTextView.view?.isHidden = false + self.textView.view?.isHidden = false*/ } @objc private func clearPressed() { self.currentPresetSearchTerm = nil - self.updateQuery("", "en") + self.updateQuery(nil) self.textField?.text = "" self.clearIconView.isHidden = true self.clearIconTintView.isHidden = true self.clearIconButton.isHidden = true - self.tintTextView.view?.isHidden = false - self.textView.view?.isHidden = false + /*self.tintTextView.view?.isHidden = false + self.textView.view?.isHidden = false*/ } func deactivate() { @@ -1802,7 +1812,7 @@ public final class EmojiSearchHeaderView: UIView, UITextFieldDelegate { self.clearIconButton.isHidden = text.isEmpty self.currentPresetSearchTerm = nil - self.updateQuery(text, inputLanguage) + self.updateQuery(.text(value: text, language: inputLanguage)) } private func update(transition: Transition) { @@ -1810,20 +1820,29 @@ public final class EmojiSearchHeaderView: UIView, UITextFieldDelegate { return } self.params = nil - self.update(theme: params.theme, strings: params.strings, text: params.text, useOpaqueTheme: params.useOpaqueTheme, isActive: params.isActive, size: params.size, canFocus: params.canFocus, hasSearchItems: params.hasSearchItems, transition: transition) + self.update(context: params.context, theme: params.theme, strings: params.strings, text: params.text, useOpaqueTheme: params.useOpaqueTheme, isActive: params.isActive, size: params.size, canFocus: params.canFocus, searchCategories: params.searchCategories, transition: transition) } - public func update(theme: PresentationTheme, strings: PresentationStrings, text: String, useOpaqueTheme: Bool, isActive: Bool, size: CGSize, canFocus: Bool, hasSearchItems: Bool, transition: Transition) { + public func update(context: AccountContext, theme: PresentationTheme, strings: PresentationStrings, text: String, useOpaqueTheme: Bool, isActive: Bool, size: CGSize, canFocus: Bool, searchCategories: EmojiSearchCategories?, transition: Transition) { + let textInputState: EmojiSearchSearchBarComponent.TextInputState + if let textField = self.textField { + textInputState = .active(hasText: !(textField.text ?? "").isEmpty) + } else { + textInputState = .inactive + } + let params = Params( + context: context, theme: theme, strings: strings, text: text, useOpaqueTheme: useOpaqueTheme, isActive: isActive, hasPresetSearch: self.currentPresetSearchTerm == nil, + textInputState: textInputState, size: size, canFocus: canFocus, - hasSearchItems: hasSearchItems + searchCategories: searchCategories ) if self.params == params { @@ -1832,8 +1851,6 @@ public final class EmojiSearchHeaderView: UIView, UITextFieldDelegate { let isActiveWithText = isActive && self.currentPresetSearchTerm == nil - let isLeftAligned = isActiveWithText || hasSearchItems - if self.params?.theme !== theme { self.searchIconView.image = generateTintedImage(image: UIImage(bundleImageName: "Components/Search Bar/Loupe"), color: theme.chat.inputMediaPanel.panelContentVibrantOverlayColor) self.searchIconTintView.image = generateTintedImage(image: UIImage(bundleImageName: "Components/Search Bar/Loupe"), color: .white) @@ -1864,26 +1881,6 @@ public final class EmojiSearchHeaderView: UIView, UITextFieldDelegate { self.backgroundLayer.cornerRadius = inputHeight * 0.5 self.tintBackgroundLayer.cornerRadius = inputHeight * 0.5 - let textSize = self.textView.update( - transition: .immediate, - component: AnyComponent(Text( - text: text, - font: Font.regular(17.0), - color: theme.chat.inputMediaPanel.panelContentVibrantOverlayColor - )), - environment: {}, - containerSize: CGSize(width: size.width - 32.0, height: 100.0) - ) - let _ = self.tintTextView.update( - transition: .immediate, - component: AnyComponent(Text( - text: text, - font: Font.regular(17.0), - color: .white - )), - environment: {}, - containerSize: CGSize(width: size.width - 32.0, height: 100.0) - ) let cancelTextSize = self.cancelButtonTitle.update( transition: .immediate, component: AnyComponent(Text( @@ -1916,10 +1913,9 @@ public final class EmojiSearchHeaderView: UIView, UITextFieldDelegate { transition.setFrame(view: self.cancelButton, frame: CGRect(origin: CGPoint(x: backgroundFrame.maxX, y: 0.0), size: CGSize(width: cancelButtonSpacing + cancelTextSize.width, height: size.height))) - var textFrame = CGRect(origin: CGPoint(x: backgroundFrame.minX + floor((backgroundFrame.width - textSize.width) / 2.0), y: backgroundFrame.minY + floor((backgroundFrame.height - textSize.height) / 2.0)), size: textSize) - if isLeftAligned { - textFrame.origin.x = backgroundFrame.minX + sideTextInset - } + let textX: CGFloat = backgroundFrame.minX + sideTextInset + let textFrame = CGRect(origin: CGPoint(x: textX, y: backgroundFrame.minY), size: CGSize(width: backgroundFrame.maxX - textX, height: backgroundFrame.height)) + self.textFrame = textFrame if let image = self.searchIconView.image { let iconFrame = CGRect(origin: CGPoint(x: textFrame.minX - image.size.width - 4.0, y: backgroundFrame.minY + floor((backgroundFrame.height - image.size.height) / 2.0)), size: image.size) @@ -1945,7 +1941,63 @@ public final class EmojiSearchHeaderView: UIView, UITextFieldDelegate { transition.setAlpha(view: self.backIconTintView, alpha: self.currentPresetSearchTerm != nil ? 1.0 : 0.0) } - if hasSearchItems { + let placeholderContentFrame = CGRect(origin: CGPoint(x: textFrame.minX - 6.0, y: backgroundFrame.minX), size: CGSize(width: backgroundFrame.maxX - (textFrame.minX - 6.0), height: backgroundFrame.height)) + let _ = self.placeholderContent.update( + transition: transition, + component: AnyComponent(EmojiSearchSearchBarComponent( + context: context, + theme: theme, + strings: strings, + textInputState: textInputState, + categories: searchCategories, + searchTermUpdated: { [weak self] term in + guard let self else { + return + } + var shouldChangeActivation = false + if (self.currentPresetSearchTerm == nil) != (term == nil) { + shouldChangeActivation = true + } + self.currentPresetSearchTerm = term + + if shouldChangeActivation { + self.update(transition: Transition(animation: .curve(duration: 0.4, curve: .spring))) + + if let term { + self.updateQuery(.category(value: term)) + self.activated() + } else { + self.deactivated(self.textField?.isFirstResponder ?? false) + self.updateQuery(nil) + } + } else { + if let term { + self.updateQuery(.category(value: term)) + } else { + self.updateQuery(nil) + } + } + }, + activateTextInput: { [weak self] in + guard let self else { + return + } + self.activateTextInput() + } + )), + environment: {}, + containerSize: placeholderContentFrame.size + ) + if let placeholderContentView = self.placeholderContent.view as? EmojiSearchSearchBarComponent.View { + if placeholderContentView.superview == nil { + self.addSubview(placeholderContentView) + self.tintContainerView.addSubview(placeholderContentView.tintContainerView) + } + transition.setFrame(view: placeholderContentView, frame: placeholderContentFrame) + transition.setFrame(view: placeholderContentView.tintContainerView, frame: placeholderContentFrame) + } + + /*if let searchCategories { let suggestedItemsView: ComponentView var suggestedItemsTransition = transition if let current = self.suggestedItemsView { @@ -1958,39 +2010,7 @@ public final class EmojiSearchHeaderView: UIView, UITextFieldDelegate { let itemsX: CGFloat = textFrame.maxX + 8.0 let suggestedItemsFrame = CGRect(origin: CGPoint(x: itemsX, y: backgroundFrame.minY), size: CGSize(width: backgroundFrame.maxX - itemsX, height: backgroundFrame.height)) - let _ = suggestedItemsView.update( - transition: suggestedItemsTransition, - component: AnyComponent(EmojiSearchSearchBarComponent( - theme: theme, - strings: strings, - searchTermUpdated: { [weak self] term in - guard let self else { - return - } - var shouldChangeActivation = false - if (self.currentPresetSearchTerm == nil) != (term == nil) { - shouldChangeActivation = true - } - self.currentPresetSearchTerm = term - - if shouldChangeActivation { - self.update(transition: Transition(animation: .curve(duration: 0.4, curve: .spring))) - - if term == nil { - self.deactivated(self.textField?.isFirstResponder ?? false) - self.updateQuery(term ?? "", "en") - } else { - self.updateQuery(term ?? "", "en") - self.activated() - } - } else { - self.updateQuery(term ?? "", "en") - } - } - )), - environment: {}, - containerSize: suggestedItemsFrame.size - ) + if let suggestedItemsComponentView = suggestedItemsView.view { if suggestedItemsComponentView.superview == nil { self.addSubview(suggestedItemsComponentView) @@ -2007,7 +2027,7 @@ public final class EmojiSearchHeaderView: UIView, UITextFieldDelegate { }) } } - } + }*/ if let image = self.clearIconView.image { let iconFrame = CGRect(origin: CGPoint(x: backgroundFrame.maxX - image.size.width - 4.0, y: backgroundFrame.minY + floor((backgroundFrame.height - image.size.height) / 2.0)), size: image.size) @@ -2016,21 +2036,6 @@ public final class EmojiSearchHeaderView: UIView, UITextFieldDelegate { transition.setFrame(view: self.clearIconButton, frame: iconFrame.insetBy(dx: -8.0, dy: -10.0)) } - if let textComponentView = self.textView.view { - if textComponentView.superview == nil { - self.addSubview(textComponentView) - textComponentView.isUserInteractionEnabled = false - } - transition.setFrame(view: textComponentView, frame: textFrame) - } - if let tintTextComponentView = self.tintTextView.view { - if tintTextComponentView.superview == nil { - self.tintContainerView.addSubview(tintTextComponentView) - tintTextComponentView.isUserInteractionEnabled = false - } - transition.setFrame(view: tintTextComponentView, frame: textFrame) - } - if let cancelButtonTitleComponentView = self.cancelButtonTitle.view { if cancelButtonTitleComponentView.superview == nil { self.addSubview(cancelButtonTitleComponentView) @@ -2055,9 +2060,10 @@ public final class EmojiSearchHeaderView: UIView, UITextFieldDelegate { hasText = true } } + let _ = hasText - self.tintTextView.view?.isHidden = hasText - self.textView.view?.isHidden = hasText + /*self.tintTextView.view?.isHidden = hasText + self.textView.view?.isHidden = hasText*/ } } @@ -2233,7 +2239,7 @@ public final class EmojiPagerContentComponent: Component { public let presentGlobalOverlayController: (ViewController) -> Void public let navigationController: () -> NavigationController? public let requestUpdate: (Transition) -> Void - public let updateSearchQuery: (String, String) -> Void + public let updateSearchQuery: (EmojiPagerContentComponent.SearchQuery?) -> Void public let updateScrollingToItemGroup: () -> Void public let chatPeerId: PeerId? public let peekBehavior: EmojiContentPeekBehavior? @@ -2256,7 +2262,7 @@ public final class EmojiPagerContentComponent: Component { presentGlobalOverlayController: @escaping (ViewController) -> Void, navigationController: @escaping () -> NavigationController?, requestUpdate: @escaping (Transition) -> Void, - updateSearchQuery: @escaping (String, String) -> Void, + updateSearchQuery: @escaping (SearchQuery?) -> Void, updateScrollingToItemGroup: @escaping () -> Void, chatPeerId: PeerId?, peekBehavior: EmojiContentPeekBehavior?, @@ -2301,6 +2307,11 @@ public final class EmojiPagerContentComponent: Component { case flags = 7 } + public enum SearchQuery: Equatable { + case text(value: String, language: String) + case category(value: String) + } + public enum ItemContent: Equatable { public enum Id: Hashable { case animation(EntityKeyboardAnimationData.Id) @@ -2521,6 +2532,7 @@ public final class EmojiPagerContentComponent: Component { public let itemContentUniqueId: AnyHashable? public let warpContentsOnEdges: Bool public let displaySearchWithPlaceholder: String? + public let searchCategories: EmojiSearchCategories? public let searchInitiallyHidden: Bool public let searchIsPlaceholderOnly: Bool public let emptySearchResults: EmptySearchResults? @@ -2540,6 +2552,7 @@ public final class EmojiPagerContentComponent: Component { itemContentUniqueId: AnyHashable?, warpContentsOnEdges: Bool, displaySearchWithPlaceholder: String?, + searchCategories: EmojiSearchCategories?, searchInitiallyHidden: Bool, searchIsPlaceholderOnly: Bool, emptySearchResults: EmptySearchResults?, @@ -2558,6 +2571,7 @@ public final class EmojiPagerContentComponent: Component { self.itemContentUniqueId = itemContentUniqueId self.warpContentsOnEdges = warpContentsOnEdges self.displaySearchWithPlaceholder = displaySearchWithPlaceholder + self.searchCategories = searchCategories self.searchInitiallyHidden = searchInitiallyHidden self.searchIsPlaceholderOnly = searchIsPlaceholderOnly self.emptySearchResults = emptySearchResults @@ -2579,6 +2593,7 @@ public final class EmojiPagerContentComponent: Component { itemContentUniqueId: itemContentUniqueId, warpContentsOnEdges: self.warpContentsOnEdges, displaySearchWithPlaceholder: self.displaySearchWithPlaceholder, + searchCategories: self.searchCategories, searchInitiallyHidden: self.searchInitiallyHidden, searchIsPlaceholderOnly: self.searchIsPlaceholderOnly, emptySearchResults: emptySearchResults, @@ -2624,6 +2639,9 @@ public final class EmojiPagerContentComponent: Component { if lhs.displaySearchWithPlaceholder != rhs.displaySearchWithPlaceholder { return false } + if lhs.searchCategories != rhs.searchCategories { + return false + } if lhs.searchInitiallyHidden != rhs.searchInitiallyHidden { return false } @@ -6157,11 +6175,13 @@ public final class EmojiPagerContentComponent: Component { let scrollSize = CGSize(width: availableSize.width, height: availableSize.height) transition.setPosition(view: self.scrollView, position: CGPoint(x: 0.0, y: scrollOriginY)) - transition.setFrame(view: self.scrollViewClippingView, frame: CGRect(origin: CGPoint(x: 0.0, y: self.isSearchActivated ? itemLayout.itemInsets.top : 0.0), size: availableSize)) - transition.setBounds(view: self.scrollViewClippingView, bounds: CGRect(origin: CGPoint(x: 0.0, y: self.isSearchActivated ? itemLayout.itemInsets.top : 0.0), size: availableSize)) + let clippingTopInset: CGFloat = itemLayout.searchInsets.top + itemLayout.searchHeight + 2.0 - transition.setFrame(view: self.vibrancyClippingView, frame: CGRect(origin: CGPoint(x: 0.0, y: self.isSearchActivated ? itemLayout.itemInsets.top : 0.0), size: availableSize)) - transition.setBounds(view: self.vibrancyClippingView, bounds: CGRect(origin: CGPoint(x: 0.0, y: self.isSearchActivated ? itemLayout.itemInsets.top : 0.0), size: availableSize)) + transition.setFrame(view: self.scrollViewClippingView, frame: CGRect(origin: CGPoint(x: 0.0, y: self.isSearchActivated ? clippingTopInset : 0.0), size: availableSize)) + transition.setBounds(view: self.scrollViewClippingView, bounds: CGRect(origin: CGPoint(x: 0.0, y: self.isSearchActivated ? clippingTopInset : 0.0), size: availableSize)) + + transition.setFrame(view: self.vibrancyClippingView, frame: CGRect(origin: CGPoint(x: 0.0, y: self.isSearchActivated ? clippingTopInset : 0.0), size: availableSize)) + transition.setBounds(view: self.vibrancyClippingView, bounds: CGRect(origin: CGPoint(x: 0.0, y: self.isSearchActivated ? clippingTopInset : 0.0), size: availableSize)) let previousSize = self.scrollView.bounds.size var resetScrolling = false @@ -6180,7 +6200,7 @@ public final class EmojiPagerContentComponent: Component { let warpHeight: CGFloat = 50.0 var topWarpInset = pagerEnvironment.containerInsets.top if self.isSearchActivated { - topWarpInset += itemLayout.searchInsets.top + itemLayout.searchHeight + topWarpInset = itemLayout.searchInsets.top + itemLayout.searchHeight } if let warpView = self.warpView { transition.setFrame(view: warpView, frame: CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: availableSize)) @@ -6374,11 +6394,11 @@ public final class EmojiPagerContentComponent: Component { } else { strongSelf.component?.inputInteractionHolder.inputInteraction?.requestUpdate(.immediate) } - }, updateQuery: { [weak self] query, languageCode in + }, updateQuery: { [weak self] query in guard let strongSelf = self else { return } - strongSelf.component?.inputInteractionHolder.inputInteraction?.updateSearchQuery(query, languageCode) + strongSelf.component?.inputInteractionHolder.inputInteraction?.updateSearchQuery(query) }) self.visibleSearchHeader = visibleSearchHeader if self.isSearchActivated { @@ -6391,7 +6411,7 @@ public final class EmojiPagerContentComponent: Component { } let searchHeaderFrame = CGRect(origin: CGPoint(x: itemLayout.searchInsets.left, y: itemLayout.searchInsets.top), size: CGSize(width: itemLayout.width - itemLayout.searchInsets.left - itemLayout.searchInsets.right, height: itemLayout.searchHeight)) - visibleSearchHeader.update(theme: keyboardChildEnvironment.theme, strings: keyboardChildEnvironment.strings, text: displaySearchWithPlaceholder, useOpaqueTheme: useOpaqueTheme, isActive: self.isSearchActivated, size: searchHeaderFrame.size, canFocus: !component.searchIsPlaceholderOnly, hasSearchItems: component.displaySearchWithPlaceholder == keyboardChildEnvironment.strings.EmojiSearch_SearchEmojiPlaceholder, transition: transition) + visibleSearchHeader.update(context: component.context, theme: keyboardChildEnvironment.theme, strings: keyboardChildEnvironment.strings, text: displaySearchWithPlaceholder, useOpaqueTheme: useOpaqueTheme, isActive: self.isSearchActivated, size: searchHeaderFrame.size, canFocus: !component.searchIsPlaceholderOnly, searchCategories: component.searchCategories, transition: transition) transition.attachAnimation(view: visibleSearchHeader, completion: { [weak self] completed in guard let strongSelf = self, completed, let visibleSearchHeader = strongSelf.visibleSearchHeader else { return @@ -6445,7 +6465,7 @@ public final class EmojiPagerContentComponent: Component { } var animateContentCrossfade = false - if let previousComponent, previousComponent.itemContentUniqueId != component.itemContentUniqueId, itemTransition.animation.isImmediate, !transition.animation.isImmediate { + if let previousComponent, previousComponent.itemContentUniqueId != component.itemContentUniqueId, itemTransition.animation.isImmediate { animateContentCrossfade = true } @@ -6570,14 +6590,24 @@ public final class EmojiPagerContentComponent: Component { availableReactions = .single(nil) } + let searchCategories: Signal + if isEmojiSelection || isReactionSelection { + searchCategories = context.engine.stickers.emojiSearchCategories(kind: .emoji) + } else if isStatusSelection { + searchCategories = context.engine.stickers.emojiSearchCategories(kind: .status) + } else { + searchCategories = .single(nil) + } + let emojiItems: Signal = combineLatest( context.account.postbox.itemCollectionsView(orderedItemListCollectionIds: orderedItemListCollectionIds, namespaces: [Namespaces.ItemCollection.CloudEmojiPacks], aroundIndex: nil, count: 10000000), forceHasPremium ? .single(true) : hasPremium(context: context, chatPeerId: chatPeerId, premiumIfSavedMessages: true), context.account.viewTracker.featuredEmojiPacks(), availableReactions, + searchCategories, iconStatusEmoji ) - |> map { view, hasPremium, featuredEmojiPacks, availableReactions, iconStatusEmoji -> EmojiPagerContentComponent in + |> map { view, hasPremium, featuredEmojiPacks, availableReactions, searchCategories, iconStatusEmoji -> EmojiPagerContentComponent in struct ItemGroup { var supergroupId: AnyHashable var id: AnyHashable @@ -7282,6 +7312,7 @@ public final class EmojiPagerContentComponent: Component { } else if isEmojiSelection { displaySearchWithPlaceholder = strings.EmojiSearch_SearchEmojiPlaceholder } else if isProfilePhotoEmojiSelection || isGroupPhotoEmojiSelection { + //TODO:localize displaySearchWithPlaceholder = "Search" } } @@ -7334,6 +7365,7 @@ public final class EmojiPagerContentComponent: Component { itemContentUniqueId: nil, warpContentsOnEdges: isReactionSelection || isStatusSelection, displaySearchWithPlaceholder: displaySearchWithPlaceholder, + searchCategories: searchCategories, searchInitiallyHidden: searchInitiallyHidden, searchIsPlaceholderOnly: false, emptySearchResults: nil, @@ -7848,6 +7880,7 @@ public final class EmojiPagerContentComponent: Component { itemContentUniqueId: nil, warpContentsOnEdges: false, displaySearchWithPlaceholder: hasSearch ? strings.StickersSearch_SearchStickersPlaceholder : nil, + searchCategories: nil, searchInitiallyHidden: true, searchIsPlaceholderOnly: searchIsPlaceholderOnly, emptySearchResults: nil, diff --git a/submodules/TelegramUI/Components/EntityKeyboard/Sources/EmojiSearchSearchBarComponent.swift b/submodules/TelegramUI/Components/EntityKeyboard/Sources/EmojiSearchSearchBarComponent.swift index ca359ddf2a..de62a5dfc2 100644 --- a/submodules/TelegramUI/Components/EntityKeyboard/Sources/EmojiSearchSearchBarComponent.swift +++ b/submodules/TelegramUI/Components/EntityKeyboard/Sources/EmojiSearchSearchBarComponent.swift @@ -12,20 +12,76 @@ import AccountContext import AsyncDisplayKit import ComponentDisplayAdapters import LottieAnimationComponent +import EmojiStatusComponent +import LottieComponent +import LottieComponentEmojiContent + +private final class RoundMaskView: UIImageView { + private var currentDiameter: CGFloat? + + func update(diameter: CGFloat) { + if self.currentDiameter != diameter { + self.currentDiameter = diameter + + let shadowWidth: CGFloat = 6.0 + self.image = generateImage(CGSize(width: shadowWidth * 2.0 + diameter, height: diameter), rotatedContext: { size, context in + context.clear(CGRect(origin: CGPoint(), size: size)) + + let shadowColor = UIColor.black + + let stepCount = 10 + var colors: [CGColor] = [] + var locations: [CGFloat] = [] + + for i in 0 ... stepCount { + let t = CGFloat(i) / CGFloat(stepCount) + colors.append(shadowColor.withAlphaComponent(t * t).cgColor) + locations.append(t) + } + + let gradient = CGGradient(colorsSpace: deviceColorSpace, colors: colors as CFArray, locations: &locations)! + + let center = CGPoint(x: size.width / 2.0, y: size.height / 2.0) + let gradientWidth = shadowWidth + context.drawRadialGradient(gradient, startCenter: center, startRadius: size.width / 2.0, endCenter: center, endRadius: size.width / 2.0 - gradientWidth, options: []) + + context.setFillColor(shadowColor.cgColor) + context.fillEllipse(in: CGRect(origin: CGPoint(x: shadowWidth, y: 0.0), size: CGSize(width: size.height, height: size.height)).insetBy(dx: -0.5, dy: -0.5)) + })?.stretchableImage(withLeftCapWidth: Int(shadowWidth * 0.5 + diameter * 0.5), topCapHeight: Int(diameter * 0.5)) + } + } +} final class EmojiSearchSearchBarComponent: Component { + enum TextInputState: Equatable { + case inactive + case active(hasText: Bool) + } + + let context: AccountContext let theme: PresentationTheme let strings: PresentationStrings + let textInputState: TextInputState + let categories: EmojiSearchCategories? let searchTermUpdated: (String?) -> Void + let activateTextInput: () -> Void init( + context: AccountContext, theme: PresentationTheme, strings: PresentationStrings, - searchTermUpdated: @escaping (String?) -> Void + textInputState: TextInputState, + categories: EmojiSearchCategories?, + searchTermUpdated: @escaping (String?) -> Void, + activateTextInput: @escaping () -> Void ) { + self.context = context self.theme = theme self.strings = strings + self.textInputState = textInputState + self.categories = categories self.searchTermUpdated = searchTermUpdated + self.activateTextInput = activateTextInput } static func ==(lhs: EmojiSearchSearchBarComponent, rhs: EmojiSearchSearchBarComponent) -> Bool { @@ -35,6 +91,12 @@ final class EmojiSearchSearchBarComponent: Component { if lhs.strings !== rhs.strings { return false } + if lhs.textInputState != rhs.textInputState { + return false + } + if lhs.categories != rhs.categories { + return false + } return true } @@ -44,20 +106,29 @@ final class EmojiSearchSearchBarComponent: Component { let itemSize: CGSize let itemSpacing: CGFloat let contentSize: CGSize - let sideInset: CGFloat + let leftInset: CGFloat + let rightInset: CGFloat - init(containerSize: CGSize, itemCount: Int) { + let textSpacing: CGFloat + let textFrame: CGRect + + init(containerSize: CGSize, textSize: CGSize, itemCount: Int) { self.containerSize = containerSize self.itemCount = itemCount self.itemSpacing = 8.0 - self.sideInset = 8.0 + self.leftInset = 6.0 + self.rightInset = 8.0 self.itemSize = CGSize(width: 24.0, height: 24.0) + self.textSpacing = 8.0 - self.contentSize = CGSize(width: self.sideInset * 2.0 + self.itemSize.width * CGFloat(self.itemCount) + self.itemSpacing * CGFloat(max(0, self.itemCount - 1)), height: containerSize.height) + self.textFrame = CGRect(origin: CGPoint(x: self.leftInset, y: floor((containerSize.height - textSize.height) * 0.5)), size: textSize) + + self.contentSize = CGSize(width: self.leftInset + textSize.width + self.textSpacing + self.itemSize.width * CGFloat(self.itemCount) + self.itemSpacing * CGFloat(max(0, self.itemCount - 1)) + self.rightInset, height: containerSize.height) } func visibleItems(for rect: CGRect) -> Range? { - let offsetRect = rect.offsetBy(dx: -self.sideInset, dy: 0.0) + let baseItemX: CGFloat = self.textFrame.maxX + self.textSpacing + let offsetRect = rect.offsetBy(dx: -baseItemX, dy: 0.0) var minVisibleIndex = Int(floor((offsetRect.minX - self.itemSpacing) / (self.itemSize.width + self.itemSpacing))) minVisibleIndex = max(0, minVisibleIndex) var maxVisibleIndex = Int(ceil((offsetRect.maxX - self.itemSpacing) / (self.itemSize.height + self.itemSpacing))) @@ -71,17 +142,54 @@ final class EmojiSearchSearchBarComponent: Component { } func frame(at index: Int) -> CGRect { - return CGRect(origin: CGPoint(x: self.sideInset + CGFloat(index) * (self.itemSize.width + self.itemSpacing), y: floor((self.containerSize.height - self.itemSize.height) * 0.5)), size: self.itemSize) + return CGRect(origin: CGPoint(x: self.textFrame.maxX + self.textSpacing + CGFloat(index) * (self.itemSize.width + self.itemSpacing), y: floor((self.containerSize.height - self.itemSize.height) * 0.5)), size: self.itemSize) + } + } + + private final class ContentScrollView: UIScrollView, PagerExpandableScrollView { + override static var layerClass: AnyClass { + return EmojiPagerContentComponent.View.ContentScrollLayer.self + } + + private let mirrorView: UIView + + init(mirrorView: UIView) { + self.mirrorView = mirrorView + + super.init(frame: CGRect()) + + (self.layer as? EmojiPagerContentComponent.View.ContentScrollLayer)?.mirrorLayer = mirrorView.layer + self.canCancelContentTouches = true + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override func touchesShouldCancel(in view: UIView) -> Bool { + return true + } + } + + private final class ItemView { + let view = ComponentView() + let tintView = UIImageView() + + init() { } } final class View: UIView, UIScrollViewDelegate { - private let scrollView: UIScrollView + let tintContainerView: UIView + private let scrollView: ContentScrollView + private let tintScrollView: UIView - private var visibleItemViews: [AnyHashable: ComponentView] = [:] + private let tintTextView = ComponentView() + private let textView = ComponentView() + + private var visibleItemViews: [AnyHashable: ItemView] = [:] private let selectedItemBackground: SimpleLayer - - private var items: [String] = [] + private let selectedItemTintBackground: SimpleLayer private var component: EmojiSearchSearchBarComponent? private weak var state: EmptyComponentState? @@ -89,15 +197,23 @@ final class EmojiSearchSearchBarComponent: Component { private var itemLayout: ItemLayout? private var ignoreScrolling: Bool = false - private let maskLayer: SimpleLayer + private let roundMaskView: RoundMaskView + private let tintRoundMaskView: RoundMaskView - private var selectedItem: String? + private var selectedItem: AnyHashable? override init(frame: CGRect) { - self.scrollView = UIScrollView() - self.maskLayer = SimpleLayer() + self.tintContainerView = UIView() + + self.tintScrollView = UIView() + self.tintScrollView.clipsToBounds = true + self.scrollView = ContentScrollView(mirrorView: self.tintScrollView) + + self.roundMaskView = RoundMaskView() + self.tintRoundMaskView = RoundMaskView() self.selectedItemBackground = SimpleLayer() + self.selectedItemTintBackground = SimpleLayer() super.init(frame: frame) @@ -111,20 +227,20 @@ final class EmojiSearchSearchBarComponent: Component { self.scrollView.showsVerticalScrollIndicator = true self.scrollView.showsHorizontalScrollIndicator = false self.scrollView.delegate = self - self.scrollView.clipsToBounds = false + self.scrollView.clipsToBounds = true self.scrollView.scrollsToTop = false self.addSubview(self.scrollView) - //self.layer.mask = self.maskLayer - self.layer.addSublayer(self.maskLayer) - self.layer.masksToBounds = true + self.tintContainerView.addSubview(self.tintScrollView) + + self.mask = self.roundMaskView + self.tintContainerView.mask = self.tintRoundMaskView self.scrollView.layer.addSublayer(self.selectedItemBackground) + self.tintScrollView.layer.addSublayer(self.selectedItemTintBackground) - self.scrollView.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.tapGesture(_:)))) - - self.items = ["Smile", "🤔", "😝", "😡", "😐", "đŸŒī¸â€â™€ī¸", "🎉", "😨", "â¤ī¸", "😄", "👍", "â˜šī¸", "👎", "⛔", "💤", "đŸ’ŧ", "🍔", "🏠", "🛁", "🏖", "âšŊī¸", "🕔"] + self.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.tapGesture(_:)))) } required init?(coder: NSCoder) { @@ -133,18 +249,28 @@ final class EmojiSearchSearchBarComponent: Component { @objc private func tapGesture(_ recognizer: UITapGestureRecognizer) { if case .ended = recognizer.state { + guard let component = self.component, let itemLayout = self.itemLayout else { + return + } let location = recognizer.location(in: self.scrollView) - for (id, itemView) in self.visibleItemViews { - if let itemComponentView = itemView.view, itemComponentView.frame.contains(location), let item = id.base as? String { - if self.selectedItem == item { - self.selectedItem = nil - } else { - self.selectedItem = item + if location.x <= itemLayout.textFrame.maxX + itemLayout.textSpacing { + component.activateTextInput() + } else { + for (id, itemView) in self.visibleItemViews { + if let itemComponentView = itemView.view.view, itemComponentView.frame.contains(location), let itemId = id.base as? Int64 { + if self.selectedItem == AnyHashable(id) { + self.selectedItem = nil + } else { + self.selectedItem = AnyHashable(id) + } + self.state?.updated(transition: .immediate) + + if let categories = component.categories, let group = categories.groups.first(where: { $0.id == itemId }) { + component.searchTermUpdated(group.identifiers.joined(separator: "")) + } + + break } - self.state?.updated(transition: .immediate) - self.component?.searchTermUpdated(self.selectedItem) - - break } } } @@ -155,7 +281,7 @@ final class EmojiSearchSearchBarComponent: Component { self.selectedItem = nil self.state?.updated(transition: .immediate) if dispatchEvent { - self.component?.searchTermUpdated(self.selectedItem) + self.component?.searchTermUpdated(nil) } } } @@ -171,6 +297,14 @@ final class EmojiSearchSearchBarComponent: Component { return } + let itemAlpha: CGFloat + switch component.textInputState { + case .active: + itemAlpha = 0.0 + case .inactive: + itemAlpha = 1.0 + } + var validItemIds = Set() let visibleBounds = self.scrollView.bounds @@ -179,70 +313,49 @@ final class EmojiSearchSearchBarComponent: Component { animateAppearingItems = true } - let items = self.items + let items = component.categories?.groups ?? [] for i in 0 ..< items.count { let itemFrame = itemLayout.frame(at: i) if visibleBounds.intersects(itemFrame) { let item = items[i] - validItemIds.insert(AnyHashable(item)) + validItemIds.insert(AnyHashable(item.id)) var animateItem = false var itemTransition = transition - let itemView: ComponentView - if let current = self.visibleItemViews[item] { + let itemView: ItemView + if let current = self.visibleItemViews[AnyHashable(item.id)] { itemView = current } else { animateItem = animateAppearingItems itemTransition = .immediate - itemView = ComponentView() - self.visibleItemViews[item] = itemView + itemView = ItemView() + self.visibleItemViews[AnyHashable(item.id)] = itemView } - let animationName: String + let color = component.theme.chat.inputMediaPanel.panelContentVibrantOverlayColor - switch EmojiPagerContentComponent.StaticEmojiSegment.allCases[i % EmojiPagerContentComponent.StaticEmojiSegment.allCases.count] { - case .people: - animationName = "emojicat_smiles" - case .animalsAndNature: - animationName = "emojicat_animals" - case .foodAndDrink: - animationName = "emojicat_food" - case .activityAndSport: - animationName = "emojicat_activity" - case .travelAndPlaces: - animationName = "emojicat_places" - case .objects: - animationName = "emojicat_objects" - case .symbols: - animationName = "emojicat_symbols" - case .flags: - animationName = "emojicat_flags" - } - - let baseColor: UIColor - baseColor = component.theme.chat.inputMediaPanel.panelIconColor - - let baseHighlightedColor = component.theme.chat.inputMediaPanel.panelHighlightedIconBackgroundColor.blitOver(component.theme.chat.inputPanel.panelBackgroundColor, alpha: 1.0) - let color = baseColor.blitOver(baseHighlightedColor, alpha: 1.0) - - let _ = itemTransition - let _ = itemView.update( + let _ = itemView.view.update( transition: .immediate, - component: AnyComponent(LottieAnimationComponent( - animation: LottieAnimationComponent.AnimationItem( - name: animationName, - mode: .still(position: .end) + component: AnyComponent(LottieComponent( + content: LottieComponent.EmojiContent( + context: component.context, + fileId: item.id ), - colors: ["__allcolors__": color], - size: itemLayout.itemSize + color: color )), environment: {}, containerSize: itemLayout.itemSize ) - if let view = itemView.view { + + itemView.tintView.tintColor = .white + + if let view = itemView.view.view as? LottieComponent.View { if view.superview == nil { self.scrollView.addSubview(view) + + view.output = itemView.tintView + self.tintScrollView.addSubview(itemView.tintView) } itemTransition.setPosition(view: view, position: CGPoint(x: itemFrame.midX, y: itemFrame.midY)) @@ -250,18 +363,24 @@ final class EmojiSearchSearchBarComponent: Component { let scaleFactor = itemFrame.width / itemLayout.itemSize.width itemTransition.setSublayerTransform(view: view, transform: CATransform3DMakeScale(scaleFactor, scaleFactor, 1.0)) + itemTransition.setPosition(view: itemView.tintView, position: CGPoint(x: itemFrame.midX, y: itemFrame.midY)) + itemTransition.setBounds(view: itemView.tintView, bounds: CGRect(origin: CGPoint(), size: CGSize(width: itemLayout.itemSize.width, height: itemLayout.itemSize.height))) + itemTransition.setSublayerTransform(view: itemView.tintView, transform: CATransform3DMakeScale(scaleFactor, scaleFactor, 1.0)) + + itemTransition.setAlpha(view: view, alpha: itemAlpha) + itemTransition.setAlpha(view: itemView.tintView, alpha: itemAlpha) + let isHidden = !visibleBounds.intersects(itemFrame) if isHidden != view.isHidden { view.isHidden = isHidden + itemView.tintView.isHidden = true if !isHidden { - if let view = view as? LottieAnimationComponent.View { - view.playOnce() - } + view.playOnce() } } else if animateItem { - if let view = view as? LottieAnimationComponent.View { - view.playOnce() + if fromScrolling { + view.playOnce(delay: 0.08) } } } @@ -272,25 +391,32 @@ final class EmojiSearchSearchBarComponent: Component { for (id, itemView) in self.visibleItemViews { if !validItemIds.contains(id) { removedItemIds.append(id) - itemView.view?.removeFromSuperview() + itemView.view.view?.removeFromSuperview() + itemView.tintView.removeFromSuperview() } } for id in removedItemIds { self.visibleItemViews.removeValue(forKey: id) } - if let selectedItem = self.selectedItem, let index = self.items.firstIndex(of: selectedItem) { + if let selectedItem = self.selectedItem, let index = items.firstIndex(where: { AnyHashable($0.id) == selectedItem }) { self.selectedItemBackground.isHidden = false + self.selectedItemTintBackground.isHidden = false let selectedItemCenter = itemLayout.frame(at: index).center let selectionSize = CGSize(width: 28.0, height: 28.0) - self.selectedItemBackground.backgroundColor = component.theme.chat.inputMediaPanel.panelContentControlOpaqueSelectionColor.cgColor + self.selectedItemBackground.backgroundColor = component.theme.chat.inputMediaPanel.panelContentControlVibrantSelectionColor.cgColor + self.selectedItemTintBackground.backgroundColor = UIColor(white: 1.0, alpha: 0.2).cgColor self.selectedItemBackground.cornerRadius = selectionSize.height * 0.5 + self.selectedItemTintBackground.cornerRadius = selectionSize.height * 0.5 - self.selectedItemBackground.frame = CGRect(origin: CGPoint(x: floor(selectedItemCenter.x - selectionSize.width * 0.5), y: floor(selectedItemCenter.y - selectionSize.height * 0.5)), size: selectionSize) + let selectionFrame = CGRect(origin: CGPoint(x: floor(selectedItemCenter.x - selectionSize.width * 0.5), y: floor(selectedItemCenter.y - selectionSize.height * 0.5)), size: selectionSize) + self.selectedItemBackground.frame = selectionFrame + self.selectedItemTintBackground.frame = selectionFrame } else { self.selectedItemBackground.isHidden = true + self.selectedItemTintBackground.isHidden = true } } @@ -298,11 +424,43 @@ final class EmojiSearchSearchBarComponent: Component { self.component = component self.state = state - transition.setCornerRadius(layer: self.layer, cornerRadius: availableSize.height * 0.5) + let textSize = self.textView.update( + transition: .immediate, + component: AnyComponent(Text( + text: component.strings.Common_Search, + font: Font.regular(17.0), + color: component.theme.chat.inputMediaPanel.panelContentVibrantOverlayColor + )), + environment: {}, + containerSize: CGSize(width: availableSize.width - 32.0, height: 100.0) + ) + let _ = self.tintTextView.update( + transition: .immediate, + component: AnyComponent(Text( + text: component.strings.Common_Search, + font: Font.regular(17.0), + color: .white + )), + environment: {}, + containerSize: CGSize(width: availableSize.width - 32.0, height: 100.0) + ) - let itemLayout = ItemLayout(containerSize: availableSize, itemCount: self.items.count) + let itemLayout = ItemLayout(containerSize: availableSize, textSize: textSize, itemCount: component.categories?.groups.count ?? 0) self.itemLayout = itemLayout + if let textComponentView = self.textView.view { + if textComponentView.superview == nil { + self.scrollView.addSubview(textComponentView) + } + transition.setFrame(view: textComponentView, frame: itemLayout.textFrame) + } + if let tintTextComponentView = self.tintTextView.view { + if tintTextComponentView.superview == nil { + self.tintScrollView.addSubview(tintTextComponentView) + } + transition.setFrame(view: tintTextComponentView, frame: itemLayout.textFrame) + } + self.ignoreScrolling = true if self.scrollView.bounds.size != availableSize { transition.setFrame(view: self.scrollView, frame: CGRect(origin: CGPoint(), size: availableSize)) @@ -312,8 +470,25 @@ final class EmojiSearchSearchBarComponent: Component { } self.ignoreScrolling = false + let maskFrame = CGRect(origin: CGPoint(), size: availableSize) + transition.setFrame(view: self.roundMaskView, frame: maskFrame) + self.roundMaskView.update(diameter: maskFrame.height) + transition.setFrame(view: self.tintRoundMaskView, frame: maskFrame) + self.tintRoundMaskView.update(diameter: maskFrame.height) + self.updateScrolling(transition: transition, fromScrolling: false) + switch component.textInputState { + case let .active(hasText): + self.isUserInteractionEnabled = false + self.textView.view?.isHidden = hasText + self.tintTextView.view?.isHidden = hasText + case .inactive: + self.isUserInteractionEnabled = true + self.textView.view?.isHidden = false + self.tintTextView.view?.isHidden = false + } + return availableSize } } diff --git a/submodules/TelegramUI/Components/EntityKeyboard/Sources/GifPagerContentComponent.swift b/submodules/TelegramUI/Components/EntityKeyboard/Sources/GifPagerContentComponent.swift index 651b556585..2b60f081b9 100644 --- a/submodules/TelegramUI/Components/EntityKeyboard/Sources/GifPagerContentComponent.swift +++ b/submodules/TelegramUI/Components/EntityKeyboard/Sources/GifPagerContentComponent.swift @@ -971,7 +971,7 @@ public final class GifPagerContentComponent: Component { } strongSelf.component?.inputInteraction.openSearch() }, deactivated: { _ in - }, updateQuery: {_, _ in + }, updateQuery: { _ in }) self.visibleSearchHeader = visibleSearchHeader self.scrollView.addSubview(visibleSearchHeader) @@ -979,7 +979,7 @@ public final class GifPagerContentComponent: Component { } let searchHeaderFrame = CGRect(origin: CGPoint(x: itemLayout.searchInsets.left, y: itemLayout.searchInsets.top), size: CGSize(width: itemLayout.width - itemLayout.searchInsets.left - itemLayout.searchInsets.right, height: itemLayout.searchHeight)) - visibleSearchHeader.update(theme: keyboardChildEnvironment.theme, strings: keyboardChildEnvironment.strings, text: displaySearchWithPlaceholder, useOpaqueTheme: false, isActive: false, size: searchHeaderFrame.size, canFocus: false, hasSearchItems: true, transition: transition) + visibleSearchHeader.update(context: component.context, theme: keyboardChildEnvironment.theme, strings: keyboardChildEnvironment.strings, text: displaySearchWithPlaceholder, useOpaqueTheme: false, isActive: false, size: searchHeaderFrame.size, canFocus: false, searchCategories: nil, transition: transition) transition.setFrame(view: visibleSearchHeader, frame: searchHeaderFrame, completion: { [weak self] completed in guard let strongSelf = self, completed, let visibleSearchHeader = strongSelf.visibleSearchHeader else { return diff --git a/submodules/TelegramUI/Components/ForumCreateTopicScreen/Sources/ForumCreateTopicScreen.swift b/submodules/TelegramUI/Components/ForumCreateTopicScreen/Sources/ForumCreateTopicScreen.swift index 50390d204e..84531affee 100644 --- a/submodules/TelegramUI/Components/ForumCreateTopicScreen/Sources/ForumCreateTopicScreen.swift +++ b/submodules/TelegramUI/Components/ForumCreateTopicScreen/Sources/ForumCreateTopicScreen.swift @@ -963,7 +963,7 @@ private final class ForumCreateTopicScreenComponent: CombinedComponent { }, requestUpdate: { _ in }, - updateSearchQuery: { _, _ in + updateSearchQuery: { _ in }, updateScrollingToItemGroup: { }, diff --git a/submodules/TelegramUI/Components/LottieComponent/BUILD b/submodules/TelegramUI/Components/LottieComponent/BUILD new file mode 100644 index 0000000000..db1a67839a --- /dev/null +++ b/submodules/TelegramUI/Components/LottieComponent/BUILD @@ -0,0 +1,22 @@ +load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library") + +swift_library( + name = "LottieComponent", + module_name = "LottieComponent", + srcs = glob([ + "Sources/**/*.swift", + ]), + copts = [ + "-warnings-as-errors", + ], + deps = [ + "//submodules/Display", + "//submodules/ComponentFlow", + "//submodules/Components/HierarchyTrackingLayer", + "//submodules/rlottie:RLottieBinding", + "//submodules/SSignalKit/SwiftSignalKit", + ], + visibility = [ + "//visibility:public", + ], +) diff --git a/submodules/TelegramUI/Components/LottieComponent/Sources/LottieComponent.swift b/submodules/TelegramUI/Components/LottieComponent/Sources/LottieComponent.swift new file mode 100644 index 0000000000..f773b61d1f --- /dev/null +++ b/submodules/TelegramUI/Components/LottieComponent/Sources/LottieComponent.swift @@ -0,0 +1,246 @@ +import Foundation +import UIKit +import Display +import ComponentFlow +import HierarchyTrackingLayer +import RLottieBinding +import SwiftSignalKit +import Accelerate + +public final class LottieComponent: Component { + public typealias EnvironmentType = Empty + + open class Content: Equatable { + public init() { + } + + public static func ==(lhs: Content, rhs: Content) -> Bool { + if lhs === rhs { + return true + } + return lhs.isEqual(to: rhs) + } + + open func isEqual(to other: Content) -> Bool { + preconditionFailure() + } + + open func load(_ f: @escaping (Data) -> Void) -> Disposable { + preconditionFailure() + } + } + + public let content: Content + public let color: UIColor + + public init( + content: Content, + color: UIColor + ) { + self.content = content + self.color = color + } + + public static func ==(lhs: LottieComponent, rhs: LottieComponent) -> Bool { + if lhs.content != rhs.content { + return false + } + if lhs.color != rhs.color { + return false + } + return true + } + + public final class View: UIImageView { + private weak var state: EmptyComponentState? + private var component: LottieComponent? + + private var scheduledPlayOnce: Bool = false + private var animationInstance: LottieInstance? + private var currentDisplaySize: CGSize? + private var currentContentDisposable: Disposable? + + private var currentFrame: Int = 0 + private var currentFrameStartTime: Double? + + private var displayLink: SharedDisplayLinkDriver.Link? + + private var currentTemplateFrameImage: UIImage? + + public weak var output: UIImageView? { + didSet { + if let output = self.output, let currentTemplateFrameImage = self.currentTemplateFrameImage { + output.image = currentTemplateFrameImage + } + } + } + + override init(frame: CGRect) { + //self.hierarchyTrackingLayer = HierarchyTrackingLayer() + + super.init(frame: frame) + + //self.layer.addSublayer(self.hierarchyTrackingLayer) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + deinit { + self.currentContentDisposable?.dispose() + } + + public func playOnce(delay: Double = 0.0) { + guard let _ = self.animationInstance else { + self.scheduledPlayOnce = true + return + } + + self.scheduledPlayOnce = false + + if self.currentFrame != 0 { + self.currentFrame = 0 + self.updateImage() + } + + if delay != 0.0 { + self.isHidden = true + DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + 0.08, execute: { [weak self] in + guard let self else { + return + } + self.isHidden = false + + self.currentFrameStartTime = CACurrentMediaTime() + if self.displayLink == nil { + self.displayLink = SharedDisplayLinkDriver.shared.add(needsHighestFramerate: false, { [weak self] in + guard let self else { + return + } + self.advanceIfNeeded() + }) + } + }) + } else { + self.currentFrameStartTime = CACurrentMediaTime() + if self.displayLink == nil { + self.displayLink = SharedDisplayLinkDriver.shared.add(needsHighestFramerate: false, { [weak self] in + guard let self else { + return + } + self.advanceIfNeeded() + }) + } + } + } + + private func loadAnimation(data: Data) { + self.animationInstance = LottieInstance(data: data, fitzModifier: .none, colorReplacements: nil, cacheKey: "") + if self.scheduledPlayOnce { + self.scheduledPlayOnce = false + self.playOnce() + } else if let animationInstance = self.animationInstance { + self.currentFrame = Int(animationInstance.frameCount - 1) + self.updateImage() + } + } + + private func advanceIfNeeded() { + guard let animationInstance = self.animationInstance else { + return + } + guard let currentFrameStartTime = self.currentFrameStartTime else { + return + } + + let secondsPerFrame: Double + if animationInstance.frameRate == 0 { + secondsPerFrame = 1.0 / 60.0 + } else { + secondsPerFrame = 1.0 / Double(animationInstance.frameRate) + } + + let timestamp = CACurrentMediaTime() + if currentFrameStartTime + timestamp >= secondsPerFrame * 0.9 { + self.currentFrame += 1 + if self.currentFrame >= Int(animationInstance.frameCount) - 1 { + self.currentFrame = Int(animationInstance.frameCount) - 1 + self.updateImage() + self.displayLink?.invalidate() + self.displayLink = nil + } else { + self.currentFrameStartTime = timestamp + self.updateImage() + } + } + } + + private func updateImage() { + guard let animationInstance = self.animationInstance, let currentDisplaySize = self.currentDisplaySize else { + return + } + guard let context = DrawingContext(size: currentDisplaySize, scale: 1.0, opaque: false, clear: true) else { + return + } + + var destinationBuffer = vImage_Buffer() + destinationBuffer.width = UInt(context.scaledSize.width) + destinationBuffer.height = UInt(context.scaledSize.height) + destinationBuffer.data = context.bytes + destinationBuffer.rowBytes = context.bytesPerRow + + animationInstance.renderFrame(with: Int32(self.currentFrame % Int(animationInstance.frameCount)), into: context.bytes.assumingMemoryBound(to: UInt8.self), width: Int32(currentDisplaySize.width), height: Int32(currentDisplaySize.height), bytesPerRow: Int32(context.bytesPerRow)) + self.currentTemplateFrameImage = context.generateImage()?.withRenderingMode(.alwaysTemplate) + self.image = self.currentTemplateFrameImage + + if let output = self.output, let currentTemplateFrameImage = self.currentTemplateFrameImage { + output.image = currentTemplateFrameImage + } + } + + func update(component: LottieComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + let previousComponent = self.component + + self.component = component + self.state = state + + var redrawImage = false + + let displaySize = CGSize(width: availableSize.width * UIScreenScale, height: availableSize.height * UIScreenScale) + if self.currentDisplaySize != displaySize { + self.currentDisplaySize = displaySize + redrawImage = true + } + + if previousComponent?.content != component.content { + self.currentContentDisposable?.dispose() + let content = component.content + self.currentContentDisposable = component.content.load { [weak self, weak content] data in + Queue.mainQueue().async { + guard let self, self.component?.content == content else { + return + } + self.loadAnimation(data: data) + } + } + } else if redrawImage { + self.updateImage() + } + + if self.tintColor != component.color { + self.tintColor = component.color + } + + return availableSize + } + } + + public func makeView() -> View { + return View(frame: CGRect()) + } + + public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) + } +} diff --git a/submodules/TelegramUI/Components/LottieComponentEmojiContent/BUILD b/submodules/TelegramUI/Components/LottieComponentEmojiContent/BUILD new file mode 100644 index 0000000000..e6e41c711f --- /dev/null +++ b/submodules/TelegramUI/Components/LottieComponentEmojiContent/BUILD @@ -0,0 +1,22 @@ +load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library") + +swift_library( + name = "LottieComponentEmojiContent", + module_name = "LottieComponentEmojiContent", + srcs = glob([ + "Sources/**/*.swift", + ]), + copts = [ + "-warnings-as-errors", + ], + deps = [ + "//submodules/SSignalKit/SwiftSignalKit", + "//submodules/TelegramCore", + "//submodules/TelegramUI/Components/LottieComponent", + "//submodules/AccountContext", + "//submodules/GZip:GZip", + ], + visibility = [ + "//visibility:public", + ], +) diff --git a/submodules/TelegramUI/Components/LottieComponentEmojiContent/Sources/LottieComponentEmojiContent.swift b/submodules/TelegramUI/Components/LottieComponentEmojiContent/Sources/LottieComponentEmojiContent.swift new file mode 100644 index 0000000000..aa770065fc --- /dev/null +++ b/submodules/TelegramUI/Components/LottieComponentEmojiContent/Sources/LottieComponentEmojiContent.swift @@ -0,0 +1,67 @@ +import Foundation +import LottieComponent +import SwiftSignalKit +import TelegramCore +import AccountContext +import GZip + +public extension LottieComponent { + final class EmojiContent: LottieComponent.Content { + private let context: AccountContext + private let fileId: Int64 + + public init( + context: AccountContext, + fileId: Int64 + ) { + self.context = context + self.fileId = fileId + + super.init() + } + + override public func isEqual(to other: Content) -> Bool { + guard let other = other as? EmojiContent else { + return false + } + if self.fileId != other.fileId { + return false + } + return true + } + + override public func load(_ f: @escaping (Data) -> Void) -> Disposable { + let fileId = self.fileId + let mediaBox = self.context.account.postbox.mediaBox + return (self.context.engine.stickers.resolveInlineStickers(fileIds: [fileId]) + |> mapToSignal { files -> Signal in + guard let file = files[fileId] else { + return .single(nil) + } + return Signal { subscriber in + let dataDisposable = (mediaBox.resourceData(file.resource) + |> filter { data in return data.complete }).start(next: { data in + if let contents = try? Data(contentsOf: URL(fileURLWithPath: data.path)) { + let result = TGGUnzipData(contents, 2 * 1024 * 1024) ?? contents + subscriber.putNext(result) + subscriber.putCompletion() + } else { + subscriber.putNext(nil) + } + }) + let fetchDisposable = mediaBox.fetchedResource(file.resource, parameters: nil).start() + + return ActionDisposable { + dataDisposable.dispose() + fetchDisposable.dispose() + } + } + }).start(next: { data in + guard let data else { + return + } + f(data) + }) + } + } +} diff --git a/submodules/TelegramUI/Sources/ChatController.swift b/submodules/TelegramUI/Sources/ChatController.swift index 41e0a8c570..7699c7acc6 100644 --- a/submodules/TelegramUI/Sources/ChatController.swift +++ b/submodules/TelegramUI/Sources/ChatController.swift @@ -7130,6 +7130,11 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G } strongSelf.commitPurposefulAction() + var hasDisabledContent = false + if "".isEmpty { + hasDisabledContent = false + } + if let channel = strongSelf.presentationInterfaceState.renderedPeer?.peer as? TelegramChannel, channel.isRestrictedBySlowmode { let forwardCount = messages.reduce(0, { count, message -> Int in if case .forward = message { @@ -7144,6 +7149,8 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G errorText = strongSelf.presentationData.strings.Chat_AttachmentMultipleForwardDisabled } else if isAnyMessageTextPartitioned { errorText = strongSelf.presentationData.strings.Chat_MultipleTextMessagesDisabled + } else if hasDisabledContent { + errorText = strongSelf.restrictedSendingContentsText() } if let errorText = errorText { @@ -8449,6 +8456,8 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G return } + strongSelf.dismissAllTooltips() + strongSelf.mediaRecordingModeTooltipController?.dismiss() strongSelf.interfaceInteraction?.updateShowWebView { _ in return false @@ -8460,14 +8469,12 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G bannedMediaInput = true } else if channel.hasBannedPermission(.banSendVoice) != nil { if !isVideo { - //TODO:localize - strongSelf.controllerInteraction?.displayUndo(.info(title: nil, text: "The admins of this group do not allow to send voice messages.")) + strongSelf.controllerInteraction?.displayUndo(.info(title: nil, text: strongSelf.restrictedSendingContentsText())) return } } else if channel.hasBannedPermission(.banSendInstantVideos) != nil { if isVideo { - //TODO:localize - strongSelf.controllerInteraction?.displayUndo(.info(title: nil, text: "The admins of this group do not allow to send video messages.")) + strongSelf.controllerInteraction?.displayUndo(.info(title: nil, text: strongSelf.restrictedSendingContentsText())) return } } @@ -8476,22 +8483,19 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G bannedMediaInput = true } else if group.hasBannedPermission(.banSendVoice) { if !isVideo { - //TODO:localize - strongSelf.controllerInteraction?.displayUndo(.info(title: nil, text: "The admins of this group do not allow to send voice messages.")) + strongSelf.controllerInteraction?.displayUndo(.info(title: nil, text: strongSelf.restrictedSendingContentsText())) return } } else if group.hasBannedPermission(.banSendInstantVideos) { if isVideo { - //TODO:localize - strongSelf.controllerInteraction?.displayUndo(.info(title: nil, text: "The admins of this group do not allow to send video messages.")) + strongSelf.controllerInteraction?.displayUndo(.info(title: nil, text: strongSelf.restrictedSendingContentsText())) return } } } if bannedMediaInput { - //TODO:localize - strongSelf.controllerInteraction?.displayUndo(.info(title: nil, text: "The admins of this group do not allow to send video and voice messages.")) + strongSelf.controllerInteraction?.displayUndo(.info(title: nil, text: strongSelf.restrictedSendingContentsText())) return } @@ -8792,8 +8796,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G } if bannedMediaInput { - //TODO:localize - strongSelf.controllerInteraction?.displayUndo(.info(title: nil, text: "The admins of this group do not allow to send video and voice messages.")) + strongSelf.controllerInteraction?.displayUndo(.info(title: nil, text: strongSelf.restrictedSendingContentsText())) return } @@ -10133,8 +10136,8 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G text = strongSelf.presentationData.strings.Conversation_SendMessageErrorGroupRestricted moreInfo = true case .mediaRestricted: - strongSelf.interfaceInteraction?.displayRestrictedInfo(.mediaRecording, .alert) - return + text = strongSelf.restrictedSendingContentsText() + moreInfo = false case .slowmodeActive: text = strongSelf.presentationData.strings.Chat_SlowmodeSendError moreInfo = false @@ -12239,6 +12242,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G self.chatDisplayNode.dismissInput() + var banSendText: (Int32, Bool)? var bannedSendPhotos: (Int32, Bool)? var bannedSendVideos: (Int32, Bool)? var bannedSendFiles: (Int32, Bool)? @@ -12258,6 +12262,9 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G if let value = channel.hasBannedPermission(.banSendFiles) { bannedSendFiles = value } + if let value = channel.hasBannedPermission(.banSendText) { + banSendText = value + } if channel.hasBannedPermission(.banSendPolls) != nil { canSendPolls = false } @@ -12271,14 +12278,21 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G if group.hasBannedPermission(.banSendFiles) { bannedSendFiles = (Int32.max, false) } + if group.hasBannedPermission(.banSendText) { + banSendText = (Int32.max, false) + } if group.hasBannedPermission(.banSendPolls) { canSendPolls = false } } - var availableButtons: [AttachmentButtonType] = [.gallery, .file, .location, .contact] + var availableButtons: [AttachmentButtonType] = [.gallery, .file] + if banSendText == nil { + availableButtons.append(.location) + availableButtons.append(.contact) + } if canSendPolls { - availableButtons.insert(.poll, at: availableButtons.count - 1) + availableButtons.insert(.poll, at: max(0, availableButtons.count - 1)) } let presentationData = self.presentationData @@ -13402,22 +13416,19 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G switch itemType { case .image: if bannedSendPhotos != nil { - //TODO:localize - strongSelf.present(standardTextAlertController(theme: AlertControllerTheme(presentationData: strongSelf.presentationData), title: nil, text: "Sending photos is not allowed", actions: [TextAlertAction(type: .defaultAction, title: strongSelf.presentationData.strings.Common_OK, action: {})]), in: .window(.root)) + strongSelf.present(standardTextAlertController(theme: AlertControllerTheme(presentationData: strongSelf.presentationData), title: nil, text: strongSelf.restrictedSendingContentsText(), actions: [TextAlertAction(type: .defaultAction, title: strongSelf.presentationData.strings.Common_OK, action: {})]), in: .window(.root)) return false } case .video: if bannedSendVideos != nil { - //TODO:localize - strongSelf.present(standardTextAlertController(theme: AlertControllerTheme(presentationData: strongSelf.presentationData), title: nil, text: "Sending videos is not allowed", actions: [TextAlertAction(type: .defaultAction, title: strongSelf.presentationData.strings.Common_OK, action: {})]), in: .window(.root)) + strongSelf.present(standardTextAlertController(theme: AlertControllerTheme(presentationData: strongSelf.presentationData), title: nil, text: strongSelf.restrictedSendingContentsText(), actions: [TextAlertAction(type: .defaultAction, title: strongSelf.presentationData.strings.Common_OK, action: {})]), in: .window(.root)) return false } case .gif: if bannedSendGifs != nil { - //TODO:localize - strongSelf.present(standardTextAlertController(theme: AlertControllerTheme(presentationData: strongSelf.presentationData), title: nil, text: "Sending gifs is not allowed", actions: [TextAlertAction(type: .defaultAction, title: strongSelf.presentationData.strings.Common_OK, action: {})]), in: .window(.root)) + strongSelf.present(standardTextAlertController(theme: AlertControllerTheme(presentationData: strongSelf.presentationData), title: nil, text: strongSelf.restrictedSendingContentsText(), actions: [TextAlertAction(type: .defaultAction, title: strongSelf.presentationData.strings.Common_OK, action: {})]), in: .window(.root)) return false } @@ -17982,6 +17993,83 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G let transition = ContainedViewLayoutTransition.animated(duration: 0.45, curve: .customSpring(damping: 180.0, initialVelocity: 0.0)) transition.updateTransformScale(node: self.chatDisplayNode.historyNodeContainer, scale: scale) } + + func restrictedSendingContentsText() -> String { + //TODO:localize + guard let peer = self.presentationInterfaceState.renderedPeer?.peer else { + return "Sending messages is disabled in this chat" + } + + var itemList: [String] = [] + + var flags: TelegramChatBannedRightsFlags = [] + if let channel = peer as? TelegramChannel { + if let bannedRights = channel.bannedRights { + flags = bannedRights.flags + } + } else if let group = peer as? TelegramGroup { + if let bannedRights = group.defaultBannedRights { + flags = bannedRights.flags + } + } + + let order: [TelegramChatBannedRightsFlags] = [ + .banSendText, + .banSendPhotos, + .banSendVideos, + .banSendVoice, + .banSendInstantVideos, + .banSendFiles, + .banSendMusic, + .banSendStickers + ] + + for right in order { + if !flags.contains(right) { + var title: String? + switch right { + case .banSendText: + title = "text messages" + case .banSendPhotos: + title = "photos" + case .banSendVideos: + title = "videos" + case .banSendVoice: + title = "voice messages" + case .banSendInstantVideos: + title = "video messages" + case .banSendFiles: + title = "files" + case .banSendMusic: + title = "music" + case .banSendStickers: + title = "Stickers & GIFs" + default: + break + } + if let title { + itemList.append(title) + } + } + } + + if itemList.isEmpty { + return "Sending messages is disabled in this chat" + } + + var itemListString = "" + for i in 0 ..< itemList.count { + if i != 0 { + itemListString.append(", ") + } + if i == itemList.count - 1 && i != 0 { + itemListString.append("and ") + } + itemListString.append(itemList[i]) + } + + return "The admins of this group only allow to send \(itemListString)." + } } private final class ContextControllerContentSourceImpl: ContextControllerContentSource { diff --git a/submodules/TelegramUI/Sources/ChatControllerNode.swift b/submodules/TelegramUI/Sources/ChatControllerNode.swift index cb6c037705..1d91c2b4fe 100644 --- a/submodules/TelegramUI/Sources/ChatControllerNode.swift +++ b/submodules/TelegramUI/Sources/ChatControllerNode.swift @@ -615,7 +615,7 @@ class ChatControllerNode: ASDisplayNode, UIScrollViewDelegate { self.addSubnode(self.messageTransitionNode) self.contentContainerNode.addSubnode(self.navigateButtons) - self.contentContainerNode.addSubnode(self.presentationContextMarker) + self.addSubnode(self.presentationContextMarker) self.contentContainerNode.addSubnode(self.contentDimNode) self.navigationBar?.additionalContentNode.addSubnode(self.titleAccessoryPanelContainer)