Merge commit '6df5f109739b05467716dbeaef51b197e1efc106'

This commit is contained in:
isaac
2026-04-25 20:50:35 +04:00
18 changed files with 1551 additions and 334 deletions
@@ -20,6 +20,8 @@ private let markdownInlineHTMLInlineIntent = InlinePresentationIntent(rawValue:
private let markdownDefaultBlockImageDimensions = PixelDimensions(width: 1200, height: 900)
private let markdownDefaultInlineImageDimensions = PixelDimensions(width: 18, height: 18)
private let markdownTaskListUncheckedNumber = "\u{001f}tg-md-task:unchecked"
private let markdownTaskListCheckedNumber = "\u{001f}tg-md-task:checked"
private struct MarkdownPageResult {
let blocks: [InstantPageBlock]
@@ -88,6 +90,11 @@ private enum MarkdownResolvedImageSource {
case unsupported
}
private enum MarkdownTaskListState {
case unchecked
case checked
}
private final class MarkdownConversionContext {
private let context: AccountContext
fileprivate let documentURL: URL
@@ -326,8 +333,6 @@ private func markdownBlocks(from node: MarkdownIntentNode, context: MarkdownConv
return []
}
if level <= 1 {
return [.title(text)]
} else if level == 2 {
return [.header(text)]
} else {
return [.heading(text: text, level: Int32(max(3, min(level, 6))))]
@@ -396,18 +401,28 @@ private func markdownListItems(from nodes: [MarkdownIntentNode], ordered: Bool,
guard case let .listItem(ordinal) = node.kind else {
continue
}
let blocks = markdownBlocks(from: node.children, context: context)
guard !blocks.isEmpty else {
continue
}
var blocks = markdownBlocks(from: node.children, context: context)
let taskListState = markdownApplyTaskListMarker(to: &blocks)
let number: String?
if ordered {
if let taskListState {
number = markdownTaskListNumber(for: taskListState)
} else if ordered {
number = "\(ordinal)"
} else {
number = nil
}
if blocks.isEmpty {
if let number {
result.append(.text(.plain(" "), number))
}
continue
}
if blocks.count == 1, case let .paragraph(text) = blocks[0] {
result.append(.text(text, number))
if number != nil && markdownIsWhitespaceOnly(text) {
result.append(.text(.plain(" "), number))
} else {
result.append(.text(text, number))
}
} else {
result.append(.blocks(blocks, number))
}
@@ -415,6 +430,52 @@ private func markdownListItems(from nodes: [MarkdownIntentNode], ordered: Bool,
return result
}
private func markdownTaskListNumber(for state: MarkdownTaskListState) -> String {
switch state {
case .unchecked:
return markdownTaskListUncheckedNumber
case .checked:
return markdownTaskListCheckedNumber
}
}
private func markdownApplyTaskListMarker(to blocks: inout [InstantPageBlock]) -> MarkdownTaskListState? {
guard !blocks.isEmpty, case let .paragraph(text) = blocks[0] else {
return nil
}
guard let (state, strippedText) = markdownStrippingTaskListMarker(from: text) else {
return nil
}
if blocks.count > 1 && markdownIsWhitespaceOnly(strippedText) {
blocks.removeFirst()
} else {
blocks[0] = .paragraph(strippedText)
}
return state
}
private func markdownStrippingTaskListMarker(from text: RichText) -> (MarkdownTaskListState, RichText)? {
guard let (state, markerLength) = markdownTaskListMarker(in: text.plainText) else {
return nil
}
return (state, markdownDroppingPrefixLength(markerLength, from: text))
}
private func markdownTaskListMarker(in plainText: String) -> (MarkdownTaskListState, Int)? {
switch plainText {
case _ where plainText.hasPrefix("[ ] "):
return (.unchecked, 4)
case "[ ]":
return (.unchecked, 3)
case _ where plainText.hasPrefix("[x] "), _ where plainText.hasPrefix("[X] "):
return (.checked, 4)
case "[x]", "[X]":
return (.checked, 3)
default:
return nil
}
}
private func markdownTableRows(from nodes: [MarkdownIntentNode], alignments: [TableHorizontalAlignment], context: MarkdownConversionContext) -> [InstantPageTableRow] {
var result: [InstantPageTableRow] = []
for node in nodes {
@@ -831,6 +892,79 @@ private func markdownCompact(_ fragments: [RichText]) -> RichText {
}
}
private func markdownDroppingPrefixLength(_ length: Int, from text: RichText) -> RichText {
guard length > 0 else {
return text
}
switch text {
case .empty:
return .empty
case let .plain(string):
let nsString = string as NSString
if nsString.length <= length {
return .empty
} else {
return .plain(nsString.substring(from: length))
}
case let .bold(inner):
let dropped = markdownDroppingPrefixLength(length, from: inner)
return dropped == .empty ? .empty : .bold(dropped)
case let .italic(inner):
let dropped = markdownDroppingPrefixLength(length, from: inner)
return dropped == .empty ? .empty : .italic(dropped)
case let .underline(inner):
let dropped = markdownDroppingPrefixLength(length, from: inner)
return dropped == .empty ? .empty : .underline(dropped)
case let .strikethrough(inner):
let dropped = markdownDroppingPrefixLength(length, from: inner)
return dropped == .empty ? .empty : .strikethrough(dropped)
case let .fixed(inner):
let dropped = markdownDroppingPrefixLength(length, from: inner)
return dropped == .empty ? .empty : .fixed(dropped)
case let .url(inner, url, webpageId):
let dropped = markdownDroppingPrefixLength(length, from: inner)
return dropped == .empty ? .empty : .url(text: dropped, url: url, webpageId: webpageId)
case let .email(inner, email):
let dropped = markdownDroppingPrefixLength(length, from: inner)
return dropped == .empty ? .empty : .email(text: dropped, email: email)
case let .concat(items):
var remainingLength = length
var result: [RichText] = []
result.reserveCapacity(items.count)
for item in items {
if remainingLength > 0 {
let itemLength = (item.plainText as NSString).length
if itemLength <= remainingLength {
remainingLength -= itemLength
continue
}
result.append(markdownDroppingPrefixLength(remainingLength, from: item))
remainingLength = 0
} else {
result.append(item)
}
}
return markdownCompact(result)
case let .subscript(inner):
let dropped = markdownDroppingPrefixLength(length, from: inner)
return dropped == .empty ? .empty : .subscript(dropped)
case let .superscript(inner):
let dropped = markdownDroppingPrefixLength(length, from: inner)
return dropped == .empty ? .empty : .superscript(dropped)
case let .marked(inner):
let dropped = markdownDroppingPrefixLength(length, from: inner)
return dropped == .empty ? .empty : .marked(dropped)
case let .phone(inner, phone):
let dropped = markdownDroppingPrefixLength(length, from: inner)
return dropped == .empty ? .empty : .phone(text: dropped, phone: phone)
case .image:
return text
case let .anchor(inner, name):
let dropped = markdownDroppingPrefixLength(length, from: inner)
return dropped == .empty ? .empty : .anchor(text: dropped, name: name)
}
}
private func markdownHasDisplayableContent(_ richText: RichText) -> Bool {
switch richText {
case .empty:
+1
View File
@@ -12,6 +12,7 @@ swift_library(
deps = [
"//submodules/SSignalKit/SwiftSignalKit:SwiftSignalKit",
"//submodules/AsyncDisplayKit:AsyncDisplayKit",
"//submodules/CheckNode:CheckNode",
"//submodules/Display:Display",
"//submodules/Postbox:Postbox",
"//submodules/TelegramCore:TelegramCore",
@@ -0,0 +1,101 @@
import Foundation
import UIKit
import TelegramCore
import AsyncDisplayKit
import Display
import TelegramPresentationData
import TelegramUIPreferences
import AccountContext
import ContextUI
import CheckNode
final class InstantPageChecklistMarkerItem: InstantPageItem {
var frame: CGRect
let checked: Bool
let wantsNode: Bool = true
let separatesTiles: Bool = false
let medias: [InstantPageMedia] = []
init(frame: CGRect, checked: Bool) {
self.frame = frame
self.checked = checked
}
func matchesAnchor(_ anchor: String) -> Bool {
return false
}
func drawInTile(context: CGContext) {
}
func node(context: AccountContext, strings: PresentationStrings, nameDisplayOrder: PresentationPersonNameOrder, theme: InstantPageTheme, sourceLocation: InstantPageSourceLocation, openMedia: @escaping (InstantPageMedia) -> Void, longPressMedia: @escaping (InstantPageMedia) -> Void, activatePinchPreview: ((PinchSourceContainerNode) -> Void)?, pinchPreviewFinished: ((InstantPageNode) -> Void)?, openPeer: @escaping (EnginePeer) -> Void, openUrl: @escaping (InstantPageUrlItem) -> Void, updateWebEmbedHeight: @escaping (CGFloat) -> Void, updateDetailsExpanded: @escaping (Bool) -> Void, currentExpandedDetails: [Int : Bool]?, getPreloadedResource: @escaping (String) -> Data?) -> InstantPageNode? {
return InstantPageChecklistMarkerNode(theme: theme, checked: self.checked)
}
func matchesNode(_ node: InstantPageNode) -> Bool {
if let node = node as? InstantPageChecklistMarkerNode {
return node.checked == self.checked
} else {
return false
}
}
func linkSelectionRects(at point: CGPoint) -> [CGRect] {
return []
}
func distanceThresholdGroup() -> Int? {
return nil
}
func distanceThresholdWithGroupCount(_ count: Int) -> CGFloat {
return 0.0
}
}
private func instantPageChecklistMarkerTheme(theme: InstantPageTheme) -> CheckNodeTheme {
return CheckNodeTheme(
backgroundColor: theme.panelAccentColor,
strokeColor: theme.pageBackgroundColor,
borderColor: theme.controlColor,
overlayBorder: false,
hasInset: false,
hasShadow: false
)
}
final class InstantPageChecklistMarkerNode: ASDisplayNode, InstantPageNode {
let checked: Bool
private let checkNode: CheckNode
init(theme: InstantPageTheme, checked: Bool) {
self.checked = checked
self.checkNode = CheckNode(theme: instantPageChecklistMarkerTheme(theme: theme), content: .check(isRectangle: true))
super.init()
self.isUserInteractionEnabled = false
self.checkNode.isUserInteractionEnabled = false
self.addSubnode(self.checkNode)
self.checkNode.setSelected(checked, animated: false)
}
func updateIsVisible(_ isVisible: Bool) {
}
func transitionNode(media: InstantPageMedia) -> (ASDisplayNode, CGRect, () -> (UIView?, UIView?))? {
return nil
}
func updateHiddenMedia(media: InstantPageMedia?) {
}
func update(strings: PresentationStrings, theme: InstantPageTheme) {
self.checkNode.theme = instantPageChecklistMarkerTheme(theme: theme)
}
func updateLayout(size: CGSize, transition: ContainedViewLayoutTransition) {
transition.updateFrame(node: self.checkNode, frame: CGRect(origin: .zero, size: size))
}
}
@@ -131,6 +131,40 @@ private func attributedStringForPreformattedText(_ text: RichText, language: Str
return attributedString
}
private let instantPageTaskListUncheckedNumber = "\u{001f}tg-md-task:unchecked"
private let instantPageTaskListCheckedNumber = "\u{001f}tg-md-task:checked"
private let instantPageChecklistMarkerSize = CGSize(width: 18.0, height: 18.0)
private func instantPageTaskListMarkerState(_ number: String?) -> Bool? {
switch number {
case instantPageTaskListUncheckedNumber:
return false
case instantPageTaskListCheckedNumber:
return true
default:
return nil
}
}
private func instantPageFirstTextLineMidY(in items: [InstantPageItem]) -> CGFloat? {
for item in items {
if let textItem = item as? InstantPageTextItem {
if let line = textItem.lines.first {
return textItem.frame.minY + line.frame.midY
} else {
return textItem.frame.midY
}
} else if let scrollableTextItem = item as? InstantPageScrollableTextItem {
if let line = scrollableTextItem.item.lines.first {
return scrollableTextItem.frame.minY + scrollableTextItem.item.frame.minY + line.frame.midY
} else {
return scrollableTextItem.frame.midY
}
}
}
return nil
}
public func layoutInstantPageBlock(webpage: TelegramMediaWebpage, userLocation: MediaResourceUserLocation, rtl: Bool, block: InstantPageBlock, boundingWidth: CGFloat, horizontalInset: CGFloat, safeInset: CGFloat, isCover: Bool, previousItems: [InstantPageItem], fillToSize: CGSize?, media: [EngineMedia.Id: EngineMedia], mediaIndexCounter: inout Int, embedIndexCounter: inout Int, detailsIndexCounter: inout Int, theme: InstantPageTheme, strings: PresentationStrings, dateTimeFormat: PresentationDateTimeFormat, webEmbedHeights: [Int : CGFloat] = [:], cachedMessageSyntaxHighlight: CachedMessageSyntaxHighlight? = nil, excludeCaptions: Bool) -> InstantPageLayout {
let layoutCaption: (InstantPageCaption, CGSize) -> ([InstantPageItem], CGSize) = { caption, contentSize in
@@ -298,12 +332,21 @@ public func layoutInstantPageBlock(webpage: TelegramMediaWebpage, userLocation:
var maxIndexWidth: CGFloat = 0.0
var listItems: [InstantPageItem] = []
var indexItems: [InstantPageItem] = []
var hasTaskMarkers = false
var hasNums = false
if ordered {
for item in contentItems {
if let num = item.num, !num.isEmpty {
if instantPageTaskListMarkerState(item.num) != nil {
hasTaskMarkers = true
} else if let num = item.num, !num.isEmpty {
hasNums = true
}
}
} else {
for item in contentItems {
if instantPageTaskListMarkerState(item.num) != nil {
hasTaskMarkers = true
break
}
}
@@ -311,7 +354,13 @@ public func layoutInstantPageBlock(webpage: TelegramMediaWebpage, userLocation:
for i in 0 ..< contentItems.count {
let item = contentItems[i]
if ordered {
if let checked = instantPageTaskListMarkerState(item.num) {
let checklistItem = InstantPageChecklistMarkerItem(frame: CGRect(origin: .zero, size: instantPageChecklistMarkerSize), checked: checked)
if ordered {
maxIndexWidth = max(maxIndexWidth, instantPageChecklistMarkerSize.width)
}
indexItems.append(checklistItem)
} else if ordered {
let styleStack = InstantPageTextStyleStack()
setupStyleStack(styleStack, theme: theme, category: .paragraph, link: false)
let value: String
@@ -335,7 +384,7 @@ public func layoutInstantPageBlock(webpage: TelegramMediaWebpage, userLocation:
indexItems.append(shapeItem)
}
}
let indexSpacing: CGFloat = ordered ? 12.0 : 20.0
let indexSpacing: CGFloat = ordered ? (hasTaskMarkers ? 16.0 : 12.0) : (hasTaskMarkers ? 24.0 : 20.0)
for (i, item) in contentItems.enumerated() {
if (i != 0) {
contentSize.height += 18.0
@@ -366,6 +415,12 @@ public func layoutInstantPageBlock(webpage: TelegramMediaWebpage, userLocation:
if let textIndexItem = indexItem as? InstantPageTextItem, let line = textIndexItem.lines.first {
itemFrame = itemFrame.offsetBy(dx: horizontalInset + maxIndexWidth - line.frame.width, dy: floorToScreenPixels(lineMidY - (itemFrame.height / 2.0)))
} else if indexItem is InstantPageChecklistMarkerItem {
if ordered {
itemFrame = itemFrame.offsetBy(dx: horizontalInset + maxIndexWidth - itemFrame.width, dy: floorToScreenPixels(lineMidY - (itemFrame.height / 2.0)))
} else {
itemFrame = itemFrame.offsetBy(dx: horizontalInset, dy: floorToScreenPixels(lineMidY - (itemFrame.height / 2.0)))
}
} else {
itemFrame = itemFrame.offsetBy(dx: horizontalInset, dy: floorToScreenPixels(lineMidY - itemFrame.height / 2.0))
}
@@ -375,6 +430,7 @@ public func layoutInstantPageBlock(webpage: TelegramMediaWebpage, userLocation:
case let .blocks(blocks, _):
var previousBlock: InstantPageBlock?
var originY: CGFloat = contentSize.height
var firstBlockLineMidY: CGFloat?
for subBlock in blocks {
let subLayout = layoutInstantPageBlock(webpage: webpage, userLocation: userLocation, rtl: rtl, block: subBlock, boundingWidth: boundingWidth - horizontalInset * 2.0 - indexSpacing - maxIndexWidth, horizontalInset: 0.0, safeInset: 0.0, isCover: false, previousItems: listItems, fillToSize: nil, media: media, mediaIndexCounter: &mediaIndexCounter, embedIndexCounter: &embedIndexCounter, detailsIndexCounter: &detailsIndexCounter, theme: theme, strings: strings, dateTimeFormat: dateTimeFormat, webEmbedHeights: webEmbedHeights, cachedMessageSyntaxHighlight: cachedMessageSyntaxHighlight, excludeCaptions: false)
@@ -383,6 +439,9 @@ public func layoutInstantPageBlock(webpage: TelegramMediaWebpage, userLocation:
if previousBlock == nil {
originY += spacing
}
if firstBlockLineMidY == nil {
firstBlockLineMidY = instantPageFirstTextLineMidY(in: blockItems)
}
listItems.append(contentsOf: blockItems)
contentSize.height += subLayout.contentSize.height + spacing
previousBlock = subBlock
@@ -391,6 +450,18 @@ public func layoutInstantPageBlock(webpage: TelegramMediaWebpage, userLocation:
var indexItemFrame = indexItem.frame
if let textIndexItem = indexItem as? InstantPageTextItem, let line = textIndexItem.lines.first {
indexItemFrame = indexItemFrame.offsetBy(dx: horizontalInset + maxIndexWidth - line.frame.width, dy: originY)
} else if indexItem is InstantPageChecklistMarkerItem {
let markerOriginY: CGFloat
if let firstBlockLineMidY {
markerOriginY = floorToScreenPixels(firstBlockLineMidY - indexItemFrame.height / 2.0)
} else {
markerOriginY = originY
}
if ordered {
indexItemFrame = indexItemFrame.offsetBy(dx: horizontalInset + maxIndexWidth - indexItemFrame.width, dy: markerOriginY)
} else {
indexItemFrame = indexItemFrame.offsetBy(dx: horizontalInset, dy: markerOriginY)
}
} else {
indexItemFrame = indexItemFrame.offsetBy(dx: horizontalInset, dy: originY)
}
@@ -1093,6 +1093,7 @@ fileprivate let parsers: [Int32 : (BufferReader) -> Any?] = {
dict[1236871718] = { return Api.TodoList.parse_todoList($0) }
dict[-305282981] = { return Api.TopPeer.parse_topPeer($0) }
dict[-39945236] = { return Api.TopPeerCategory.parse_topPeerCategoryBotsApp($0) }
dict[1814361053] = { return Api.TopPeerCategory.parse_topPeerCategoryBotsGuestChat($0) }
dict[344356834] = { return Api.TopPeerCategory.parse_topPeerCategoryBotsInline($0) }
dict[-1419371685] = { return Api.TopPeerCategory.parse_topPeerCategoryBotsPM($0) }
dict[371037736] = { return Api.TopPeerCategory.parse_topPeerCategoryChannels($0) }
@@ -1694,6 +1694,7 @@ public extension Api {
public extension Api {
enum TopPeerCategory: TypeConstructorDescription {
case topPeerCategoryBotsApp
case topPeerCategoryBotsGuestChat
case topPeerCategoryBotsInline
case topPeerCategoryBotsPM
case topPeerCategoryChannels
@@ -1710,6 +1711,11 @@ public extension Api {
buffer.appendInt32(-39945236)
}
break
case .topPeerCategoryBotsGuestChat:
if boxed {
buffer.appendInt32(1814361053)
}
break
case .topPeerCategoryBotsInline:
if boxed {
buffer.appendInt32(344356834)
@@ -1757,6 +1763,8 @@ public extension Api {
switch self {
case .topPeerCategoryBotsApp:
return ("topPeerCategoryBotsApp", [])
case .topPeerCategoryBotsGuestChat:
return ("topPeerCategoryBotsGuestChat", [])
case .topPeerCategoryBotsInline:
return ("topPeerCategoryBotsInline", [])
case .topPeerCategoryBotsPM:
@@ -1779,6 +1787,9 @@ public extension Api {
public static func parse_topPeerCategoryBotsApp(_ reader: BufferReader) -> TopPeerCategory? {
return Api.TopPeerCategory.topPeerCategoryBotsApp
}
public static func parse_topPeerCategoryBotsGuestChat(_ reader: BufferReader) -> TopPeerCategory? {
return Api.TopPeerCategory.topPeerCategoryBotsGuestChat
}
public static func parse_topPeerCategoryBotsInline(_ reader: BufferReader) -> TopPeerCategory? {
return Api.TopPeerCategory.topPeerCategoryBotsInline
}
@@ -7486,6 +7486,25 @@ public extension Api.functions.messages {
})
}
}
public extension Api.functions.messages {
static func getPersonalChannelHistory(userId: Api.InputUser, limit: Int32, maxId: Int32, minId: Int32, hash: Int64) -> (FunctionDescription, Buffer, DeserializeFunctionResponse<Api.messages.Messages>) {
let buffer = Buffer()
buffer.appendInt32(1442515350)
userId.serialize(buffer, true)
serializeInt32(limit, buffer: buffer, boxed: false)
serializeInt32(maxId, buffer: buffer, boxed: false)
serializeInt32(minId, buffer: buffer, boxed: false)
serializeInt64(hash, buffer: buffer, boxed: false)
return (FunctionDescription(name: "messages.getPersonalChannelHistory", parameters: [("userId", ConstructorParameterDescription(userId)), ("limit", ConstructorParameterDescription(limit)), ("maxId", ConstructorParameterDescription(maxId)), ("minId", ConstructorParameterDescription(minId)), ("hash", ConstructorParameterDescription(hash))]), buffer, DeserializeFunctionResponse { (buffer: Buffer) -> Api.messages.Messages? in
let reader = BufferReader(buffer)
var result: Api.messages.Messages?
if let signature = reader.readInt32() {
result = Api.parse(reader, signature: signature) as? Api.messages.Messages
}
return result
})
}
}
public extension Api.functions.messages {
static func getPinnedDialogs(folderId: Int32) -> (FunctionDescription, Buffer, DeserializeFunctionResponse<Api.messages.PeerDialogs>) {
let buffer = Buffer()
@@ -174,7 +174,7 @@ public extension TelegramEngine {
return _internal_deleteAllReactionsWithAuthor(account: self.account, peerId: peerId, authorId: authorId)
}
public func deleteReaction(peerId: EnginePeer.Id, messageId: EngineMessage.Id, authorId: EnginePeer.Id) -> Signal<Never, NoError> {
public func deleteReaction(messageId: EngineMessage.Id, authorId: EnginePeer.Id) -> Signal<Never, NoError> {
return _internal_deleteReaction(account: self.account, messageId: messageId, authorId: authorId)
}
@@ -151,7 +151,10 @@ func _internal_updateRecentPeersEnabled(postbox: Postbox, network: Network, enab
}
func _internal_managedRecentlyUsedInlineBots(postbox: Postbox, network: Network, accountPeerId: PeerId) -> Signal<Void, NoError> {
let remotePeers = network.request(Api.functions.contacts.getTopPeers(flags: 1 << 2, offset: 0, limit: 16, hash: 0))
var flags: Int32 = 0
flags |= 1 << 2
flags |= 1 << 17
let remotePeers = network.request(Api.functions.contacts.getTopPeers(flags: flags, offset: 0, limit: 24, hash: 0))
|> retryRequestIfNotFrozen
|> map { result -> (AccumulatedPeers, [(PeerId, Double)])? in
switch result {
@@ -196,6 +196,11 @@ private enum AdminUserActionOptionSection {
case ban
}
private enum AdminUserDeleteAllOption {
case messages
case reactions
}
private enum AdminUserActionConfigItem: Hashable, CaseIterable {
case sendMessages
case sendMedia
@@ -318,6 +323,7 @@ private final class AdminUserActionsContentComponent: Component {
let toggleOptionSelection: (AdminUserActionOptionSection) -> Void
let toggleOptionExpansion: (AdminUserActionOptionSection) -> Void
let togglePeerSelection: (AdminUserActionOptionSection, EnginePeer) -> Void
let toggleDeleteAllOptionPeerSelection: (AdminUserDeleteAllOption, EnginePeer) -> Void
let toggleConfiguration: () -> Void
let toggleConfigItem: (AdminUserActionConfigItem) -> Void
let toggleMediaSectionExpansion: () -> Void
@@ -336,6 +342,7 @@ private final class AdminUserActionsContentComponent: Component {
toggleOptionSelection: @escaping (AdminUserActionOptionSection) -> Void,
toggleOptionExpansion: @escaping (AdminUserActionOptionSection) -> Void,
togglePeerSelection: @escaping (AdminUserActionOptionSection, EnginePeer) -> Void,
toggleDeleteAllOptionPeerSelection: @escaping (AdminUserDeleteAllOption, EnginePeer) -> Void,
toggleConfiguration: @escaping () -> Void,
toggleConfigItem: @escaping (AdminUserActionConfigItem) -> Void,
toggleMediaSectionExpansion: @escaping () -> Void,
@@ -353,6 +360,7 @@ private final class AdminUserActionsContentComponent: Component {
self.toggleOptionSelection = toggleOptionSelection
self.toggleOptionExpansion = toggleOptionExpansion
self.togglePeerSelection = togglePeerSelection
self.toggleDeleteAllOptionPeerSelection = toggleDeleteAllOptionPeerSelection
self.toggleConfiguration = toggleConfiguration
self.toggleConfigItem = toggleConfigItem
self.toggleMediaSectionExpansion = toggleMediaSectionExpansion
@@ -436,11 +444,12 @@ private final class AdminUserActionsContentComponent: Component {
var accessory: ListActionItemComponent.Accessory?
var isExpandable = false
if component.peers.count > 1 {
let selectedCount = selectedPeers.union(additionalSelectedPeers).count
accessory = .custom(ListActionItemComponent.CustomAccessory(
component: AnyComponentWithIdentity(id: 0, component: AnyComponent(PlainButtonComponent(
content: AnyComponent(OptionSectionExpandIndicatorComponent(
theme: component.theme,
count: selectedPeers.isEmpty ? component.peers.count : selectedPeers.count,
count: selectedCount == 0 ? component.peers.count : selectedCount,
isExpanded: isExpanded
)),
effectAlignment: .center,
@@ -464,7 +473,7 @@ private final class AdminUserActionsContentComponent: Component {
AnyComponentWithIdentity(id: 1, component: AnyComponent(MediaSectionExpandIndicatorComponent(
theme: component.theme,
title: "\(count)/2",
isExpanded: component.sheetState.isMediaSectionExpanded
isExpanded: isExpanded
)))
)
isExpandable = true
@@ -521,7 +530,7 @@ private final class AdminUserActionsContentComponent: Component {
sideInset: 0.0,
title: peer.peer.displayTitle(strings: component.strings, displayOrder: .firstLast),
peer: peer.peer,
selectionState: .editing(isSelected: selectedPeers.contains(peer.peer.id)),
selectionState: .editing(isSelected: selectedPeers.contains(peer.peer.id) || additionalSelectedPeers.contains(peer.peer.id)),
action: { peer in
component.togglePeerSelection(section, peer)
}
@@ -544,13 +553,13 @@ private final class AdminUserActionsContentComponent: Component {
leftIcon: .check(ListActionItemComponent.LeftIcon.Check(
isSelected: !selectedPeers.isEmpty,
toggle: {
component.toggleOptionSelection(section)
component.toggleDeleteAllOptionPeerSelection(.messages, component.peers[0].peer)
}
)),
icon: .none,
accessory: nil,
action: { _ in
component.toggleOptionSelection(section)
component.toggleDeleteAllOptionPeerSelection(.messages, component.peers[0].peer)
},
highlighting: .disabled
)))
@@ -570,13 +579,13 @@ private final class AdminUserActionsContentComponent: Component {
leftIcon: .check(ListActionItemComponent.LeftIcon.Check(
isSelected: !additionalSelectedPeers.isEmpty,
toggle: {
component.toggleOptionSelection(section)
component.toggleDeleteAllOptionPeerSelection(.reactions, component.peers[0].peer)
}
)),
icon: .none,
accessory: nil,
action: { _ in
component.toggleOptionSelection(section)
component.toggleDeleteAllOptionPeerSelection(.reactions, component.peers[0].peer)
},
highlighting: .disabled
)))
@@ -992,6 +1001,7 @@ private final class AdminUserActionsSheetComponent: Component {
private var optionReportSelectedPeers = Set<EnginePeer.Id>()
private var isOptionDeleteAllExpanded: Bool = false
private var optionDeleteAllSelectedPeers = Set<EnginePeer.Id>()
private var optionDeleteAllReactionsSelectedPeers = Set<EnginePeer.Id>()
private var isOptionBanExpanded: Bool = false
private var optionBanSelectedPeers = Set<EnginePeer.Id>()
@@ -1034,7 +1044,7 @@ private final class AdminUserActionsSheetComponent: Component {
deleteAllFromPeers.append(id)
}
for id in self.optionDeleteAllSelectedPeers.sorted() {
for id in self.optionDeleteAllReactionsSelectedPeers.sorted() {
deleteAllReactionsFromPeers.append(id)
}
@@ -1182,7 +1192,7 @@ private final class AdminUserActionsSheetComponent: Component {
optionReportSelectedPeers: self.optionReportSelectedPeers,
isOptionDeleteAllExpanded: self.isOptionDeleteAllExpanded,
optionDeleteAllSelectedPeers: self.optionDeleteAllSelectedPeers,
optionDeleteAllReactionsSelectedPeers: self.optionDeleteAllSelectedPeers,
optionDeleteAllReactionsSelectedPeers: self.optionDeleteAllReactionsSelectedPeers,
isOptionBanExpanded: self.isOptionBanExpanded,
optionBanSelectedPeers: self.optionBanSelectedPeers,
isConfigurationExpanded: self.isConfigurationExpanded,
@@ -1238,7 +1248,17 @@ private final class AdminUserActionsSheetComponent: Component {
case .report:
selectedPeers = self.optionReportSelectedPeers
case .deleteAll:
selectedPeers = self.optionDeleteAllSelectedPeers
let allPeerIds = Set(component.peers.map { $0.peer.id })
if self.optionDeleteAllSelectedPeers.isEmpty && self.optionDeleteAllReactionsSelectedPeers.isEmpty {
self.optionDeleteAllSelectedPeers = allPeerIds
self.optionDeleteAllReactionsSelectedPeers = allPeerIds
} else {
self.optionDeleteAllSelectedPeers.removeAll()
self.optionDeleteAllReactionsSelectedPeers.removeAll()
}
self.state?.updated(transition: .spring(duration: 0.35))
return
case .ban:
selectedPeers = self.optionBanSelectedPeers
}
@@ -1291,7 +1311,16 @@ private final class AdminUserActionsSheetComponent: Component {
case .report:
selectedPeers = self.optionReportSelectedPeers
case .deleteAll:
selectedPeers = self.optionDeleteAllSelectedPeers
if self.optionDeleteAllSelectedPeers.contains(peer.id) || self.optionDeleteAllReactionsSelectedPeers.contains(peer.id) {
self.optionDeleteAllSelectedPeers.remove(peer.id)
self.optionDeleteAllReactionsSelectedPeers.remove(peer.id)
} else {
self.optionDeleteAllSelectedPeers.insert(peer.id)
self.optionDeleteAllReactionsSelectedPeers.insert(peer.id)
}
self.state?.updated(transition: ComponentTransition(animation: .curve(duration: 0.3, curve: .easeInOut)))
return
case .ban:
selectedPeers = self.optionBanSelectedPeers
}
@@ -1313,6 +1342,28 @@ private final class AdminUserActionsSheetComponent: Component {
self.state?.updated(transition: ComponentTransition(animation: .curve(duration: 0.3, curve: .easeInOut)))
},
toggleDeleteAllOptionPeerSelection: { [weak self] option, peer in
guard let self else {
return
}
switch option {
case .messages:
if self.optionDeleteAllSelectedPeers.contains(peer.id) {
self.optionDeleteAllSelectedPeers.remove(peer.id)
} else {
self.optionDeleteAllSelectedPeers.insert(peer.id)
}
case .reactions:
if self.optionDeleteAllReactionsSelectedPeers.contains(peer.id) {
self.optionDeleteAllReactionsSelectedPeers.remove(peer.id)
} else {
self.optionDeleteAllReactionsSelectedPeers.insert(peer.id)
}
}
self.state?.updated(transition: ComponentTransition(animation: .curve(duration: 0.3, curve: .easeInOut)))
},
toggleConfiguration: { [weak self] in
guard let self, let component = self.component else {
return
@@ -2634,7 +2634,7 @@ public class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDeleg
size: CGSize(width: baseWidth, height: panelHeight)
)
let audioRecordingTimeFrame = CGRect(origin: CGPoint(x: hideOffset.x + leftInset + leftMenuInset + 8.0 + 22.0, y: (accessoryPanel != nil ? 52.0 : 0.0) + panelHeight - minimalHeight + floor((minimalHeight - audioRecordingTimeSize.height) / 2.0) + 1.0 - UIScreenPixel), size: audioRecordingTimeSize)
let audioRecordingTimeFrame = CGRect(origin: CGPoint(x: hideOffset.x + leftInset + leftMenuInset + 8.0 + 34.0, y: (accessoryPanel != nil ? 52.0 : 0.0) + panelHeight - minimalHeight + floor((minimalHeight - audioRecordingTimeSize.height) / 2.0) + 1.0 - UIScreenPixel), size: audioRecordingTimeSize)
if animateTimeSlideIn {
var previousAudioRecordingTimeFrame = audioRecordingTimeFrame
@@ -17,9 +17,13 @@ swift_library(
"//submodules/TelegramCore:TelegramCore",
"//submodules/AccountContext:AccountContext",
"//submodules/ChatPresentationInterfaceState:ChatPresentationInterfaceState",
"//submodules/TelegramUI/Components/ButtonComponent",
"//submodules/ComponentFlow:ComponentFlow",
"//submodules/Components/ComponentDisplayAdapters:ComponentDisplayAdapters",
"//submodules/Components/MultilineTextComponent",
"//submodules/TelegramUI/Components/EdgeEffect",
"//submodules/TelegramUI/Components/EntityKeyboard:EntityKeyboard",
"//submodules/TelegramUI/Components/EmojiTextAttachmentView",
"//submodules/TelegramUI/Components/AnimationCache:AnimationCache",
"//submodules/TelegramUI/Components/MultiAnimationRenderer:MultiAnimationRenderer",
"//submodules/TextFormat:TextFormat",
@@ -39,6 +43,7 @@ swift_library(
"//submodules/TelegramUI/Components/ChatControllerInteraction:ChatControllerInteraction",
"//submodules/FeaturedStickersScreen:FeaturedStickersScreen",
"//submodules/TelegramUI/Components/EntityKeyboardGifContent:EntityKeyboardGifContent",
"//submodules/TelegramUI/Components/GlassControls",
"//submodules/TelegramUI/Components/LegacyMessageInputPanelInputView:LegacyMessageInputPanelInputView",
"//submodules/TelegramUI/Components/BatchVideoRendering",
"//submodules/TelegramUI/Components/GlassBackgroundComponent",
@@ -2,6 +2,7 @@ import Foundation
import UIKit
import AsyncDisplayKit
import Display
import ComponentFlow
import SearchBarNode
import SwiftSignalKit
import Postbox
@@ -10,12 +11,16 @@ import TelegramPresentationData
import AccountContext
import ChatPresentationInterfaceState
import EntityKeyboard
import ContextUI
import GlassControls
import MultilineTextComponent
import ChatControllerInteraction
import MultiplexedVideoNode
import FeaturedStickersScreen
import StickerPeekUI
import EntityKeyboardGifContent
import BatchVideoRendering
import UndoUI
private let searchBarHeight: CGFloat = 76.0
private let searchBarTopInset: CGFloat = 16.0
@@ -39,14 +44,14 @@ public protocol PaneSearchContentNode {
var ready: Signal<Void, NoError> { get }
var deactivateSearchBar: (() -> Void)? { get set }
var updateActivity: ((Bool) -> Void)? { get set }
func updateThemeAndStrings(theme: PresentationTheme, strings: PresentationStrings)
func updateText(_ text: String, languageCode: String?)
func updateLayout(size: CGSize, leftInset: CGFloat, rightInset: CGFloat, bottomInset: CGFloat, inputHeight: CGFloat, deviceMetrics: DeviceMetrics, transition: ContainedViewLayoutTransition)
func animateIn(additivePosition: CGFloat, transition: ContainedViewLayoutTransition)
func animateOut(transition: ContainedViewLayoutTransition)
func updatePreviewing(animated: Bool)
func itemAt(point: CGPoint) -> (ASDisplayNode, Any)?
}
@@ -58,27 +63,34 @@ public final class PaneSearchContainerNode: ASDisplayNode, EntitySearchContainer
private let interaction: ChatEntityKeyboardInputNode.Interaction
private let inputNodeInteraction: ChatMediaInputNodeInteraction
private let peekBehavior: EmojiContentPeekBehavior?
private let backgroundNode: ASDisplayNode
private let searchBar: SearchBarNode
private var validLayout: CGSize?
private let navigationButtons = ComponentView<Empty>()
private let selectedPackTitle = ComponentView<Empty>()
private var theme: PresentationTheme
private var strings: PresentationStrings
private var validLayout: (size: CGSize, leftInset: CGFloat, rightInset: CGFloat, bottomInset: CGFloat, inputHeight: CGFloat, deviceMetrics: DeviceMetrics)?
private weak var animatedPlaceholder: PaneSearchBarPlaceholderNode?
private var selectedStickerPack: StickerPaneSearchSelectedPack?
public var onCancel: (() -> Void)?
public var openGifContextMenu: ((MultiplexedVideoNodeFile, ASDisplayNode, CGRect, ContextGesture, Bool) -> Void)?
public var ready: Signal<Void, NoError> {
return self.contentNode.ready
}
public init(context: AccountContext, theme: PresentationTheme, strings: PresentationStrings, interaction: ChatEntityKeyboardInputNode.Interaction, inputNodeInteraction: ChatMediaInputNodeInteraction, mode: ChatMediaInputSearchMode, batchVideoRenderingContext: BatchVideoRenderingContext?, stickerActionTitle: String? = nil, trendingGifsPromise: Promise<ChatMediaInputGifPaneTrendingState?>, cancel: @escaping () -> Void, peekBehavior: EmojiContentPeekBehavior?) {
self.context = context
self.mode = mode
self.interaction = interaction
self.inputNodeInteraction = inputNodeInteraction
self.peekBehavior = peekBehavior
self.theme = theme
self.strings = strings
switch mode {
case .gif:
self.contentNode = GifPaneSearchContentNode(context: context, theme: theme, strings: strings, interaction: interaction, inputNodeInteraction: inputNodeInteraction, batchVideoRenderingContext: batchVideoRenderingContext ?? BatchVideoRenderingContext(context: context), trendingPromise: trendingGifsPromise)
@@ -86,7 +98,7 @@ public final class PaneSearchContainerNode: ASDisplayNode, EntitySearchContainer
self.contentNode = StickerPaneSearchContentNode(context: context, theme: theme, strings: strings, interaction: interaction, inputNodeInteraction: inputNodeInteraction, stickerActionTitle: stickerActionTitle)
}
self.backgroundNode = ASDisplayNode()
self.searchBar = SearchBarNode(
theme: paneSearchBarTheme(theme),
presentationTheme: theme,
@@ -94,35 +106,35 @@ public final class PaneSearchContainerNode: ASDisplayNode, EntitySearchContainer
fieldStyle: .glass,
displayBackground: false
)
super.init()
self.clipsToBounds = true
self.addSubnode(self.backgroundNode)
self.addSubnode(self.contentNode)
self.addSubnode(self.searchBar)
self.contentNode.deactivateSearchBar = { [weak self] in
self?.searchBar.deactivate(clear: false)
}
self.contentNode.updateActivity = { [weak self] active in
self?.searchBar.activity = active
}
self.searchBar.cancel = { [weak self] in
self?.searchBar.deactivate(clear: false)
cancel()
self?.onCancel?()
}
self.searchBar.activate()
self.searchBar.textUpdated = { [weak self] text, languageCode in
self?.contentNode.updateText(text, languageCode: languageCode)
}
self.updateThemeAndStrings(theme: theme, strings: strings)
if let contentNode = self.contentNode as? GifPaneSearchContentNode {
contentNode.requestUpdateQuery = { [weak self] query in
self?.updateQuery(query)
@@ -131,7 +143,20 @@ public final class PaneSearchContainerNode: ASDisplayNode, EntitySearchContainer
self?.openGifContextMenu?(file, node, rect, gesture, isSaved)
}
}
if let contentNode = self.contentNode as? StickerPaneSearchContentNode {
contentNode.selectedPackUpdated = { [weak self] pack in
guard let self else {
return
}
self.selectedStickerPack = pack
if pack != nil {
self.searchBar.deactivate(clear: false)
}
self.requestLayout(transition: .animated(duration: 0.2, curve: .easeInOut))
}
}
if let contentNode = self.contentNode as? StickerPaneSearchContentNode, let peekBehavior = self.peekBehavior {
peekBehavior.setGestureRecognizerEnabled(view: self.contentNode.view, isEnabled: true, itemAtPoint: { [weak contentNode] point in
guard let contentNode else {
@@ -140,7 +165,7 @@ public final class PaneSearchContainerNode: ASDisplayNode, EntitySearchContainer
guard let (itemNode, item) = contentNode.itemAt(point: point) else {
return nil
}
var maybeFile: TelegramMediaFile?
if let item = item as? StickerPreviewPeekItem {
switch item {
@@ -155,7 +180,7 @@ public final class PaneSearchContainerNode: ASDisplayNode, EntitySearchContainer
guard let file = maybeFile else {
return nil
}
var groupId: AnyHashable = AnyHashable("search")
for attribute in file.attributes {
if case let .Sticker(_, packReference, _) = attribute {
@@ -164,17 +189,19 @@ public final class PaneSearchContainerNode: ASDisplayNode, EntitySearchContainer
}
}
}
return (groupId, itemNode.layer, file)
})
}
}
public func updateThemeAndStrings(theme: PresentationTheme, strings: PresentationStrings) {
self.theme = theme
self.strings = strings
self.backgroundNode.backgroundColor = theme.chat.inputMediaPanel.stickersBackgroundColor.withAlphaComponent(1.0)
self.contentNode.updateThemeAndStrings(theme: theme, strings: strings)
self.searchBar.updateThemeAndStrings(theme: paneSearchBarTheme(theme), presentationTheme: theme, strings: strings)
let placeholder: String
switch mode {
case .gif:
@@ -184,61 +211,203 @@ public final class PaneSearchContainerNode: ASDisplayNode, EntitySearchContainer
}
self.searchBar.placeholderString = NSAttributedString(string: placeholder, font: Font.regular(17.0), textColor: theme.rootController.navigationSearchBar.inputPlaceholderTextColor)
}
public func updateQuery(_ query: String) {
self.searchBar.text = query
}
public func itemAt(point: CGPoint) -> (ASDisplayNode, Any)? {
return self.contentNode.itemAt(point: CGPoint(x: point.x, y: point.y - searchBarHeight))
}
private func openSelectedPackMoreMenu() {
guard let selectedStickerPack = self.selectedStickerPack, let controlsView = self.navigationButtons.view as? GlassControlPanelComponent.View, let rightItemView = controlsView.rightItemView, let sourceView = rightItemView.itemView(id: AnyHashable("more")) else {
return
}
let link = "https://t.me/addstickers/\(selectedStickerPack.info.shortName)"
let presentationData = self.context.sharedContext.currentPresentationData.with { $0 }.withUpdated(theme: self.theme)
let strings = self.strings
var items: [ContextMenuItem] = []
items.append(.action(ContextMenuActionItem(text: strings.StickerPack_Share, icon: { theme in
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Share"), color: theme.contextMenu.primaryColor)
}, action: { [weak self] _, f in
f(.default)
guard let self else {
return
}
let shareController = self.context.sharedContext.makeShareController(
context: self.context,
params: ShareControllerParams(
subject: .url(link),
externalShare: false,
actionCompleted: { [weak self] in
guard let self else {
return
}
let presentationData = self.context.sharedContext.currentPresentationData.with { $0 }.withUpdated(theme: self.theme)
self.interaction.presentController(UndoOverlayController(presentationData: presentationData, content: .linkCopied(title: nil, text: presentationData.strings.Conversation_LinkCopied), elevatedLayout: false, animateInAsReplacement: false, action: { _ in
return false
}), nil)
}
)
)
self.interaction.presentController(shareController, nil)
})))
items.append(.action(ContextMenuActionItem(text: strings.StickerPack_CopyLink, icon: { theme in
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Link"), color: theme.contextMenu.primaryColor)
}, action: { [weak self] _, f in
f(.default)
UIPasteboard.general.string = link
guard let self else {
return
}
let presentationData = self.context.sharedContext.currentPresentationData.with { $0 }.withUpdated(theme: self.theme)
self.interaction.presentController(UndoOverlayController(presentationData: presentationData, content: .linkCopied(title: nil, text: presentationData.strings.Conversation_LinkCopied), elevatedLayout: false, animateInAsReplacement: false, action: { _ in
return false
}), nil)
})))
let contextController = makeContextController(
presentationData: presentationData,
source: .reference(StickerPaneSearchHeaderContextReferenceContentSource(sourceView: sourceView)),
items: .single(ContextController.Items(content: .list(items))),
gesture: nil
)
self.interaction.presentGlobalOverlayController(contextController, nil)
}
private func requestLayout(transition: ContainedViewLayoutTransition) {
guard let (size, leftInset, rightInset, bottomInset, inputHeight, deviceMetrics) = self.validLayout else {
return
}
self.updateLayout(size: size, leftInset: leftInset, rightInset: rightInset, bottomInset: bottomInset, inputHeight: inputHeight, deviceMetrics: deviceMetrics, transition: transition)
}
public func updateLayout(size: CGSize, leftInset: CGFloat, rightInset: CGFloat, bottomInset: CGFloat, inputHeight: CGFloat, deviceMetrics: DeviceMetrics, transition: ContainedViewLayoutTransition) {
self.validLayout = size
self.validLayout = (size, leftInset, rightInset, bottomInset, inputHeight, deviceMetrics)
transition.updateFrame(node: self.backgroundNode, frame: CGRect(origin: CGPoint(), size: size))
let searchBarFrame = CGRect(origin: CGPoint(x: 0.0, y: searchBarTopInset), size: CGSize(width: size.width, height: searchBarFieldHeight))
transition.updateFrame(node: self.searchBar, frame: searchBarFrame)
self.searchBar.updateLayout(boundingSize: searchBarFrame.size, leftInset: leftInset, rightInset: rightInset, transition: transition)
self.searchBar.isUserInteractionEnabled = self.selectedStickerPack == nil
transition.updateAlpha(node: self.searchBar, alpha: self.selectedStickerPack == nil ? 1.0 : 0.0)
let componentTransition = ComponentTransition(transition)
let navigationButtonsFrame = CGRect(origin: CGPoint(x: leftInset + 16.0, y: searchBarTopInset), size: CGSize(width: max(1.0, size.width - leftInset - rightInset - 16.0 * 2.0), height: 48.0))
let navigationButtonsSize = self.navigationButtons.update(
transition: componentTransition,
component: AnyComponent(GlassControlPanelComponent(
theme: self.theme,
leftItem: self.selectedStickerPack == nil ? nil : GlassControlPanelComponent.Item(
items: [
GlassControlGroupComponent.Item(
id: AnyHashable("back"),
content: .icon("Navigation/Back"),
action: { [weak self] in
guard let self, let contentNode = self.contentNode as? StickerPaneSearchContentNode else {
return
}
contentNode.clearSelectedPack()
}
)
],
background: .panel
),
centralItem: nil,
rightItem: self.selectedStickerPack == nil ? nil : GlassControlPanelComponent.Item(
items: [
GlassControlGroupComponent.Item(
id: AnyHashable("more"),
content: .animation("anim_morewide"),
action: { [weak self] in
self?.openSelectedPackMoreMenu()
}
)
],
background: .panel
),
centerAlignmentIfPossible: true,
isDark: self.theme.overallDarkAppearance
)),
environment: {},
containerSize: navigationButtonsFrame.size
)
if let navigationButtons = self.navigationButtons.view {
if navigationButtons.superview == nil {
self.view.addSubview(navigationButtons)
}
navigationButtons.isUserInteractionEnabled = self.selectedStickerPack != nil
componentTransition.setFrame(view: navigationButtons, frame: CGRect(origin: navigationButtonsFrame.origin, size: navigationButtonsSize))
//componentTransition.setAlpha(view: navigationButtons, alpha: self.selectedStickerPack != nil ? 1.0 : 0.0)
}
let title = self.selectedStickerPack?.info.title ?? ""
let titleSize = self.selectedPackTitle.update(
transition: componentTransition,
component: AnyComponent(MultilineTextComponent(
text: .plain(NSAttributedString(string: title, font: Font.semibold(17.0), textColor: self.theme.chat.inputPanel.primaryTextColor)),
horizontalAlignment: .center
)),
environment: {},
containerSize: CGSize(width: max(1.0, size.width - leftInset - rightInset - 140.0), height: searchBarFieldHeight)
)
if let titleView = self.selectedPackTitle.view {
if titleView.superview == nil {
self.view.addSubview(titleView)
}
titleView.isUserInteractionEnabled = false
let titleOrigin = CGPoint(x: leftInset + floor((size.width - leftInset - rightInset - titleSize.width) / 2.0), y: searchBarTopInset + floor((searchBarFieldHeight - titleSize.height) / 2.0))
titleView.frame = CGRect(origin: titleOrigin, size: titleSize)
componentTransition.setAlpha(view: titleView, alpha: self.selectedStickerPack != nil ? 1.0 : 0.0)
}
let contentFrame = CGRect(origin: CGPoint(x: leftInset, y: searchBarHeight), size: CGSize(width: size.width - leftInset - rightInset, height: size.height - searchBarHeight))
transition.updateFrame(node: self.contentNode, frame: contentFrame)
self.contentNode.updateLayout(size: contentFrame.size, leftInset: leftInset, rightInset: rightInset, bottomInset: bottomInset, inputHeight: inputHeight, deviceMetrics: deviceMetrics, transition: transition)
}
public func deactivate() {
if let contentNode = self.contentNode as? StickerPaneSearchContentNode {
contentNode.clearSelectedPack()
}
self.searchBar.deactivate(clear: true)
}
public func animateIn(from placeholder: PaneSearchBarPlaceholderNode?, anchorTop: CGPoint, anhorTopView: UIView, transition: ContainedViewLayoutTransition, completion: @escaping () -> Void) {
var verticalOrigin: CGFloat = anhorTopView.convert(anchorTop, to: self.view).y
if let placeholder = placeholder {
self.animatedPlaceholder = placeholder
placeholder.isHidden = true
let placeholderFrame = placeholder.view.convert(placeholder.bounds, to: self.view)
verticalOrigin = placeholderFrame.minY - 4.0
self.contentNode.animateIn(additivePosition: verticalOrigin, transition: transition)
} else {
self.contentNode.animateIn(additivePosition: 0.0, transition: transition)
}
let searchBarFrame = self.searchBar.frame
let initialSearchBarFrame = CGRect(origin: CGPoint(x: searchBarFrame.minX, y: verticalOrigin), size: searchBarFrame.size)
switch transition {
case let .animated(duration, curve):
self.backgroundNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: duration / 2.0)
self.searchBar.alpha = 1.0
self.searchBar.layer.animateAlpha(from: 0.0, to: 1.0, duration: duration, timingFunction: curve.timingFunction, completion: { _ in
completion()
})
self.searchBar.layer.animateFrame(from: initialSearchBarFrame, to: searchBarFrame, duration: duration, timingFunction: curve.timingFunction)
if let size = self.validLayout {
let initialBackgroundFrame = CGRect(origin: CGPoint(x: 0.0, y: verticalOrigin), size: CGSize(width: size.width, height: max(0.0, size.height - verticalOrigin)))
if let layout = self.validLayout {
let initialBackgroundFrame = CGRect(origin: CGPoint(x: 0.0, y: verticalOrigin), size: CGSize(width: layout.size.width, height: max(0.0, layout.size.height - verticalOrigin)))
self.backgroundNode.layer.animateFrame(from: initialBackgroundFrame, to: self.backgroundNode.frame, duration: duration, timingFunction: curve.timingFunction)
}
case .immediate:
@@ -246,7 +415,7 @@ public final class PaneSearchContainerNode: ASDisplayNode, EntitySearchContainer
break
}
}
public func animateOut(to placeholder: PaneSearchBarPlaceholderNode, animateOutSearchBar: Bool, transition: ContainedViewLayoutTransition, completion: @escaping () -> Void) {
let finish: () -> Void = { [weak self] in
placeholder.isHidden = false
@@ -255,16 +424,16 @@ public final class PaneSearchContainerNode: ASDisplayNode, EntitySearchContainer
}
completion()
}
if case let .animated(duration, curve) = transition {
let placeholderFrame = placeholder.view.convert(placeholder.bounds, to: self.view)
let verticalOrigin = placeholderFrame.minY - 4.0
let targetSearchBarFrame = CGRect(origin: CGPoint(x: self.searchBar.frame.minX, y: verticalOrigin), size: self.searchBar.frame.size)
if let size = self.validLayout {
self.backgroundNode.layer.animateFrame(from: self.backgroundNode.frame, to: CGRect(origin: CGPoint(x: 0.0, y: verticalOrigin), size: CGSize(width: size.width, height: max(0.0, size.height - verticalOrigin))), duration: duration, timingFunction: curve.timingFunction, removeOnCompletion: false)
if let layout = self.validLayout {
self.backgroundNode.layer.animateFrame(from: self.backgroundNode.frame, to: CGRect(origin: CGPoint(x: 0.0, y: verticalOrigin), size: CGSize(width: layout.size.width, height: max(0.0, layout.size.height - verticalOrigin))), duration: duration, timingFunction: curve.timingFunction, removeOnCompletion: false)
}
self.searchBar.layer.animateFrame(from: self.searchBar.frame, to: targetSearchBarFrame, duration: duration, timingFunction: curve.timingFunction, removeOnCompletion: false)
if animateOutSearchBar {
self.searchBar.alpha = 0.0
@@ -282,12 +451,34 @@ public final class PaneSearchContainerNode: ASDisplayNode, EntitySearchContainer
}
finish()
}
transition.updateAlpha(node: self.backgroundNode, alpha: 0.0)
if animateOutSearchBar {
transition.updateAlpha(node: self.searchBar, alpha: 0.0)
}
let componentTransition = ComponentTransition(transition)
if let headerView = self.navigationButtons.view {
componentTransition.setAlpha(view: headerView, alpha: 0.0)
}
if let titleView = self.selectedPackTitle.view {
componentTransition.setAlpha(view: titleView, alpha: 0.0)
}
self.contentNode.animateOut(transition: transition)
self.deactivate()
}
}
private final class StickerPaneSearchHeaderContextReferenceContentSource: ContextReferenceContentSource {
private weak var sourceView: UIView?
init(sourceView: UIView) {
self.sourceView = sourceView
}
func transitionInfo() -> ContextControllerReferenceViewInfo? {
guard let sourceView = self.sourceView else {
return nil
}
return ContextControllerReferenceViewInfo(referenceView: sourceView, contentAreaInScreenSpace: UIScreen.main.bounds)
}
}
@@ -12,7 +12,7 @@ public final class EntityKeyboardTopContainerPanelEnvironment: Equatable {
let visibilityFractionUpdated: ActionSlot<(CGFloat, ComponentTransition)>
let isExpandedUpdated: (Bool, ComponentTransition) -> Void
init(
public init(
isContentInFocus: Bool,
height: CGFloat,
visibilityFractionUpdated: ActionSlot<(CGFloat, ComponentTransition)>,
@@ -1218,11 +1218,14 @@ public final class EntityKeyboardTopPanelComponent: Component {
let containerSideInset: CGFloat
let defaultActiveItemId: AnyHashable?
let forceActiveItemId: AnyHashable?
let displayHighlightInExpanded: Bool
let automaticallySelectsFirstItem: Bool
let itemSpacing: CGFloat
let activeContentItemIdUpdated: ActionSlot<(AnyHashable, AnyHashable?, ComponentTransition)>
let activeContentItemMapping: [AnyHashable: AnyHashable]
let reorderItems: ([Item]) -> Void
init(
public init(
id: AnyHashable,
theme: PresentationTheme,
customTintColor: UIColor?,
@@ -1230,6 +1233,9 @@ public final class EntityKeyboardTopPanelComponent: Component {
containerSideInset: CGFloat,
defaultActiveItemId: AnyHashable? = nil,
forceActiveItemId: AnyHashable? = nil,
displayHighlightInExpanded: Bool = false,
automaticallySelectsFirstItem: Bool = true,
itemSpacing: CGFloat = 8.0,
activeContentItemIdUpdated: ActionSlot<(AnyHashable, AnyHashable?, ComponentTransition)>,
activeContentItemMapping: [AnyHashable: AnyHashable] = [:],
reorderItems: @escaping ([Item]) -> Void
@@ -1241,6 +1247,9 @@ public final class EntityKeyboardTopPanelComponent: Component {
self.containerSideInset = containerSideInset
self.defaultActiveItemId = defaultActiveItemId
self.forceActiveItemId = forceActiveItemId
self.displayHighlightInExpanded = displayHighlightInExpanded
self.automaticallySelectsFirstItem = automaticallySelectsFirstItem
self.itemSpacing = itemSpacing
self.activeContentItemIdUpdated = activeContentItemIdUpdated
self.activeContentItemMapping = activeContentItemMapping
self.reorderItems = reorderItems
@@ -1268,6 +1277,15 @@ public final class EntityKeyboardTopPanelComponent: Component {
if lhs.forceActiveItemId != rhs.forceActiveItemId {
return false
}
if lhs.displayHighlightInExpanded != rhs.displayHighlightInExpanded {
return false
}
if lhs.automaticallySelectsFirstItem != rhs.automaticallySelectsFirstItem {
return false
}
if lhs.itemSpacing != rhs.itemSpacing {
return false
}
if lhs.activeContentItemIdUpdated !== rhs.activeContentItemIdUpdated {
return false
}
@@ -1393,7 +1411,7 @@ public final class EntityKeyboardTopPanelComponent: Component {
let isExpanded: Bool
let items: [Item]
init(isExpanded: Bool, containerSideInset: CGFloat, height: CGFloat, items: [ItemDescription]) {
init(isExpanded: Bool, containerSideInset: CGFloat, height: CGFloat, itemSpacing: CGFloat, items: [ItemDescription]) {
self.sideInset = containerSideInset + 7.0
self.isExpanded = isExpanded
@@ -1401,7 +1419,7 @@ public final class EntityKeyboardTopPanelComponent: Component {
self.staticItemSize = self.itemSize
self.staticExpandedItemSize = self.isExpanded ? self.staticItemSize : CGSize(width: 134.0, height: 28.0)
self.innerItemSize = self.isExpanded ? CGSize(width: 50.0, height: 62.0) : CGSize(width: 24.0, height: 24.0)
self.itemSpacing = 8.0
self.itemSpacing = itemSpacing
var contentSize = CGSize(width: sideInset, height: height)
var resultItems: [Item] = []
@@ -2002,6 +2020,9 @@ public final class EntityKeyboardTopPanelComponent: Component {
if let forceActiveItemId = component.forceActiveItemId {
self.activeContentItemId = forceActiveItemId
} else if component.defaultActiveItemId == nil && !component.automaticallySelectsFirstItem {
self.activeContentItemId = nil
self.activeSubcontentItemId = nil
} else if self.activeContentItemId == nil, let defaultActiveItemId = component.defaultActiveItemId {
self.activeContentItemId = defaultActiveItemId
}
@@ -2045,12 +2066,12 @@ public final class EntityKeyboardTopPanelComponent: Component {
}
self.items = items
if self.activeContentItemId == nil {
if self.activeContentItemId == nil && component.automaticallySelectsFirstItem {
self.activeContentItemId = items.first?.id
}
let previousItemLayout = self.itemLayout
let itemLayout = ItemLayout(isExpanded: isExpanded, containerSideInset: component.containerSideInset, height: availableSize.height, items: self.items.map { item -> ItemLayout.ItemDescription in
let itemLayout = ItemLayout(isExpanded: isExpanded, containerSideInset: component.containerSideInset, height: availableSize.height, itemSpacing: component.itemSpacing, items: self.items.map { item -> ItemLayout.ItemDescription in
let isStatic = item.id == AnyHashable("static")
return ItemLayout.ItemDescription(
isStatic: isStatic,
@@ -2063,7 +2084,14 @@ public final class EntityKeyboardTopPanelComponent: Component {
var updatedBounds: CGRect?
if wasExpanded != isExpanded, let previousItemLayout = previousItemLayout {
if !isExpanded {
let keepInitialScrollOffset = component.displayHighlightInExpanded && self.scrollView.bounds.origin.x <= 1.0
if keepInitialScrollOffset {
let maxContentOffsetX = max(0.0, itemLayout.contentSize.width - availableSize.width)
updatedBounds = CGRect(
origin: CGPoint(x: min(max(0.0, self.scrollView.bounds.origin.x), maxContentOffsetX), y: 0.0),
size: availableSize
)
} else if !isExpanded {
if let draggingEndOffset = self.draggingEndOffset {
if abs(self.scrollView.contentOffset.x - draggingEndOffset) > 16.0 {
self.draggingFocusItemIndex = nil
@@ -2073,105 +2101,107 @@ public final class EntityKeyboardTopPanelComponent: Component {
}
}
var visibleBounds = self.scrollView.bounds
visibleBounds.origin.x -= 280.0
visibleBounds.size.width += 560.0
let previousVisibleRange = previousItemLayout.visibleItemRange(for: visibleBounds)
if previousVisibleRange.minIndex <= previousVisibleRange.maxIndex {
var itemIndex = self.draggingFocusItemIndex ?? ((previousVisibleRange.minIndex + previousVisibleRange.maxIndex) / 2)
if !isExpanded {
if self.scrollView.bounds.maxX >= self.scrollView.contentSize.width {
itemIndex = component.items.count - 1
}
if self.scrollView.bounds.minX <= 0.0 {
itemIndex = 0
}
}
if !keepInitialScrollOffset {
var visibleBounds = self.scrollView.bounds
visibleBounds.origin.x -= 280.0
visibleBounds.size.width += 560.0
var previousItemFrame = previousItemLayout.containerFrame(at: itemIndex)
var updatedItemFrame = itemLayout.containerFrame(at: itemIndex)
let previousDistanceToItem = (previousItemFrame.minX - self.scrollView.bounds.minX)
let previousDistanceToItemRight = (previousItemFrame.maxX - self.scrollView.bounds.maxX)
var newBounds = CGRect(origin: CGPoint(x: updatedItemFrame.minX - previousDistanceToItem, y: 0.0), size: availableSize)
var useRightAnchor = false
if newBounds.minX > itemLayout.contentSize.width - self.scrollView.bounds.width {
newBounds.origin.x = itemLayout.contentSize.width - self.scrollView.bounds.width
itemIndex = component.items.count - 1
useRightAnchor = true
}
if itemIndex == component.items.count - 1 {
useRightAnchor = true
}
if newBounds.minX < 0.0 {
newBounds.origin.x = 0.0
itemIndex = 0
useRightAnchor = false
}
if useRightAnchor {
let _ = previousDistanceToItemRight
newBounds.origin.x = itemLayout.contentSize.width - self.scrollView.bounds.width
}
previousItemFrame = previousItemLayout.containerFrame(at: itemIndex)
updatedItemFrame = itemLayout.containerFrame(at: itemIndex)
self.draggingFocusItemIndex = itemIndex
updatedBounds = newBounds
var updatedVisibleBounds = newBounds
updatedVisibleBounds.origin.x -= 280.0
updatedVisibleBounds.size.width += 560.0
let updatedVisibleRange = itemLayout.visibleItemRange(for: updatedVisibleBounds)
if useRightAnchor {
let baseFrame = CGRect(origin: CGPoint(x: updatedItemFrame.maxX - previousItemFrame.width, y: previousItemFrame.minY), size: previousItemFrame.size)
for index in updatedVisibleRange.minIndex ... updatedVisibleRange.maxIndex {
let indexDifference = index - itemIndex
if let itemView = self.itemViews[self.items[index].id] {
let itemContainerMaxX = baseFrame.maxX + CGFloat(indexDifference) * (previousItemLayout.itemSize.width + previousItemLayout.itemSpacing)
let itemContainerFrame = CGRect(origin: CGPoint(x: itemContainerMaxX - baseFrame.width, y: baseFrame.minY), size: baseFrame.size)
let itemOuterFrame = previousItemLayout.contentFrame(index: index, containerFrame: itemContainerFrame)
let itemSize = itemView.bounds.size
itemView.frame = CGRect(origin: CGPoint(x: itemOuterFrame.minX + floor((itemOuterFrame.width - itemSize.width) / 2.0), y: itemOuterFrame.minY + floor((itemOuterFrame.height - itemSize.height) / 2.0)), size: itemSize)
if let activeContentItemId = self.activeContentItemId, activeContentItemId == self.items[index].id {
self.highlightedIconBackgroundView.frame = itemOuterFrame
self.highlightedIconTintBackgroundView.frame = itemOuterFrame
}
let previousVisibleRange = previousItemLayout.visibleItemRange(for: visibleBounds)
if previousVisibleRange.minIndex <= previousVisibleRange.maxIndex {
var itemIndex = self.draggingFocusItemIndex ?? ((previousVisibleRange.minIndex + previousVisibleRange.maxIndex) / 2)
if !isExpanded {
if self.scrollView.bounds.maxX >= self.scrollView.contentSize.width {
itemIndex = component.items.count - 1
}
if self.scrollView.bounds.minX <= 0.0 {
itemIndex = 0
}
}
} else {
let baseFrame = CGRect(origin: CGPoint(x: updatedItemFrame.minX, y: previousItemFrame.minY), size: previousItemFrame.size)
for index in updatedVisibleRange.minIndex ... updatedVisibleRange.maxIndex {
let indexDifference = index - itemIndex
if let itemView = self.itemViews[self.items[index].id] {
var itemContainerOriginX = baseFrame.minX
if indexDifference > 0 {
for i in 0 ..< indexDifference {
itemContainerOriginX += previousItemLayout.itemSpacing
itemContainerOriginX += previousItemLayout.containerFrame(at: itemIndex + i).width
}
} else if indexDifference < 0 {
for i in 0 ..< (-indexDifference) {
itemContainerOriginX -= previousItemLayout.itemSpacing
itemContainerOriginX -= previousItemLayout.containerFrame(at: itemIndex - i - 1).width
var previousItemFrame = previousItemLayout.containerFrame(at: itemIndex)
var updatedItemFrame = itemLayout.containerFrame(at: itemIndex)
let previousDistanceToItem = (previousItemFrame.minX - self.scrollView.bounds.minX)
let previousDistanceToItemRight = (previousItemFrame.maxX - self.scrollView.bounds.maxX)
var newBounds = CGRect(origin: CGPoint(x: updatedItemFrame.minX - previousDistanceToItem, y: 0.0), size: availableSize)
var useRightAnchor = false
if newBounds.minX > itemLayout.contentSize.width - self.scrollView.bounds.width {
newBounds.origin.x = itemLayout.contentSize.width - self.scrollView.bounds.width
itemIndex = component.items.count - 1
useRightAnchor = true
}
if itemIndex == component.items.count - 1 {
useRightAnchor = true
}
if newBounds.minX < 0.0 {
newBounds.origin.x = 0.0
itemIndex = 0
useRightAnchor = false
}
if useRightAnchor {
let _ = previousDistanceToItemRight
newBounds.origin.x = itemLayout.contentSize.width - self.scrollView.bounds.width
}
previousItemFrame = previousItemLayout.containerFrame(at: itemIndex)
updatedItemFrame = itemLayout.containerFrame(at: itemIndex)
self.draggingFocusItemIndex = itemIndex
updatedBounds = newBounds
var updatedVisibleBounds = newBounds
updatedVisibleBounds.origin.x -= 280.0
updatedVisibleBounds.size.width += 560.0
let updatedVisibleRange = itemLayout.visibleItemRange(for: updatedVisibleBounds)
if useRightAnchor {
let baseFrame = CGRect(origin: CGPoint(x: updatedItemFrame.maxX - previousItemFrame.width, y: previousItemFrame.minY), size: previousItemFrame.size)
for index in updatedVisibleRange.minIndex ... updatedVisibleRange.maxIndex {
let indexDifference = index - itemIndex
if let itemView = self.itemViews[self.items[index].id] {
let itemContainerMaxX = baseFrame.maxX + CGFloat(indexDifference) * (previousItemLayout.itemSize.width + previousItemLayout.itemSpacing)
let itemContainerFrame = CGRect(origin: CGPoint(x: itemContainerMaxX - baseFrame.width, y: baseFrame.minY), size: baseFrame.size)
let itemOuterFrame = previousItemLayout.contentFrame(index: index, containerFrame: itemContainerFrame)
let itemSize = itemView.bounds.size
itemView.frame = CGRect(origin: CGPoint(x: itemOuterFrame.minX + floor((itemOuterFrame.width - itemSize.width) / 2.0), y: itemOuterFrame.minY + floor((itemOuterFrame.height - itemSize.height) / 2.0)), size: itemSize)
if let activeContentItemId = self.activeContentItemId, activeContentItemId == self.items[index].id {
self.highlightedIconBackgroundView.frame = itemOuterFrame
self.highlightedIconTintBackgroundView.frame = itemOuterFrame
}
}
let previousContainerFrame = previousItemLayout.containerFrame(at: index)
let itemContainerFrame = CGRect(origin: CGPoint(x: itemContainerOriginX, y: previousContainerFrame.minY), size: previousContainerFrame.size)
let itemOuterFrame = previousItemLayout.contentFrame(index: index, containerFrame: itemContainerFrame)
let itemSize = itemView.bounds.size
itemView.frame = CGRect(origin: CGPoint(x: itemOuterFrame.minX + floor((itemOuterFrame.width - itemSize.width) / 2.0), y: itemOuterFrame.minY + floor((itemOuterFrame.height - itemSize.height) / 2.0)), size: itemSize)
if let activeContentItemId = self.activeContentItemId, activeContentItemId == self.items[index].id {
self.highlightedIconBackgroundView.frame = itemOuterFrame
}
} else {
let baseFrame = CGRect(origin: CGPoint(x: updatedItemFrame.minX, y: previousItemFrame.minY), size: previousItemFrame.size)
for index in updatedVisibleRange.minIndex ... updatedVisibleRange.maxIndex {
let indexDifference = index - itemIndex
if let itemView = self.itemViews[self.items[index].id] {
var itemContainerOriginX = baseFrame.minX
if indexDifference > 0 {
for i in 0 ..< indexDifference {
itemContainerOriginX += previousItemLayout.itemSpacing
itemContainerOriginX += previousItemLayout.containerFrame(at: itemIndex + i).width
}
} else if indexDifference < 0 {
for i in 0 ..< (-indexDifference) {
itemContainerOriginX -= previousItemLayout.itemSpacing
itemContainerOriginX -= previousItemLayout.containerFrame(at: itemIndex - i - 1).width
}
}
let previousContainerFrame = previousItemLayout.containerFrame(at: index)
let itemContainerFrame = CGRect(origin: CGPoint(x: itemContainerOriginX, y: previousContainerFrame.minY), size: previousContainerFrame.size)
let itemOuterFrame = previousItemLayout.contentFrame(index: index, containerFrame: itemContainerFrame)
let itemSize = itemView.bounds.size
itemView.frame = CGRect(origin: CGPoint(x: itemOuterFrame.minX + floor((itemOuterFrame.width - itemSize.width) / 2.0), y: itemOuterFrame.minY + floor((itemOuterFrame.height - itemSize.height) / 2.0)), size: itemSize)
if let activeContentItemId = self.activeContentItemId, activeContentItemId == self.items[index].id {
self.highlightedIconBackgroundView.frame = itemOuterFrame
}
}
}
}
@@ -2197,7 +2227,10 @@ public final class EntityKeyboardTopPanelComponent: Component {
if let activeContentItemId = self.activeContentItemId {
if let index = self.items.firstIndex(where: { $0.id == activeContentItemId }) {
let itemFrame = itemLayout.containerFrame(at: index)
var itemFrame = itemLayout.containerFrame(at: index)
if isExpanded && component.displayHighlightInExpanded {
itemFrame = CGRect(origin: CGPoint(x: itemFrame.midX - itemFrame.height / 2.0, y: itemFrame.minY), size: CGSize(width: itemFrame.height, height: itemFrame.height))
}
var highlightTransition = transition
if self.highlightedIconBackgroundView.isHidden {
@@ -2228,8 +2261,9 @@ public final class EntityKeyboardTopPanelComponent: Component {
self.highlightedIconBackgroundView.isHidden = true
self.highlightedIconTintBackgroundView.isHidden = true
}
transition.setAlpha(view: self.highlightedIconBackgroundView, alpha: isExpanded ? 0.0 : 1.0)
transition.setAlpha(view: self.highlightedIconTintBackgroundView, alpha: isExpanded ? 0.0 : 1.0)
let highlightAlpha: CGFloat = isExpanded && !component.displayHighlightInExpanded ? 0.0 : 1.0
transition.setAlpha(view: self.highlightedIconBackgroundView, alpha: highlightAlpha)
transition.setAlpha(view: self.highlightedIconTintBackgroundView, alpha: highlightAlpha)
panelEnvironment.visibilityFractionUpdated.connect { [weak self] (fraction, transition) in
guard let strongSelf = self else {
@@ -6773,6 +6773,19 @@ public final class PeerInfoScreenImpl: ViewController, PeerInfoScreen, KeyShortc
})
self.updateTabBarSearchState(ViewController.TabBarSearchState(isActive: false), transition: .immediate)
if let sourceMessageId {
let _ = (context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: peerId))
|> deliverOnMainQueue).start(next: { peer in
guard case let .user(user) = peer else {
return
}
if case .personal = user.accessHash {
} else {
let _ = context.engine.peers.fetchAndUpdateCachedPeerData(peerId: peerId, sourceMessageId: sourceMessageId).startStandalone()
}
})
}
}
required init(coder aDecoder: NSCoder) {
@@ -72,7 +72,13 @@ extension ChatControllerImpl {
}
do {
let _ = self.context.engine.messages.deleteMessagesInteractively(messageIds: Array(messageIds), type: .forEveryone).startStandalone()
if let reactionPeerId {
if let messageId = messageIds.first {
let _ = self.context.engine.messages.deleteReaction(messageId: messageId, authorId: reactionPeerId).startStandalone()
}
} else {
let _ = self.context.engine.messages.deleteMessagesInteractively(messageIds: Array(messageIds), type: .forEveryone).startStandalone()
}
for authorId in result.deleteAllFromPeers {
let _ = self.context.engine.messages.deleteAllMessagesWithAuthor(peerId: messagesPeerId, authorId: authorId, namespace: Namespaces.Message.Cloud).startStandalone()