This commit is contained in:
Ali 2023-01-19 17:50:17 +04:00
parent 7cea72619c
commit dac58e50b3
34 changed files with 2317 additions and 930 deletions

View File

@ -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))

View File

@ -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
}
}

View File

@ -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

View File

@ -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)

View File

@ -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 {

View File

@ -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))))
}))
}
},

View File

@ -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()
}

View File

@ -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:

View File

@ -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
}
}
}
}

View File

@ -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
}
}
}
}

View File

@ -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?

View File

@ -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
}
}
}
}

View File

@ -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?)

View File

@ -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())

View File

@ -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)
}
}

View File

@ -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
}

View File

@ -2,6 +2,7 @@ import Foundation
import Postbox
import TelegramApi
import SwiftSignalKit
import MtProtoKit
private func hashForIds(_ ids: [Int64]) -> Int64 {
var acc: UInt64 = 0

View File

@ -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 {

View File

@ -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> {

View File

@ -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([])
}
}
}
}
}

View File

@ -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: {
},

View File

@ -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: {
},

View File

@ -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: {

View File

@ -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",

View File

@ -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,

View File

@ -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
}
}

View File

@ -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

View File

@ -963,7 +963,7 @@ private final class ForumCreateTopicScreenComponent: CombinedComponent {
},
requestUpdate: { _ in
},
updateSearchQuery: { _, _ in
updateSearchQuery: { _ in
},
updateScrollingToItemGroup: {
},

View 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",
],
)

View File

@ -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)
}
}

View File

@ -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",
],
)

View File

@ -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)
})
}
}
}

View File

@ -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 {

View File

@ -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)