mirror of
https://github.com/Swiftgram/Telegram-iOS.git
synced 2025-10-08 19:10:53 +00:00
UI fixes
This commit is contained in:
parent
7cea72619c
commit
dac58e50b3
@ -352,7 +352,7 @@ class StickerPickerScreen: ViewController {
|
||||
},
|
||||
requestUpdate: { _ in
|
||||
},
|
||||
updateSearchQuery: { _, _ in
|
||||
updateSearchQuery: { _ in
|
||||
},
|
||||
updateScrollingToItemGroup: { [weak self] in
|
||||
self?.update(isExpanded: true, transition: .animated(duration: 0.4, curve: .spring))
|
||||
@ -429,7 +429,7 @@ class StickerPickerScreen: ViewController {
|
||||
},
|
||||
requestUpdate: { _ in
|
||||
},
|
||||
updateSearchQuery: { _, _ in
|
||||
updateSearchQuery: { _ in
|
||||
},
|
||||
updateScrollingToItemGroup: { [weak self] in
|
||||
self?.update(isExpanded: true, transition: .animated(duration: 0.4, curve: .spring))
|
||||
@ -553,7 +553,7 @@ class StickerPickerScreen: ViewController {
|
||||
},
|
||||
requestUpdate: { _ in
|
||||
},
|
||||
updateSearchQuery: { _, _ in
|
||||
updateSearchQuery: { _ in
|
||||
},
|
||||
updateScrollingToItemGroup: { [weak self] in
|
||||
self?.update(isExpanded: true, transition: .animated(duration: 0.4, curve: .spring))
|
||||
|
@ -18,15 +18,18 @@ public class ItemListExpandableSwitchItem: ListViewItem, ItemListItem {
|
||||
public var id: AnyHashable
|
||||
public var title: String
|
||||
public var isSelected: Bool
|
||||
public var isEnabled: Bool
|
||||
|
||||
public init(
|
||||
id: AnyHashable,
|
||||
title: String,
|
||||
isSelected: Bool
|
||||
isSelected: Bool,
|
||||
isEnabled: Bool
|
||||
) {
|
||||
self.id = id
|
||||
self.title = title
|
||||
self.isSelected = isSelected
|
||||
self.isEnabled = isEnabled
|
||||
}
|
||||
}
|
||||
|
||||
@ -140,7 +143,7 @@ private final class SubItemNode: HighlightTrackingButtonNode {
|
||||
}
|
||||
|
||||
@objc private func pressed() {
|
||||
guard let item = self.item, let action = self.action else {
|
||||
guard let item = self.item, item.isEnabled, let action = self.action else {
|
||||
return
|
||||
}
|
||||
action(item)
|
||||
@ -180,6 +183,8 @@ private final class SubItemNode: HighlightTrackingButtonNode {
|
||||
self.textNode.attributedText = NSAttributedString(string: item.title, font: Font.regular(17.0), textColor: presentationData.theme.list.itemPrimaryTextColor)
|
||||
let titleSize = self.textNode.updateLayout(CGSize(width: size.width - leftInset, height: 100.0))
|
||||
self.textNode.frame = CGRect(origin: CGPoint(x: leftInset, y: floor((size.height - titleSize.height) / 2.0)), size: titleSize)
|
||||
|
||||
self.alpha = item.isEnabled ? 1.0 : 0.5
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1237,9 +1237,13 @@ public final class MediaPickerScreen: ViewController, AttachmentContainable {
|
||||
self.navigationItem.leftBarButtonItem = UIBarButtonItem(backButtonAppearanceWithTitle: self.presentationData.strings.Common_Back, target: self, action: #selector(self.backPressed))
|
||||
} else {
|
||||
self.navigationItem.leftBarButtonItem = UIBarButtonItem(title: self.presentationData.strings.Common_Cancel, style: .plain, target: self, action: #selector(self.cancelPressed))
|
||||
self.navigationItem.rightBarButtonItem = UIBarButtonItem(customDisplayNode: self.moreButtonNode)
|
||||
self.navigationItem.rightBarButtonItem?.action = #selector(self.rightButtonPressed)
|
||||
self.navigationItem.rightBarButtonItem?.target = self
|
||||
|
||||
if self.bannedSendPhotos != nil && self.bannedSendVideos != nil {
|
||||
} else {
|
||||
self.navigationItem.rightBarButtonItem = UIBarButtonItem(customDisplayNode: self.moreButtonNode)
|
||||
self.navigationItem.rightBarButtonItem?.action = #selector(self.rightButtonPressed)
|
||||
self.navigationItem.rightBarButtonItem?.target = self
|
||||
}
|
||||
}
|
||||
|
||||
self.moreButtonNode.action = { [weak self] _, gesture in
|
||||
|
@ -18,13 +18,15 @@ private final class ChannelBannedMemberControllerArguments {
|
||||
let context: AccountContext
|
||||
let toggleRight: (TelegramChatBannedRightsFlags, Bool) -> Void
|
||||
let toggleRightWhileDisabled: (TelegramChatBannedRightsFlags) -> Void
|
||||
let toggleIsOptionExpanded: (TelegramChatBannedRightsFlags) -> Void
|
||||
let openTimeout: () -> Void
|
||||
let delete: () -> Void
|
||||
|
||||
init(context: AccountContext, toggleRight: @escaping (TelegramChatBannedRightsFlags, Bool) -> Void, toggleRightWhileDisabled: @escaping (TelegramChatBannedRightsFlags) -> Void, openTimeout: @escaping () -> Void, delete: @escaping () -> Void) {
|
||||
init(context: AccountContext, toggleRight: @escaping (TelegramChatBannedRightsFlags, Bool) -> Void, toggleRightWhileDisabled: @escaping (TelegramChatBannedRightsFlags) -> Void, toggleIsOptionExpanded: @escaping (TelegramChatBannedRightsFlags) -> Void, openTimeout: @escaping () -> Void, delete: @escaping () -> Void) {
|
||||
self.context = context
|
||||
self.toggleRight = toggleRight
|
||||
self.toggleRightWhileDisabled = toggleRightWhileDisabled
|
||||
self.toggleIsOptionExpanded = toggleIsOptionExpanded
|
||||
self.openTimeout = openTimeout
|
||||
self.delete = delete
|
||||
}
|
||||
@ -49,7 +51,7 @@ private enum ChannelBannedMemberEntryStableId: Hashable {
|
||||
private enum ChannelBannedMemberEntry: ItemListNodeEntry {
|
||||
case info(PresentationTheme, PresentationStrings, PresentationDateTimeFormat, EnginePeer, EnginePeer.Presence?)
|
||||
case rightsHeader(PresentationTheme, String)
|
||||
case rightItem(PresentationTheme, Int, String, TelegramChatBannedRightsFlags, Bool, Bool)
|
||||
case rightItem(PresentationTheme, Int, String, TelegramChatBannedRightsFlags, Bool, Bool, [SubPermission], Bool)
|
||||
case timeout(PresentationTheme, String, String)
|
||||
case exceptionInfo(PresentationTheme, String)
|
||||
case delete(PresentationTheme, String)
|
||||
@ -73,7 +75,7 @@ private enum ChannelBannedMemberEntry: ItemListNodeEntry {
|
||||
return .info
|
||||
case .rightsHeader:
|
||||
return .rightsHeader
|
||||
case let .rightItem(_, _, _, right, _, _):
|
||||
case let .rightItem(_, _, _, right, _, _, _, _):
|
||||
return .right(right)
|
||||
case .timeout:
|
||||
return .timeout
|
||||
@ -114,8 +116,8 @@ private enum ChannelBannedMemberEntry: ItemListNodeEntry {
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
case let .rightItem(lhsTheme, lhsIndex, lhsText, lhsRight, lhsValue, lhsEnabled):
|
||||
if case let .rightItem(rhsTheme, rhsIndex, rhsText, rhsRight, rhsValue, rhsEnabled) = rhs {
|
||||
case let .rightItem(lhsTheme, lhsIndex, lhsText, lhsRight, lhsValue, lhsEnabled, lhsSubItems, lhsIsExpanded):
|
||||
if case let .rightItem(rhsTheme, rhsIndex, rhsText, rhsRight, rhsValue, rhsEnabled, rhsSubItems, rhsIsExpanded) = rhs {
|
||||
if lhsTheme !== rhsTheme {
|
||||
return false
|
||||
}
|
||||
@ -134,6 +136,12 @@ private enum ChannelBannedMemberEntry: ItemListNodeEntry {
|
||||
if lhsEnabled != rhsEnabled {
|
||||
return false
|
||||
}
|
||||
if lhsSubItems != rhsSubItems {
|
||||
return false
|
||||
}
|
||||
if lhsIsExpanded != rhsIsExpanded {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
} else {
|
||||
return false
|
||||
@ -175,11 +183,11 @@ private enum ChannelBannedMemberEntry: ItemListNodeEntry {
|
||||
default:
|
||||
return true
|
||||
}
|
||||
case let .rightItem(_, lhsIndex, _, _, _, _):
|
||||
case let .rightItem(_, lhsIndex, _, _, _, _, _, _):
|
||||
switch rhs {
|
||||
case .info, .rightsHeader:
|
||||
return false
|
||||
case let .rightItem(_, rhsIndex, _, _, _, _):
|
||||
case let .rightItem(_, rhsIndex, _, _, _, _, _, _):
|
||||
return lhsIndex < rhsIndex
|
||||
default:
|
||||
return true
|
||||
@ -212,12 +220,40 @@ private enum ChannelBannedMemberEntry: ItemListNodeEntry {
|
||||
})
|
||||
case let .rightsHeader(_, text):
|
||||
return ItemListSectionHeaderItem(presentationData: presentationData, text: text, sectionId: self.section)
|
||||
case let .rightItem(_, _, text, right, value, enabled):
|
||||
return ItemListSwitchItem(presentationData: presentationData, title: text, value: value, type: .icon, enableInteractiveChanges: enabled, enabled: enabled, sectionId: self.section, style: .blocks, updated: { value in
|
||||
arguments.toggleRight(right, value)
|
||||
}, activatedWhileDisabled: {
|
||||
arguments.toggleRightWhileDisabled(right)
|
||||
})
|
||||
case let .rightItem(_, _, text, right, value, enabled, subPermissions, isExpanded):
|
||||
if !subPermissions.isEmpty {
|
||||
return ItemListExpandableSwitchItem(presentationData: presentationData, title: text, value: value, isExpanded: isExpanded, subItems: subPermissions.map { item in
|
||||
return ItemListExpandableSwitchItem.SubItem(
|
||||
id: AnyHashable(item.flags.rawValue),
|
||||
title: item.title,
|
||||
isSelected: item.isSelected,
|
||||
isEnabled: item.isEnabled
|
||||
)
|
||||
}, type: .icon, enableInteractiveChanges: enabled, enabled: enabled, sectionId: self.section, style: .blocks, updated: { value in
|
||||
arguments.toggleRight(right, value)
|
||||
}, activatedWhileDisabled: {
|
||||
arguments.toggleRightWhileDisabled(right)
|
||||
}, selectAction: {
|
||||
arguments.toggleIsOptionExpanded(right)
|
||||
}, subAction: { item in
|
||||
guard let value = item.id.base as? Int32 else {
|
||||
return
|
||||
}
|
||||
let subRights = TelegramChatBannedRightsFlags(rawValue: value)
|
||||
|
||||
if item.isEnabled {
|
||||
arguments.toggleRight(subRights, !item.isSelected)
|
||||
} else {
|
||||
arguments.toggleIsOptionExpanded(subRights)
|
||||
}
|
||||
})
|
||||
} else {
|
||||
return ItemListSwitchItem(presentationData: presentationData, title: text, value: value, type: .icon, enableInteractiveChanges: enabled, enabled: enabled, sectionId: self.section, style: .blocks, updated: { value in
|
||||
arguments.toggleRight(right, value)
|
||||
}, activatedWhileDisabled: {
|
||||
arguments.toggleRightWhileDisabled(right)
|
||||
})
|
||||
}
|
||||
case let .timeout(_, text, value):
|
||||
return ItemListDisclosureItem(presentationData: presentationData, title: text, label: value, sectionId: self.section, style: .blocks, action: {
|
||||
arguments.openTimeout()
|
||||
@ -237,6 +273,7 @@ private struct ChannelBannedMemberControllerState: Equatable {
|
||||
var updatedFlags: TelegramChatBannedRightsFlags?
|
||||
var updatedTimeout: Int32?
|
||||
var updating: Bool = false
|
||||
var expandedPermissions = Set<TelegramChatBannedRightsFlags>()
|
||||
}
|
||||
|
||||
func completeRights(_ flags: TelegramChatBannedRightsFlags) -> TelegramChatBannedRightsFlags {
|
||||
@ -293,7 +330,21 @@ private func channelBannedMemberControllerEntries(presentationData: Presentation
|
||||
var index = 0
|
||||
for (right, _) in allGroupPermissionList(peer: .channel(channel)) {
|
||||
let defaultEnabled = !defaultBannedRights.flags.contains(right) && channel.hasPermission(.banMembers)
|
||||
entries.append(.rightItem(presentationData.theme, index, stringForGroupPermission(strings: presentationData.strings, right: right, isForum: channel.flags.contains(.isForum)), right, defaultEnabled && !currentRightsFlags.contains(right), defaultEnabled && !state.updating))
|
||||
|
||||
var isSelected = defaultEnabled && !currentRightsFlags.contains(right)
|
||||
|
||||
var subItems: [SubPermission] = []
|
||||
if right == .banSendMedia {
|
||||
isSelected = banSendMediaSubList().allSatisfy({ defaultEnabled && !currentRightsFlags.contains($0.0) })
|
||||
|
||||
for (subRight, _) in banSendMediaSubList() {
|
||||
let subItemEnabled = defaultEnabled && !state.updating && !defaultBannedRights.flags.contains(subRight) && channel.hasPermission(.banMembers)
|
||||
|
||||
subItems.append(SubPermission(title: stringForGroupPermission(strings: presentationData.strings, right: subRight, isForum: channel.isForum), flags: subRight, isSelected: defaultEnabled && !currentRightsFlags.contains(subRight), isEnabled: subItemEnabled))
|
||||
}
|
||||
}
|
||||
|
||||
entries.append(.rightItem(presentationData.theme, index, stringForGroupPermission(strings: presentationData.strings, right: right, isForum: channel.flags.contains(.isForum)), right, isSelected, defaultEnabled && !state.updating, subItems, state.expandedPermissions.contains(right)))
|
||||
index += 1
|
||||
}
|
||||
|
||||
@ -339,7 +390,21 @@ private func channelBannedMemberControllerEntries(presentationData: Presentation
|
||||
var index = 0
|
||||
for (right, _) in allGroupPermissionList(peer: .legacyGroup(group)) {
|
||||
let defaultEnabled = !defaultBannedRightsFlags.contains(right)
|
||||
entries.append(.rightItem(presentationData.theme, index, stringForGroupPermission(strings: presentationData.strings, right: right, isForum: false), right, defaultEnabled && !currentRightsFlags.contains(right), defaultEnabled && !state.updating))
|
||||
|
||||
var isSelected = defaultEnabled && !currentRightsFlags.contains(right)
|
||||
|
||||
var subItems: [SubPermission] = []
|
||||
if right == .banSendMedia {
|
||||
isSelected = banSendMediaSubList().allSatisfy({ defaultEnabled && !currentRightsFlags.contains($0.0) })
|
||||
|
||||
for (subRight, _) in banSendMediaSubList() {
|
||||
let subItemEnabled = defaultEnabled && !state.updating && !defaultBannedRightsFlags.contains(subRight)
|
||||
|
||||
subItems.append(SubPermission(title: stringForGroupPermission(strings: presentationData.strings, right: subRight, isForum: false), flags: subRight, isSelected: defaultEnabled && !currentRightsFlags.contains(subRight), isEnabled: subItemEnabled))
|
||||
}
|
||||
}
|
||||
|
||||
entries.append(.rightItem(presentationData.theme, index, stringForGroupPermission(strings: presentationData.strings, right: right, isForum: false), right, isSelected, defaultEnabled && !state.updating, subItems, state.expandedPermissions.contains(right)))
|
||||
index += 1
|
||||
}
|
||||
|
||||
@ -439,6 +504,16 @@ public func channelBannedMemberController(context: AccountContext, updatedPresen
|
||||
}
|
||||
presentControllerImpl?(textAlertController(context: context, updatedPresentationData: updatedPresentationData, title: nil, text: text, actions: [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_OK, action: {})]), nil)
|
||||
})
|
||||
}, toggleIsOptionExpanded: { flags in
|
||||
updateState { state in
|
||||
var state = state
|
||||
if state.expandedPermissions.contains(flags) {
|
||||
state.expandedPermissions.remove(flags)
|
||||
} else {
|
||||
state.expandedPermissions.insert(flags)
|
||||
}
|
||||
return state
|
||||
}
|
||||
}, openTimeout: {
|
||||
let presentationData = updatedPresentationData?.initial ?? context.sharedContext.currentPresentationData.with { $0 }
|
||||
let actionSheet = ActionSheetController(presentationData: presentationData)
|
||||
|
@ -65,10 +65,11 @@ private enum ChannelPermissionsEntryStableId: Hashable {
|
||||
case peer(PeerId)
|
||||
}
|
||||
|
||||
private struct SubPermission: Equatable {
|
||||
struct SubPermission: Equatable {
|
||||
var title: String
|
||||
var flags: TelegramChatBannedRightsFlags
|
||||
var isSelected: Bool
|
||||
var isEnabled: Bool
|
||||
}
|
||||
|
||||
private enum ChannelPermissionsEntry: ItemListNodeEntry {
|
||||
@ -270,7 +271,8 @@ private enum ChannelPermissionsEntry: ItemListNodeEntry {
|
||||
return ItemListExpandableSwitchItem.SubItem(
|
||||
id: AnyHashable(item.flags.rawValue),
|
||||
title: item.title,
|
||||
isSelected: item.isSelected
|
||||
isSelected: item.isSelected,
|
||||
isEnabled: item.isEnabled
|
||||
)
|
||||
}, type: .icon, enableInteractiveChanges: enabled != nil, enabled: enabled ?? true, sectionId: self.section, style: .blocks, updated: { value in
|
||||
if let _ = enabled {
|
||||
@ -533,7 +535,7 @@ private func channelPermissionsControllerEntries(context: AccountContext, presen
|
||||
entries.append(.permissionsHeader(presentationData.theme, presentationData.strings.GroupInfo_Permissions_SectionTitle))
|
||||
var rightIndex: Int = 0
|
||||
for (rights, correspondingAdminRight) in allGroupPermissionList(peer: .channel(channel)) {
|
||||
var enabled: Bool? = true
|
||||
var enabled = true
|
||||
if channel.addressName != nil && publicGroupRestrictedPermissions.contains(rights) {
|
||||
enabled = false
|
||||
}
|
||||
@ -552,7 +554,9 @@ private func channelPermissionsControllerEntries(context: AccountContext, presen
|
||||
isSelected = banSendMediaSubList().allSatisfy({ !effectiveRightsFlags.contains($0.0) })
|
||||
|
||||
for (subRight, _) in banSendMediaSubList() {
|
||||
subItems.append(SubPermission(title: stringForGroupPermission(strings: presentationData.strings, right: subRight, isForum: channel.isForum), flags: subRight, isSelected: !effectiveRightsFlags.contains(subRight)))
|
||||
let subRightEnabled = true
|
||||
|
||||
subItems.append(SubPermission(title: stringForGroupPermission(strings: presentationData.strings, right: subRight, isForum: channel.isForum), flags: subRight, isSelected: !effectiveRightsFlags.contains(subRight), isEnabled: enabled && subRightEnabled))
|
||||
}
|
||||
}
|
||||
|
||||
@ -596,7 +600,9 @@ private func channelPermissionsControllerEntries(context: AccountContext, presen
|
||||
var subItems: [SubPermission] = []
|
||||
if rights == .banSendMedia {
|
||||
for (subRight, _) in banSendMediaSubList() {
|
||||
subItems.append(SubPermission(title: stringForGroupPermission(strings: presentationData.strings, right: subRight, isForum: false), flags: subRight, isSelected: !effectiveRightsFlags.contains(subRight)))
|
||||
let subRightEnabled = true
|
||||
|
||||
subItems.append(SubPermission(title: stringForGroupPermission(strings: presentationData.strings, right: subRight, isForum: false), flags: subRight, isSelected: !effectiveRightsFlags.contains(subRight), isEnabled: subRightEnabled))
|
||||
}
|
||||
}
|
||||
|
||||
@ -793,7 +799,7 @@ public func channelPermissionsController(context: AccountContext, updatedPresent
|
||||
|> take(1)
|
||||
|> deliverOnMainQueue).start(next: { peerId, _ in
|
||||
var dismissController: (() -> Void)?
|
||||
let controller = ChannelMembersSearchController(context: context, peerId: peerId, mode: .ban, openPeer: { peer, participant in
|
||||
let controller = ChannelMembersSearchController(context: context, peerId: peerId, mode: .ban, filters: [.disable([context.account.peerId])], openPeer: { peer, participant in
|
||||
if let participant = participant {
|
||||
let presentationData = updatedPresentationData?.initial ?? context.sharedContext.currentPresentationData.with { $0 }
|
||||
switch participant.participant {
|
||||
|
@ -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<MediaId>()
|
||||
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<MediaId>()
|
||||
for item in result {
|
||||
if let itemFile = item.1 {
|
||||
if existingIds.contains(itemFile.fileId) {
|
||||
continue
|
||||
}
|
||||
existingIds.insert(itemFile.fileId)
|
||||
let animationData = EntityKeyboardAnimationData(file: itemFile)
|
||||
let item = EmojiPagerContentComponent.Item(
|
||||
animationData: animationData,
|
||||
content: .animation(animationData),
|
||||
itemFile: itemFile, subgroupId: nil,
|
||||
icon: .none,
|
||||
tintMode: animationData.isTemplate ? .primary : .none
|
||||
)
|
||||
items.append(item)
|
||||
}
|
||||
}
|
||||
|
||||
return [EmojiPagerContentComponent.ItemGroup(
|
||||
supergroupId: "search",
|
||||
groupId: "search",
|
||||
title: nil,
|
||||
subtitle: nil,
|
||||
actionButtonTitle: nil,
|
||||
isFeatured: false,
|
||||
isPremiumLocked: false,
|
||||
isEmbedded: false,
|
||||
hasClear: false,
|
||||
collapsedLineCount: nil,
|
||||
displayPremiumBadges: false,
|
||||
headerItem: nil,
|
||||
items: items
|
||||
)]
|
||||
}
|
||||
|
||||
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<MediaId>()
|
||||
for itemFile in files {
|
||||
if existingIds.contains(itemFile.fileId) {
|
||||
continue
|
||||
}
|
||||
existingIds.insert(itemFile.fileId)
|
||||
let animationData = EntityKeyboardAnimationData(file: itemFile)
|
||||
let item = EmojiPagerContentComponent.Item(
|
||||
animationData: animationData,
|
||||
content: .animation(animationData),
|
||||
itemFile: itemFile, subgroupId: nil,
|
||||
icon: .none,
|
||||
tintMode: animationData.isTemplate ? .primary : .none
|
||||
)
|
||||
items.append(item)
|
||||
}
|
||||
|
||||
return .single([EmojiPagerContentComponent.ItemGroup(
|
||||
supergroupId: "search",
|
||||
groupId: "search",
|
||||
title: nil,
|
||||
subtitle: nil,
|
||||
actionButtonTitle: nil,
|
||||
isFeatured: false,
|
||||
isPremiumLocked: false,
|
||||
isEmbedded: false,
|
||||
hasClear: false,
|
||||
collapsedLineCount: nil,
|
||||
displayPremiumBadges: false,
|
||||
headerItem: nil,
|
||||
items: items
|
||||
)])
|
||||
}
|
||||
|
||||
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))))
|
||||
}))
|
||||
}
|
||||
},
|
||||
|
@ -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()
|
||||
}
|
||||
|
@ -207,6 +207,7 @@ fileprivate let parsers: [Int32 : (BufferReader) -> Any?] = {
|
||||
dict[1383932651] = { return Api.EmailVerifyPurpose.parse_emailVerifyPurposeLoginChange($0) }
|
||||
dict[1128644211] = { return Api.EmailVerifyPurpose.parse_emailVerifyPurposeLoginSetup($0) }
|
||||
dict[-1141565819] = { return Api.EmailVerifyPurpose.parse_emailVerifyPurposePassport($0) }
|
||||
dict[2056961449] = { return Api.EmojiGroup.parse_emojiGroup($0) }
|
||||
dict[-709641735] = { return Api.EmojiKeyword.parse_emojiKeyword($0) }
|
||||
dict[594408994] = { return Api.EmojiKeyword.parse_emojiKeywordDeleted($0) }
|
||||
dict[1556570557] = { return Api.EmojiKeywordsDifference.parse_emojiKeywordsDifference($0) }
|
||||
@ -1031,6 +1032,8 @@ fileprivate let parsers: [Int32 : (BufferReader) -> Any?] = {
|
||||
dict[-253500010] = { return Api.messages.Dialogs.parse_dialogsNotModified($0) }
|
||||
dict[1910543603] = { return Api.messages.Dialogs.parse_dialogsSlice($0) }
|
||||
dict[-1506535550] = { return Api.messages.DiscussionMessage.parse_discussionMessage($0) }
|
||||
dict[-2011186869] = { return Api.messages.EmojiGroups.parse_emojiGroups($0) }
|
||||
dict[1874111879] = { return Api.messages.EmojiGroups.parse_emojiGroupsNotModified($0) }
|
||||
dict[410107472] = { return Api.messages.ExportedChatInvite.parse_exportedChatInvite($0) }
|
||||
dict[572915951] = { return Api.messages.ExportedChatInvite.parse_exportedChatInviteReplaced($0) }
|
||||
dict[-1111085620] = { return Api.messages.ExportedChatInvites.parse_exportedChatInvites($0) }
|
||||
@ -1074,9 +1077,7 @@ fileprivate let parsers: [Int32 : (BufferReader) -> Any?] = {
|
||||
dict[816245886] = { return Api.messages.Stickers.parse_stickers($0) }
|
||||
dict[-244016606] = { return Api.messages.Stickers.parse_stickersNotModified($0) }
|
||||
dict[-1821037486] = { return Api.messages.TranscribedAudio.parse_transcribedAudio($0) }
|
||||
dict[1741309751] = { return Api.messages.TranslatedText.parse_translateNoResult($0) }
|
||||
dict[870003448] = { return Api.messages.TranslatedText.parse_translateResult($0) }
|
||||
dict[-1575684144] = { return Api.messages.TranslatedText.parse_translateResultText($0) }
|
||||
dict[136574537] = { return Api.messages.VotesList.parse_votesList($0) }
|
||||
dict[1042605427] = { return Api.payments.BankCardData.parse_bankCardData($0) }
|
||||
dict[-1362048039] = { return Api.payments.ExportedInvoice.parse_exportedInvoice($0) }
|
||||
@ -1295,6 +1296,8 @@ public extension Api {
|
||||
_1.serialize(buffer, boxed)
|
||||
case let _1 as Api.EmailVerifyPurpose:
|
||||
_1.serialize(buffer, boxed)
|
||||
case let _1 as Api.EmojiGroup:
|
||||
_1.serialize(buffer, boxed)
|
||||
case let _1 as Api.EmojiKeyword:
|
||||
_1.serialize(buffer, boxed)
|
||||
case let _1 as Api.EmojiKeywordsDifference:
|
||||
@ -1823,6 +1826,8 @@ public extension Api {
|
||||
_1.serialize(buffer, boxed)
|
||||
case let _1 as Api.messages.DiscussionMessage:
|
||||
_1.serialize(buffer, boxed)
|
||||
case let _1 as Api.messages.EmojiGroups:
|
||||
_1.serialize(buffer, boxed)
|
||||
case let _1 as Api.messages.ExportedChatInvite:
|
||||
_1.serialize(buffer, boxed)
|
||||
case let _1 as Api.messages.ExportedChatInvites:
|
||||
|
@ -800,6 +800,64 @@ public extension Api.messages {
|
||||
|
||||
}
|
||||
}
|
||||
public extension Api.messages {
|
||||
enum EmojiGroups: TypeConstructorDescription {
|
||||
case emojiGroups(hash: Int32, groups: [Api.EmojiGroup])
|
||||
case emojiGroupsNotModified
|
||||
|
||||
public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) {
|
||||
switch self {
|
||||
case .emojiGroups(let hash, let groups):
|
||||
if boxed {
|
||||
buffer.appendInt32(-2011186869)
|
||||
}
|
||||
serializeInt32(hash, buffer: buffer, boxed: false)
|
||||
buffer.appendInt32(481674261)
|
||||
buffer.appendInt32(Int32(groups.count))
|
||||
for item in groups {
|
||||
item.serialize(buffer, true)
|
||||
}
|
||||
break
|
||||
case .emojiGroupsNotModified:
|
||||
if boxed {
|
||||
buffer.appendInt32(1874111879)
|
||||
}
|
||||
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
public func descriptionFields() -> (String, [(String, Any)]) {
|
||||
switch self {
|
||||
case .emojiGroups(let hash, let groups):
|
||||
return ("emojiGroups", [("hash", hash as Any), ("groups", groups as Any)])
|
||||
case .emojiGroupsNotModified:
|
||||
return ("emojiGroupsNotModified", [])
|
||||
}
|
||||
}
|
||||
|
||||
public static func parse_emojiGroups(_ reader: BufferReader) -> EmojiGroups? {
|
||||
var _1: Int32?
|
||||
_1 = reader.readInt32()
|
||||
var _2: [Api.EmojiGroup]?
|
||||
if let _ = reader.readInt32() {
|
||||
_2 = Api.parseVector(reader, elementSignature: 0, elementType: Api.EmojiGroup.self)
|
||||
}
|
||||
let _c1 = _1 != nil
|
||||
let _c2 = _2 != nil
|
||||
if _c1 && _c2 {
|
||||
return Api.messages.EmojiGroups.emojiGroups(hash: _1!, groups: _2!)
|
||||
}
|
||||
else {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
public static func parse_emojiGroupsNotModified(_ reader: BufferReader) -> EmojiGroups? {
|
||||
return Api.messages.EmojiGroups.emojiGroupsNotModified
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
public extension Api.messages {
|
||||
enum ExportedChatInvite: TypeConstructorDescription {
|
||||
case exportedChatInvite(invite: Api.ExportedChatInvite, users: [Api.User])
|
||||
@ -1364,101 +1422,3 @@ public extension Api.messages {
|
||||
|
||||
}
|
||||
}
|
||||
public extension Api.messages {
|
||||
enum InactiveChats: TypeConstructorDescription {
|
||||
case inactiveChats(dates: [Int32], chats: [Api.Chat], users: [Api.User])
|
||||
|
||||
public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) {
|
||||
switch self {
|
||||
case .inactiveChats(let dates, let chats, let users):
|
||||
if boxed {
|
||||
buffer.appendInt32(-1456996667)
|
||||
}
|
||||
buffer.appendInt32(481674261)
|
||||
buffer.appendInt32(Int32(dates.count))
|
||||
for item in dates {
|
||||
serializeInt32(item, buffer: buffer, boxed: false)
|
||||
}
|
||||
buffer.appendInt32(481674261)
|
||||
buffer.appendInt32(Int32(chats.count))
|
||||
for item in chats {
|
||||
item.serialize(buffer, true)
|
||||
}
|
||||
buffer.appendInt32(481674261)
|
||||
buffer.appendInt32(Int32(users.count))
|
||||
for item in users {
|
||||
item.serialize(buffer, true)
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
public func descriptionFields() -> (String, [(String, Any)]) {
|
||||
switch self {
|
||||
case .inactiveChats(let dates, let chats, let users):
|
||||
return ("inactiveChats", [("dates", dates as Any), ("chats", chats as Any), ("users", users as Any)])
|
||||
}
|
||||
}
|
||||
|
||||
public static func parse_inactiveChats(_ reader: BufferReader) -> InactiveChats? {
|
||||
var _1: [Int32]?
|
||||
if let _ = reader.readInt32() {
|
||||
_1 = Api.parseVector(reader, elementSignature: -1471112230, elementType: Int32.self)
|
||||
}
|
||||
var _2: [Api.Chat]?
|
||||
if let _ = reader.readInt32() {
|
||||
_2 = Api.parseVector(reader, elementSignature: 0, elementType: Api.Chat.self)
|
||||
}
|
||||
var _3: [Api.User]?
|
||||
if let _ = reader.readInt32() {
|
||||
_3 = Api.parseVector(reader, elementSignature: 0, elementType: Api.User.self)
|
||||
}
|
||||
let _c1 = _1 != nil
|
||||
let _c2 = _2 != nil
|
||||
let _c3 = _3 != nil
|
||||
if _c1 && _c2 && _c3 {
|
||||
return Api.messages.InactiveChats.inactiveChats(dates: _1!, chats: _2!, users: _3!)
|
||||
}
|
||||
else {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
public extension Api.messages {
|
||||
enum MessageEditData: TypeConstructorDescription {
|
||||
case messageEditData(flags: Int32)
|
||||
|
||||
public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) {
|
||||
switch self {
|
||||
case .messageEditData(let flags):
|
||||
if boxed {
|
||||
buffer.appendInt32(649453030)
|
||||
}
|
||||
serializeInt32(flags, buffer: buffer, boxed: false)
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
public func descriptionFields() -> (String, [(String, Any)]) {
|
||||
switch self {
|
||||
case .messageEditData(let flags):
|
||||
return ("messageEditData", [("flags", flags as Any)])
|
||||
}
|
||||
}
|
||||
|
||||
public static func parse_messageEditData(_ reader: BufferReader) -> MessageEditData? {
|
||||
var _1: Int32?
|
||||
_1 = reader.readInt32()
|
||||
let _c1 = _1 != nil
|
||||
if _c1 {
|
||||
return Api.messages.MessageEditData.messageEditData(flags: _1!)
|
||||
}
|
||||
else {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
@ -1,3 +1,101 @@
|
||||
public extension Api.messages {
|
||||
enum InactiveChats: TypeConstructorDescription {
|
||||
case inactiveChats(dates: [Int32], chats: [Api.Chat], users: [Api.User])
|
||||
|
||||
public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) {
|
||||
switch self {
|
||||
case .inactiveChats(let dates, let chats, let users):
|
||||
if boxed {
|
||||
buffer.appendInt32(-1456996667)
|
||||
}
|
||||
buffer.appendInt32(481674261)
|
||||
buffer.appendInt32(Int32(dates.count))
|
||||
for item in dates {
|
||||
serializeInt32(item, buffer: buffer, boxed: false)
|
||||
}
|
||||
buffer.appendInt32(481674261)
|
||||
buffer.appendInt32(Int32(chats.count))
|
||||
for item in chats {
|
||||
item.serialize(buffer, true)
|
||||
}
|
||||
buffer.appendInt32(481674261)
|
||||
buffer.appendInt32(Int32(users.count))
|
||||
for item in users {
|
||||
item.serialize(buffer, true)
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
public func descriptionFields() -> (String, [(String, Any)]) {
|
||||
switch self {
|
||||
case .inactiveChats(let dates, let chats, let users):
|
||||
return ("inactiveChats", [("dates", dates as Any), ("chats", chats as Any), ("users", users as Any)])
|
||||
}
|
||||
}
|
||||
|
||||
public static func parse_inactiveChats(_ reader: BufferReader) -> InactiveChats? {
|
||||
var _1: [Int32]?
|
||||
if let _ = reader.readInt32() {
|
||||
_1 = Api.parseVector(reader, elementSignature: -1471112230, elementType: Int32.self)
|
||||
}
|
||||
var _2: [Api.Chat]?
|
||||
if let _ = reader.readInt32() {
|
||||
_2 = Api.parseVector(reader, elementSignature: 0, elementType: Api.Chat.self)
|
||||
}
|
||||
var _3: [Api.User]?
|
||||
if let _ = reader.readInt32() {
|
||||
_3 = Api.parseVector(reader, elementSignature: 0, elementType: Api.User.self)
|
||||
}
|
||||
let _c1 = _1 != nil
|
||||
let _c2 = _2 != nil
|
||||
let _c3 = _3 != nil
|
||||
if _c1 && _c2 && _c3 {
|
||||
return Api.messages.InactiveChats.inactiveChats(dates: _1!, chats: _2!, users: _3!)
|
||||
}
|
||||
else {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
public extension Api.messages {
|
||||
enum MessageEditData: TypeConstructorDescription {
|
||||
case messageEditData(flags: Int32)
|
||||
|
||||
public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) {
|
||||
switch self {
|
||||
case .messageEditData(let flags):
|
||||
if boxed {
|
||||
buffer.appendInt32(649453030)
|
||||
}
|
||||
serializeInt32(flags, buffer: buffer, boxed: false)
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
public func descriptionFields() -> (String, [(String, Any)]) {
|
||||
switch self {
|
||||
case .messageEditData(let flags):
|
||||
return ("messageEditData", [("flags", flags as Any)])
|
||||
}
|
||||
}
|
||||
|
||||
public static func parse_messageEditData(_ reader: BufferReader) -> MessageEditData? {
|
||||
var _1: Int32?
|
||||
_1 = reader.readInt32()
|
||||
let _c1 = _1 != nil
|
||||
if _c1 {
|
||||
return Api.messages.MessageEditData.messageEditData(flags: _1!)
|
||||
}
|
||||
else {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
public extension Api.messages {
|
||||
enum MessageReactionsList: TypeConstructorDescription {
|
||||
case messageReactionsList(flags: Int32, count: Int32, reactions: [Api.MessagePeerReaction], chats: [Api.Chat], users: [Api.User], nextOffset: String?)
|
||||
@ -1246,18 +1344,10 @@ public extension Api.messages {
|
||||
}
|
||||
public extension Api.messages {
|
||||
enum TranslatedText: TypeConstructorDescription {
|
||||
case translateNoResult
|
||||
case translateResult(result: [Api.TextWithEntities])
|
||||
case translateResultText(text: String)
|
||||
|
||||
public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) {
|
||||
switch self {
|
||||
case .translateNoResult:
|
||||
if boxed {
|
||||
buffer.appendInt32(1741309751)
|
||||
}
|
||||
|
||||
break
|
||||
case .translateResult(let result):
|
||||
if boxed {
|
||||
buffer.appendInt32(870003448)
|
||||
@ -1268,29 +1358,16 @@ public extension Api.messages {
|
||||
item.serialize(buffer, true)
|
||||
}
|
||||
break
|
||||
case .translateResultText(let text):
|
||||
if boxed {
|
||||
buffer.appendInt32(-1575684144)
|
||||
}
|
||||
serializeString(text, buffer: buffer, boxed: false)
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
public func descriptionFields() -> (String, [(String, Any)]) {
|
||||
switch self {
|
||||
case .translateNoResult:
|
||||
return ("translateNoResult", [])
|
||||
case .translateResult(let result):
|
||||
return ("translateResult", [("result", result as Any)])
|
||||
case .translateResultText(let text):
|
||||
return ("translateResultText", [("text", text as Any)])
|
||||
}
|
||||
}
|
||||
|
||||
public static func parse_translateNoResult(_ reader: BufferReader) -> TranslatedText? {
|
||||
return Api.messages.TranslatedText.translateNoResult
|
||||
}
|
||||
public static func parse_translateResult(_ reader: BufferReader) -> TranslatedText? {
|
||||
var _1: [Api.TextWithEntities]?
|
||||
if let _ = reader.readInt32() {
|
||||
@ -1304,17 +1381,6 @@ public extension Api.messages {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
public static func parse_translateResultText(_ reader: BufferReader) -> TranslatedText? {
|
||||
var _1: String?
|
||||
_1 = parseString(reader)
|
||||
let _c1 = _1 != nil
|
||||
if _c1 {
|
||||
return Api.messages.TranslatedText.translateResultText(text: _1!)
|
||||
}
|
||||
else {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
@ -4564,6 +4564,21 @@ public extension Api.functions.messages {
|
||||
})
|
||||
}
|
||||
}
|
||||
public extension Api.functions.messages {
|
||||
static func getEmojiGroups(hash: Int32) -> (FunctionDescription, Buffer, DeserializeFunctionResponse<Api.messages.EmojiGroups>) {
|
||||
let buffer = Buffer()
|
||||
buffer.appendInt32(1955122779)
|
||||
serializeInt32(hash, buffer: buffer, boxed: false)
|
||||
return (FunctionDescription(name: "messages.getEmojiGroups", parameters: [("hash", String(describing: hash))]), buffer, DeserializeFunctionResponse { (buffer: Buffer) -> Api.messages.EmojiGroups? in
|
||||
let reader = BufferReader(buffer)
|
||||
var result: Api.messages.EmojiGroups?
|
||||
if let signature = reader.readInt32() {
|
||||
result = Api.parse(reader, signature: signature) as? Api.messages.EmojiGroups
|
||||
}
|
||||
return result
|
||||
})
|
||||
}
|
||||
}
|
||||
public extension Api.functions.messages {
|
||||
static func getEmojiKeywords(langCode: String) -> (FunctionDescription, Buffer, DeserializeFunctionResponse<Api.EmojiKeywordsDifference>) {
|
||||
let buffer = Buffer()
|
||||
@ -4614,6 +4629,21 @@ public extension Api.functions.messages {
|
||||
})
|
||||
}
|
||||
}
|
||||
public extension Api.functions.messages {
|
||||
static func getEmojiStatusGroups(hash: Int32) -> (FunctionDescription, Buffer, DeserializeFunctionResponse<Api.messages.EmojiGroups>) {
|
||||
let buffer = Buffer()
|
||||
buffer.appendInt32(785209037)
|
||||
serializeInt32(hash, buffer: buffer, boxed: false)
|
||||
return (FunctionDescription(name: "messages.getEmojiStatusGroups", parameters: [("hash", String(describing: hash))]), buffer, DeserializeFunctionResponse { (buffer: Buffer) -> Api.messages.EmojiGroups? in
|
||||
let reader = BufferReader(buffer)
|
||||
var result: Api.messages.EmojiGroups?
|
||||
if let signature = reader.readInt32() {
|
||||
result = Api.parse(reader, signature: signature) as? Api.messages.EmojiGroups
|
||||
}
|
||||
return result
|
||||
})
|
||||
}
|
||||
}
|
||||
public extension Api.functions.messages {
|
||||
static func getEmojiStickers(hash: Int64) -> (FunctionDescription, Buffer, DeserializeFunctionResponse<Api.messages.AllStickers>) {
|
||||
let buffer = Buffer()
|
||||
@ -6027,6 +6057,22 @@ public extension Api.functions.messages {
|
||||
})
|
||||
}
|
||||
}
|
||||
public extension Api.functions.messages {
|
||||
static func searchCustomEmoji(emoticon: String, hash: Int64) -> (FunctionDescription, Buffer, DeserializeFunctionResponse<Api.EmojiList>) {
|
||||
let buffer = Buffer()
|
||||
buffer.appendInt32(739360983)
|
||||
serializeString(emoticon, buffer: buffer, boxed: false)
|
||||
serializeInt64(hash, buffer: buffer, boxed: false)
|
||||
return (FunctionDescription(name: "messages.searchCustomEmoji", parameters: [("emoticon", String(describing: emoticon)), ("hash", String(describing: hash))]), buffer, DeserializeFunctionResponse { (buffer: Buffer) -> Api.EmojiList? in
|
||||
let reader = BufferReader(buffer)
|
||||
var result: Api.EmojiList?
|
||||
if let signature = reader.readInt32() {
|
||||
result = Api.parse(reader, signature: signature) as? Api.EmojiList
|
||||
}
|
||||
return result
|
||||
})
|
||||
}
|
||||
}
|
||||
public extension Api.functions.messages {
|
||||
static func searchGlobal(flags: Int32, folderId: Int32?, q: String, filter: Api.MessagesFilter, minDate: Int32, maxDate: Int32, offsetRate: Int32, offsetPeer: Api.InputPeer, offsetId: Int32, limit: Int32) -> (FunctionDescription, Buffer, DeserializeFunctionResponse<Api.messages.Messages>) {
|
||||
let buffer = Buffer()
|
||||
@ -6690,6 +6736,22 @@ public extension Api.functions.messages {
|
||||
})
|
||||
}
|
||||
}
|
||||
public extension Api.functions.messages {
|
||||
static func togglePeerTranslations(flags: Int32, peer: Api.InputPeer) -> (FunctionDescription, Buffer, DeserializeFunctionResponse<Api.Bool>) {
|
||||
let buffer = Buffer()
|
||||
buffer.appendInt32(-461589127)
|
||||
serializeInt32(flags, buffer: buffer, boxed: false)
|
||||
peer.serialize(buffer, true)
|
||||
return (FunctionDescription(name: "messages.togglePeerTranslations", parameters: [("flags", String(describing: flags)), ("peer", String(describing: peer))]), buffer, DeserializeFunctionResponse { (buffer: Buffer) -> Api.Bool? in
|
||||
let reader = BufferReader(buffer)
|
||||
var result: Api.Bool?
|
||||
if let signature = reader.readInt32() {
|
||||
result = Api.parse(reader, signature: signature) as? Api.Bool
|
||||
}
|
||||
return result
|
||||
})
|
||||
}
|
||||
}
|
||||
public extension Api.functions.messages {
|
||||
static func toggleStickerSets(flags: Int32, stickersets: [Api.InputStickerSet]) -> (FunctionDescription, Buffer, DeserializeFunctionResponse<Api.Bool>) {
|
||||
let buffer = Buffer()
|
||||
@ -7676,12 +7738,12 @@ public extension Api.functions.photos {
|
||||
public extension Api.functions.photos {
|
||||
static func uploadProfilePhoto(flags: Int32, file: Api.InputFile?, video: Api.InputFile?, videoStartTs: Double?, videoEmojiMarkup: Api.VideoSize?) -> (FunctionDescription, Buffer, DeserializeFunctionResponse<Api.photos.Photo>) {
|
||||
let buffer = Buffer()
|
||||
buffer.appendInt32(-771759753)
|
||||
buffer.appendInt32(154966609)
|
||||
serializeInt32(flags, buffer: buffer, boxed: false)
|
||||
if Int(flags) & Int(1 << 0) != 0 {file!.serialize(buffer, true)}
|
||||
if Int(flags) & Int(1 << 1) != 0 {video!.serialize(buffer, true)}
|
||||
if Int(flags) & Int(1 << 2) != 0 {serializeDouble(videoStartTs!, buffer: buffer, boxed: false)}
|
||||
if Int(flags) & Int(1 << 3) != 0 {videoEmojiMarkup!.serialize(buffer, true)}
|
||||
if Int(flags) & Int(1 << 4) != 0 {videoEmojiMarkup!.serialize(buffer, true)}
|
||||
return (FunctionDescription(name: "photos.uploadProfilePhoto", parameters: [("flags", String(describing: flags)), ("file", String(describing: file)), ("video", String(describing: video)), ("videoStartTs", String(describing: videoStartTs)), ("videoEmojiMarkup", String(describing: videoEmojiMarkup))]), buffer, DeserializeFunctionResponse { (buffer: Buffer) -> Api.photos.Photo? in
|
||||
let reader = BufferReader(buffer)
|
||||
var result: Api.photos.Photo?
|
||||
|
@ -220,6 +220,56 @@ public extension Api {
|
||||
|
||||
}
|
||||
}
|
||||
public extension Api {
|
||||
enum EmojiGroup: TypeConstructorDescription {
|
||||
case emojiGroup(title: String, iconEmojiId: Int64, emoticons: [String])
|
||||
|
||||
public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) {
|
||||
switch self {
|
||||
case .emojiGroup(let title, let iconEmojiId, let emoticons):
|
||||
if boxed {
|
||||
buffer.appendInt32(2056961449)
|
||||
}
|
||||
serializeString(title, buffer: buffer, boxed: false)
|
||||
serializeInt64(iconEmojiId, buffer: buffer, boxed: false)
|
||||
buffer.appendInt32(481674261)
|
||||
buffer.appendInt32(Int32(emoticons.count))
|
||||
for item in emoticons {
|
||||
serializeString(item, buffer: buffer, boxed: false)
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
public func descriptionFields() -> (String, [(String, Any)]) {
|
||||
switch self {
|
||||
case .emojiGroup(let title, let iconEmojiId, let emoticons):
|
||||
return ("emojiGroup", [("title", title as Any), ("iconEmojiId", iconEmojiId as Any), ("emoticons", emoticons as Any)])
|
||||
}
|
||||
}
|
||||
|
||||
public static func parse_emojiGroup(_ reader: BufferReader) -> EmojiGroup? {
|
||||
var _1: String?
|
||||
_1 = parseString(reader)
|
||||
var _2: Int64?
|
||||
_2 = reader.readInt64()
|
||||
var _3: [String]?
|
||||
if let _ = reader.readInt32() {
|
||||
_3 = Api.parseVector(reader, elementSignature: -1255641564, elementType: String.self)
|
||||
}
|
||||
let _c1 = _1 != nil
|
||||
let _c2 = _2 != nil
|
||||
let _c3 = _3 != nil
|
||||
if _c1 && _c2 && _c3 {
|
||||
return Api.EmojiGroup.emojiGroup(title: _1!, iconEmojiId: _2!, emoticons: _3!)
|
||||
}
|
||||
else {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
public extension Api {
|
||||
enum EmojiKeyword: TypeConstructorDescription {
|
||||
case emojiKeyword(keyword: String, emoticons: [String])
|
||||
@ -1054,47 +1104,3 @@ public extension Api {
|
||||
|
||||
}
|
||||
}
|
||||
public extension Api {
|
||||
enum FileHash: TypeConstructorDescription {
|
||||
case fileHash(offset: Int64, limit: Int32, hash: Buffer)
|
||||
|
||||
public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) {
|
||||
switch self {
|
||||
case .fileHash(let offset, let limit, let hash):
|
||||
if boxed {
|
||||
buffer.appendInt32(-207944868)
|
||||
}
|
||||
serializeInt64(offset, buffer: buffer, boxed: false)
|
||||
serializeInt32(limit, buffer: buffer, boxed: false)
|
||||
serializeBytes(hash, buffer: buffer, boxed: false)
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
public func descriptionFields() -> (String, [(String, Any)]) {
|
||||
switch self {
|
||||
case .fileHash(let offset, let limit, let hash):
|
||||
return ("fileHash", [("offset", offset as Any), ("limit", limit as Any), ("hash", hash as Any)])
|
||||
}
|
||||
}
|
||||
|
||||
public static func parse_fileHash(_ reader: BufferReader) -> FileHash? {
|
||||
var _1: Int64?
|
||||
_1 = reader.readInt64()
|
||||
var _2: Int32?
|
||||
_2 = reader.readInt32()
|
||||
var _3: Buffer?
|
||||
_3 = parseBytes(reader)
|
||||
let _c1 = _1 != nil
|
||||
let _c2 = _2 != nil
|
||||
let _c3 = _3 != nil
|
||||
if _c1 && _c2 && _c3 {
|
||||
return Api.FileHash.fileHash(offset: _1!, limit: _2!, hash: _3!)
|
||||
}
|
||||
else {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
@ -1,3 +1,47 @@
|
||||
public extension Api {
|
||||
enum FileHash: TypeConstructorDescription {
|
||||
case fileHash(offset: Int64, limit: Int32, hash: Buffer)
|
||||
|
||||
public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) {
|
||||
switch self {
|
||||
case .fileHash(let offset, let limit, let hash):
|
||||
if boxed {
|
||||
buffer.appendInt32(-207944868)
|
||||
}
|
||||
serializeInt64(offset, buffer: buffer, boxed: false)
|
||||
serializeInt32(limit, buffer: buffer, boxed: false)
|
||||
serializeBytes(hash, buffer: buffer, boxed: false)
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
public func descriptionFields() -> (String, [(String, Any)]) {
|
||||
switch self {
|
||||
case .fileHash(let offset, let limit, let hash):
|
||||
return ("fileHash", [("offset", offset as Any), ("limit", limit as Any), ("hash", hash as Any)])
|
||||
}
|
||||
}
|
||||
|
||||
public static func parse_fileHash(_ reader: BufferReader) -> FileHash? {
|
||||
var _1: Int64?
|
||||
_1 = reader.readInt64()
|
||||
var _2: Int32?
|
||||
_2 = reader.readInt32()
|
||||
var _3: Buffer?
|
||||
_3 = parseBytes(reader)
|
||||
let _c1 = _1 != nil
|
||||
let _c2 = _2 != nil
|
||||
let _c3 = _3 != nil
|
||||
if _c1 && _c2 && _c3 {
|
||||
return Api.FileHash.fileHash(offset: _1!, limit: _2!, hash: _3!)
|
||||
}
|
||||
else {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
public extension Api {
|
||||
enum Folder: TypeConstructorDescription {
|
||||
case folder(flags: Int32, id: Int32, title: String, photo: Api.ChatPhoto?)
|
||||
|
@ -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())
|
||||
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
@ -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<EmojiSearchCategories?, NoError> {
|
||||
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<Never, NoError> {
|
||||
let poll = Signal<Never, NoError> { subscriber in
|
||||
let signal: Signal<Never, NoError> = _internal_cachedEmojiSearchCategories(postbox: postbox, kind: kind)
|
||||
|> mapToSignal { current in
|
||||
let signal: Signal<Api.messages.EmojiGroups, NoError>
|
||||
switch kind {
|
||||
case .emoji:
|
||||
signal = network.request(Api.functions.messages.getEmojiGroups(hash: current?.hash ?? 0))
|
||||
|> `catch` { _ -> Signal<Api.messages.EmojiGroups, NoError> in
|
||||
return .single(.emojiGroupsNotModified)
|
||||
}
|
||||
case .status:
|
||||
signal = network.request(Api.functions.messages.getEmojiStatusGroups(hash: current?.hash ?? 0))
|
||||
|> `catch` { _ -> Signal<Api.messages.EmojiGroups, NoError> in
|
||||
return .single(.emojiGroupsNotModified)
|
||||
}
|
||||
}
|
||||
|
||||
return signal
|
||||
|> mapToSignal { result -> Signal<Never, NoError> in
|
||||
return postbox.transaction { transaction -> Signal<Never, NoError> 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
|
||||
}
|
@ -2,6 +2,7 @@ import Foundation
|
||||
import Postbox
|
||||
import TelegramApi
|
||||
import SwiftSignalKit
|
||||
import MtProtoKit
|
||||
|
||||
private func hashForIds(_ ids: [Int64]) -> Int64 {
|
||||
var acc: UInt64 = 0
|
||||
|
@ -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 {
|
||||
|
@ -5,6 +5,10 @@ import TelegramApi
|
||||
import MtProtoKit
|
||||
|
||||
func _internal_translate(network: Network, text: String, fromLang: String?, toLang: String) -> Signal<String?, NoError> {
|
||||
#if DEBUG
|
||||
return .single(nil)
|
||||
#else
|
||||
|
||||
var flags: Int32 = 0
|
||||
flags |= (1 << 1)
|
||||
|
||||
@ -30,6 +34,7 @@ func _internal_translate(network: Network, text: String, fromLang: String?, toLa
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
func _internal_translateMessages(postbox: Postbox, network: Network, messageIds: [EngineMessage.Id], toLang: String) -> Signal<Void, NoError> {
|
||||
|
@ -105,6 +105,10 @@ public extension TelegramEngine {
|
||||
return _internal_cachedAvailableReactions(postbox: self.account.postbox)
|
||||
}
|
||||
|
||||
public func emojiSearchCategories(kind: EmojiSearchCategories.Kind) -> Signal<EmojiSearchCategories?, NoError> {
|
||||
return _internal_cachedEmojiSearchCategories(postbox: self.account.postbox, kind: kind)
|
||||
}
|
||||
|
||||
public func updateQuickReaction(reaction: MessageReaction.Reaction) -> Signal<Never, NoError> {
|
||||
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<Api.EmojiList?, NoError> 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([])
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -232,166 +232,219 @@ final class AvatarEditorScreenComponent: Component {
|
||||
self.state?.selectedFile = data.emoji.panelItemGroups.first?.items.first?.itemFile
|
||||
self.state?.updated(transition: .immediate)
|
||||
|
||||
let updateSearchQuery: (String, String) -> Void = { [weak self] rawQuery, languageCode in
|
||||
let updateSearchQuery: (EmojiPagerContentComponent.SearchQuery?) -> Void = { [weak self] query in
|
||||
guard let strongSelf = self, let context = strongSelf.state?.context else {
|
||||
return
|
||||
}
|
||||
|
||||
let query = rawQuery.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
|
||||
if query.isEmpty {
|
||||
switch query {
|
||||
case .none:
|
||||
strongSelf.emojiSearchDisposable.set(nil)
|
||||
strongSelf.emojiSearchResult.set(.single(nil))
|
||||
} else {
|
||||
var signal = context.engine.stickers.searchEmojiKeywords(inputLanguageCode: languageCode, query: query, completeMatch: false)
|
||||
if !languageCode.lowercased().hasPrefix("en") {
|
||||
signal = signal
|
||||
|> mapToSignal { keywords in
|
||||
return .single(keywords)
|
||||
|> then(
|
||||
context.engine.stickers.searchEmojiKeywords(inputLanguageCode: "en-US", query: query, completeMatch: query.count < 3)
|
||||
|> map { englishKeywords in
|
||||
return keywords + englishKeywords
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
let resultSignal = signal
|
||||
|> mapToSignal { keywords -> Signal<[EmojiPagerContentComponent.ItemGroup], NoError> in
|
||||
return combineLatest(
|
||||
context.account.postbox.itemCollectionsView(orderedItemListCollectionIds: [], namespaces: [Namespaces.ItemCollection.CloudEmojiPacks], aroundIndex: nil, count: 10000000) |> take(1),
|
||||
combineLatest(keywords.map { context.engine.stickers.searchStickers(query: $0.emoticons.first!) })
|
||||
)
|
||||
|> map { view, stickers -> [EmojiPagerContentComponent.ItemGroup] in
|
||||
let hasPremium = true
|
||||
|
||||
var emojis: [(String, TelegramMediaFile?, String)] = []
|
||||
|
||||
var existingEmoticons = Set<String>()
|
||||
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<MediaId>()
|
||||
for item in emojis {
|
||||
if let itemFile = item.1 {
|
||||
if existingIds.contains(itemFile.fileId) {
|
||||
continue
|
||||
}
|
||||
existingIds.insert(itemFile.fileId)
|
||||
let animationData = EntityKeyboardAnimationData(file: itemFile)
|
||||
let item = EmojiPagerContentComponent.Item(
|
||||
animationData: animationData,
|
||||
content: .animation(animationData),
|
||||
itemFile: itemFile,
|
||||
subgroupId: nil,
|
||||
icon: .none,
|
||||
tintMode: animationData.isTemplate ? .primary : .none
|
||||
)
|
||||
emojiItems.append(item)
|
||||
}
|
||||
}
|
||||
|
||||
var stickerItems: [EmojiPagerContentComponent.Item] = []
|
||||
for stickerResult in stickers {
|
||||
for sticker in stickerResult {
|
||||
if existingIds.contains(sticker.file.fileId) {
|
||||
continue
|
||||
}
|
||||
|
||||
existingIds.insert(sticker.file.fileId)
|
||||
let animationData = EntityKeyboardAnimationData(file: sticker.file)
|
||||
let item = EmojiPagerContentComponent.Item(
|
||||
animationData: animationData,
|
||||
content: .animation(animationData),
|
||||
itemFile: sticker.file,
|
||||
subgroupId: nil,
|
||||
icon: .none,
|
||||
tintMode: .none
|
||||
)
|
||||
stickerItems.append(item)
|
||||
}
|
||||
}
|
||||
|
||||
var result: [EmojiPagerContentComponent.ItemGroup] = []
|
||||
if !emojiItems.isEmpty {
|
||||
result.append(
|
||||
EmojiPagerContentComponent.ItemGroup(
|
||||
supergroupId: "search",
|
||||
groupId: "emoji",
|
||||
title: "Emoji",
|
||||
subtitle: nil,
|
||||
actionButtonTitle: nil,
|
||||
isFeatured: false,
|
||||
isPremiumLocked: false,
|
||||
isEmbedded: false,
|
||||
hasClear: false,
|
||||
collapsedLineCount: nil,
|
||||
displayPremiumBadges: false,
|
||||
headerItem: nil,
|
||||
items: emojiItems
|
||||
)
|
||||
)
|
||||
}
|
||||
if !stickerItems.isEmpty {
|
||||
result.append(
|
||||
EmojiPagerContentComponent.ItemGroup(
|
||||
supergroupId: "search",
|
||||
groupId: "stickers",
|
||||
title: "Stickers",
|
||||
subtitle: nil,
|
||||
actionButtonTitle: nil,
|
||||
isFeatured: false,
|
||||
isPremiumLocked: false,
|
||||
isEmbedded: false,
|
||||
hasClear: false,
|
||||
collapsedLineCount: nil,
|
||||
displayPremiumBadges: false,
|
||||
headerItem: nil,
|
||||
items: stickerItems
|
||||
)
|
||||
)
|
||||
}
|
||||
return result
|
||||
}
|
||||
}
|
||||
case let .text(rawQuery, languageCode):
|
||||
let query = rawQuery.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
|
||||
if query.isEmpty {
|
||||
strongSelf.emojiSearchDisposable.set(nil)
|
||||
strongSelf.emojiSearchResult.set(.single(nil))
|
||||
} else {
|
||||
var signal = context.engine.stickers.searchEmojiKeywords(inputLanguageCode: languageCode, query: query, completeMatch: false)
|
||||
if !languageCode.lowercased().hasPrefix("en") {
|
||||
signal = signal
|
||||
|> mapToSignal { keywords in
|
||||
return .single(keywords)
|
||||
|> then(
|
||||
context.engine.stickers.searchEmojiKeywords(inputLanguageCode: "en-US", query: query, completeMatch: query.count < 3)
|
||||
|> map { englishKeywords in
|
||||
return keywords + englishKeywords
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
let resultSignal = signal
|
||||
|> mapToSignal { keywords -> Signal<[EmojiPagerContentComponent.ItemGroup], NoError> in
|
||||
return combineLatest(
|
||||
context.account.postbox.itemCollectionsView(orderedItemListCollectionIds: [], namespaces: [Namespaces.ItemCollection.CloudEmojiPacks], aroundIndex: nil, count: 10000000) |> take(1),
|
||||
combineLatest(keywords.map { context.engine.stickers.searchStickers(query: $0.emoticons.first!) })
|
||||
)
|
||||
|> map { view, stickers -> [EmojiPagerContentComponent.ItemGroup] in
|
||||
let hasPremium = true
|
||||
|
||||
var emojis: [(String, TelegramMediaFile?, String)] = []
|
||||
|
||||
var existingEmoticons = Set<String>()
|
||||
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<MediaId>()
|
||||
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<MediaId>()
|
||||
for itemFile in files {
|
||||
if existingIds.contains(itemFile.fileId) {
|
||||
continue
|
||||
}
|
||||
existingIds.insert(itemFile.fileId)
|
||||
let animationData = EntityKeyboardAnimationData(file: itemFile)
|
||||
let item = EmojiPagerContentComponent.Item(
|
||||
animationData: animationData,
|
||||
content: .animation(animationData),
|
||||
itemFile: itemFile, subgroupId: nil,
|
||||
icon: .none,
|
||||
tintMode: animationData.isTemplate ? .primary : .none
|
||||
)
|
||||
items.append(item)
|
||||
}
|
||||
|
||||
return .single([EmojiPagerContentComponent.ItemGroup(
|
||||
supergroupId: "search",
|
||||
groupId: "search",
|
||||
title: nil,
|
||||
subtitle: nil,
|
||||
actionButtonTitle: nil,
|
||||
isFeatured: false,
|
||||
isPremiumLocked: false,
|
||||
isEmbedded: false,
|
||||
hasClear: false,
|
||||
collapsedLineCount: nil,
|
||||
displayPremiumBadges: false,
|
||||
headerItem: nil,
|
||||
items: items
|
||||
)])
|
||||
}
|
||||
|
||||
strongSelf.emojiSearchDisposable.set((resultSignal
|
||||
|> delay(0.15, queue: .mainQueue())
|
||||
|> deliverOnMainQueue).start(next: { [weak self] result in
|
||||
guard let strongSelf = self else {
|
||||
return
|
||||
}
|
||||
strongSelf.emojiSearchResult.set(.single((result, AnyHashable(query))))
|
||||
strongSelf.emojiSearchResult.set(.single((result, AnyHashable(value))))
|
||||
}))
|
||||
}
|
||||
}
|
||||
@ -491,8 +544,8 @@ final class AvatarEditorScreenComponent: Component {
|
||||
strongSelf.state?.updated(transition: transition)
|
||||
}
|
||||
},
|
||||
updateSearchQuery: { rawQuery, languageCode in
|
||||
updateSearchQuery(rawQuery, languageCode)
|
||||
updateSearchQuery: { query in
|
||||
updateSearchQuery(query)
|
||||
},
|
||||
updateScrollingToItemGroup: {
|
||||
},
|
||||
@ -621,8 +674,8 @@ final class AvatarEditorScreenComponent: Component {
|
||||
strongSelf.state?.updated(transition: transition)
|
||||
}
|
||||
},
|
||||
updateSearchQuery: { rawQuery, languageCode in
|
||||
updateSearchQuery(rawQuery, languageCode)
|
||||
updateSearchQuery: { query in
|
||||
updateSearchQuery(query)
|
||||
},
|
||||
updateScrollingToItemGroup: {
|
||||
},
|
||||
|
@ -816,145 +816,198 @@ public final class ChatEntityKeyboardInputNode: ChatInputNode {
|
||||
strongSelf.interfaceInteraction?.requestLayout(transition.containedViewLayoutTransition)
|
||||
}
|
||||
},
|
||||
updateSearchQuery: { [weak self] rawQuery, languageCode in
|
||||
updateSearchQuery: { [weak self] query in
|
||||
guard let strongSelf = self else {
|
||||
return
|
||||
}
|
||||
|
||||
let query = rawQuery.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
|
||||
if query.isEmpty {
|
||||
switch query {
|
||||
case .none:
|
||||
strongSelf.emojiSearchDisposable.set(nil)
|
||||
strongSelf.emojiSearchResult.set(.single(nil))
|
||||
} else {
|
||||
let context = strongSelf.context
|
||||
case let .text(rawQuery, languageCode):
|
||||
let query = rawQuery.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
|
||||
var signal = context.engine.stickers.searchEmojiKeywords(inputLanguageCode: languageCode, query: query, completeMatch: false)
|
||||
if !languageCode.lowercased().hasPrefix("en") {
|
||||
signal = signal
|
||||
|> mapToSignal { keywords in
|
||||
return .single(keywords)
|
||||
|> then(
|
||||
context.engine.stickers.searchEmojiKeywords(inputLanguageCode: "en-US", query: query, completeMatch: query.count < 3)
|
||||
|> map { englishKeywords in
|
||||
return keywords + englishKeywords
|
||||
}
|
||||
if query.isEmpty {
|
||||
strongSelf.emojiSearchDisposable.set(nil)
|
||||
strongSelf.emojiSearchResult.set(.single(nil))
|
||||
} else {
|
||||
let context = strongSelf.context
|
||||
|
||||
var signal = context.engine.stickers.searchEmojiKeywords(inputLanguageCode: languageCode, query: query, completeMatch: false)
|
||||
if !languageCode.lowercased().hasPrefix("en") {
|
||||
signal = signal
|
||||
|> mapToSignal { keywords in
|
||||
return .single(keywords)
|
||||
|> then(
|
||||
context.engine.stickers.searchEmojiKeywords(inputLanguageCode: "en-US", query: query, completeMatch: query.count < 3)
|
||||
|> map { englishKeywords in
|
||||
return keywords + englishKeywords
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
let hasPremium = context.engine.data.subscribe(TelegramEngine.EngineData.Item.Peer.Peer(id: context.account.peerId))
|
||||
|> map { peer -> Bool in
|
||||
guard case let .user(user) = peer else {
|
||||
return false
|
||||
}
|
||||
return user.isPremium
|
||||
}
|
||||
|> distinctUntilChanged
|
||||
|
||||
let resultSignal = signal
|
||||
|> mapToSignal { keywords -> Signal<[EmojiPagerContentComponent.ItemGroup], NoError> in
|
||||
return combineLatest(
|
||||
context.account.postbox.itemCollectionsView(orderedItemListCollectionIds: [], namespaces: [Namespaces.ItemCollection.CloudEmojiPacks], aroundIndex: nil, count: 10000000),
|
||||
context.engine.stickers.availableReactions(),
|
||||
hasPremium
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
let hasPremium = context.engine.data.subscribe(TelegramEngine.EngineData.Item.Peer.Peer(id: context.account.peerId))
|
||||
|> map { peer -> Bool in
|
||||
guard case let .user(user) = peer else {
|
||||
return false
|
||||
}
|
||||
return user.isPremium
|
||||
}
|
||||
|> distinctUntilChanged
|
||||
|
||||
let resultSignal = signal
|
||||
|> mapToSignal { keywords -> Signal<[EmojiPagerContentComponent.ItemGroup], NoError> in
|
||||
return combineLatest(
|
||||
context.account.postbox.itemCollectionsView(orderedItemListCollectionIds: [], namespaces: [Namespaces.ItemCollection.CloudEmojiPacks], aroundIndex: nil, count: 10000000),
|
||||
context.engine.stickers.availableReactions(),
|
||||
hasPremium
|
||||
)
|
||||
|> take(1)
|
||||
|> map { view, availableReactions, hasPremium -> [EmojiPagerContentComponent.ItemGroup] in
|
||||
var result: [(String, TelegramMediaFile?, String)] = []
|
||||
|
||||
var existingEmoticons = Set<String>()
|
||||
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<String>()
|
||||
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<MediaId>()
|
||||
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<MediaId>()
|
||||
for item in result {
|
||||
if let itemFile = item.1 {
|
||||
if existingIds.contains(itemFile.fileId) {
|
||||
continue
|
||||
}
|
||||
existingIds.insert(itemFile.fileId)
|
||||
let animationData = EntityKeyboardAnimationData(file: itemFile)
|
||||
let item = EmojiPagerContentComponent.Item(
|
||||
animationData: animationData,
|
||||
content: .animation(animationData),
|
||||
itemFile: itemFile,
|
||||
subgroupId: nil,
|
||||
icon: .none,
|
||||
tintMode: animationData.isTemplate ? .primary : .none
|
||||
)
|
||||
items.append(item)
|
||||
}
|
||||
}
|
||||
|
||||
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<MediaId>()
|
||||
for itemFile in files {
|
||||
if existingIds.contains(itemFile.fileId) {
|
||||
continue
|
||||
}
|
||||
existingIds.insert(itemFile.fileId)
|
||||
let animationData = EntityKeyboardAnimationData(file: itemFile)
|
||||
let item = EmojiPagerContentComponent.Item(
|
||||
animationData: animationData,
|
||||
content: .animation(animationData),
|
||||
itemFile: itemFile, subgroupId: nil,
|
||||
icon: .none,
|
||||
tintMode: animationData.isTemplate ? .primary : .none
|
||||
)
|
||||
items.append(item)
|
||||
}
|
||||
|
||||
return .single([EmojiPagerContentComponent.ItemGroup(
|
||||
supergroupId: "search",
|
||||
groupId: "search",
|
||||
title: nil,
|
||||
subtitle: nil,
|
||||
actionButtonTitle: nil,
|
||||
isFeatured: false,
|
||||
isPremiumLocked: false,
|
||||
isEmbedded: false,
|
||||
hasClear: false,
|
||||
collapsedLineCount: nil,
|
||||
displayPremiumBadges: false,
|
||||
headerItem: nil,
|
||||
items: items
|
||||
)])
|
||||
}
|
||||
|
||||
strongSelf.emojiSearchDisposable.set((resultSignal
|
||||
|> delay(0.15, queue: .mainQueue())
|
||||
|> deliverOnMainQueue).start(next: { [weak self] result in
|
||||
guard let strongSelf = self else {
|
||||
return
|
||||
}
|
||||
strongSelf.emojiSearchResult.set(.single((result, AnyHashable(query))))
|
||||
strongSelf.emojiSearchResult.set(.single((result, AnyHashable(value))))
|
||||
}))
|
||||
}
|
||||
},
|
||||
@ -1161,7 +1214,7 @@ public final class ChatEntityKeyboardInputNode: ChatInputNode {
|
||||
},
|
||||
requestUpdate: { _ in
|
||||
},
|
||||
updateSearchQuery: { _, _ in
|
||||
updateSearchQuery: { _ in
|
||||
},
|
||||
updateScrollingToItemGroup: {
|
||||
},
|
||||
@ -1937,7 +1990,7 @@ public final class EntityInputView: UIInputView, AttachmentTextInputPanelInputVi
|
||||
},
|
||||
requestUpdate: { _ in
|
||||
},
|
||||
updateSearchQuery: { _, _ in
|
||||
updateSearchQuery: { _ in
|
||||
},
|
||||
updateScrollingToItemGroup: {
|
||||
},
|
||||
|
@ -432,127 +432,237 @@ public final class EmojiStatusSelectionController: ViewController {
|
||||
},
|
||||
requestUpdate: { _ in
|
||||
},
|
||||
updateSearchQuery: { rawQuery, languageCode in
|
||||
updateSearchQuery: { query in
|
||||
guard let strongSelf = self else {
|
||||
return
|
||||
}
|
||||
|
||||
let query = rawQuery.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
|
||||
if query.isEmpty {
|
||||
switch query {
|
||||
case .none:
|
||||
strongSelf.emojiSearchDisposable.set(nil)
|
||||
strongSelf.emojiSearchResult.set(.single(nil))
|
||||
} else {
|
||||
let context = strongSelf.context
|
||||
case let .text(rawQuery, languageCode):
|
||||
let query = rawQuery.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
|
||||
var signal = context.engine.stickers.searchEmojiKeywords(inputLanguageCode: languageCode, query: query, completeMatch: false)
|
||||
if !languageCode.lowercased().hasPrefix("en") {
|
||||
signal = signal
|
||||
|> mapToSignal { keywords in
|
||||
return .single(keywords)
|
||||
|> then(
|
||||
context.engine.stickers.searchEmojiKeywords(inputLanguageCode: "en-US", query: query, completeMatch: query.count < 3)
|
||||
|> map { englishKeywords in
|
||||
return keywords + englishKeywords
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
let hasPremium = context.engine.data.subscribe(TelegramEngine.EngineData.Item.Peer.Peer(id: context.account.peerId))
|
||||
|> map { peer -> Bool in
|
||||
guard case let .user(user) = peer else {
|
||||
return false
|
||||
}
|
||||
return user.isPremium
|
||||
}
|
||||
|> distinctUntilChanged
|
||||
|
||||
let resultSignal = signal
|
||||
|> mapToSignal { keywords -> Signal<[EmojiPagerContentComponent.ItemGroup], NoError> in
|
||||
return combineLatest(
|
||||
context.account.postbox.itemCollectionsView(orderedItemListCollectionIds: [], namespaces: [Namespaces.ItemCollection.CloudEmojiPacks], aroundIndex: nil, count: 10000000),
|
||||
context.engine.stickers.availableReactions(),
|
||||
hasPremium
|
||||
)
|
||||
|> take(1)
|
||||
|> map { view, availableReactions, hasPremium -> [EmojiPagerContentComponent.ItemGroup] in
|
||||
var result: [(String, TelegramMediaFile?, String)] = []
|
||||
|
||||
var allEmoticons: [String: String] = [:]
|
||||
for keyword in keywords {
|
||||
for emoticon in keyword.emoticons {
|
||||
allEmoticons[emoticon] = keyword.keyword
|
||||
}
|
||||
if query.isEmpty {
|
||||
strongSelf.emojiSearchDisposable.set(nil)
|
||||
strongSelf.emojiSearchResult.set(.single(nil))
|
||||
} else {
|
||||
let context = strongSelf.context
|
||||
|
||||
var signal = context.engine.stickers.searchEmojiKeywords(inputLanguageCode: languageCode, query: query, completeMatch: false)
|
||||
if !languageCode.lowercased().hasPrefix("en") {
|
||||
signal = signal
|
||||
|> mapToSignal { keywords in
|
||||
return .single(keywords)
|
||||
|> then(
|
||||
context.engine.stickers.searchEmojiKeywords(inputLanguageCode: "en-US", query: query, completeMatch: query.count < 3)
|
||||
|> map { englishKeywords in
|
||||
return keywords + englishKeywords
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
for entry in view.entries {
|
||||
guard let item = entry.item as? StickerPackItem else {
|
||||
continue
|
||||
}
|
||||
for attribute in item.file.attributes {
|
||||
switch attribute {
|
||||
case let .CustomEmoji(_, _, alt, _):
|
||||
if !item.file.isPremiumEmoji || hasPremium {
|
||||
if !alt.isEmpty, let keyword = allEmoticons[alt] {
|
||||
result.append((alt, item.file, keyword))
|
||||
} else if alt == query {
|
||||
result.append((alt, item.file, alt))
|
||||
}
|
||||
}
|
||||
default:
|
||||
break
|
||||
}
|
||||
|
||||
let hasPremium = context.engine.data.subscribe(TelegramEngine.EngineData.Item.Peer.Peer(id: context.account.peerId))
|
||||
|> map { peer -> Bool in
|
||||
guard case let .user(user) = peer else {
|
||||
return false
|
||||
}
|
||||
return user.isPremium
|
||||
}
|
||||
|> distinctUntilChanged
|
||||
|
||||
let resultSignal = signal
|
||||
|> mapToSignal { keywords -> Signal<[EmojiPagerContentComponent.ItemGroup], NoError> in
|
||||
return combineLatest(
|
||||
context.account.postbox.itemCollectionsView(orderedItemListCollectionIds: [], namespaces: [Namespaces.ItemCollection.CloudEmojiPacks], aroundIndex: nil, count: 10000000),
|
||||
context.engine.stickers.availableReactions(),
|
||||
hasPremium
|
||||
)
|
||||
|> take(1)
|
||||
|> map { view, availableReactions, hasPremium -> [EmojiPagerContentComponent.ItemGroup] in
|
||||
var result: [(String, TelegramMediaFile?, String)] = []
|
||||
|
||||
var allEmoticons: [String: String] = [:]
|
||||
for keyword in keywords {
|
||||
for emoticon in keyword.emoticons {
|
||||
allEmoticons[emoticon] = keyword.keyword
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var items: [EmojiPagerContentComponent.Item] = []
|
||||
|
||||
var existingIds = Set<MediaId>()
|
||||
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<MediaId>()
|
||||
for item in result {
|
||||
if let itemFile = item.1 {
|
||||
if existingIds.contains(itemFile.fileId) {
|
||||
continue
|
||||
}
|
||||
existingIds.insert(itemFile.fileId)
|
||||
let animationData = EntityKeyboardAnimationData(file: itemFile)
|
||||
let item = EmojiPagerContentComponent.Item(
|
||||
animationData: animationData,
|
||||
content: .animation(animationData),
|
||||
itemFile: itemFile, subgroupId: nil,
|
||||
icon: .none,
|
||||
tintMode: animationData.isTemplate ? .primary : .none
|
||||
)
|
||||
items.append(item)
|
||||
}
|
||||
}
|
||||
|
||||
return [EmojiPagerContentComponent.ItemGroup(
|
||||
supergroupId: "search",
|
||||
groupId: "search",
|
||||
title: nil,
|
||||
subtitle: nil,
|
||||
actionButtonTitle: nil,
|
||||
isFeatured: false,
|
||||
isPremiumLocked: false,
|
||||
isEmbedded: false,
|
||||
hasClear: false,
|
||||
collapsedLineCount: nil,
|
||||
displayPremiumBadges: false,
|
||||
headerItem: nil,
|
||||
items: items
|
||||
)]
|
||||
}
|
||||
|
||||
return [EmojiPagerContentComponent.ItemGroup(
|
||||
supergroupId: "search",
|
||||
groupId: "search",
|
||||
title: nil,
|
||||
subtitle: nil,
|
||||
actionButtonTitle: nil,
|
||||
isFeatured: false,
|
||||
isPremiumLocked: false,
|
||||
isEmbedded: false,
|
||||
hasClear: false,
|
||||
collapsedLineCount: nil,
|
||||
displayPremiumBadges: false,
|
||||
headerItem: nil,
|
||||
items: items
|
||||
)]
|
||||
}
|
||||
|
||||
strongSelf.emojiSearchDisposable.set((resultSignal
|
||||
|> delay(0.15, queue: .mainQueue())
|
||||
|> deliverOnMainQueue).start(next: { result in
|
||||
guard let strongSelf = self else {
|
||||
return
|
||||
}
|
||||
strongSelf.emojiSearchResult.set(.single((result, AnyHashable(query))))
|
||||
}))
|
||||
}
|
||||
case let .category(query):
|
||||
if query.isEmpty {
|
||||
strongSelf.emojiSearchDisposable.set(nil)
|
||||
strongSelf.emojiSearchResult.set(.single(nil))
|
||||
} else {
|
||||
let context = strongSelf.context
|
||||
|
||||
let signal = context.engine.stickers.searchEmojiKeywords(inputLanguageCode: "en", query: query, completeMatch: false)
|
||||
|
||||
strongSelf.emojiSearchDisposable.set((resultSignal
|
||||
|> delay(0.15, queue: .mainQueue())
|
||||
|> deliverOnMainQueue).start(next: { result in
|
||||
guard let strongSelf = self else {
|
||||
return
|
||||
let hasPremium = context.engine.data.subscribe(TelegramEngine.EngineData.Item.Peer.Peer(id: context.account.peerId))
|
||||
|> map { peer -> Bool in
|
||||
guard case let .user(user) = peer else {
|
||||
return false
|
||||
}
|
||||
return user.isPremium
|
||||
}
|
||||
strongSelf.emojiSearchResult.set(.single((result, AnyHashable(query))))
|
||||
}))
|
||||
|> distinctUntilChanged
|
||||
|
||||
let resultSignal = signal
|
||||
|> mapToSignal { keywords -> Signal<[EmojiPagerContentComponent.ItemGroup], NoError> in
|
||||
return combineLatest(
|
||||
context.account.postbox.itemCollectionsView(orderedItemListCollectionIds: [], namespaces: [Namespaces.ItemCollection.CloudEmojiPacks], aroundIndex: nil, count: 10000000),
|
||||
context.engine.stickers.availableReactions(),
|
||||
hasPremium
|
||||
)
|
||||
|> take(1)
|
||||
|> map { view, availableReactions, hasPremium -> [EmojiPagerContentComponent.ItemGroup] in
|
||||
var result: [(String, TelegramMediaFile?, String)] = []
|
||||
|
||||
var allEmoticons: [String: String] = [:]
|
||||
for keyword in keywords {
|
||||
for emoticon in keyword.emoticons {
|
||||
allEmoticons[emoticon] = keyword.keyword
|
||||
}
|
||||
}
|
||||
|
||||
for entry in view.entries {
|
||||
guard let item = entry.item as? StickerPackItem else {
|
||||
continue
|
||||
}
|
||||
for attribute in item.file.attributes {
|
||||
switch attribute {
|
||||
case let .CustomEmoji(_, _, alt, _):
|
||||
if !item.file.isPremiumEmoji || hasPremium {
|
||||
if !alt.isEmpty, let keyword = allEmoticons[alt] {
|
||||
result.append((alt, item.file, keyword))
|
||||
} else if alt == query {
|
||||
result.append((alt, item.file, alt))
|
||||
}
|
||||
}
|
||||
default:
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var items: [EmojiPagerContentComponent.Item] = []
|
||||
|
||||
var existingIds = Set<MediaId>()
|
||||
for item in result {
|
||||
if let itemFile = item.1 {
|
||||
if existingIds.contains(itemFile.fileId) {
|
||||
continue
|
||||
}
|
||||
existingIds.insert(itemFile.fileId)
|
||||
let animationData = EntityKeyboardAnimationData(file: itemFile)
|
||||
let item = EmojiPagerContentComponent.Item(
|
||||
animationData: animationData,
|
||||
content: .animation(animationData),
|
||||
itemFile: itemFile, subgroupId: nil,
|
||||
icon: .none,
|
||||
tintMode: animationData.isTemplate ? .primary : .none
|
||||
)
|
||||
items.append(item)
|
||||
}
|
||||
}
|
||||
|
||||
return [EmojiPagerContentComponent.ItemGroup(
|
||||
supergroupId: "search",
|
||||
groupId: "search",
|
||||
title: nil,
|
||||
subtitle: nil,
|
||||
actionButtonTitle: nil,
|
||||
isFeatured: false,
|
||||
isPremiumLocked: false,
|
||||
isEmbedded: false,
|
||||
hasClear: false,
|
||||
collapsedLineCount: nil,
|
||||
displayPremiumBadges: false,
|
||||
headerItem: nil,
|
||||
items: items
|
||||
)]
|
||||
}
|
||||
}
|
||||
|
||||
strongSelf.emojiSearchDisposable.set((resultSignal
|
||||
|> delay(0.15, queue: .mainQueue())
|
||||
|> deliverOnMainQueue).start(next: { result in
|
||||
guard let strongSelf = self else {
|
||||
return
|
||||
}
|
||||
strongSelf.emojiSearchResult.set(.single((result, AnyHashable(query))))
|
||||
}))
|
||||
}
|
||||
}
|
||||
},
|
||||
updateScrollingToItemGroup: {
|
||||
|
@ -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",
|
||||
|
@ -1515,17 +1515,22 @@ public final class EmojiSearchHeaderView: UIView, UITextFieldDelegate {
|
||||
}
|
||||
|
||||
private struct Params: Equatable {
|
||||
var context: AccountContext
|
||||
var theme: PresentationTheme
|
||||
var strings: PresentationStrings
|
||||
var text: String
|
||||
var useOpaqueTheme: Bool
|
||||
var isActive: Bool
|
||||
var hasPresetSearch: Bool
|
||||
var textInputState: EmojiSearchSearchBarComponent.TextInputState
|
||||
var size: CGSize
|
||||
var canFocus: Bool
|
||||
var hasSearchItems: Bool
|
||||
var searchCategories: EmojiSearchCategories?
|
||||
|
||||
static func ==(lhs: Params, rhs: Params) -> Bool {
|
||||
if lhs.context !== rhs.context {
|
||||
return false
|
||||
}
|
||||
if lhs.theme !== rhs.theme {
|
||||
return false
|
||||
}
|
||||
@ -1544,13 +1549,16 @@ public final class EmojiSearchHeaderView: UIView, UITextFieldDelegate {
|
||||
if lhs.hasPresetSearch != rhs.hasPresetSearch {
|
||||
return false
|
||||
}
|
||||
if lhs.textInputState != rhs.textInputState {
|
||||
return false
|
||||
}
|
||||
if lhs.size != rhs.size {
|
||||
return false
|
||||
}
|
||||
if lhs.canFocus != rhs.canFocus {
|
||||
return false
|
||||
}
|
||||
if lhs.hasSearchItems != rhs.hasSearchItems {
|
||||
if lhs.searchCategories != rhs.searchCategories {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
@ -1563,7 +1571,7 @@ public final class EmojiSearchHeaderView: UIView, UITextFieldDelegate {
|
||||
|
||||
private let activated: () -> Void
|
||||
private let deactivated: (Bool) -> Void
|
||||
private let updateQuery: (String, String) -> Void
|
||||
private let updateQuery: (EmojiPagerContentComponent.SearchQuery?) -> Void
|
||||
|
||||
let tintContainerView: UIView
|
||||
|
||||
@ -1580,14 +1588,13 @@ public final class EmojiSearchHeaderView: UIView, UITextFieldDelegate {
|
||||
private let clearIconTintView: UIImageView
|
||||
private let clearIconButton: HighlightTrackingButton
|
||||
|
||||
private let tintTextView: ComponentView<Empty>
|
||||
private let textView: ComponentView<Empty>
|
||||
private let cancelButtonTintTitle: ComponentView<Empty>
|
||||
private let cancelButtonTitle: ComponentView<Empty>
|
||||
private let cancelButton: HighlightTrackingButton
|
||||
|
||||
private var suggestedItemsView: ComponentView<Empty>?
|
||||
private var placeholderContent = ComponentView<Empty>()
|
||||
|
||||
private var textFrame: CGRect?
|
||||
private var textField: EmojiSearchTextField?
|
||||
|
||||
private var tapRecognizer: UITapGestureRecognizer?
|
||||
@ -1599,7 +1606,7 @@ public final class EmojiSearchHeaderView: UIView, UITextFieldDelegate {
|
||||
return self.textField != nil
|
||||
}
|
||||
|
||||
init(activated: @escaping () -> Void, deactivated: @escaping (Bool) -> Void, updateQuery: @escaping (String, String) -> Void) {
|
||||
init(activated: @escaping () -> Void, deactivated: @escaping (Bool) -> Void, updateQuery: @escaping (EmojiPagerContentComponent.SearchQuery?) -> Void) {
|
||||
self.activated = activated
|
||||
self.deactivated = deactivated
|
||||
self.updateQuery = updateQuery
|
||||
@ -1622,8 +1629,6 @@ public final class EmojiSearchHeaderView: UIView, UITextFieldDelegate {
|
||||
self.clearIconTintView.isHidden = true
|
||||
self.clearIconButton.isHidden = true
|
||||
|
||||
self.tintTextView = ComponentView()
|
||||
self.textView = ComponentView()
|
||||
self.cancelButtonTintTitle = ComponentView()
|
||||
self.cancelButtonTitle = ComponentView()
|
||||
self.cancelButton = HighlightTrackingButton()
|
||||
@ -1703,66 +1708,71 @@ public final class EmojiSearchHeaderView: UIView, UITextFieldDelegate {
|
||||
if case .ended = recognizer.state {
|
||||
let location = recognizer.location(in: self)
|
||||
if self.backIconView.frame.contains(location) {
|
||||
if let suggestedItemsView = self.suggestedItemsView?.view as? EmojiSearchSearchBarComponent.View {
|
||||
suggestedItemsView.clearSelection(dispatchEvent : true)
|
||||
if let placeholderContentView = self.placeholderContent.view as? EmojiSearchSearchBarComponent.View {
|
||||
placeholderContentView.clearSelection(dispatchEvent : true)
|
||||
}
|
||||
} else {
|
||||
if self.textField == nil, let textComponentView = self.textView.view, self.params?.canFocus == true {
|
||||
let backgroundFrame = self.backgroundLayer.frame
|
||||
let textFieldFrame = CGRect(origin: CGPoint(x: textComponentView.frame.minX, y: backgroundFrame.minY), size: CGSize(width: backgroundFrame.maxX - textComponentView.frame.minX, height: backgroundFrame.height))
|
||||
|
||||
let textField = EmojiSearchTextField(frame: textFieldFrame)
|
||||
textField.autocorrectionType = .no
|
||||
self.textField = textField
|
||||
self.insertSubview(textField, belowSubview: self.clearIconView)
|
||||
textField.delegate = self
|
||||
textField.addTarget(self, action: #selector(self.textFieldChanged(_:)), for: .editingChanged)
|
||||
}
|
||||
|
||||
self.currentPresetSearchTerm = nil
|
||||
if let suggestedItemsView = self.suggestedItemsView?.view as? EmojiSearchSearchBarComponent.View {
|
||||
suggestedItemsView.clearSelection(dispatchEvent: false)
|
||||
}
|
||||
|
||||
self.activated()
|
||||
|
||||
self.textField?.becomeFirstResponder()
|
||||
self.activateTextInput()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func activateTextInput() {
|
||||
if self.textField == nil, let textFrame = self.textFrame, self.params?.canFocus == true {
|
||||
let backgroundFrame = self.backgroundLayer.frame
|
||||
let textFieldFrame = CGRect(origin: CGPoint(x: textFrame.minX, y: backgroundFrame.minY), size: CGSize(width: backgroundFrame.maxX - textFrame.minX, height: backgroundFrame.height))
|
||||
|
||||
let textField = EmojiSearchTextField(frame: textFieldFrame)
|
||||
textField.autocorrectionType = .no
|
||||
self.textField = textField
|
||||
self.insertSubview(textField, belowSubview: self.clearIconView)
|
||||
textField.delegate = self
|
||||
textField.addTarget(self, action: #selector(self.textFieldChanged(_:)), for: .editingChanged)
|
||||
}
|
||||
|
||||
self.currentPresetSearchTerm = nil
|
||||
if let placeholderContentView = self.placeholderContent.view as? EmojiSearchSearchBarComponent.View {
|
||||
placeholderContentView.clearSelection(dispatchEvent: false)
|
||||
}
|
||||
|
||||
self.activated()
|
||||
|
||||
self.textField?.becomeFirstResponder()
|
||||
}
|
||||
|
||||
@objc private func cancelPressed() {
|
||||
self.currentPresetSearchTerm = nil
|
||||
self.updateQuery("", "en")
|
||||
self.updateQuery(nil)
|
||||
|
||||
self.clearIconView.isHidden = true
|
||||
self.clearIconTintView.isHidden = true
|
||||
self.clearIconButton.isHidden = true
|
||||
|
||||
self.deactivated(self.textField?.isFirstResponder ?? false)
|
||||
|
||||
if let textField = self.textField {
|
||||
self.textField = nil
|
||||
|
||||
let textField = self.textField
|
||||
self.textField = nil
|
||||
|
||||
self.deactivated(textField?.isFirstResponder ?? false)
|
||||
|
||||
if let textField {
|
||||
textField.resignFirstResponder()
|
||||
textField.removeFromSuperview()
|
||||
}
|
||||
|
||||
self.tintTextView.view?.isHidden = false
|
||||
self.textView.view?.isHidden = false
|
||||
/*self.tintTextView.view?.isHidden = false
|
||||
self.textView.view?.isHidden = false*/
|
||||
}
|
||||
|
||||
@objc private func clearPressed() {
|
||||
self.currentPresetSearchTerm = nil
|
||||
self.updateQuery("", "en")
|
||||
self.updateQuery(nil)
|
||||
self.textField?.text = ""
|
||||
|
||||
self.clearIconView.isHidden = true
|
||||
self.clearIconTintView.isHidden = true
|
||||
self.clearIconButton.isHidden = true
|
||||
|
||||
self.tintTextView.view?.isHidden = false
|
||||
self.textView.view?.isHidden = false
|
||||
/*self.tintTextView.view?.isHidden = false
|
||||
self.textView.view?.isHidden = false*/
|
||||
}
|
||||
|
||||
func deactivate() {
|
||||
@ -1802,7 +1812,7 @@ public final class EmojiSearchHeaderView: UIView, UITextFieldDelegate {
|
||||
self.clearIconButton.isHidden = text.isEmpty
|
||||
|
||||
self.currentPresetSearchTerm = nil
|
||||
self.updateQuery(text, inputLanguage)
|
||||
self.updateQuery(.text(value: text, language: inputLanguage))
|
||||
}
|
||||
|
||||
private func update(transition: Transition) {
|
||||
@ -1810,20 +1820,29 @@ public final class EmojiSearchHeaderView: UIView, UITextFieldDelegate {
|
||||
return
|
||||
}
|
||||
self.params = nil
|
||||
self.update(theme: params.theme, strings: params.strings, text: params.text, useOpaqueTheme: params.useOpaqueTheme, isActive: params.isActive, size: params.size, canFocus: params.canFocus, hasSearchItems: params.hasSearchItems, transition: transition)
|
||||
self.update(context: params.context, theme: params.theme, strings: params.strings, text: params.text, useOpaqueTheme: params.useOpaqueTheme, isActive: params.isActive, size: params.size, canFocus: params.canFocus, searchCategories: params.searchCategories, transition: transition)
|
||||
}
|
||||
|
||||
public func update(theme: PresentationTheme, strings: PresentationStrings, text: String, useOpaqueTheme: Bool, isActive: Bool, size: CGSize, canFocus: Bool, hasSearchItems: Bool, transition: Transition) {
|
||||
public func update(context: AccountContext, theme: PresentationTheme, strings: PresentationStrings, text: String, useOpaqueTheme: Bool, isActive: Bool, size: CGSize, canFocus: Bool, searchCategories: EmojiSearchCategories?, transition: Transition) {
|
||||
let textInputState: EmojiSearchSearchBarComponent.TextInputState
|
||||
if let textField = self.textField {
|
||||
textInputState = .active(hasText: !(textField.text ?? "").isEmpty)
|
||||
} else {
|
||||
textInputState = .inactive
|
||||
}
|
||||
|
||||
let params = Params(
|
||||
context: context,
|
||||
theme: theme,
|
||||
strings: strings,
|
||||
text: text,
|
||||
useOpaqueTheme: useOpaqueTheme,
|
||||
isActive: isActive,
|
||||
hasPresetSearch: self.currentPresetSearchTerm == nil,
|
||||
textInputState: textInputState,
|
||||
size: size,
|
||||
canFocus: canFocus,
|
||||
hasSearchItems: hasSearchItems
|
||||
searchCategories: searchCategories
|
||||
)
|
||||
|
||||
if self.params == params {
|
||||
@ -1832,8 +1851,6 @@ public final class EmojiSearchHeaderView: UIView, UITextFieldDelegate {
|
||||
|
||||
let isActiveWithText = isActive && self.currentPresetSearchTerm == nil
|
||||
|
||||
let isLeftAligned = isActiveWithText || hasSearchItems
|
||||
|
||||
if self.params?.theme !== theme {
|
||||
self.searchIconView.image = generateTintedImage(image: UIImage(bundleImageName: "Components/Search Bar/Loupe"), color: theme.chat.inputMediaPanel.panelContentVibrantOverlayColor)
|
||||
self.searchIconTintView.image = generateTintedImage(image: UIImage(bundleImageName: "Components/Search Bar/Loupe"), color: .white)
|
||||
@ -1864,26 +1881,6 @@ public final class EmojiSearchHeaderView: UIView, UITextFieldDelegate {
|
||||
self.backgroundLayer.cornerRadius = inputHeight * 0.5
|
||||
self.tintBackgroundLayer.cornerRadius = inputHeight * 0.5
|
||||
|
||||
let textSize = self.textView.update(
|
||||
transition: .immediate,
|
||||
component: AnyComponent(Text(
|
||||
text: text,
|
||||
font: Font.regular(17.0),
|
||||
color: theme.chat.inputMediaPanel.panelContentVibrantOverlayColor
|
||||
)),
|
||||
environment: {},
|
||||
containerSize: CGSize(width: size.width - 32.0, height: 100.0)
|
||||
)
|
||||
let _ = self.tintTextView.update(
|
||||
transition: .immediate,
|
||||
component: AnyComponent(Text(
|
||||
text: text,
|
||||
font: Font.regular(17.0),
|
||||
color: .white
|
||||
)),
|
||||
environment: {},
|
||||
containerSize: CGSize(width: size.width - 32.0, height: 100.0)
|
||||
)
|
||||
let cancelTextSize = self.cancelButtonTitle.update(
|
||||
transition: .immediate,
|
||||
component: AnyComponent(Text(
|
||||
@ -1916,10 +1913,9 @@ public final class EmojiSearchHeaderView: UIView, UITextFieldDelegate {
|
||||
|
||||
transition.setFrame(view: self.cancelButton, frame: CGRect(origin: CGPoint(x: backgroundFrame.maxX, y: 0.0), size: CGSize(width: cancelButtonSpacing + cancelTextSize.width, height: size.height)))
|
||||
|
||||
var textFrame = CGRect(origin: CGPoint(x: backgroundFrame.minX + floor((backgroundFrame.width - textSize.width) / 2.0), y: backgroundFrame.minY + floor((backgroundFrame.height - textSize.height) / 2.0)), size: textSize)
|
||||
if isLeftAligned {
|
||||
textFrame.origin.x = backgroundFrame.minX + sideTextInset
|
||||
}
|
||||
let textX: CGFloat = backgroundFrame.minX + sideTextInset
|
||||
let textFrame = CGRect(origin: CGPoint(x: textX, y: backgroundFrame.minY), size: CGSize(width: backgroundFrame.maxX - textX, height: backgroundFrame.height))
|
||||
self.textFrame = textFrame
|
||||
|
||||
if let image = self.searchIconView.image {
|
||||
let iconFrame = CGRect(origin: CGPoint(x: textFrame.minX - image.size.width - 4.0, y: backgroundFrame.minY + floor((backgroundFrame.height - image.size.height) / 2.0)), size: image.size)
|
||||
@ -1945,7 +1941,63 @@ public final class EmojiSearchHeaderView: UIView, UITextFieldDelegate {
|
||||
transition.setAlpha(view: self.backIconTintView, alpha: self.currentPresetSearchTerm != nil ? 1.0 : 0.0)
|
||||
}
|
||||
|
||||
if hasSearchItems {
|
||||
let placeholderContentFrame = CGRect(origin: CGPoint(x: textFrame.minX - 6.0, y: backgroundFrame.minX), size: CGSize(width: backgroundFrame.maxX - (textFrame.minX - 6.0), height: backgroundFrame.height))
|
||||
let _ = self.placeholderContent.update(
|
||||
transition: transition,
|
||||
component: AnyComponent(EmojiSearchSearchBarComponent(
|
||||
context: context,
|
||||
theme: theme,
|
||||
strings: strings,
|
||||
textInputState: textInputState,
|
||||
categories: searchCategories,
|
||||
searchTermUpdated: { [weak self] term in
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
var shouldChangeActivation = false
|
||||
if (self.currentPresetSearchTerm == nil) != (term == nil) {
|
||||
shouldChangeActivation = true
|
||||
}
|
||||
self.currentPresetSearchTerm = term
|
||||
|
||||
if shouldChangeActivation {
|
||||
self.update(transition: Transition(animation: .curve(duration: 0.4, curve: .spring)))
|
||||
|
||||
if let term {
|
||||
self.updateQuery(.category(value: term))
|
||||
self.activated()
|
||||
} else {
|
||||
self.deactivated(self.textField?.isFirstResponder ?? false)
|
||||
self.updateQuery(nil)
|
||||
}
|
||||
} else {
|
||||
if let term {
|
||||
self.updateQuery(.category(value: term))
|
||||
} else {
|
||||
self.updateQuery(nil)
|
||||
}
|
||||
}
|
||||
},
|
||||
activateTextInput: { [weak self] in
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
self.activateTextInput()
|
||||
}
|
||||
)),
|
||||
environment: {},
|
||||
containerSize: placeholderContentFrame.size
|
||||
)
|
||||
if let placeholderContentView = self.placeholderContent.view as? EmojiSearchSearchBarComponent.View {
|
||||
if placeholderContentView.superview == nil {
|
||||
self.addSubview(placeholderContentView)
|
||||
self.tintContainerView.addSubview(placeholderContentView.tintContainerView)
|
||||
}
|
||||
transition.setFrame(view: placeholderContentView, frame: placeholderContentFrame)
|
||||
transition.setFrame(view: placeholderContentView.tintContainerView, frame: placeholderContentFrame)
|
||||
}
|
||||
|
||||
/*if let searchCategories {
|
||||
let suggestedItemsView: ComponentView<Empty>
|
||||
var suggestedItemsTransition = transition
|
||||
if let current = self.suggestedItemsView {
|
||||
@ -1958,39 +2010,7 @@ public final class EmojiSearchHeaderView: UIView, UITextFieldDelegate {
|
||||
|
||||
let itemsX: CGFloat = textFrame.maxX + 8.0
|
||||
let suggestedItemsFrame = CGRect(origin: CGPoint(x: itemsX, y: backgroundFrame.minY), size: CGSize(width: backgroundFrame.maxX - itemsX, height: backgroundFrame.height))
|
||||
let _ = suggestedItemsView.update(
|
||||
transition: suggestedItemsTransition,
|
||||
component: AnyComponent(EmojiSearchSearchBarComponent(
|
||||
theme: theme,
|
||||
strings: strings,
|
||||
searchTermUpdated: { [weak self] term in
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
var shouldChangeActivation = false
|
||||
if (self.currentPresetSearchTerm == nil) != (term == nil) {
|
||||
shouldChangeActivation = true
|
||||
}
|
||||
self.currentPresetSearchTerm = term
|
||||
|
||||
if shouldChangeActivation {
|
||||
self.update(transition: Transition(animation: .curve(duration: 0.4, curve: .spring)))
|
||||
|
||||
if term == nil {
|
||||
self.deactivated(self.textField?.isFirstResponder ?? false)
|
||||
self.updateQuery(term ?? "", "en")
|
||||
} else {
|
||||
self.updateQuery(term ?? "", "en")
|
||||
self.activated()
|
||||
}
|
||||
} else {
|
||||
self.updateQuery(term ?? "", "en")
|
||||
}
|
||||
}
|
||||
)),
|
||||
environment: {},
|
||||
containerSize: suggestedItemsFrame.size
|
||||
)
|
||||
|
||||
if let suggestedItemsComponentView = suggestedItemsView.view {
|
||||
if suggestedItemsComponentView.superview == nil {
|
||||
self.addSubview(suggestedItemsComponentView)
|
||||
@ -2007,7 +2027,7 @@ public final class EmojiSearchHeaderView: UIView, UITextFieldDelegate {
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}*/
|
||||
|
||||
if let image = self.clearIconView.image {
|
||||
let iconFrame = CGRect(origin: CGPoint(x: backgroundFrame.maxX - image.size.width - 4.0, y: backgroundFrame.minY + floor((backgroundFrame.height - image.size.height) / 2.0)), size: image.size)
|
||||
@ -2016,21 +2036,6 @@ public final class EmojiSearchHeaderView: UIView, UITextFieldDelegate {
|
||||
transition.setFrame(view: self.clearIconButton, frame: iconFrame.insetBy(dx: -8.0, dy: -10.0))
|
||||
}
|
||||
|
||||
if let textComponentView = self.textView.view {
|
||||
if textComponentView.superview == nil {
|
||||
self.addSubview(textComponentView)
|
||||
textComponentView.isUserInteractionEnabled = false
|
||||
}
|
||||
transition.setFrame(view: textComponentView, frame: textFrame)
|
||||
}
|
||||
if let tintTextComponentView = self.tintTextView.view {
|
||||
if tintTextComponentView.superview == nil {
|
||||
self.tintContainerView.addSubview(tintTextComponentView)
|
||||
tintTextComponentView.isUserInteractionEnabled = false
|
||||
}
|
||||
transition.setFrame(view: tintTextComponentView, frame: textFrame)
|
||||
}
|
||||
|
||||
if let cancelButtonTitleComponentView = self.cancelButtonTitle.view {
|
||||
if cancelButtonTitleComponentView.superview == nil {
|
||||
self.addSubview(cancelButtonTitleComponentView)
|
||||
@ -2055,9 +2060,10 @@ public final class EmojiSearchHeaderView: UIView, UITextFieldDelegate {
|
||||
hasText = true
|
||||
}
|
||||
}
|
||||
let _ = hasText
|
||||
|
||||
self.tintTextView.view?.isHidden = hasText
|
||||
self.textView.view?.isHidden = hasText
|
||||
/*self.tintTextView.view?.isHidden = hasText
|
||||
self.textView.view?.isHidden = hasText*/
|
||||
}
|
||||
}
|
||||
|
||||
@ -2233,7 +2239,7 @@ public final class EmojiPagerContentComponent: Component {
|
||||
public let presentGlobalOverlayController: (ViewController) -> Void
|
||||
public let navigationController: () -> NavigationController?
|
||||
public let requestUpdate: (Transition) -> Void
|
||||
public let updateSearchQuery: (String, String) -> Void
|
||||
public let updateSearchQuery: (EmojiPagerContentComponent.SearchQuery?) -> Void
|
||||
public let updateScrollingToItemGroup: () -> Void
|
||||
public let chatPeerId: PeerId?
|
||||
public let peekBehavior: EmojiContentPeekBehavior?
|
||||
@ -2256,7 +2262,7 @@ public final class EmojiPagerContentComponent: Component {
|
||||
presentGlobalOverlayController: @escaping (ViewController) -> Void,
|
||||
navigationController: @escaping () -> NavigationController?,
|
||||
requestUpdate: @escaping (Transition) -> Void,
|
||||
updateSearchQuery: @escaping (String, String) -> Void,
|
||||
updateSearchQuery: @escaping (SearchQuery?) -> Void,
|
||||
updateScrollingToItemGroup: @escaping () -> Void,
|
||||
chatPeerId: PeerId?,
|
||||
peekBehavior: EmojiContentPeekBehavior?,
|
||||
@ -2301,6 +2307,11 @@ public final class EmojiPagerContentComponent: Component {
|
||||
case flags = 7
|
||||
}
|
||||
|
||||
public enum SearchQuery: Equatable {
|
||||
case text(value: String, language: String)
|
||||
case category(value: String)
|
||||
}
|
||||
|
||||
public enum ItemContent: Equatable {
|
||||
public enum Id: Hashable {
|
||||
case animation(EntityKeyboardAnimationData.Id)
|
||||
@ -2521,6 +2532,7 @@ public final class EmojiPagerContentComponent: Component {
|
||||
public let itemContentUniqueId: AnyHashable?
|
||||
public let warpContentsOnEdges: Bool
|
||||
public let displaySearchWithPlaceholder: String?
|
||||
public let searchCategories: EmojiSearchCategories?
|
||||
public let searchInitiallyHidden: Bool
|
||||
public let searchIsPlaceholderOnly: Bool
|
||||
public let emptySearchResults: EmptySearchResults?
|
||||
@ -2540,6 +2552,7 @@ public final class EmojiPagerContentComponent: Component {
|
||||
itemContentUniqueId: AnyHashable?,
|
||||
warpContentsOnEdges: Bool,
|
||||
displaySearchWithPlaceholder: String?,
|
||||
searchCategories: EmojiSearchCategories?,
|
||||
searchInitiallyHidden: Bool,
|
||||
searchIsPlaceholderOnly: Bool,
|
||||
emptySearchResults: EmptySearchResults?,
|
||||
@ -2558,6 +2571,7 @@ public final class EmojiPagerContentComponent: Component {
|
||||
self.itemContentUniqueId = itemContentUniqueId
|
||||
self.warpContentsOnEdges = warpContentsOnEdges
|
||||
self.displaySearchWithPlaceholder = displaySearchWithPlaceholder
|
||||
self.searchCategories = searchCategories
|
||||
self.searchInitiallyHidden = searchInitiallyHidden
|
||||
self.searchIsPlaceholderOnly = searchIsPlaceholderOnly
|
||||
self.emptySearchResults = emptySearchResults
|
||||
@ -2579,6 +2593,7 @@ public final class EmojiPagerContentComponent: Component {
|
||||
itemContentUniqueId: itemContentUniqueId,
|
||||
warpContentsOnEdges: self.warpContentsOnEdges,
|
||||
displaySearchWithPlaceholder: self.displaySearchWithPlaceholder,
|
||||
searchCategories: self.searchCategories,
|
||||
searchInitiallyHidden: self.searchInitiallyHidden,
|
||||
searchIsPlaceholderOnly: self.searchIsPlaceholderOnly,
|
||||
emptySearchResults: emptySearchResults,
|
||||
@ -2624,6 +2639,9 @@ public final class EmojiPagerContentComponent: Component {
|
||||
if lhs.displaySearchWithPlaceholder != rhs.displaySearchWithPlaceholder {
|
||||
return false
|
||||
}
|
||||
if lhs.searchCategories != rhs.searchCategories {
|
||||
return false
|
||||
}
|
||||
if lhs.searchInitiallyHidden != rhs.searchInitiallyHidden {
|
||||
return false
|
||||
}
|
||||
@ -6157,11 +6175,13 @@ public final class EmojiPagerContentComponent: Component {
|
||||
let scrollSize = CGSize(width: availableSize.width, height: availableSize.height)
|
||||
transition.setPosition(view: self.scrollView, position: CGPoint(x: 0.0, y: scrollOriginY))
|
||||
|
||||
transition.setFrame(view: self.scrollViewClippingView, frame: CGRect(origin: CGPoint(x: 0.0, y: self.isSearchActivated ? itemLayout.itemInsets.top : 0.0), size: availableSize))
|
||||
transition.setBounds(view: self.scrollViewClippingView, bounds: CGRect(origin: CGPoint(x: 0.0, y: self.isSearchActivated ? itemLayout.itemInsets.top : 0.0), size: availableSize))
|
||||
let clippingTopInset: CGFloat = itemLayout.searchInsets.top + itemLayout.searchHeight + 2.0
|
||||
|
||||
transition.setFrame(view: self.vibrancyClippingView, frame: CGRect(origin: CGPoint(x: 0.0, y: self.isSearchActivated ? itemLayout.itemInsets.top : 0.0), size: availableSize))
|
||||
transition.setBounds(view: self.vibrancyClippingView, bounds: CGRect(origin: CGPoint(x: 0.0, y: self.isSearchActivated ? itemLayout.itemInsets.top : 0.0), size: availableSize))
|
||||
transition.setFrame(view: self.scrollViewClippingView, frame: CGRect(origin: CGPoint(x: 0.0, y: self.isSearchActivated ? clippingTopInset : 0.0), size: availableSize))
|
||||
transition.setBounds(view: self.scrollViewClippingView, bounds: CGRect(origin: CGPoint(x: 0.0, y: self.isSearchActivated ? clippingTopInset : 0.0), size: availableSize))
|
||||
|
||||
transition.setFrame(view: self.vibrancyClippingView, frame: CGRect(origin: CGPoint(x: 0.0, y: self.isSearchActivated ? clippingTopInset : 0.0), size: availableSize))
|
||||
transition.setBounds(view: self.vibrancyClippingView, bounds: CGRect(origin: CGPoint(x: 0.0, y: self.isSearchActivated ? clippingTopInset : 0.0), size: availableSize))
|
||||
|
||||
let previousSize = self.scrollView.bounds.size
|
||||
var resetScrolling = false
|
||||
@ -6180,7 +6200,7 @@ public final class EmojiPagerContentComponent: Component {
|
||||
let warpHeight: CGFloat = 50.0
|
||||
var topWarpInset = pagerEnvironment.containerInsets.top
|
||||
if self.isSearchActivated {
|
||||
topWarpInset += itemLayout.searchInsets.top + itemLayout.searchHeight
|
||||
topWarpInset = itemLayout.searchInsets.top + itemLayout.searchHeight
|
||||
}
|
||||
if let warpView = self.warpView {
|
||||
transition.setFrame(view: warpView, frame: CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: availableSize))
|
||||
@ -6374,11 +6394,11 @@ public final class EmojiPagerContentComponent: Component {
|
||||
} else {
|
||||
strongSelf.component?.inputInteractionHolder.inputInteraction?.requestUpdate(.immediate)
|
||||
}
|
||||
}, updateQuery: { [weak self] query, languageCode in
|
||||
}, updateQuery: { [weak self] query in
|
||||
guard let strongSelf = self else {
|
||||
return
|
||||
}
|
||||
strongSelf.component?.inputInteractionHolder.inputInteraction?.updateSearchQuery(query, languageCode)
|
||||
strongSelf.component?.inputInteractionHolder.inputInteraction?.updateSearchQuery(query)
|
||||
})
|
||||
self.visibleSearchHeader = visibleSearchHeader
|
||||
if self.isSearchActivated {
|
||||
@ -6391,7 +6411,7 @@ public final class EmojiPagerContentComponent: Component {
|
||||
}
|
||||
|
||||
let searchHeaderFrame = CGRect(origin: CGPoint(x: itemLayout.searchInsets.left, y: itemLayout.searchInsets.top), size: CGSize(width: itemLayout.width - itemLayout.searchInsets.left - itemLayout.searchInsets.right, height: itemLayout.searchHeight))
|
||||
visibleSearchHeader.update(theme: keyboardChildEnvironment.theme, strings: keyboardChildEnvironment.strings, text: displaySearchWithPlaceholder, useOpaqueTheme: useOpaqueTheme, isActive: self.isSearchActivated, size: searchHeaderFrame.size, canFocus: !component.searchIsPlaceholderOnly, hasSearchItems: component.displaySearchWithPlaceholder == keyboardChildEnvironment.strings.EmojiSearch_SearchEmojiPlaceholder, transition: transition)
|
||||
visibleSearchHeader.update(context: component.context, theme: keyboardChildEnvironment.theme, strings: keyboardChildEnvironment.strings, text: displaySearchWithPlaceholder, useOpaqueTheme: useOpaqueTheme, isActive: self.isSearchActivated, size: searchHeaderFrame.size, canFocus: !component.searchIsPlaceholderOnly, searchCategories: component.searchCategories, transition: transition)
|
||||
transition.attachAnimation(view: visibleSearchHeader, completion: { [weak self] completed in
|
||||
guard let strongSelf = self, completed, let visibleSearchHeader = strongSelf.visibleSearchHeader else {
|
||||
return
|
||||
@ -6445,7 +6465,7 @@ public final class EmojiPagerContentComponent: Component {
|
||||
}
|
||||
|
||||
var animateContentCrossfade = false
|
||||
if let previousComponent, previousComponent.itemContentUniqueId != component.itemContentUniqueId, itemTransition.animation.isImmediate, !transition.animation.isImmediate {
|
||||
if let previousComponent, previousComponent.itemContentUniqueId != component.itemContentUniqueId, itemTransition.animation.isImmediate {
|
||||
animateContentCrossfade = true
|
||||
}
|
||||
|
||||
@ -6570,14 +6590,24 @@ public final class EmojiPagerContentComponent: Component {
|
||||
availableReactions = .single(nil)
|
||||
}
|
||||
|
||||
let searchCategories: Signal<EmojiSearchCategories?, NoError>
|
||||
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<EmojiPagerContentComponent, NoError> = combineLatest(
|
||||
context.account.postbox.itemCollectionsView(orderedItemListCollectionIds: orderedItemListCollectionIds, namespaces: [Namespaces.ItemCollection.CloudEmojiPacks], aroundIndex: nil, count: 10000000),
|
||||
forceHasPremium ? .single(true) : hasPremium(context: context, chatPeerId: chatPeerId, premiumIfSavedMessages: true),
|
||||
context.account.viewTracker.featuredEmojiPacks(),
|
||||
availableReactions,
|
||||
searchCategories,
|
||||
iconStatusEmoji
|
||||
)
|
||||
|> map { view, hasPremium, featuredEmojiPacks, availableReactions, iconStatusEmoji -> EmojiPagerContentComponent in
|
||||
|> map { view, hasPremium, featuredEmojiPacks, availableReactions, searchCategories, iconStatusEmoji -> EmojiPagerContentComponent in
|
||||
struct ItemGroup {
|
||||
var supergroupId: AnyHashable
|
||||
var id: AnyHashable
|
||||
@ -7282,6 +7312,7 @@ public final class EmojiPagerContentComponent: Component {
|
||||
} else if isEmojiSelection {
|
||||
displaySearchWithPlaceholder = strings.EmojiSearch_SearchEmojiPlaceholder
|
||||
} else if isProfilePhotoEmojiSelection || isGroupPhotoEmojiSelection {
|
||||
//TODO:localize
|
||||
displaySearchWithPlaceholder = "Search"
|
||||
}
|
||||
}
|
||||
@ -7334,6 +7365,7 @@ public final class EmojiPagerContentComponent: Component {
|
||||
itemContentUniqueId: nil,
|
||||
warpContentsOnEdges: isReactionSelection || isStatusSelection,
|
||||
displaySearchWithPlaceholder: displaySearchWithPlaceholder,
|
||||
searchCategories: searchCategories,
|
||||
searchInitiallyHidden: searchInitiallyHidden,
|
||||
searchIsPlaceholderOnly: false,
|
||||
emptySearchResults: nil,
|
||||
@ -7848,6 +7880,7 @@ public final class EmojiPagerContentComponent: Component {
|
||||
itemContentUniqueId: nil,
|
||||
warpContentsOnEdges: false,
|
||||
displaySearchWithPlaceholder: hasSearch ? strings.StickersSearch_SearchStickersPlaceholder : nil,
|
||||
searchCategories: nil,
|
||||
searchInitiallyHidden: true,
|
||||
searchIsPlaceholderOnly: searchIsPlaceholderOnly,
|
||||
emptySearchResults: nil,
|
||||
|
@ -12,20 +12,76 @@ import AccountContext
|
||||
import AsyncDisplayKit
|
||||
import ComponentDisplayAdapters
|
||||
import LottieAnimationComponent
|
||||
import EmojiStatusComponent
|
||||
import LottieComponent
|
||||
import LottieComponentEmojiContent
|
||||
|
||||
private final class RoundMaskView: UIImageView {
|
||||
private var currentDiameter: CGFloat?
|
||||
|
||||
func update(diameter: CGFloat) {
|
||||
if self.currentDiameter != diameter {
|
||||
self.currentDiameter = diameter
|
||||
|
||||
let shadowWidth: CGFloat = 6.0
|
||||
self.image = generateImage(CGSize(width: shadowWidth * 2.0 + diameter, height: diameter), rotatedContext: { size, context in
|
||||
context.clear(CGRect(origin: CGPoint(), size: size))
|
||||
|
||||
let shadowColor = UIColor.black
|
||||
|
||||
let stepCount = 10
|
||||
var colors: [CGColor] = []
|
||||
var locations: [CGFloat] = []
|
||||
|
||||
for i in 0 ... stepCount {
|
||||
let t = CGFloat(i) / CGFloat(stepCount)
|
||||
colors.append(shadowColor.withAlphaComponent(t * t).cgColor)
|
||||
locations.append(t)
|
||||
}
|
||||
|
||||
let gradient = CGGradient(colorsSpace: deviceColorSpace, colors: colors as CFArray, locations: &locations)!
|
||||
|
||||
let center = CGPoint(x: size.width / 2.0, y: size.height / 2.0)
|
||||
let gradientWidth = shadowWidth
|
||||
context.drawRadialGradient(gradient, startCenter: center, startRadius: size.width / 2.0, endCenter: center, endRadius: size.width / 2.0 - gradientWidth, options: [])
|
||||
|
||||
context.setFillColor(shadowColor.cgColor)
|
||||
context.fillEllipse(in: CGRect(origin: CGPoint(x: shadowWidth, y: 0.0), size: CGSize(width: size.height, height: size.height)).insetBy(dx: -0.5, dy: -0.5))
|
||||
})?.stretchableImage(withLeftCapWidth: Int(shadowWidth * 0.5 + diameter * 0.5), topCapHeight: Int(diameter * 0.5))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
final class EmojiSearchSearchBarComponent: Component {
|
||||
enum TextInputState: Equatable {
|
||||
case inactive
|
||||
case active(hasText: Bool)
|
||||
}
|
||||
|
||||
let context: AccountContext
|
||||
let theme: PresentationTheme
|
||||
let strings: PresentationStrings
|
||||
let textInputState: TextInputState
|
||||
let categories: EmojiSearchCategories?
|
||||
let searchTermUpdated: (String?) -> Void
|
||||
let activateTextInput: () -> Void
|
||||
|
||||
init(
|
||||
context: AccountContext,
|
||||
theme: PresentationTheme,
|
||||
strings: PresentationStrings,
|
||||
searchTermUpdated: @escaping (String?) -> Void
|
||||
textInputState: TextInputState,
|
||||
categories: EmojiSearchCategories?,
|
||||
searchTermUpdated: @escaping (String?) -> Void,
|
||||
activateTextInput: @escaping () -> Void
|
||||
) {
|
||||
self.context = context
|
||||
self.theme = theme
|
||||
self.strings = strings
|
||||
self.textInputState = textInputState
|
||||
self.categories = categories
|
||||
self.searchTermUpdated = searchTermUpdated
|
||||
self.activateTextInput = activateTextInput
|
||||
}
|
||||
|
||||
static func ==(lhs: EmojiSearchSearchBarComponent, rhs: EmojiSearchSearchBarComponent) -> Bool {
|
||||
@ -35,6 +91,12 @@ final class EmojiSearchSearchBarComponent: Component {
|
||||
if lhs.strings !== rhs.strings {
|
||||
return false
|
||||
}
|
||||
if lhs.textInputState != rhs.textInputState {
|
||||
return false
|
||||
}
|
||||
if lhs.categories != rhs.categories {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
@ -44,20 +106,29 @@ final class EmojiSearchSearchBarComponent: Component {
|
||||
let itemSize: CGSize
|
||||
let itemSpacing: CGFloat
|
||||
let contentSize: CGSize
|
||||
let sideInset: CGFloat
|
||||
let leftInset: CGFloat
|
||||
let rightInset: CGFloat
|
||||
|
||||
init(containerSize: CGSize, itemCount: Int) {
|
||||
let textSpacing: CGFloat
|
||||
let textFrame: CGRect
|
||||
|
||||
init(containerSize: CGSize, textSize: CGSize, itemCount: Int) {
|
||||
self.containerSize = containerSize
|
||||
self.itemCount = itemCount
|
||||
self.itemSpacing = 8.0
|
||||
self.sideInset = 8.0
|
||||
self.leftInset = 6.0
|
||||
self.rightInset = 8.0
|
||||
self.itemSize = CGSize(width: 24.0, height: 24.0)
|
||||
self.textSpacing = 8.0
|
||||
|
||||
self.contentSize = CGSize(width: self.sideInset * 2.0 + self.itemSize.width * CGFloat(self.itemCount) + self.itemSpacing * CGFloat(max(0, self.itemCount - 1)), height: containerSize.height)
|
||||
self.textFrame = CGRect(origin: CGPoint(x: self.leftInset, y: floor((containerSize.height - textSize.height) * 0.5)), size: textSize)
|
||||
|
||||
self.contentSize = CGSize(width: self.leftInset + textSize.width + self.textSpacing + self.itemSize.width * CGFloat(self.itemCount) + self.itemSpacing * CGFloat(max(0, self.itemCount - 1)) + self.rightInset, height: containerSize.height)
|
||||
}
|
||||
|
||||
func visibleItems(for rect: CGRect) -> Range<Int>? {
|
||||
let offsetRect = rect.offsetBy(dx: -self.sideInset, dy: 0.0)
|
||||
let baseItemX: CGFloat = self.textFrame.maxX + self.textSpacing
|
||||
let offsetRect = rect.offsetBy(dx: -baseItemX, dy: 0.0)
|
||||
var minVisibleIndex = Int(floor((offsetRect.minX - self.itemSpacing) / (self.itemSize.width + self.itemSpacing)))
|
||||
minVisibleIndex = max(0, minVisibleIndex)
|
||||
var maxVisibleIndex = Int(ceil((offsetRect.maxX - self.itemSpacing) / (self.itemSize.height + self.itemSpacing)))
|
||||
@ -71,17 +142,54 @@ final class EmojiSearchSearchBarComponent: Component {
|
||||
}
|
||||
|
||||
func frame(at index: Int) -> CGRect {
|
||||
return CGRect(origin: CGPoint(x: self.sideInset + CGFloat(index) * (self.itemSize.width + self.itemSpacing), y: floor((self.containerSize.height - self.itemSize.height) * 0.5)), size: self.itemSize)
|
||||
return CGRect(origin: CGPoint(x: self.textFrame.maxX + self.textSpacing + CGFloat(index) * (self.itemSize.width + self.itemSpacing), y: floor((self.containerSize.height - self.itemSize.height) * 0.5)), size: self.itemSize)
|
||||
}
|
||||
}
|
||||
|
||||
private final class ContentScrollView: UIScrollView, PagerExpandableScrollView {
|
||||
override static var layerClass: AnyClass {
|
||||
return EmojiPagerContentComponent.View.ContentScrollLayer.self
|
||||
}
|
||||
|
||||
private let mirrorView: UIView
|
||||
|
||||
init(mirrorView: UIView) {
|
||||
self.mirrorView = mirrorView
|
||||
|
||||
super.init(frame: CGRect())
|
||||
|
||||
(self.layer as? EmojiPagerContentComponent.View.ContentScrollLayer)?.mirrorLayer = mirrorView.layer
|
||||
self.canCancelContentTouches = true
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
override func touchesShouldCancel(in view: UIView) -> Bool {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
private final class ItemView {
|
||||
let view = ComponentView<Empty>()
|
||||
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<Empty>] = [:]
|
||||
private let tintTextView = ComponentView<Empty>()
|
||||
private let textView = ComponentView<Empty>()
|
||||
|
||||
private var visibleItemViews: [AnyHashable: ItemView] = [:]
|
||||
private let selectedItemBackground: SimpleLayer
|
||||
|
||||
private var items: [String] = []
|
||||
private let selectedItemTintBackground: SimpleLayer
|
||||
|
||||
private var component: EmojiSearchSearchBarComponent?
|
||||
private weak var state: EmptyComponentState?
|
||||
@ -89,15 +197,23 @@ final class EmojiSearchSearchBarComponent: Component {
|
||||
private var itemLayout: ItemLayout?
|
||||
private var ignoreScrolling: Bool = false
|
||||
|
||||
private let maskLayer: SimpleLayer
|
||||
private let roundMaskView: RoundMaskView
|
||||
private let tintRoundMaskView: RoundMaskView
|
||||
|
||||
private var selectedItem: String?
|
||||
private var selectedItem: AnyHashable?
|
||||
|
||||
override init(frame: CGRect) {
|
||||
self.scrollView = UIScrollView()
|
||||
self.maskLayer = SimpleLayer()
|
||||
self.tintContainerView = UIView()
|
||||
|
||||
self.tintScrollView = UIView()
|
||||
self.tintScrollView.clipsToBounds = true
|
||||
self.scrollView = ContentScrollView(mirrorView: self.tintScrollView)
|
||||
|
||||
self.roundMaskView = RoundMaskView()
|
||||
self.tintRoundMaskView = RoundMaskView()
|
||||
|
||||
self.selectedItemBackground = SimpleLayer()
|
||||
self.selectedItemTintBackground = SimpleLayer()
|
||||
|
||||
super.init(frame: frame)
|
||||
|
||||
@ -111,20 +227,20 @@ final class EmojiSearchSearchBarComponent: Component {
|
||||
self.scrollView.showsVerticalScrollIndicator = true
|
||||
self.scrollView.showsHorizontalScrollIndicator = false
|
||||
self.scrollView.delegate = self
|
||||
self.scrollView.clipsToBounds = false
|
||||
self.scrollView.clipsToBounds = true
|
||||
self.scrollView.scrollsToTop = false
|
||||
|
||||
self.addSubview(self.scrollView)
|
||||
|
||||
//self.layer.mask = self.maskLayer
|
||||
self.layer.addSublayer(self.maskLayer)
|
||||
self.layer.masksToBounds = true
|
||||
self.tintContainerView.addSubview(self.tintScrollView)
|
||||
|
||||
self.mask = self.roundMaskView
|
||||
self.tintContainerView.mask = self.tintRoundMaskView
|
||||
|
||||
self.scrollView.layer.addSublayer(self.selectedItemBackground)
|
||||
self.tintScrollView.layer.addSublayer(self.selectedItemTintBackground)
|
||||
|
||||
self.scrollView.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.tapGesture(_:))))
|
||||
|
||||
self.items = ["Smile", "🤔", "😝", "😡", "😐", "🏌️♀️", "🎉", "😨", "❤️", "😄", "👍", "☹️", "👎", "⛔", "💤", "💼", "🍔", "🏠", "🛁", "🏖", "⚽️", "🕔"]
|
||||
self.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.tapGesture(_:))))
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
@ -133,18 +249,28 @@ final class EmojiSearchSearchBarComponent: Component {
|
||||
|
||||
@objc private func tapGesture(_ recognizer: UITapGestureRecognizer) {
|
||||
if case .ended = recognizer.state {
|
||||
guard let component = self.component, let itemLayout = self.itemLayout else {
|
||||
return
|
||||
}
|
||||
let location = recognizer.location(in: self.scrollView)
|
||||
for (id, itemView) in self.visibleItemViews {
|
||||
if let itemComponentView = itemView.view, itemComponentView.frame.contains(location), let item = id.base as? String {
|
||||
if self.selectedItem == item {
|
||||
self.selectedItem = nil
|
||||
} else {
|
||||
self.selectedItem = item
|
||||
if location.x <= itemLayout.textFrame.maxX + itemLayout.textSpacing {
|
||||
component.activateTextInput()
|
||||
} else {
|
||||
for (id, itemView) in self.visibleItemViews {
|
||||
if let itemComponentView = itemView.view.view, itemComponentView.frame.contains(location), let itemId = id.base as? Int64 {
|
||||
if self.selectedItem == AnyHashable(id) {
|
||||
self.selectedItem = nil
|
||||
} else {
|
||||
self.selectedItem = AnyHashable(id)
|
||||
}
|
||||
self.state?.updated(transition: .immediate)
|
||||
|
||||
if let categories = component.categories, let group = categories.groups.first(where: { $0.id == itemId }) {
|
||||
component.searchTermUpdated(group.identifiers.joined(separator: ""))
|
||||
}
|
||||
|
||||
break
|
||||
}
|
||||
self.state?.updated(transition: .immediate)
|
||||
self.component?.searchTermUpdated(self.selectedItem)
|
||||
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -155,7 +281,7 @@ final class EmojiSearchSearchBarComponent: Component {
|
||||
self.selectedItem = nil
|
||||
self.state?.updated(transition: .immediate)
|
||||
if dispatchEvent {
|
||||
self.component?.searchTermUpdated(self.selectedItem)
|
||||
self.component?.searchTermUpdated(nil)
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -171,6 +297,14 @@ final class EmojiSearchSearchBarComponent: Component {
|
||||
return
|
||||
}
|
||||
|
||||
let itemAlpha: CGFloat
|
||||
switch component.textInputState {
|
||||
case .active:
|
||||
itemAlpha = 0.0
|
||||
case .inactive:
|
||||
itemAlpha = 1.0
|
||||
}
|
||||
|
||||
var validItemIds = Set<AnyHashable>()
|
||||
let visibleBounds = self.scrollView.bounds
|
||||
|
||||
@ -179,70 +313,49 @@ final class EmojiSearchSearchBarComponent: Component {
|
||||
animateAppearingItems = true
|
||||
}
|
||||
|
||||
let items = self.items
|
||||
let items = component.categories?.groups ?? []
|
||||
|
||||
for i in 0 ..< items.count {
|
||||
let itemFrame = itemLayout.frame(at: i)
|
||||
if visibleBounds.intersects(itemFrame) {
|
||||
let item = items[i]
|
||||
validItemIds.insert(AnyHashable(item))
|
||||
validItemIds.insert(AnyHashable(item.id))
|
||||
|
||||
var animateItem = false
|
||||
var itemTransition = transition
|
||||
let itemView: ComponentView<Empty>
|
||||
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<Empty>()
|
||||
self.visibleItemViews[item] = itemView
|
||||
itemView = ItemView()
|
||||
self.visibleItemViews[AnyHashable(item.id)] = itemView
|
||||
}
|
||||
|
||||
let animationName: String
|
||||
let color = component.theme.chat.inputMediaPanel.panelContentVibrantOverlayColor
|
||||
|
||||
switch EmojiPagerContentComponent.StaticEmojiSegment.allCases[i % EmojiPagerContentComponent.StaticEmojiSegment.allCases.count] {
|
||||
case .people:
|
||||
animationName = "emojicat_smiles"
|
||||
case .animalsAndNature:
|
||||
animationName = "emojicat_animals"
|
||||
case .foodAndDrink:
|
||||
animationName = "emojicat_food"
|
||||
case .activityAndSport:
|
||||
animationName = "emojicat_activity"
|
||||
case .travelAndPlaces:
|
||||
animationName = "emojicat_places"
|
||||
case .objects:
|
||||
animationName = "emojicat_objects"
|
||||
case .symbols:
|
||||
animationName = "emojicat_symbols"
|
||||
case .flags:
|
||||
animationName = "emojicat_flags"
|
||||
}
|
||||
|
||||
let baseColor: UIColor
|
||||
baseColor = component.theme.chat.inputMediaPanel.panelIconColor
|
||||
|
||||
let baseHighlightedColor = component.theme.chat.inputMediaPanel.panelHighlightedIconBackgroundColor.blitOver(component.theme.chat.inputPanel.panelBackgroundColor, alpha: 1.0)
|
||||
let color = baseColor.blitOver(baseHighlightedColor, alpha: 1.0)
|
||||
|
||||
let _ = itemTransition
|
||||
let _ = itemView.update(
|
||||
let _ = itemView.view.update(
|
||||
transition: .immediate,
|
||||
component: AnyComponent(LottieAnimationComponent(
|
||||
animation: LottieAnimationComponent.AnimationItem(
|
||||
name: animationName,
|
||||
mode: .still(position: .end)
|
||||
component: AnyComponent(LottieComponent(
|
||||
content: LottieComponent.EmojiContent(
|
||||
context: component.context,
|
||||
fileId: item.id
|
||||
),
|
||||
colors: ["__allcolors__": color],
|
||||
size: itemLayout.itemSize
|
||||
color: color
|
||||
)),
|
||||
environment: {},
|
||||
containerSize: itemLayout.itemSize
|
||||
)
|
||||
if let view = itemView.view {
|
||||
|
||||
itemView.tintView.tintColor = .white
|
||||
|
||||
if let view = itemView.view.view as? LottieComponent.View {
|
||||
if view.superview == nil {
|
||||
self.scrollView.addSubview(view)
|
||||
|
||||
view.output = itemView.tintView
|
||||
self.tintScrollView.addSubview(itemView.tintView)
|
||||
}
|
||||
|
||||
itemTransition.setPosition(view: view, position: CGPoint(x: itemFrame.midX, y: itemFrame.midY))
|
||||
@ -250,18 +363,24 @@ final class EmojiSearchSearchBarComponent: Component {
|
||||
let scaleFactor = itemFrame.width / itemLayout.itemSize.width
|
||||
itemTransition.setSublayerTransform(view: view, transform: CATransform3DMakeScale(scaleFactor, scaleFactor, 1.0))
|
||||
|
||||
itemTransition.setPosition(view: itemView.tintView, position: CGPoint(x: itemFrame.midX, y: itemFrame.midY))
|
||||
itemTransition.setBounds(view: itemView.tintView, bounds: CGRect(origin: CGPoint(), size: CGSize(width: itemLayout.itemSize.width, height: itemLayout.itemSize.height)))
|
||||
itemTransition.setSublayerTransform(view: itemView.tintView, transform: CATransform3DMakeScale(scaleFactor, scaleFactor, 1.0))
|
||||
|
||||
itemTransition.setAlpha(view: view, alpha: itemAlpha)
|
||||
itemTransition.setAlpha(view: itemView.tintView, alpha: itemAlpha)
|
||||
|
||||
let isHidden = !visibleBounds.intersects(itemFrame)
|
||||
if isHidden != view.isHidden {
|
||||
view.isHidden = isHidden
|
||||
itemView.tintView.isHidden = true
|
||||
|
||||
if !isHidden {
|
||||
if let view = view as? LottieAnimationComponent.View {
|
||||
view.playOnce()
|
||||
}
|
||||
view.playOnce()
|
||||
}
|
||||
} else if animateItem {
|
||||
if let view = view as? LottieAnimationComponent.View {
|
||||
view.playOnce()
|
||||
if fromScrolling {
|
||||
view.playOnce(delay: 0.08)
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -272,25 +391,32 @@ final class EmojiSearchSearchBarComponent: Component {
|
||||
for (id, itemView) in self.visibleItemViews {
|
||||
if !validItemIds.contains(id) {
|
||||
removedItemIds.append(id)
|
||||
itemView.view?.removeFromSuperview()
|
||||
itemView.view.view?.removeFromSuperview()
|
||||
itemView.tintView.removeFromSuperview()
|
||||
}
|
||||
}
|
||||
for id in removedItemIds {
|
||||
self.visibleItemViews.removeValue(forKey: id)
|
||||
}
|
||||
|
||||
if let selectedItem = self.selectedItem, let index = self.items.firstIndex(of: selectedItem) {
|
||||
if let selectedItem = self.selectedItem, let index = items.firstIndex(where: { AnyHashable($0.id) == selectedItem }) {
|
||||
self.selectedItemBackground.isHidden = false
|
||||
self.selectedItemTintBackground.isHidden = false
|
||||
|
||||
let selectedItemCenter = itemLayout.frame(at: index).center
|
||||
let selectionSize = CGSize(width: 28.0, height: 28.0)
|
||||
|
||||
self.selectedItemBackground.backgroundColor = component.theme.chat.inputMediaPanel.panelContentControlOpaqueSelectionColor.cgColor
|
||||
self.selectedItemBackground.backgroundColor = component.theme.chat.inputMediaPanel.panelContentControlVibrantSelectionColor.cgColor
|
||||
self.selectedItemTintBackground.backgroundColor = UIColor(white: 1.0, alpha: 0.2).cgColor
|
||||
self.selectedItemBackground.cornerRadius = selectionSize.height * 0.5
|
||||
self.selectedItemTintBackground.cornerRadius = selectionSize.height * 0.5
|
||||
|
||||
self.selectedItemBackground.frame = CGRect(origin: CGPoint(x: floor(selectedItemCenter.x - selectionSize.width * 0.5), y: floor(selectedItemCenter.y - selectionSize.height * 0.5)), size: selectionSize)
|
||||
let selectionFrame = CGRect(origin: CGPoint(x: floor(selectedItemCenter.x - selectionSize.width * 0.5), y: floor(selectedItemCenter.y - selectionSize.height * 0.5)), size: selectionSize)
|
||||
self.selectedItemBackground.frame = selectionFrame
|
||||
self.selectedItemTintBackground.frame = selectionFrame
|
||||
} else {
|
||||
self.selectedItemBackground.isHidden = true
|
||||
self.selectedItemTintBackground.isHidden = true
|
||||
}
|
||||
}
|
||||
|
||||
@ -298,11 +424,43 @@ final class EmojiSearchSearchBarComponent: Component {
|
||||
self.component = component
|
||||
self.state = state
|
||||
|
||||
transition.setCornerRadius(layer: self.layer, cornerRadius: availableSize.height * 0.5)
|
||||
let textSize = self.textView.update(
|
||||
transition: .immediate,
|
||||
component: AnyComponent(Text(
|
||||
text: component.strings.Common_Search,
|
||||
font: Font.regular(17.0),
|
||||
color: component.theme.chat.inputMediaPanel.panelContentVibrantOverlayColor
|
||||
)),
|
||||
environment: {},
|
||||
containerSize: CGSize(width: availableSize.width - 32.0, height: 100.0)
|
||||
)
|
||||
let _ = self.tintTextView.update(
|
||||
transition: .immediate,
|
||||
component: AnyComponent(Text(
|
||||
text: component.strings.Common_Search,
|
||||
font: Font.regular(17.0),
|
||||
color: .white
|
||||
)),
|
||||
environment: {},
|
||||
containerSize: CGSize(width: availableSize.width - 32.0, height: 100.0)
|
||||
)
|
||||
|
||||
let itemLayout = ItemLayout(containerSize: availableSize, itemCount: self.items.count)
|
||||
let itemLayout = ItemLayout(containerSize: availableSize, textSize: textSize, itemCount: component.categories?.groups.count ?? 0)
|
||||
self.itemLayout = itemLayout
|
||||
|
||||
if let textComponentView = self.textView.view {
|
||||
if textComponentView.superview == nil {
|
||||
self.scrollView.addSubview(textComponentView)
|
||||
}
|
||||
transition.setFrame(view: textComponentView, frame: itemLayout.textFrame)
|
||||
}
|
||||
if let tintTextComponentView = self.tintTextView.view {
|
||||
if tintTextComponentView.superview == nil {
|
||||
self.tintScrollView.addSubview(tintTextComponentView)
|
||||
}
|
||||
transition.setFrame(view: tintTextComponentView, frame: itemLayout.textFrame)
|
||||
}
|
||||
|
||||
self.ignoreScrolling = true
|
||||
if self.scrollView.bounds.size != availableSize {
|
||||
transition.setFrame(view: self.scrollView, frame: CGRect(origin: CGPoint(), size: availableSize))
|
||||
@ -312,8 +470,25 @@ final class EmojiSearchSearchBarComponent: Component {
|
||||
}
|
||||
self.ignoreScrolling = false
|
||||
|
||||
let maskFrame = CGRect(origin: CGPoint(), size: availableSize)
|
||||
transition.setFrame(view: self.roundMaskView, frame: maskFrame)
|
||||
self.roundMaskView.update(diameter: maskFrame.height)
|
||||
transition.setFrame(view: self.tintRoundMaskView, frame: maskFrame)
|
||||
self.tintRoundMaskView.update(diameter: maskFrame.height)
|
||||
|
||||
self.updateScrolling(transition: transition, fromScrolling: false)
|
||||
|
||||
switch component.textInputState {
|
||||
case let .active(hasText):
|
||||
self.isUserInteractionEnabled = false
|
||||
self.textView.view?.isHidden = hasText
|
||||
self.tintTextView.view?.isHidden = hasText
|
||||
case .inactive:
|
||||
self.isUserInteractionEnabled = true
|
||||
self.textView.view?.isHidden = false
|
||||
self.tintTextView.view?.isHidden = false
|
||||
}
|
||||
|
||||
return availableSize
|
||||
}
|
||||
}
|
||||
|
@ -971,7 +971,7 @@ public final class GifPagerContentComponent: Component {
|
||||
}
|
||||
strongSelf.component?.inputInteraction.openSearch()
|
||||
}, deactivated: { _ in
|
||||
}, updateQuery: {_, _ in
|
||||
}, updateQuery: { _ in
|
||||
})
|
||||
self.visibleSearchHeader = visibleSearchHeader
|
||||
self.scrollView.addSubview(visibleSearchHeader)
|
||||
@ -979,7 +979,7 @@ public final class GifPagerContentComponent: Component {
|
||||
}
|
||||
|
||||
let searchHeaderFrame = CGRect(origin: CGPoint(x: itemLayout.searchInsets.left, y: itemLayout.searchInsets.top), size: CGSize(width: itemLayout.width - itemLayout.searchInsets.left - itemLayout.searchInsets.right, height: itemLayout.searchHeight))
|
||||
visibleSearchHeader.update(theme: keyboardChildEnvironment.theme, strings: keyboardChildEnvironment.strings, text: displaySearchWithPlaceholder, useOpaqueTheme: false, isActive: false, size: searchHeaderFrame.size, canFocus: false, hasSearchItems: true, transition: transition)
|
||||
visibleSearchHeader.update(context: component.context, theme: keyboardChildEnvironment.theme, strings: keyboardChildEnvironment.strings, text: displaySearchWithPlaceholder, useOpaqueTheme: false, isActive: false, size: searchHeaderFrame.size, canFocus: false, searchCategories: nil, transition: transition)
|
||||
transition.setFrame(view: visibleSearchHeader, frame: searchHeaderFrame, completion: { [weak self] completed in
|
||||
guard let strongSelf = self, completed, let visibleSearchHeader = strongSelf.visibleSearchHeader else {
|
||||
return
|
||||
|
@ -963,7 +963,7 @@ private final class ForumCreateTopicScreenComponent: CombinedComponent {
|
||||
},
|
||||
requestUpdate: { _ in
|
||||
},
|
||||
updateSearchQuery: { _, _ in
|
||||
updateSearchQuery: { _ in
|
||||
},
|
||||
updateScrollingToItemGroup: {
|
||||
},
|
||||
|
22
submodules/TelegramUI/Components/LottieComponent/BUILD
Normal file
22
submodules/TelegramUI/Components/LottieComponent/BUILD
Normal file
@ -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",
|
||||
],
|
||||
)
|
@ -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<EnvironmentType>, transition: Transition) -> CGSize {
|
||||
let previousComponent = self.component
|
||||
|
||||
self.component = component
|
||||
self.state = state
|
||||
|
||||
var redrawImage = false
|
||||
|
||||
let displaySize = CGSize(width: availableSize.width * UIScreenScale, height: availableSize.height * UIScreenScale)
|
||||
if self.currentDisplaySize != displaySize {
|
||||
self.currentDisplaySize = displaySize
|
||||
redrawImage = true
|
||||
}
|
||||
|
||||
if previousComponent?.content != component.content {
|
||||
self.currentContentDisposable?.dispose()
|
||||
let content = component.content
|
||||
self.currentContentDisposable = component.content.load { [weak self, weak content] data in
|
||||
Queue.mainQueue().async {
|
||||
guard let self, self.component?.content == content else {
|
||||
return
|
||||
}
|
||||
self.loadAnimation(data: data)
|
||||
}
|
||||
}
|
||||
} else if redrawImage {
|
||||
self.updateImage()
|
||||
}
|
||||
|
||||
if self.tintColor != component.color {
|
||||
self.tintColor = component.color
|
||||
}
|
||||
|
||||
return availableSize
|
||||
}
|
||||
}
|
||||
|
||||
public func makeView() -> View {
|
||||
return View(frame: CGRect())
|
||||
}
|
||||
|
||||
public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment<EnvironmentType>, transition: Transition) -> CGSize {
|
||||
return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition)
|
||||
}
|
||||
}
|
@ -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",
|
||||
],
|
||||
)
|
@ -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<Data?, NoError> 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)
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
@ -7130,6 +7130,11 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
|
||||
}
|
||||
strongSelf.commitPurposefulAction()
|
||||
|
||||
var hasDisabledContent = false
|
||||
if "".isEmpty {
|
||||
hasDisabledContent = false
|
||||
}
|
||||
|
||||
if let channel = strongSelf.presentationInterfaceState.renderedPeer?.peer as? TelegramChannel, channel.isRestrictedBySlowmode {
|
||||
let forwardCount = messages.reduce(0, { count, message -> Int in
|
||||
if case .forward = message {
|
||||
@ -7144,6 +7149,8 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
|
||||
errorText = strongSelf.presentationData.strings.Chat_AttachmentMultipleForwardDisabled
|
||||
} else if isAnyMessageTextPartitioned {
|
||||
errorText = strongSelf.presentationData.strings.Chat_MultipleTextMessagesDisabled
|
||||
} else if hasDisabledContent {
|
||||
errorText = strongSelf.restrictedSendingContentsText()
|
||||
}
|
||||
|
||||
if let errorText = errorText {
|
||||
@ -8449,6 +8456,8 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
|
||||
return
|
||||
}
|
||||
|
||||
strongSelf.dismissAllTooltips()
|
||||
|
||||
strongSelf.mediaRecordingModeTooltipController?.dismiss()
|
||||
strongSelf.interfaceInteraction?.updateShowWebView { _ in
|
||||
return false
|
||||
@ -8460,14 +8469,12 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
|
||||
bannedMediaInput = true
|
||||
} else if channel.hasBannedPermission(.banSendVoice) != nil {
|
||||
if !isVideo {
|
||||
//TODO:localize
|
||||
strongSelf.controllerInteraction?.displayUndo(.info(title: nil, text: "The admins of this group do not allow to send voice messages."))
|
||||
strongSelf.controllerInteraction?.displayUndo(.info(title: nil, text: strongSelf.restrictedSendingContentsText()))
|
||||
return
|
||||
}
|
||||
} else if channel.hasBannedPermission(.banSendInstantVideos) != nil {
|
||||
if isVideo {
|
||||
//TODO:localize
|
||||
strongSelf.controllerInteraction?.displayUndo(.info(title: nil, text: "The admins of this group do not allow to send video messages."))
|
||||
strongSelf.controllerInteraction?.displayUndo(.info(title: nil, text: strongSelf.restrictedSendingContentsText()))
|
||||
return
|
||||
}
|
||||
}
|
||||
@ -8476,22 +8483,19 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
|
||||
bannedMediaInput = true
|
||||
} else if group.hasBannedPermission(.banSendVoice) {
|
||||
if !isVideo {
|
||||
//TODO:localize
|
||||
strongSelf.controllerInteraction?.displayUndo(.info(title: nil, text: "The admins of this group do not allow to send voice messages."))
|
||||
strongSelf.controllerInteraction?.displayUndo(.info(title: nil, text: strongSelf.restrictedSendingContentsText()))
|
||||
return
|
||||
}
|
||||
} else if group.hasBannedPermission(.banSendInstantVideos) {
|
||||
if isVideo {
|
||||
//TODO:localize
|
||||
strongSelf.controllerInteraction?.displayUndo(.info(title: nil, text: "The admins of this group do not allow to send video messages."))
|
||||
strongSelf.controllerInteraction?.displayUndo(.info(title: nil, text: strongSelf.restrictedSendingContentsText()))
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if bannedMediaInput {
|
||||
//TODO:localize
|
||||
strongSelf.controllerInteraction?.displayUndo(.info(title: nil, text: "The admins of this group do not allow to send video and voice messages."))
|
||||
strongSelf.controllerInteraction?.displayUndo(.info(title: nil, text: strongSelf.restrictedSendingContentsText()))
|
||||
return
|
||||
}
|
||||
|
||||
@ -8792,8 +8796,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
|
||||
}
|
||||
|
||||
if bannedMediaInput {
|
||||
//TODO:localize
|
||||
strongSelf.controllerInteraction?.displayUndo(.info(title: nil, text: "The admins of this group do not allow to send video and voice messages."))
|
||||
strongSelf.controllerInteraction?.displayUndo(.info(title: nil, text: strongSelf.restrictedSendingContentsText()))
|
||||
return
|
||||
}
|
||||
|
||||
@ -10133,8 +10136,8 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
|
||||
text = strongSelf.presentationData.strings.Conversation_SendMessageErrorGroupRestricted
|
||||
moreInfo = true
|
||||
case .mediaRestricted:
|
||||
strongSelf.interfaceInteraction?.displayRestrictedInfo(.mediaRecording, .alert)
|
||||
return
|
||||
text = strongSelf.restrictedSendingContentsText()
|
||||
moreInfo = false
|
||||
case .slowmodeActive:
|
||||
text = strongSelf.presentationData.strings.Chat_SlowmodeSendError
|
||||
moreInfo = false
|
||||
@ -12239,6 +12242,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
|
||||
|
||||
self.chatDisplayNode.dismissInput()
|
||||
|
||||
var banSendText: (Int32, Bool)?
|
||||
var bannedSendPhotos: (Int32, Bool)?
|
||||
var bannedSendVideos: (Int32, Bool)?
|
||||
var bannedSendFiles: (Int32, Bool)?
|
||||
@ -12258,6 +12262,9 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
|
||||
if let value = channel.hasBannedPermission(.banSendFiles) {
|
||||
bannedSendFiles = value
|
||||
}
|
||||
if let value = channel.hasBannedPermission(.banSendText) {
|
||||
banSendText = value
|
||||
}
|
||||
if channel.hasBannedPermission(.banSendPolls) != nil {
|
||||
canSendPolls = false
|
||||
}
|
||||
@ -12271,14 +12278,21 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
|
||||
if group.hasBannedPermission(.banSendFiles) {
|
||||
bannedSendFiles = (Int32.max, false)
|
||||
}
|
||||
if group.hasBannedPermission(.banSendText) {
|
||||
banSendText = (Int32.max, false)
|
||||
}
|
||||
if group.hasBannedPermission(.banSendPolls) {
|
||||
canSendPolls = false
|
||||
}
|
||||
}
|
||||
|
||||
var availableButtons: [AttachmentButtonType] = [.gallery, .file, .location, .contact]
|
||||
var availableButtons: [AttachmentButtonType] = [.gallery, .file]
|
||||
if banSendText == nil {
|
||||
availableButtons.append(.location)
|
||||
availableButtons.append(.contact)
|
||||
}
|
||||
if canSendPolls {
|
||||
availableButtons.insert(.poll, at: availableButtons.count - 1)
|
||||
availableButtons.insert(.poll, at: max(0, availableButtons.count - 1))
|
||||
}
|
||||
|
||||
let presentationData = self.presentationData
|
||||
@ -13402,22 +13416,19 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
|
||||
switch itemType {
|
||||
case .image:
|
||||
if bannedSendPhotos != nil {
|
||||
//TODO:localize
|
||||
strongSelf.present(standardTextAlertController(theme: AlertControllerTheme(presentationData: strongSelf.presentationData), title: nil, text: "Sending photos is not allowed", actions: [TextAlertAction(type: .defaultAction, title: strongSelf.presentationData.strings.Common_OK, action: {})]), in: .window(.root))
|
||||
strongSelf.present(standardTextAlertController(theme: AlertControllerTheme(presentationData: strongSelf.presentationData), title: nil, text: strongSelf.restrictedSendingContentsText(), actions: [TextAlertAction(type: .defaultAction, title: strongSelf.presentationData.strings.Common_OK, action: {})]), in: .window(.root))
|
||||
|
||||
return false
|
||||
}
|
||||
case .video:
|
||||
if bannedSendVideos != nil {
|
||||
//TODO:localize
|
||||
strongSelf.present(standardTextAlertController(theme: AlertControllerTheme(presentationData: strongSelf.presentationData), title: nil, text: "Sending videos is not allowed", actions: [TextAlertAction(type: .defaultAction, title: strongSelf.presentationData.strings.Common_OK, action: {})]), in: .window(.root))
|
||||
strongSelf.present(standardTextAlertController(theme: AlertControllerTheme(presentationData: strongSelf.presentationData), title: nil, text: strongSelf.restrictedSendingContentsText(), actions: [TextAlertAction(type: .defaultAction, title: strongSelf.presentationData.strings.Common_OK, action: {})]), in: .window(.root))
|
||||
|
||||
return false
|
||||
}
|
||||
case .gif:
|
||||
if bannedSendGifs != nil {
|
||||
//TODO:localize
|
||||
strongSelf.present(standardTextAlertController(theme: AlertControllerTheme(presentationData: strongSelf.presentationData), title: nil, text: "Sending gifs is not allowed", actions: [TextAlertAction(type: .defaultAction, title: strongSelf.presentationData.strings.Common_OK, action: {})]), in: .window(.root))
|
||||
strongSelf.present(standardTextAlertController(theme: AlertControllerTheme(presentationData: strongSelf.presentationData), title: nil, text: strongSelf.restrictedSendingContentsText(), actions: [TextAlertAction(type: .defaultAction, title: strongSelf.presentationData.strings.Common_OK, action: {})]), in: .window(.root))
|
||||
|
||||
return false
|
||||
}
|
||||
@ -17982,6 +17993,83 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
|
||||
let transition = ContainedViewLayoutTransition.animated(duration: 0.45, curve: .customSpring(damping: 180.0, initialVelocity: 0.0))
|
||||
transition.updateTransformScale(node: self.chatDisplayNode.historyNodeContainer, scale: scale)
|
||||
}
|
||||
|
||||
func restrictedSendingContentsText() -> String {
|
||||
//TODO:localize
|
||||
guard let peer = self.presentationInterfaceState.renderedPeer?.peer else {
|
||||
return "Sending messages is disabled in this chat"
|
||||
}
|
||||
|
||||
var itemList: [String] = []
|
||||
|
||||
var flags: TelegramChatBannedRightsFlags = []
|
||||
if let channel = peer as? TelegramChannel {
|
||||
if let bannedRights = channel.bannedRights {
|
||||
flags = bannedRights.flags
|
||||
}
|
||||
} else if let group = peer as? TelegramGroup {
|
||||
if let bannedRights = group.defaultBannedRights {
|
||||
flags = bannedRights.flags
|
||||
}
|
||||
}
|
||||
|
||||
let order: [TelegramChatBannedRightsFlags] = [
|
||||
.banSendText,
|
||||
.banSendPhotos,
|
||||
.banSendVideos,
|
||||
.banSendVoice,
|
||||
.banSendInstantVideos,
|
||||
.banSendFiles,
|
||||
.banSendMusic,
|
||||
.banSendStickers
|
||||
]
|
||||
|
||||
for right in order {
|
||||
if !flags.contains(right) {
|
||||
var title: String?
|
||||
switch right {
|
||||
case .banSendText:
|
||||
title = "text messages"
|
||||
case .banSendPhotos:
|
||||
title = "photos"
|
||||
case .banSendVideos:
|
||||
title = "videos"
|
||||
case .banSendVoice:
|
||||
title = "voice messages"
|
||||
case .banSendInstantVideos:
|
||||
title = "video messages"
|
||||
case .banSendFiles:
|
||||
title = "files"
|
||||
case .banSendMusic:
|
||||
title = "music"
|
||||
case .banSendStickers:
|
||||
title = "Stickers & GIFs"
|
||||
default:
|
||||
break
|
||||
}
|
||||
if let title {
|
||||
itemList.append(title)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if itemList.isEmpty {
|
||||
return "Sending messages is disabled in this chat"
|
||||
}
|
||||
|
||||
var itemListString = ""
|
||||
for i in 0 ..< itemList.count {
|
||||
if i != 0 {
|
||||
itemListString.append(", ")
|
||||
}
|
||||
if i == itemList.count - 1 && i != 0 {
|
||||
itemListString.append("and ")
|
||||
}
|
||||
itemListString.append(itemList[i])
|
||||
}
|
||||
|
||||
return "The admins of this group only allow to send \(itemListString)."
|
||||
}
|
||||
}
|
||||
|
||||
private final class ContextControllerContentSourceImpl: ContextControllerContentSource {
|
||||
|
@ -615,7 +615,7 @@ class ChatControllerNode: ASDisplayNode, UIScrollViewDelegate {
|
||||
|
||||
self.addSubnode(self.messageTransitionNode)
|
||||
self.contentContainerNode.addSubnode(self.navigateButtons)
|
||||
self.contentContainerNode.addSubnode(self.presentationContextMarker)
|
||||
self.addSubnode(self.presentationContextMarker)
|
||||
self.contentContainerNode.addSubnode(self.contentDimNode)
|
||||
|
||||
self.navigationBar?.additionalContentNode.addSubnode(self.titleAccessoryPanelContainer)
|
||||
|
Loading…
x
Reference in New Issue
Block a user