mirror of
https://github.com/TelegramMessenger/Telegram-iOS.git
synced 2026-05-21 18:20:41 +00:00
Merge commit '6df5f109739b05467716dbeaef51b197e1efc106'
This commit is contained in:
@@ -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:
|
||||
|
||||
@@ -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()
|
||||
|
||||
+1
-1
@@ -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 {
|
||||
|
||||
+62
-11
@@ -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
|
||||
|
||||
+1
-1
@@ -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",
|
||||
|
||||
+238
-47
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
+733
-157
File diff suppressed because it is too large
Load Diff
+1
-1
@@ -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)>,
|
||||
|
||||
+137
-103
@@ -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()
|
||||
|
||||
Reference in New Issue
Block a user