diff --git a/Telegram/Telegram-iOS/en.lproj/Localizable.strings b/Telegram/Telegram-iOS/en.lproj/Localizable.strings index 558e28b190..0a78104a7d 100644 --- a/Telegram/Telegram-iOS/en.lproj/Localizable.strings +++ b/Telegram/Telegram-iOS/en.lproj/Localizable.strings @@ -8729,3 +8729,6 @@ Sorry for the inconvenience."; "ChatList.PremiumAnnualDiscountText" = "Sign up for the annual payment plan for Telegram Premium now to get the discount."; "ChatList.PremiumAnnualUpgradeTitle" = "Save on your subscription up to %@"; "ChatList.PremiumAnnualUpgradeText" = "Upgrade to the annual payment plan for Telegram Premium to enjoy the discount."; + +"Premium.Emoji.Description" = "Unlock this emoji and many more by subscribing to Telegram Premium."; +"Premium.Emoji.Proceed" = "Unlock Premium Emoji"; diff --git a/submodules/AccountContext/Sources/AccountContext.swift b/submodules/AccountContext/Sources/AccountContext.swift index 7948b075a2..4bdf68a2b0 100644 --- a/submodules/AccountContext/Sources/AccountContext.swift +++ b/submodules/AccountContext/Sources/AccountContext.swift @@ -294,7 +294,7 @@ public enum ResolvedUrl { case joinVoiceChat(PeerId, String?) case importStickers case startAttach(peerId: PeerId, payload: String?, choose: ResolvedBotChoosePeerTypes?) - case invoice(slug: String, invoice: TelegramMediaInvoice) + case invoice(slug: String, invoice: TelegramMediaInvoice?) case premiumOffer(reference: String?) } diff --git a/submodules/ChatListUI/Sources/ChatListController.swift b/submodules/ChatListUI/Sources/ChatListController.swift index 0bb0ea591e..033a104006 100644 --- a/submodules/ChatListUI/Sources/ChatListController.swift +++ b/submodules/ChatListUI/Sources/ChatListController.swift @@ -792,6 +792,7 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController isStatusSelection: true, isReactionSelection: false, isEmojiSelection: false, + hasTrending: false, topReactionItems: [], areUnicodeEmojiEnabled: false, areCustomEmojiEnabled: true, diff --git a/submodules/ComponentFlow/Source/Base/Transition.swift b/submodules/ComponentFlow/Source/Base/Transition.swift index a6679fc7ba..5bf0b3954f 100644 --- a/submodules/ComponentFlow/Source/Base/Transition.swift +++ b/submodules/ComponentFlow/Source/Base/Transition.swift @@ -724,4 +724,60 @@ public struct Transition { ) } } + + public func setTintColor(view: UIView, color: UIColor, completion: ((Bool) -> Void)? = nil) { + if let current = view.tintColor, current == color { + completion?(true) + return + } + + switch self.animation { + case .none: + view.tintColor = color + completion?(true) + case let .curve(duration, curve): + let previousColor: UIColor = view.tintColor ?? UIColor.clear + view.tintColor = color + + view.layer.animate( + from: previousColor, + to: color.cgColor, + keyPath: "contentsMultiplyColor", + duration: duration, + delay: 0.0, + curve: curve, + removeOnCompletion: true, + additive: false, + completion: completion + ) + } + } + + public func setTintColor(layer: CALayer, color: UIColor, completion: ((Bool) -> Void)? = nil) { + if let current = layer.layerTintColor, current == color.cgColor { + completion?(true) + return + } + + switch self.animation { + case .none: + layer.layerTintColor = color.cgColor + completion?(true) + case let .curve(duration, curve): + let previousColor: CGColor = layer.layerTintColor ?? UIColor.clear.cgColor + layer.layerTintColor = color.cgColor + + layer.animate( + from: previousColor, + to: color.cgColor, + keyPath: "contentsMultiplyColor", + duration: duration, + delay: 0.0, + curve: curve, + removeOnCompletion: true, + additive: false, + completion: completion + ) + } + } } diff --git a/submodules/Components/PagerComponent/Sources/PagerComponent.swift b/submodules/Components/PagerComponent/Sources/PagerComponent.swift index 0c098a7635..8d6e0c57fa 100644 --- a/submodules/Components/PagerComponent/Sources/PagerComponent.swift +++ b/submodules/Components/PagerComponent/Sources/PagerComponent.swift @@ -154,10 +154,12 @@ public enum PagerComponentPanelHideBehavior { public final class PagerComponentContentIcon: Equatable { public let id: AnyHashable public let imageName: String + public let title: String - public init(id: AnyHashable, imageName: String) { + public init(id: AnyHashable, imageName: String, title: String) { self.id = id self.imageName = imageName + self.title = title } public static func ==(lhs: PagerComponentContentIcon, rhs: PagerComponentContentIcon) -> Bool { @@ -170,6 +172,9 @@ public final class PagerComponentContentIcon: Equatable { if lhs.imageName != rhs.imageName { return false } + if lhs.title != rhs.title { + return false + } return true } } @@ -301,6 +306,7 @@ public final class PagerComponent 0.0 { + nextIndex = max(0, centralIndex - 1) + } else { + nextIndex = min(component.contents.count - 1, centralIndex + 1) + } + + paneTransitionGestureState.nextIndex = nextIndex + self.paneTransitionGestureState = paneTransitionGestureState self.state?.updated(transition: .immediate) } @@ -422,8 +437,26 @@ public final class PagerComponent 0.0 { + nextIndex = max(0, index - 1) + } else { + nextIndex = min(component.contents.count - 1, index + 1) + } + if let nextIndexValue = paneTransitionGestureState.nextIndex { + nextIndex = nextIndexValue + } + + let nextId = component.contents[nextIndex].id + + var centralPanelFraction: CGFloat = 1.0 + if !component.contentTopPanels.contains(where: { $0.id == centralId }) { + centralPanelFraction = 0.0 + } + var nextPanelFraction: CGFloat = 1.0 + if !component.contentTopPanels.contains(where: { $0.id == nextId }) { + nextPanelFraction = 0.0 + } + + topPanelVisibility = centralPanelFraction * (1.0 - abs(paneTransitionGestureState.fraction)) + nextPanelFraction * abs(paneTransitionGestureState.fraction) + } else { + if !component.contentTopPanels.contains(where: { $0.id == centralId }) { + topPanelVisibility = 0.0 + } + } + } + var topPanelHeight: CGFloat = 0.0 if let topPanel = component.topPanel { + let effectiveTopPanelOffsetFraction = scrollingPanelOffsetFraction + let topPanelView: ComponentHostView> var topPanelTransition = panelStateTransition if let current = self.topPanelView { @@ -511,6 +578,11 @@ public final class PagerComponent 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 { @@ -291,9 +328,23 @@ private func channelBannedMemberControllerEntries(presentationData: Presentation entries.append(.rightsHeader(presentationData.theme, presentationData.strings.GroupPermission_SectionTitle)) var index = 0 - for (right, _) in allGroupPermissionList(peer: .channel(channel)) { + for (right, _) in allGroupPermissionList(peer: .channel(channel), expandMedia: false) { 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 } @@ -337,9 +388,23 @@ private func channelBannedMemberControllerEntries(presentationData: Presentation entries.append(.rightsHeader(presentationData.theme, presentationData.strings.GroupPermission_SectionTitle)) var index = 0 - for (right, _) in allGroupPermissionList(peer: .legacyGroup(group)) { + for (right, _) in allGroupPermissionList(peer: .legacyGroup(group), expandMedia: false) { 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 } @@ -406,7 +471,7 @@ public func channelBannedMemberController(context: AccountContext, updatedPresen effectiveRightsFlags = effectiveRightsFlags.subtracting(groupPermissionDependencies(rights)) } else { effectiveRightsFlags.insert(rights) - for (right, _) in allGroupPermissionList(peer: EnginePeer(peer)) { + for (right, _) in allGroupPermissionList(peer: EnginePeer(peer), expandMedia: false) { if groupPermissionDependencies(right).contains(rights) { effectiveRightsFlags.insert(right) } @@ -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/ChannelMembersSearchContainerNode.swift b/submodules/PeerInfoUI/Sources/ChannelMembersSearchContainerNode.swift index 029fbf5497..85cdc75b53 100644 --- a/submodules/PeerInfoUI/Sources/ChannelMembersSearchContainerNode.swift +++ b/submodules/PeerInfoUI/Sources/ChannelMembersSearchContainerNode.swift @@ -826,7 +826,7 @@ public final class ChannelMembersSearchContainerNode: SearchDisplayControllerCon case let .member(_, _, _, banInfo, _): if let banInfo = banInfo { var exceptionsString = "" - for (rights, _) in allGroupPermissionList(peer: .channel(channel)) { + for (rights, _) in allGroupPermissionList(peer: .channel(channel), expandMedia: true) { if banInfo.rights.flags.contains(rights) { if !exceptionsString.isEmpty { exceptionsString.append(", ") @@ -1086,7 +1086,7 @@ public final class ChannelMembersSearchContainerNode: SearchDisplayControllerCon case let .member(_, _, _, banInfo, _): if let banInfo = banInfo { var exceptionsString = "" - for (rights, _) in allGroupPermissionList(peer: .legacyGroup(group)) { + for (rights, _) in allGroupPermissionList(peer: .legacyGroup(group), expandMedia: true) { if banInfo.rights.flags.contains(rights) { if !exceptionsString.isEmpty { exceptionsString.append(", ") diff --git a/submodules/PeerInfoUI/Sources/ChannelPermissionsController.swift b/submodules/PeerInfoUI/Sources/ChannelPermissionsController.swift index afe3d23aa2..10bd87f989 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 { @@ -420,6 +422,18 @@ func compactStringForGroupPermission(strings: PresentationStrings, right: Telegr return strings.GroupPermission_NoSendMessages } else if right.contains(.banSendMedia) { return strings.GroupPermission_NoSendMedia + } else if right.contains(.banSendPhotos) { + return "no photos" + } else if right.contains(.banSendVideos) { + return "no videos" + } else if right.contains(.banSendMusic) { + return "no music" + } else if right.contains(.banSendFiles) { + return "no files" + } else if right.contains(.banSendVoice) { + return "no voice messages" + } else if right.contains(.banSendInstantVideos) { + return "no video messages" } else if right.contains(.banSendGifs) { return strings.GroupPermission_NoSendGifs } else if right.contains(.banEmbedLinks) { @@ -457,9 +471,10 @@ private let internal_allPossibleGroupPermissionList: [(TelegramChatBannedRightsF (.banChangeInfo, .changeInfo) ] -public func allGroupPermissionList(peer: EnginePeer) -> [(TelegramChatBannedRightsFlags, TelegramChannelPermission)] { +public func allGroupPermissionList(peer: EnginePeer, expandMedia: Bool) -> [(TelegramChatBannedRightsFlags, TelegramChannelPermission)] { + var result: [(TelegramChatBannedRightsFlags, TelegramChannelPermission)] if case let .channel(channel) = peer, channel.flags.contains(.isForum) { - return [ + result = [ (.banSendText, .banMembers), (.banSendMedia, .banMembers), (.banAddMembers, .banMembers), @@ -468,7 +483,7 @@ public func allGroupPermissionList(peer: EnginePeer) -> [(TelegramChatBannedRigh (.banChangeInfo, .changeInfo) ] } else { - return [ + result = [ (.banSendText, .banMembers), (.banSendMedia, .banMembers), (.banAddMembers, .banMembers), @@ -476,6 +491,16 @@ public func allGroupPermissionList(peer: EnginePeer) -> [(TelegramChatBannedRigh (.banChangeInfo, .changeInfo) ] } + + if expandMedia, let index = result.firstIndex(where: { $0.0 == .banSendMedia }) { + result.remove(at: index) + + for (subRight, permission) in banSendMediaSubList().reversed() { + result.insert((subRight, permission), at: index) + } + } + + return result } public func banSendMediaSubList() -> [(TelegramChatBannedRightsFlags, TelegramChannelPermission)] { @@ -532,8 +557,8 @@ 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 + for (rights, correspondingAdminRight) in allGroupPermissionList(peer: .channel(channel), expandMedia: false) { + var enabled = true if channel.addressName != nil && publicGroupRestrictedPermissions.contains(rights) { enabled = false } @@ -552,7 +577,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)) } } @@ -592,11 +619,13 @@ private func channelPermissionsControllerEntries(context: AccountContext, presen entries.append(.permissionsHeader(presentationData.theme, presentationData.strings.GroupInfo_Permissions_SectionTitle)) var rightIndex: Int = 0 - for (rights, _) in allGroupPermissionList(peer: .legacyGroup(group)) { + for (rights, _) in allGroupPermissionList(peer: .legacyGroup(group), expandMedia: false) { 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)) } } @@ -707,7 +736,7 @@ public func channelPermissionsController(context: AccountContext, updatedPresent } } else { effectiveRightsFlags.insert(rights) - for (right, _) in allGroupPermissionList(peer: .channel(channel)) { + for (right, _) in allGroupPermissionList(peer: .channel(channel), expandMedia: false) { if groupPermissionDependencies(right).contains(rights) { effectiveRightsFlags.insert(right) } @@ -715,7 +744,7 @@ public func channelPermissionsController(context: AccountContext, updatedPresent for item in banSendMediaSubList() { effectiveRightsFlags.insert(item.0) - for (right, _) in allGroupPermissionList(peer: .channel(channel)) { + for (right, _) in allGroupPermissionList(peer: .channel(channel), expandMedia: false) { if groupPermissionDependencies(right).contains(item.0) { effectiveRightsFlags.insert(right) } @@ -728,7 +757,7 @@ public func channelPermissionsController(context: AccountContext, updatedPresent effectiveRightsFlags = effectiveRightsFlags.subtracting(groupPermissionDependencies(rights)) } else { effectiveRightsFlags.insert(rights) - for (right, _) in allGroupPermissionList(peer: .channel(channel)) { + for (right, _) in allGroupPermissionList(peer: .channel(channel), expandMedia: false) { if groupPermissionDependencies(right).contains(rights) { effectiveRightsFlags.insert(right) } @@ -764,7 +793,7 @@ public func channelPermissionsController(context: AccountContext, updatedPresent effectiveRightsFlags = effectiveRightsFlags.subtracting(groupPermissionDependencies(rights)) } else { effectiveRightsFlags.insert(rights) - for (right, _) in allGroupPermissionList(peer: .legacyGroup(group)) { + for (right, _) in allGroupPermissionList(peer: .legacyGroup(group), expandMedia: false) { if groupPermissionDependencies(right).contains(rights) { effectiveRightsFlags.insert(right) } @@ -793,7 +822,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 { @@ -866,7 +895,7 @@ public func channelPermissionsController(context: AccountContext, updatedPresent guard let channel = view.peers[view.peerId] as? TelegramChannel else { return } - for (listRight, permission) in allGroupPermissionList(peer: .channel(channel)) { + for (listRight, permission) in allGroupPermissionList(peer: .channel(channel), expandMedia: false) { if listRight == right { let text: String let presentationData = updatedPresentationData?.initial ?? context.sharedContext.currentPresentationData.with { $0 } diff --git a/submodules/ReactionSelectionNode/Sources/ReactionContextNode.swift b/submodules/ReactionSelectionNode/Sources/ReactionContextNode.swift index dca2e17162..00a8343191 100644 --- a/submodules/ReactionSelectionNode/Sources/ReactionContextNode.swift +++ b/submodules/ReactionSelectionNode/Sources/ReactionContextNode.swift @@ -906,7 +906,7 @@ public final class ReactionContextNode: ASDisplayNode, UIScrollViewDelegate { expandItemSize = 30.0 expandTintOffset = 0.0 } - let baseNextFrame = CGRect(origin: CGPoint(x: self.scrollNode.view.bounds.width - expandItemSize - 9.0, y: containerHeight - contentHeight + floor((contentHeight - expandItemSize) / 2.0) + (self.isExpanded ? (46.0) : 0.0)), size: CGSize(width: expandItemSize, height: expandItemSize + self.extensionDistance)) + let baseNextFrame = CGRect(origin: CGPoint(x: self.scrollNode.view.bounds.width - expandItemSize - 9.0, y: containerHeight - contentHeight + floor((contentHeight - expandItemSize) / 2.0) + (self.isExpanded ? (46.0 + 54.0 - 4.0) : 0.0)), size: CGSize(width: expandItemSize, height: expandItemSize + self.extensionDistance)) transition.updateFrame(view: expandItemView, frame: baseNextFrame) transition.updateFrame(view: expandItemView.tintView, frame: baseNextFrame.offsetBy(dx: 0.0, dy: expandTintOffset)) @@ -1007,7 +1007,7 @@ public final class ReactionContextNode: ASDisplayNode, UIScrollViewDelegate { visibleItemCount: itemCount ) - var scrollFrame = CGRect(origin: CGPoint(x: 0.0, y: self.isExpanded ? (46.0) : 0.0), size: actualBackgroundFrame.size) + var scrollFrame = CGRect(origin: CGPoint(x: 0.0, y: self.isExpanded ? (46.0 + 54.0 - 4.0) : 0.0), size: actualBackgroundFrame.size) scrollFrame.origin.y += floorToScreenPixels(self.extensionDistance / 2.0) transition.updateFrame(node: self.contentContainer, frame: visualBackgroundFrame, beginWithCurrentState: true) @@ -1107,7 +1107,7 @@ public final class ReactionContextNode: ASDisplayNode, UIScrollViewDelegate { if let mirrorContentClippingView = emojiView.mirrorContentClippingView { mirrorContentClippingView.clipsToBounds = false - Transition(transition).animateBoundsOrigin(view: mirrorContentClippingView, from: CGPoint(x: 0.0, y: 46.0), to: CGPoint(), additive: true, completion: { [weak mirrorContentClippingView] _ in + Transition(transition).animateBoundsOrigin(view: mirrorContentClippingView, from: CGPoint(x: 0.0, y: 46.0 + 54.0 - 4.0), to: CGPoint(), additive: true, completion: { [weak mirrorContentClippingView] _ in mirrorContentClippingView?.clipsToBounds = true }) } @@ -1138,7 +1138,7 @@ public final class ReactionContextNode: ASDisplayNode, UIScrollViewDelegate { componentTransition.setFrame(view: componentView, frame: CGRect(origin: componentFrame.origin, size: CGSize(width: componentFrame.width, height: componentFrame.height))) if animateIn { - transition.animatePositionAdditive(layer: componentView.layer, offset: CGPoint(x: 0.0, y: -(46.0) + floorToScreenPixels(self.animateFromExtensionDistance / 2.0))) + transition.animatePositionAdditive(layer: componentView.layer, offset: CGPoint(x: 0.0, y: -(46.0 + 54.0 - 4.0) + floorToScreenPixels(self.animateFromExtensionDistance / 2.0))) } } } @@ -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/DataAndStorageSettingsController.swift b/submodules/SettingsUI/Sources/Data and Storage/DataAndStorageSettingsController.swift index 977471ec34..01a3ed0ebd 100644 --- a/submodules/SettingsUI/Sources/Data and Storage/DataAndStorageSettingsController.swift +++ b/submodules/SettingsUI/Sources/Data and Storage/DataAndStorageSettingsController.swift @@ -785,9 +785,23 @@ public func dataAndStorageController(context: AccountContext, focusOnItemTag: Da return storageUsageExceptionsScreen(context: context, category: category) })) }, openNetworkUsage: { - let _ = (accountNetworkUsageStats(account: context.account, reset: []) + let mediaAutoDownloadSettings = context.sharedContext.accountManager.sharedData(keys: [ApplicationSpecificSharedDataKeys.automaticMediaDownloadSettings]) + |> map { sharedData -> MediaAutoDownloadSettings in + var automaticMediaDownloadSettings: MediaAutoDownloadSettings + if let value = sharedData.entries[ApplicationSpecificSharedDataKeys.automaticMediaDownloadSettings]?.get(MediaAutoDownloadSettings.self) { + automaticMediaDownloadSettings = value + } else { + automaticMediaDownloadSettings = .defaultSettings + } + return automaticMediaDownloadSettings + } + + let _ = (combineLatest( + accountNetworkUsageStats(account: context.account, reset: []), + mediaAutoDownloadSettings + ) |> take(1) - |> deliverOnMainQueue).start(next: { stats in + |> deliverOnMainQueue).start(next: { stats, mediaAutoDownloadSettings in var stats = stats if stats.resetWifiTimestamp == 0 { @@ -797,7 +811,9 @@ public func dataAndStorageController(context: AccountContext, focusOnItemTag: Da } } - pushControllerImpl?(DataUsageScreen(context: context, stats: stats)) + pushControllerImpl?(DataUsageScreen(context: context, stats: stats, mediaAutoDownloadSettings: mediaAutoDownloadSettings, makeAutodownloadSettingsController: { isCellular in + return autodownloadMediaConnectionTypeController(context: context, connectionType: isCellular ? .cellular : .wifi) + })) }) }, openProxy: { pushControllerImpl?(proxySettingsController(context: context)) 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/SettingsUI/Sources/Reactions/QuickReactionSetupController.swift b/submodules/SettingsUI/Sources/Reactions/QuickReactionSetupController.swift index 364207321b..6d9eb02631 100644 --- a/submodules/SettingsUI/Sources/Reactions/QuickReactionSetupController.swift +++ b/submodules/SettingsUI/Sources/Reactions/QuickReactionSetupController.swift @@ -348,6 +348,7 @@ public func quickReactionSetupController( isStatusSelection: false, isReactionSelection: true, isEmojiSelection: false, + hasTrending: false, isQuickReactionSelection: true, topReactionItems: [], areUnicodeEmojiEnabled: false, diff --git a/submodules/StickerPeekUI/Sources/StickerPreviewPeekContent.swift b/submodules/StickerPeekUI/Sources/StickerPreviewPeekContent.swift index e629c476d9..26a569fe15 100644 --- a/submodules/StickerPeekUI/Sources/StickerPreviewPeekContent.swift +++ b/submodules/StickerPeekUI/Sources/StickerPreviewPeekContent.swift @@ -73,7 +73,9 @@ public final class StickerPreviewPeekContent: PeekControllerContent { public func fullScreenAccessoryNode(blurView: UIVisualEffectView) -> (PeekControllerAccessoryNode & ASDisplayNode)? { if self.isLocked { - return PremiumStickerPackAccessoryNode(theme: self.theme, strings: self.strings, proceed: self.openPremiumIntro) + let isEmoji = self.item.file.isCustomEmoji + + return PremiumStickerPackAccessoryNode(theme: self.theme, strings: self.strings, isEmoji: isEmoji, proceed: self.openPremiumIntro) } else { return nil } @@ -266,17 +268,17 @@ final class PremiumStickerPackAccessoryNode: SparseNode, PeekControllerAccessory let proceedButton: SolidRoundedButtonNode let cancelButton: HighlightableButtonNode - init(theme: PresentationTheme, strings: PresentationStrings, proceed: @escaping () -> Void) { + init(theme: PresentationTheme, strings: PresentationStrings, isEmoji: Bool, proceed: @escaping () -> Void) { self.proceed = proceed self.textNode = ImmediateTextNode() self.textNode.displaysAsynchronously = false self.textNode.textAlignment = .center self.textNode.maximumNumberOfLines = 0 - self.textNode.attributedText = NSAttributedString(string: strings.Premium_Stickers_Description, font: Font.regular(17.0), textColor: theme.actionSheet.secondaryTextColor) + self.textNode.attributedText = NSAttributedString(string: isEmoji ? strings.Premium_Stickers_Description : strings.Premium_Stickers_Description, font: Font.regular(17.0), textColor: theme.actionSheet.secondaryTextColor) self.textNode.lineSpacing = 0.1 - self.proceedButton = SolidRoundedButtonNode(title: strings.Premium_Stickers_Proceed, theme: SolidRoundedButtonTheme( + self.proceedButton = SolidRoundedButtonNode(title: isEmoji ? strings.Premium_Emoji_Proceed: strings.Premium_Stickers_Proceed, theme: SolidRoundedButtonTheme( backgroundColor: .white, backgroundColors: [ UIColor(rgb: 0x0077ff), 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/Network/FetchedMediaResource.swift b/submodules/TelegramCore/Sources/Network/FetchedMediaResource.swift index 776e482182..84b2b2d3e5 100644 --- a/submodules/TelegramCore/Sources/Network/FetchedMediaResource.swift +++ b/submodules/TelegramCore/Sources/Network/FetchedMediaResource.swift @@ -104,7 +104,7 @@ public func fetchedMediaResource( if let ranges = ranges { let signals = ranges.map { (range, priority) -> Signal in return mediaBox.fetchedResourceData(reference.resource, in: range, priority: priority, parameters: MediaResourceFetchParameters( - tag: TelegramMediaResourceFetchTag(statsCategory: statsCategory), + tag: TelegramMediaResourceFetchTag(statsCategory: statsCategory, userContentType: userContentType), info: TelegramCloudMediaResourceFetchInfo(reference: reference, preferBackgroundReferenceRevalidation: preferBackgroundReferenceRevalidation, continueInBackground: continueInBackground), location: location, contentType: userContentType, @@ -117,7 +117,7 @@ public func fetchedMediaResource( |> then(.single(.local)) } else { return mediaBox.fetchedResource(reference.resource, parameters: MediaResourceFetchParameters( - tag: TelegramMediaResourceFetchTag(statsCategory: statsCategory), + tag: TelegramMediaResourceFetchTag(statsCategory: statsCategory, userContentType: userContentType), info: TelegramCloudMediaResourceFetchInfo(reference: reference, preferBackgroundReferenceRevalidation: preferBackgroundReferenceRevalidation, continueInBackground: continueInBackground), location: location, contentType: userContentType, diff --git a/submodules/TelegramCore/Sources/Network/Network.swift b/submodules/TelegramCore/Sources/Network/Network.swift index 60c1dee9ba..c8d553c96b 100644 --- a/submodules/TelegramCore/Sources/Network/Network.swift +++ b/submodules/TelegramCore/Sources/Network/Network.swift @@ -186,6 +186,10 @@ private struct UsageCalculationTag { return 4 * 4 + self.connection.rawValue * 2 + self.direction.rawValue * 1 case .call: return 5 * 4 + self.connection.rawValue * 2 + self.direction.rawValue * 1 + case .stickers: + return 6 * 4 + self.connection.rawValue * 2 + self.direction.rawValue * 1 + case .voiceMessages: + return 7 * 4 + self.connection.rawValue * 2 + self.direction.rawValue * 1 } } } @@ -311,7 +315,17 @@ func networkUsageStats(basePath: String, reset: ResetNetworkUsageStats) -> Signa UsageCalculationTag(connection: .cellular, direction: .incoming, category: .call), UsageCalculationTag(connection: .cellular, direction: .outgoing, category: .call), UsageCalculationTag(connection: .wifi, direction: .incoming, category: .call), - UsageCalculationTag(connection: .wifi, direction: .outgoing, category: .call) + UsageCalculationTag(connection: .wifi, direction: .outgoing, category: .call), + + UsageCalculationTag(connection: .cellular, direction: .incoming, category: .stickers), + UsageCalculationTag(connection: .cellular, direction: .outgoing, category: .stickers), + UsageCalculationTag(connection: .wifi, direction: .incoming, category: .stickers), + UsageCalculationTag(connection: .wifi, direction: .outgoing, category: .stickers), + + UsageCalculationTag(connection: .cellular, direction: .incoming, category: .voiceMessages), + UsageCalculationTag(connection: .cellular, direction: .outgoing, category: .voiceMessages), + UsageCalculationTag(connection: .wifi, direction: .incoming, category: .voiceMessages), + UsageCalculationTag(connection: .wifi, direction: .outgoing, category: .voiceMessages) ] var keys: [NSNumber] = rawKeys.map { $0.key as NSNumber } @@ -386,18 +400,18 @@ func networkUsageStats(basePath: String, reset: ResetNetworkUsageStats) -> Signa outgoing: dict[UsageCalculationTag(connection: .wifi, direction: .outgoing, category: .call).key]!)), sticker: NetworkUsageStatsConnectionsEntry( cellular: NetworkUsageStatsDirectionsEntry( - incoming: 0, - outgoing: 0), + incoming: dict[UsageCalculationTag(connection: .cellular, direction: .incoming, category: .stickers).key]!, + outgoing: dict[UsageCalculationTag(connection: .cellular, direction: .outgoing, category: .stickers).key]!), wifi: NetworkUsageStatsDirectionsEntry( - incoming: 0, - outgoing: 0)), + incoming: dict[UsageCalculationTag(connection: .wifi, direction: .incoming, category: .stickers).key]!, + outgoing: dict[UsageCalculationTag(connection: .wifi, direction: .outgoing, category: .stickers).key]!)), voiceMessage: NetworkUsageStatsConnectionsEntry( cellular: NetworkUsageStatsDirectionsEntry( - incoming: 0, - outgoing: 0), + incoming: dict[UsageCalculationTag(connection: .cellular, direction: .incoming, category: .voiceMessages).key]!, + outgoing: dict[UsageCalculationTag(connection: .cellular, direction: .outgoing, category: .voiceMessages).key]!), wifi: NetworkUsageStatsDirectionsEntry( - incoming: 0, - outgoing: 0)), + incoming: dict[UsageCalculationTag(connection: .wifi, direction: .incoming, category: .voiceMessages).key]!, + outgoing: dict[UsageCalculationTag(connection: .wifi, direction: .outgoing, category: .voiceMessages).key]!)), resetWifiTimestamp: Int32(dict[UsageCalculationResetKey.wifi.rawValue]!), resetCellularTimestamp: Int32(dict[UsageCalculationResetKey.cellular.rawValue]!) )) diff --git a/submodules/TelegramCore/Sources/PendingMessages/PendingMessageUploadedContent.swift b/submodules/TelegramCore/Sources/PendingMessages/PendingMessageUploadedContent.swift index 060e5dd801..e9d3f5659a 100644 --- a/submodules/TelegramCore/Sources/PendingMessages/PendingMessageUploadedContent.swift +++ b/submodules/TelegramCore/Sources/PendingMessages/PendingMessageUploadedContent.swift @@ -417,7 +417,7 @@ private func uploadedMediaImageContent(network: Network, postbox: Postbox, trans } else { imageReference = .standalone(media: transformedImage) } - return multipartUpload(network: network, postbox: postbox, source: .resource(imageReference.resourceReference(largestRepresentation.resource)), encrypt: peerId.namespace == Namespaces.Peer.SecretChat, tag: TelegramMediaResourceFetchTag(statsCategory: .image), hintFileSize: nil, hintFileIsLarge: false, forceNoBigParts: false) + return multipartUpload(network: network, postbox: postbox, source: .resource(imageReference.resourceReference(largestRepresentation.resource)), encrypt: peerId.namespace == Namespaces.Peer.SecretChat, tag: TelegramMediaResourceFetchTag(statsCategory: .image, userContentType: .image), hintFileSize: nil, hintFileIsLarge: false, forceNoBigParts: false) |> mapError { _ -> PendingMessageUploadError in return .generic } |> mapToSignal { next -> Signal in switch next { @@ -581,7 +581,7 @@ private enum UploadedMediaFileAndThumbnail { } private func uploadedThumbnail(network: Network, postbox: Postbox, resourceReference: MediaResourceReference) -> Signal { - return multipartUpload(network: network, postbox: postbox, source: .resource(resourceReference), encrypt: false, tag: TelegramMediaResourceFetchTag(statsCategory: .image), hintFileSize: nil, hintFileIsLarge: false, forceNoBigParts: false) + return multipartUpload(network: network, postbox: postbox, source: .resource(resourceReference), encrypt: false, tag: TelegramMediaResourceFetchTag(statsCategory: .image, userContentType: .image), hintFileSize: nil, hintFileIsLarge: false, forceNoBigParts: false) |> mapError { _ -> PendingMessageUploadError in return .generic } |> mapToSignal { result -> Signal in switch result { @@ -598,10 +598,18 @@ private func uploadedThumbnail(network: Network, postbox: Postbox, resourceRefer public func statsCategoryForFileWithAttributes(_ attributes: [TelegramMediaFileAttribute]) -> MediaResourceStatsCategory { for attribute in attributes { switch attribute { - case .Audio: - return .audio - case .Video: - return .video + case let .Audio(isVoice, _, _, _, _): + if isVoice { + return .voiceMessages + } else { + return .audio + } + case let .Video(_, _, flags): + if flags.contains(TelegramMediaVideoFlags.instantRoundVideo) { + return .voiceMessages + } else { + return .video + } default: break } @@ -659,7 +667,7 @@ private func uploadedMediaFileContent(network: Network, postbox: Postbox, auxili } else { fileReference = .standalone(media: file) } - let upload = messageMediaPreuploadManager.upload(network: network, postbox: postbox, source: .resource(fileReference.resourceReference(file.resource)), encrypt: peerId.namespace == Namespaces.Peer.SecretChat, tag: TelegramMediaResourceFetchTag(statsCategory: statsCategoryForFileWithAttributes(file.attributes)), hintFileSize: hintSize, hintFileIsLarge: hintFileIsLarge) + let upload = messageMediaPreuploadManager.upload(network: network, postbox: postbox, source: .resource(fileReference.resourceReference(file.resource)), encrypt: peerId.namespace == Namespaces.Peer.SecretChat, tag: TelegramMediaResourceFetchTag(statsCategory: statsCategoryForFileWithAttributes(file.attributes), userContentType: nil), hintFileSize: hintSize, hintFileIsLarge: hintFileIsLarge) |> mapError { _ -> PendingMessageUploadError in return .generic } var alreadyTransformed = false diff --git a/submodules/TelegramCore/Sources/PendingMessages/StandaloneSendMessage.swift b/submodules/TelegramCore/Sources/PendingMessages/StandaloneSendMessage.swift index 4feed08749..51ea2c13e1 100644 --- a/submodules/TelegramCore/Sources/PendingMessages/StandaloneSendMessage.swift +++ b/submodules/TelegramCore/Sources/PendingMessages/StandaloneSendMessage.swift @@ -150,7 +150,7 @@ private enum UploadMediaEvent { } private func uploadedImage(account: Account, data: Data) -> Signal { - return multipartUpload(network: account.network, postbox: account.postbox, source: .data(data), encrypt: false, tag: TelegramMediaResourceFetchTag(statsCategory: .image), hintFileSize: nil, hintFileIsLarge: false, forceNoBigParts: false) + return multipartUpload(network: account.network, postbox: account.postbox, source: .data(data), encrypt: false, tag: TelegramMediaResourceFetchTag(statsCategory: .image, userContentType: .image), hintFileSize: nil, hintFileIsLarge: false, forceNoBigParts: false) |> mapError { _ -> StandaloneSendMessageError in return .generic } |> map { next -> UploadMediaEvent in switch next { @@ -165,7 +165,7 @@ private func uploadedImage(account: Account, data: Data) -> Signal Signal { - return multipartUpload(network: account.network, postbox: account.postbox, source: .data(data), encrypt: false, tag: TelegramMediaResourceFetchTag(statsCategory: statsCategoryForFileWithAttributes(attributes)), hintFileSize: Int64(data.count), hintFileIsLarge: false, forceNoBigParts: false) + return multipartUpload(network: account.network, postbox: account.postbox, source: .data(data), encrypt: false, tag: TelegramMediaResourceFetchTag(statsCategory: statsCategoryForFileWithAttributes(attributes), userContentType: nil), hintFileSize: Int64(data.count), hintFileIsLarge: false, forceNoBigParts: false) |> mapError { _ -> PendingMessageUploadError in return .generic } |> map { next -> UploadMediaEvent in switch next { diff --git a/submodules/TelegramCore/Sources/PendingMessages/StandaloneUploadedMedia.swift b/submodules/TelegramCore/Sources/PendingMessages/StandaloneUploadedMedia.swift index 514cb63210..f030b55ba4 100644 --- a/submodules/TelegramCore/Sources/PendingMessages/StandaloneUploadedMedia.swift +++ b/submodules/TelegramCore/Sources/PendingMessages/StandaloneUploadedMedia.swift @@ -38,7 +38,7 @@ public enum StandaloneUploadMediaEvent { } private func uploadedThumbnail(network: Network, postbox: Postbox, data: Data) -> Signal { - return multipartUpload(network: network, postbox: postbox, source: .data(data), encrypt: false, tag: TelegramMediaResourceFetchTag(statsCategory: .image), hintFileSize: nil, hintFileIsLarge: false, forceNoBigParts: false) + return multipartUpload(network: network, postbox: postbox, source: .data(data), encrypt: false, tag: TelegramMediaResourceFetchTag(statsCategory: .image, userContentType: .image), hintFileSize: nil, hintFileIsLarge: false, forceNoBigParts: false) |> mapError { _ -> StandaloneUploadMediaError in return .generic } |> mapToSignal { result -> Signal in switch result { @@ -53,7 +53,7 @@ private func uploadedThumbnail(network: Network, postbox: Postbox, data: Data) - } public func standaloneUploadedImage(account: Account, peerId: PeerId, text: String, data: Data, thumbnailData: Data? = nil, dimensions: PixelDimensions) -> Signal { - return multipartUpload(network: account.network, postbox: account.postbox, source: .data(data), encrypt: peerId.namespace == Namespaces.Peer.SecretChat, tag: TelegramMediaResourceFetchTag(statsCategory: .image), hintFileSize: nil, hintFileIsLarge: false, forceNoBigParts: false) + return multipartUpload(network: account.network, postbox: account.postbox, source: .data(data), encrypt: peerId.namespace == Namespaces.Peer.SecretChat, tag: TelegramMediaResourceFetchTag(statsCategory: .image, userContentType: .image), hintFileSize: nil, hintFileIsLarge: false, forceNoBigParts: false) |> mapError { _ -> StandaloneUploadMediaError in return .generic } |> mapToSignal { next -> Signal in switch next { @@ -114,7 +114,7 @@ public func standaloneUploadedImage(account: Account, peerId: PeerId, text: Stri } public func standaloneUploadedFile(account: Account, peerId: PeerId, text: String, source: MultipartUploadSource, thumbnailData: Data? = nil, mimeType: String, attributes: [TelegramMediaFileAttribute], hintFileIsLarge: Bool) -> Signal { - let upload = multipartUpload(network: account.network, postbox: account.postbox, source: source, encrypt: peerId.namespace == Namespaces.Peer.SecretChat, tag: TelegramMediaResourceFetchTag(statsCategory: statsCategoryForFileWithAttributes(attributes)), hintFileSize: nil, hintFileIsLarge: hintFileIsLarge, forceNoBigParts: false) + let upload = multipartUpload(network: account.network, postbox: account.postbox, source: source, encrypt: peerId.namespace == Namespaces.Peer.SecretChat, tag: TelegramMediaResourceFetchTag(statsCategory: statsCategoryForFileWithAttributes(attributes), userContentType: nil), hintFileSize: nil, hintFileIsLarge: hintFileIsLarge, forceNoBigParts: false) |> mapError { _ -> StandaloneUploadMediaError in return .generic } let uploadThumbnail: Signal 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_LocalizationListState.swift b/submodules/TelegramCore/Sources/SyncCore/SyncCore_LocalizationListState.swift index 51e470c916..2be90920de 100644 --- a/submodules/TelegramCore/Sources/SyncCore/SyncCore_LocalizationListState.swift +++ b/submodules/TelegramCore/Sources/SyncCore/SyncCore_LocalizationListState.swift @@ -1,6 +1,6 @@ import Postbox -public struct LocalizationListState: Codable { +public struct LocalizationListState: Codable, Equatable { public var availableOfficialLocalizations: [LocalizationInfo] public var availableSavedLocalizations: [LocalizationInfo] 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/Peers/PeerPhotoUpdater.swift b/submodules/TelegramCore/Sources/TelegramEngine/Peers/PeerPhotoUpdater.swift index 9c2aae8649..d8040a2b43 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Peers/PeerPhotoUpdater.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Peers/PeerPhotoUpdater.swift @@ -46,7 +46,7 @@ enum UploadedPeerPhotoDataContent { } func _internal_uploadedPeerPhoto(postbox: Postbox, network: Network, resource: MediaResource) -> Signal { - return multipartUpload(network: network, postbox: postbox, source: .resource(.standalone(resource: resource)), encrypt: false, tag: TelegramMediaResourceFetchTag(statsCategory: .image), hintFileSize: nil, hintFileIsLarge: false, forceNoBigParts: false) + return multipartUpload(network: network, postbox: postbox, source: .resource(.standalone(resource: resource)), encrypt: false, tag: TelegramMediaResourceFetchTag(statsCategory: .image, userContentType: .image), hintFileSize: nil, hintFileIsLarge: false, forceNoBigParts: false) |> map { result -> UploadedPeerPhotoData in return UploadedPeerPhotoData(resource: resource, content: .result(result)) } @@ -57,7 +57,7 @@ func _internal_uploadedPeerPhoto(postbox: Postbox, network: Network, resource: M func _internal_uploadedPeerVideo(postbox: Postbox, network: Network, messageMediaPreuploadManager: MessageMediaPreuploadManager?, resource: MediaResource) -> Signal { if let messageMediaPreuploadManager = messageMediaPreuploadManager { - return messageMediaPreuploadManager.upload(network: network, postbox: postbox, source: .resource(.standalone(resource: resource)), encrypt: false, tag: TelegramMediaResourceFetchTag(statsCategory: .video), hintFileSize: nil, hintFileIsLarge: false) + return messageMediaPreuploadManager.upload(network: network, postbox: postbox, source: .resource(.standalone(resource: resource)), encrypt: false, tag: TelegramMediaResourceFetchTag(statsCategory: .video, userContentType: .video), hintFileSize: nil, hintFileIsLarge: false) |> map { result -> UploadedPeerPhotoData in return UploadedPeerPhotoData(resource: resource, content: .result(result)) } @@ -65,7 +65,7 @@ func _internal_uploadedPeerVideo(postbox: Postbox, network: Network, messageMedi return .single(UploadedPeerPhotoData(resource: resource, content: .error)) } } else { - return multipartUpload(network: network, postbox: postbox, source: .resource(.standalone(resource: resource)), encrypt: false, tag: TelegramMediaResourceFetchTag(statsCategory: .video), hintFileSize: nil, hintFileIsLarge: false, forceNoBigParts: false) + return multipartUpload(network: network, postbox: postbox, source: .resource(.standalone(resource: resource)), encrypt: false, tag: TelegramMediaResourceFetchTag(statsCategory: .video, userContentType: .video), hintFileSize: nil, hintFileIsLarge: false, forceNoBigParts: false) |> map { result -> UploadedPeerPhotoData in return UploadedPeerPhotoData(resource: resource, content: .result(result)) } diff --git a/submodules/TelegramCore/Sources/TelegramEngine/SecureId/UploadSecureIdFile.swift b/submodules/TelegramCore/Sources/TelegramEngine/SecureId/UploadSecureIdFile.swift index 59bc00b993..c8f7c31235 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/SecureId/UploadSecureIdFile.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/SecureId/UploadSecureIdFile.swift @@ -104,7 +104,7 @@ public func uploadSecureIdFile(context: SecureIdAccessContext, postbox: Postbox, return .fail(.generic) } - return multipartUpload(network: network, postbox: postbox, source: .data(encryptedData.data), encrypt: false, tag: TelegramMediaResourceFetchTag(statsCategory: .image), hintFileSize: nil, hintFileIsLarge: false, forceNoBigParts: false) + return multipartUpload(network: network, postbox: postbox, source: .data(encryptedData.data), encrypt: false, tag: TelegramMediaResourceFetchTag(statsCategory: .image, userContentType: .image), hintFileSize: nil, hintFileIsLarge: false, forceNoBigParts: false) |> mapError { _ -> UploadSecureIdFileError in return .generic } diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Stickers/ImportStickers.swift b/submodules/TelegramCore/Sources/TelegramEngine/Stickers/ImportStickers.swift index 90dbd56f70..1c43133c1f 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Stickers/ImportStickers.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Stickers/ImportStickers.swift @@ -24,7 +24,7 @@ private enum UploadedStickerDataContent { } private func uploadedSticker(postbox: Postbox, network: Network, resource: MediaResource) -> Signal { - return multipartUpload(network: network, postbox: postbox, source: .resource(.standalone(resource: resource)), encrypt: false, tag: TelegramMediaResourceFetchTag(statsCategory: .file), hintFileSize: nil, hintFileIsLarge: false, forceNoBigParts: false) + return multipartUpload(network: network, postbox: postbox, source: .resource(.standalone(resource: resource)), encrypt: false, tag: TelegramMediaResourceFetchTag(statsCategory: .stickers, userContentType: .sticker), hintFileSize: nil, hintFileIsLarge: false, forceNoBigParts: false) |> map { result -> UploadedStickerData in return UploadedStickerData(resource: resource, content: .result(result)) } 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/TelegramCore/Sources/Themes.swift b/submodules/TelegramCore/Sources/Themes.swift index 1e1c3b5c31..92d1992607 100644 --- a/submodules/TelegramCore/Sources/Themes.swift +++ b/submodules/TelegramCore/Sources/Themes.swift @@ -204,7 +204,7 @@ private enum UploadedThemeDataContent { } private func uploadedTheme(postbox: Postbox, network: Network, resource: MediaResource) -> Signal { - return multipartUpload(network: network, postbox: postbox, source: .resource(.standalone(resource: resource)), encrypt: false, tag: TelegramMediaResourceFetchTag(statsCategory: .file), hintFileSize: nil, hintFileIsLarge: false, forceNoBigParts: false) + return multipartUpload(network: network, postbox: postbox, source: .resource(.standalone(resource: resource)), encrypt: false, tag: TelegramMediaResourceFetchTag(statsCategory: .file, userContentType: .file), hintFileSize: nil, hintFileIsLarge: false, forceNoBigParts: false) |> map { result -> UploadedThemeData in return UploadedThemeData(content: .result(result)) } @@ -214,7 +214,7 @@ private func uploadedTheme(postbox: Postbox, network: Network, resource: MediaRe } private func uploadedThemeThumbnail(postbox: Postbox, network: Network, data: Data) -> Signal { - return multipartUpload(network: network, postbox: postbox, source: .data(data), encrypt: false, tag: TelegramMediaResourceFetchTag(statsCategory: .image), hintFileSize: nil, hintFileIsLarge: false, forceNoBigParts: false) + return multipartUpload(network: network, postbox: postbox, source: .data(data), encrypt: false, tag: TelegramMediaResourceFetchTag(statsCategory: .image, userContentType: .image), hintFileSize: nil, hintFileIsLarge: false, forceNoBigParts: false) |> map { result -> UploadedThemeData in return UploadedThemeData(content: .result(result)) } diff --git a/submodules/TelegramCore/Sources/Utils/MediaResourceNetworkStatsTag.swift b/submodules/TelegramCore/Sources/Utils/MediaResourceNetworkStatsTag.swift index 8d008623d3..818111988f 100644 --- a/submodules/TelegramCore/Sources/Utils/MediaResourceNetworkStatsTag.swift +++ b/submodules/TelegramCore/Sources/Utils/MediaResourceNetworkStatsTag.swift @@ -7,12 +7,29 @@ public enum MediaResourceStatsCategory { case audio case file case call + case stickers + case voiceMessages } final class TelegramMediaResourceFetchTag: MediaResourceFetchTag { public let statsCategory: MediaResourceStatsCategory - public init(statsCategory: MediaResourceStatsCategory) { - self.statsCategory = statsCategory + public init(statsCategory: MediaResourceStatsCategory, userContentType: MediaResourceUserContentType?) { + switch userContentType { + case .file: + self.statsCategory = .file + case .image: + self.statsCategory = .image + case .video: + self.statsCategory = .video + case .audio: + self.statsCategory = .audio + case .sticker: + self.statsCategory = .stickers + case .audioVideoMessage: + self.statsCategory = .voiceMessages + default: + self.statsCategory = statsCategory + } } } diff --git a/submodules/TelegramCore/Sources/Wallpapers.swift b/submodules/TelegramCore/Sources/Wallpapers.swift index 2846a09c81..e2a8b18d60 100644 --- a/submodules/TelegramCore/Sources/Wallpapers.swift +++ b/submodules/TelegramCore/Sources/Wallpapers.swift @@ -109,7 +109,7 @@ private enum UploadedWallpaperDataContent { } private func uploadedWallpaper(postbox: Postbox, network: Network, resource: MediaResource) -> Signal { - return multipartUpload(network: network, postbox: postbox, source: .resource(.standalone(resource: resource)), encrypt: false, tag: TelegramMediaResourceFetchTag(statsCategory: .image), hintFileSize: nil, hintFileIsLarge: false, forceNoBigParts: false) + return multipartUpload(network: network, postbox: postbox, source: .resource(.standalone(resource: resource)), encrypt: false, tag: TelegramMediaResourceFetchTag(statsCategory: .image, userContentType: .image), hintFileSize: nil, hintFileIsLarge: false, forceNoBigParts: false) |> map { result -> UploadedWallpaperData in return UploadedWallpaperData(resource: resource, content: .result(result)) } diff --git a/submodules/TelegramNotices/Sources/Notices.swift b/submodules/TelegramNotices/Sources/Notices.swift index 17cb4cb159..f80d464bdc 100644 --- a/submodules/TelegramNotices/Sources/Notices.swift +++ b/submodules/TelegramNotices/Sources/Notices.swift @@ -166,6 +166,7 @@ private enum ApplicationSpecificGlobalNotice: Int32 { case emojiTooltip = 32 case audioTranscriptionSuggestion = 33 case clearStorageDismissedTipSize = 34 + case dismissedTrendingEmojiPacks = 35 var key: ValueBoxKey { let v = ValueBoxKey(length: 4) @@ -354,6 +355,10 @@ private struct ApplicationSpecificNoticeKeys { static func clearStorageDismissedTipSize() -> NoticeEntryKey { return NoticeEntryKey(namespace: noticeNamespace(namespace: globalNamespace), key: ApplicationSpecificGlobalNotice.clearStorageDismissedTipSize.key) } + + static func dismissedTrendingEmojiPacks() -> NoticeEntryKey { + return NoticeEntryKey(namespace: noticeNamespace(namespace: globalNamespace), key: ApplicationSpecificGlobalNotice.dismissedTrendingEmojiPacks.key) + } } public struct ApplicationSpecificNotice { @@ -1011,6 +1016,25 @@ public struct ApplicationSpecificNotice { } } + public static func dismissedTrendingEmojiPacks(accountManager: AccountManager) -> Signal<[Int64]?, NoError> { + return accountManager.noticeEntry(key: ApplicationSpecificNoticeKeys.dismissedTrendingEmojiPacks()) + |> map { view -> [Int64]? in + if let value = view.value?.get(ApplicationSpecificInt64ArrayNotice.self) { + return value.values + } else { + return nil + } + } + } + + public static func setDismissedTrendingEmojiPacks(accountManager: AccountManager, values: [Int64]) -> Signal { + return accountManager.transaction { transaction -> Void in + if let entry = CodableEntry(ApplicationSpecificInt64ArrayNotice(values: values)) { + transaction.setNotice(ApplicationSpecificNoticeKeys.dismissedTrendingEmojiPacks(), entry) + } + } + } + public static func getChatSpecificThemeLightPreviewTip(accountManager: AccountManager) -> Signal<(Int32, Int32), NoError> { return accountManager.transaction { transaction -> (Int32, Int32) in if let value = transaction.getNotice(ApplicationSpecificNoticeKeys.chatSpecificThemeLightPreviewTip())?.get(ApplicationSpecificTimestampAndCounterNotice.self) { diff --git a/submodules/TelegramPresentationData/Sources/DefaultDarkPresentationTheme.swift b/submodules/TelegramPresentationData/Sources/DefaultDarkPresentationTheme.swift index df1fdbfde1..bc24c91fa9 100644 --- a/submodules/TelegramPresentationData/Sources/DefaultDarkPresentationTheme.swift +++ b/submodules/TelegramPresentationData/Sources/DefaultDarkPresentationTheme.swift @@ -652,6 +652,12 @@ public func makeDefaultDarkPresentationTheme(extendingThemeReference: Presentati panelContentControlVibrantSelectionColor: UIColor(white: 1.0, alpha: 0.1), panelContentControlOpaqueOverlayColor: UIColor(white: 1.0, alpha: 0.1), panelContentControlOpaqueSelectionColor: UIColor(white: 1.0, alpha: 0.1), + panelContentVibrantSearchOverlayColor: UIColor(rgb: 0x808080), + panelContentVibrantSearchOverlaySelectedColor: UIColor(rgb: 0x808080), + panelContentVibrantSearchOverlayHighlightColor: UIColor(rgb: 0x808080).withMultipliedAlpha(0.25), + panelContentOpaqueSearchOverlayColor: UIColor(rgb: 0x808080), + panelContentOpaqueSearchOverlaySelectedColor: UIColor(rgb: 0x808080), + panelContentOpaqueSearchOverlayHighlightColor: UIColor(rgb: 0x808080).withMultipliedAlpha(0.25), stickersBackgroundColor: UIColor(rgb: 0x000000), stickersSectionTextColor: UIColor(rgb: 0x7b7b7b), stickersSearchBackgroundColor: UIColor(rgb: 0x1c1c1d), diff --git a/submodules/TelegramPresentationData/Sources/DefaultDarkTintedPresentationTheme.swift b/submodules/TelegramPresentationData/Sources/DefaultDarkTintedPresentationTheme.swift index f0d6ab8816..76c1c159e5 100644 --- a/submodules/TelegramPresentationData/Sources/DefaultDarkTintedPresentationTheme.swift +++ b/submodules/TelegramPresentationData/Sources/DefaultDarkTintedPresentationTheme.swift @@ -449,6 +449,12 @@ public func customizeDefaultDarkTintedPresentationTheme(theme: PresentationTheme panelContentControlVibrantOverlayColor: mainSecondaryTextColor?.withAlphaComponent(0.3), panelContentControlOpaqueOverlayColor: mainSecondaryTextColor?.withAlphaComponent(0.3), panelContentControlOpaqueSelectionColor: mainSecondaryTextColor?.withAlphaComponent(0.3), + panelContentVibrantSearchOverlayColor: mainSecondaryTextColor?.withAlphaComponent(0.3), + panelContentVibrantSearchOverlaySelectedColor: mainSecondaryTextColor?.withAlphaComponent(0.3), + panelContentVibrantSearchOverlayHighlightColor: mainSecondaryTextColor?.withAlphaComponent(0.3 * 0.5), + panelContentOpaqueSearchOverlayColor: mainSecondaryTextColor?.withAlphaComponent(0.3), + panelContentOpaqueSearchOverlaySelectedColor: mainSecondaryTextColor?.withAlphaComponent(0.3), + panelContentOpaqueSearchOverlayHighlightColor: mainSecondaryTextColor?.withAlphaComponent(0.3 * 0.5), stickersBackgroundColor: additionalBackgroundColor, stickersSectionTextColor: mainSecondaryTextColor?.withAlphaComponent(0.5), stickersSearchBackgroundColor: accentColor?.withMultiplied(hue: 1.009, saturation: 0.621, brightness: 0.15), @@ -866,6 +872,12 @@ public func makeDefaultDarkTintedPresentationTheme(extendingThemeReference: Pres panelContentControlVibrantSelectionColor: mainSecondaryTextColor.withAlphaComponent(0.1), panelContentControlOpaqueOverlayColor: mainSecondaryTextColor.withAlphaComponent(0.1), panelContentControlOpaqueSelectionColor: mainSecondaryTextColor.withAlphaComponent(0.1), + panelContentVibrantSearchOverlayColor: mainSecondaryTextColor.withAlphaComponent(0.5), + panelContentVibrantSearchOverlaySelectedColor: mainSecondaryTextColor.withAlphaComponent(0.5), + panelContentVibrantSearchOverlayHighlightColor: mainSecondaryTextColor.withAlphaComponent(0.1), + panelContentOpaqueSearchOverlayColor: mainSecondaryTextColor.withAlphaComponent(0.5), + panelContentOpaqueSearchOverlaySelectedColor: mainSecondaryTextColor.withAlphaComponent(0.5), + panelContentOpaqueSearchOverlayHighlightColor: mainSecondaryTextColor.withAlphaComponent(0.1), stickersBackgroundColor: additionalBackgroundColor, stickersSectionTextColor: mainSecondaryTextColor.withAlphaComponent(0.5), stickersSearchBackgroundColor: accentColor.withMultiplied(hue: 1.009, saturation: 0.621, brightness: 0.15), diff --git a/submodules/TelegramPresentationData/Sources/DefaultDayPresentationTheme.swift b/submodules/TelegramPresentationData/Sources/DefaultDayPresentationTheme.swift index 4940685ff5..f9d53abc71 100644 --- a/submodules/TelegramPresentationData/Sources/DefaultDayPresentationTheme.swift +++ b/submodules/TelegramPresentationData/Sources/DefaultDayPresentationTheme.swift @@ -901,6 +901,12 @@ public func makeDefaultDayPresentationTheme(extendingThemeReference: Presentatio panelContentControlVibrantSelectionColor: UIColor(white: 0.85, alpha: 0.1), panelContentControlOpaqueOverlayColor: UIColor(white: 0.0, alpha: 0.2), panelContentControlOpaqueSelectionColor: UIColor(white: 0.0, alpha: 0.1), + panelContentVibrantSearchOverlayColor: UIColor(white: 0.6, alpha: 0.55), + panelContentVibrantSearchOverlaySelectedColor: UIColor(white: 0.4, alpha: 0.6), + panelContentVibrantSearchOverlayHighlightColor: UIColor(white: 0.2, alpha: 0.02), + panelContentOpaqueSearchOverlayColor: UIColor(white: 0.0, alpha: 0.3), + panelContentOpaqueSearchOverlaySelectedColor: UIColor(white: 0.0, alpha: 0.4), + panelContentOpaqueSearchOverlayHighlightColor: UIColor(white: 0.0, alpha: 0.1), stickersBackgroundColor: UIColor(rgb: 0xe8ebf0), stickersSectionTextColor: UIColor(rgb: 0x9099a2), stickersSearchBackgroundColor: UIColor(rgb: 0xd9dbe1), diff --git a/submodules/TelegramPresentationData/Sources/PresentationTheme.swift b/submodules/TelegramPresentationData/Sources/PresentationTheme.swift index ca0f78f2e2..a50dcdb2a8 100644 --- a/submodules/TelegramPresentationData/Sources/PresentationTheme.swift +++ b/submodules/TelegramPresentationData/Sources/PresentationTheme.swift @@ -1163,6 +1163,12 @@ public final class PresentationThemeInputMediaPanel { public let panelContentControlVibrantSelectionColor: UIColor public let panelContentControlOpaqueOverlayColor: UIColor public let panelContentControlOpaqueSelectionColor: UIColor + public let panelContentVibrantSearchOverlayColor: UIColor + public let panelContentVibrantSearchOverlaySelectedColor: UIColor + public let panelContentVibrantSearchOverlayHighlightColor: UIColor + public let panelContentOpaqueSearchOverlayColor: UIColor + public let panelContentOpaqueSearchOverlaySelectedColor: UIColor + public let panelContentOpaqueSearchOverlayHighlightColor: UIColor public let stickersBackgroundColor: UIColor public let stickersSectionTextColor: UIColor public let stickersSearchBackgroundColor: UIColor @@ -1182,6 +1188,12 @@ public final class PresentationThemeInputMediaPanel { panelContentControlVibrantSelectionColor: UIColor, panelContentControlOpaqueOverlayColor: UIColor, panelContentControlOpaqueSelectionColor: UIColor, + panelContentVibrantSearchOverlayColor: UIColor, + panelContentVibrantSearchOverlaySelectedColor: UIColor, + panelContentVibrantSearchOverlayHighlightColor: UIColor, + panelContentOpaqueSearchOverlayColor: UIColor, + panelContentOpaqueSearchOverlaySelectedColor: UIColor, + panelContentOpaqueSearchOverlayHighlightColor: UIColor, stickersBackgroundColor: UIColor, stickersSectionTextColor: UIColor, stickersSearchBackgroundColor: UIColor, @@ -1200,6 +1212,12 @@ public final class PresentationThemeInputMediaPanel { self.panelContentControlVibrantSelectionColor = panelContentControlVibrantSelectionColor self.panelContentControlOpaqueOverlayColor = panelContentControlOpaqueOverlayColor self.panelContentControlOpaqueSelectionColor = panelContentControlOpaqueSelectionColor + self.panelContentVibrantSearchOverlayColor = panelContentVibrantSearchOverlayColor + self.panelContentVibrantSearchOverlaySelectedColor = panelContentVibrantSearchOverlaySelectedColor + self.panelContentVibrantSearchOverlayHighlightColor = panelContentVibrantSearchOverlayHighlightColor + self.panelContentOpaqueSearchOverlayColor = panelContentOpaqueSearchOverlayColor + self.panelContentOpaqueSearchOverlaySelectedColor = panelContentOpaqueSearchOverlaySelectedColor + self.panelContentOpaqueSearchOverlayHighlightColor = panelContentOpaqueSearchOverlayHighlightColor self.stickersBackgroundColor = stickersBackgroundColor self.stickersSectionTextColor = stickersSectionTextColor self.stickersSearchBackgroundColor = stickersSearchBackgroundColor @@ -1220,6 +1238,12 @@ public final class PresentationThemeInputMediaPanel { panelContentControlVibrantSelectionColor: UIColor? = nil, panelContentControlOpaqueOverlayColor: UIColor? = nil, panelContentControlOpaqueSelectionColor: UIColor? = nil, + panelContentVibrantSearchOverlayColor: UIColor? = nil, + panelContentVibrantSearchOverlaySelectedColor: UIColor? = nil, + panelContentVibrantSearchOverlayHighlightColor: UIColor? = nil, + panelContentOpaqueSearchOverlayColor: UIColor? = nil, + panelContentOpaqueSearchOverlaySelectedColor: UIColor? = nil, + panelContentOpaqueSearchOverlayHighlightColor: UIColor? = nil, stickersBackgroundColor: UIColor? = nil, stickersSectionTextColor: UIColor? = nil, stickersSearchBackgroundColor: UIColor? = nil, @@ -1239,6 +1263,12 @@ public final class PresentationThemeInputMediaPanel { panelContentControlVibrantSelectionColor: panelContentControlVibrantSelectionColor ?? self.panelContentControlVibrantSelectionColor, panelContentControlOpaqueOverlayColor: panelContentControlOpaqueOverlayColor ?? self.panelContentControlOpaqueOverlayColor, panelContentControlOpaqueSelectionColor: panelContentControlOpaqueSelectionColor ?? self.panelContentControlOpaqueSelectionColor, + panelContentVibrantSearchOverlayColor: panelContentVibrantSearchOverlayColor ?? self.panelContentVibrantSearchOverlayColor, + panelContentVibrantSearchOverlaySelectedColor: panelContentVibrantSearchOverlaySelectedColor ?? self.panelContentVibrantSearchOverlaySelectedColor, + panelContentVibrantSearchOverlayHighlightColor: panelContentVibrantSearchOverlayHighlightColor ?? self.panelContentVibrantSearchOverlayHighlightColor, + panelContentOpaqueSearchOverlayColor: panelContentOpaqueSearchOverlayColor ?? self.panelContentOpaqueSearchOverlayColor, + panelContentOpaqueSearchOverlaySelectedColor: panelContentOpaqueSearchOverlaySelectedColor ?? self.panelContentOpaqueSearchOverlaySelectedColor, + panelContentOpaqueSearchOverlayHighlightColor: panelContentOpaqueSearchOverlayHighlightColor ?? self.panelContentOpaqueSearchOverlayHighlightColor, stickersBackgroundColor: stickersBackgroundColor ?? self.stickersBackgroundColor, stickersSectionTextColor: stickersSectionTextColor ?? self.stickersSectionTextColor, stickersSearchBackgroundColor: stickersSearchBackgroundColor ?? self.stickersSearchBackgroundColor, diff --git a/submodules/TelegramPresentationData/Sources/PresentationThemeCodable.swift b/submodules/TelegramPresentationData/Sources/PresentationThemeCodable.swift index fac8cfa68e..10ed993ba4 100644 --- a/submodules/TelegramPresentationData/Sources/PresentationThemeCodable.swift +++ b/submodules/TelegramPresentationData/Sources/PresentationThemeCodable.swift @@ -1642,6 +1642,12 @@ extension PresentationThemeInputMediaPanel: Codable { case stickersSearchControl case gifsBg case bg + case panelContentVibrantSearchOverlay + case panelContentVibrantSearchOverlaySelected + case panelContentVibrantSearchOverlayHighlight + case panelContentOpaqueSearchOverlay + case panelContentOpaqueSearchOverlaySelected + case panelContentOpaqueSearchOverlayHighlight } public convenience init(from decoder: Decoder) throws { @@ -1675,6 +1681,12 @@ extension PresentationThemeInputMediaPanel: Codable { panelContentControlVibrantSelectionColor: try decodeColor(values, .panelContentControlVibrantSelection, fallbackKey: "\(codingPath).stickersSectionText"), panelContentControlOpaqueOverlayColor: try decodeColor(values, .panelContentControlOpaqueOverlay, fallbackKey: "\(codingPath).stickersSectionText"), panelContentControlOpaqueSelectionColor: try decodeColor(values, .panelContentControlOpaqueSelection, fallbackKey: "\(codingPath).stickersSectionText"), + panelContentVibrantSearchOverlayColor: try decodeColor(values, .panelContentVibrantSearchOverlay, fallbackKey: "\(codingPath).stickersSectionText"), + panelContentVibrantSearchOverlaySelectedColor: try decodeColor(values, .panelContentVibrantSearchOverlaySelected, fallbackKey: "\(codingPath).stickersSectionText"), + panelContentVibrantSearchOverlayHighlightColor: try decodeColor(values, .panelContentVibrantSearchOverlayHighlight, fallbackKey: "\(codingPath).panelHighlightedIconBg"), + panelContentOpaqueSearchOverlayColor: try decodeColor(values, .panelContentOpaqueSearchOverlay, fallbackKey: "\(codingPath).stickersSectionText"), + panelContentOpaqueSearchOverlaySelectedColor: try decodeColor(values, .panelContentOpaqueSearchOverlaySelected, fallbackKey: "\(codingPath).stickersSectionText"), + panelContentOpaqueSearchOverlayHighlightColor: try decodeColor(values, .panelContentOpaqueSearchOverlayHighlight, fallbackKey: "\(codingPath).panelHighlightedIconBg"), stickersBackgroundColor: try decodeColor(values, .stickersBg), stickersSectionTextColor: try decodeColor(values, .stickersSectionText), stickersSearchBackgroundColor: try decodeColor(values, .stickersSearchBg), @@ -1695,6 +1707,14 @@ extension PresentationThemeInputMediaPanel: Codable { try encodeColor(&values, self.panelContentControlVibrantSelectionColor, .panelContentControlVibrantSelection) try encodeColor(&values, self.panelContentControlOpaqueOverlayColor, .panelContentControlOpaqueOverlay) try encodeColor(&values, self.panelContentControlOpaqueSelectionColor, .panelContentControlOpaqueSelection) + + try encodeColor(&values, self.panelContentVibrantSearchOverlayColor, .panelContentVibrantSearchOverlay) + try encodeColor(&values, self.panelContentVibrantSearchOverlaySelectedColor, .panelContentVibrantSearchOverlaySelected) + try encodeColor(&values, self.panelContentVibrantSearchOverlayHighlightColor, .panelContentVibrantSearchOverlayHighlight) + try encodeColor(&values, self.panelContentOpaqueSearchOverlayColor, .panelContentOpaqueSearchOverlay) + try encodeColor(&values, self.panelContentOpaqueSearchOverlaySelectedColor, .panelContentOpaqueSearchOverlaySelected) + try encodeColor(&values, self.panelContentOpaqueSearchOverlayHighlightColor, .panelContentOpaqueSearchOverlayHighlight) + try encodeColor(&values, self.stickersBackgroundColor, .stickersBg) try encodeColor(&values, self.stickersSectionTextColor, .stickersSectionText) try encodeColor(&values, self.stickersSearchBackgroundColor, .stickersSearchBg) diff --git a/submodules/TelegramUI/Components/AvatarEditorScreen/Sources/AvatarEditorScreen.swift b/submodules/TelegramUI/Components/AvatarEditorScreen/Sources/AvatarEditorScreen.swift index 1e614bbb3b..1d46b95b1e 100644 --- a/submodules/TelegramUI/Components/AvatarEditorScreen/Sources/AvatarEditorScreen.swift +++ b/submodules/TelegramUI/Components/AvatarEditorScreen/Sources/AvatarEditorScreen.swift @@ -247,167 +247,219 @@ final class AvatarEditorScreenComponent: Component { self.state?.updated(transition: .immediate) self.state?.ready.set(.single(true)) - 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) - let presentationData = context.sharedContext.currentPresentationData.with { $0 } - - 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: presentationData.strings.AvatarEditor_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: presentationData.strings.AvatarEditor_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)))) })) } } @@ -520,8 +572,8 @@ final class AvatarEditorScreenComponent: Component { strongSelf.state?.updated(transition: transition) } }, - updateSearchQuery: { rawQuery, languageCode in - updateSearchQuery(rawQuery, languageCode) + updateSearchQuery: { query in + updateSearchQuery(query) }, updateScrollingToItemGroup: { }, @@ -650,8 +702,8 @@ final class AvatarEditorScreenComponent: Component { strongSelf.state?.updated(transition: transition) } }, - updateSearchQuery: { rawQuery, languageCode in - updateSearchQuery(rawQuery, languageCode) + updateSearchQuery: { query in + updateSearchQuery(query) }, updateScrollingToItemGroup: { }, @@ -1297,6 +1349,7 @@ public final class AvatarEditorScreen: ViewControllerComponentContainer { isStatusSelection: false, isReactionSelection: false, isEmojiSelection: false, + hasTrending: false, isProfilePhotoEmojiSelection: !isGroup, isGroupPhotoEmojiSelection: isGroup, topReactionItems: [], diff --git a/submodules/TelegramUI/Components/ChatEntityKeyboardInputNode/BUILD b/submodules/TelegramUI/Components/ChatEntityKeyboardInputNode/BUILD index 5ec6a15c17..82231a6d07 100644 --- a/submodules/TelegramUI/Components/ChatEntityKeyboardInputNode/BUILD +++ b/submodules/TelegramUI/Components/ChatEntityKeyboardInputNode/BUILD @@ -38,6 +38,7 @@ swift_library( "//submodules/TelegramUI/Components/MultiplexedVideoNode:MultiplexedVideoNode", "//submodules/TelegramUI/Components/ChatControllerInteraction:ChatControllerInteraction", "//submodules/FeaturedStickersScreen:FeaturedStickersScreen", + "//submodules/StickerPackPreviewUI", ], visibility = [ "//visibility:public", diff --git a/submodules/TelegramUI/Components/ChatEntityKeyboardInputNode/Sources/ChatEntityKeyboardInputNode.swift b/submodules/TelegramUI/Components/ChatEntityKeyboardInputNode/Sources/ChatEntityKeyboardInputNode.swift index d78a8ffeb4..92ccb07f48 100644 --- a/submodules/TelegramUI/Components/ChatEntityKeyboardInputNode/Sources/ChatEntityKeyboardInputNode.swift +++ b/submodules/TelegramUI/Components/ChatEntityKeyboardInputNode/Sources/ChatEntityKeyboardInputNode.swift @@ -30,6 +30,7 @@ import MultiplexedVideoNode import ChatControllerInteraction import FeaturedStickersScreen import Pasteboard +import StickerPackPreviewUI public struct ChatMediaInputPaneScrollState { let absoluteOffset: CGFloat? @@ -68,13 +69,13 @@ public final class EntityKeyboardGifContent: Equatable { public final class ChatEntityKeyboardInputNode: ChatInputNode { public struct InputData: Equatable { - public var emoji: EmojiPagerContentComponent + public var emoji: EmojiPagerContentComponent? public var stickers: EmojiPagerContentComponent? public var gifs: EntityKeyboardGifContent? public var availableGifSearchEmojies: [EntityKeyboardComponent.GifSearchEmoji] public init( - emoji: EmojiPagerContentComponent, + emoji: EmojiPagerContentComponent?, stickers: EmojiPagerContentComponent?, gifs: EntityKeyboardGifContent?, availableGifSearchEmojies: [EntityKeyboardComponent.GifSearchEmoji] @@ -107,7 +108,7 @@ public final class ChatEntityKeyboardInputNode: ChatInputNode { let animationCache = context.animationCache let animationRenderer = context.animationRenderer - let emojiItems = EmojiPagerContentComponent.emojiInputData(context: context, animationCache: animationCache, animationRenderer: animationRenderer, isStandalone: false, isStatusSelection: false, isReactionSelection: false, isEmojiSelection: true, topReactionItems: [], areUnicodeEmojiEnabled: true, areCustomEmojiEnabled: areCustomEmojiEnabled, chatPeerId: chatPeerId) + let emojiItems = EmojiPagerContentComponent.emojiInputData(context: context, animationCache: animationCache, animationRenderer: animationRenderer, isStandalone: false, isStatusSelection: false, isReactionSelection: false, isEmojiSelection: true, hasTrending: true, topReactionItems: [], areUnicodeEmojiEnabled: true, areCustomEmojiEnabled: areCustomEmojiEnabled, chatPeerId: chatPeerId) let stickerNamespaces: [ItemCollectionId.Namespace] = [Namespaces.ItemCollection.CloudStickerPacks] let stickerOrderedItemListCollectionIds: [Int32] = [Namespaces.OrderedItemList.CloudSavedStickers, Namespaces.OrderedItemList.CloudRecentStickers, Namespaces.OrderedItemList.CloudAllPremiumStickers] @@ -159,6 +160,8 @@ public final class ChatEntityKeyboardInputNode: ChatInputNode { loadMore: { _ in }, openSearch: { + }, + updateSearchQuery: { _ in } ) @@ -173,6 +176,7 @@ public final class ChatEntityKeyboardInputNode: ChatInputNode { isLoading: false, loadMoreToken: nil, displaySearchWithPlaceholder: nil, + searchCategories: nil, searchInitiallyHidden: true ) )) @@ -243,6 +247,9 @@ public final class ChatEntityKeyboardInputNode: ChatInputNode { private let emojiSearchDisposable = MetaDisposable() private let emojiSearchResult = Promise<(groups: [EmojiPagerContentComponent.ItemGroup], id: AnyHashable)?>(nil) + private let stickerSearchDisposable = MetaDisposable() + private let stickerSearchResult = Promise<(groups: [EmojiPagerContentComponent.ItemGroup], id: AnyHashable)?>(nil) + private let controllerInteraction: ChatControllerInteraction? private var inputNodeInteraction: ChatMediaInputNodeInteraction? @@ -320,12 +327,17 @@ public final class ChatEntityKeyboardInputNode: ChatInputNode { return !savedGifs.isEmpty } + let searchCategories: Signal = context.engine.stickers.emojiSearchCategories(kind: .emoji) + let presentationData = context.sharedContext.currentPresentationData.with { $0 } let gifItems: Signal switch subject { case .recent: - gifItems = context.engine.data.subscribe(TelegramEngine.EngineData.Item.OrderedLists.ListItems(collectionId: Namespaces.OrderedItemList.CloudRecentGifs)) - |> map { savedGifs -> EntityKeyboardGifContent in + gifItems = combineLatest( + context.engine.data.subscribe(TelegramEngine.EngineData.Item.OrderedLists.ListItems(collectionId: Namespaces.OrderedItemList.CloudRecentGifs)), + searchCategories + ) + |> map { savedGifs, searchCategories -> EntityKeyboardGifContent in var items: [GifPagerContentComponent.Item] = [] for gifItem in savedGifs { items.append(GifPagerContentComponent.Item( @@ -342,14 +354,15 @@ public final class ChatEntityKeyboardInputNode: ChatInputNode { items: items, isLoading: false, loadMoreToken: nil, - displaySearchWithPlaceholder: presentationData.strings.GifSearch_SearchGifPlaceholder, + displaySearchWithPlaceholder: presentationData.strings.Common_Search, + searchCategories: searchCategories, searchInitiallyHidden: true ) ) } case .trending: - gifItems = combineLatest(hasRecentGifs, trendingGifs) - |> map { hasRecentGifs, trendingGifs -> EntityKeyboardGifContent in + gifItems = combineLatest(hasRecentGifs, trendingGifs, searchCategories) + |> map { hasRecentGifs, trendingGifs, searchCategories -> EntityKeyboardGifContent in var items: [GifPagerContentComponent.Item] = [] var isLoading = false @@ -373,14 +386,19 @@ public final class ChatEntityKeyboardInputNode: ChatInputNode { items: items, isLoading: isLoading, loadMoreToken: nil, - displaySearchWithPlaceholder: nil, + displaySearchWithPlaceholder: presentationData.strings.Common_Search, + searchCategories: searchCategories, searchInitiallyHidden: true ) ) } case let .emojiSearch(query): - gifItems = combineLatest(hasRecentGifs, paneGifSearchForQuery(context: context, query: query, offset: nil, incompleteResults: true, staleCachedResults: true, delayRequest: false, updateActivity: nil)) - |> map { hasRecentGifs, result -> EntityKeyboardGifContent in + gifItems = combineLatest( + hasRecentGifs, + paneGifSearchForQuery(context: context, query: query, offset: nil, incompleteResults: true, staleCachedResults: true, delayRequest: false, updateActivity: nil), + searchCategories + ) + |> map { hasRecentGifs, result, searchCategories -> EntityKeyboardGifContent in var items: [GifPagerContentComponent.Item] = [] var loadMoreToken: String? @@ -406,7 +424,8 @@ public final class ChatEntityKeyboardInputNode: ChatInputNode { items: items, isLoading: isLoading, loadMoreToken: loadMoreToken, - displaySearchWithPlaceholder: nil, + displaySearchWithPlaceholder: presentationData.strings.Common_Search, + searchCategories: searchCategories, searchInitiallyHidden: true ) ) @@ -448,9 +467,12 @@ public final class ChatEntityKeyboardInputNode: ChatInputNode { return !savedGifs.isEmpty } + let searchCategories: Signal = context.engine.stickers.emojiSearchCategories(kind: .emoji) + let presentationData = context.sharedContext.currentPresentationData.with { $0 } + let gifItems: Signal - gifItems = combineLatest(hasRecentGifs, paneGifSearchForQuery(context: context, query: query, offset: token, incompleteResults: true, staleCachedResults: true, delayRequest: false, updateActivity: nil)) - |> map { hasRecentGifs, result -> EntityKeyboardGifContent in + gifItems = combineLatest(hasRecentGifs, paneGifSearchForQuery(context: context, query: query, offset: token, incompleteResults: true, staleCachedResults: true, delayRequest: false, updateActivity: nil), searchCategories) + |> map { hasRecentGifs, result, searchCategories -> EntityKeyboardGifContent in var items: [GifPagerContentComponent.Item] = [] var existingIds = Set() for item in componentValue.component.items { @@ -487,7 +509,8 @@ public final class ChatEntityKeyboardInputNode: ChatInputNode { items: items, isLoading: isLoading, loadMoreToken: loadMoreToken, - displaySearchWithPlaceholder: nil, + displaySearchWithPlaceholder: presentationData.strings.Common_Search, + searchCategories: searchCategories, searchInitiallyHidden: true ) ) @@ -623,7 +646,42 @@ public final class ChatEntityKeyboardInputNode: ChatInputNode { return } - if let file = item.itemFile { + if groupId == AnyHashable("featuredTop"), let file = item.itemFile { + let viewKey = PostboxViewKey.orderedItemList(id: Namespaces.OrderedItemList.CloudFeaturedEmojiPacks) + let _ = (context.account.postbox.combinedView(keys: [viewKey]) + |> take(1) + |> deliverOnMainQueue).start(next: { [weak interfaceInteraction, weak controllerInteraction] views in + guard let controllerInteraction = controllerInteraction else { + return + } + guard let view = views.views[viewKey] as? OrderedItemListView else { + return + } + for featuredStickerPack in view.items.lazy.map({ $0.contents.get(FeaturedStickerPackItem.self)! }) { + if featuredStickerPack.topItems.contains(where: { $0.file.fileId == file.fileId }) { + let controller = StickerPackScreen( + context: context, + updatedPresentationData: controllerInteraction.updatedPresentationData, + mode: .default, + mainStickerPack: .id(id: featuredStickerPack.info.id.id, accessHash: featuredStickerPack.info.accessHash), + stickerPacks: [.id(id: featuredStickerPack.info.id.id, accessHash: featuredStickerPack.info.accessHash)], + loadedStickerPacks: [.result(info: featuredStickerPack.info, items: featuredStickerPack.topItems, installed: false)], + parentNavigationController: controllerInteraction.navigationController(), + sendSticker: nil, + sendEmoji: { [weak interfaceInteraction] text, emojiAttribute in + guard let interfaceInteraction else { + return + } + interfaceInteraction.insertText(NSAttributedString(string: text, attributes: [ChatTextInputAttributes.customEmoji: emojiAttribute])) + } + ) + controllerInteraction.presentController(controller, nil) + + break + } + } + }) + } else if let file = item.itemFile { var text = "." var emojiAttribute: ChatTextInputTextCustomEmojiAttribute? loop: for attribute in file.attributes { @@ -785,6 +843,20 @@ public final class ChatEntityKeyboardInputNode: ChatInputNode { }) ])]) controllerInteraction.presentController(actionSheet, nil) + } else if groupId == AnyHashable("featuredTop") { + let viewKey = PostboxViewKey.orderedItemList(id: Namespaces.OrderedItemList.CloudFeaturedEmojiPacks) + let _ = (context.account.postbox.combinedView(keys: [viewKey]) + |> take(1) + |> deliverOnMainQueue).start(next: { views in + guard let view = views.views[viewKey] as? OrderedItemListView else { + return + } + var emojiPackIds: [Int64] = [] + for featuredEmojiPack in view.items.lazy.map({ $0.contents.get(FeaturedStickerPackItem.self)! }) { + emojiPackIds.append(featuredEmojiPack.info.id.id) + } + let _ = ApplicationSpecificNotice.setDismissedTrendingEmojiPacks(accountManager: context.sharedContext.accountManager, values: emojiPackIds).start() + }) } }, pushController: { [weak controllerInteraction] controller in @@ -816,145 +888,197 @@ 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 +1285,66 @@ public final class ChatEntityKeyboardInputNode: ChatInputNode { }, requestUpdate: { _ in }, - updateSearchQuery: { _, _ in + updateSearchQuery: { [weak self] query in + guard let strongSelf = self else { + return + } + + switch query { + case .none: + strongSelf.stickerSearchDisposable.set(nil) + strongSelf.stickerSearchResult.set(.single(nil)) + case .text: + strongSelf.stickerSearchDisposable.set(nil) + strongSelf.stickerSearchResult.set(.single(nil)) + case let .category(value): + let resultSignal = strongSelf.context.engine.stickers.searchStickers(query: value, scope: [.installed, .remote]) + |> mapToSignal { files -> Signal<[EmojiPagerContentComponent.ItemGroup], NoError> in + var items: [EmojiPagerContentComponent.Item] = [] + + var existingIds = Set() + for item in files { + let itemFile = item.file + 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.stickerSearchDisposable.set((resultSignal + |> deliverOnMainQueue).start(next: { [weak self] result in + guard let strongSelf = self else { + return + } + strongSelf.stickerSearchResult.set(.single((result, AnyHashable(value)))) + })) + } }, updateScrollingToItemGroup: { }, @@ -1177,9 +1360,10 @@ public final class ChatEntityKeyboardInputNode: ChatInputNode { self.inputDataDisposable = (combineLatest(queue: .mainQueue(), updatedInputData, self.gifComponent.get(), - self.emojiSearchResult.get() + self.emojiSearchResult.get(), + self.stickerSearchResult.get() ) - |> deliverOnMainQueue).start(next: { [weak self] inputData, gifs, emojiSearchResult in + |> deliverOnMainQueue).start(next: { [weak self] inputData, gifs, emojiSearchResult, stickerSearchResult in guard let strongSelf = self else { return } @@ -1196,7 +1380,23 @@ public final class ChatEntityKeyboardInputNode: ChatInputNode { iconFile: nil ) } - inputData.emoji = inputData.emoji.withUpdatedItemGroups(panelItemGroups: inputData.emoji.panelItemGroups, contentItemGroups: emojiSearchResult.groups, itemContentUniqueId: emojiSearchResult.id, emptySearchResults: emptySearchResults) + if let emoji = inputData.emoji { + inputData.emoji = emoji.withUpdatedItemGroups(panelItemGroups: emoji.panelItemGroups, contentItemGroups: emojiSearchResult.groups, itemContentUniqueId: emojiSearchResult.id, emptySearchResults: emptySearchResults) + } + } + + if let stickerSearchResult = stickerSearchResult { + var stickerSearchResults: EmojiPagerContentComponent.EmptySearchResults? + if !stickerSearchResult.groups.contains(where: { !$0.items.isEmpty }) { + //TODO:localize + stickerSearchResults = EmojiPagerContentComponent.EmptySearchResults( + text: "No stickers found", + iconFile: nil + ) + } + if let stickers = inputData.stickers { + inputData.stickers = stickers.withUpdatedItemGroups(panelItemGroups: stickers.panelItemGroups, contentItemGroups: stickerSearchResult.groups, itemContentUniqueId: stickerSearchResult.id, emptySearchResults: stickerSearchResults) + } } var transition: Transition = .immediate @@ -1285,6 +1485,16 @@ public final class ChatEntityKeyboardInputNode: ChatInputNode { if let strongSelf = self, let pagerView = strongSelf.entityKeyboardView.componentView as? EntityKeyboardComponent.View { pagerView.openSearch() } + }, + updateSearchQuery: { [weak self] query in + guard let self else { + return + } + if let query { + self.gifMode = .emojiSearch(query) + } else { + self.gifMode = .recent + } } ) @@ -1324,6 +1534,7 @@ public final class ChatEntityKeyboardInputNode: ChatInputNode { self.inputDataDisposable?.dispose() self.hasRecentGifsDisposable?.dispose() self.emojiSearchDisposable.dispose() + self.stickerSearchDisposable.dispose() } private func reloadGifContext() { @@ -1386,27 +1597,38 @@ public final class ChatEntityKeyboardInputNode: ChatInputNode { mappedTransition = mappedTransition.withUserData(EntityKeyboardComponent.MarkInputCollapsed()) } + var emojiContent: EmojiPagerContentComponent? = self.currentInputData.emoji var stickerContent: EmojiPagerContentComponent? = self.currentInputData.stickers var gifContent: EntityKeyboardGifContent? = self.currentInputData.gifs var stickersEnabled = true + var emojiEnabled = true if let peer = interfaceState.renderedPeer?.peer as? TelegramChannel { if peer.hasBannedPermission(.banSendStickers) != nil { stickersEnabled = false } + if peer.hasBannedPermission(.banSendText) != nil { + emojiEnabled = false + } } else if let peer = interfaceState.renderedPeer?.peer as? TelegramGroup { if peer.hasBannedPermission(.banSendStickers) { stickersEnabled = false } + if peer.hasBannedPermission(.banSendText) { + emojiEnabled = false + } } if !stickersEnabled || interfaceState.interfaceState.editMessage != nil { stickerContent = nil gifContent = nil } + if !emojiEnabled && interfaceState.interfaceState.editMessage == nil { + emojiContent = nil + } stickerContent?.inputInteractionHolder.inputInteraction = self.stickerInputInteraction - self.currentInputData.emoji.inputInteractionHolder.inputInteraction = self.emojiInputInteraction + self.currentInputData.emoji?.inputInteractionHolder.inputInteraction = self.emojiInputInteraction let startTime = CFAbsoluteTimeGetCurrent() @@ -1418,7 +1640,7 @@ public final class ChatEntityKeyboardInputNode: ChatInputNode { isContentInFocus: isVisible, containerInsets: UIEdgeInsets(top: self.isEmojiSearchActive ? -34.0 : 0.0, left: leftInset, bottom: bottomInset, right: rightInset), topPanelInsets: UIEdgeInsets(), - emojiContent: self.currentInputData.emoji, + emojiContent: emojiContent, stickerContent: stickerContent, maskContent: nil, gifContent: gifContent?.component, @@ -1586,9 +1808,11 @@ public final class ChatEntityKeyboardInputNode: ChatInputNode { private func processInputData(inputData: InputData) -> InputData { return InputData( - emoji: inputData.emoji.withUpdatedItemGroups(panelItemGroups: self.processStableItemGroupList(category: .emoji, itemGroups: inputData.emoji.panelItemGroups), contentItemGroups: self.processStableItemGroupList(category: .emoji, itemGroups: inputData.emoji.contentItemGroups), itemContentUniqueId: inputData.emoji.itemContentUniqueId, emptySearchResults: inputData.emoji.emptySearchResults), + emoji: inputData.emoji.flatMap { emoji in + return emoji.withUpdatedItemGroups(panelItemGroups: self.processStableItemGroupList(category: .emoji, itemGroups: emoji.panelItemGroups), contentItemGroups: self.processStableItemGroupList(category: .emoji, itemGroups: emoji.contentItemGroups), itemContentUniqueId: emoji.itemContentUniqueId, emptySearchResults: emoji.emptySearchResults) + }, stickers: inputData.stickers.flatMap { stickers in - return stickers.withUpdatedItemGroups(panelItemGroups: self.processStableItemGroupList(category: .stickers, itemGroups: stickers.panelItemGroups), contentItemGroups: self.processStableItemGroupList(category: .stickers, itemGroups: stickers.contentItemGroups), itemContentUniqueId: nil, emptySearchResults: nil) + return stickers.withUpdatedItemGroups(panelItemGroups: self.processStableItemGroupList(category: .stickers, itemGroups: stickers.panelItemGroups), contentItemGroups: self.processStableItemGroupList(category: .stickers, itemGroups: stickers.contentItemGroups), itemContentUniqueId: stickers.itemContentUniqueId, emptySearchResults: nil) }, gifs: inputData.gifs, availableGifSearchEmojies: inputData.availableGifSearchEmojies @@ -1843,9 +2067,11 @@ public final class EntityInputView: UIInputView, AttachmentTextInputPanelInputVi return } - if let file = item.itemFile { - var text = "." - var emojiAttribute: ChatTextInputTextCustomEmojiAttribute? + if groupId == AnyHashable("featuredTop") { + } else { + if let file = item.itemFile { + var text = "." + var emojiAttribute: ChatTextInputTextCustomEmojiAttribute? loop: for attribute in file.attributes { switch attribute { case let .CustomEmoji(_, _, displayText, _): @@ -1860,34 +2086,35 @@ public final class EntityInputView: UIInputView, AttachmentTextInputPanelInputVi break } } - - if file.isPremiumEmoji && !hasPremium { - let presentationData = context.sharedContext.currentPresentationData.with { $0 } - strongSelf.presentController?(UndoOverlayController(presentationData: presentationData, content: .sticker(context: context, file: file, title: nil, text: presentationData.strings.EmojiInput_PremiumEmojiToast_Text, undoText: presentationData.strings.EmojiInput_PremiumEmojiToast_Action, customAction: { - guard let strongSelf = self else { - return - } - - var replaceImpl: ((ViewController) -> Void)? - let controller = PremiumDemoScreen(context: strongSelf.context, subject: .animatedEmoji, action: { - let controller = PremiumIntroScreen(context: strongSelf.context, source: .animatedEmoji) - replaceImpl?(controller) - }) - replaceImpl = { [weak controller] c in - controller?.replace(with: c) - } - strongSelf.presentController?(controller) - }), elevatedLayout: false, animateInAsReplacement: false, action: { _ in return false })) - return - } - - if let emojiAttribute = emojiAttribute { + + if file.isPremiumEmoji && !hasPremium { + let presentationData = context.sharedContext.currentPresentationData.with { $0 } + strongSelf.presentController?(UndoOverlayController(presentationData: presentationData, content: .sticker(context: context, file: file, title: nil, text: presentationData.strings.EmojiInput_PremiumEmojiToast_Text, undoText: presentationData.strings.EmojiInput_PremiumEmojiToast_Action, customAction: { + guard let strongSelf = self else { + return + } + + var replaceImpl: ((ViewController) -> Void)? + let controller = PremiumDemoScreen(context: strongSelf.context, subject: .animatedEmoji, action: { + let controller = PremiumIntroScreen(context: strongSelf.context, source: .animatedEmoji) + replaceImpl?(controller) + }) + replaceImpl = { [weak controller] c in + controller?.replace(with: c) + } + strongSelf.presentController?(controller) + }), elevatedLayout: false, animateInAsReplacement: false, action: { _ in return false })) + return + } + + if let emojiAttribute = emojiAttribute { + AudioServicesPlaySystemSound(0x450) + strongSelf.insertText?(NSAttributedString(string: text, attributes: [ChatTextInputAttributes.customEmoji: emojiAttribute])) + } + } else if case let .staticEmoji(staticEmoji) = item.content { AudioServicesPlaySystemSound(0x450) - strongSelf.insertText?(NSAttributedString(string: text, attributes: [ChatTextInputAttributes.customEmoji: emojiAttribute])) + strongSelf.insertText?(NSAttributedString(string: staticEmoji, attributes: [:])) } - } else if case let .staticEmoji(staticEmoji) = item.content { - AudioServicesPlaySystemSound(0x450) - strongSelf.insertText?(NSAttributedString(string: staticEmoji, attributes: [:])) } }) }, @@ -1937,7 +2164,7 @@ public final class EntityInputView: UIInputView, AttachmentTextInputPanelInputVi }, requestUpdate: { _ in }, - updateSearchQuery: { _, _ in + updateSearchQuery: { _ in }, updateScrollingToItemGroup: { }, @@ -1952,7 +2179,7 @@ public final class EntityInputView: UIInputView, AttachmentTextInputPanelInputVi let semaphore = DispatchSemaphore(value: 0) var emojiComponent: EmojiPagerContentComponent? - let _ = EmojiPagerContentComponent.emojiInputData(context: context, animationCache: self.animationCache, animationRenderer: self.animationRenderer, isStandalone: true, isStatusSelection: false, isReactionSelection: false, isEmojiSelection: false, topReactionItems: [], areUnicodeEmojiEnabled: true, areCustomEmojiEnabled: areCustomEmojiEnabled, chatPeerId: nil, forceHasPremium: forceHasPremium).start(next: { value in + let _ = EmojiPagerContentComponent.emojiInputData(context: context, animationCache: self.animationCache, animationRenderer: self.animationRenderer, isStandalone: true, isStatusSelection: false, isReactionSelection: false, isEmojiSelection: false, hasTrending: false, topReactionItems: [], areUnicodeEmojiEnabled: true, areCustomEmojiEnabled: areCustomEmojiEnabled, chatPeerId: nil, forceHasPremium: forceHasPremium).start(next: { value in emojiComponent = value semaphore.signal() }) @@ -1967,7 +2194,7 @@ public final class EntityInputView: UIInputView, AttachmentTextInputPanelInputVi gifs: nil, availableGifSearchEmojies: [] ), - updatedInputData: EmojiPagerContentComponent.emojiInputData(context: context, animationCache: self.animationCache, animationRenderer: self.animationRenderer, isStandalone: true, isStatusSelection: false, isReactionSelection: false, isEmojiSelection: false, topReactionItems: [], areUnicodeEmojiEnabled: true, areCustomEmojiEnabled: areCustomEmojiEnabled, chatPeerId: nil, forceHasPremium: forceHasPremium) |> map { emojiComponent -> ChatEntityKeyboardInputNode.InputData in + updatedInputData: EmojiPagerContentComponent.emojiInputData(context: context, animationCache: self.animationCache, animationRenderer: self.animationRenderer, isStandalone: true, isStatusSelection: false, isReactionSelection: false, isEmojiSelection: false, hasTrending: false, topReactionItems: [], areUnicodeEmojiEnabled: true, areCustomEmojiEnabled: areCustomEmojiEnabled, chatPeerId: nil, forceHasPremium: forceHasPremium) |> map { emojiComponent -> ChatEntityKeyboardInputNode.InputData in return ChatEntityKeyboardInputNode.InputData( emoji: emojiComponent, stickers: nil, @@ -2164,7 +2391,7 @@ public final class EmojiContentPeekBehaviorImpl: EmojiContentPeekBehavior { interaction.copyEmoji(file) } - if let _ = strongSelf.chatPeerId, !isLocked { + if let _ = strongSelf.chatPeerId { //TODO:localize menuItems.append(.action(ContextMenuActionItem(text: "Send Emoji", icon: { theme in if let image = generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Download"), color: theme.actionSheet.primaryTextColor) { diff --git a/submodules/TelegramUI/Components/EmojiStatusSelectionComponent/Sources/EmojiStatusSelectionComponent.swift b/submodules/TelegramUI/Components/EmojiStatusSelectionComponent/Sources/EmojiStatusSelectionComponent.swift index 627f28145a..fd1d470605 100644 --- a/submodules/TelegramUI/Components/EmojiStatusSelectionComponent/Sources/EmojiStatusSelectionComponent.swift +++ b/submodules/TelegramUI/Components/EmojiStatusSelectionComponent/Sources/EmojiStatusSelectionComponent.swift @@ -432,126 +432,179 @@ 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(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: { result in guard let strongSelf = self else { return } - strongSelf.emojiSearchResult.set(.single((result, AnyHashable(query)))) + strongSelf.emojiSearchResult.set(.single((result, AnyHashable(value)))) })) } }, @@ -702,7 +755,8 @@ public final class EmojiStatusSelectionController: ViewController { cache: animationCache, renderer: animationRenderer, placeholderColor: UIColor(white: 0.0, alpha: 0.0), - pointSize: CGSize(width: 32.0, height: 32.0) + pointSize: CGSize(width: 32.0, height: 32.0), + dynamicColor: self.presentationData.theme.list.itemAccentColor ) switch item.tintMode { case .accent: diff --git a/submodules/TelegramUI/Components/EmojiSuggestionsComponent/Sources/EmojiSuggestionsComponent.swift b/submodules/TelegramUI/Components/EmojiSuggestionsComponent/Sources/EmojiSuggestionsComponent.swift index cf1c08aa50..a93adb17bc 100644 --- a/submodules/TelegramUI/Components/EmojiSuggestionsComponent/Sources/EmojiSuggestionsComponent.swift +++ b/submodules/TelegramUI/Components/EmojiSuggestionsComponent/Sources/EmojiSuggestionsComponent.swift @@ -281,6 +281,7 @@ public final class EmojiSuggestionsComponent: Component { let itemLayer: InlineStickerItemLayer if let current = self.visibleLayers[item.fileId] { itemLayer = current + itemLayer.dynamicColor = component.theme.list.itemPrimaryTextColor } else { itemLayer = InlineStickerItemLayer( context: component.context, @@ -291,7 +292,8 @@ public final class EmojiSuggestionsComponent: Component { cache: component.animationCache, renderer: component.animationRenderer, placeholderColor: component.theme.list.mediaPlaceholderColor, - pointSize: itemFrame.size + pointSize: itemFrame.size, + dynamicColor: component.theme.list.itemPrimaryTextColor ) self.visibleLayers[item.fileId] = itemLayer self.scrollView.layer.addSublayer(itemLayer) 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 42d9f67b76..bce7df4cd2 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 @@ -1561,9 +1569,9 @@ public final class EmojiSearchHeaderView: UIView, UITextFieldDelegate { return PassthroughLayer.self } - private let activated: () -> Void + private let activated: (Bool) -> 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 (Bool) -> 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(true) + + 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,16 +1851,20 @@ 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.searchIconView.image = generateTintedImage(image: UIImage(bundleImageName: "Components/Search Bar/Loupe"), color: .white)?.withRenderingMode(.alwaysTemplate) + self.searchIconView.tintColor = useOpaqueTheme ? theme.chat.inputMediaPanel.panelContentOpaqueSearchOverlayColor : theme.chat.inputMediaPanel.panelContentVibrantSearchOverlayColor + self.searchIconTintView.image = generateTintedImage(image: UIImage(bundleImageName: "Components/Search Bar/Loupe"), color: .white) - self.backIconView.image = generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Back"), color: theme.chat.inputMediaPanel.panelContentVibrantOverlayColor) + self.backIconView.image = generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Back"), color: .white)?.withRenderingMode(.alwaysTemplate) + self.backIconView.tintColor = useOpaqueTheme ? theme.chat.inputMediaPanel.panelContentOpaqueSearchOverlayColor : theme.chat.inputMediaPanel.panelContentVibrantSearchOverlayColor + self.backIconTintView.image = generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Back"), color: .white) - self.clearIconView.image = generateTintedImage(image: UIImage(bundleImageName: "Components/Search Bar/Clear"), color: theme.chat.inputMediaPanel.panelContentVibrantOverlayColor) + self.clearIconView.image = generateTintedImage(image: UIImage(bundleImageName: "Components/Search Bar/Clear"), color: .white)?.withRenderingMode(.alwaysTemplate) + self.clearIconView.tintColor = useOpaqueTheme ? theme.chat.inputMediaPanel.panelContentOpaqueSearchOverlayColor : theme.chat.inputMediaPanel.panelContentVibrantSearchOverlayColor + self.clearIconTintView.image = generateTintedImage(image: UIImage(bundleImageName: "Components/Search Bar/Clear"), color: .white) } @@ -1864,26 +1887,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 +1919,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 +1947,64 @@ 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, + useOpaqueTheme: useOpaqueTheme, + 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(false) + } 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 +2017,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 +2034,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 +2043,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 +2067,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 +2246,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 +2269,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 +2314,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 +2539,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 +2559,7 @@ public final class EmojiPagerContentComponent: Component { itemContentUniqueId: AnyHashable?, warpContentsOnEdges: Bool, displaySearchWithPlaceholder: String?, + searchCategories: EmojiSearchCategories?, searchInitiallyHidden: Bool, searchIsPlaceholderOnly: Bool, emptySearchResults: EmptySearchResults?, @@ -2558,6 +2578,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 +2600,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 +2646,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 +6182,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 - 1.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 +6207,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)) @@ -6344,12 +6371,12 @@ public final class EmojiPagerContentComponent: Component { }*/ } } else { - visibleSearchHeader = EmojiSearchHeaderView(activated: { [weak self] in + visibleSearchHeader = EmojiSearchHeaderView(activated: { [weak self] isTextInput in guard let strongSelf = self, let visibleSearchHeader = strongSelf.visibleSearchHeader else { return } - if let component = strongSelf.component, component.searchIsPlaceholderOnly { + if let component = strongSelf.component, component.searchIsPlaceholderOnly, isTextInput { component.inputInteractionHolder.inputInteraction?.openSearch() } else { strongSelf.isSearchActivated = true @@ -6374,11 +6401,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 +6418,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,27 +6472,27 @@ 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 } if animateContentCrossfade { - for (_, itemLayer) in self.visibleItemLayers { + /*for (_, itemLayer) in self.visibleItemLayers { if let snapshotLayer = itemLayer.snapshotContentTree() { itemLayer.superlayer?.insertSublayer(snapshotLayer, above: itemLayer) snapshotLayer.animateAlpha(from: CGFloat(snapshotLayer.opacity), to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { [weak snapshotLayer] _ in snapshotLayer?.removeFromSuperlayer() }) } - } + }*/ } self.updateVisibleItems(transition: itemTransition, attemptSynchronousLoads: attemptSynchronousLoads, previousItemPositions: previousItemPositions, previousAbsoluteItemPositions: previousAbsoluteItemPositions, updatedItemPositions: updatedItemPositions, hintDisappearingGroupFrame: hintDisappearingGroupFrame) if animateContentCrossfade { - for (_, itemLayer) in self.visibleItemLayers { + /*for (_, itemLayer) in self.visibleItemLayers { itemLayer.animateAlpha(from: 0.0, to: CGFloat(itemLayer.opacity), duration: 0.2) - } + }*/ } return availableSize @@ -6505,6 +6532,7 @@ public final class EmojiPagerContentComponent: Component { isStatusSelection: Bool, isReactionSelection: Bool, isEmojiSelection: Bool, + hasTrending: Bool, isTopicIconSelection: Bool = false, isQuickReactionSelection: Bool = false, isProfilePhotoEmojiSelection: Bool = false, @@ -6570,14 +6598,25 @@ 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, - iconStatusEmoji + searchCategories, + iconStatusEmoji, + ApplicationSpecificNotice.dismissedTrendingEmojiPacks(accountManager: context.sharedContext.accountManager) ) - |> map { view, hasPremium, featuredEmojiPacks, availableReactions, iconStatusEmoji -> EmojiPagerContentComponent in + |> map { view, hasPremium, featuredEmojiPacks, availableReactions, searchCategories, iconStatusEmoji, dismissedTrendingEmojiPacks -> EmojiPagerContentComponent in struct ItemGroup { var supergroupId: AnyHashable var id: AnyHashable @@ -6593,6 +6632,81 @@ public final class EmojiPagerContentComponent: Component { var itemGroups: [ItemGroup] = [] var itemGroupIndexById: [AnyHashable: Int] = [:] + var installedCollectionIds = Set() + for (id, _, _) in view.collectionInfos { + installedCollectionIds.insert(id) + } + + let dismissedTrendingEmojiPacksSet = Set(dismissedTrendingEmojiPacks ?? []) + let featuredEmojiPacksSet = Set(featuredEmojiPacks.map(\.info.id.id)) + + if dismissedTrendingEmojiPacksSet != featuredEmojiPacksSet && hasTrending { + for featuredEmojiPack in featuredEmojiPacks { + if installedCollectionIds.contains(featuredEmojiPack.info.id) { + continue + } + + guard let item = featuredEmojiPack.topItems.first else { + continue + } + + let animationData: EntityKeyboardAnimationData + + if let thumbnail = featuredEmojiPack.info.thumbnail { + let type: EntityKeyboardAnimationData.ItemType + if item.file.isAnimatedSticker { + type = .lottie + } else if item.file.isVideoEmoji || item.file.isVideoSticker { + type = .video + } else { + type = .still + } + + animationData = EntityKeyboardAnimationData( + id: .stickerPackThumbnail(featuredEmojiPack.info.id), + type: type, + resource: .stickerPackThumbnail(stickerPack: .id(id: featuredEmojiPack.info.id.id, accessHash: featuredEmojiPack.info.accessHash), resource: thumbnail.resource), + dimensions: thumbnail.dimensions.cgSize, + immediateThumbnailData: featuredEmojiPack.info.immediateThumbnailData, + isReaction: false, + isTemplate: false + ) + } else { + animationData = EntityKeyboardAnimationData(file: item.file) + } + + var tintMode: Item.TintMode = .none + if item.file.isCustomTemplateEmoji { + tintMode = .primary + } + + let resultItem = EmojiPagerContentComponent.Item( + animationData: animationData, + content: .animation(animationData), + itemFile: item.file, + subgroupId: nil, + icon: .none, + tintMode: tintMode + ) + + let supergroupId = "featuredTop" + let groupId: AnyHashable = supergroupId + let isPremiumLocked: Bool = item.file.isPremiumSticker && !hasPremium + if isPremiumLocked && isPremiumDisabled { + continue + } + if let groupIndex = itemGroupIndexById[groupId] { + itemGroups[groupIndex].items.append(resultItem) + } else { + itemGroupIndexById[groupId] = itemGroups.count + + //TODO:localize + let title = "TRENDING EMOJI" + itemGroups.append(ItemGroup(supergroupId: groupId, id: groupId, title: title, subtitle: nil, isPremiumLocked: false, isFeatured: false, collapsedLineCount: 0, isClearable: false, headerItem: nil, items: [resultItem])) + } + } + } + var recentEmoji: OrderedItemListView? var featuredStatusEmoji: OrderedItemListView? var recentStatusEmoji: OrderedItemListView? @@ -7100,11 +7214,6 @@ public final class EmojiPagerContentComponent: Component { } } - var installedCollectionIds = Set() - for (id, _, _) in view.collectionInfos { - installedCollectionIds.insert(id) - } - if areCustomEmojiEnabled { for entry in view.entries { guard let item = entry.item as? StickerPackItem else { @@ -7282,11 +7391,19 @@ public final class EmojiPagerContentComponent: Component { } else if isEmojiSelection { displaySearchWithPlaceholder = strings.EmojiSearch_SearchEmojiPlaceholder } else if isProfilePhotoEmojiSelection || isGroupPhotoEmojiSelection { + //TODO:localize displaySearchWithPlaceholder = "Search" } } let allItemGroups = itemGroups.map { group -> EmojiPagerContentComponent.ItemGroup in + var hasClear = group.isClearable + var isEmbedded = false + if group.id == AnyHashable("featuredTop") { + hasClear = true + isEmbedded = true + } + var headerItem = group.headerItem if let groupId = group.id.base as? ItemCollectionId { @@ -7312,8 +7429,8 @@ public final class EmojiPagerContentComponent: Component { actionButtonTitle: nil, isFeatured: group.isFeatured, isPremiumLocked: group.isPremiumLocked, - isEmbedded: false, - hasClear: group.isClearable, + isEmbedded: isEmbedded, + hasClear: hasClear, collapsedLineCount: group.collapsedLineCount, displayPremiumBadges: false, headerItem: headerItem, @@ -7334,6 +7451,7 @@ public final class EmojiPagerContentComponent: Component { itemContentUniqueId: nil, warpContentsOnEdges: isReactionSelection || isStatusSelection || isProfilePhotoEmojiSelection || isGroupPhotoEmojiSelection, displaySearchWithPlaceholder: displaySearchWithPlaceholder, + searchCategories: searchCategories, searchInitiallyHidden: searchInitiallyHidden, searchIsPlaceholderOnly: false, emptySearchResults: nil, @@ -7405,15 +7523,18 @@ public final class EmojiPagerContentComponent: Component { let strings = context.sharedContext.currentPresentationData.with({ $0 }).strings + let searchCategories: Signal = context.engine.stickers.emojiSearchCategories(kind: .emoji) + return combineLatest( context.account.postbox.itemCollectionsView(orderedItemListCollectionIds: stickerOrderedItemListCollectionIds, namespaces: stickerNamespaces, aroundIndex: nil, count: 10000000), hasPremium(context: context, chatPeerId: chatPeerId, premiumIfSavedMessages: false), hasTrending ? context.account.viewTracker.featuredStickerPacks() : .single([]), context.engine.data.get(TelegramEngine.EngineData.Item.ItemCache.Item(collectionId: Namespaces.CachedItemCollection.featuredStickersConfiguration, id: ValueBoxKey(length: 0))), ApplicationSpecificNotice.dismissedTrendingStickerPacks(accountManager: context.sharedContext.accountManager), - peerSpecificPack + peerSpecificPack, + searchCategories ) - |> map { view, hasPremium, featuredStickerPacks, featuredStickersConfiguration, dismissedTrendingStickerPacks, peerSpecificPack -> EmojiPagerContentComponent in + |> map { view, hasPremium, featuredStickerPacks, featuredStickersConfiguration, dismissedTrendingStickerPacks, peerSpecificPack, searchCategories -> EmojiPagerContentComponent in let actuallyHasPremium = hasPremium let hasPremium = forceHasPremium || hasPremium struct ItemGroup { @@ -7850,6 +7971,7 @@ public final class EmojiPagerContentComponent: Component { itemContentUniqueId: nil, warpContentsOnEdges: isProfilePhotoEmojiSelection || isGroupPhotoEmojiSelection, displaySearchWithPlaceholder: hasSearch ? strings.StickersSearch_SearchStickersPlaceholder : nil, + searchCategories: searchCategories, 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..a950fef04d 100644 --- a/submodules/TelegramUI/Components/EntityKeyboard/Sources/EmojiSearchSearchBarComponent.swift +++ b/submodules/TelegramUI/Components/EntityKeyboard/Sources/EmojiSearchSearchBarComponent.swift @@ -12,20 +12,79 @@ 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 useOpaqueTheme: Bool + let textInputState: TextInputState + let categories: EmojiSearchCategories? let searchTermUpdated: (String?) -> Void + let activateTextInput: () -> Void init( + context: AccountContext, theme: PresentationTheme, strings: PresentationStrings, - searchTermUpdated: @escaping (String?) -> Void + useOpaqueTheme: Bool, + textInputState: TextInputState, + categories: EmojiSearchCategories?, + searchTermUpdated: @escaping (String?) -> Void, + activateTextInput: @escaping () -> Void ) { + self.context = context self.theme = theme self.strings = strings + self.useOpaqueTheme = useOpaqueTheme + self.textInputState = textInputState + self.categories = categories self.searchTermUpdated = searchTermUpdated + self.activateTextInput = activateTextInput } static func ==(lhs: EmojiSearchSearchBarComponent, rhs: EmojiSearchSearchBarComponent) -> Bool { @@ -35,6 +94,15 @@ final class EmojiSearchSearchBarComponent: Component { if lhs.strings !== rhs.strings { return false } + if lhs.useOpaqueTheme != rhs.useOpaqueTheme { + return false + } + if lhs.textInputState != rhs.textInputState { + return false + } + if lhs.categories != rhs.categories { + return false + } return true } @@ -44,20 +112,32 @@ final class EmojiSearchSearchBarComponent: Component { let itemSize: CGSize let itemSpacing: CGFloat let contentSize: CGSize - let sideInset: CGFloat + let leftInset: CGFloat + let rightInset: CGFloat + let itemStartX: 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.itemSpacing = 11.0 + self.leftInset = 6.0 + self.rightInset = 8.0 self.itemSize = CGSize(width: 24.0, height: 24.0) + self.textSpacing = 11.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.itemStartX = self.textFrame.maxX + self.textSpacing + + self.contentSize = CGSize(width: self.itemStartX + 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.itemStartX + 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 +151,57 @@ 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.itemStartX + 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 textView = ComponentView() + private let textContainerView: UIView + + private let tintTextView = ComponentView() + private let tintTextContainerView: UIView + + 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 +209,28 @@ 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.textContainerView = UIView() + self.textContainerView.isUserInteractionEnabled = false + self.tintTextContainerView = UIView() + self.tintTextContainerView.isUserInteractionEnabled = false + + self.roundMaskView = RoundMaskView() + self.tintRoundMaskView = RoundMaskView() self.selectedItemBackground = SimpleLayer() + self.selectedItemTintBackground = SimpleLayer() super.init(frame: frame) @@ -111,20 +244,22 @@ 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.addSubview(self.textContainerView) - //self.layer.mask = self.maskLayer - self.layer.addSublayer(self.maskLayer) - self.layer.masksToBounds = true + self.tintContainerView.addSubview(self.tintScrollView) + self.tintContainerView.addSubview(self.tintTextContainerView) + + 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 +268,30 @@ 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 (component.categories?.groups ?? []).isEmpty || location.x <= itemLayout.itemStartX - 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 _ = self.selectedItem, let categories = component.categories, let group = categories.groups.first(where: { $0.id == itemId }) { + component.searchTermUpdated(group.identifiers.joined(separator: "")) + } else { + component.searchTermUpdated(nil) + } + + break } - self.state?.updated(transition: .immediate) - self.component?.searchTermUpdated(self.selectedItem) - - break } } } @@ -155,7 +302,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 +318,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 +334,54 @@ 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 - - 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 color: UIColor + if component.useOpaqueTheme { + color = self.selectedItem == AnyHashable(item.id) ? component.theme.chat.inputMediaPanel.panelContentOpaqueSearchOverlaySelectedColor : component.theme.chat.inputMediaPanel.panelContentOpaqueSearchOverlayColor + } else { + color = self.selectedItem == AnyHashable(item.id) ? component.theme.chat.inputMediaPanel.panelContentVibrantSearchOverlaySelectedColor : component.theme.chat.inputMediaPanel.panelContentVibrantSearchOverlayColor } - 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 +389,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,48 +417,120 @@ 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.useOpaqueTheme ? component.theme.chat.inputMediaPanel.panelContentOpaqueSearchOverlayHighlightColor.cgColor : component.theme.chat.inputMediaPanel.panelContentVibrantSearchOverlayHighlightColor.cgColor + self.selectedItemTintBackground.backgroundColor = UIColor(white: 1.0, alpha: 0.15).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 } + + let scrollBounds = self.scrollView.bounds + let textOffset = max(0.0, scrollBounds.minX - (itemLayout.itemStartX - itemLayout.textFrame.maxX - itemLayout.textSpacing)) + + transition.setPosition(view: self.textContainerView, position: self.scrollView.center) + transition.setBounds(view: self.textContainerView, bounds: CGRect(origin: CGPoint(x: textOffset, y: 0.0), size: scrollBounds.size)) + + transition.setPosition(view: self.tintTextContainerView, position: self.scrollView.center) + transition.setBounds(view: self.tintTextContainerView, bounds: CGRect(origin: CGPoint(x: textOffset, y: 0.0), size: scrollBounds.size)) } func update(component: EmojiSearchSearchBarComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { 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.useOpaqueTheme ? component.theme.chat.inputMediaPanel.panelContentOpaqueSearchOverlayColor : component.theme.chat.inputMediaPanel.panelContentVibrantSearchOverlayColor + )), + 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.textContainerView.addSubview(textComponentView) + } + transition.setFrame(view: textComponentView, frame: itemLayout.textFrame) + } + if let tintTextComponentView = self.tintTextView.view { + if tintTextComponentView.superview == nil { + self.tintTextContainerView.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)) } + if case .active = component.textInputState { + transition.setBounds(view: self.scrollView, bounds: CGRect(origin: CGPoint(), size: availableSize)) + } if self.scrollView.contentSize != itemLayout.contentSize { self.scrollView.contentSize = itemLayout.contentSize } 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 + + /*if self.scrollView.contentOffset.x != 0.0 { + self.scrollView.setContentOffset(CGPoint(), animated: true) + }*/ + 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/EntityKeyboard.swift b/submodules/TelegramUI/Components/EntityKeyboard/Sources/EntityKeyboard.swift index 49177bee6d..6784518cc8 100644 --- a/submodules/TelegramUI/Components/EntityKeyboard/Sources/EntityKeyboard.swift +++ b/submodules/TelegramUI/Components/EntityKeyboard/Sources/EntityKeyboard.swift @@ -368,7 +368,8 @@ public final class EntityKeyboardComponent: Component { strongSelf.reorderPacks(category: .masks, items: items) } )))) - contentIcons.append(PagerComponentContentIcon(id: "masks", imageName: "Chat/Input/Media/EntityInputMasksIcon")) + //TODO:localize + contentIcons.append(PagerComponentContentIcon(id: "masks", imageName: "Chat/Input/Media/EntityInputMasksIcon", title: "Masks")) if let _ = component.maskContent?.inputInteractionHolder.inputInteraction?.openStickerSettings { contentAccessoryRightButtons.append(AnyComponentWithIdentity(id: "masks", component: AnyComponent(Button( content: AnyComponent(BundleIconComponent( @@ -385,7 +386,7 @@ public final class EntityKeyboardComponent: Component { if let gifContent = component.gifContent { contents.append(AnyComponentWithIdentity(id: "gifs", component: AnyComponent(gifContent))) - var topGifItems: [EntityKeyboardTopPanelComponent.Item] = [] + /*var topGifItems: [EntityKeyboardTopPanelComponent.Item] = [] if component.hasRecentGifs { topGifItems.append(EntityKeyboardTopPanelComponent.Item( id: "recent", @@ -453,8 +454,9 @@ public final class EntityKeyboardComponent: Component { activeContentItemIdUpdated: gifsContentItemIdUpdated, reorderItems: { _ in } - )))) - contentIcons.append(PagerComponentContentIcon(id: "gifs", imageName: "Chat/Input/Media/EntityInputGifsIcon")) + ))))*/ + //TODO:localize + contentIcons.append(PagerComponentContentIcon(id: "gifs", imageName: "Chat/Input/Media/EntityInputGifsIcon", title: "GIFs")) } if let stickerContent = component.stickerContent { @@ -560,7 +562,8 @@ public final class EntityKeyboardComponent: Component { strongSelf.reorderPacks(category: .stickers, items: items) } )))) - contentIcons.append(PagerComponentContentIcon(id: "stickers", imageName: "Chat/Input/Media/EntityInputStickersIcon")) + //TODO:localize + contentIcons.append(PagerComponentContentIcon(id: "stickers", imageName: "Chat/Input/Media/EntityInputStickersIcon", title: "Stickers")) if let _ = component.stickerContent?.inputInteractionHolder.inputInteraction?.openStickerSettings { contentAccessoryRightButtons.append(AnyComponentWithIdentity(id: "stickers", component: AnyComponent(Button( content: AnyComponent(BundleIconComponent( @@ -659,7 +662,8 @@ public final class EntityKeyboardComponent: Component { strongSelf.reorderPacks(category: .emoji, items: items) } )))) - contentIcons.append(PagerComponentContentIcon(id: "emoji", imageName: "Chat/Input/Media/EntityInputEmojiIcon")) + //TODO:localize + contentIcons.append(PagerComponentContentIcon(id: "emoji", imageName: "Chat/Input/Media/EntityInputEmojiIcon", title: "Emoji")) if let _ = deleteBackwards { contentAccessoryLeftButtons.append(AnyComponentWithIdentity(id: "emoji", component: AnyComponent(Button( content: AnyComponent(BundleIconComponent( diff --git a/submodules/TelegramUI/Components/EntityKeyboard/Sources/EntityKeyboardBottomPanelComponent.swift b/submodules/TelegramUI/Components/EntityKeyboard/Sources/EntityKeyboardBottomPanelComponent.swift index fde1c16782..9465e1d7c5 100644 --- a/submodules/TelegramUI/Components/EntityKeyboard/Sources/EntityKeyboardBottomPanelComponent.swift +++ b/submodules/TelegramUI/Components/EntityKeyboard/Sources/EntityKeyboardBottomPanelComponent.swift @@ -10,25 +10,25 @@ import ComponentDisplayAdapters import BundleIconComponent private final class BottomPanelIconComponent: Component { - let imageName: String + let title: String let isHighlighted: Bool let theme: PresentationTheme let action: () -> Void init( - imageName: String, + title: String, isHighlighted: Bool, theme: PresentationTheme, action: @escaping () -> Void ) { - self.imageName = imageName + self.title = title self.isHighlighted = isHighlighted self.theme = theme self.action = action } static func ==(lhs: BottomPanelIconComponent, rhs: BottomPanelIconComponent) -> Bool { - if lhs.imageName != rhs.imageName { + if lhs.title != rhs.title { return false } if lhs.isHighlighted != rhs.isHighlighted { @@ -67,13 +67,23 @@ private final class BottomPanelIconComponent: Component { } func update(component: BottomPanelIconComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { - if self.component?.imageName != component.imageName { - self.contentView.image = UIImage(bundleImageName: component.imageName) + if self.component?.title != component.title { + let text = NSAttributedString(string: component.title, font: Font.medium(15.0), textColor: .white) + let textBounds = text.boundingRect(with: CGSize(width: 120.0, height: 100.0), options: .usesLineFragmentOrigin, context: nil) + self.contentView.image = generateImage(CGSize(width: ceil(textBounds.width), height: ceil(textBounds.height)), rotatedContext: { size, context in + context.clear(CGRect(origin: CGPoint(), size: size)) + UIGraphicsPushContext(context) + text.draw(in: textBounds) + UIGraphicsPopContext() + })?.withRenderingMode(.alwaysTemplate) } self.component = component - let size = CGSize(width: 28.0, height: 28.0) + let textInset: CGFloat = 12.0 + + let textSize = self.contentView.image?.size ?? CGSize() + let size = CGSize(width: textSize.width + textInset * 2.0, height: 28.0) let color = component.isHighlighted ? component.theme.chat.inputMediaPanel.panelHighlightedIconColor : component.theme.chat.inputMediaPanel.panelIconColor @@ -87,8 +97,7 @@ private final class BottomPanelIconComponent: Component { } } - let contentSize = self.contentView.image?.size ?? size - transition.setFrame(view: self.contentView, frame: CGRect(origin: CGPoint(x: floor((size.width - contentSize.width) / 2.0), y: (size.height - contentSize.height) / 2.0), size: contentSize)) + transition.setFrame(view: self.contentView, frame: CGRect(origin: CGPoint(x: floor((size.width - textSize.width) / 2.0), y: (size.height - textSize.height) / 2.0 - 1.0), size: textSize)) return size } @@ -304,7 +313,7 @@ final class EntityKeyboardBottomPanelComponent: Component { var iconInfos: [AnyHashable: (size: CGSize, transition: Transition)] = [:] var iconTotalSize = CGSize() - let iconSpacing: CGFloat = 22.0 + let iconSpacing: CGFloat = 4.0 let navigateToContentId = panelEnvironment.navigateToContentId @@ -326,7 +335,7 @@ final class EntityKeyboardBottomPanelComponent: Component { let iconSize = iconView.update( transition: iconTransition, component: AnyComponent(BottomPanelIconComponent( - imageName: icon.imageName, + title: icon.title, isHighlighted: icon.id == activeContentId, theme: component.theme, action: { @@ -365,12 +374,7 @@ final class EntityKeyboardBottomPanelComponent: Component { self.highlightedIconBackgroundView.isHidden = false transition.setFrame(view: self.highlightedIconBackgroundView, frame: iconFrame) - let cornerRadius: CGFloat - if icon.id == AnyHashable("emoji") { - cornerRadius = min(iconFrame.width, iconFrame.height) / 2.0 - } else { - cornerRadius = 10.0 - } + let cornerRadius: CGFloat = min(iconFrame.width, iconFrame.height) / 2.0 transition.setCornerRadius(layer: self.highlightedIconBackgroundView.layer, cornerRadius: cornerRadius) } diff --git a/submodules/TelegramUI/Components/EntityKeyboard/Sources/GifPagerContentComponent.swift b/submodules/TelegramUI/Components/EntityKeyboard/Sources/GifPagerContentComponent.swift index 651b556585..7ba5364458 100644 --- a/submodules/TelegramUI/Components/EntityKeyboard/Sources/GifPagerContentComponent.swift +++ b/submodules/TelegramUI/Components/EntityKeyboard/Sources/GifPagerContentComponent.swift @@ -139,17 +139,20 @@ public final class GifPagerContentComponent: Component { public let openGifContextMenu: (Item, UIView, CGRect, ContextGesture, Bool) -> Void public let loadMore: (String) -> Void public let openSearch: () -> Void + public let updateSearchQuery: (String?) -> Void public init( performItemAction: @escaping (Item, UIView, CGRect) -> Void, openGifContextMenu: @escaping (Item, UIView, CGRect, ContextGesture, Bool) -> Void, loadMore: @escaping (String) -> Void, - openSearch: @escaping () -> Void + openSearch: @escaping () -> Void, + updateSearchQuery: @escaping (String?) -> Void ) { self.performItemAction = performItemAction self.openGifContextMenu = openGifContextMenu self.loadMore = loadMore self.openSearch = openSearch + self.updateSearchQuery = updateSearchQuery } } @@ -184,6 +187,7 @@ public final class GifPagerContentComponent: Component { public let isLoading: Bool public let loadMoreToken: String? public let displaySearchWithPlaceholder: String? + public let searchCategories: EmojiSearchCategories? public let searchInitiallyHidden: Bool public init( @@ -194,6 +198,7 @@ public final class GifPagerContentComponent: Component { isLoading: Bool, loadMoreToken: String?, displaySearchWithPlaceholder: String?, + searchCategories: EmojiSearchCategories?, searchInitiallyHidden: Bool ) { self.context = context @@ -203,6 +208,7 @@ public final class GifPagerContentComponent: Component { self.isLoading = isLoading self.loadMoreToken = loadMoreToken self.displaySearchWithPlaceholder = displaySearchWithPlaceholder + self.searchCategories = searchCategories self.searchInitiallyHidden = searchInitiallyHidden } @@ -228,6 +234,9 @@ public final class GifPagerContentComponent: Component { if lhs.displaySearchWithPlaceholder != rhs.displaySearchWithPlaceholder { return false } + if lhs.searchCategories != rhs.searchCategories { + return false + } if lhs.searchInitiallyHidden != rhs.searchInitiallyHidden { return false } @@ -307,7 +316,7 @@ public final class GifPagerContentComponent: Component { func visibleItems(for rect: CGRect) -> Range? { let offsetRect = rect.offsetBy(dx: -self.containerInsets.left, dy: -containerInsets.top) - var minVisibleRow = Int(floor((offsetRect.minY - self.verticalSpacing) / (self.itemSize + self.verticalSpacing))) + var minVisibleRow = Int(floor((offsetRect.minY - self.searchHeight - self.verticalSpacing) / (self.itemSize + self.verticalSpacing))) minVisibleRow = max(0, minVisibleRow) let maxVisibleRow = Int(ceil((offsetRect.maxY - self.verticalSpacing) / (self.itemSize + self.verticalSpacing))) @@ -430,6 +439,19 @@ public final class GifPagerContentComponent: Component { } } + private final class SearchHeaderContainer: UIView { + override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { + var result: UIView? + for subview in self.subviews.reversed() { + if let value = subview.hitTest(self.convert(point, to: subview), with: event) { + result = value + break + } + } + return result + } + } + public final class ContentScrollLayer: CALayer { public var mirrorLayer: CALayer? @@ -524,9 +546,12 @@ public final class GifPagerContentComponent: Component { private var vibrancyEffectView: UIVisualEffectView? private let mirrorContentScrollView: UIView private let scrollView: ContentScrollView + private let scrollClippingView: UIView private let placeholdersContainerView: UIView private var visibleSearchHeader: EmojiSearchHeaderView? + private let searchHeaderContainer: SearchHeaderContainer + private let mirrorSearchHeaderContainer: UIView private var visibleItemPlaceholderViews: [ItemKey: ItemPlaceholderView] = [:] private var visibleItemLayers: [ItemKey: ItemLayer] = [:] private var ignoreScrolling: Bool = false @@ -552,6 +577,14 @@ public final class GifPagerContentComponent: Component { self.scrollView = ContentScrollView(mirrorView: self.mirrorContentScrollView) self.scrollView.layer.anchorPoint = CGPoint() + self.searchHeaderContainer = SearchHeaderContainer() + self.searchHeaderContainer.layer.anchorPoint = CGPoint() + self.mirrorSearchHeaderContainer = UIView() + self.mirrorSearchHeaderContainer.layer.anchorPoint = CGPoint() + + self.scrollClippingView = UIView() + self.scrollClippingView.clipsToBounds = true + super.init(frame: frame) self.addSubview(self.backgroundView) @@ -570,9 +603,12 @@ public final class GifPagerContentComponent: Component { self.scrollView.showsHorizontalScrollIndicator = false self.scrollView.scrollsToTop = false self.scrollView.delegate = self - self.addSubview(self.scrollView) + + self.scrollClippingView.addSubview(self.scrollView) + self.addSubview(self.scrollClippingView) self.scrollView.addSubview(self.placeholdersContainerView) + self.addSubview(self.searchHeaderContainer) self.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.tapGesture(_:)))) @@ -665,7 +701,7 @@ public final class GifPagerContentComponent: Component { return } - self.updateVisibleItems(attemptSynchronousLoads: false) + self.updateVisibleItems(attemptSynchronousLoads: false, transition: .immediate, fromScrolling: true) self.updateScrollingOffset(transition: .immediate) @@ -741,7 +777,7 @@ public final class GifPagerContentComponent: Component { self.updateScrollingOffset(transition: transition) } - private func updateVisibleItems(attemptSynchronousLoads: Bool) { + private func updateVisibleItems(attemptSynchronousLoads: Bool, transition: Transition, fromScrolling: Bool) { guard let component = self.component, let itemLayout = self.itemLayout else { return } @@ -776,7 +812,7 @@ public final class GifPagerContentComponent: Component { let itemFrame = itemLayout.frame(at: index).offsetBy(dx: 0.0, dy: searchInset) - let itemTransition: Transition = .immediate + var itemTransition: Transition = transition var updateItemLayerPlaceholder = false let itemLayer: ItemLayer @@ -784,6 +820,7 @@ public final class GifPagerContentComponent: Component { itemLayer = current } else { updateItemLayerPlaceholder = true + itemTransition = .immediate itemLayer = ItemLayer( item: item, @@ -837,7 +874,10 @@ public final class GifPagerContentComponent: Component { let itemPosition = CGPoint(x: itemFrame.midX, y: itemFrame.midY) let itemBounds = CGRect(origin: CGPoint(), size: itemFrame.size) - itemTransition.setFrame(layer: itemLayer, frame: itemFrame) + //itemTransition.setFrame(layer: itemLayer, frame: itemFrame) + itemLayer.bounds = CGRect(origin: CGPoint(), size: itemFrame.size) + itemTransition.setPosition(layer: itemLayer, position: itemFrame.center) + itemLayer.isVisibleForAnimations = true if let placeholderView = self.visibleItemPlaceholderViews[itemId] { @@ -871,6 +911,16 @@ public final class GifPagerContentComponent: Component { for id in removedIds { self.visibleItemLayers.removeValue(forKey: id) } + + transition.setPosition(view: self.searchHeaderContainer, position: self.scrollView.center) + var searchContainerBounds = self.scrollView.bounds + if case .emojiSearch = component.subject { + searchContainerBounds.origin.y = 0.0 + } + transition.setBounds(view: self.searchHeaderContainer, bounds: searchContainerBounds) + + transition.setPosition(view: self.mirrorSearchHeaderContainer, position: self.scrollView.center) + transition.setBounds(view: self.mirrorSearchHeaderContainer, bounds: searchContainerBounds) } private func updateShimmerIfNeeded() { @@ -900,6 +950,7 @@ public final class GifPagerContentComponent: Component { self.vibrancyEffectView = vibrancyEffectView self.backgroundView.addSubview(vibrancyEffectView) vibrancyEffectView.contentView.addSubview(self.mirrorContentScrollView) + vibrancyEffectView.contentView.addSubview(self.mirrorSearchHeaderContainer) } } self.backgroundView.updateColor(color: theme.chat.inputMediaPanel.backgroundColor, enableBlur: true, forceKeepBlur: false, transition: transition.containedViewLayoutTransition) @@ -965,30 +1016,45 @@ public final class GifPagerContentComponent: Component { if let current = self.visibleSearchHeader { visibleSearchHeader = current } else { - visibleSearchHeader = EmojiSearchHeaderView(activated: { [weak self] in + visibleSearchHeader = EmojiSearchHeaderView(activated: { [weak self] isTextInput in guard let strongSelf = self else { return } - strongSelf.component?.inputInteraction.openSearch() + if isTextInput { + strongSelf.component?.inputInteraction.openSearch() + } }, deactivated: { _ in - }, updateQuery: {_, _ in + }, updateQuery: { [weak self] query in + guard let self, let component = self.component else { + return + } + switch query { + case .none: + component.inputInteraction.updateSearchQuery(nil) + case .text: + break + case let .category(value): + component.inputInteraction.updateSearchQuery(value) + } }) self.visibleSearchHeader = visibleSearchHeader - self.scrollView.addSubview(visibleSearchHeader) - self.mirrorContentScrollView.addSubview(visibleSearchHeader.tintContainerView) + self.searchHeaderContainer.addSubview(visibleSearchHeader) + self.mirrorSearchHeaderContainer.addSubview(visibleSearchHeader.tintContainerView) } 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: component.searchCategories, transition: transition) transition.setFrame(view: visibleSearchHeader, frame: searchHeaderFrame, completion: { [weak self] completed in - guard let strongSelf = self, completed, let visibleSearchHeader = strongSelf.visibleSearchHeader else { + let _ = self + let _ = completed + /*guard let strongSelf = self, completed, let visibleSearchHeader = strongSelf.visibleSearchHeader else { return } if visibleSearchHeader.superview != strongSelf.scrollView { strongSelf.scrollView.addSubview(visibleSearchHeader) - strongSelf.mirrorContentScrollView.addSubview(visibleSearchHeader.tintContainerView) - } + strongSelf.mirrorSearchHeaderContainer.addSubview(visibleSearchHeader.tintContainerView) + }*/ }) } else { if let visibleSearchHeader = self.visibleSearchHeader { @@ -998,7 +1064,15 @@ public final class GifPagerContentComponent: Component { } } - self.updateVisibleItems(attemptSynchronousLoads: true) + self.updateVisibleItems(attemptSynchronousLoads: true, transition: transition, fromScrolling: false) + + var clippingInset: CGFloat = 0.0 + if case .emojiSearch = component.subject { + clippingInset = itemLayout.searchInsets.top + itemLayout.searchHeight - 1.0 + } + let clippingFrame = CGRect(origin: CGPoint(x: 0.0, y: clippingInset), size: CGSize(width: availableSize.width, height: availableSize.height - clippingInset)) + transition.setPosition(view: self.scrollClippingView, position: clippingFrame.center) + transition.setBounds(view: self.scrollClippingView, bounds: clippingFrame) return availableSize } diff --git a/submodules/TelegramUI/Components/ForumCreateTopicScreen/Sources/ForumCreateTopicScreen.swift b/submodules/TelegramUI/Components/ForumCreateTopicScreen/Sources/ForumCreateTopicScreen.swift index 50390d204e..d01c58fd8a 100644 --- a/submodules/TelegramUI/Components/ForumCreateTopicScreen/Sources/ForumCreateTopicScreen.swift +++ b/submodules/TelegramUI/Components/ForumCreateTopicScreen/Sources/ForumCreateTopicScreen.swift @@ -553,6 +553,7 @@ private final class ForumCreateTopicScreenComponent: CombinedComponent { isStatusSelection: false, isReactionSelection: false, isEmojiSelection: false, + hasTrending: false, isTopicIconSelection: true, topReactionItems: [], areUnicodeEmojiEnabled: false, @@ -621,6 +622,7 @@ private final class ForumCreateTopicScreenComponent: CombinedComponent { isStatusSelection: false, isReactionSelection: false, isEmojiSelection: false, + hasTrending: false, isTopicIconSelection: true, topReactionItems: [], areUnicodeEmojiEnabled: false, @@ -963,7 +965,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..584f59703c --- /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 { + transition.setTintColor(view: self, color: 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/Components/StorageUsageScreen/BUILD b/submodules/TelegramUI/Components/StorageUsageScreen/BUILD index abb9847aa3..5354215835 100644 --- a/submodules/TelegramUI/Components/StorageUsageScreen/BUILD +++ b/submodules/TelegramUI/Components/StorageUsageScreen/BUILD @@ -41,6 +41,7 @@ swift_library( "//submodules/LegacyComponents", "//submodules/GalleryData", "//submodules/SegmentedControlNode", + "//submodules/TelegramUIPreferences", ], visibility = [ "//visibility:public", diff --git a/submodules/TelegramUI/Components/StorageUsageScreen/Sources/DataUsageScreen.swift b/submodules/TelegramUI/Components/StorageUsageScreen/Sources/DataUsageScreen.swift index f120f422c7..29b04bff7c 100644 --- a/submodules/TelegramUI/Components/StorageUsageScreen/Sources/DataUsageScreen.swift +++ b/submodules/TelegramUI/Components/StorageUsageScreen/Sources/DataUsageScreen.swift @@ -23,19 +23,26 @@ import TelegramAnimatedStickerNode import TelegramStringFormatting import GalleryData import AnimatedTextComponent +import TelegramUIPreferences final class DataUsageScreenComponent: Component { typealias EnvironmentType = ViewControllerComponentContainer.Environment let context: AccountContext let statsSet: StatsSet + let mediaAutoDownloadSettings: MediaAutoDownloadSettings + let makeAutodownloadSettingsController: (Bool) -> ViewController init( context: AccountContext, - statsSet: StatsSet + statsSet: StatsSet, + mediaAutoDownloadSettings: MediaAutoDownloadSettings, + makeAutodownloadSettingsController: @escaping (Bool) -> ViewController ) { self.context = context self.statsSet = statsSet + self.mediaAutoDownloadSettings = mediaAutoDownloadSettings + self.makeAutodownloadSettingsController = makeAutodownloadSettingsController } static func ==(lhs: DataUsageScreenComponent, rhs: DataUsageScreenComponent) -> Bool { @@ -45,6 +52,9 @@ final class DataUsageScreenComponent: Component { if lhs.statsSet != rhs.statsSet { return false } + if lhs.mediaAutoDownloadSettings != rhs.mediaAutoDownloadSettings { + return false + } return true } @@ -278,6 +288,22 @@ final class DataUsageScreenComponent: Component { case wifi } + private final class HeaderContainer: UIView { + override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { + var result: UIView? + for subview in self.subviews.reversed() { + if let value = subview.hitTest(self.convert(point, to: subview), with: event) { + result = value + break + } + } + if result == self { + return nil + } + return result + } + } + class View: UIView, UIScrollViewDelegate { private let scrollView: ScrollViewImpl @@ -285,12 +311,15 @@ final class DataUsageScreenComponent: Component { private var selectedStats: SelectedStats = .all private var expandedCategories: Set = Set() + private var mediaAutoDownloadSettings: MediaAutoDownloadSettings = .defaultSettings + private var mediaAutoDownloadSettingsDisposable: Disposable? + private let navigationBackgroundView: BlurredBackgroundView private let navigationSeparatorLayer: SimpleLayer private let navigationSeparatorLayerContainer: SimpleLayer private let headerView = ComponentView() - private let headerOffsetContainer: UIView + private let headerOffsetContainer: HeaderContainer private let headerDescriptionView = ComponentView() private var doneLabel: ComponentView? @@ -311,6 +340,10 @@ final class DataUsageScreenComponent: Component { private let totalCategoriesTitleView = ComponentView() private let totalCategoriesView = ComponentView() + private let autoDownloadSettingsContainerView: UIView + private let autoDownloadSettingsView = ComponentView() + private let autoDownloadSettingsDescriptionView = ComponentView() + private var component: DataUsageScreenComponent? private weak var state: EmptyComponentState? private var navigationMetrics: (navigationHeight: CGFloat, statusBarHeight: CGFloat)? @@ -323,14 +356,13 @@ final class DataUsageScreenComponent: Component { private var ignoreScrolling: Bool = false override init(frame: CGRect) { - self.headerOffsetContainer = UIView() - self.headerOffsetContainer.isUserInteractionEnabled = false + self.headerOffsetContainer = HeaderContainer() self.navigationBackgroundView = BlurredBackgroundView(color: nil, enableBlur: true) self.navigationBackgroundView.alpha = 0.0 self.navigationSeparatorLayer = SimpleLayer() - self.navigationSeparatorLayer.opacity = 0.0 + self.navigationSeparatorLayer.opacity = 1.0 self.navigationSeparatorLayerContainer = SimpleLayer() self.navigationSeparatorLayerContainer.opacity = 0.0 @@ -338,6 +370,10 @@ final class DataUsageScreenComponent: Component { self.scrollView = ScrollViewImpl() + self.autoDownloadSettingsContainerView = UIView() + self.autoDownloadSettingsContainerView.clipsToBounds = true + self.autoDownloadSettingsContainerView.layer.cornerRadius = 10.0 + super.init(frame: frame) self.scrollView.delaysContentTouches = true @@ -359,6 +395,8 @@ final class DataUsageScreenComponent: Component { self.scrollView.addSubview(self.scrollContainerView) + self.scrollContainerView.addSubview(self.autoDownloadSettingsContainerView) + self.addSubview(self.navigationBackgroundView) self.navigationSeparatorLayerContainer.addSublayer(self.navigationSeparatorLayer) @@ -372,6 +410,7 @@ final class DataUsageScreenComponent: Component { } deinit { + self.mediaAutoDownloadSettingsDisposable?.dispose() } func scrollViewWillBeginDragging(_ scrollView: UIScrollView) { @@ -399,7 +438,7 @@ final class DataUsageScreenComponent: Component { private func updateScrolling(transition: Transition) { let scrollBounds = self.scrollView.bounds - if let headerView = self.headerView.view, let navigationMetrics = self.navigationMetrics { + if let headerView = self.segmentedControlView.view, let navigationMetrics = self.navigationMetrics { var headerOffset: CGFloat = scrollBounds.minY let minY = navigationMetrics.statusBarHeight + floor((navigationMetrics.navigationHeight - navigationMetrics.statusBarHeight) / 2.0) @@ -411,20 +450,36 @@ final class DataUsageScreenComponent: Component { let animatedTransition = Transition(animation: .curve(duration: 0.18, curve: .easeInOut)) let navigationBackgroundAlpha: CGFloat = abs(headerOffset - minOffset) < 4.0 ? 1.0 : 0.0 + let navigationButtonAlpha: CGFloat = abs(headerOffset - minOffset) < 62.0 ? 0.0 : 1.0 + animatedTransition.setAlpha(view: self.navigationBackgroundView, alpha: navigationBackgroundAlpha) animatedTransition.setAlpha(layer: self.navigationSeparatorLayerContainer, alpha: navigationBackgroundAlpha) - let expansionDistance: CGFloat = 32.0 + /*let expansionDistance: CGFloat = 32.0 var expansionDistanceFactor: CGFloat = abs(scrollBounds.maxY - self.scrollView.contentSize.height) / expansionDistance expansionDistanceFactor = max(0.0, min(1.0, expansionDistanceFactor)) - transition.setAlpha(layer: self.navigationSeparatorLayer, alpha: expansionDistanceFactor) + transition.setAlpha(layer: self.navigationSeparatorLayer, alpha: expansionDistanceFactor)*/ - var offsetFraction: CGFloat = abs(headerOffset - minOffset) / 60.0 + /*var offsetFraction: CGFloat = abs(headerOffset - minOffset) / 60.0 offsetFraction = min(1.0, max(0.0, offsetFraction)) - transition.setScale(view: headerView, scale: 1.0 * offsetFraction + 0.8 * (1.0 - offsetFraction)) + transition.setScale(view: headerView, scale: 1.0 * offsetFraction + 0.8 * (1.0 - offsetFraction))*/ transition.setBounds(view: self.headerOffsetContainer, bounds: CGRect(origin: CGPoint(x: 0.0, y: headerOffset), size: self.headerOffsetContainer.bounds.size)) + + if let controller = self.controller?(), let backButtonNode = controller.navigationBar?.backButtonNode { + if backButtonNode.isHidden { + backButtonNode.alpha = 0.0 + backButtonNode.isHidden = false + } + animatedTransition.setAlpha(layer: backButtonNode.layer, alpha: navigationButtonAlpha, completion: { [weak backButtonNode] completed in + if let backButtonNode, completed { + if navigationButtonAlpha.isZero { + backButtonNode.isHidden = true + } + } + }) + } } } @@ -435,6 +490,27 @@ final class DataUsageScreenComponent: Component { if self.allStats == nil { self.allStats = component.statsSet } + if self.mediaAutoDownloadSettingsDisposable == nil { + self.mediaAutoDownloadSettingsDisposable = (component.context.sharedContext.accountManager.sharedData(keys: [ApplicationSpecificSharedDataKeys.automaticMediaDownloadSettings]) + |> map { sharedData -> MediaAutoDownloadSettings in + var automaticMediaDownloadSettings: MediaAutoDownloadSettings + if let value = sharedData.entries[ApplicationSpecificSharedDataKeys.automaticMediaDownloadSettings]?.get(MediaAutoDownloadSettings.self) { + automaticMediaDownloadSettings = value + } else { + automaticMediaDownloadSettings = .defaultSettings + } + return automaticMediaDownloadSettings + } + |> deliverOnMainQueue).start(next: { [weak self] settings in + guard let self else { + return + } + if self.mediaAutoDownloadSettings != settings { + self.mediaAutoDownloadSettings = settings + self.state?.updated(transition: .immediate) + } + }) + } let environment = environment[ViewControllerComponentContainer.Environment.self].value @@ -574,7 +650,7 @@ final class DataUsageScreenComponent: Component { } emptyValue = 1.0 } - if let allStats = self.allStats, allStats.wifi.isEmpty && allStats.cellular.isEmpty { + if totalSize == 0 { chartItems.removeAll() } else { chartItems.append(PieChartComponent.ChartData.Item(id: "empty", displayValue: 0.0, displaySize: 0, value: emptyValue, color: UIColor(rgb: 0xC4C4C6), particle: nil, title: "", mergeable: false, mergeFactor: 1.0)) @@ -635,7 +711,7 @@ final class DataUsageScreenComponent: Component { pieChartTransition.setFrame(view: pieChartComponentView, frame: pieChartFrame) } - if let allStats = self.allStats, allStats.wifi.isEmpty && allStats.cellular.isEmpty { + if totalSize == 0 { let checkColor = environment.theme.list.itemAccentColor var doneLabelTransition = transition @@ -709,7 +785,7 @@ final class DataUsageScreenComponent: Component { headerText = "Data Usage" } let headerViewSize = self.headerView.update( - transition: transition, + transition: .immediate, component: AnyComponent(Text(text: headerText, font: Font.semibold(20.0), color: environment.theme.list.itemPrimaryTextColor)), environment: {}, containerSize: CGSize(width: floor((availableSize.width) / 0.8), height: 100.0) @@ -717,10 +793,10 @@ final class DataUsageScreenComponent: Component { let headerViewFrame = CGRect(origin: CGPoint(x: floor((availableSize.width - headerViewSize.width) / 2.0), y: contentHeight), size: headerViewSize) if let headerComponentView = self.headerView.view { if headerComponentView.superview == nil { - self.headerOffsetContainer.addSubview(headerComponentView) + self.scrollContainerView.addSubview(headerComponentView) } transition.setPosition(view: headerComponentView, position: headerViewFrame.center) - transition.setBounds(view: headerComponentView, bounds: CGRect(origin: CGPoint(), size: headerViewFrame.size)) + headerComponentView.bounds = CGRect(origin: CGPoint(), size: headerViewFrame.size) } contentHeight += headerViewSize.height @@ -747,7 +823,7 @@ final class DataUsageScreenComponent: Component { let totalUsageText: String = timestampString let headerDescriptionSize = self.headerDescriptionView.update( - transition: transition, + transition: .immediate, component: AnyComponent(MultilineTextComponent(text: .markdown(text: totalUsageText, attributes: MarkdownAttributes( body: body, bold: bold, @@ -762,16 +838,22 @@ final class DataUsageScreenComponent: Component { if headerDescriptionComponentView.superview == nil { self.scrollContainerView.addSubview(headerDescriptionComponentView) } - transition.setFrame(view: headerDescriptionComponentView, frame: headerDescriptionFrame) + transition.setPosition(view: headerDescriptionComponentView, position: headerDescriptionFrame.center) + headerDescriptionComponentView.bounds = CGRect(origin: CGPoint(), size: headerDescriptionFrame.size) } contentHeight += headerDescriptionSize.height contentHeight += 8.0 contentHeight += 12.0 - if let allStats = self.allStats, allStats.wifi.isEmpty && allStats.cellular.isEmpty { + if totalSize == 0 { if let chartTotalLabelView = self.chartTotalLabel.view { - chartTotalLabelView.removeFromSuperview() + transition.setAlpha(view: chartTotalLabelView, alpha: 0.0) + + let chartTotalLabelSize = chartTotalLabelView.bounds.size + let chartAreaHeight: CGFloat = 100.0 + let totalLabelFrame = CGRect(origin: CGPoint(x: floor((availableSize.width - chartTotalLabelSize.width) / 2.0), y: pieChartFrame.minY + floor((chartAreaHeight - chartTotalLabelSize.height) / 2.0)), size: chartTotalLabelSize) + transition.setFrame(view: chartTotalLabelView, frame: totalLabelFrame) } } else { let sizeText = dataSizeString(Int(totalSize), forceDecimal: true, formatting: DataSizeStringFormatting(strings: environment.strings, decimalSeparator: ".")) @@ -810,9 +892,17 @@ final class DataUsageScreenComponent: Component { if chartTotalLabelView.superview == nil { self.scrollContainerView.addSubview(chartTotalLabelView) } - let totalLabelFrame = CGRect(origin: CGPoint(x: pieChartFrame.minX + floor((pieChartFrame.width - chartTotalLabelSize.width) / 2.0), y: pieChartFrame.minY + floor((pieChartFrame.height - chartTotalLabelSize.height) / 2.0)), size: chartTotalLabelSize) + + let chartAreaHeight: CGFloat + if totalSize == 0 { + chartAreaHeight = 100.0 + } else { + chartAreaHeight = pieChartFrame.height + } + + let totalLabelFrame = CGRect(origin: CGPoint(x: pieChartFrame.minX + floor((pieChartFrame.width - chartTotalLabelSize.width) / 2.0), y: pieChartFrame.minY + floor((chartAreaHeight - chartTotalLabelSize.height) / 2.0)), size: chartTotalLabelSize) labelTransition.setFrame(view: chartTotalLabelView, frame: totalLabelFrame) - labelTransition.setAlpha(view: chartTotalLabelView, alpha: listCategories.isEmpty ? 0.0 : 1.0) + transition.setAlpha(view: chartTotalLabelView, alpha: 1.0) } } @@ -831,7 +921,7 @@ final class DataUsageScreenComponent: Component { return } self.selectedStats = id - self.state?.updated(transition: Transition(animation: .none).withUserData(AnimationHint(value: .modeChanged))) + self.state?.updated(transition: Transition(animation: .curve(duration: 0.4, curve: .spring)).withUserData(AnimationHint(value: .modeChanged))) })), environment: {}, containerSize: CGSize(width: availableSize.width - sideInset * 2.0, height: 100.0) @@ -839,7 +929,7 @@ final class DataUsageScreenComponent: Component { let segmentedControlFrame = CGRect(origin: CGPoint(x: floor((availableSize.width - segmentedSize.width) * 0.5), y: contentHeight), size: segmentedSize) if let segmentedControlComponentView = self.segmentedControlView.view { if segmentedControlComponentView.superview == nil { - self.scrollContainerView.addSubview(segmentedControlComponentView) + self.headerOffsetContainer.addSubview(segmentedControlComponentView) } transition.setFrame(view: segmentedControlComponentView, frame: segmentedControlFrame) } @@ -893,9 +983,11 @@ final class DataUsageScreenComponent: Component { let categoriesDescriptionFrame = CGRect(origin: CGPoint(x: sideInset + 15.0, y: contentHeight), size: categoriesDescriptionSize) if let categoriesDescriptionComponentView = self.categoriesDescriptionView.view { if categoriesDescriptionComponentView.superview == nil { + categoriesDescriptionComponentView.layer.anchorPoint = CGPoint() self.scrollContainerView.addSubview(categoriesDescriptionComponentView) } - transition.setFrame(view: categoriesDescriptionComponentView, frame: categoriesDescriptionFrame) + transition.setPosition(view: categoriesDescriptionComponentView, position: categoriesDescriptionFrame.topLeft) + categoriesDescriptionComponentView.bounds = CGRect(origin: CGPoint(), size: categoriesDescriptionFrame.size) } contentHeight += categoriesDescriptionSize.height contentHeight += 40.0 @@ -924,9 +1016,12 @@ final class DataUsageScreenComponent: Component { let totalCategoriesTitleFrame = CGRect(origin: CGPoint(x: sideInset + 15.0, y: contentHeight), size: totalCategoriesTitleSize) if let totalCategoriesTitleComponentView = self.totalCategoriesTitleView.view { if totalCategoriesTitleComponentView.superview == nil { + totalCategoriesTitleComponentView.layer.anchorPoint = CGPoint() self.scrollContainerView.addSubview(totalCategoriesTitleComponentView) } - transition.setFrame(view: totalCategoriesTitleComponentView, frame: totalCategoriesTitleFrame) + + transition.setPosition(view: totalCategoriesTitleComponentView, position: totalCategoriesTitleFrame.topLeft) + totalCategoriesTitleComponentView.bounds = CGRect(origin: CGPoint(), size: totalCategoriesTitleFrame.size) } contentHeight += totalCategoriesTitleSize.height contentHeight += 8.0 @@ -954,32 +1049,103 @@ final class DataUsageScreenComponent: Component { contentHeight += totalCategoriesSize.height contentHeight += 40.0 - if let allStats = self.allStats, !(allStats.wifi.isEmpty && allStats.cellular.isEmpty) { - let clearButtonSize = self.clearButtonView.update( + var autoDownloadSettingsContentHeight: CGFloat = 0.0 + //TODO:localize + let autoDownloadSettingsSize: CGSize + if case .all = self.selectedStats, let autoDownloadSettingsComponentView = self.autoDownloadSettingsView.view { + autoDownloadSettingsSize = autoDownloadSettingsComponentView.bounds.size + } else { + autoDownloadSettingsSize = self.autoDownloadSettingsView.update( transition: transition, - component: AnyComponent(DataButtonComponent( + component: AnyComponent(StoragePeerTypeItemComponent( theme: environment.theme, - title: "Reset Statistics", - action: { [weak self] in - self?.requestClear() + iconName: self.selectedStats == .mobile ? "Settings/Menu/Cellular" : "Settings/Menu/WiFi", + title: "Auto-Download Settings", + subtitle: stringForAutoDownloadSetting(strings: environment.strings, decimalSeparator: environment.dateTimeFormat.decimalSeparator, settings: self.mediaAutoDownloadSettings, isCellular: self.selectedStats == .mobile), + value: "", + hasNext: false, + action: { [weak self] sourceView in + guard let self, let component = self.component else { + return + } + self.controller?()?.push(component.makeAutodownloadSettingsController(self.selectedStats == .mobile)) } )), environment: {}, - containerSize: CGSize(width: availableSize.width - sideInset * 2.0, height: 1000.0) + containerSize: CGSize(width: availableSize.width - sideInset * 2.0, height: 10000.0) ) - let clearButtonFrame = CGRect(origin: CGPoint(x: sideInset, y: contentHeight), size: clearButtonSize) - if let clearButtonComponentView = self.clearButtonView.view { - if clearButtonComponentView.superview == nil { - self.scrollContainerView.addSubview(clearButtonComponentView) - } - transition.setFrame(view: clearButtonComponentView, frame: clearButtonFrame) + } + let itemFrame = CGRect(origin: CGPoint(x: 0.0, y: autoDownloadSettingsContentHeight), size: autoDownloadSettingsSize) + if let itemView = self.autoDownloadSettingsView.view { + if itemView.superview == nil { + self.autoDownloadSettingsContainerView.addSubview(itemView) } + transition.setFrame(view: itemView, frame: itemFrame) + } + autoDownloadSettingsContentHeight += autoDownloadSettingsSize.height + self.autoDownloadSettingsContainerView.backgroundColor = environment.theme.list.itemBlocksBackgroundColor + transition.setFrame(view: self.autoDownloadSettingsContainerView, frame: CGRect(origin: CGPoint(x: sideInset, y: contentHeight), size: CGSize(width: availableSize.width - sideInset * 2.0, height: autoDownloadSettingsContentHeight))) + + let autoDownloadSettingsDescriptionSize: CGSize + if case .all = self.selectedStats, let autoDownloadSettingsDescriptionComponentView = self.autoDownloadSettingsDescriptionView.view { + autoDownloadSettingsDescriptionSize = autoDownloadSettingsDescriptionComponentView.bounds.size + } else { + autoDownloadSettingsDescriptionSize = self.autoDownloadSettingsDescriptionView.update( + transition: transition, + component: AnyComponent(MultilineTextComponent( + text: .markdown( + text: self.selectedStats == .mobile ? "You can change your auto-download settings for media to reduce data usage when cellular." : "You can change your auto-download settings for media to reduce data usage when on wifi.", attributes: MarkdownAttributes( + body: body, + bold: bold, + link: body, + linkAttribute: { _ in nil } + ) + ), + maximumNumberOfLines: 0 + )), + environment: {}, + containerSize: CGSize(width: availableSize.width - sideInset * 2.0 - 15.0 * 2.0, height: 10000.0) + ) + } + let autoDownloadSettingsDescriptionFrame = CGRect(origin: CGPoint(x: sideInset + 15.0, y: contentHeight + autoDownloadSettingsContentHeight + 8.0), size: autoDownloadSettingsDescriptionSize) + if let autoDownloadSettingsDescriptionComponentView = self.autoDownloadSettingsDescriptionView.view { + if autoDownloadSettingsDescriptionComponentView.superview == nil { + self.scrollContainerView.addSubview(autoDownloadSettingsDescriptionComponentView) + } + transition.setFrame(view: autoDownloadSettingsDescriptionComponentView, frame: autoDownloadSettingsDescriptionFrame) + transition.setAlpha(view: autoDownloadSettingsDescriptionComponentView, alpha: self.selectedStats == .all ? 0.0 : 1.0) + } + + transition.setAlpha(view: self.autoDownloadSettingsContainerView, alpha: self.selectedStats == .all ? 0.0 : 1.0) + + let combinedAutoDownloadSettingsContentHeight = autoDownloadSettingsContentHeight + 8.0 + autoDownloadSettingsDescriptionSize.height + 40.0 + if self.selectedStats != .all { + contentHeight += combinedAutoDownloadSettingsContentHeight + } + + let clearButtonSize = self.clearButtonView.update( + transition: transition, + component: AnyComponent(DataButtonComponent( + theme: environment.theme, + title: "Reset Statistics", + action: { [weak self] in + self?.requestClear() + } + )), + environment: {}, + containerSize: CGSize(width: availableSize.width - sideInset * 2.0, height: 1000.0) + ) + let clearButtonFrame = CGRect(origin: CGPoint(x: sideInset, y: contentHeight), size: clearButtonSize) + if let clearButtonComponentView = self.clearButtonView.view { + if clearButtonComponentView.superview == nil { + self.scrollContainerView.addSubview(clearButtonComponentView) + } + transition.setFrame(view: clearButtonComponentView, frame: clearButtonFrame) + transition.setAlpha(view: clearButtonComponentView, alpha: totalSize != 0 ? 1.0 : 0.0) + } + if totalSize != 0 { contentHeight += clearButtonSize.height contentHeight += 40.0 - } else { - if let clearButtonComponentView = self.clearButtonView.view { - clearButtonComponentView.isHidden = true - } } contentHeight += bottomInset @@ -1037,7 +1203,28 @@ final class DataUsageScreenComponent: Component { } private func requestClear() { - self.commitClear() + guard let component = self.component else { + return + } + + let context = component.context + + let presentationData = context.sharedContext.currentPresentationData.with { $0 } + let actionSheet = ActionSheetController(presentationData: presentationData) + + //TODO:localize + actionSheet.setItemGroups([ActionSheetItemGroup(items: [ + ActionSheetButtonItem(title: "Reset Statistics", color: .destructive, action: { [weak self, weak actionSheet] in + actionSheet?.dismissAnimated() + + self?.commitClear() + }) + ]), ActionSheetItemGroup(items: [ + ActionSheetButtonItem(title: presentationData.strings.Common_Cancel, color: .accent, font: .bold, action: { [weak actionSheet] in + actionSheet?.dismissAnimated() + }) + ])]) + self.controller?()?.present(actionSheet, in: .window(.root)) } private func commitClear() { @@ -1073,13 +1260,11 @@ public final class DataUsageScreen: ViewControllerComponentContainer { return self.readyValue } - fileprivate var childCompleted: ((@escaping () -> Void) -> Void)? - - public init(context: AccountContext, stats: NetworkUsageStats) { + public init(context: AccountContext, stats: NetworkUsageStats, mediaAutoDownloadSettings: MediaAutoDownloadSettings, makeAutodownloadSettingsController: @escaping (Bool) -> ViewController) { self.context = context //let componentReady = Promise() - super.init(context: context, component: DataUsageScreenComponent(context: context, statsSet: DataUsageScreenComponent.StatsSet(stats: stats)), navigationBarAppearance: .transparent) + super.init(context: context, component: DataUsageScreenComponent(context: context, statsSet: DataUsageScreenComponent.StatsSet(stats: stats), mediaAutoDownloadSettings: mediaAutoDownloadSettings, makeAutodownloadSettingsController: makeAutodownloadSettingsController), navigationBarAppearance: .transparent) //self.readyValue.set(componentReady.get() |> timeout(0.3, queue: .mainQueue(), alternate: .single(true))) self.readyValue.set(.single(true)) @@ -1093,3 +1278,71 @@ public final class DataUsageScreen: ViewControllerComponentContainer { super.viewDidLoad() } } + +public func autodownloadDataSizeString(_ size: Int64, decimalSeparator: String = ".") -> String { + if size >= 1024 * 1024 * 1024 { + let remainder = (size % (1024 * 1024 * 1024)) / (1024 * 1024 * 102) + if remainder != 0 { + return "\(size / (1024 * 1024 * 1024))\(decimalSeparator)\(remainder) GB" + } else { + return "\(size / (1024 * 1024 * 1024)) GB" + } + } else if size >= 1024 * 1024 { + let remainder = (size % (1024 * 1024)) / (1024 * 102) + if size < 10 * 1024 * 1024 { + return "\(size / (1024 * 1024))\(decimalSeparator)\(remainder) MB" + } else { + return "\(size / (1024 * 1024)) MB" + } + } else if size >= 1024 { + return "\(size / 1024) KB" + } else { + return "\(size) B" + } +} + +private func stringForAutoDownloadTypes(strings: PresentationStrings, decimalSeparator: String, photo: Bool, videoSize: Int64?, fileSize: Int64?) -> String { + var types: [String] = [] + if photo && videoSize == nil { + types.append(strings.ChatSettings_AutoDownloadSettings_TypePhoto) + } + if let videoSize = videoSize { + if photo { + types.append(strings.ChatSettings_AutoDownloadSettings_TypeMedia(autodownloadDataSizeString(videoSize, decimalSeparator: decimalSeparator)).string) + } else { + types.append(strings.ChatSettings_AutoDownloadSettings_TypeVideo(autodownloadDataSizeString(videoSize, decimalSeparator: decimalSeparator)).string) + } + } + if let fileSize = fileSize { + types.append(strings.ChatSettings_AutoDownloadSettings_TypeFile(autodownloadDataSizeString(fileSize, decimalSeparator: decimalSeparator)).string) + } + + if types.isEmpty { + return strings.ChatSettings_AutoDownloadSettings_OffForAll + } + + var string: String = "" + for i in 0 ..< types.count { + if !string.isEmpty { + string.append(strings.ChatSettings_AutoDownloadSettings_Delimeter) + } + string.append(types[i]) + } + return string +} + + +private func stringForAutoDownloadSetting(strings: PresentationStrings, decimalSeparator: String, settings: MediaAutoDownloadSettings, isCellular: Bool) -> String { + let connection: MediaAutoDownloadConnection = isCellular ? settings.cellular : settings.wifi + if !connection.enabled { + return strings.ChatSettings_AutoDownloadSettings_OffForAll + } else { + let categories = effectiveAutodownloadCategories(settings: settings, networkType: isCellular ? .cellular : .wifi) + + let photo = isAutodownloadEnabledForAnyPeerType(category: categories.photo) + let video = isAutodownloadEnabledForAnyPeerType(category: categories.video) + let file = isAutodownloadEnabledForAnyPeerType(category: categories.file) + + return stringForAutoDownloadTypes(strings: strings, decimalSeparator: decimalSeparator, photo: photo, videoSize: video ? categories.video.sizeLimit : nil, fileSize: file ? categories.file.sizeLimit : nil) + } +} diff --git a/submodules/TelegramUI/Components/StorageUsageScreen/Sources/PieChartComponent.swift b/submodules/TelegramUI/Components/StorageUsageScreen/Sources/PieChartComponent.swift index 8ffb2a11dc..a3a88e5411 100644 --- a/submodules/TelegramUI/Components/StorageUsageScreen/Sources/PieChartComponent.swift +++ b/submodules/TelegramUI/Components/StorageUsageScreen/Sources/PieChartComponent.swift @@ -440,19 +440,29 @@ final class PieChartComponent: Component { let beforeSpacingFraction: CGFloat = 1.0 let afterSpacingFraction: CGFloat = 1.0 - let innerStartAngle = startAngle + innerAngleSpacing * 0.5 - let arcInnerStartAngle = startAngle + innerAngleSpacing * 0.5 * beforeSpacingFraction + let itemInnerAngleSpacing: CGFloat + let itemAngleSpacing: CGFloat + if abs(angleValue - CGFloat.pi * 2.0) <= 0.0001 { + itemInnerAngleSpacing = 0.0 + itemAngleSpacing = 0.0 + } else { + itemInnerAngleSpacing = innerAngleSpacing + itemAngleSpacing = angleSpacing + } - var innerEndAngle = startAngle + angleValue - innerAngleSpacing * 0.5 + let innerStartAngle = startAngle + itemInnerAngleSpacing * 0.5 + let arcInnerStartAngle = startAngle + itemInnerAngleSpacing * 0.5 * beforeSpacingFraction + + var innerEndAngle = startAngle + angleValue - itemInnerAngleSpacing * 0.5 innerEndAngle = max(innerEndAngle, innerStartAngle) - var arcInnerEndAngle = startAngle + angleValue - innerAngleSpacing * 0.5 * afterSpacingFraction + var arcInnerEndAngle = startAngle + angleValue - itemInnerAngleSpacing * 0.5 * afterSpacingFraction arcInnerEndAngle = max(arcInnerEndAngle, arcInnerStartAngle) - let outerStartAngle = startAngle + angleSpacing * 0.5 - let arcOuterStartAngle = startAngle + angleSpacing * 0.5 * beforeSpacingFraction - var outerEndAngle = startAngle + angleValue - angleSpacing * 0.5 + let outerStartAngle = startAngle + itemAngleSpacing * 0.5 + let arcOuterStartAngle = startAngle + itemAngleSpacing * 0.5 * beforeSpacingFraction + var outerEndAngle = startAngle + angleValue - itemAngleSpacing * 0.5 outerEndAngle = max(outerEndAngle, outerStartAngle) - var arcOuterEndAngle = startAngle + angleValue - angleSpacing * 0.5 * afterSpacingFraction + var arcOuterEndAngle = startAngle + angleValue - itemAngleSpacing * 0.5 * afterSpacingFraction arcOuterEndAngle = max(arcOuterEndAngle, arcOuterStartAngle) let itemColor: UIColor = isEmpty ? emptyColor : item.color diff --git a/submodules/TelegramUI/Components/StorageUsageScreen/Sources/StoragePeerTypeItemComponent.swift b/submodules/TelegramUI/Components/StorageUsageScreen/Sources/StoragePeerTypeItemComponent.swift index b239cf9ddf..9a75fced24 100644 --- a/submodules/TelegramUI/Components/StorageUsageScreen/Sources/StoragePeerTypeItemComponent.swift +++ b/submodules/TelegramUI/Components/StorageUsageScreen/Sources/StoragePeerTypeItemComponent.swift @@ -156,6 +156,7 @@ final class StoragePeerTypeItemComponent: Component { func update(component: StoragePeerTypeItemComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { let themeUpdated = self.component?.theme !== component.theme + let previousComponent = self.component self.component = component let leftInset: CGFloat = 62.0 @@ -225,29 +226,42 @@ final class StoragePeerTypeItemComponent: Component { if let titleView = self.title.view { if titleView.superview == nil { titleView.isUserInteractionEnabled = false + titleView.layer.anchorPoint = CGPoint() self.addSubview(titleView) } - transition.setFrame(view: titleView, frame: titleFrame) + + transition.setPosition(view: titleView, position: titleFrame.topLeft) + titleView.bounds = CGRect(origin: CGPoint(), size: titleFrame.size) } if let subtitleView = self.subtitle?.view, let subtitleFrame { if subtitleView.superview == nil { subtitleView.isUserInteractionEnabled = false + subtitleView.layer.anchorPoint = CGPoint() self.addSubview(subtitleView) } - transition.setFrame(view: subtitleView, frame: subtitleFrame) + transition.setPosition(view: subtitleView, position: subtitleFrame.topLeft) + subtitleView.bounds = CGRect(origin: CGPoint(), size: subtitleFrame.size) } if let labelView = self.label.view { if labelView.superview == nil { labelView.isUserInteractionEnabled = false + labelView.layer.anchorPoint = CGPoint(x: 1.0, y: 0.0) self.addSubview(labelView) } - transition.setFrame(view: labelView, frame: labelFrame) + + transition.setPosition(view: labelView, position: labelFrame.topRight) + labelView.bounds = CGRect(origin: CGPoint(), size: labelFrame.size) } - if themeUpdated { + if themeUpdated || previousComponent?.iconName != component.iconName { self.separatorLayer.backgroundColor = component.theme.list.itemBlocksSeparatorColor.cgColor self.iconView.image = UIImage(bundleImageName: component.iconName) - self.arrowIconView.image = PresentationResourcesItemList.disclosureOptionArrowsImage(component.theme) + + if component.value.isEmpty { + self.arrowIconView.image = PresentationResourcesItemList.disclosureArrowImage(component.theme) + } else { + self.arrowIconView.image = PresentationResourcesItemList.disclosureOptionArrowsImage(component.theme) + } } if let image = self.iconView.image { diff --git a/submodules/TelegramUI/Components/StorageUsageScreen/Sources/StorageUsageScreen.swift b/submodules/TelegramUI/Components/StorageUsageScreen/Sources/StorageUsageScreen.swift index 4965000c73..b892fbb1e0 100644 --- a/submodules/TelegramUI/Components/StorageUsageScreen/Sources/StorageUsageScreen.swift +++ b/submodules/TelegramUI/Components/StorageUsageScreen/Sources/StorageUsageScreen.swift @@ -1263,7 +1263,7 @@ final class StorageUsageScreenComponent: Component { if let aggregatedData = self.aggregatedData { for (key, value) in aggregatedData.contextStats.categories { totalSize += value.size - if aggregatedData.selectedCategories.contains(Category(key)) { + if aggregatedData.selectedCategories.isEmpty || aggregatedData.selectedCategories.contains(Category(key)) { totalSelectedCategorySize += value.size } } @@ -1295,7 +1295,7 @@ final class StorageUsageScreenComponent: Component { } let categoryFraction: Double - if !aggregatedData.selectedCategories.contains(category) { + if !aggregatedData.selectedCategories.isEmpty && !aggregatedData.selectedCategories.contains(category) { categoryFraction = 0.0 } else if categorySize == 0 || totalSelectedCategorySize == 0 { categoryFraction = 0.0 @@ -1771,7 +1771,6 @@ final class StorageUsageScreenComponent: Component { contentHeight += keepDurationTitleSize.height contentHeight += 8.0 - var keepContentHeight: CGFloat = 0.0 for i in 0 ..< 3 { let item: ComponentView @@ -3011,82 +3010,6 @@ final class StorageUsageScreenComponent: Component { }) } } - - /*if !aggregatedData.selectedCategories.isEmpty { - let peerId: EnginePeer.Id? = component.peer?.id - - var mappedCategories: [StorageUsageStats.CategoryKey] = [] - for category in categories { - switch category { - case .photos: - mappedCategories.append(.photos) - case .videos: - mappedCategories.append(.videos) - case .files: - mappedCategories.append(.files) - case .music: - mappedCategories.append(.music) - case .other: - break - case .stickers: - mappedCategories.append(.stickers) - case .avatars: - mappedCategories.append(.avatars) - case .misc: - mappedCategories.append(.misc) - } - } - - self.isClearing = true - self.state?.updated(transition: .immediate) - - let _ = (component.context.engine.resources.clearStorage(peerId: peerId, categories: mappedCategories, includeMessages: [], excludeMessages: []) - |> deliverOnMainQueue).start(completed: { [weak self] in - guard let self, let _ = self.component, let aggregatedData = self.aggregatedData else { - return - } - var totalSize: Int64 = 0 - - let contextStats = aggregatedData.contextStats - - for category in categories { - let mappedCategory: StorageUsageStats.CategoryKey - switch category { - case .photos: - mappedCategory = .photos - case .videos: - mappedCategory = .videos - case .files: - mappedCategory = .files - case .music: - mappedCategory = .music - case .other: - continue - case .stickers: - mappedCategory = .stickers - case .avatars: - mappedCategory = .avatars - case .misc: - mappedCategory = .misc - } - - if let value = contextStats.categories[mappedCategory] { - totalSize += value.size - } - } - - self.reloadStats(firstTime: false, completion: { [weak self] in - guard let self else { - return - } - if totalSize != 0 { - self.reportClearedStorage(size: totalSize) - } - }) - }) - } else if !peers.isEmpty || !messages.isEmpty { - - }*/ } private func openKeepMediaCategory(mappedCategory: CacheStorageSettings.PeerStorageCategory, sourceView: StoragePeerTypeItemComponent.View) { diff --git a/submodules/TelegramUI/Sources/ChatController.swift b/submodules/TelegramUI/Sources/ChatController.swift index fc78d69305..adf90daa50 100644 --- a/submodules/TelegramUI/Sources/ChatController.swift +++ b/submodules/TelegramUI/Sources/ChatController.swift @@ -1169,6 +1169,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G isStatusSelection: false, isReactionSelection: true, isEmojiSelection: false, + hasTrending: false, topReactionItems: reactionItems, areUnicodeEmojiEnabled: false, areCustomEmojiEnabled: true, @@ -7214,6 +7215,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 { @@ -7228,6 +7234,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 { @@ -8533,6 +8541,8 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G return } + strongSelf.dismissAllTooltips() + strongSelf.mediaRecordingModeTooltipController?.dismiss() strongSelf.interfaceInteraction?.updateShowWebView { _ in return false @@ -8544,14 +8554,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 } } @@ -8560,22 +8568,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 } @@ -8876,8 +8881,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 } @@ -10287,8 +10291,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 @@ -12393,6 +12397,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)? @@ -12412,6 +12417,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 } @@ -12425,14 +12433,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 @@ -13556,22 +13571,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 } @@ -18136,6 +18148,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 70fb3f2863..9d127c45ab 100644 --- a/submodules/TelegramUI/Sources/ChatControllerNode.swift +++ b/submodules/TelegramUI/Sources/ChatControllerNode.swift @@ -617,7 +617,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) diff --git a/submodules/TelegramUI/Sources/ChatMessageTextBubbleContentNode.swift b/submodules/TelegramUI/Sources/ChatMessageTextBubbleContentNode.swift index 6ff743a4a9..96ec6c6b38 100644 --- a/submodules/TelegramUI/Sources/ChatMessageTextBubbleContentNode.swift +++ b/submodules/TelegramUI/Sources/ChatMessageTextBubbleContentNode.swift @@ -55,6 +55,7 @@ class ChatMessageTextBubbleContentNode: ChatMessageBubbleContentNode { private let textAccessibilityOverlayNode: TextAccessibilityOverlayNode private let statusNode: ChatMessageDateAndStatusNode private var linkHighlightingNode: LinkHighlightingNode? + private var shimmeringNode: ShimmeringLinkNode? private var textSelectionNode: TextSelectionNode? private var textHighlightingNodes: [LinkHighlightingNode] = [] diff --git a/submodules/TelegramUI/Sources/ChatRecentActionsControllerNode.swift b/submodules/TelegramUI/Sources/ChatRecentActionsControllerNode.swift index 5a14599c26..8e43fa2241 100644 --- a/submodules/TelegramUI/Sources/ChatRecentActionsControllerNode.swift +++ b/submodules/TelegramUI/Sources/ChatRecentActionsControllerNode.swift @@ -992,29 +992,34 @@ final class ChatRecentActionsControllerNode: ViewControllerTracingNode { let packReference: StickerPackReference = .name(name) strongSelf.presentController(StickerPackScreen(context: strongSelf.context, mainStickerPack: packReference, stickerPacks: [packReference], parentNavigationController: strongSelf.getNavigationController()), .window(.root), nil) case let .invoice(slug, invoice): - let inputData = Promise() - inputData.set(BotCheckoutController.InputData.fetch(context: strongSelf.context, source: .slug(slug)) - |> map(Optional.init) - |> `catch` { _ -> Signal in - return .single(nil) - }) - strongSelf.controllerInteraction.presentController(BotCheckoutController(context: strongSelf.context, invoice: invoice, source: .slug(slug), inputData: inputData, completed: { currencyValue, receiptMessageId in - guard let strongSelf = self else { - return - } - let _ = strongSelf - /*strongSelf.present(UndoOverlayController(presentationData: strongSelf.presentationData, content: .paymentSent(currencyValue: currencyValue, itemTitle: invoice.title), elevatedLayout: false, action: { action in - guard let strongSelf = self, let receiptMessageId = receiptMessageId else { - return false + if let invoice { + let inputData = Promise() + inputData.set(BotCheckoutController.InputData.fetch(context: strongSelf.context, source: .slug(slug)) + |> map(Optional.init) + |> `catch` { _ -> Signal in + return .single(nil) + }) + strongSelf.controllerInteraction.presentController(BotCheckoutController(context: strongSelf.context, invoice: invoice, source: .slug(slug), inputData: inputData, completed: { currencyValue, receiptMessageId in + guard let strongSelf = self else { + return } - - if case .info = action { - strongSelf.present(BotReceiptController(context: strongSelf.context, messageId: receiptMessageId), in: .window(.root), with: ViewControllerPresentationArguments(presentationAnimation: .modalSheet)) - return true - } - return false - }), in: .current)*/ - }), ViewControllerPresentationArguments(presentationAnimation: .modalSheet)) + let _ = strongSelf + /*strongSelf.present(UndoOverlayController(presentationData: strongSelf.presentationData, content: .paymentSent(currencyValue: currencyValue, itemTitle: invoice.title), elevatedLayout: false, action: { action in + guard let strongSelf = self, let receiptMessageId = receiptMessageId else { + return false + } + + if case .info = action { + strongSelf.present(BotReceiptController(context: strongSelf.context, messageId: receiptMessageId), in: .window(.root), with: ViewControllerPresentationArguments(presentationAnimation: .modalSheet)) + return true + } + return false + }), in: .current)*/ + }), ViewControllerPresentationArguments(presentationAnimation: .modalSheet)) + } else { + //TODO:localize + strongSelf.controllerInteraction.presentController(textAlertController(context: strongSelf.context, updatedPresentationData: strongSelf.controller?.updatedPresentationData, title: nil, text: "Invoice not found", actions: [TextAlertAction(type: .defaultAction, title: strongSelf.presentationData.strings.Common_OK, action: {})]), nil) + } case let .instantView(webpage, anchor): strongSelf.pushController(InstantPageController(context: strongSelf.context, webPage: webpage, sourceLocation: InstantPageSourceLocation(userLocation: .peer(strongSelf.peer.id), peerType: .channel), anchor: anchor)) case let .join(link): diff --git a/submodules/TelegramUI/Sources/EmojisChatInputPanelItem.swift b/submodules/TelegramUI/Sources/EmojisChatInputPanelItem.swift index 842d3266d1..7bd6a012c9 100644 --- a/submodules/TelegramUI/Sources/EmojisChatInputPanelItem.swift +++ b/submodules/TelegramUI/Sources/EmojisChatInputPanelItem.swift @@ -157,6 +157,8 @@ final class EmojisChatInputPanelItemNode: ListViewItemNode { emojiView.center = emojiFrame.center emojiView.bounds = CGRect(origin: CGPoint(), size: emojiFrame.size) } + + emojiView.updateTextColor(item.theme.list.itemPrimaryTextColor) } else { strongSelf.symbolNode.isHidden = false diff --git a/submodules/TelegramUI/Sources/OpenResolvedUrl.swift b/submodules/TelegramUI/Sources/OpenResolvedUrl.swift index 3f89bac505..6f60a515eb 100644 --- a/submodules/TelegramUI/Sources/OpenResolvedUrl.swift +++ b/submodules/TelegramUI/Sources/OpenResolvedUrl.swift @@ -687,28 +687,34 @@ func openResolvedUrlImpl(_ resolvedUrl: ResolvedUrl, context: AccountContext, ur }) case let .invoice(slug, invoice): dismissInput() - if let navigationController = navigationController { - let inputData = Promise() - inputData.set(BotCheckoutController.InputData.fetch(context: context, source: .slug(slug)) - |> map(Optional.init) - |> `catch` { _ -> Signal in - return .single(nil) - }) - let checkoutController = BotCheckoutController(context: context, invoice: invoice, source: .slug(slug), inputData: inputData, completed: { currencyValue, receiptMessageId in - /*strongSelf.present(UndoOverlayController(presentationData: strongSelf.presentationData, content: .paymentSent(currencyValue: currencyValue, itemTitle: invoice.title), elevatedLayout: false, action: { action in - guard let strongSelf = self, let receiptMessageId = receiptMessageId else { - return false - } - - if case .info = action { - strongSelf.present(BotReceiptController(context: strongSelf.context, messageId: receiptMessageId), in: .window(.root), with: ViewControllerPresentationArguments(presentationAnimation: .modalSheet)) - return true - } - return false - }), in: .current)*/ - }) - checkoutController.navigationPresentation = .modal - navigationController.pushViewController(checkoutController) + + if let invoice { + if let navigationController = navigationController { + let inputData = Promise() + inputData.set(BotCheckoutController.InputData.fetch(context: context, source: .slug(slug)) + |> map(Optional.init) + |> `catch` { _ -> Signal in + return .single(nil) + }) + let checkoutController = BotCheckoutController(context: context, invoice: invoice, source: .slug(slug), inputData: inputData, completed: { currencyValue, receiptMessageId in + /*strongSelf.present(UndoOverlayController(presentationData: strongSelf.presentationData, content: .paymentSent(currencyValue: currencyValue, itemTitle: invoice.title), elevatedLayout: false, action: { action in + guard let strongSelf = self, let receiptMessageId = receiptMessageId else { + return false + } + + if case .info = action { + strongSelf.present(BotReceiptController(context: strongSelf.context, messageId: receiptMessageId), in: .window(.root), with: ViewControllerPresentationArguments(presentationAnimation: .modalSheet)) + return true + } + return false + }), in: .current)*/ + }) + checkoutController.navigationPresentation = .modal + navigationController.pushViewController(checkoutController) + } + } else { + //TODO:localize + present(textAlertController(context: context, updatedPresentationData: updatedPresentationData, title: nil, text: "Invoice not found", actions: [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_OK, action: {})]), nil) } } } diff --git a/submodules/TelegramUI/Sources/PeerInfo/PeerInfoScreen.swift b/submodules/TelegramUI/Sources/PeerInfo/PeerInfoScreen.swift index 79d46b3b65..39406f28d8 100644 --- a/submodules/TelegramUI/Sources/PeerInfo/PeerInfoScreen.swift +++ b/submodules/TelegramUI/Sources/PeerInfo/PeerInfoScreen.swift @@ -1763,7 +1763,7 @@ private func editingItems(data: PeerInfoScreenData?, state: PeerInfoState, chatL var activePermissionCount: Int? if let defaultBannedRights = channel.defaultBannedRights { var count = 0 - for (right, _) in allGroupPermissionList(peer: .channel(channel)) { + for (right, _) in allGroupPermissionList(peer: .channel(channel), expandMedia: true) { if right == .banSendMedia { if banSendMediaSubList().allSatisfy({ !defaultBannedRights.flags.contains($0.0) }) { count += 1 @@ -1781,7 +1781,7 @@ private func editingItems(data: PeerInfoScreenData?, state: PeerInfoState, chatL interaction.openParticipantsSection(.members) })) if !channel.flags.contains(.isGigagroup) { - items[.peerSettings]!.append(PeerInfoScreenDisclosureItem(id: ItemPermissions, label: .text(activePermissionCount.flatMap({ "\($0)/\(allGroupPermissionList(peer: .channel(channel)).count)" }) ?? ""), text: presentationData.strings.GroupInfo_Permissions, icon: UIImage(bundleImageName: "Settings/Menu/SetPasscode"), action: { + items[.peerSettings]!.append(PeerInfoScreenDisclosureItem(id: ItemPermissions, label: .text(activePermissionCount.flatMap({ "\($0)/\(allGroupPermissionList(peer: .channel(channel), expandMedia: true).count)" }) ?? ""), text: presentationData.strings.GroupInfo_Permissions, icon: UIImage(bundleImageName: "Settings/Menu/SetPasscode"), action: { interaction.openPermissions() })) } @@ -1886,7 +1886,7 @@ private func editingItems(data: PeerInfoScreenData?, state: PeerInfoState, chatL var activePermissionCount: Int? if let defaultBannedRights = group.defaultBannedRights { var count = 0 - for (right, _) in allGroupPermissionList(peer: .legacyGroup(group)) { + for (right, _) in allGroupPermissionList(peer: .legacyGroup(group), expandMedia: true) { if right == .banSendMedia { if banSendMediaSubList().allSatisfy({ !defaultBannedRights.flags.contains($0.0) }) { count += 1 @@ -1900,7 +1900,7 @@ private func editingItems(data: PeerInfoScreenData?, state: PeerInfoState, chatL activePermissionCount = count } - items[.peerSettings]!.append(PeerInfoScreenDisclosureItem(id: ItemPermissions, label: .text(activePermissionCount.flatMap({ "\($0)/\(allGroupPermissionList(peer: .legacyGroup(group)).count)" }) ?? ""), text: presentationData.strings.GroupInfo_Permissions, icon: UIImage(bundleImageName: "Settings/Menu/SetPasscode"), action: { + items[.peerSettings]!.append(PeerInfoScreenDisclosureItem(id: ItemPermissions, label: .text(activePermissionCount.flatMap({ "\($0)/\(allGroupPermissionList(peer: .legacyGroup(group), expandMedia: true).count)" }) ?? ""), text: presentationData.strings.GroupInfo_Permissions, icon: UIImage(bundleImageName: "Settings/Menu/SetPasscode"), action: { interaction.openPermissions() })) @@ -3476,6 +3476,7 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, UIScrollViewDelegate isStatusSelection: true, isReactionSelection: false, isEmojiSelection: false, + hasTrending: false, topReactionItems: [], areUnicodeEmojiEnabled: false, areCustomEmojiEnabled: true, diff --git a/submodules/UrlHandling/Sources/UrlHandling.swift b/submodules/UrlHandling/Sources/UrlHandling.swift index 086dd11966..75d38106de 100644 --- a/submodules/UrlHandling/Sources/UrlHandling.swift +++ b/submodules/UrlHandling/Sources/UrlHandling.swift @@ -272,7 +272,10 @@ public func parseInternalUrl(query: String) -> ParsedInternalUrl? { return .join(String(component.dropFirst())) } } else if pathComponents[0].hasPrefix("$") || pathComponents[0].hasPrefix("%24") { - let component = pathComponents[0].replacingOccurrences(of: "%24", with: "$") + var component = pathComponents[0].replacingOccurrences(of: "%24", with: "$") + if component.hasPrefix("$") { + component = String(component[component.index(after: component.startIndex)...]) + } return .invoice(component) } return .peer(.name(peerName), nil) @@ -745,7 +748,7 @@ private func resolveInternalUrl(context: AccountContext, url: ParsedInternalUrl) } |> map { invoice -> ResolvedUrl? in guard let invoice = invoice else { - return nil + return .invoice(slug: slug, invoice: nil) } return .invoice(slug: slug, invoice: invoice) }