mirror of
https://github.com/TelegramMessenger/Telegram-iOS.git
synced 2026-07-05 19:28:46 +02:00
Various improvements
This commit is contained in:
parent
1dd23f6641
commit
dbd40fe7d3
76 changed files with 5186 additions and 2625 deletions
|
|
@ -16202,5 +16202,9 @@ Error: %8$@";
|
|||
|
||||
"Settings.About.PrivacyHelpEmpty" = "A few words about you.";
|
||||
"Settings.About.PrivacyHelpEveryone" = "Everyone can see your bio. [Change >]()";
|
||||
"Settings.About.PrivacyHelpContacts" = "Only uour contacts can see your bio. [Change >]()";
|
||||
"Settings.About.PrivacyHelpContacts" = "Only your contacts can see your bio. [Change >]()";
|
||||
"Settings.About.PrivacyHelpNobody" = "Nobody can see your bio. [Change >]()";
|
||||
|
||||
"Settings.Birthday.PrivacyHelpEveryone" = "Everyone can see your birthday. [Change >]()";
|
||||
"Settings.Birthday.PrivacyHelpContacts" = "Only your contacts can see your birthday. [Change >]()";
|
||||
"Settings.Birthday.PrivacyHelpNobody" = "Nobody can see your birthday. [Change >]()";
|
||||
|
|
|
|||
|
|
@ -65,6 +65,8 @@ final class BrowserInstantPageContent: UIView, BrowserContent, UIScrollViewDeleg
|
|||
var currentWebEmbedHeights: [Int : CGFloat] = [:]
|
||||
var currentExpandedDetails: [Int : Bool]?
|
||||
var currentDetailsItems: [InstantPageDetailsItem] = []
|
||||
private var resolvedExternalMediaDimensions: [MediaId: PixelDimensions] = [:]
|
||||
private var pendingResolvedExternalMediaDimensions = Set<MediaId>()
|
||||
|
||||
var currentAccessibilityAreas: [AccessibilityAreaNode] = []
|
||||
|
||||
|
|
@ -87,6 +89,7 @@ final class BrowserInstantPageContent: UIView, BrowserContent, UIScrollViewDeleg
|
|||
private let loadWebpageDisposable = MetaDisposable()
|
||||
private let resolveUrlDisposable = MetaDisposable()
|
||||
private let updateLayoutDisposable = MetaDisposable()
|
||||
private let updateExternalMediaDimensionsDisposable = MetaDisposable()
|
||||
|
||||
private let loadProgress = ValuePromise<CGFloat>(1.0, ignoreRepeated: true)
|
||||
private let readingProgress = ValuePromise<CGFloat>(0.0, ignoreRepeated: true)
|
||||
|
|
@ -179,12 +182,14 @@ final class BrowserInstantPageContent: UIView, BrowserContent, UIScrollViewDeleg
|
|||
}
|
||||
self.scrollNode.view.addGestureRecognizer(recognizer)
|
||||
|
||||
self.webpageDisposable = (actualizedWebpage(account: context.account, webpage: webPage) |> deliverOnMainQueue).start(next: { [weak self] result in
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
self.updateWebPage(result, anchor: self.initialAnchor)
|
||||
})
|
||||
if case let .Loaded(content) = webPage.content, let scheme = URL(string: content.url)?.scheme?.lowercased(), scheme == "http" || scheme == "https" {
|
||||
self.webpageDisposable = (actualizedWebpage(account: context.account, webpage: webPage) |> deliverOnMainQueue).start(next: { [weak self] result in
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
self.updateWebPage(result, anchor: self.initialAnchor)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
deinit {
|
||||
|
|
@ -193,6 +198,7 @@ final class BrowserInstantPageContent: UIView, BrowserContent, UIScrollViewDeleg
|
|||
self.loadWebpageDisposable.dispose()
|
||||
self.resolveUrlDisposable.dispose()
|
||||
self.updateLayoutDisposable.dispose()
|
||||
self.updateExternalMediaDimensionsDisposable.dispose()
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
|
|
@ -304,6 +310,8 @@ final class BrowserInstantPageContent: UIView, BrowserContent, UIScrollViewDeleg
|
|||
} else {
|
||||
self.webPage = nil
|
||||
}
|
||||
self.resolvedExternalMediaDimensions.removeAll()
|
||||
self.pendingResolvedExternalMediaDimensions.removeAll()
|
||||
if let anchor = anchor {
|
||||
self.initialAnchor = anchor.removingPercentEncoding
|
||||
} else if let state = state {
|
||||
|
|
@ -478,17 +486,12 @@ final class BrowserInstantPageContent: UIView, BrowserContent, UIScrollViewDeleg
|
|||
}
|
||||
|
||||
private func updatePageLayout() {
|
||||
guard let (size, insets, _) = self.containerLayout, let (webPage, instantPage) = self.webPage else {
|
||||
guard let (size, insets, _) = self.containerLayout, let (webPage, instantPage) = self.resolvedWebPage() else {
|
||||
return
|
||||
}
|
||||
|
||||
let currentLayout = instantPageLayoutForWebPage(webPage, instantPage: instantPage, userLocation: self.sourceLocation.userLocation, boundingWidth: size.width, safeInset: insets.left, strings: self.presentationData.strings, theme: self.theme, dateTimeFormat: self.presentationData.dateTimeFormat, webEmbedHeights: self.currentWebEmbedHeights)
|
||||
|
||||
for (_, tileNode) in self.visibleTiles {
|
||||
tileNode.removeFromSupernode()
|
||||
}
|
||||
self.visibleTiles.removeAll()
|
||||
|
||||
let currentLayoutTiles = instantPageTilesFromLayout(currentLayout, boundingWidth: size.width)
|
||||
|
||||
var currentDetailsItems: [InstantPageDetailsItem] = []
|
||||
|
|
@ -655,6 +658,7 @@ final class BrowserInstantPageContent: UIView, BrowserContent, UIScrollViewDeleg
|
|||
topNode = newNode
|
||||
self.visibleItemsWithNodes[itemIndex] = newNode
|
||||
itemNode = newNode
|
||||
self.configureExternalMediaDimensionsUpdates(for: newNode)
|
||||
|
||||
if let itemNode = itemNode as? InstantPageDetailsNode {
|
||||
itemNode.requestLayoutUpdate = { [weak self] animated in
|
||||
|
|
@ -678,6 +682,10 @@ final class BrowserInstantPageContent: UIView, BrowserContent, UIScrollViewDeleg
|
|||
}
|
||||
}
|
||||
|
||||
if let itemNode = itemNode {
|
||||
self.configureExternalMediaDimensionsUpdates(for: itemNode)
|
||||
}
|
||||
|
||||
if let itemNode = itemNode as? InstantPageDetailsNode {
|
||||
itemNode.updateVisibleItems(visibleBounds: visibleBounds.offsetBy(dx: -itemNode.frame.minX, dy: -itemNode.frame.minY), animated: animated)
|
||||
}
|
||||
|
|
@ -708,8 +716,11 @@ final class BrowserInstantPageContent: UIView, BrowserContent, UIScrollViewDeleg
|
|||
topNode = tileNode
|
||||
self.visibleTiles[tileIndex] = tileNode
|
||||
} else {
|
||||
if visibleTiles[tileIndex]!.frame != tileFrame {
|
||||
transition.updateFrame(node: self.visibleTiles[tileIndex]!, frame: tileFrame)
|
||||
if let tileNode = self.visibleTiles[tileIndex] {
|
||||
tileNode.update(tile: tile, backgroundColor: theme.pageBackgroundColor)
|
||||
if tileNode.frame != tileFrame {
|
||||
transition.updateFrame(node: tileNode, frame: tileFrame)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -929,6 +940,385 @@ final class BrowserInstantPageContent: UIView, BrowserContent, UIScrollViewDeleg
|
|||
return nil
|
||||
}
|
||||
|
||||
private func configureExternalMediaDimensionsUpdates(for itemNode: InstantPageNode) {
|
||||
let update: (MediaId, PixelDimensions) -> Void = { [weak self] mediaId, dimensions in
|
||||
self?.updateExternalMediaDimensions(mediaId, dimensions)
|
||||
}
|
||||
if let itemNode = itemNode as? InstantPageExternalMediaDimensionsNode {
|
||||
itemNode.updateExternalMediaDimensions = update
|
||||
}
|
||||
if let itemNode = itemNode as? InstantPageDetailsNode {
|
||||
itemNode.contentNode.updateExternalMediaDimensions = update
|
||||
}
|
||||
}
|
||||
|
||||
private func updateExternalMediaDimensions(_ mediaId: MediaId, _ dimensions: PixelDimensions) {
|
||||
if self.resolvedExternalMediaDimensions[mediaId] == dimensions {
|
||||
return
|
||||
}
|
||||
self.resolvedExternalMediaDimensions[mediaId] = dimensions
|
||||
self.pendingResolvedExternalMediaDimensions.insert(mediaId)
|
||||
|
||||
let signal: Signal<Void, NoError> = (.complete() |> delay(0.08, queue: Queue.mainQueue()))
|
||||
self.updateExternalMediaDimensionsDisposable.set(signal.start(completed: { [weak self] in
|
||||
self?.relayoutForResolvedExternalMediaDimensions()
|
||||
}))
|
||||
}
|
||||
|
||||
private func relayoutForResolvedExternalMediaDimensions() {
|
||||
guard !self.pendingResolvedExternalMediaDimensions.isEmpty else {
|
||||
return
|
||||
}
|
||||
|
||||
let mediaIds = Array(self.pendingResolvedExternalMediaDimensions)
|
||||
self.pendingResolvedExternalMediaDimensions.removeAll()
|
||||
|
||||
let detailsStateMaps = self.captureExpandedDetailsStateMaps()
|
||||
let viewportTop = self.scrollNode.view.contentOffset.y + self.scrollNode.view.contentInset.top
|
||||
var oldFrames: [MediaId: CGRect] = [:]
|
||||
for mediaId in mediaIds {
|
||||
if let frame = self.effectiveFrameForMedia(mediaId, detailsStateMaps: detailsStateMaps) {
|
||||
oldFrames[mediaId] = frame
|
||||
}
|
||||
}
|
||||
|
||||
self.updatePageLayout()
|
||||
|
||||
var newFrames: [MediaId: CGRect] = [:]
|
||||
for mediaId in mediaIds {
|
||||
if let frame = self.effectiveFrameForMedia(mediaId, detailsStateMaps: detailsStateMaps) {
|
||||
newFrames[mediaId] = frame
|
||||
}
|
||||
}
|
||||
|
||||
if let compensatedViewportTop = self.compensatedViewportTop(oldFrames: oldFrames, newFrames: newFrames, viewportTop: viewportTop) {
|
||||
self.setViewportTop(compensatedViewportTop)
|
||||
}
|
||||
self.updateVisibleItems(visibleBounds: self.scrollNode.view.bounds)
|
||||
}
|
||||
|
||||
private func setViewportTop(_ viewportTop: CGFloat) {
|
||||
let scrollView = self.scrollNode.view
|
||||
let minOffsetY = -scrollView.contentInset.top
|
||||
let maxOffsetY = max(minOffsetY, scrollView.contentSize.height - scrollView.bounds.height + scrollView.contentInset.bottom)
|
||||
let contentOffsetY = min(max(viewportTop - scrollView.contentInset.top, minOffsetY), maxOffsetY)
|
||||
if contentOffsetY.isFinite {
|
||||
scrollView.contentOffset = CGPoint(x: scrollView.contentOffset.x, y: contentOffsetY)
|
||||
}
|
||||
}
|
||||
|
||||
private func compensatedViewportTop(oldFrames: [MediaId: CGRect], newFrames: [MediaId: CGRect], viewportTop: CGFloat) -> CGFloat? {
|
||||
var pairedFrames: [(old: CGRect, new: CGRect)] = []
|
||||
for (mediaId, oldFrame) in oldFrames {
|
||||
if let newFrame = newFrames[mediaId] {
|
||||
pairedFrames.append((oldFrame, newFrame))
|
||||
}
|
||||
}
|
||||
if pairedFrames.isEmpty {
|
||||
return nil
|
||||
}
|
||||
|
||||
if let intersecting = pairedFrames
|
||||
.filter({ $0.old.height > 0.0 && $0.new.height > 0.0 && viewportTop > $0.old.minY && viewportTop < $0.old.maxY })
|
||||
.max(by: { $0.old.minY < $1.old.minY }) {
|
||||
let ratio = min(max((viewportTop - intersecting.old.minY) / intersecting.old.height, 0.0), 1.0)
|
||||
return intersecting.new.minY + ratio * intersecting.new.height
|
||||
}
|
||||
|
||||
if let above = pairedFrames
|
||||
.filter({ viewportTop >= $0.old.maxY })
|
||||
.max(by: { $0.old.maxY < $1.old.maxY }) {
|
||||
return viewportTop + (above.new.maxY - above.old.maxY)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
private func captureExpandedDetailsStateMaps() -> [String: [Int: Bool]] {
|
||||
guard let currentLayout = self.currentLayout else {
|
||||
return [:]
|
||||
}
|
||||
var result: [String: [Int: Bool]] = [:]
|
||||
self.captureExpandedDetailsStateMaps(items: currentLayout.items, visibleItemsWithNodes: self.visibleItemsWithNodes, path: [], result: &result)
|
||||
return result
|
||||
}
|
||||
|
||||
private func captureExpandedDetailsStateMaps(items: [InstantPageItem], visibleItemsWithNodes: [Int: InstantPageNode], path: [Int], result: inout [String: [Int: Bool]]) {
|
||||
let detailsNodes = visibleItemsWithNodes.compactMap { $0.value as? InstantPageDetailsNode }
|
||||
|
||||
var detailsIndex = -1
|
||||
for item in items {
|
||||
guard let detailsItem = item as? InstantPageDetailsItem else {
|
||||
continue
|
||||
}
|
||||
detailsIndex += 1
|
||||
|
||||
guard let detailsNode = detailsNodes.first(where: { $0.item === detailsItem }) else {
|
||||
continue
|
||||
}
|
||||
let nextPath = path + [detailsIndex]
|
||||
result[self.detailsStateKey(nextPath)] = detailsNode.contentNode.currentExpandedDetails ?? [:]
|
||||
self.captureExpandedDetailsStateMaps(items: detailsItem.items, visibleItemsWithNodes: detailsNode.contentNode.visibleItemsWithNodes, path: nextPath, result: &result)
|
||||
}
|
||||
}
|
||||
|
||||
private func detailsStateKey(_ path: [Int]) -> String {
|
||||
if path.isEmpty {
|
||||
return ""
|
||||
}
|
||||
return path.map(String.init).joined(separator: ".")
|
||||
}
|
||||
|
||||
private func effectiveFrameForMedia(_ mediaId: MediaId, detailsStateMaps: [String: [Int: Bool]]) -> CGRect? {
|
||||
guard let currentLayout = self.currentLayout else {
|
||||
return nil
|
||||
}
|
||||
return self.effectiveFrameForMedia(mediaId, items: currentLayout.items, origin: .zero, expandedDetails: self.currentExpandedDetails, path: [], detailsStateMaps: detailsStateMaps)
|
||||
}
|
||||
|
||||
private func effectiveFrameForMedia(_ mediaId: MediaId, items: [InstantPageItem], origin: CGPoint, expandedDetails: [Int: Bool]?, path: [Int], detailsStateMaps: [String: [Int: Bool]]) -> CGRect? {
|
||||
var collapseOffset: CGFloat = 0.0
|
||||
var detailsIndex = -1
|
||||
|
||||
for item in items {
|
||||
if item is InstantPageDetailsItem {
|
||||
detailsIndex += 1
|
||||
}
|
||||
|
||||
var itemFrame = item.frame.offsetBy(dx: origin.x, dy: origin.y - collapseOffset)
|
||||
if let detailsItem = item as? InstantPageDetailsItem {
|
||||
let nextPath = path + [detailsIndex]
|
||||
let nestedExpandedDetails = detailsStateMaps[self.detailsStateKey(nextPath)]
|
||||
let expanded = expandedDetails?[detailsIndex] ?? detailsItem.initiallyExpanded
|
||||
let height = expanded ? detailsItem.titleHeight + self.effectiveContentHeight(items: detailsItem.items, baseHeight: detailsItem.frame.height - detailsItem.titleHeight, expandedDetails: nestedExpandedDetails, path: nextPath, detailsStateMaps: detailsStateMaps) : detailsItem.titleHeight
|
||||
collapseOffset += item.frame.height - height
|
||||
itemFrame.size.height = height
|
||||
|
||||
if expanded, let nestedFrame = self.effectiveFrameForMedia(mediaId, items: detailsItem.items, origin: CGPoint(x: itemFrame.minX, y: itemFrame.minY + detailsItem.titleHeight), expandedDetails: nestedExpandedDetails, path: nextPath, detailsStateMaps: detailsStateMaps) {
|
||||
return nestedFrame
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
if self.itemContainsMedia(item, mediaId: mediaId) {
|
||||
return itemFrame
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
private func effectiveContentHeight(items: [InstantPageItem], baseHeight: CGFloat, expandedDetails: [Int: Bool]?, path: [Int], detailsStateMaps: [String: [Int: Bool]]) -> CGFloat {
|
||||
var contentHeight = baseHeight
|
||||
var detailsIndex = -1
|
||||
|
||||
for item in items {
|
||||
guard let detailsItem = item as? InstantPageDetailsItem else {
|
||||
continue
|
||||
}
|
||||
detailsIndex += 1
|
||||
|
||||
let nextPath = path + [detailsIndex]
|
||||
let nestedExpandedDetails = detailsStateMaps[self.detailsStateKey(nextPath)]
|
||||
let expanded = expandedDetails?[detailsIndex] ?? detailsItem.initiallyExpanded
|
||||
let height = expanded ? detailsItem.titleHeight + self.effectiveContentHeight(items: detailsItem.items, baseHeight: detailsItem.frame.height - detailsItem.titleHeight, expandedDetails: nestedExpandedDetails, path: nextPath, detailsStateMaps: detailsStateMaps) : detailsItem.titleHeight
|
||||
contentHeight += -detailsItem.frame.height + height
|
||||
}
|
||||
|
||||
return contentHeight
|
||||
}
|
||||
|
||||
private func itemContainsMedia(_ item: InstantPageItem, mediaId: MediaId) -> Bool {
|
||||
for media in item.medias {
|
||||
if media.media.id == mediaId {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
private func resolvedWebPage() -> (webPage: TelegramMediaWebpage, instantPage: InstantPage?)? {
|
||||
guard let (webPage, instantPage) = self.webPage else {
|
||||
return nil
|
||||
}
|
||||
guard !self.resolvedExternalMediaDimensions.isEmpty, case let .Loaded(content) = webPage.content else {
|
||||
return (webPage, instantPage)
|
||||
}
|
||||
|
||||
var instantPageUpdated = false
|
||||
var effectiveInstantPage = instantPage
|
||||
if let instantPage {
|
||||
var media = instantPage.media
|
||||
for (mediaId, currentMedia) in instantPage.media {
|
||||
if let updatedMedia = self.updatedMediaIfNeeded(currentMedia) {
|
||||
media[mediaId] = updatedMedia
|
||||
instantPageUpdated = true
|
||||
}
|
||||
}
|
||||
if instantPageUpdated {
|
||||
effectiveInstantPage = InstantPage(blocks: instantPage.blocks, media: media, isComplete: instantPage.isComplete, rtl: instantPage.rtl, url: instantPage.url, views: instantPage.views)
|
||||
}
|
||||
}
|
||||
|
||||
var imageUpdated = false
|
||||
let effectiveImage = content.image.map { image -> TelegramMediaImage in
|
||||
if let updated = self.updatedImageIfNeeded(image) {
|
||||
imageUpdated = true
|
||||
return updated
|
||||
} else {
|
||||
return image
|
||||
}
|
||||
}
|
||||
|
||||
var fileUpdated = false
|
||||
let effectiveFile = content.file.map { file -> TelegramMediaFile in
|
||||
if let updated = self.updatedFileIfNeeded(file) {
|
||||
fileUpdated = true
|
||||
return updated
|
||||
} else {
|
||||
return file
|
||||
}
|
||||
}
|
||||
|
||||
if !instantPageUpdated && !imageUpdated && !fileUpdated {
|
||||
return (webPage, instantPage)
|
||||
}
|
||||
|
||||
let effectiveContent = TelegramMediaWebpageLoadedContent(
|
||||
url: content.url,
|
||||
displayUrl: content.displayUrl,
|
||||
hash: content.hash,
|
||||
type: content.type,
|
||||
websiteName: content.websiteName,
|
||||
title: content.title,
|
||||
text: content.text,
|
||||
embedUrl: content.embedUrl,
|
||||
embedType: content.embedType,
|
||||
embedSize: content.embedSize,
|
||||
duration: content.duration,
|
||||
author: content.author,
|
||||
isMediaLargeByDefault: content.isMediaLargeByDefault,
|
||||
imageIsVideoCover: content.imageIsVideoCover,
|
||||
image: effectiveImage,
|
||||
file: effectiveFile,
|
||||
story: content.story,
|
||||
attributes: content.attributes,
|
||||
instantPage: effectiveInstantPage
|
||||
)
|
||||
return (TelegramMediaWebpage(webpageId: webPage.webpageId, content: .Loaded(effectiveContent)), effectiveInstantPage)
|
||||
}
|
||||
|
||||
private func updatedMediaIfNeeded(_ media: Media) -> Media? {
|
||||
if let image = media as? TelegramMediaImage {
|
||||
return self.updatedImageIfNeeded(image)
|
||||
} else if let file = media as? TelegramMediaFile {
|
||||
return self.updatedFileIfNeeded(file)
|
||||
} else {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
private func updatedImageIfNeeded(_ image: TelegramMediaImage) -> TelegramMediaImage? {
|
||||
guard let dimensions = self.resolvedExternalMediaDimensions[image.imageId] else {
|
||||
return nil
|
||||
}
|
||||
|
||||
var updatedRepresentations = image.representations
|
||||
var didUpdate = false
|
||||
for i in 0 ..< updatedRepresentations.count {
|
||||
let representation = updatedRepresentations[i]
|
||||
guard representation.resource is InstantPageExternalMediaResource, representation.dimensions != dimensions else {
|
||||
continue
|
||||
}
|
||||
updatedRepresentations[i] = TelegramMediaImageRepresentation(
|
||||
dimensions: dimensions,
|
||||
resource: representation.resource,
|
||||
progressiveSizes: representation.progressiveSizes,
|
||||
immediateThumbnailData: representation.immediateThumbnailData,
|
||||
hasVideo: representation.hasVideo,
|
||||
isPersonal: representation.isPersonal,
|
||||
typeHint: representation.typeHint
|
||||
)
|
||||
didUpdate = true
|
||||
}
|
||||
|
||||
guard didUpdate else {
|
||||
return nil
|
||||
}
|
||||
return TelegramMediaImage(
|
||||
imageId: image.imageId,
|
||||
representations: updatedRepresentations,
|
||||
videoRepresentations: image.videoRepresentations,
|
||||
immediateThumbnailData: image.immediateThumbnailData,
|
||||
emojiMarkup: image.emojiMarkup,
|
||||
reference: image.reference,
|
||||
partialReference: image.partialReference,
|
||||
flags: image.flags,
|
||||
video: image.video
|
||||
)
|
||||
}
|
||||
|
||||
private func updatedFileIfNeeded(_ file: TelegramMediaFile) -> TelegramMediaFile? {
|
||||
guard let dimensions = self.resolvedExternalMediaDimensions[file.fileId], file.resource is InstantPageExternalMediaResource else {
|
||||
return nil
|
||||
}
|
||||
|
||||
let (attributes, didUpdate) = self.fileAttributesWithResolvedDimensions(file.attributes, dimensions: dimensions)
|
||||
guard didUpdate else {
|
||||
return nil
|
||||
}
|
||||
|
||||
return TelegramMediaFile(
|
||||
fileId: file.fileId,
|
||||
partialReference: file.partialReference,
|
||||
resource: file.resource,
|
||||
previewRepresentations: file.previewRepresentations,
|
||||
videoThumbnails: file.videoThumbnails,
|
||||
videoCover: file.videoCover,
|
||||
immediateThumbnailData: file.immediateThumbnailData,
|
||||
mimeType: file.mimeType,
|
||||
size: file.size,
|
||||
attributes: attributes,
|
||||
alternativeRepresentations: file.alternativeRepresentations
|
||||
)
|
||||
}
|
||||
|
||||
private func fileAttributesWithResolvedDimensions(_ attributes: [TelegramMediaFileAttribute], dimensions: PixelDimensions) -> ([TelegramMediaFileAttribute], Bool) {
|
||||
var updatedAttributes: [TelegramMediaFileAttribute] = []
|
||||
var didUpdate = false
|
||||
var hasSizeAttribute = false
|
||||
|
||||
for attribute in attributes {
|
||||
switch attribute {
|
||||
case let .ImageSize(size):
|
||||
hasSizeAttribute = true
|
||||
if size != dimensions {
|
||||
updatedAttributes.append(.ImageSize(size: dimensions))
|
||||
didUpdate = true
|
||||
} else {
|
||||
updatedAttributes.append(attribute)
|
||||
}
|
||||
case let .Video(duration, size, flags, preloadSize, coverTime, videoCodec):
|
||||
hasSizeAttribute = true
|
||||
if size != dimensions {
|
||||
updatedAttributes.append(.Video(duration: duration, size: dimensions, flags: flags, preloadSize: preloadSize, coverTime: coverTime, videoCodec: videoCodec))
|
||||
didUpdate = true
|
||||
} else {
|
||||
updatedAttributes.append(attribute)
|
||||
}
|
||||
default:
|
||||
updatedAttributes.append(attribute)
|
||||
}
|
||||
}
|
||||
|
||||
if !hasSizeAttribute {
|
||||
updatedAttributes.append(.ImageSize(size: dimensions))
|
||||
didUpdate = true
|
||||
}
|
||||
|
||||
return (updatedAttributes, didUpdate)
|
||||
}
|
||||
|
||||
private func openUrl(_ url: InstantPageUrlItem) {
|
||||
var baseUrl = url.url
|
||||
var anchor: String?
|
||||
|
|
|
|||
1143
submodules/BrowserUI/Sources/BrowserMarkdown.swift
Normal file
1143
submodules/BrowserUI/Sources/BrowserMarkdown.swift
Normal file
File diff suppressed because it is too large
Load diff
|
|
@ -364,7 +364,7 @@ final class BrowserPdfContent: UIView, BrowserContent, UIScrollViewDelegate, PDF
|
|||
let pageIndicatorSize = self.pageIndicator.update(
|
||||
transition: .immediate,
|
||||
component: AnyComponent(
|
||||
Text(text: "\(self.pageNumber?.0 ?? 1) of \(self.pageNumber?.1 ?? 1)", font: Font.with(size: 15.0, weight: .regular, traits: .monospacedNumbers), color: self.presentationData.theme.list.itemPrimaryTextColor)
|
||||
Text(text: self.presentationData.strings.Items_NOfM("\(self.pageNumber?.0 ?? 1)", "\(self.pageNumber?.1 ?? 1)").string, font: Font.with(size: 15.0, weight: .regular, traits: .monospacedNumbers), color: self.presentationData.theme.list.itemPrimaryTextColor)
|
||||
),
|
||||
environment: {},
|
||||
containerSize: size
|
||||
|
|
|
|||
|
|
@ -29,6 +29,7 @@ private final class BrowserScreenComponent: CombinedComponent {
|
|||
let context: AccountContext
|
||||
let contentState: BrowserContentState?
|
||||
let presentationState: BrowserPresentationState
|
||||
let toolbarMode: BrowserToolbarMode
|
||||
let canShare: Bool
|
||||
let performAction: ActionSlot<BrowserScreen.Action>
|
||||
let performHoldAction: (UIView, ContextGesture?, BrowserScreen.Action) -> Void
|
||||
|
|
@ -38,6 +39,7 @@ private final class BrowserScreenComponent: CombinedComponent {
|
|||
context: AccountContext,
|
||||
contentState: BrowserContentState?,
|
||||
presentationState: BrowserPresentationState,
|
||||
toolbarMode: BrowserToolbarMode,
|
||||
canShare: Bool,
|
||||
performAction: ActionSlot<BrowserScreen.Action>,
|
||||
performHoldAction: @escaping (UIView, ContextGesture?, BrowserScreen.Action) -> Void,
|
||||
|
|
@ -46,6 +48,7 @@ private final class BrowserScreenComponent: CombinedComponent {
|
|||
self.context = context
|
||||
self.contentState = contentState
|
||||
self.presentationState = presentationState
|
||||
self.toolbarMode = toolbarMode
|
||||
self.canShare = canShare
|
||||
self.performAction = performAction
|
||||
self.performHoldAction = performHoldAction
|
||||
|
|
@ -62,6 +65,9 @@ private final class BrowserScreenComponent: CombinedComponent {
|
|||
if lhs.presentationState != rhs.presentationState {
|
||||
return false
|
||||
}
|
||||
if lhs.toolbarMode != rhs.toolbarMode {
|
||||
return false
|
||||
}
|
||||
if lhs.canShare != rhs.canShare {
|
||||
return false
|
||||
}
|
||||
|
|
@ -112,8 +118,7 @@ private final class BrowserScreenComponent: CombinedComponent {
|
|||
navigationLeftItems = []
|
||||
navigationRightItems = []
|
||||
} else {
|
||||
let contentType = context.component.contentState?.contentType ?? .instantPage
|
||||
switch contentType {
|
||||
switch context.component.toolbarMode {
|
||||
case .webPage:
|
||||
navigationContent = AnyComponentWithIdentity(
|
||||
id: "addressBar",
|
||||
|
|
@ -131,7 +136,7 @@ private final class BrowserScreenComponent: CombinedComponent {
|
|||
)
|
||||
)
|
||||
)
|
||||
case .instantPage, .document:
|
||||
case .instantPage, .document, .markdown:
|
||||
let title = context.component.contentState?.title ?? ""
|
||||
navigationContent = AnyComponentWithIdentity(
|
||||
id: "titleBar_\(title)",
|
||||
|
|
@ -190,47 +195,46 @@ private final class BrowserScreenComponent: CombinedComponent {
|
|||
// )
|
||||
// )
|
||||
// #endif
|
||||
|
||||
let canGoBack = context.component.contentState?.canGoBack ?? false
|
||||
let canGoForward = context.component.contentState?.canGoForward ?? false
|
||||
|
||||
navigationLeftItems.append(
|
||||
AnyComponentWithIdentity(
|
||||
id: "back",
|
||||
component: AnyComponent(
|
||||
Button(
|
||||
content: AnyComponent(
|
||||
BundleIconComponent(
|
||||
name: "Instant View/Back",
|
||||
tintColor: environment.theme.chat.inputPanel.panelControlColor.withAlphaComponent(canGoBack ? 1.0 : 0.4)
|
||||
)
|
||||
),
|
||||
action: {
|
||||
performAction.invoke(.navigateBack)
|
||||
}
|
||||
).minSize(CGSize(width: 44.0, height: 44.0))
|
||||
if context.component.toolbarMode != .markdown {
|
||||
let canGoBack = context.component.contentState?.canGoBack ?? false
|
||||
let canGoForward = context.component.contentState?.canGoForward ?? false
|
||||
navigationLeftItems.append(
|
||||
AnyComponentWithIdentity(
|
||||
id: "back",
|
||||
component: AnyComponent(
|
||||
Button(
|
||||
content: AnyComponent(
|
||||
BundleIconComponent(
|
||||
name: "Instant View/Back",
|
||||
tintColor: environment.theme.chat.inputPanel.panelControlColor.withAlphaComponent(canGoBack ? 1.0 : 0.4)
|
||||
)
|
||||
),
|
||||
action: {
|
||||
performAction.invoke(.navigateBack)
|
||||
}
|
||||
).minSize(CGSize(width: 44.0, height: 44.0))
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
navigationLeftItems.append(
|
||||
AnyComponentWithIdentity(
|
||||
id: "forward",
|
||||
component: AnyComponent(
|
||||
Button(
|
||||
content: AnyComponent(
|
||||
BundleIconComponent(
|
||||
name: "Instant View/Forward",
|
||||
tintColor: environment.theme.chat.inputPanel.panelControlColor.withAlphaComponent(canGoForward ? 1.0 : 0.4)
|
||||
)
|
||||
),
|
||||
action: {
|
||||
performAction.invoke(.navigateForward)
|
||||
}
|
||||
).minSize(CGSize(width: 44.0, height: 44.0))
|
||||
navigationLeftItems.append(
|
||||
AnyComponentWithIdentity(
|
||||
id: "forward",
|
||||
component: AnyComponent(
|
||||
Button(
|
||||
content: AnyComponent(
|
||||
BundleIconComponent(
|
||||
name: "Instant View/Forward",
|
||||
tintColor: environment.theme.chat.inputPanel.panelControlColor.withAlphaComponent(canGoForward ? 1.0 : 0.4)
|
||||
)
|
||||
),
|
||||
action: {
|
||||
performAction.invoke(.navigateForward)
|
||||
}
|
||||
).minSize(CGSize(width: 44.0, height: 44.0))
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
navigationRightItems = [
|
||||
|
|
@ -258,25 +262,27 @@ private final class BrowserScreenComponent: CombinedComponent {
|
|||
]
|
||||
|
||||
if isTablet {
|
||||
navigationRightItems.insert(
|
||||
AnyComponentWithIdentity(
|
||||
id: "bookmarks",
|
||||
component: AnyComponent(
|
||||
Button(
|
||||
content: AnyComponent(
|
||||
BundleIconComponent(
|
||||
name: "Instant View/Bookmark",
|
||||
tintColor: environment.theme.chat.inputPanel.panelControlColor
|
||||
)
|
||||
),
|
||||
action: {
|
||||
performAction.invoke(.openBookmarks)
|
||||
}
|
||||
).minSize(CGSize(width: 44.0, height: 44.0))
|
||||
)
|
||||
),
|
||||
at: 0
|
||||
)
|
||||
if context.component.toolbarMode != .markdown {
|
||||
navigationRightItems.insert(
|
||||
AnyComponentWithIdentity(
|
||||
id: "bookmarks",
|
||||
component: AnyComponent(
|
||||
Button(
|
||||
content: AnyComponent(
|
||||
BundleIconComponent(
|
||||
name: "Instant View/Bookmark",
|
||||
tintColor: environment.theme.chat.inputPanel.panelControlColor
|
||||
)
|
||||
),
|
||||
action: {
|
||||
performAction.invoke(.openBookmarks)
|
||||
}
|
||||
).minSize(CGSize(width: 44.0, height: 44.0))
|
||||
)
|
||||
),
|
||||
at: 0
|
||||
)
|
||||
}
|
||||
if context.component.canShare {
|
||||
navigationRightItems.insert(
|
||||
AnyComponentWithIdentity(
|
||||
|
|
@ -369,7 +375,7 @@ private final class BrowserScreenComponent: CombinedComponent {
|
|||
canGoForward: context.component.contentState?.canGoForward ?? false,
|
||||
canOpenIn: canOpenIn,
|
||||
canShare: context.component.canShare,
|
||||
isDocument: context.component.contentState?.contentType == .document,
|
||||
mode: context.component.toolbarMode,
|
||||
performAction: performAction,
|
||||
performHoldAction: performHoldAction
|
||||
)
|
||||
|
|
@ -517,7 +523,45 @@ public class BrowserScreen: ViewController, MinimizableController {
|
|||
private var presentationData: PresentationData
|
||||
private var presentationDataDisposable: Disposable?
|
||||
private var validLayout: (ContainerViewLayout, CGFloat)?
|
||||
|
||||
|
||||
private var isMarkdownDocument: Bool {
|
||||
guard let controller = self.controller else {
|
||||
return false
|
||||
}
|
||||
if case .markdownDocument = controller.subject {
|
||||
return true
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
}
|
||||
private var isMarkdownTopLevelContent: Bool {
|
||||
return self.isMarkdownDocument && self.content.count <= 1
|
||||
}
|
||||
|
||||
private var isMarkdownInstantPageContent: Bool {
|
||||
return self.isMarkdownTopLevelContent && self.content.last is BrowserInstantPageContent
|
||||
}
|
||||
|
||||
private var toolbarMode: BrowserToolbarMode {
|
||||
if self.isMarkdownInstantPageContent {
|
||||
return .markdown
|
||||
}
|
||||
switch self.contentState?.contentType {
|
||||
case .document:
|
||||
return .document
|
||||
case .webPage:
|
||||
return .webPage
|
||||
case .instantPage:
|
||||
return .instantPage
|
||||
case .none:
|
||||
if self.content.last is BrowserDocumentContent || self.content.last is BrowserPdfContent {
|
||||
return .document
|
||||
} else {
|
||||
return .instantPage
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
init(controller: BrowserScreen) {
|
||||
self.context = controller.context
|
||||
self.controller = controller
|
||||
|
|
@ -535,7 +579,7 @@ public class BrowserScreen: ViewController, MinimizableController {
|
|||
super.init()
|
||||
|
||||
self.pushContent(controller.subject, transition: .immediate)
|
||||
if let content = self.content.last {
|
||||
if let content = self.content.last, !self.isMarkdownDocument {
|
||||
content.addToRecentlyVisited()
|
||||
}
|
||||
|
||||
|
|
@ -562,7 +606,44 @@ public class BrowserScreen: ViewController, MinimizableController {
|
|||
let presentationData = self.presentationData
|
||||
let subject: ShareControllerSubject
|
||||
var isDocument = false
|
||||
if let content = self.content.last {
|
||||
if let controller = self.controller {
|
||||
switch controller.subject {
|
||||
case let .document(file, _), let .pdfDocument(file, _):
|
||||
subject = .media(file.abstract, nil)
|
||||
isDocument = true
|
||||
case let .markdownDocument(file, _):
|
||||
if self.isMarkdownTopLevelContent {
|
||||
subject = .media(file.abstract, nil)
|
||||
isDocument = true
|
||||
} else if let content = self.content.last {
|
||||
if let documentContent = content as? BrowserDocumentContent {
|
||||
subject = .media(documentContent.file.abstract, nil)
|
||||
isDocument = true
|
||||
} else if let documentContent = content as? BrowserPdfContent {
|
||||
subject = .media(documentContent.file.abstract, nil)
|
||||
isDocument = true
|
||||
} else {
|
||||
subject = .url(url)
|
||||
}
|
||||
} else {
|
||||
subject = .url(url)
|
||||
}
|
||||
default:
|
||||
if let content = self.content.last {
|
||||
if let documentContent = content as? BrowserDocumentContent {
|
||||
subject = .media(documentContent.file.abstract, nil)
|
||||
isDocument = true
|
||||
} else if let documentContent = content as? BrowserPdfContent {
|
||||
subject = .media(documentContent.file.abstract, nil)
|
||||
isDocument = true
|
||||
} else {
|
||||
subject = .url(url)
|
||||
}
|
||||
} else {
|
||||
subject = .url(url)
|
||||
}
|
||||
}
|
||||
} else if let content = self.content.last {
|
||||
if let documentContent = content as? BrowserDocumentContent {
|
||||
subject = .media(documentContent.file.abstract, nil)
|
||||
isDocument = true
|
||||
|
|
@ -641,7 +722,10 @@ public class BrowserScreen: ViewController, MinimizableController {
|
|||
var processed = false
|
||||
if let controller = self.controller {
|
||||
switch controller.subject {
|
||||
case let .document(file, canShare), let .pdfDocument(file, canShare):
|
||||
case let .document(file, canShare), let .pdfDocument(file, canShare), let .markdownDocument(file, canShare):
|
||||
if case .markdownDocument = controller.subject, !self.isMarkdownTopLevelContent {
|
||||
break
|
||||
}
|
||||
processed = true
|
||||
controller.openDocument(file.media, canShare)
|
||||
default:
|
||||
|
|
@ -855,6 +939,20 @@ public class BrowserScreen: ViewController, MinimizableController {
|
|||
browserContent = BrowserDocumentContent(context: self.context, presentationData: self.presentationData, file: file)
|
||||
case let .pdfDocument(file, _):
|
||||
browserContent = BrowserPdfContent(context: self.context, presentationData: self.presentationData, file: file)
|
||||
case let .markdownDocument(file, _):
|
||||
if let (webPage, fileURL) = markdownWebpage(context: self.context, file: file) {
|
||||
browserContent = BrowserInstantPageContent(
|
||||
context: self.context,
|
||||
presentationData: self.presentationData,
|
||||
webPage: webPage,
|
||||
anchor: nil,
|
||||
url: fileURL.absoluteString,
|
||||
sourceLocation: InstantPageSourceLocation(userLocation: .other, peerType: .otherPrivate),
|
||||
preloadedResouces: nil
|
||||
)
|
||||
} else {
|
||||
browserContent = BrowserDocumentContent(context: self.context, presentationData: self.presentationData, file: file)
|
||||
}
|
||||
}
|
||||
browserContent.pushContent = { [weak self] content, additionalContent in
|
||||
guard let self else {
|
||||
|
|
@ -925,6 +1023,10 @@ public class BrowserScreen: ViewController, MinimizableController {
|
|||
}
|
||||
|
||||
func popContent(transition: ComponentTransition) {
|
||||
guard self.content.count > 1 else {
|
||||
return
|
||||
}
|
||||
|
||||
self.content.removeLast()
|
||||
self.requestLayout(transition: transition)
|
||||
|
||||
|
|
@ -1148,10 +1250,11 @@ public class BrowserScreen: ViewController, MinimizableController {
|
|||
}
|
||||
|
||||
let canOpenIn = !(self.contentState?.url.hasPrefix("tonsite") ?? false)
|
||||
let toolbarMode = self.toolbarMode
|
||||
var canShare = true
|
||||
if let controller = self.controller {
|
||||
switch controller.subject {
|
||||
case let .document(_, canShareValue), let .pdfDocument(_, canShareValue):
|
||||
case let .document(_, canShareValue), let .pdfDocument(_, canShareValue), let .markdownDocument(_, canShareValue):
|
||||
canShare = canShareValue
|
||||
default:
|
||||
break
|
||||
|
|
@ -1202,7 +1305,7 @@ public class BrowserScreen: ViewController, MinimizableController {
|
|||
})))
|
||||
}
|
||||
|
||||
if [.webPage, .instantPage].contains(contentState.contentType) {
|
||||
if toolbarMode != .markdown && [.webPage, .instantPage].contains(contentState.contentType) {
|
||||
items.append(.action(ContextMenuActionItem(text: self.presentationData.strings.WebBrowser_AddBookmark, icon: { theme in return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Fave"), color: theme.contextMenu.primaryColor) }, action: { (controller, action) in
|
||||
performAction.invoke(.addBookmark)
|
||||
action(.default)
|
||||
|
|
@ -1384,7 +1487,7 @@ public class BrowserScreen: ViewController, MinimizableController {
|
|||
var canShare = true
|
||||
if let controller = self.controller {
|
||||
switch controller.subject {
|
||||
case let .document(_, canShareValue), let .pdfDocument(_, canShareValue):
|
||||
case let .document(_, canShareValue), let .pdfDocument(_, canShareValue), let .markdownDocument(_, canShareValue):
|
||||
canShare = canShareValue
|
||||
default:
|
||||
break
|
||||
|
|
@ -1398,6 +1501,7 @@ public class BrowserScreen: ViewController, MinimizableController {
|
|||
context: self.context,
|
||||
contentState: self.contentState,
|
||||
presentationState: self.presentationState,
|
||||
toolbarMode: self.toolbarMode,
|
||||
canShare: canShare,
|
||||
performAction: self.performAction,
|
||||
performHoldAction: { [weak self] view, gesture, action in
|
||||
|
|
@ -1477,10 +1581,11 @@ public class BrowserScreen: ViewController, MinimizableController {
|
|||
case instantPage(webPage: TelegramMediaWebpage, anchor: String?, sourceLocation: InstantPageSourceLocation, preloadedResources: [Any]?)
|
||||
case document(file: FileMediaReference, canShare: Bool)
|
||||
case pdfDocument(file: FileMediaReference, canShare: Bool)
|
||||
case markdownDocument(file: FileMediaReference, canShare: Bool)
|
||||
|
||||
public var fileId: MediaId? {
|
||||
switch self {
|
||||
case let .document(file, _), let .pdfDocument(file, _):
|
||||
case let .document(file, _), let .pdfDocument(file, _), let .markdownDocument(file, _):
|
||||
return file.media.fileId
|
||||
default:
|
||||
return nil
|
||||
|
|
@ -1506,7 +1611,10 @@ public class BrowserScreen: ViewController, MinimizableController {
|
|||
"application/vnd.ms-excel",
|
||||
"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
|
||||
"application/vnd.openxmlformats-officedocument.spreadsheetml.template",
|
||||
"application/vnd.openxmlformats-officedocument.presentationml.presentation"
|
||||
"application/vnd.openxmlformats-officedocument.presentationml.presentation",
|
||||
"text/markdown",
|
||||
"text/x-markdown",
|
||||
"text/x-web-markdown"
|
||||
]
|
||||
|
||||
public static let supportedDocumentExtensions: [String] = [
|
||||
|
|
@ -1517,7 +1625,8 @@ public class BrowserScreen: ViewController, MinimizableController {
|
|||
"docx",
|
||||
"xls",
|
||||
"xlsx",
|
||||
"pptx"
|
||||
"pptx",
|
||||
"md"
|
||||
]
|
||||
|
||||
public init(context: AccountContext, subject: Subject, preferredConfiguration: WKWebViewConfiguration? = nil, openPreviousOnClose: Bool = false) {
|
||||
|
|
|
|||
|
|
@ -9,6 +9,13 @@ import ContextReferenceButtonComponent
|
|||
import GlassBackgroundComponent
|
||||
import EdgeEffect
|
||||
|
||||
enum BrowserToolbarMode: Equatable {
|
||||
case webPage
|
||||
case instantPage
|
||||
case document
|
||||
case markdown
|
||||
}
|
||||
|
||||
final class BrowserToolbarComponent: CombinedComponent {
|
||||
let theme: PresentationTheme
|
||||
let bottomInset: CGFloat
|
||||
|
|
@ -131,7 +138,7 @@ final class NavigationToolbarContentComponent: CombinedComponent {
|
|||
let canGoForward: Bool
|
||||
let canOpenIn: Bool
|
||||
let canShare: Bool
|
||||
let isDocument: Bool
|
||||
let mode: BrowserToolbarMode
|
||||
let performAction: ActionSlot<BrowserScreen.Action>
|
||||
let performHoldAction: (UIView, ContextGesture?, BrowserScreen.Action) -> Void
|
||||
|
||||
|
|
@ -141,7 +148,7 @@ final class NavigationToolbarContentComponent: CombinedComponent {
|
|||
canGoForward: Bool,
|
||||
canOpenIn: Bool,
|
||||
canShare: Bool,
|
||||
isDocument: Bool,
|
||||
mode: BrowserToolbarMode,
|
||||
performAction: ActionSlot<BrowserScreen.Action>,
|
||||
performHoldAction: @escaping (UIView, ContextGesture?, BrowserScreen.Action) -> Void
|
||||
) {
|
||||
|
|
@ -150,7 +157,7 @@ final class NavigationToolbarContentComponent: CombinedComponent {
|
|||
self.canGoForward = canGoForward
|
||||
self.canOpenIn = canOpenIn
|
||||
self.canShare = canShare
|
||||
self.isDocument = isDocument
|
||||
self.mode = mode
|
||||
self.performAction = performAction
|
||||
self.performHoldAction = performHoldAction
|
||||
}
|
||||
|
|
@ -171,7 +178,7 @@ final class NavigationToolbarContentComponent: CombinedComponent {
|
|||
if lhs.canShare != rhs.canShare {
|
||||
return false
|
||||
}
|
||||
if lhs.isDocument != rhs.isDocument {
|
||||
if lhs.mode != rhs.mode {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
|
|
@ -217,7 +224,8 @@ final class NavigationToolbarContentComponent: CombinedComponent {
|
|||
transition: .easeInOut(duration: 0.2)
|
||||
)
|
||||
|
||||
if context.component.isDocument {
|
||||
switch context.component.mode {
|
||||
case .document:
|
||||
var originX: CGFloat = sideInset
|
||||
|
||||
let search = search.update(
|
||||
|
|
@ -270,7 +278,40 @@ final class NavigationToolbarContentComponent: CombinedComponent {
|
|||
.position(CGPoint(x: originX, y: availableSize.height / 2.0))
|
||||
)
|
||||
size.width = originX + sideInset
|
||||
} else {
|
||||
case .markdown:
|
||||
var originX: CGFloat = sideInset
|
||||
|
||||
if !context.component.canShare {
|
||||
context.add(share
|
||||
.position(CGPoint(x: availableSize.width / 2.0, y: 10000.0))
|
||||
)
|
||||
} else {
|
||||
context.add(share
|
||||
.position(CGPoint(x: originX, y: availableSize.height / 2.0))
|
||||
)
|
||||
originX += spacing
|
||||
}
|
||||
|
||||
let quickLook = quickLook.update(
|
||||
component: Button(
|
||||
content: AnyComponent(
|
||||
BundleIconComponent(
|
||||
name: "Instant View/OpenDocument",
|
||||
tintColor: textColor
|
||||
)
|
||||
),
|
||||
action: {
|
||||
performAction.invoke(.openIn)
|
||||
}
|
||||
).minSize(buttonSize),
|
||||
availableSize: buttonSize,
|
||||
transition: .easeInOut(duration: 0.2)
|
||||
)
|
||||
context.add(quickLook
|
||||
.position(CGPoint(x: originX, y: availableSize.height / 2.0))
|
||||
)
|
||||
size.width = originX + sideInset
|
||||
case .webPage, .instantPage:
|
||||
var originX: CGFloat = sideInset
|
||||
|
||||
let canGoBack = context.component.canGoBack
|
||||
|
|
|
|||
|
|
@ -23,8 +23,6 @@ func fetchFavicon(context: AccountContext, url: String, size: CGSize) -> Signal<
|
|||
if let data {
|
||||
if let image = UIImage(data: data) {
|
||||
return image
|
||||
} else if url.lowercased().contains(".svg"), let preparedData = prepareSvgImage(data, false), let image = renderPreparedImage(preparedData, size, .clear, UIScreenScale, false) {
|
||||
return image
|
||||
}
|
||||
return nil
|
||||
} else {
|
||||
|
|
|
|||
|
|
@ -2384,11 +2384,8 @@ public class ChatListItemNode: ItemListRevealOptionsItemNode {
|
|||
var currentStatusIconContent: EmojiStatusComponent.Content?
|
||||
var currentStatusIconParticleColor: UIColor?
|
||||
var currentSecretIconImage: UIImage?
|
||||
var currentForwardedIcon: UIImage?
|
||||
var currentStoryIcon: UIImage?
|
||||
var currentGiftIcon: UIImage?
|
||||
var currentLocationIcon: UIImage?
|
||||
var currentPollIcon: UIImage?
|
||||
var currentMessageTypeIcon: UIImage?
|
||||
var currentMessageTypeIconOffset: CGPoint = .zero
|
||||
|
||||
var selectableControlSizeAndApply: (CGFloat, (CGSize, Bool) -> ItemListSelectableControlNode)?
|
||||
var reorderControlSizeAndApply: (CGFloat, (CGFloat, Bool, ContainedViewLayoutTransition) -> ItemListEditableReorderControlNode)?
|
||||
|
|
@ -2559,12 +2556,26 @@ public class ChatListItemNode: ItemListRevealOptionsItemNode {
|
|||
var contentImageSpecs: [ContentImageSpec] = []
|
||||
var avatarContentImageSpec: ContentImageSpec?
|
||||
var forumThread: (id: Int64, title: String, iconId: Int64?, iconColor: Int32, threadPeer: EnginePeer?, isUnread: Bool)?
|
||||
|
||||
var displayForwardedIcon = false
|
||||
var displayStoryReplyIcon = false
|
||||
var displayGiftIcon = false
|
||||
var displayLocationIcon = false
|
||||
var displayPollIcon = false
|
||||
|
||||
enum MessageTypeIcon {
|
||||
enum CallType {
|
||||
case voice
|
||||
case video
|
||||
}
|
||||
enum CallDirection {
|
||||
case incoming
|
||||
case outgoing
|
||||
}
|
||||
case call(CallType, CallDirection)
|
||||
case forward
|
||||
case story
|
||||
case gift
|
||||
case location
|
||||
case poll
|
||||
case todo
|
||||
case game
|
||||
}
|
||||
var messageTypeIcon: MessageTypeIcon?
|
||||
var ignoreForwardedIcon = false
|
||||
|
||||
switch contentData {
|
||||
|
|
@ -2874,24 +2885,29 @@ public class ChatListItemNode: ItemListRevealOptionsItemNode {
|
|||
|
||||
if !ignoreForwardedIcon {
|
||||
if case .savedMessagesChats = item.chatListLocation {
|
||||
displayForwardedIcon = false
|
||||
} else if let forwardInfo = message.forwardInfo, !forwardInfo.flags.contains(.isImported) && !message.id.peerId.isVerificationCodes {
|
||||
displayForwardedIcon = true
|
||||
messageTypeIcon = .forward
|
||||
} else if let _ = message.attributes.first(where: { $0 is ReplyStoryAttribute }) {
|
||||
displayStoryReplyIcon = true
|
||||
messageTypeIcon = .story
|
||||
} else {
|
||||
for media in message.media {
|
||||
if let _ = media as? TelegramMediaPoll {
|
||||
displayPollIcon = true
|
||||
messageTypeIcon = .poll
|
||||
} else if let _ = media as? TelegramMediaTodo {
|
||||
messageTypeIcon = .todo
|
||||
} else if let _ = media as? TelegramMediaGame {
|
||||
messageTypeIcon = .game
|
||||
} else if let _ = media as? TelegramMediaMap {
|
||||
displayLocationIcon = true
|
||||
messageTypeIcon = .location
|
||||
} else if let action = media as? TelegramMediaAction {
|
||||
switch action.action {
|
||||
case let .phoneCall(_, _, _, isVideo):
|
||||
messageTypeIcon = .call(isVideo ? .video : .voice, message.flags.contains(.Incoming) ? .incoming : .outgoing)
|
||||
case .giftPremium, .giftStars, .starGift, .starGiftUnique:
|
||||
displayGiftIcon = true
|
||||
messageTypeIcon = .gift
|
||||
case let .giftCode(_, _, _, boostPeerId, _, _, _, _, _, _, _):
|
||||
if boostPeerId == nil {
|
||||
displayGiftIcon = true
|
||||
messageTypeIcon = .gift
|
||||
}
|
||||
default:
|
||||
break
|
||||
|
|
@ -3048,64 +3064,50 @@ public class ChatListItemNode: ItemListRevealOptionsItemNode {
|
|||
attributedText = textString
|
||||
}
|
||||
|
||||
if displayForwardedIcon {
|
||||
currentForwardedIcon = PresentationResourcesChatList.forwardedIcon(item.presentationData.theme)
|
||||
}
|
||||
|
||||
if displayStoryReplyIcon {
|
||||
currentStoryIcon = PresentationResourcesChatList.storyReplyIcon(item.presentationData.theme)
|
||||
}
|
||||
|
||||
if displayGiftIcon {
|
||||
currentGiftIcon = PresentationResourcesChatList.giftIcon(item.presentationData.theme)
|
||||
}
|
||||
|
||||
if displayLocationIcon {
|
||||
currentLocationIcon = PresentationResourcesChatList.locationIcon(item.presentationData.theme)
|
||||
}
|
||||
|
||||
if displayPollIcon {
|
||||
currentPollIcon = PresentationResourcesChatList.pollIcon(item.presentationData.theme)
|
||||
}
|
||||
|
||||
if let currentForwardedIcon {
|
||||
textLeftCutout += currentForwardedIcon.size.width
|
||||
if !contentImageSpecs.isEmpty {
|
||||
textLeftCutout += forwardedIconSpacing
|
||||
} else {
|
||||
textLeftCutout += contentImageTrailingSpace
|
||||
switch messageTypeIcon {
|
||||
case let .call(type, direction):
|
||||
switch type {
|
||||
case .voice:
|
||||
switch direction {
|
||||
case .incoming:
|
||||
currentMessageTypeIcon = PresentationResourcesChatList.callIncomingIcon(item.presentationData.theme)
|
||||
case .outgoing:
|
||||
currentMessageTypeIcon = PresentationResourcesChatList.callOutgoingIcon(item.presentationData.theme)
|
||||
}
|
||||
case .video:
|
||||
switch direction {
|
||||
case .incoming:
|
||||
currentMessageTypeIcon = PresentationResourcesChatList.callVideoIncomingIcon(item.presentationData.theme)
|
||||
case .outgoing:
|
||||
currentMessageTypeIcon = PresentationResourcesChatList.callVideoOutgoingIcon(item.presentationData.theme)
|
||||
}
|
||||
}
|
||||
case .forward:
|
||||
currentMessageTypeIcon = PresentationResourcesChatList.forwardedIcon(item.presentationData.theme)
|
||||
currentMessageTypeIconOffset.y = 3.0
|
||||
case .story:
|
||||
currentMessageTypeIcon = PresentationResourcesChatList.storyReplyIcon(item.presentationData.theme)
|
||||
case .gift:
|
||||
currentMessageTypeIcon = PresentationResourcesChatList.giftIcon(item.presentationData.theme)
|
||||
currentMessageTypeIconOffset.y = -2.0 - UIScreenPixel
|
||||
case .location:
|
||||
currentMessageTypeIcon = PresentationResourcesChatList.locationIcon(item.presentationData.theme)
|
||||
currentMessageTypeIconOffset.y = -1.0 - UIScreenPixel
|
||||
case .poll:
|
||||
currentMessageTypeIcon = PresentationResourcesChatList.pollIcon(item.presentationData.theme)
|
||||
currentMessageTypeIconOffset.y = -1.0
|
||||
case .todo:
|
||||
currentMessageTypeIcon = PresentationResourcesChatList.todoIcon(item.presentationData.theme)
|
||||
currentMessageTypeIconOffset.y = -1.0
|
||||
case .game:
|
||||
currentMessageTypeIcon = PresentationResourcesChatList.gameIcon(item.presentationData.theme)
|
||||
currentMessageTypeIconOffset.y = -1.0
|
||||
default:
|
||||
break
|
||||
}
|
||||
|
||||
if let currentStoryIcon {
|
||||
textLeftCutout += currentStoryIcon.size.width
|
||||
if !contentImageSpecs.isEmpty {
|
||||
textLeftCutout += forwardedIconSpacing
|
||||
} else {
|
||||
textLeftCutout += contentImageTrailingSpace
|
||||
}
|
||||
}
|
||||
|
||||
if let currentGiftIcon {
|
||||
textLeftCutout += currentGiftIcon.size.width
|
||||
if !contentImageSpecs.isEmpty {
|
||||
textLeftCutout += forwardedIconSpacing
|
||||
} else {
|
||||
textLeftCutout += contentImageTrailingSpace
|
||||
}
|
||||
}
|
||||
|
||||
if let currentLocationIcon {
|
||||
textLeftCutout += currentLocationIcon.size.width
|
||||
if !contentImageSpecs.isEmpty {
|
||||
textLeftCutout += forwardedIconSpacing
|
||||
} else {
|
||||
textLeftCutout += contentImageTrailingSpace
|
||||
}
|
||||
}
|
||||
|
||||
if let currentPollIcon {
|
||||
textLeftCutout += currentPollIcon.size.width
|
||||
if let currentMessageTypeIcon {
|
||||
textLeftCutout += currentMessageTypeIcon.size.width
|
||||
if !contentImageSpecs.isEmpty {
|
||||
textLeftCutout += forwardedIconSpacing
|
||||
} else {
|
||||
|
|
@ -4765,31 +4767,16 @@ public class ChatListItemNode: ItemListRevealOptionsItemNode {
|
|||
|
||||
var mediaPreviewOffset = textNodeFrame.origin.offsetBy(dx: 1.0, dy: 1.0 + floor((measureLayout.size.height - contentImageSize.height) / 2.0))
|
||||
|
||||
var messageTypeIcon: UIImage?
|
||||
var messageTypeIconOffset = mediaPreviewOffset
|
||||
if let currentForwardedIcon {
|
||||
messageTypeIcon = currentForwardedIcon
|
||||
messageTypeIconOffset.y += 3.0
|
||||
} else if let currentStoryIcon {
|
||||
messageTypeIcon = currentStoryIcon
|
||||
} else if let currentGiftIcon {
|
||||
messageTypeIcon = currentGiftIcon
|
||||
messageTypeIconOffset.y -= 2.0 - UIScreenPixel
|
||||
} else if let currentLocationIcon {
|
||||
messageTypeIcon = currentLocationIcon
|
||||
messageTypeIconOffset.y -= 2.0 - UIScreenPixel
|
||||
} else if let currentPollIcon {
|
||||
messageTypeIcon = currentPollIcon
|
||||
messageTypeIconOffset.y -= 2.0 - UIScreenPixel
|
||||
}
|
||||
let messageTypeIconImage = currentMessageTypeIcon
|
||||
let messageTypeIconOffset = CGPoint(x: mediaPreviewOffset.x + currentMessageTypeIconOffset.x, y: mediaPreviewOffset.y + currentMessageTypeIconOffset.y)
|
||||
|
||||
if let messageTypeIcon {
|
||||
strongSelf.forwardedIconNode.image = messageTypeIcon
|
||||
if let messageTypeIconImage {
|
||||
strongSelf.forwardedIconNode.image = messageTypeIconImage
|
||||
if strongSelf.forwardedIconNode.supernode == nil {
|
||||
strongSelf.mainContentContainerNode.addSubnode(strongSelf.forwardedIconNode)
|
||||
}
|
||||
transition.updateFrame(node: strongSelf.forwardedIconNode, frame: CGRect(origin: messageTypeIconOffset, size: messageTypeIcon.size))
|
||||
mediaPreviewOffset.x += messageTypeIcon.size.width + forwardedIconSpacing
|
||||
transition.updateFrame(node: strongSelf.forwardedIconNode, frame: CGRect(origin: messageTypeIconOffset, size: messageTypeIconImage.size))
|
||||
mediaPreviewOffset.x += messageTypeIconImage.size.width + forwardedIconSpacing
|
||||
} else if strongSelf.forwardedIconNode.supernode != nil {
|
||||
strongSelf.forwardedIconNode.removeFromSupernode()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -311,7 +311,7 @@ public func chatListItemStrings(strings: PresentationStrings, nameDisplayOrder:
|
|||
case _ as TelegramMediaContact:
|
||||
messageText = strings.Message_Contact
|
||||
case let game as TelegramMediaGame:
|
||||
messageText = "🎮 \(game.title)"
|
||||
messageText = game.title
|
||||
case let invoice as TelegramMediaInvoice:
|
||||
messageText = invoice.title
|
||||
case let action as TelegramMediaAction:
|
||||
|
|
@ -438,15 +438,11 @@ public func chatListItemStrings(strings: PresentationStrings, nameDisplayOrder:
|
|||
messageText = content.displayUrl
|
||||
}
|
||||
case let todo as TelegramMediaTodo:
|
||||
let pollPrefix = "☑️ "
|
||||
let entityOffset = (pollPrefix as NSString).length
|
||||
messageText = "\(pollPrefix)\(todo.text)"
|
||||
messageText = todo.text
|
||||
customEmojiRanges = []
|
||||
for entity in todo.textEntities {
|
||||
if case let .CustomEmoji(_, fileId) = entity.type {
|
||||
if customEmojiRanges == nil {
|
||||
customEmojiRanges = []
|
||||
}
|
||||
let range = NSRange(location: entityOffset + entity.range.lowerBound, length: entity.range.upperBound - entity.range.lowerBound)
|
||||
let range = NSRange(location: entity.range.lowerBound, length: entity.range.upperBound - entity.range.lowerBound)
|
||||
let attribute = ChatTextInputTextCustomEmojiAttribute(interactivelySelectedFromPackId: nil, fileId: fileId, file: message.associatedMedia[EngineMedia.Id(namespace: Namespaces.Media.CloudFile, id: fileId)] as? TelegramMediaFile)
|
||||
customEmojiRanges?.append((range, attribute))
|
||||
}
|
||||
|
|
|
|||
|
|
@ -383,8 +383,15 @@ public final class ReactionListContextMenuContent: ContextControllerItemsContent
|
|||
private var iconFrame: CGRect?
|
||||
private var file: TelegramMediaFile?
|
||||
private var fileDisposable: Disposable?
|
||||
private var longTapRecognizer: UILongPressGestureRecognizer?
|
||||
private var skipNextTapAction = false
|
||||
|
||||
let action: () -> Void
|
||||
var longTapAction: (() -> Void)? {
|
||||
didSet {
|
||||
self.longTapRecognizer?.isEnabled = self.longTapAction != nil
|
||||
}
|
||||
}
|
||||
|
||||
private var item: EngineMessageReactionListContext.Item?
|
||||
|
||||
|
|
@ -422,6 +429,12 @@ public final class ReactionListContextMenuContent: ContextControllerItemsContent
|
|||
self.view.addSubview(self.readIconView)
|
||||
|
||||
self.addTarget(self, action: #selector(self.pressed), forControlEvents: .touchUpInside)
|
||||
|
||||
let longTapRecognizer = UILongPressGestureRecognizer(target: self, action: #selector(self.longTapGesture(_:)))
|
||||
longTapRecognizer.isEnabled = false
|
||||
longTapRecognizer.cancelsTouchesInView = true
|
||||
self.view.addGestureRecognizer(longTapRecognizer)
|
||||
self.longTapRecognizer = longTapRecognizer
|
||||
}
|
||||
|
||||
deinit {
|
||||
|
|
@ -429,9 +442,30 @@ public final class ReactionListContextMenuContent: ContextControllerItemsContent
|
|||
}
|
||||
|
||||
@objc private func pressed() {
|
||||
if self.skipNextTapAction {
|
||||
self.skipNextTapAction = false
|
||||
return
|
||||
}
|
||||
self.action()
|
||||
}
|
||||
|
||||
@objc private func longTapGesture(_ recognizer: UILongPressGestureRecognizer) {
|
||||
switch recognizer.state {
|
||||
case .began:
|
||||
guard let item = self.item, item.reaction != nil else {
|
||||
return
|
||||
}
|
||||
self.skipNextTapAction = true
|
||||
self.longTapAction?()
|
||||
case .cancelled, .ended, .failed:
|
||||
DispatchQueue.main.async { [weak self] in
|
||||
self?.skipNextTapAction = false
|
||||
}
|
||||
default:
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
private func updateReactionLayer() {
|
||||
guard let file = self.file else {
|
||||
return
|
||||
|
|
@ -771,6 +805,7 @@ public final class ReactionListContextMenuContent: ContextControllerItemsContent
|
|||
private let requestUpdate: (ReactionsTabNode, ContainedViewLayoutTransition) -> Void
|
||||
private let requestUpdateApparentHeight: (ReactionsTabNode, ContainedViewLayoutTransition) -> Void
|
||||
private let openPeer: (EnginePeer, Bool) -> Void
|
||||
private let deleteReaction: ((EnginePeer, MessageReaction.Reaction) -> Void)?
|
||||
|
||||
private var hasMore: Bool = false
|
||||
|
||||
|
|
@ -804,7 +839,8 @@ public final class ReactionListContextMenuContent: ContextControllerItemsContent
|
|||
readStats: MessageReadStats?,
|
||||
requestUpdate: @escaping (ReactionsTabNode, ContainedViewLayoutTransition) -> Void,
|
||||
requestUpdateApparentHeight: @escaping (ReactionsTabNode, ContainedViewLayoutTransition) -> Void,
|
||||
openPeer: @escaping (EnginePeer, Bool) -> Void
|
||||
openPeer: @escaping (EnginePeer, Bool) -> Void,
|
||||
deleteReaction: ((EnginePeer, MessageReaction.Reaction) -> Void)?
|
||||
) {
|
||||
self.context = context
|
||||
self.displayReadTimestamps = displayReadTimestamps
|
||||
|
|
@ -816,6 +852,7 @@ public final class ReactionListContextMenuContent: ContextControllerItemsContent
|
|||
self.requestUpdate = requestUpdate
|
||||
self.requestUpdateApparentHeight = requestUpdateApparentHeight
|
||||
self.openPeer = openPeer
|
||||
self.deleteReaction = deleteReaction
|
||||
|
||||
self.listContext = context.engine.messages.messageReactionList(message: message, readStats: readStats, reaction: reaction)
|
||||
self.state = ItemsState(listState: EngineMessageReactionListContext.State(message: message, readStats: readStats, reaction: reaction), readStats: readStats)
|
||||
|
|
@ -928,6 +965,14 @@ public final class ReactionListContextMenuContent: ContextControllerItemsContent
|
|||
}
|
||||
|
||||
itemNode.update(size: itemFrame.size, presentationData: presentationData, item: item, isLast: self.state.item(at: index + 1) == nil, syncronousLoad: syncronousLoad)
|
||||
if let deleteReaction = self.deleteReaction, let reaction = item.reaction {
|
||||
let peer = item.peer
|
||||
itemNode.longTapAction = {
|
||||
deleteReaction(peer, reaction)
|
||||
}
|
||||
} else {
|
||||
itemNode.longTapAction = nil
|
||||
}
|
||||
itemNode.frame = itemFrame
|
||||
} else if index < self.state.totalCount {
|
||||
validPlaceholderIds.insert(index)
|
||||
|
|
@ -1079,6 +1124,7 @@ public final class ReactionListContextMenuContent: ContextControllerItemsContent
|
|||
private let reactions: [(MessageReaction.Reaction?, Int)]
|
||||
private let requestUpdate: (ContainedViewLayoutTransition) -> Void
|
||||
private let requestUpdateApparentHeight: (ContainedViewLayoutTransition) -> Void
|
||||
private let deleteReaction: ((EnginePeer, MessageReaction.Reaction) -> Void)?
|
||||
|
||||
private var presentationData: PresentationData
|
||||
|
||||
|
|
@ -1111,7 +1157,8 @@ public final class ReactionListContextMenuContent: ContextControllerItemsContent
|
|||
requestUpdate: @escaping (ContainedViewLayoutTransition) -> Void,
|
||||
requestUpdateApparentHeight: @escaping (ContainedViewLayoutTransition) -> Void,
|
||||
back: (() -> Void)?,
|
||||
openPeer: @escaping (EnginePeer, Bool) -> Void
|
||||
openPeer: @escaping (EnginePeer, Bool) -> Void,
|
||||
deleteReaction: ((EnginePeer, MessageReaction.Reaction) -> Void)?
|
||||
) {
|
||||
self.context = context
|
||||
self.displayReadTimestamps = displayReadTimestamps
|
||||
|
|
@ -1126,6 +1173,7 @@ public final class ReactionListContextMenuContent: ContextControllerItemsContent
|
|||
|
||||
self.requestUpdate = requestUpdate
|
||||
self.requestUpdateApparentHeight = requestUpdateApparentHeight
|
||||
self.deleteReaction = deleteReaction
|
||||
|
||||
if let back = back {
|
||||
self.backButtonNode = BackButtonNode()
|
||||
|
|
@ -1332,7 +1380,8 @@ public final class ReactionListContextMenuContent: ContextControllerItemsContent
|
|||
strongSelf.requestUpdateApparentHeight(transition)
|
||||
}
|
||||
},
|
||||
openPeer: self.openPeer
|
||||
openPeer: self.openPeer,
|
||||
deleteReaction: self.deleteReaction
|
||||
)
|
||||
self.addSubnode(tabNode)
|
||||
self.visibleTabNodes[index] = tabNode
|
||||
|
|
@ -1426,6 +1475,7 @@ public final class ReactionListContextMenuContent: ContextControllerItemsContent
|
|||
let readStats: MessageReadStats?
|
||||
let back: (() -> Void)?
|
||||
let openPeer: (EnginePeer, Bool) -> Void
|
||||
let deleteReaction: ((EnginePeer, MessageReaction.Reaction) -> Void)?
|
||||
|
||||
public init(
|
||||
context: AccountContext,
|
||||
|
|
@ -1437,7 +1487,8 @@ public final class ReactionListContextMenuContent: ContextControllerItemsContent
|
|||
reaction: MessageReaction.Reaction?,
|
||||
readStats: MessageReadStats?,
|
||||
back: (() -> Void)?,
|
||||
openPeer: @escaping (EnginePeer, Bool) -> Void
|
||||
openPeer: @escaping (EnginePeer, Bool) -> Void,
|
||||
deleteReaction: ((EnginePeer, MessageReaction.Reaction) -> Void)? = nil
|
||||
) {
|
||||
self.context = context
|
||||
self.displayReadTimestamps = displayReadTimestamps
|
||||
|
|
@ -1449,6 +1500,7 @@ public final class ReactionListContextMenuContent: ContextControllerItemsContent
|
|||
self.readStats = readStats
|
||||
self.back = back
|
||||
self.openPeer = openPeer
|
||||
self.deleteReaction = deleteReaction
|
||||
}
|
||||
|
||||
public func node(
|
||||
|
|
@ -1467,7 +1519,8 @@ public final class ReactionListContextMenuContent: ContextControllerItemsContent
|
|||
requestUpdate: requestUpdate,
|
||||
requestUpdateApparentHeight: requestUpdateApparentHeight,
|
||||
back: self.back,
|
||||
openPeer: self.openPeer
|
||||
openPeer: self.openPeer,
|
||||
deleteReaction: self.deleteReaction
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@ public final class ResizableSheetComponentEnvironment: Equatable {
|
|||
public let bounds: CGRect
|
||||
public let isInteractive: Bool
|
||||
}
|
||||
|
||||
|
||||
public let theme: PresentationTheme
|
||||
public let statusBarHeight: CGFloat
|
||||
public let safeInsets: UIEdgeInsets
|
||||
|
|
@ -26,7 +26,7 @@ public final class ResizableSheetComponentEnvironment: Equatable {
|
|||
public let regularMetricsSize: CGSize?
|
||||
public let dismiss: (Bool) -> Void
|
||||
public let boundsUpdated: ActionSlot<BoundsUpdate>
|
||||
|
||||
|
||||
public init(
|
||||
theme: PresentationTheme,
|
||||
statusBarHeight: CGFloat,
|
||||
|
|
@ -54,7 +54,7 @@ public final class ResizableSheetComponentEnvironment: Equatable {
|
|||
self.dismiss = dismiss
|
||||
self.boundsUpdated = boundsUpdated
|
||||
}
|
||||
|
||||
|
||||
public static func ==(lhs: ResizableSheetComponentEnvironment, rhs: ResizableSheetComponentEnvironment) -> Bool {
|
||||
if lhs.theme != rhs.theme {
|
||||
return false
|
||||
|
|
@ -92,19 +92,24 @@ public final class ResizableSheetComponentEnvironment: Equatable {
|
|||
|
||||
public final class ResizableSheetComponent<ChildEnvironmentType: Sendable & Equatable>: Component {
|
||||
public typealias EnvironmentType = (ChildEnvironmentType, ResizableSheetComponentEnvironment)
|
||||
|
||||
|
||||
public class ExternalState {
|
||||
public fileprivate(set) var contentHeight: CGFloat
|
||||
|
||||
fileprivate var trackedScrollViewUpdated: ((UIScrollView?) -> Void)?
|
||||
|
||||
public init() {
|
||||
self.contentHeight = 0.0
|
||||
}
|
||||
|
||||
public func setTrackedScrollView(_ scrollView: UIScrollView?) {
|
||||
self.trackedScrollViewUpdated?(scrollView)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public enum BackgroundColor: Equatable {
|
||||
case color(UIColor)
|
||||
}
|
||||
|
||||
|
||||
public let content: AnyComponent<ChildEnvironmentType>
|
||||
public let titleItem: AnyComponent<Empty>?
|
||||
public let leftItem: AnyComponent<Empty>?
|
||||
|
|
@ -113,9 +118,10 @@ public final class ResizableSheetComponent<ChildEnvironmentType: Sendable & Equa
|
|||
public let bottomItem: AnyComponent<Empty>?
|
||||
public let backgroundColor: BackgroundColor
|
||||
public let isFullscreen: Bool
|
||||
public let defaultHeight: CGFloat?
|
||||
public let externalState: ExternalState?
|
||||
public let animateOut: ActionSlot<Action<()>>
|
||||
|
||||
|
||||
public init(
|
||||
content: AnyComponent<ChildEnvironmentType>,
|
||||
titleItem: AnyComponent<Empty>? = nil,
|
||||
|
|
@ -125,6 +131,7 @@ public final class ResizableSheetComponent<ChildEnvironmentType: Sendable & Equa
|
|||
bottomItem: AnyComponent<Empty>? = nil,
|
||||
backgroundColor: BackgroundColor,
|
||||
isFullscreen: Bool = false,
|
||||
defaultHeight: CGFloat? = nil,
|
||||
externalState: ExternalState? = nil,
|
||||
animateOut: ActionSlot<Action<()>>,
|
||||
) {
|
||||
|
|
@ -136,10 +143,11 @@ public final class ResizableSheetComponent<ChildEnvironmentType: Sendable & Equa
|
|||
self.bottomItem = bottomItem
|
||||
self.backgroundColor = backgroundColor
|
||||
self.isFullscreen = isFullscreen
|
||||
self.defaultHeight = defaultHeight
|
||||
self.externalState = externalState
|
||||
self.animateOut = animateOut
|
||||
}
|
||||
|
||||
|
||||
public static func ==(lhs: ResizableSheetComponent, rhs: ResizableSheetComponent) -> Bool {
|
||||
if lhs.content != rhs.content {
|
||||
return false
|
||||
|
|
@ -165,12 +173,15 @@ public final class ResizableSheetComponent<ChildEnvironmentType: Sendable & Equa
|
|||
if lhs.isFullscreen != rhs.isFullscreen {
|
||||
return false
|
||||
}
|
||||
if lhs.defaultHeight != rhs.defaultHeight {
|
||||
return false
|
||||
}
|
||||
if lhs.animateOut != rhs.animateOut {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
|
||||
private struct ItemLayout: Equatable {
|
||||
var containerSize: CGSize
|
||||
var containerInset: CGFloat
|
||||
|
|
@ -179,7 +190,7 @@ public final class ResizableSheetComponent<ChildEnvironmentType: Sendable & Equa
|
|||
var topInset: CGFloat
|
||||
var fillingSize: CGFloat
|
||||
let isTablet: Bool
|
||||
|
||||
|
||||
init(containerSize: CGSize, containerInset: CGFloat, containerCornerRadius: CGFloat, bottomInset: CGFloat, topInset: CGFloat, fillingSize: CGFloat, isTablet: Bool) {
|
||||
self.containerSize = containerSize
|
||||
self.containerInset = containerInset
|
||||
|
|
@ -190,26 +201,26 @@ public final class ResizableSheetComponent<ChildEnvironmentType: Sendable & Equa
|
|||
self.isTablet = isTablet
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private final class ScrollView: UIScrollView {
|
||||
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
|
||||
return super.hitTest(point, with: event)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public final class View: UIView, UIScrollViewDelegate, ComponentTaggedView, UIGestureRecognizerDelegate {
|
||||
public final class Tag {
|
||||
public init() {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public func matches(tag: Any) -> Bool {
|
||||
if let _ = tag as? Tag {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
|
||||
private let dimView: UIView
|
||||
public let containerView: UIView
|
||||
private let backgroundLayer: SimpleLayer
|
||||
|
|
@ -218,54 +229,57 @@ public final class ResizableSheetComponent<ChildEnvironmentType: Sendable & Equa
|
|||
private let scrollView: ScrollView
|
||||
private let scrollContentClippingView: SparseContainerView
|
||||
private let scrollContentView: UIView
|
||||
|
||||
|
||||
private let topEdgeEffectView: EdgeEffectView
|
||||
private let bottomEdgeEffectView: EdgeEffectView
|
||||
private let contentView: ComponentView<ChildEnvironmentType>
|
||||
|
||||
|
||||
private var titleItemView: ComponentView<Empty>?
|
||||
private var leftItemView: ComponentView<Empty>?
|
||||
private var rightItemView: ComponentView<Empty>?
|
||||
private var bottomItemView: ComponentView<Empty>?
|
||||
|
||||
|
||||
private let backgroundHandleView: UIImageView
|
||||
|
||||
|
||||
private var ignoreScrolling: Bool = false
|
||||
private var isDismissingInteractively: Bool = false
|
||||
private var dismissTranslation: CGFloat = 0.0
|
||||
private var dismissStartTranslation: CGFloat?
|
||||
private var dismissPanGesture: UIPanGestureRecognizer?
|
||||
|
||||
|
||||
private var component: ResizableSheetComponent?
|
||||
private weak var state: EmptyComponentState?
|
||||
private var isUpdating: Bool = false
|
||||
private var environment: ResizableSheetComponentEnvironment?
|
||||
private var itemLayout: ItemLayout?
|
||||
|
||||
private var registeredExternalState: ExternalState?
|
||||
private weak var trackedScrollView: UIScrollView?
|
||||
private var trackedScrollViewWasAtTopOnGestureBegan = false
|
||||
|
||||
override init(frame: CGRect) {
|
||||
self.dimView = UIView()
|
||||
self.containerView = UIView()
|
||||
|
||||
|
||||
self.containerView.clipsToBounds = true
|
||||
self.containerView.layer.cornerRadius = 40.0
|
||||
self.containerView.layer.maskedCorners = [.layerMinXMaxYCorner, .layerMaxXMaxYCorner]
|
||||
|
||||
|
||||
self.backgroundLayer = SimpleLayer()
|
||||
self.backgroundLayer.maskedCorners = [.layerMinXMinYCorner, .layerMaxXMinYCorner]
|
||||
self.backgroundLayer.cornerRadius = 40.0
|
||||
|
||||
|
||||
self.backgroundHandleView = UIImageView()
|
||||
|
||||
|
||||
self.navigationBarContainer = SparseContainerView()
|
||||
self.bottomContainer = SparseContainerView()
|
||||
|
||||
|
||||
self.scrollView = ScrollView()
|
||||
|
||||
|
||||
self.scrollContentClippingView = SparseContainerView()
|
||||
self.scrollContentClippingView.clipsToBounds = true
|
||||
|
||||
|
||||
self.scrollContentView = UIView()
|
||||
|
||||
|
||||
self.topEdgeEffectView = EdgeEffectView()
|
||||
self.topEdgeEffectView.clipsToBounds = true
|
||||
self.topEdgeEffectView.layer.maskedCorners = [.layerMinXMinYCorner, .layerMaxXMinYCorner]
|
||||
|
|
@ -275,15 +289,16 @@ public final class ResizableSheetComponent<ChildEnvironmentType: Sendable & Equa
|
|||
self.bottomEdgeEffectView.clipsToBounds = true
|
||||
self.bottomEdgeEffectView.layer.maskedCorners = [.layerMinXMaxYCorner, .layerMaxXMaxYCorner]
|
||||
self.bottomEdgeEffectView.layer.cornerRadius = 40.0
|
||||
self.bottomEdgeEffectView.isUserInteractionEnabled = false
|
||||
|
||||
self.contentView = ComponentView()
|
||||
|
||||
|
||||
super.init(frame: frame)
|
||||
|
||||
|
||||
self.addSubview(self.dimView)
|
||||
self.addSubview(self.containerView)
|
||||
self.containerView.layer.addSublayer(self.backgroundLayer)
|
||||
|
||||
|
||||
self.scrollView.delaysContentTouches = true
|
||||
self.scrollView.canCancelContentTouches = true
|
||||
self.scrollView.clipsToBounds = false
|
||||
|
|
@ -296,34 +311,39 @@ public final class ResizableSheetComponent<ChildEnvironmentType: Sendable & Equa
|
|||
self.scrollView.scrollsToTop = false
|
||||
self.scrollView.delegate = self
|
||||
self.scrollView.clipsToBounds = true
|
||||
|
||||
|
||||
self.containerView.addSubview(self.scrollContentClippingView)
|
||||
self.scrollContentClippingView.addSubview(self.scrollView)
|
||||
|
||||
|
||||
self.scrollView.addSubview(self.scrollContentView)
|
||||
|
||||
|
||||
self.containerView.addSubview(self.navigationBarContainer)
|
||||
self.containerView.addSubview(self.bottomContainer)
|
||||
|
||||
|
||||
self.dimView.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.dimTapGesture(_:))))
|
||||
|
||||
|
||||
let dismissPanGesture = UIPanGestureRecognizer(target: self, action: #selector(self.dismissPanGesture(_:)))
|
||||
dismissPanGesture.maximumNumberOfTouches = 1
|
||||
dismissPanGesture.delegate = self
|
||||
self.addGestureRecognizer(dismissPanGesture)
|
||||
self.dismissPanGesture = dismissPanGesture
|
||||
}
|
||||
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
|
||||
deinit {
|
||||
self.registeredExternalState?.trackedScrollViewUpdated = nil
|
||||
self.setTrackedScrollView(nil)
|
||||
}
|
||||
|
||||
public func scrollViewDidScroll(_ scrollView: UIScrollView) {
|
||||
if !self.ignoreScrolling {
|
||||
self.updateScrolling(transition: .immediate)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
|
||||
if !self.bounds.contains(point) {
|
||||
return nil
|
||||
|
|
@ -331,7 +351,7 @@ public final class ResizableSheetComponent<ChildEnvironmentType: Sendable & Equa
|
|||
if !self.backgroundLayer.frame.contains(point) {
|
||||
return self.dimView
|
||||
}
|
||||
|
||||
|
||||
if let result = self.navigationBarContainer.hitTest(self.convert(point, to: self.navigationBarContainer), with: event) {
|
||||
return result
|
||||
}
|
||||
|
|
@ -341,7 +361,7 @@ public final class ResizableSheetComponent<ChildEnvironmentType: Sendable & Equa
|
|||
let result = super.hitTest(point, with: event)
|
||||
return result
|
||||
}
|
||||
|
||||
|
||||
override public func gestureRecognizerShouldBegin(_ gestureRecognizer: UIGestureRecognizer) -> Bool {
|
||||
if gestureRecognizer === self.dismissPanGesture {
|
||||
let pan = gestureRecognizer as! UIPanGestureRecognizer
|
||||
|
|
@ -352,22 +372,25 @@ public final class ResizableSheetComponent<ChildEnvironmentType: Sendable & Equa
|
|||
}
|
||||
return true
|
||||
}
|
||||
|
||||
|
||||
public func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool {
|
||||
if gestureRecognizer === self.dismissPanGesture {
|
||||
if otherGestureRecognizer === self.scrollView.panGestureRecognizer {
|
||||
return true
|
||||
}
|
||||
if otherGestureRecognizer === self.trackedScrollView?.panGestureRecognizer {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
|
||||
@objc private func dimTapGesture(_ recognizer: UITapGestureRecognizer) {
|
||||
if case .ended = recognizer.state {
|
||||
self.dismissAnimated()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public func dismissAnimated() {
|
||||
guard let environment = self.environment else {
|
||||
return
|
||||
|
|
@ -375,16 +398,16 @@ public final class ResizableSheetComponent<ChildEnvironmentType: Sendable & Equa
|
|||
self.endEditing(true)
|
||||
environment.dismiss(true)
|
||||
}
|
||||
|
||||
|
||||
private func updateDismissTranslation(_ translation: CGFloat) {
|
||||
self.dismissTranslation = translation
|
||||
self.updateScrolling(transition: .immediate)
|
||||
|
||||
|
||||
let maxAlphaDistance = max(1.0, self.bounds.height * 0.9)
|
||||
let alpha = 1.0 - min(1.0, translation / maxAlphaDistance)
|
||||
self.dimView.alpha = alpha
|
||||
}
|
||||
|
||||
|
||||
private func resetDismissTranslation(animated: Bool) {
|
||||
self.dismissTranslation = 0.0
|
||||
if animated {
|
||||
|
|
@ -396,12 +419,12 @@ public final class ResizableSheetComponent<ChildEnvironmentType: Sendable & Equa
|
|||
self.updateScrolling(transition: .immediate)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@objc private func dismissPanGesture(_ recognizer: UIPanGestureRecognizer) {
|
||||
guard let component = self.component else {
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
let translation = recognizer.translation(in: self)
|
||||
switch recognizer.state {
|
||||
case .began:
|
||||
|
|
@ -414,7 +437,7 @@ public final class ResizableSheetComponent<ChildEnvironmentType: Sendable & Equa
|
|||
self.dismissStartTranslation = translation.y
|
||||
self.scrollView.isScrollEnabled = false
|
||||
}
|
||||
|
||||
|
||||
let start = self.dismissStartTranslation ?? translation.y
|
||||
let dismissOffset = max(0.0, translation.y - start)
|
||||
self.scrollView.contentOffset = .zero
|
||||
|
|
@ -430,10 +453,10 @@ public final class ResizableSheetComponent<ChildEnvironmentType: Sendable & Equa
|
|||
let currentOffset = self.dismissTranslation
|
||||
let threshold = min(180.0, self.bounds.height * 0.25)
|
||||
let shouldDismiss = currentOffset > threshold || velocityY > 1000.0
|
||||
|
||||
|
||||
self.isDismissingInteractively = false
|
||||
self.scrollView.isScrollEnabled = !component.isFullscreen
|
||||
|
||||
|
||||
if shouldDismiss {
|
||||
let animateOffset = self.bounds.height - self.backgroundLayer.frame.minY
|
||||
let initialVelocity = animateOffset > 0.0 ? max(0.0, velocityY) / animateOffset : 0.0
|
||||
|
|
@ -448,73 +471,170 @@ public final class ResizableSheetComponent<ChildEnvironmentType: Sendable & Equa
|
|||
break
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private func trackedScrollViewTopOffset(_ scrollView: UIScrollView) -> CGFloat {
|
||||
return -scrollView.contentInset.top
|
||||
}
|
||||
|
||||
private func isSheetFullyExpanded(itemLayout: ItemLayout) -> Bool {
|
||||
return itemLayout.topInset <= 0.5 || self.scrollView.contentOffset.y >= itemLayout.topInset - 0.5
|
||||
}
|
||||
|
||||
private func pinTrackedScrollViewToTop(_ scrollView: UIScrollView) {
|
||||
let topOffset = self.trackedScrollViewTopOffset(scrollView)
|
||||
if abs(scrollView.contentOffset.y - topOffset) > 0.5 {
|
||||
scrollView.contentOffset = CGPoint(x: scrollView.contentOffset.x, y: topOffset)
|
||||
}
|
||||
}
|
||||
|
||||
private func updateTrackedScrollViewLock() {
|
||||
guard let component = self.component, let itemLayout = self.itemLayout, let trackedScrollView = self.trackedScrollView else {
|
||||
return
|
||||
}
|
||||
trackedScrollView.isScrollEnabled = true
|
||||
if component.isFullscreen {
|
||||
return
|
||||
}
|
||||
if !self.isSheetFullyExpanded(itemLayout: itemLayout) || self.isDismissingInteractively {
|
||||
self.pinTrackedScrollViewToTop(trackedScrollView)
|
||||
}
|
||||
}
|
||||
|
||||
private func setTrackedScrollView(_ scrollView: UIScrollView?) {
|
||||
if self.trackedScrollView === scrollView {
|
||||
self.updateTrackedScrollViewLock()
|
||||
return
|
||||
}
|
||||
|
||||
if let trackedScrollView = self.trackedScrollView {
|
||||
trackedScrollView.panGestureRecognizer.removeTarget(self, action: #selector(self.trackedScrollViewPanGesture(_:)))
|
||||
trackedScrollView.isScrollEnabled = true
|
||||
}
|
||||
|
||||
self.trackedScrollView = scrollView
|
||||
|
||||
if let scrollView = scrollView {
|
||||
scrollView.panGestureRecognizer.addTarget(self, action: #selector(self.trackedScrollViewPanGesture(_:)))
|
||||
}
|
||||
|
||||
self.trackedScrollViewWasAtTopOnGestureBegan = false
|
||||
self.updateTrackedScrollViewLock()
|
||||
}
|
||||
|
||||
@objc private func trackedScrollViewPanGesture(_ recognizer: UIPanGestureRecognizer) {
|
||||
guard let component = self.component, let itemLayout = self.itemLayout, let trackedScrollView = recognizer.view as? UIScrollView else {
|
||||
return
|
||||
}
|
||||
guard !component.isFullscreen, itemLayout.topInset > 0.5 else {
|
||||
return
|
||||
}
|
||||
|
||||
let topOffset = self.trackedScrollViewTopOffset(trackedScrollView)
|
||||
let isAtTop = trackedScrollView.contentOffset.y <= topOffset + 8.0
|
||||
|
||||
switch recognizer.state {
|
||||
case .began, .changed:
|
||||
if recognizer.state == .began {
|
||||
self.trackedScrollViewWasAtTopOnGestureBegan = isAtTop
|
||||
}
|
||||
let translation = recognizer.translation(in: trackedScrollView)
|
||||
let currentSheetOffset = min(max(0.0, self.scrollView.contentOffset.y), itemLayout.topInset)
|
||||
let shouldExpandSheet = self.trackedScrollViewWasAtTopOnGestureBegan && currentSheetOffset < itemLayout.topInset - 0.5
|
||||
|
||||
if translation.y < 0.0 && shouldExpandSheet {
|
||||
let consumedOffset = min(itemLayout.topInset - currentSheetOffset, -translation.y)
|
||||
if consumedOffset > 0.0 {
|
||||
self.scrollView.contentOffset = CGPoint(x: self.scrollView.contentOffset.x, y: currentSheetOffset + consumedOffset)
|
||||
self.pinTrackedScrollViewToTop(trackedScrollView)
|
||||
recognizer.setTranslation(.zero, in: trackedScrollView)
|
||||
}
|
||||
} else if translation.y > 0.0 && isAtTop && currentSheetOffset > 0.5 {
|
||||
let consumedOffset = min(currentSheetOffset, translation.y)
|
||||
if consumedOffset > 0.0 {
|
||||
self.scrollView.contentOffset = CGPoint(x: self.scrollView.contentOffset.x, y: currentSheetOffset - consumedOffset)
|
||||
self.pinTrackedScrollViewToTop(trackedScrollView)
|
||||
recognizer.setTranslation(.zero, in: trackedScrollView)
|
||||
}
|
||||
} else if self.isDismissingInteractively || (translation.y > 0.0 && isAtTop && currentSheetOffset <= 0.5) {
|
||||
self.pinTrackedScrollViewToTop(trackedScrollView)
|
||||
}
|
||||
case .ended, .cancelled, .failed:
|
||||
self.trackedScrollViewWasAtTopOnGestureBegan = false
|
||||
if !self.isSheetFullyExpanded(itemLayout: itemLayout) || self.isDismissingInteractively {
|
||||
self.pinTrackedScrollViewToTop(trackedScrollView)
|
||||
}
|
||||
default:
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
private func updateScrolling(transition: ComponentTransition) {
|
||||
guard let itemLayout = self.itemLayout, let component = self.component else {
|
||||
guard let itemLayout = self.itemLayout, let component = self.component, let environment = self.environment else {
|
||||
return
|
||||
}
|
||||
var topOffset = -self.scrollView.bounds.minY + itemLayout.topInset
|
||||
topOffset = max(0.0, topOffset)
|
||||
transition.setTransform(layer: self.backgroundLayer, transform: CATransform3DMakeTranslation(0.0, topOffset + itemLayout.containerInset, 0.0))
|
||||
|
||||
|
||||
transition.setPosition(view: self.navigationBarContainer, position: CGPoint(x: 0.0, y: topOffset + itemLayout.containerInset))
|
||||
|
||||
|
||||
var topOffsetFraction = self.scrollView.bounds.minY / 100.0
|
||||
topOffsetFraction = max(0.0, min(1.0, topOffsetFraction))
|
||||
|
||||
if component.isFullscreen {
|
||||
|
||||
if component.isFullscreen || environment.inputHeight > 0.0 {
|
||||
topOffsetFraction = 1.0
|
||||
}
|
||||
|
||||
#if DEBUG// && false
|
||||
if "".isEmpty {
|
||||
topOffsetFraction = 1.0
|
||||
}
|
||||
#endif
|
||||
|
||||
// #if DEBUG// && false
|
||||
// if "".isEmpty {
|
||||
// topOffsetFraction = 1.0
|
||||
// }
|
||||
// #endif
|
||||
|
||||
let minScale: CGFloat = itemLayout.isTablet ? 1.0 : (itemLayout.containerSize.width - 6.0 * 2.0) / itemLayout.containerSize.width
|
||||
let minScaledTranslation: CGFloat = itemLayout.isTablet ? 0.0 : (itemLayout.containerSize.height - itemLayout.containerSize.height * minScale) * 0.5 - 6.0
|
||||
let minScaledCornerRadius: CGFloat = itemLayout.containerCornerRadius
|
||||
|
||||
|
||||
let scale = minScale * (1.0 - topOffsetFraction) + 1.0 * topOffsetFraction
|
||||
let scaledTranslation = minScaledTranslation * (1.0 - topOffsetFraction)
|
||||
let scaledCornerRadius = minScaledCornerRadius * (1.0 - topOffsetFraction) + itemLayout.containerCornerRadius * topOffsetFraction
|
||||
|
||||
|
||||
var containerTransform = CATransform3DIdentity
|
||||
containerTransform = CATransform3DTranslate(containerTransform, 0.0, scaledTranslation, 0.0)
|
||||
containerTransform = CATransform3DScale(containerTransform, scale, scale, scale)
|
||||
containerTransform = CATransform3DTranslate(containerTransform, 0.0, self.dismissTranslation, 0.0)
|
||||
transition.setTransform(view: self.containerView, transform: containerTransform)
|
||||
transition.setCornerRadius(layer: self.containerView.layer, cornerRadius: scaledCornerRadius)
|
||||
|
||||
|
||||
if component.isFullscreen {
|
||||
transition.setBounds(view: self.scrollView, bounds: CGRect(origin: .zero, size: self.scrollView.bounds.size))
|
||||
self.scrollView.isScrollEnabled = false
|
||||
} else {
|
||||
self.scrollView.isScrollEnabled = !self.isDismissingInteractively
|
||||
}
|
||||
|
||||
|
||||
var bounds = self.scrollView.bounds
|
||||
bounds.size.width = itemLayout.fillingSize
|
||||
self.environment?.boundsUpdated.invoke(ResizableSheetComponentEnvironment.BoundsUpdate(bounds: bounds, isInteractive: self.scrollView.isTracking))
|
||||
self.updateTrackedScrollViewLock()
|
||||
}
|
||||
|
||||
|
||||
private var didPlayAppearanceAnimation = false
|
||||
func animateIn() {
|
||||
self.didPlayAppearanceAnimation = true
|
||||
|
||||
|
||||
self.dimView.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.3)
|
||||
let animateOffset: CGFloat = self.bounds.height - self.backgroundLayer.frame.minY
|
||||
self.containerView.layer.animatePosition(from: CGPoint(x: 0.0, y: animateOffset), to: CGPoint(), duration: 0.5, timingFunction: kCAMediaTimingFunctionSpring, additive: true)
|
||||
}
|
||||
|
||||
|
||||
func animateOut(initialVelocity: CGFloat? = nil, completion: @escaping () -> Void) {
|
||||
let animateOffset: CGFloat = self.bounds.height - self.backgroundLayer.frame.minY
|
||||
|
||||
|
||||
self.dimView.layer.animateAlpha(from: self.dimView.alpha, to: 0.0, duration: 0.3, removeOnCompletion: false)
|
||||
if let initialVelocity = initialVelocity {
|
||||
let transition = ContainedViewLayoutTransition.animated(duration: 0.35, curve: .customSpring(damping: 124.0, initialVelocity: initialVelocity))
|
||||
|
||||
|
||||
transition.updatePosition(layer: self.containerView.layer, position: CGPoint(x: self.containerView.layer.position.x, y: self.containerView.layer.position.y + animateOffset), completion: { _ in
|
||||
completion()
|
||||
})
|
||||
|
|
@ -525,13 +645,13 @@ public final class ResizableSheetComponent<ChildEnvironmentType: Sendable & Equa
|
|||
})
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
func update(component: ResizableSheetComponent<ChildEnvironmentType>, availableSize: CGSize, state: EmptyComponentState, environment: Environment<EnvironmentType>, transition: ComponentTransition) -> CGSize {
|
||||
self.isUpdating = true
|
||||
defer {
|
||||
self.isUpdating = false
|
||||
}
|
||||
|
||||
|
||||
let sheetEnvironment = environment[ResizableSheetComponentEnvironment.self].value
|
||||
component.animateOut.connect { [weak self] completion in
|
||||
guard let self else {
|
||||
|
|
@ -541,9 +661,9 @@ public final class ResizableSheetComponent<ChildEnvironmentType: Sendable & Equa
|
|||
completion(Void())
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
let resetScrolling = self.scrollView.bounds.width != availableSize.width
|
||||
|
||||
|
||||
let fillingSize: CGFloat
|
||||
if case .regular = sheetEnvironment.metrics.widthClass {
|
||||
fillingSize = min(availableSize.width, 414.0) - sheetEnvironment.safeInsets.left * 2.0
|
||||
|
|
@ -555,18 +675,30 @@ public final class ResizableSheetComponent<ChildEnvironmentType: Sendable & Equa
|
|||
self.component = component
|
||||
self.state = state
|
||||
self.environment = sheetEnvironment
|
||||
|
||||
self.dimView.backgroundColor = UIColor(white: 0.0, alpha: 0.5)
|
||||
|
||||
|
||||
if self.registeredExternalState !== component.externalState {
|
||||
self.registeredExternalState?.trackedScrollViewUpdated = nil
|
||||
self.registeredExternalState = component.externalState
|
||||
if let externalState = component.externalState {
|
||||
externalState.trackedScrollViewUpdated = { [weak self] scrollView in
|
||||
self?.setTrackedScrollView(scrollView)
|
||||
}
|
||||
} else {
|
||||
self.setTrackedScrollView(nil)
|
||||
}
|
||||
}
|
||||
|
||||
self.dimView.backgroundColor = UIColor(white: 0.0, alpha: 0.4)
|
||||
|
||||
let backgroundColor: UIColor
|
||||
switch component.backgroundColor {
|
||||
case let .color(color):
|
||||
backgroundColor = color
|
||||
self.backgroundLayer.backgroundColor = backgroundColor.cgColor
|
||||
}
|
||||
|
||||
|
||||
transition.setFrame(view: self.dimView, frame: CGRect(origin: CGPoint(), size: availableSize))
|
||||
|
||||
|
||||
var containerSize: CGSize
|
||||
if !"".isEmpty, sheetEnvironment.isCentered {
|
||||
let verticalInset: CGFloat = 44.0
|
||||
|
|
@ -579,13 +711,13 @@ public final class ResizableSheetComponent<ChildEnvironmentType: Sendable & Equa
|
|||
} else {
|
||||
containerSize = CGSize(width: fillingSize, height: .greatestFiniteMagnitude)
|
||||
}
|
||||
|
||||
|
||||
var containerInset: CGFloat = sheetEnvironment.statusBarHeight + 10.0
|
||||
if component.isFullscreen {
|
||||
containerInset = 0.0
|
||||
}
|
||||
let clippingY: CGFloat
|
||||
|
||||
|
||||
self.contentView.parentState = state
|
||||
let contentViewSize = self.contentView.update(
|
||||
transition: transition,
|
||||
|
|
@ -596,17 +728,24 @@ public final class ResizableSheetComponent<ChildEnvironmentType: Sendable & Equa
|
|||
containerSize: containerSize
|
||||
)
|
||||
component.externalState?.contentHeight = contentViewSize.height
|
||||
|
||||
|
||||
if let contentView = self.contentView.view {
|
||||
if contentView.superview == nil {
|
||||
self.scrollContentView.addSubview(contentView)
|
||||
}
|
||||
transition.setFrame(view: contentView, frame: CGRect(origin: CGPoint(x: rawSideInset, y: 0.0), size: contentViewSize))
|
||||
}
|
||||
|
||||
|
||||
let contentHeight = contentViewSize.height
|
||||
let initialContentHeight = contentHeight
|
||||
|
||||
let initialContentHeight: CGFloat
|
||||
if component.isFullscreen || sheetEnvironment.inputHeight > 0.0 {
|
||||
initialContentHeight = contentHeight
|
||||
} else if let defaultHeight = component.defaultHeight {
|
||||
initialContentHeight = min(contentHeight, max(0.0, defaultHeight))
|
||||
} else {
|
||||
initialContentHeight = contentHeight
|
||||
}
|
||||
|
||||
let edgeEffectHeight: CGFloat = 80.0
|
||||
let edgeEffectFrame = CGRect(origin: CGPoint(x: rawSideInset, y: 0.0), size: CGSize(width: fillingSize, height: edgeEffectHeight))
|
||||
transition.setFrame(view: self.topEdgeEffectView, frame: edgeEffectFrame)
|
||||
|
|
@ -615,7 +754,7 @@ public final class ResizableSheetComponent<ChildEnvironmentType: Sendable & Equa
|
|||
self.navigationBarContainer.insertSubview(self.topEdgeEffectView, at: 0)
|
||||
}
|
||||
self.topEdgeEffectView.isHidden = !component.hasTopEdgeEffect
|
||||
|
||||
|
||||
if let titleItem = component.titleItem {
|
||||
let titleItemView: ComponentView<Empty>
|
||||
if let current = self.titleItemView {
|
||||
|
|
@ -624,12 +763,12 @@ public final class ResizableSheetComponent<ChildEnvironmentType: Sendable & Equa
|
|||
titleItemView = ComponentView<Empty>()
|
||||
self.titleItemView = titleItemView
|
||||
}
|
||||
|
||||
|
||||
let titleItemSize = titleItemView.update(
|
||||
transition: transition,
|
||||
component: titleItem,
|
||||
environment: {},
|
||||
containerSize: CGSize(width: containerSize.width - 66.0 * 2.0, height: 66.0)
|
||||
containerSize: CGSize(width: containerSize.width - 72.0 * 2.0, height: 66.0)
|
||||
)
|
||||
let titleItemFrame = CGRect(origin: CGPoint(x: rawSideInset + floorToScreenPixels((containerSize.width - titleItemSize.width)) / 2.0, y: floorToScreenPixels(38.0 - titleItemSize.height * 0.5)), size: titleItemSize)
|
||||
if let view = titleItemView.view {
|
||||
|
|
@ -642,7 +781,7 @@ public final class ResizableSheetComponent<ChildEnvironmentType: Sendable & Equa
|
|||
self.titleItemView = nil
|
||||
titleItemView.view?.removeFromSuperview()
|
||||
}
|
||||
|
||||
|
||||
if let leftItem = component.leftItem {
|
||||
var leftItemTransition = transition
|
||||
let leftItemView: ComponentView<Empty>
|
||||
|
|
@ -653,7 +792,7 @@ public final class ResizableSheetComponent<ChildEnvironmentType: Sendable & Equa
|
|||
leftItemView = ComponentView<Empty>()
|
||||
self.leftItemView = leftItemView
|
||||
}
|
||||
|
||||
|
||||
let leftItemSize = leftItemView.update(
|
||||
transition: leftItemTransition,
|
||||
component: leftItem,
|
||||
|
|
@ -664,7 +803,7 @@ public final class ResizableSheetComponent<ChildEnvironmentType: Sendable & Equa
|
|||
if let view = leftItemView.view {
|
||||
if view.superview == nil {
|
||||
self.navigationBarContainer.addSubview(view)
|
||||
|
||||
|
||||
if !transition.animation.isImmediate {
|
||||
view.layer.animateScale(from: 0.01, to: 1.0, duration: 0.25)
|
||||
view.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.25)
|
||||
|
|
@ -683,7 +822,7 @@ public final class ResizableSheetComponent<ChildEnvironmentType: Sendable & Equa
|
|||
leftItemView.view?.removeFromSuperview()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
if let rightItem = component.rightItem {
|
||||
var rightItemTransition = transition
|
||||
let rightItemView: ComponentView<Empty>
|
||||
|
|
@ -694,7 +833,7 @@ public final class ResizableSheetComponent<ChildEnvironmentType: Sendable & Equa
|
|||
rightItemView = ComponentView<Empty>()
|
||||
self.rightItemView = rightItemView
|
||||
}
|
||||
|
||||
|
||||
let rightItemSize = rightItemView.update(
|
||||
transition: rightItemTransition,
|
||||
component: rightItem,
|
||||
|
|
@ -705,7 +844,7 @@ public final class ResizableSheetComponent<ChildEnvironmentType: Sendable & Equa
|
|||
if let view = rightItemView.view {
|
||||
if view.superview == nil {
|
||||
self.navigationBarContainer.addSubview(view)
|
||||
|
||||
|
||||
if !transition.animation.isImmediate {
|
||||
view.layer.animateScale(from: 0.01, to: 1.0, duration: 0.25)
|
||||
view.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.25)
|
||||
|
|
@ -724,14 +863,15 @@ public final class ResizableSheetComponent<ChildEnvironmentType: Sendable & Equa
|
|||
rightItemView.view?.removeFromSuperview()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
var bottomInsets = ContainerViewLayout.concentricInsets(bottomInset: sheetEnvironment.safeInsets.bottom, innerDiameter: 52.0, sideInset: 30.0)
|
||||
if sheetEnvironment.inputHeight > 0.0 {
|
||||
bottomInsets.left = 16.0
|
||||
bottomInsets.right = 16.0
|
||||
bottomInsets.bottom = sheetEnvironment.inputHeight + 8.0
|
||||
}
|
||||
|
||||
|
||||
var bottomEdgeEffectHeight = edgeEffectHeight
|
||||
if let bottomItem = component.bottomItem {
|
||||
var bottomItemTransition = transition
|
||||
let bottomItemView: ComponentView<Empty>
|
||||
|
|
@ -742,7 +882,7 @@ public final class ResizableSheetComponent<ChildEnvironmentType: Sendable & Equa
|
|||
bottomItemView = ComponentView<Empty>()
|
||||
self.bottomItemView = bottomItemView
|
||||
}
|
||||
|
||||
|
||||
let bottomItemSize = bottomItemView.update(
|
||||
transition: bottomItemTransition,
|
||||
component: bottomItem,
|
||||
|
|
@ -753,7 +893,7 @@ public final class ResizableSheetComponent<ChildEnvironmentType: Sendable & Equa
|
|||
if let view = bottomItemView.view {
|
||||
if view.superview == nil {
|
||||
self.bottomContainer.addSubview(view)
|
||||
|
||||
|
||||
if !transition.animation.isImmediate {
|
||||
view.layer.animateScale(from: 0.01, to: 1.0, duration: 0.25)
|
||||
view.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.25)
|
||||
|
|
@ -761,6 +901,7 @@ public final class ResizableSheetComponent<ChildEnvironmentType: Sendable & Equa
|
|||
}
|
||||
bottomItemTransition.setFrame(view: view, frame: bottomItemFrame)
|
||||
}
|
||||
bottomEdgeEffectHeight = bottomItemSize.height + 36.0
|
||||
} else if let bottomItemView = self.bottomItemView {
|
||||
self.bottomItemView = nil
|
||||
if !transition.animation.isImmediate {
|
||||
|
|
@ -772,16 +913,16 @@ public final class ResizableSheetComponent<ChildEnvironmentType: Sendable & Equa
|
|||
bottomItemView.view?.removeFromSuperview()
|
||||
}
|
||||
}
|
||||
|
||||
let bottomEdgeEffectFrame = CGRect(origin: CGPoint(x: rawSideInset, y: availableSize.height - bottomInsets.bottom - edgeEffectHeight), size: CGSize(width: fillingSize, height: edgeEffectHeight + bottomInsets.bottom))
|
||||
|
||||
let bottomEdgeEffectFrame = CGRect(origin: CGPoint(x: rawSideInset, y: availableSize.height - bottomInsets.bottom - bottomEdgeEffectHeight), size: CGSize(width: fillingSize, height: bottomEdgeEffectHeight + bottomInsets.bottom))
|
||||
transition.setFrame(view: self.bottomEdgeEffectView, frame: bottomEdgeEffectFrame)
|
||||
self.bottomEdgeEffectView.update(content: .clear, blur: true, alpha: 1.0, rect: bottomEdgeEffectFrame, edge: .bottom, edgeSize: edgeEffectHeight, transition: transition)
|
||||
self.bottomEdgeEffectView.update(content: backgroundColor, blur: true, alpha: 1.0, rect: bottomEdgeEffectFrame, edge: .bottom, edgeSize: bottomEdgeEffectHeight, transition: transition)
|
||||
if self.bottomEdgeEffectView.superview == nil {
|
||||
self.bottomContainer.insertSubview(self.bottomEdgeEffectView, at: 0)
|
||||
}
|
||||
transition.setAlpha(view: self.bottomContainer, alpha: component.bottomItem != nil ? 1.0 : 0.0)
|
||||
|
||||
|
||||
|
||||
|
||||
clippingY = availableSize.height
|
||||
|
||||
var topInset: CGFloat = max(0.0, availableSize.height - containerInset - initialContentHeight - sheetEnvironment.inputHeight)
|
||||
|
|
@ -790,21 +931,21 @@ public final class ResizableSheetComponent<ChildEnvironmentType: Sendable & Equa
|
|||
}
|
||||
|
||||
let scrollContentHeight = max(topInset + contentHeight + containerInset + sheetEnvironment.inputHeight, availableSize.height - containerInset)
|
||||
|
||||
|
||||
self.scrollContentClippingView.layer.cornerRadius = 38.0
|
||||
|
||||
let containerCornerRadius = max(22.0, sheetEnvironment.deviceMetrics.screenCornerRadius)
|
||||
self.itemLayout = ItemLayout(containerSize: availableSize, containerInset: containerInset, containerCornerRadius: containerCornerRadius, bottomInset: sheetEnvironment.safeInsets.bottom, topInset: topInset, fillingSize: fillingSize, isTablet: sheetEnvironment.metrics.isTablet)
|
||||
|
||||
transition.setFrame(view: self.scrollContentView, frame: CGRect(origin: CGPoint(x: 0.0, y: topInset + containerInset), size: CGSize(width: availableSize.width, height: contentHeight)))
|
||||
|
||||
|
||||
transition.setPosition(layer: self.backgroundLayer, position: CGPoint(x: availableSize.width / 2.0, y: availableSize.height / 2.0))
|
||||
transition.setBounds(layer: self.backgroundLayer, bounds: CGRect(origin: CGPoint(), size: CGSize(width: fillingSize, height: availableSize.height)))
|
||||
|
||||
|
||||
let scrollClippingFrame = CGRect(origin: CGPoint(x: 0.0, y: containerInset), size: CGSize(width: availableSize.width, height: clippingY - containerInset))
|
||||
transition.setPosition(view: self.scrollContentClippingView, position: scrollClippingFrame.center)
|
||||
transition.setBounds(view: self.scrollContentClippingView, bounds: CGRect(origin: CGPoint(x: scrollClippingFrame.minX, y: scrollClippingFrame.minY), size: scrollClippingFrame.size))
|
||||
|
||||
|
||||
self.ignoreScrolling = true
|
||||
transition.setFrame(view: self.scrollView, frame: CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: availableSize.width, height: availableSize.height)))
|
||||
let contentSize = CGSize(width: availableSize.width, height: scrollContentHeight)
|
||||
|
|
@ -816,22 +957,22 @@ public final class ResizableSheetComponent<ChildEnvironmentType: Sendable & Equa
|
|||
}
|
||||
self.ignoreScrolling = false
|
||||
self.updateScrolling(transition: transition)
|
||||
|
||||
|
||||
transition.setPosition(view: self.containerView, position: CGRect(origin: CGPoint(), size: availableSize).center)
|
||||
transition.setBounds(view: self.containerView, bounds: CGRect(origin: CGPoint(), size: availableSize))
|
||||
|
||||
|
||||
if sheetEnvironment.isDisplaying && !self.didPlayAppearanceAnimation {
|
||||
self.animateIn()
|
||||
}
|
||||
|
||||
|
||||
return availableSize
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public func makeView() -> View {
|
||||
return View(frame: CGRect())
|
||||
}
|
||||
|
||||
|
||||
public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment<EnvironmentType>, transition: ComponentTransition) -> CGSize {
|
||||
return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -602,6 +602,7 @@ public enum ContextControllerTip: Equatable {
|
|||
case starsReactions(topCount: Int)
|
||||
case videoProcessing
|
||||
case collageReordering
|
||||
case deleteReaction
|
||||
|
||||
public static func ==(lhs: ContextControllerTip, rhs: ContextControllerTip) -> Bool {
|
||||
switch lhs {
|
||||
|
|
@ -611,6 +612,12 @@ public enum ContextControllerTip: Equatable {
|
|||
} else {
|
||||
return false
|
||||
}
|
||||
case .deleteReaction:
|
||||
if case .deleteReaction = rhs {
|
||||
return true
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
case .quoteSelection:
|
||||
if case .quoteSelection = rhs {
|
||||
return true
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ import TelegramUIPreferences
|
|||
import AccountContext
|
||||
import ContextUI
|
||||
|
||||
public final class InstantPageContentNode : ASDisplayNode {
|
||||
public final class InstantPageContentNode : ASDisplayNode, InstantPageExternalMediaDimensionsNode {
|
||||
private let context: AccountContext
|
||||
private let strings: PresentationStrings
|
||||
private let nameDisplayOrder: PresentationPersonNameOrder
|
||||
|
|
@ -29,13 +29,20 @@ public final class InstantPageContentNode : ASDisplayNode {
|
|||
var distanceThresholdGroupCount: [Int: Int] = [:]
|
||||
|
||||
var visibleTiles: [Int: InstantPageTileNode] = [:]
|
||||
var visibleItemsWithNodes: [Int: InstantPageNode] = [:]
|
||||
public var visibleItemsWithNodes: [Int: InstantPageNode] = [:]
|
||||
|
||||
var currentWebEmbedHeights: [Int : CGFloat] = [:]
|
||||
var currentExpandedDetails: [Int : Bool]?
|
||||
public var currentExpandedDetails: [Int : Bool]?
|
||||
var currentDetailsItems: [InstantPageDetailsItem] = []
|
||||
|
||||
var requestLayoutUpdate: ((Bool) -> Void)?
|
||||
public var updateExternalMediaDimensions: ((EngineMedia.Id, PixelDimensions) -> Void)? {
|
||||
didSet {
|
||||
for (_, itemNode) in self.visibleItemsWithNodes {
|
||||
self.applyExternalMediaDimensionsUpdater(to: itemNode)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var currentLayout: InstantPageLayout
|
||||
let contentSize: CGSize
|
||||
|
|
@ -223,6 +230,7 @@ public final class InstantPageContentNode : ASDisplayNode {
|
|||
topNode = newNode
|
||||
self.visibleItemsWithNodes[itemIndex] = newNode
|
||||
itemNode = newNode
|
||||
self.applyExternalMediaDimensionsUpdater(to: newNode)
|
||||
|
||||
if let itemNode = itemNode as? InstantPageDetailsNode {
|
||||
itemNode.requestLayoutUpdate = { [weak self] animated in
|
||||
|
|
@ -303,6 +311,16 @@ public final class InstantPageContentNode : ASDisplayNode {
|
|||
}
|
||||
}
|
||||
|
||||
private func applyExternalMediaDimensionsUpdater(to itemNode: InstantPageNode) {
|
||||
if let itemNode = itemNode as? InstantPageImageNode {
|
||||
itemNode.updateExternalMediaDimensions = self.updateExternalMediaDimensions
|
||||
} else if let itemNode = itemNode as? InstantPageDetailsNode {
|
||||
itemNode.contentNode.updateExternalMediaDimensions = self.updateExternalMediaDimensions
|
||||
} else if let itemNode = itemNode as? InstantPageSlideshowNode {
|
||||
itemNode.updateExternalMediaDimensions = self.updateExternalMediaDimensions
|
||||
}
|
||||
}
|
||||
|
||||
private func updateWebEmbedHeight(_ index: Int, _ height: CGFloat) {
|
||||
// let currentHeight = self.currentWebEmbedHeights[index]
|
||||
// if height != currentHeight {
|
||||
|
|
|
|||
|
|
@ -65,6 +65,8 @@ final class InstantPageControllerNode: ASDisplayNode, ASScrollViewDelegate {
|
|||
var currentWebEmbedHeights: [Int : CGFloat] = [:]
|
||||
var currentExpandedDetails: [Int : Bool]?
|
||||
var currentDetailsItems: [InstantPageDetailsItem] = []
|
||||
private var resolvedExternalMediaDimensions: [MediaId: PixelDimensions] = [:]
|
||||
private var pendingResolvedExternalMediaDimensions = Set<MediaId>()
|
||||
|
||||
var currentAccessibilityAreas: [AccessibilityAreaNode] = []
|
||||
|
||||
|
|
@ -79,6 +81,7 @@ final class InstantPageControllerNode: ASDisplayNode, ASScrollViewDelegate {
|
|||
private let loadProgressDisposable = MetaDisposable()
|
||||
|
||||
private let updateLayoutDisposable = MetaDisposable()
|
||||
private let updateExternalMediaDimensionsDisposable = MetaDisposable()
|
||||
|
||||
private var themeReferenceDate: Date?
|
||||
|
||||
|
|
@ -224,6 +227,7 @@ final class InstantPageControllerNode: ASDisplayNode, ASScrollViewDelegate {
|
|||
self.resolveUrlDisposable.dispose()
|
||||
self.loadWebpageDisposable.dispose()
|
||||
self.loadProgressDisposable.dispose()
|
||||
self.updateExternalMediaDimensionsDisposable.dispose()
|
||||
}
|
||||
|
||||
func update(settings: InstantPagePresentationSettings, themeSettings: PresentationThemeSettings?, strings: PresentationStrings) {
|
||||
|
|
@ -362,6 +366,8 @@ final class InstantPageControllerNode: ASDisplayNode, ASScrollViewDelegate {
|
|||
} else {
|
||||
self.webPage = nil
|
||||
}
|
||||
self.resolvedExternalMediaDimensions.removeAll()
|
||||
self.pendingResolvedExternalMediaDimensions.removeAll()
|
||||
if let anchor = anchor {
|
||||
self.initialAnchor = anchor.removingPercentEncoding
|
||||
} else if let state = state {
|
||||
|
|
@ -465,17 +471,12 @@ final class InstantPageControllerNode: ASDisplayNode, ASScrollViewDelegate {
|
|||
}
|
||||
|
||||
private func updateLayout() {
|
||||
guard let containerLayout = self.containerLayout, let (webPage, instantPage) = self.webPage, let theme = self.theme else {
|
||||
guard let containerLayout = self.containerLayout, let (webPage, instantPage) = self.resolvedWebPage(), let theme = self.theme else {
|
||||
return
|
||||
}
|
||||
|
||||
let currentLayout = instantPageLayoutForWebPage(webPage, instantPage: instantPage, userLocation: self.sourceLocation.userLocation, boundingWidth: containerLayout.size.width, safeInset: containerLayout.safeInsets.left, strings: self.strings, theme: theme, dateTimeFormat: self.dateTimeFormat, webEmbedHeights: self.currentWebEmbedHeights)
|
||||
|
||||
for (_, tileNode) in self.visibleTiles {
|
||||
tileNode.removeFromSupernode()
|
||||
}
|
||||
self.visibleTiles.removeAll()
|
||||
|
||||
let currentLayoutTiles = instantPageTilesFromLayout(currentLayout, boundingWidth: containerLayout.size.width)
|
||||
|
||||
var currentDetailsItems: [InstantPageDetailsItem] = []
|
||||
|
|
@ -665,6 +666,7 @@ final class InstantPageControllerNode: ASDisplayNode, ASScrollViewDelegate {
|
|||
topNode = newNode
|
||||
self.visibleItemsWithNodes[itemIndex] = newNode
|
||||
itemNode = newNode
|
||||
self.configureExternalMediaDimensionsUpdates(for: newNode)
|
||||
|
||||
if let itemNode = itemNode as? InstantPageDetailsNode {
|
||||
itemNode.requestLayoutUpdate = { [weak self] animated in
|
||||
|
|
@ -688,6 +690,10 @@ final class InstantPageControllerNode: ASDisplayNode, ASScrollViewDelegate {
|
|||
}
|
||||
}
|
||||
|
||||
if let itemNode = itemNode {
|
||||
self.configureExternalMediaDimensionsUpdates(for: itemNode)
|
||||
}
|
||||
|
||||
if let itemNode = itemNode as? InstantPageDetailsNode {
|
||||
itemNode.updateVisibleItems(visibleBounds: visibleBounds.offsetBy(dx: -itemNode.frame.minX, dy: -itemNode.frame.minY), animated: animated)
|
||||
}
|
||||
|
|
@ -718,8 +724,11 @@ final class InstantPageControllerNode: ASDisplayNode, ASScrollViewDelegate {
|
|||
topNode = tileNode
|
||||
self.visibleTiles[tileIndex] = tileNode
|
||||
} else {
|
||||
if visibleTiles[tileIndex]!.frame != tileFrame {
|
||||
transition.updateFrame(node: self.visibleTiles[tileIndex]!, frame: tileFrame)
|
||||
if let tileNode = self.visibleTiles[tileIndex] {
|
||||
tileNode.update(tile: tile, backgroundColor: theme.pageBackgroundColor)
|
||||
if tileNode.frame != tileFrame {
|
||||
transition.updateFrame(node: tileNode, frame: tileFrame)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1020,6 +1029,388 @@ final class InstantPageControllerNode: ASDisplayNode, ASScrollViewDelegate {
|
|||
return nil
|
||||
}
|
||||
|
||||
private func configureExternalMediaDimensionsUpdates(for itemNode: InstantPageNode) {
|
||||
let update: (MediaId, PixelDimensions) -> Void = { [weak self] mediaId, dimensions in
|
||||
self?.updateExternalMediaDimensions(mediaId, dimensions)
|
||||
}
|
||||
if let itemNode = itemNode as? InstantPageExternalMediaDimensionsNode {
|
||||
itemNode.updateExternalMediaDimensions = update
|
||||
}
|
||||
if let itemNode = itemNode as? InstantPageDetailsNode {
|
||||
itemNode.contentNode.updateExternalMediaDimensions = update
|
||||
}
|
||||
}
|
||||
|
||||
private func updateExternalMediaDimensions(_ mediaId: MediaId, _ dimensions: PixelDimensions) {
|
||||
if self.resolvedExternalMediaDimensions[mediaId] == dimensions {
|
||||
return
|
||||
}
|
||||
self.resolvedExternalMediaDimensions[mediaId] = dimensions
|
||||
self.pendingResolvedExternalMediaDimensions.insert(mediaId)
|
||||
|
||||
let signal: Signal<Void, NoError> = (.complete() |> delay(0.08, queue: Queue.mainQueue()))
|
||||
self.updateExternalMediaDimensionsDisposable.set(signal.start(completed: { [weak self] in
|
||||
self?.relayoutForResolvedExternalMediaDimensions()
|
||||
}))
|
||||
}
|
||||
|
||||
private func relayoutForResolvedExternalMediaDimensions() {
|
||||
guard !self.pendingResolvedExternalMediaDimensions.isEmpty else {
|
||||
return
|
||||
}
|
||||
|
||||
let mediaIds = Array(self.pendingResolvedExternalMediaDimensions)
|
||||
self.pendingResolvedExternalMediaDimensions.removeAll()
|
||||
|
||||
let detailsStateMaps = self.captureExpandedDetailsStateMaps()
|
||||
let viewportTop = self.scrollNode.view.contentOffset.y + self.scrollNode.view.contentInset.top
|
||||
var oldFrames: [MediaId: CGRect] = [:]
|
||||
for mediaId in mediaIds {
|
||||
if let frame = self.effectiveFrameForMedia(mediaId, detailsStateMaps: detailsStateMaps) {
|
||||
oldFrames[mediaId] = frame
|
||||
}
|
||||
}
|
||||
|
||||
self.updateLayout()
|
||||
|
||||
var newFrames: [MediaId: CGRect] = [:]
|
||||
for mediaId in mediaIds {
|
||||
if let frame = self.effectiveFrameForMedia(mediaId, detailsStateMaps: detailsStateMaps) {
|
||||
newFrames[mediaId] = frame
|
||||
}
|
||||
}
|
||||
|
||||
if let compensatedViewportTop = self.compensatedViewportTop(oldFrames: oldFrames, newFrames: newFrames, viewportTop: viewportTop) {
|
||||
self.setViewportTop(compensatedViewportTop)
|
||||
}
|
||||
self.updateVisibleItems(visibleBounds: self.scrollNode.view.bounds)
|
||||
}
|
||||
|
||||
private func setViewportTop(_ viewportTop: CGFloat) {
|
||||
let scrollView = self.scrollNode.view
|
||||
let minOffsetY = -scrollView.contentInset.top
|
||||
let maxOffsetY = max(minOffsetY, scrollView.contentSize.height - scrollView.bounds.height + scrollView.contentInset.bottom)
|
||||
let contentOffsetY = min(max(viewportTop - scrollView.contentInset.top, minOffsetY), maxOffsetY)
|
||||
if contentOffsetY.isFinite {
|
||||
let contentOffset = CGPoint(x: scrollView.contentOffset.x, y: contentOffsetY)
|
||||
scrollView.contentOffset = contentOffset
|
||||
self.previousContentOffset = contentOffset
|
||||
self.updateNavigationBar()
|
||||
}
|
||||
}
|
||||
|
||||
private func compensatedViewportTop(oldFrames: [MediaId: CGRect], newFrames: [MediaId: CGRect], viewportTop: CGFloat) -> CGFloat? {
|
||||
var pairedFrames: [(old: CGRect, new: CGRect)] = []
|
||||
for (mediaId, oldFrame) in oldFrames {
|
||||
if let newFrame = newFrames[mediaId] {
|
||||
pairedFrames.append((oldFrame, newFrame))
|
||||
}
|
||||
}
|
||||
if pairedFrames.isEmpty {
|
||||
return nil
|
||||
}
|
||||
|
||||
if let intersecting = pairedFrames
|
||||
.filter({ $0.old.height > 0.0 && $0.new.height > 0.0 && viewportTop > $0.old.minY && viewportTop < $0.old.maxY })
|
||||
.max(by: { $0.old.minY < $1.old.minY }) {
|
||||
let ratio = min(max((viewportTop - intersecting.old.minY) / intersecting.old.height, 0.0), 1.0)
|
||||
return intersecting.new.minY + ratio * intersecting.new.height
|
||||
}
|
||||
|
||||
if let above = pairedFrames
|
||||
.filter({ viewportTop >= $0.old.maxY })
|
||||
.max(by: { $0.old.maxY < $1.old.maxY }) {
|
||||
return viewportTop + (above.new.maxY - above.old.maxY)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
private func captureExpandedDetailsStateMaps() -> [String: [Int: Bool]] {
|
||||
guard let currentLayout = self.currentLayout else {
|
||||
return [:]
|
||||
}
|
||||
var result: [String: [Int: Bool]] = [:]
|
||||
self.captureExpandedDetailsStateMaps(items: currentLayout.items, visibleItemsWithNodes: self.visibleItemsWithNodes, path: [], result: &result)
|
||||
return result
|
||||
}
|
||||
|
||||
private func captureExpandedDetailsStateMaps(items: [InstantPageItem], visibleItemsWithNodes: [Int: InstantPageNode], path: [Int], result: inout [String: [Int: Bool]]) {
|
||||
let detailsNodes = visibleItemsWithNodes.compactMap { $0.value as? InstantPageDetailsNode }
|
||||
|
||||
var detailsIndex = -1
|
||||
for item in items {
|
||||
guard let detailsItem = item as? InstantPageDetailsItem else {
|
||||
continue
|
||||
}
|
||||
detailsIndex += 1
|
||||
|
||||
guard let detailsNode = detailsNodes.first(where: { $0.item === detailsItem }) else {
|
||||
continue
|
||||
}
|
||||
let nextPath = path + [detailsIndex]
|
||||
result[self.detailsStateKey(nextPath)] = detailsNode.contentNode.currentExpandedDetails ?? [:]
|
||||
self.captureExpandedDetailsStateMaps(items: detailsItem.items, visibleItemsWithNodes: detailsNode.contentNode.visibleItemsWithNodes, path: nextPath, result: &result)
|
||||
}
|
||||
}
|
||||
|
||||
private func detailsStateKey(_ path: [Int]) -> String {
|
||||
if path.isEmpty {
|
||||
return ""
|
||||
}
|
||||
return path.map(String.init).joined(separator: ".")
|
||||
}
|
||||
|
||||
private func effectiveFrameForMedia(_ mediaId: MediaId, detailsStateMaps: [String: [Int: Bool]]) -> CGRect? {
|
||||
guard let currentLayout = self.currentLayout else {
|
||||
return nil
|
||||
}
|
||||
return self.effectiveFrameForMedia(mediaId, items: currentLayout.items, origin: .zero, expandedDetails: self.currentExpandedDetails, path: [], detailsStateMaps: detailsStateMaps)
|
||||
}
|
||||
|
||||
private func effectiveFrameForMedia(_ mediaId: MediaId, items: [InstantPageItem], origin: CGPoint, expandedDetails: [Int: Bool]?, path: [Int], detailsStateMaps: [String: [Int: Bool]]) -> CGRect? {
|
||||
var collapseOffset: CGFloat = 0.0
|
||||
var detailsIndex = -1
|
||||
|
||||
for item in items {
|
||||
if item is InstantPageDetailsItem {
|
||||
detailsIndex += 1
|
||||
}
|
||||
|
||||
var itemFrame = item.frame.offsetBy(dx: origin.x, dy: origin.y - collapseOffset)
|
||||
if let detailsItem = item as? InstantPageDetailsItem {
|
||||
let nextPath = path + [detailsIndex]
|
||||
let nestedExpandedDetails = detailsStateMaps[self.detailsStateKey(nextPath)]
|
||||
let expanded = expandedDetails?[detailsIndex] ?? detailsItem.initiallyExpanded
|
||||
let height = expanded ? detailsItem.titleHeight + self.effectiveContentHeight(items: detailsItem.items, baseHeight: detailsItem.frame.height - detailsItem.titleHeight, expandedDetails: nestedExpandedDetails, path: nextPath, detailsStateMaps: detailsStateMaps) : detailsItem.titleHeight
|
||||
collapseOffset += item.frame.height - height
|
||||
itemFrame.size.height = height
|
||||
|
||||
if expanded, let nestedFrame = self.effectiveFrameForMedia(mediaId, items: detailsItem.items, origin: CGPoint(x: itemFrame.minX, y: itemFrame.minY + detailsItem.titleHeight), expandedDetails: nestedExpandedDetails, path: nextPath, detailsStateMaps: detailsStateMaps) {
|
||||
return nestedFrame
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
if self.itemContainsMedia(item, mediaId: mediaId) {
|
||||
return itemFrame
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
private func effectiveContentHeight(items: [InstantPageItem], baseHeight: CGFloat, expandedDetails: [Int: Bool]?, path: [Int], detailsStateMaps: [String: [Int: Bool]]) -> CGFloat {
|
||||
var contentHeight = baseHeight
|
||||
var detailsIndex = -1
|
||||
|
||||
for item in items {
|
||||
guard let detailsItem = item as? InstantPageDetailsItem else {
|
||||
continue
|
||||
}
|
||||
detailsIndex += 1
|
||||
|
||||
let nextPath = path + [detailsIndex]
|
||||
let nestedExpandedDetails = detailsStateMaps[self.detailsStateKey(nextPath)]
|
||||
let expanded = expandedDetails?[detailsIndex] ?? detailsItem.initiallyExpanded
|
||||
let height = expanded ? detailsItem.titleHeight + self.effectiveContentHeight(items: detailsItem.items, baseHeight: detailsItem.frame.height - detailsItem.titleHeight, expandedDetails: nestedExpandedDetails, path: nextPath, detailsStateMaps: detailsStateMaps) : detailsItem.titleHeight
|
||||
contentHeight += -detailsItem.frame.height + height
|
||||
}
|
||||
|
||||
return contentHeight
|
||||
}
|
||||
|
||||
private func itemContainsMedia(_ item: InstantPageItem, mediaId: MediaId) -> Bool {
|
||||
for media in item.medias {
|
||||
if media.media.id == mediaId {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
private func resolvedWebPage() -> (webPage: TelegramMediaWebpage, instantPage: InstantPage?)? {
|
||||
guard let (webPage, instantPage) = self.webPage else {
|
||||
return nil
|
||||
}
|
||||
guard !self.resolvedExternalMediaDimensions.isEmpty, case let .Loaded(content) = webPage.content else {
|
||||
return (webPage, instantPage)
|
||||
}
|
||||
|
||||
var instantPageUpdated = false
|
||||
var effectiveInstantPage = instantPage
|
||||
if let instantPage {
|
||||
var media = instantPage.media
|
||||
for (mediaId, currentMedia) in instantPage.media {
|
||||
if let updatedMedia = self.updatedMediaIfNeeded(currentMedia) {
|
||||
media[mediaId] = updatedMedia
|
||||
instantPageUpdated = true
|
||||
}
|
||||
}
|
||||
if instantPageUpdated {
|
||||
effectiveInstantPage = InstantPage(blocks: instantPage.blocks, media: media, isComplete: instantPage.isComplete, rtl: instantPage.rtl, url: instantPage.url, views: instantPage.views)
|
||||
}
|
||||
}
|
||||
|
||||
var imageUpdated = false
|
||||
let effectiveImage = content.image.map { image -> TelegramMediaImage in
|
||||
if let updated = self.updatedImageIfNeeded(image) {
|
||||
imageUpdated = true
|
||||
return updated
|
||||
} else {
|
||||
return image
|
||||
}
|
||||
}
|
||||
|
||||
var fileUpdated = false
|
||||
let effectiveFile = content.file.map { file -> TelegramMediaFile in
|
||||
if let updated = self.updatedFileIfNeeded(file) {
|
||||
fileUpdated = true
|
||||
return updated
|
||||
} else {
|
||||
return file
|
||||
}
|
||||
}
|
||||
|
||||
if !instantPageUpdated && !imageUpdated && !fileUpdated {
|
||||
return (webPage, instantPage)
|
||||
}
|
||||
|
||||
let effectiveContent = TelegramMediaWebpageLoadedContent(
|
||||
url: content.url,
|
||||
displayUrl: content.displayUrl,
|
||||
hash: content.hash,
|
||||
type: content.type,
|
||||
websiteName: content.websiteName,
|
||||
title: content.title,
|
||||
text: content.text,
|
||||
embedUrl: content.embedUrl,
|
||||
embedType: content.embedType,
|
||||
embedSize: content.embedSize,
|
||||
duration: content.duration,
|
||||
author: content.author,
|
||||
isMediaLargeByDefault: content.isMediaLargeByDefault,
|
||||
imageIsVideoCover: content.imageIsVideoCover,
|
||||
image: effectiveImage,
|
||||
file: effectiveFile,
|
||||
story: content.story,
|
||||
attributes: content.attributes,
|
||||
instantPage: effectiveInstantPage
|
||||
)
|
||||
return (TelegramMediaWebpage(webpageId: webPage.webpageId, content: .Loaded(effectiveContent)), effectiveInstantPage)
|
||||
}
|
||||
|
||||
private func updatedMediaIfNeeded(_ media: Media) -> Media? {
|
||||
if let image = media as? TelegramMediaImage {
|
||||
return self.updatedImageIfNeeded(image)
|
||||
} else if let file = media as? TelegramMediaFile {
|
||||
return self.updatedFileIfNeeded(file)
|
||||
} else {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
private func updatedImageIfNeeded(_ image: TelegramMediaImage) -> TelegramMediaImage? {
|
||||
guard let dimensions = self.resolvedExternalMediaDimensions[image.imageId] else {
|
||||
return nil
|
||||
}
|
||||
|
||||
var updatedRepresentations = image.representations
|
||||
var didUpdate = false
|
||||
for i in 0 ..< updatedRepresentations.count {
|
||||
let representation = updatedRepresentations[i]
|
||||
guard representation.resource is InstantPageExternalMediaResource, representation.dimensions != dimensions else {
|
||||
continue
|
||||
}
|
||||
updatedRepresentations[i] = TelegramMediaImageRepresentation(
|
||||
dimensions: dimensions,
|
||||
resource: representation.resource,
|
||||
progressiveSizes: representation.progressiveSizes,
|
||||
immediateThumbnailData: representation.immediateThumbnailData,
|
||||
hasVideo: representation.hasVideo,
|
||||
isPersonal: representation.isPersonal,
|
||||
typeHint: representation.typeHint
|
||||
)
|
||||
didUpdate = true
|
||||
}
|
||||
|
||||
guard didUpdate else {
|
||||
return nil
|
||||
}
|
||||
return TelegramMediaImage(
|
||||
imageId: image.imageId,
|
||||
representations: updatedRepresentations,
|
||||
videoRepresentations: image.videoRepresentations,
|
||||
immediateThumbnailData: image.immediateThumbnailData,
|
||||
emojiMarkup: image.emojiMarkup,
|
||||
reference: image.reference,
|
||||
partialReference: image.partialReference,
|
||||
flags: image.flags,
|
||||
video: image.video
|
||||
)
|
||||
}
|
||||
|
||||
private func updatedFileIfNeeded(_ file: TelegramMediaFile) -> TelegramMediaFile? {
|
||||
guard let dimensions = self.resolvedExternalMediaDimensions[file.fileId], file.resource is InstantPageExternalMediaResource else {
|
||||
return nil
|
||||
}
|
||||
|
||||
let (attributes, didUpdate) = self.fileAttributesWithResolvedDimensions(file.attributes, dimensions: dimensions)
|
||||
guard didUpdate else {
|
||||
return nil
|
||||
}
|
||||
|
||||
return TelegramMediaFile(
|
||||
fileId: file.fileId,
|
||||
partialReference: file.partialReference,
|
||||
resource: file.resource,
|
||||
previewRepresentations: file.previewRepresentations,
|
||||
videoThumbnails: file.videoThumbnails,
|
||||
videoCover: file.videoCover,
|
||||
immediateThumbnailData: file.immediateThumbnailData,
|
||||
mimeType: file.mimeType,
|
||||
size: file.size,
|
||||
attributes: attributes,
|
||||
alternativeRepresentations: file.alternativeRepresentations
|
||||
)
|
||||
}
|
||||
|
||||
private func fileAttributesWithResolvedDimensions(_ attributes: [TelegramMediaFileAttribute], dimensions: PixelDimensions) -> ([TelegramMediaFileAttribute], Bool) {
|
||||
var updatedAttributes: [TelegramMediaFileAttribute] = []
|
||||
var didUpdate = false
|
||||
var hasSizeAttribute = false
|
||||
|
||||
for attribute in attributes {
|
||||
switch attribute {
|
||||
case let .ImageSize(size):
|
||||
hasSizeAttribute = true
|
||||
if size != dimensions {
|
||||
updatedAttributes.append(.ImageSize(size: dimensions))
|
||||
didUpdate = true
|
||||
} else {
|
||||
updatedAttributes.append(attribute)
|
||||
}
|
||||
case let .Video(duration, size, flags, preloadSize, coverTime, videoCodec):
|
||||
hasSizeAttribute = true
|
||||
if size != dimensions {
|
||||
updatedAttributes.append(.Video(duration: duration, size: dimensions, flags: flags, preloadSize: preloadSize, coverTime: coverTime, videoCodec: videoCodec))
|
||||
didUpdate = true
|
||||
} else {
|
||||
updatedAttributes.append(attribute)
|
||||
}
|
||||
default:
|
||||
updatedAttributes.append(attribute)
|
||||
}
|
||||
}
|
||||
|
||||
if !hasSizeAttribute {
|
||||
updatedAttributes.append(.ImageSize(size: dimensions))
|
||||
didUpdate = true
|
||||
}
|
||||
|
||||
return (updatedAttributes, didUpdate)
|
||||
}
|
||||
|
||||
private func longPressMedia(_ media: InstantPageMedia) {
|
||||
let controller = makeContextMenuController(actions: [ContextMenuAction(content: .text(title: self.strings.Conversation_ContextMenuCopy, accessibilityLabel: self.strings.Conversation_ContextMenuCopy), action: { [weak self] in
|
||||
if let strongSelf = self, case let .image(image) = media.media {
|
||||
|
|
|
|||
|
|
@ -54,7 +54,7 @@ public final class InstantPageImageItem: InstantPageItem {
|
|||
|
||||
public func matchesNode(_ node: InstantPageNode) -> Bool {
|
||||
if let node = node as? InstantPageImageNode {
|
||||
return node.media == self.media
|
||||
return instantPageMediaMatchesNodeIdentity(node.media, self.media)
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
import Foundation
|
||||
import UIKit
|
||||
import ImageIO
|
||||
import AsyncDisplayKit
|
||||
import Display
|
||||
import TelegramCore
|
||||
|
|
@ -21,7 +22,35 @@ private struct FetchControls {
|
|||
let cancel: () -> Void
|
||||
}
|
||||
|
||||
final class InstantPageImageNode: ASDisplayNode, InstantPageNode {
|
||||
private enum ExternalImageLoadState {
|
||||
case loading
|
||||
case ready
|
||||
case failed
|
||||
}
|
||||
|
||||
private func externalImagePixelDimensions(data: Data) -> PixelDimensions? {
|
||||
guard let source = CGImageSourceCreateWithData(data as CFData, nil) else {
|
||||
return nil
|
||||
}
|
||||
guard let properties = CGImageSourceCopyPropertiesAtIndex(source, 0, nil) as? [CFString: Any] else {
|
||||
return nil
|
||||
}
|
||||
guard let pixelWidth = (properties[kCGImagePropertyPixelWidth] as? NSNumber)?.int32Value,
|
||||
let pixelHeight = (properties[kCGImagePropertyPixelHeight] as? NSNumber)?.int32Value,
|
||||
pixelWidth > 0, pixelHeight > 0 else {
|
||||
return nil
|
||||
}
|
||||
|
||||
let orientation = imageOrientationFromSource(source)
|
||||
switch orientation {
|
||||
case .left, .right, .leftMirrored, .rightMirrored:
|
||||
return PixelDimensions(width: pixelHeight, height: pixelWidth)
|
||||
default:
|
||||
return PixelDimensions(width: pixelWidth, height: pixelHeight)
|
||||
}
|
||||
}
|
||||
|
||||
final class InstantPageImageNode: ASDisplayNode, InstantPageNode, InstantPageExternalMediaDimensionsNode {
|
||||
private let context: AccountContext
|
||||
private let webPage: TelegramMediaWebpage
|
||||
private var theme: InstantPageTheme
|
||||
|
|
@ -32,8 +61,13 @@ final class InstantPageImageNode: ASDisplayNode, InstantPageNode {
|
|||
private let fit: Bool
|
||||
private let openMedia: (InstantPageMedia) -> Void
|
||||
private let longPressMedia: (InstantPageMedia) -> Void
|
||||
private let getPreloadedResource: (String) -> Data?
|
||||
|
||||
private var fetchControls: FetchControls?
|
||||
private var externalImageLoadState: ExternalImageLoadState?
|
||||
var updateExternalMediaDimensions: ((EngineMedia.Id, PixelDimensions) -> Void)?
|
||||
private var externalMediaDimensions: PixelDimensions?
|
||||
private var didReportExternalMediaDimensions = false
|
||||
|
||||
private let pinchContainerNode: PinchSourceContainerNode
|
||||
private let imageNode: TransformImageNode
|
||||
|
|
@ -48,6 +82,7 @@ final class InstantPageImageNode: ASDisplayNode, InstantPageNode {
|
|||
private var statusDisposable = MetaDisposable()
|
||||
|
||||
private var themeUpdated: Bool = false
|
||||
private var externalMediaDimensionsUpdated: Bool = false
|
||||
|
||||
init(context: AccountContext, sourceLocation: InstantPageSourceLocation, theme: InstantPageTheme, webPage: TelegramMediaWebpage, media: InstantPageMedia, attributes: [InstantPageImageAttribute], interactive: Bool, roundCorners: Bool, fit: Bool, openMedia: @escaping (InstantPageMedia) -> Void, longPressMedia: @escaping (InstantPageMedia) -> Void, activatePinchPreview: ((PinchSourceContainerNode) -> Void)?, pinchPreviewFinished: ((InstantPageNode) -> Void)?, getPreloadedResource: @escaping (String) -> Data?) {
|
||||
self.context = context
|
||||
|
|
@ -60,6 +95,7 @@ final class InstantPageImageNode: ASDisplayNode, InstantPageNode {
|
|||
self.fit = fit
|
||||
self.openMedia = openMedia
|
||||
self.longPressMedia = longPressMedia
|
||||
self.getPreloadedResource = getPreloadedResource
|
||||
|
||||
self.pinchContainerNode = PinchSourceContainerNode()
|
||||
self.imageNode = TransformImageNode()
|
||||
|
|
@ -72,33 +108,14 @@ final class InstantPageImageNode: ASDisplayNode, InstantPageNode {
|
|||
self.pinchContainerNode.contentNode.addSubnode(self.imageNode)
|
||||
self.addSubnode(self.pinchContainerNode)
|
||||
|
||||
if interactive, media.url != nil {
|
||||
self.linkIconNode.image = UIImage(bundleImageName: "Instant View/ImageLink")
|
||||
self.pinchContainerNode.contentNode.addSubnode(self.linkIconNode)
|
||||
}
|
||||
|
||||
if case let .image(image) = media.media, let largest = largestImageRepresentation(image.representations) {
|
||||
if let externalResource = largest.resource as? InstantPageExternalMediaResource {
|
||||
var url = externalResource.url
|
||||
if !url.hasPrefix("http") && !url.hasPrefix("https") && url.hasPrefix("//") {
|
||||
url = "https:\(url)"
|
||||
}
|
||||
let photoData: Signal<Tuple4<Data?, Data?, ChatMessagePhotoQuality, Bool>, NoError>
|
||||
if let preloadedData = getPreloadedResource(externalResource.url) {
|
||||
photoData = .single(Tuple4(nil, preloadedData, .full, true))
|
||||
} else {
|
||||
photoData = context.engine.resources.httpData(url: url, preserveExactUrl: true)
|
||||
|> map(Optional.init)
|
||||
|> `catch` { _ -> Signal<Data?, NoError> in
|
||||
return .single(nil)
|
||||
}
|
||||
|> map { data in
|
||||
if let data {
|
||||
return Tuple4(nil, data, .full, true)
|
||||
} else {
|
||||
return Tuple4(nil, nil, .full, false)
|
||||
}
|
||||
}
|
||||
}
|
||||
self.imageNode.setSignal(chatMessagePhotoInternal(photoData: photoData)
|
||||
|> map { _, _, generate in
|
||||
return generate
|
||||
})
|
||||
self.loadExternalImage(resourceUrl: externalResource.url)
|
||||
} else {
|
||||
let imageReference = ImageMediaReference.webPage(webPage: WebpageReference(webPage), media: image)
|
||||
self.imageNode.setSignal(chatMessagePhoto(postbox: context.account.postbox, userLocation: sourceLocation.userLocation, photoReference: imageReference))
|
||||
|
|
@ -124,38 +141,13 @@ final class InstantPageImageNode: ASDisplayNode, InstantPageNode {
|
|||
}
|
||||
}
|
||||
}))
|
||||
|
||||
if media.url != nil {
|
||||
self.linkIconNode.image = UIImage(bundleImageName: "Instant View/ImageLink")
|
||||
self.pinchContainerNode.contentNode.addSubnode(self.linkIconNode)
|
||||
}
|
||||
|
||||
|
||||
self.pinchContainerNode.contentNode.addSubnode(self.statusNode)
|
||||
}
|
||||
}
|
||||
} else if case let .file(file) = media.media {
|
||||
if let externalResource = file.resource as? InstantPageExternalMediaResource {
|
||||
let photoData: Signal<Tuple4<Data?, Data?, ChatMessagePhotoQuality, Bool>, NoError>
|
||||
if let preloadedData = getPreloadedResource(externalResource.url) {
|
||||
photoData = .single(Tuple4(nil, preloadedData, .full, true))
|
||||
} else {
|
||||
photoData = context.engine.resources.httpData(url: externalResource.url, preserveExactUrl: true)
|
||||
|> map(Optional.init)
|
||||
|> `catch` { _ -> Signal<Data?, NoError> in
|
||||
return .single(nil)
|
||||
}
|
||||
|> map { data in
|
||||
if let data {
|
||||
return Tuple4(nil, data, .full, true)
|
||||
} else {
|
||||
return Tuple4(nil, nil, .full, false)
|
||||
}
|
||||
}
|
||||
}
|
||||
self.imageNode.setSignal(chatMessagePhotoInternal(photoData: photoData)
|
||||
|> map { _, _, generate in
|
||||
return generate
|
||||
})
|
||||
self.loadExternalImage(resourceUrl: externalResource.url)
|
||||
} else {
|
||||
let fileReference = FileMediaReference.webPage(webPage: WebpageReference(webPage), media: file)
|
||||
if file.mimeType.hasPrefix("image/") {
|
||||
|
|
@ -235,6 +227,114 @@ final class InstantPageImageNode: ASDisplayNode, InstantPageNode {
|
|||
}
|
||||
}
|
||||
|
||||
private func loadExternalImage(resourceUrl: String) {
|
||||
self.externalImageLoadState = .loading
|
||||
self.updateExternalImageLoadState()
|
||||
|
||||
var requestUrl = resourceUrl
|
||||
if !requestUrl.hasPrefix("http") && !requestUrl.hasPrefix("https") && requestUrl.hasPrefix("//") {
|
||||
requestUrl = "https:\(requestUrl)"
|
||||
}
|
||||
|
||||
let photoData: Signal<Tuple4<Data?, Data?, ChatMessagePhotoQuality, Bool>, NoError>
|
||||
if let preloadedData = self.getPreloadedResource(resourceUrl) {
|
||||
photoData = .single(Tuple4(nil, preloadedData, .full, true))
|
||||
} else {
|
||||
photoData = self.context.engine.resources.httpData(url: requestUrl, preserveExactUrl: true)
|
||||
|> map(Optional.init)
|
||||
|> `catch` { _ -> Signal<Data?, NoError> in
|
||||
return .single(nil)
|
||||
}
|
||||
|> map { data in
|
||||
if let data {
|
||||
return Tuple4(nil, data, .full, true)
|
||||
} else {
|
||||
return Tuple4(nil, nil, .full, false)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let stateAwarePhotoData = photoData
|
||||
|> deliverOnMainQueue
|
||||
|> afterNext { [weak self] value in
|
||||
guard let strongSelf = self else {
|
||||
return
|
||||
}
|
||||
if let data = value._1 ?? value._0, UIImage(data: data) != nil {
|
||||
if let dimensions = externalImagePixelDimensions(data: data) {
|
||||
strongSelf.externalMediaDimensions = dimensions
|
||||
strongSelf.externalMediaDimensionsUpdated = true
|
||||
strongSelf.maybeUpdateExternalMediaDimensions(dimensions)
|
||||
}
|
||||
strongSelf.externalImageLoadState = .ready
|
||||
strongSelf.setNeedsLayout()
|
||||
} else {
|
||||
strongSelf.externalImageLoadState = .failed
|
||||
}
|
||||
strongSelf.updateExternalImageLoadState()
|
||||
}
|
||||
|
||||
self.imageNode.setSignal(chatMessagePhotoInternal(photoData: stateAwarePhotoData)
|
||||
|> map { _, _, generate in
|
||||
return generate
|
||||
})
|
||||
|
||||
self.fetchControls = FetchControls(fetch: { [weak self] _ in
|
||||
self?.loadExternalImage(resourceUrl: resourceUrl)
|
||||
}, cancel: {})
|
||||
}
|
||||
|
||||
private func currentMediaDimensions() -> PixelDimensions? {
|
||||
if case let .image(image) = self.media.media, let largest = largestImageRepresentation(image.representations) {
|
||||
return largest.dimensions
|
||||
} else if case let .file(file) = self.media.media {
|
||||
return file.dimensions
|
||||
} else {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
private func effectiveMediaDimensions() -> PixelDimensions? {
|
||||
return self.externalMediaDimensions ?? self.currentMediaDimensions()
|
||||
}
|
||||
|
||||
private func maybeUpdateExternalMediaDimensions(_ dimensions: PixelDimensions) {
|
||||
guard !self.didReportExternalMediaDimensions, let mediaId = self.media.media.id else {
|
||||
return
|
||||
}
|
||||
if let currentDimensions = self.currentMediaDimensions(), currentDimensions == dimensions {
|
||||
return
|
||||
}
|
||||
self.didReportExternalMediaDimensions = true
|
||||
self.updateExternalMediaDimensions?(mediaId, dimensions)
|
||||
}
|
||||
|
||||
private func updateExternalImageLoadState() {
|
||||
guard let externalImageLoadState = self.externalImageLoadState else {
|
||||
return
|
||||
}
|
||||
|
||||
if self.statusNode.supernode == nil {
|
||||
self.pinchContainerNode.contentNode.addSubnode(self.statusNode)
|
||||
}
|
||||
|
||||
let state: RadialStatusNodeState
|
||||
switch externalImageLoadState {
|
||||
case .loading:
|
||||
state = .progress(color: .white, lineWidth: nil, value: nil, cancelEnabled: false, animateRotation: true)
|
||||
case .ready:
|
||||
state = .none
|
||||
case .failed:
|
||||
state = .none
|
||||
}
|
||||
|
||||
self.statusNode.transitionToState(state, completion: { [weak statusNode] in
|
||||
if state == .none {
|
||||
statusNode?.removeFromSupernode()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
private func updateFetchStatus() {
|
||||
var state: RadialStatusNodeState = .none
|
||||
if let fetchStatus = self.fetchStatus {
|
||||
|
|
@ -260,19 +360,20 @@ final class InstantPageImageNode: ASDisplayNode, InstantPageNode {
|
|||
|
||||
let size = self.bounds.size
|
||||
|
||||
if self.currentSize != size || self.themeUpdated {
|
||||
if self.currentSize != size || self.themeUpdated || self.externalMediaDimensionsUpdated {
|
||||
self.currentSize = size
|
||||
self.themeUpdated = false
|
||||
self.externalMediaDimensionsUpdated = false
|
||||
|
||||
self.pinchContainerNode.frame = CGRect(origin: CGPoint(), size: size)
|
||||
self.pinchContainerNode.update(size: size, transition: .immediate)
|
||||
self.imageNode.frame = CGRect(origin: CGPoint(), size: size)
|
||||
|
||||
let radialStatusSize: CGFloat = 50.0
|
||||
let radialStatusSize: CGFloat = max(18.0, min(50.0, floor(min(size.width, size.height) * 0.7)))
|
||||
self.statusNode.frame = CGRect(x: floorToScreenPixels((size.width - radialStatusSize) / 2.0), y: floorToScreenPixels((size.height - radialStatusSize) / 2.0), width: radialStatusSize, height: radialStatusSize)
|
||||
|
||||
if case let .image(image) = self.media.media, let largest = largestImageRepresentation(image.representations) {
|
||||
let imageSize = largest.dimensions.cgSize.aspectFilled(size)
|
||||
if case .image = self.media.media, let dimensions = self.effectiveMediaDimensions() {
|
||||
let imageSize = dimensions.cgSize.aspectFilled(size)
|
||||
let boundingSize = size
|
||||
let radius: CGFloat = self.roundCorners ? floor(min(imageSize.width, imageSize.height) / 2.0) : 0.0
|
||||
let makeLayout = self.imageNode.asyncLayout()
|
||||
|
|
@ -280,7 +381,7 @@ final class InstantPageImageNode: ASDisplayNode, InstantPageNode {
|
|||
apply()
|
||||
|
||||
self.linkIconNode.frame = CGRect(x: size.width - 38.0, y: 14.0, width: 24.0, height: 24.0)
|
||||
} else if case let .file(file) = self.media.media, let dimensions = file.dimensions {
|
||||
} else if case let .file(file) = self.media.media, let dimensions = self.effectiveMediaDimensions() {
|
||||
let emptyColor = file.mimeType.hasPrefix("image/") ? self.theme.imageTintColor : nil
|
||||
|
||||
let imageSize = dimensions.cgSize.aspectFilled(size)
|
||||
|
|
@ -318,7 +419,7 @@ final class InstantPageImageNode: ASDisplayNode, InstantPageNode {
|
|||
}
|
||||
|
||||
func transitionNode(media: InstantPageMedia) -> (ASDisplayNode, CGRect, () -> (UIView?, UIView?))? {
|
||||
if media == self.media {
|
||||
if instantPageMediaMatchesNodeIdentity(media, self.media) {
|
||||
let imageNode = self.imageNode
|
||||
return (self.imageNode, self.imageNode.bounds, { [weak imageNode] in
|
||||
return (imageNode?.view.snapshotContentTree(unhide: true), nil)
|
||||
|
|
@ -329,14 +430,32 @@ final class InstantPageImageNode: ASDisplayNode, InstantPageNode {
|
|||
}
|
||||
|
||||
func updateHiddenMedia(media: InstantPageMedia?) {
|
||||
self.imageNode.isHidden = self.media == media
|
||||
if let media {
|
||||
self.imageNode.isHidden = instantPageMediaMatchesNodeIdentity(self.media, media)
|
||||
} else {
|
||||
self.imageNode.isHidden = false
|
||||
}
|
||||
self.statusNode.isHidden = self.imageNode.isHidden
|
||||
self.linkIconNode.isHidden = self.imageNode.isHidden
|
||||
}
|
||||
|
||||
@objc private func tapGesture(_ recognizer: TapLongTapOrDoubleTapGestureRecognizer) {
|
||||
switch recognizer.state {
|
||||
case .ended:
|
||||
if let (gesture, _) = recognizer.lastRecognizedGestureAndLocation {
|
||||
if let externalImageLoadState = self.externalImageLoadState {
|
||||
switch externalImageLoadState {
|
||||
case .loading:
|
||||
return
|
||||
case .failed:
|
||||
if case .tap = gesture {
|
||||
self.fetchControls?.fetch(true)
|
||||
}
|
||||
return
|
||||
case .ready:
|
||||
break
|
||||
}
|
||||
}
|
||||
if let fetchStatus = self.fetchStatus {
|
||||
switch fetchStatus {
|
||||
case .Local:
|
||||
|
|
|
|||
|
|
@ -20,3 +20,28 @@ public struct InstantPageMedia: Equatable {
|
|||
return lhs.index == rhs.index && lhs.media == rhs.media && lhs.url == rhs.url && lhs.caption == rhs.caption && lhs.credit == rhs.credit
|
||||
}
|
||||
}
|
||||
|
||||
func instantPageMediaMatchesNodeIdentity(_ lhs: InstantPageMedia, _ rhs: InstantPageMedia) -> Bool {
|
||||
if lhs.index != rhs.index {
|
||||
return false
|
||||
}
|
||||
if lhs.url != rhs.url || lhs.caption != rhs.caption || lhs.credit != rhs.credit {
|
||||
return false
|
||||
}
|
||||
if let lhsId = lhs.media.id, let rhsId = rhs.media.id {
|
||||
return lhsId == rhsId
|
||||
}
|
||||
return lhs == rhs
|
||||
}
|
||||
|
||||
func instantPageMediaArraysMatchNodeIdentity(_ lhs: [InstantPageMedia], _ rhs: [InstantPageMedia]) -> Bool {
|
||||
if lhs.count != rhs.count {
|
||||
return false
|
||||
}
|
||||
for i in 0 ..< lhs.count {
|
||||
if !instantPageMediaMatchesNodeIdentity(lhs[i], rhs[i]) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,8 +2,13 @@ import Foundation
|
|||
import UIKit
|
||||
import AsyncDisplayKit
|
||||
import Display
|
||||
import TelegramCore
|
||||
import TelegramPresentationData
|
||||
|
||||
public protocol InstantPageExternalMediaDimensionsNode: AnyObject {
|
||||
var updateExternalMediaDimensions: ((EngineMedia.Id, PixelDimensions) -> Void)? { get set }
|
||||
}
|
||||
|
||||
public protocol InstantPageNode: ASDisplayNode {
|
||||
func updateIsVisible(_ isVisible: Bool)
|
||||
|
||||
|
|
|
|||
|
|
@ -30,7 +30,7 @@ final class InstantPageSlideshowItem: InstantPageItem {
|
|||
|
||||
func matchesNode(_ node: InstantPageNode) -> Bool {
|
||||
if let node = node as? InstantPageSlideshowNode {
|
||||
return self.medias == node.medias
|
||||
return instantPageMediaArraysMatchNodeIdentity(self.medias, node.medias)
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
|
|
@ -55,4 +55,3 @@ final class InstantPageSlideshowItem: InstantPageItem {
|
|||
func drawInTile(context: CGContext) {
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -61,6 +61,12 @@ private final class InstantPageSlideshowItemNode: ASDisplayNode {
|
|||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func updateExternalMediaDimensions(_ update: ((EngineMedia.Id, PixelDimensions) -> Void)?) {
|
||||
if let node = self.contentNode as? InstantPageImageNode {
|
||||
node.updateExternalMediaDimensions = update
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private final class InstantPageSlideshowPagerNode: ASDisplayNode, ASScrollViewDelegate {
|
||||
|
|
@ -88,6 +94,13 @@ private final class InstantPageSlideshowPagerNode: ASDisplayNode, ASScrollViewDe
|
|||
}
|
||||
|
||||
private var containerLayout: ContainerViewLayout?
|
||||
var updateExternalMediaDimensions: ((EngineMedia.Id, PixelDimensions) -> Void)? {
|
||||
didSet {
|
||||
for node in self.itemNodes {
|
||||
node.updateExternalMediaDimensions(self.updateExternalMediaDimensions)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var centralItemIndexUpdated: (Int?) -> Void = { _ in }
|
||||
|
||||
|
|
@ -195,6 +208,7 @@ private final class InstantPageSlideshowPagerNode: ASDisplayNode, ASScrollViewDe
|
|||
}
|
||||
|
||||
let node = InstantPageSlideshowItemNode(contentNode: contentNode)
|
||||
node.updateExternalMediaDimensions(self.updateExternalMediaDimensions)
|
||||
|
||||
node.index = index
|
||||
return node
|
||||
|
|
@ -380,8 +394,13 @@ private final class InstantPageSlideshowPagerNode: ASDisplayNode, ASScrollViewDe
|
|||
}
|
||||
}
|
||||
|
||||
final class InstantPageSlideshowNode: ASDisplayNode, InstantPageNode {
|
||||
final class InstantPageSlideshowNode: ASDisplayNode, InstantPageNode, InstantPageExternalMediaDimensionsNode {
|
||||
var medias: [InstantPageMedia] = []
|
||||
var updateExternalMediaDimensions: ((EngineMedia.Id, PixelDimensions) -> Void)? {
|
||||
didSet {
|
||||
self.pagerNode.updateExternalMediaDimensions = self.updateExternalMediaDimensions
|
||||
}
|
||||
}
|
||||
|
||||
private let pagerNode: InstantPageSlideshowPagerNode
|
||||
private let pageControlNode: PageControlNode
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ import TelegramPresentationData
|
|||
import TelegramUIPreferences
|
||||
import AccountContext
|
||||
|
||||
final class InstantPageSubContentNode : ASDisplayNode {
|
||||
final class InstantPageSubContentNode : ASDisplayNode, InstantPageExternalMediaDimensionsNode {
|
||||
private let context: AccountContext
|
||||
private let strings: PresentationStrings
|
||||
private let nameDisplayOrder: PresentationPersonNameOrder
|
||||
|
|
@ -32,6 +32,13 @@ final class InstantPageSubContentNode : ASDisplayNode {
|
|||
var currentDetailsItems: [InstantPageDetailsItem] = []
|
||||
|
||||
var requestLayoutUpdate: ((Bool) -> Void)?
|
||||
var updateExternalMediaDimensions: ((EngineMedia.Id, PixelDimensions) -> Void)? {
|
||||
didSet {
|
||||
for (_, itemNode) in self.visibleItemsWithNodes {
|
||||
self.applyExternalMediaDimensionsUpdater(to: itemNode)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var currentLayout: InstantPageLayout
|
||||
let contentSize: CGSize
|
||||
|
|
@ -209,6 +216,7 @@ final class InstantPageSubContentNode : ASDisplayNode {
|
|||
topNode = newNode
|
||||
self.visibleItemsWithNodes[itemIndex] = newNode
|
||||
itemNode = newNode
|
||||
self.applyExternalMediaDimensionsUpdater(to: newNode)
|
||||
|
||||
if let itemNode = itemNode as? InstantPageDetailsNode {
|
||||
itemNode.requestLayoutUpdate = { [weak self] animated in
|
||||
|
|
@ -289,6 +297,16 @@ final class InstantPageSubContentNode : ASDisplayNode {
|
|||
}
|
||||
}
|
||||
|
||||
private func applyExternalMediaDimensionsUpdater(to itemNode: InstantPageNode) {
|
||||
if let itemNode = itemNode as? InstantPageImageNode {
|
||||
itemNode.updateExternalMediaDimensions = self.updateExternalMediaDimensions
|
||||
} else if let itemNode = itemNode as? InstantPageDetailsNode {
|
||||
itemNode.contentNode.updateExternalMediaDimensions = self.updateExternalMediaDimensions
|
||||
} else if let itemNode = itemNode as? InstantPageSlideshowNode {
|
||||
itemNode.updateExternalMediaDimensions = self.updateExternalMediaDimensions
|
||||
}
|
||||
}
|
||||
|
||||
private func updateWebEmbedHeight(_ index: Int, _ height: CGFloat) {
|
||||
// let currentHeight = self.currentWebEmbedHeights[index]
|
||||
// if height != currentHeight {
|
||||
|
|
|
|||
|
|
@ -15,10 +15,12 @@ private final class InstantPageTileNodeParameters: NSObject {
|
|||
}
|
||||
|
||||
public final class InstantPageTileNode: ASDisplayNode {
|
||||
private let tile: InstantPageTile
|
||||
private var tile: InstantPageTile
|
||||
private var tileBackgroundColor: UIColor
|
||||
|
||||
public init(tile: InstantPageTile, backgroundColor: UIColor) {
|
||||
self.tile = tile
|
||||
self.tileBackgroundColor = backgroundColor
|
||||
|
||||
super.init()
|
||||
|
||||
|
|
@ -27,8 +29,15 @@ public final class InstantPageTileNode: ASDisplayNode {
|
|||
self.backgroundColor = backgroundColor
|
||||
}
|
||||
|
||||
public func update(tile: InstantPageTile, backgroundColor: UIColor) {
|
||||
self.tile = tile
|
||||
self.tileBackgroundColor = backgroundColor
|
||||
self.backgroundColor = backgroundColor
|
||||
self.setNeedsDisplay()
|
||||
}
|
||||
|
||||
public override func drawParameters(forAsyncLayer layer: _ASDisplayLayer) -> NSObjectProtocol? {
|
||||
return InstantPageTileNodeParameters(tile: self.tile, backgroundColor: self.backgroundColor ?? UIColor.white)
|
||||
return InstantPageTileNodeParameters(tile: self.tile, backgroundColor: self.tileBackgroundColor)
|
||||
}
|
||||
|
||||
@objc override public class func draw(_ bounds: CGRect, withParameters parameters: Any?, isCancelled: () -> Bool, isRasterizing: Bool) {
|
||||
|
|
|
|||
|
|
@ -48,6 +48,7 @@ swift_library(
|
|||
"//submodules/Components/BundleIconComponent",
|
||||
"//submodules/TelegramUI/Components/GlassBackgroundComponent",
|
||||
"//submodules/TelegramUI/Components/GlassBarButtonComponent",
|
||||
"//submodules/TelegramUI/Components/GlassControls",
|
||||
"//submodules/TelegramUI/Components/EdgeEffect",
|
||||
"//submodules/TelegramUI/Components/SearchInputPanelComponent",
|
||||
"//submodules/TelegramUI/Components/ButtonComponent",
|
||||
|
|
|
|||
|
|
@ -100,7 +100,7 @@ public final class LocationInfoListItemNode: ListViewItemNode {
|
|||
|
||||
super.init(layerBacked: false, rotated: false, seeThrough: false)
|
||||
|
||||
self.addSubnode(self.backgroundNode)
|
||||
//self.addSubnode(self.backgroundNode)
|
||||
self.addSubnode(self.buttonNode)
|
||||
self.addSubnode(self.venueIconNode)
|
||||
|
||||
|
|
|
|||
|
|
@ -57,7 +57,7 @@ public final class LocationMapHeaderNode: ASDisplayNode {
|
|||
|
||||
private let options: ComponentView<Empty>?
|
||||
|
||||
private let optionsBackgroundView: GlassBackgroundView?
|
||||
private let optionsBackgroundView: GlassContextExtractableContainer?
|
||||
private let optionsBackgroundNode: ASImageNode
|
||||
private let optionsSeparatorNode: ASDisplayNode
|
||||
private let optionsSecondSeparatorNode: ASDisplayNode
|
||||
|
|
@ -66,7 +66,7 @@ public final class LocationMapHeaderNode: ASDisplayNode {
|
|||
private let notificationButtonNode: HighlightableButtonNode
|
||||
private let placesBackgroundView: GlassBackgroundView?
|
||||
private let placesBackgroundNode: ASImageNode
|
||||
private let placesButtonNode: HighlightableButtonNode
|
||||
private let placesButtonNode: HighlightTrackingButtonNode
|
||||
private let shadowNode: ASImageNode
|
||||
|
||||
private var validLayout: (ContainerViewLayout, CGFloat, CGFloat, CGFloat, CGFloat, CGFloat, CGSize)?
|
||||
|
|
@ -132,7 +132,7 @@ public final class LocationMapHeaderNode: ASDisplayNode {
|
|||
self.placesBackgroundNode.image = generateBackgroundImage(theme: presentationData.theme)
|
||||
self.placesBackgroundNode.isUserInteractionEnabled = true
|
||||
|
||||
self.placesButtonNode = HighlightableButtonNode()
|
||||
self.placesButtonNode = HighlightTrackingButtonNode()
|
||||
self.placesButtonNode.setTitle(presentationData.strings.Map_PlacesInThisArea, with: Font.medium(17.0), with: self.glass ? presentationData.theme.rootController.navigationBar.primaryTextColor : buttonColor, for: .normal)
|
||||
|
||||
self.shadowNode = ASImageNode()
|
||||
|
|
@ -142,7 +142,7 @@ public final class LocationMapHeaderNode: ASDisplayNode {
|
|||
self.shadowNode.image = generateShadowImage(theme: presentationData.theme, highlighted: false)
|
||||
|
||||
if glass {
|
||||
self.optionsBackgroundView = GlassBackgroundView()
|
||||
self.optionsBackgroundView = GlassContextExtractableContainer()
|
||||
self.optionsBackgroundNode.image = nil
|
||||
|
||||
self.placesBackgroundView = GlassBackgroundView()
|
||||
|
|
@ -169,16 +169,16 @@ public final class LocationMapHeaderNode: ASDisplayNode {
|
|||
self.view.addSubview(optionsBackgroundView)
|
||||
}
|
||||
self.addSubnode(self.optionsBackgroundNode)
|
||||
self.optionsBackgroundNode.addSubnode(self.optionsSeparatorNode)
|
||||
self.optionsBackgroundNode.addSubnode(self.optionsSecondSeparatorNode)
|
||||
self.optionsBackgroundNode.addSubnode(self.infoButtonNode)
|
||||
self.optionsBackgroundNode.addSubnode(self.locationButtonNode)
|
||||
self.optionsBackgroundNode.addSubnode(self.notificationButtonNode)
|
||||
self.optionsBackgroundView?.contentView.addSubview(self.optionsSeparatorNode.view)
|
||||
self.optionsBackgroundView?.contentView.addSubview(self.optionsSecondSeparatorNode.view)
|
||||
self.optionsBackgroundView?.contentView.addSubview(self.infoButtonNode.view)
|
||||
self.optionsBackgroundView?.contentView.addSubview(self.locationButtonNode.view)
|
||||
self.optionsBackgroundView?.contentView.addSubview(self.notificationButtonNode.view)
|
||||
}
|
||||
}
|
||||
|
||||
self.addSubnode(self.placesBackgroundNode)
|
||||
self.placesBackgroundNode.addSubnode(self.placesButtonNode)
|
||||
self.placesBackgroundView?.contentView.addSubview(self.placesButtonNode.view)
|
||||
//self.addSubnode(self.shadowNode)
|
||||
|
||||
self.infoButtonNode.addTarget(self, action: #selector(self.infoPressed), forControlEvents: .touchUpInside)
|
||||
|
|
@ -252,16 +252,16 @@ public final class LocationMapHeaderNode: ASDisplayNode {
|
|||
let inset: CGFloat = 6.0
|
||||
|
||||
let placesButtonSize = CGSize(width: 180.0 + panelInset * 2.0, height: 45.0 + panelInset * 2.0)
|
||||
let placesButtonFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((size.width - placesButtonSize.width) / 2.0), y: navigationBarHeight + topPadding - 6.0), size: placesButtonSize)
|
||||
let placesButtonFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((size.width - placesButtonSize.width) / 2.0), y: navigationBarHeight + topPadding - 6.0), size: placesButtonSize).insetBy(dx: 5.0, dy: 6.0)
|
||||
transition.updateFrame(node: self.placesBackgroundNode, frame: placesButtonFrame)
|
||||
|
||||
if let placesBackgroundView = self.placesBackgroundView {
|
||||
let backgroundViewFrame = CGRect(origin: .zero, size: placesButtonFrame.size).insetBy(dx: 5.0, dy: 6.0)
|
||||
let backgroundViewFrame = CGRect(origin: .zero, size: placesButtonFrame.size)
|
||||
transition.updateFrame(view: placesBackgroundView, frame: backgroundViewFrame)
|
||||
placesBackgroundView.update(size: backgroundViewFrame.size, cornerRadius: backgroundViewFrame.height * 0.5, isDark: self.presentationData.theme.overallDarkAppearance, tintColor: .init(kind: .panel), transition: .immediate)
|
||||
placesBackgroundView.update(size: backgroundViewFrame.size, cornerRadius: backgroundViewFrame.height * 0.5, isDark: self.presentationData.theme.overallDarkAppearance, tintColor: .init(kind: .panel), isInteractive: true, transition: .immediate)
|
||||
}
|
||||
|
||||
transition.updateFrame(node: self.placesButtonNode, frame: CGRect(origin: CGPoint(), size: placesButtonSize))
|
||||
transition.updateFrame(node: self.placesButtonNode, frame: CGRect(origin: CGPoint(), size: placesButtonFrame.size))
|
||||
transition.updateAlpha(node: self.placesBackgroundNode, alpha: self.displayingPlacesButton ? 1.0 : 0.0)
|
||||
transition.updateAlpha(node: self.placesButtonNode, alpha: self.displayingPlacesButton ? 1.0 : 0.0)
|
||||
|
||||
|
|
@ -327,7 +327,7 @@ public final class LocationMapHeaderNode: ASDisplayNode {
|
|||
if let optionsBackgroundView = self.optionsBackgroundView {
|
||||
let backgroundViewFrame = backgroundFrame.insetBy(dx: 4.0, dy: 4.0)
|
||||
transition.updateFrame(view: optionsBackgroundView, frame: backgroundViewFrame)
|
||||
optionsBackgroundView.update(size: backgroundViewFrame.size, cornerRadius: backgroundViewFrame.width * 0.5, isDark: self.presentationData.theme.overallDarkAppearance, tintColor: .init(kind: .panel), transition: .immediate)
|
||||
optionsBackgroundView.update(size: backgroundViewFrame.size, cornerRadius: backgroundViewFrame.width * 0.5, isDark: self.presentationData.theme.overallDarkAppearance, tintColor: .init(kind: .panel), isInteractive: true, transition: .immediate)
|
||||
}
|
||||
|
||||
let alphaTransition = ContainedViewLayoutTransition.animated(duration: 0.2, curve: .easeInOut)
|
||||
|
|
|
|||
|
|
@ -11,7 +11,6 @@ import AppBundle
|
|||
import CoreLocation
|
||||
import PresentationDataUtils
|
||||
import OpenInExternalAppUI
|
||||
|
||||
import DeviceAccess
|
||||
import UndoUI
|
||||
import MapKit
|
||||
|
|
@ -32,12 +31,6 @@ public class LocationViewParams {
|
|||
}
|
||||
}
|
||||
|
||||
enum LocationViewRightBarButton {
|
||||
case none
|
||||
case share
|
||||
case showAll
|
||||
}
|
||||
|
||||
class LocationViewInteraction {
|
||||
let toggleMapModeSelection: () -> Void
|
||||
let updateMapMode: (LocationMapMode) -> Void
|
||||
|
|
@ -49,10 +42,9 @@ class LocationViewInteraction {
|
|||
let updateSendActionHighlight: (Bool) -> Void
|
||||
let sendLiveLocation: (Int32?, Bool, EngineMessage.Id?) -> Void
|
||||
let stopLiveLocation: () -> Void
|
||||
let updateRightBarButton: (LocationViewRightBarButton) -> Void
|
||||
let present: (ViewController) -> Void
|
||||
|
||||
init(toggleMapModeSelection: @escaping () -> Void, updateMapMode: @escaping (LocationMapMode) -> Void, toggleTrackingMode: @escaping () -> Void, goToCoordinate: @escaping (CLLocationCoordinate2D) -> Void, requestDirections: @escaping (TelegramMediaMap, String?, OpenInLocationDirections) -> Void, share: @escaping () -> Void, setupProximityNotification: @escaping (Bool, EngineMessage.Id?) -> Void, updateSendActionHighlight: @escaping (Bool) -> Void, sendLiveLocation: @escaping (Int32?, Bool, EngineMessage.Id?) -> Void, stopLiveLocation: @escaping () -> Void, updateRightBarButton: @escaping (LocationViewRightBarButton) -> Void, present: @escaping (ViewController) -> Void) {
|
||||
init(toggleMapModeSelection: @escaping () -> Void, updateMapMode: @escaping (LocationMapMode) -> Void, toggleTrackingMode: @escaping () -> Void, goToCoordinate: @escaping (CLLocationCoordinate2D) -> Void, requestDirections: @escaping (TelegramMediaMap, String?, OpenInLocationDirections) -> Void, share: @escaping () -> Void, setupProximityNotification: @escaping (Bool, EngineMessage.Id?) -> Void, updateSendActionHighlight: @escaping (Bool) -> Void, sendLiveLocation: @escaping (Int32?, Bool, EngineMessage.Id?) -> Void, stopLiveLocation: @escaping () -> Void, present: @escaping (ViewController) -> Void) {
|
||||
self.toggleMapModeSelection = toggleMapModeSelection
|
||||
self.updateMapMode = updateMapMode
|
||||
self.toggleTrackingMode = toggleTrackingMode
|
||||
|
|
@ -63,7 +55,6 @@ class LocationViewInteraction {
|
|||
self.updateSendActionHighlight = updateSendActionHighlight
|
||||
self.sendLiveLocation = sendLiveLocation
|
||||
self.stopLiveLocation = stopLiveLocation
|
||||
self.updateRightBarButton = updateRightBarButton
|
||||
self.present = present
|
||||
}
|
||||
}
|
||||
|
|
@ -83,9 +74,7 @@ public final class LocationViewController: ViewController {
|
|||
private let locationManager = LocationManager()
|
||||
|
||||
private var interaction: LocationViewInteraction?
|
||||
|
||||
private var rightBarButtonAction: LocationViewRightBarButton = .none
|
||||
|
||||
|
||||
public var dismissed: () -> Void = {}
|
||||
|
||||
public init(context: AccountContext, updatedPresentationData: (initial: PresentationData, signal: Signal<PresentationData, NoError>)? = nil, subject: EngineMessage, isStoryLocation: Bool = false, isPreview: Bool = false, params: LocationViewParams) {
|
||||
|
|
@ -97,33 +86,19 @@ public final class LocationViewController: ViewController {
|
|||
|
||||
self.presentationData = updatedPresentationData?.initial ?? context.sharedContext.currentPresentationData.with { $0 }
|
||||
|
||||
let navigationBarPresentationData: NavigationBarPresentationData?
|
||||
if !isPreview {
|
||||
navigationBarPresentationData = NavigationBarPresentationData(theme: NavigationBarTheme(rootControllerTheme: self.presentationData.theme).withUpdatedSeparatorColor(.clear), strings: NavigationBarStrings(presentationStrings: self.presentationData.strings))
|
||||
} else {
|
||||
navigationBarPresentationData = nil
|
||||
}
|
||||
super.init(navigationBarPresentationData: nil)
|
||||
|
||||
super.init(navigationBarPresentationData: navigationBarPresentationData)
|
||||
self._hasGlassStyle = true
|
||||
|
||||
self.navigationPresentation = .modal
|
||||
|
||||
if !self.isPreview {
|
||||
self.title = self.presentationData.strings.Map_LocationTitle
|
||||
self.navigationItem.leftBarButtonItem = UIBarButtonItem(title: self.presentationData.strings.Common_Close, style: .plain, target: self, action: #selector(self.cancelPressed))
|
||||
}
|
||||
|
||||
|
||||
self.presentationDataDisposable = ((updatedPresentationData?.signal ?? context.sharedContext.presentationData)
|
||||
|> deliverOnMainQueue).start(next: { [weak self] presentationData in
|
||||
guard let strongSelf = self, strongSelf.presentationData.theme !== presentationData.theme else {
|
||||
return
|
||||
}
|
||||
strongSelf.presentationData = presentationData
|
||||
|
||||
strongSelf.navigationBar?.updatePresentationData(NavigationBarPresentationData(theme: NavigationBarTheme(rootControllerTheme: strongSelf.presentationData.theme).withUpdatedSeparatorColor(.clear), strings: NavigationBarStrings(presentationStrings: strongSelf.presentationData.strings)), transition: .immediate)
|
||||
|
||||
strongSelf.updateRightBarButton()
|
||||
|
||||
|
||||
if strongSelf.isNodeLoaded {
|
||||
strongSelf.controllerNode.updatePresentationData(presentationData)
|
||||
}
|
||||
|
|
@ -466,15 +441,6 @@ public final class LocationViewController: ViewController {
|
|||
}, stopLiveLocation: { [weak self] in
|
||||
params.stopLiveLocation(nil)
|
||||
self?.dismiss()
|
||||
}, updateRightBarButton: { [weak self] action in
|
||||
guard let strongSelf = self else {
|
||||
return
|
||||
}
|
||||
|
||||
if action != strongSelf.rightBarButtonAction {
|
||||
strongSelf.rightBarButtonAction = action
|
||||
strongSelf.updateRightBarButton()
|
||||
}
|
||||
}, present: { [weak self] c in
|
||||
if let strongSelf = self {
|
||||
strongSelf.present(c, in: .window(.root))
|
||||
|
|
@ -515,7 +481,7 @@ public final class LocationViewController: ViewController {
|
|||
return
|
||||
}
|
||||
|
||||
self.displayNode = LocationViewControllerNode(context: self.context, presentationData: self.presentationData, subject: self.subject, interaction: interaction, locationManager: self.locationManager, isStoryLocation: self.isStoryLocation, isPreview: self.isPreview)
|
||||
self.displayNode = LocationViewControllerNode(context: self.context, controller: self, presentationData: self.presentationData, subject: self.subject, interaction: interaction, locationManager: self.locationManager, isStoryLocation: self.isStoryLocation, isPreview: self.isPreview)
|
||||
self.displayNodeDidLoad()
|
||||
|
||||
self.controllerNode.onAnnotationsReady = { [weak self] in
|
||||
|
|
@ -528,39 +494,12 @@ public final class LocationViewController: ViewController {
|
|||
self.controllerNode.headerNode.mapNode.disableHorizontalTransitionGesture = self.isStoryLocation
|
||||
}
|
||||
|
||||
private func updateRightBarButton() {
|
||||
guard !self.isPreview else {
|
||||
return
|
||||
}
|
||||
switch self.rightBarButtonAction {
|
||||
case .none:
|
||||
self.navigationItem.rightBarButtonItem = nil
|
||||
case .share:
|
||||
self.navigationItem.rightBarButtonItem = UIBarButtonItem(image: PresentationResourcesRootController.navigationShareIcon(self.presentationData.theme), style: .plain, target: self, action: #selector(self.sharePressed))
|
||||
self.navigationItem.rightBarButtonItem?.accessibilityLabel = self.presentationData.strings.VoiceOver_MessageContextShare
|
||||
case .showAll:
|
||||
self.navigationItem.rightBarButtonItem = UIBarButtonItem(title: self.presentationData.strings.Map_LiveLocationShowAll, style: .plain, target: self, action: #selector(self.showAllPressed))
|
||||
}
|
||||
}
|
||||
|
||||
override public func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) {
|
||||
super.containerLayoutUpdated(layout, transition: transition)
|
||||
|
||||
self.controllerNode.containerLayoutUpdated(layout, navigationHeight: self.navigationLayout(layout: layout).navigationFrame.maxY, transition: transition)
|
||||
}
|
||||
|
||||
@objc private func cancelPressed() {
|
||||
self.dismiss()
|
||||
}
|
||||
|
||||
@objc private func sharePressed() {
|
||||
self.interaction?.share()
|
||||
}
|
||||
|
||||
@objc private func showAllPressed() {
|
||||
self.controllerNode.showAll()
|
||||
}
|
||||
|
||||
|
||||
private var didDismiss = false
|
||||
public override func viewWillDisappear(_ animated: Bool) {
|
||||
super.viewWillDisappear(animated)
|
||||
|
|
|
|||
|
|
@ -17,6 +17,11 @@ import CoreLocation
|
|||
import Geocoding
|
||||
import DeviceAccess
|
||||
import TooltipUI
|
||||
import ComponentFlow
|
||||
import GlassControls
|
||||
import BundleIconComponent
|
||||
import EdgeEffect
|
||||
import MultilineTextComponent
|
||||
|
||||
func getLocation(from message: EngineMessage) -> TelegramMediaMap? {
|
||||
if let poll = message.media.first(where: { $0 is TelegramMediaPoll } ) as? TelegramMediaPoll, let map = poll.attachedMedia as? TelegramMediaMap {
|
||||
|
|
@ -212,6 +217,12 @@ private func preparedTransition(from fromEntries: [LocationViewEntry], to toEntr
|
|||
return LocationViewTransaction(deletions: deletions, insertions: insertions, updates: updates, gotTravelTimes: gotTravelTimes, count: toEntries.count, animated: animated)
|
||||
}
|
||||
|
||||
enum LocationViewRightBarButton {
|
||||
case none
|
||||
case share
|
||||
case showAll
|
||||
}
|
||||
|
||||
public enum LocationViewLocation: Equatable {
|
||||
case initial
|
||||
case user
|
||||
|
|
@ -268,6 +279,7 @@ public struct LocationViewState {
|
|||
|
||||
final class LocationViewControllerNode: ViewControllerTracingNode, CLLocationManagerDelegate {
|
||||
private let context: AccountContext
|
||||
private weak var controller: LocationViewController?
|
||||
private var presentationData: PresentationData
|
||||
private let presentationDataPromise: Promise<PresentationData>
|
||||
private var subject: EngineMessage
|
||||
|
|
@ -276,9 +288,14 @@ final class LocationViewControllerNode: ViewControllerTracingNode, CLLocationMan
|
|||
private let isStoryLocation: Bool
|
||||
private let isPreview: Bool
|
||||
|
||||
private var rightBarButtonAction: LocationViewRightBarButton = .none
|
||||
|
||||
private let topEdgeEffectView = EdgeEffectView()
|
||||
private let buttons = ComponentView<Empty>()
|
||||
private let title = ComponentView<Empty>()
|
||||
|
||||
private let listNode: ListView
|
||||
let headerNode: LocationMapHeaderNode
|
||||
private let optionsNode: LocationOptionsNode
|
||||
|
||||
private var enqueuedTransitions: [LocationViewTransaction] = []
|
||||
|
||||
|
|
@ -302,8 +319,9 @@ final class LocationViewControllerNode: ViewControllerTracingNode, CLLocationMan
|
|||
}
|
||||
private let travelTimesPromise = Promise<[EngineMessage.Id: (Double, ExpectedTravelTime, ExpectedTravelTime, ExpectedTravelTime)]>([:])
|
||||
|
||||
init(context: AccountContext, presentationData: PresentationData, subject: EngineMessage, interaction: LocationViewInteraction, locationManager: LocationManager, isStoryLocation: Bool, isPreview: Bool) {
|
||||
init(context: AccountContext, controller: LocationViewController, presentationData: PresentationData, subject: EngineMessage, interaction: LocationViewInteraction, locationManager: LocationManager, isStoryLocation: Bool, isPreview: Bool) {
|
||||
self.context = context
|
||||
self.controller = controller
|
||||
self.presentationData = presentationData
|
||||
self.presentationDataPromise = Promise(presentationData)
|
||||
self.subject = subject
|
||||
|
|
@ -316,7 +334,7 @@ final class LocationViewControllerNode: ViewControllerTracingNode, CLLocationMan
|
|||
self.statePromise = Promise(self.state)
|
||||
|
||||
self.listNode = ListViewImpl()
|
||||
self.listNode.backgroundColor = self.presentationData.theme.list.plainBackgroundColor
|
||||
self.listNode.backgroundColor = .clear //self.presentationData.theme.list.plainBackgroundColor
|
||||
self.listNode.verticalScrollIndicatorColor = UIColor(white: 0.0, alpha: 0.3)
|
||||
self.listNode.verticalScrollIndicatorFollowsOverscroll = true
|
||||
self.listNode.accessibilityPageScrolledString = { row, count in
|
||||
|
|
@ -326,29 +344,24 @@ final class LocationViewControllerNode: ViewControllerTracingNode, CLLocationMan
|
|||
var setupProximityNotificationImpl: ((Bool) -> Void)?
|
||||
self.headerNode = LocationMapHeaderNode(
|
||||
presentationData: presentationData,
|
||||
glass: false,
|
||||
isPreview: true,
|
||||
glass: true,
|
||||
isPreview: self.isPreview,
|
||||
toggleMapModeSelection: interaction.toggleMapModeSelection,
|
||||
updateMapMode: interaction.updateMapMode,
|
||||
goToUserLocation: interaction.toggleTrackingMode,
|
||||
setupProximityNotification: { reset in
|
||||
setupProximityNotificationImpl?(reset)
|
||||
})
|
||||
//self.headerNode.mapNode.isRotateEnabled = false
|
||||
|
||||
self.optionsNode = LocationOptionsNode(presentationData: presentationData, updateMapMode: interaction.updateMapMode)
|
||||
|
||||
setupProximityNotificationImpl?(reset)
|
||||
}
|
||||
)
|
||||
|
||||
super.init()
|
||||
|
||||
self.backgroundColor = self.presentationData.theme.list.plainBackgroundColor
|
||||
self.backgroundColor = .red // self.presentationData.theme.list.plainBackgroundColor
|
||||
|
||||
if !self.isPreview {
|
||||
self.addSubnode(self.listNode)
|
||||
}
|
||||
self.addSubnode(self.headerNode)
|
||||
if !self.isPreview {
|
||||
self.addSubnode(self.optionsNode)
|
||||
}
|
||||
|
||||
let userLocation: Signal<CLLocation?, NoError> = .single(nil)
|
||||
|> then(
|
||||
|
|
@ -707,7 +720,7 @@ final class LocationViewControllerNode: ViewControllerTracingNode, CLLocationMan
|
|||
} else {
|
||||
rightBarButtonAction = .share
|
||||
}
|
||||
strongSelf.interaction.updateRightBarButton(rightBarButtonAction)
|
||||
strongSelf.rightBarButtonAction = rightBarButtonAction
|
||||
|
||||
if let (layout, navigationBarHeight) = strongSelf.validLayout {
|
||||
var updateLayout = false
|
||||
|
|
@ -800,10 +813,9 @@ final class LocationViewControllerNode: ViewControllerTracingNode, CLLocationMan
|
|||
self.presentationData = presentationData
|
||||
self.presentationDataPromise.set(.single(presentationData))
|
||||
|
||||
self.backgroundColor = self.presentationData.theme.list.plainBackgroundColor
|
||||
self.listNode.backgroundColor = self.presentationData.theme.list.plainBackgroundColor
|
||||
self.backgroundColor = .red //self.presentationData.theme.list.plainBackgroundColor
|
||||
self.listNode.backgroundColor = .clear // self.presentationData.theme.list.plainBackgroundColor
|
||||
self.headerNode.updatePresentationData(self.presentationData)
|
||||
self.optionsNode.updatePresentationData(self.presentationData)
|
||||
}
|
||||
|
||||
func updateState(_ f: (LocationViewState) -> LocationViewState) {
|
||||
|
|
@ -970,10 +982,10 @@ final class LocationViewControllerNode: ViewControllerTracingNode, CLLocationMan
|
|||
} else {
|
||||
headerHeight = topInset + overlap
|
||||
}
|
||||
let headerFrame = CGRect(origin: CGPoint(), size: CGSize(width: layout.size.width, height: headerHeight))
|
||||
let headerFrame = CGRect(origin: CGPoint(), size: CGSize(width: layout.size.width, height: layout.size.height))
|
||||
transition.updateFrame(node: self.headerNode, frame: headerFrame)
|
||||
|
||||
self.headerNode.updateLayout(layout: layout, navigationBarHeight: navigationHeight, topPadding: self.state.displayingMapModeOptions ? optionsHeight : 0.0, controlsTopPadding: self.state.displayingMapModeOptions ? optionsHeight : 0.0, controlsBottomPadding: 0.0, offset: 0.0, size: headerFrame.size, transition: transition)
|
||||
self.headerNode.updateLayout(layout: layout, navigationBarHeight: navigationHeight, topPadding: self.state.displayingMapModeOptions ? optionsHeight : 0.0, controlsTopPadding: 0.0, controlsBottomPadding: 0.0, offset: 0.0, size: CGSize(width: headerFrame.width, height: headerHeight), transition: transition)
|
||||
|
||||
let (duration, curve) = listViewAnimationDurationAndCurve(transition: transition)
|
||||
|
||||
|
|
@ -982,18 +994,118 @@ final class LocationViewControllerNode: ViewControllerTracingNode, CLLocationMan
|
|||
|
||||
let listFrame: CGRect = CGRect(origin: CGPoint(), size: layout.size)
|
||||
transition.updateFrame(node: self.listNode, frame: listFrame)
|
||||
|
||||
if !self.isPreview {
|
||||
let topEdgeEffectFrame = CGRect(origin: .zero, size: CGSize(width: layout.size.width, height: 80.0))
|
||||
transition.updateFrame(view: self.topEdgeEffectView, frame: topEdgeEffectFrame)
|
||||
self.topEdgeEffectView.update(content: self.headerNode.mapNode.mapMode == .map ? self.presentationData.theme.list.plainBackgroundColor : .clear, blur: true, alpha: 0.65, rect: topEdgeEffectFrame, edge: .top, edgeSize: topEdgeEffectFrame.height, transition: ComponentTransition(transition))
|
||||
if self.topEdgeEffectView.superview == nil {
|
||||
self.view.addSubview(self.topEdgeEffectView)
|
||||
}
|
||||
|
||||
let leftControlItems: [GlassControlGroupComponent.Item] = [
|
||||
GlassControlGroupComponent.Item(
|
||||
id: AnyHashable("close"),
|
||||
content: .icon("Navigation/Close"),
|
||||
action: { [weak self] in
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
self.controller?.dismiss()
|
||||
}
|
||||
)
|
||||
]
|
||||
var rightControlItems: [GlassControlGroupComponent.Item] = []
|
||||
switch self.rightBarButtonAction {
|
||||
case .none:
|
||||
break
|
||||
case .share:
|
||||
rightControlItems.append(
|
||||
GlassControlGroupComponent.Item(
|
||||
id: AnyHashable("share"),
|
||||
content: .icon("Navigation/Share"),
|
||||
action: { [weak self] in
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
self.interaction.share()
|
||||
}
|
||||
)
|
||||
)
|
||||
case .showAll:
|
||||
rightControlItems.append(
|
||||
GlassControlGroupComponent.Item(
|
||||
id: AnyHashable("share"),
|
||||
content: .text(self.presentationData.strings.Map_LiveLocationShowAll),
|
||||
action: { [weak self] in
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
self.showAll()
|
||||
}
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
let barButtonSideInset: CGFloat = 16.0
|
||||
let buttonsSize = self.buttons.update(
|
||||
transition: ComponentTransition(transition),
|
||||
component: AnyComponent(GlassControlPanelComponent(
|
||||
theme: self.presentationData.theme,
|
||||
leftItem: GlassControlPanelComponent.Item(
|
||||
items: leftControlItems,
|
||||
background: .panel
|
||||
),
|
||||
centralItem: nil,
|
||||
rightItem: rightControlItems.isEmpty ? nil : GlassControlPanelComponent.Item(
|
||||
items: rightControlItems,
|
||||
background: .panel
|
||||
),
|
||||
centerAlignmentIfPossible: true,
|
||||
isDark: self.presentationData.theme.overallDarkAppearance
|
||||
)),
|
||||
environment: {},
|
||||
containerSize: CGSize(width: layout.size.width - barButtonSideInset * 2.0 - layout.safeInsets.left - layout.safeInsets.right, height: 44.0)
|
||||
)
|
||||
let buttonsFrame = CGRect(origin: CGPoint(x: barButtonSideInset + layout.safeInsets.left, y: barButtonSideInset), size: buttonsSize)
|
||||
if let view = self.buttons.view {
|
||||
if view.superview == nil {
|
||||
self.view.addSubview(view)
|
||||
}
|
||||
view.bounds = CGRect(origin: .zero, size: buttonsFrame.size)
|
||||
view.center = buttonsFrame.center
|
||||
}
|
||||
|
||||
let titleSize = self.title.update(
|
||||
transition: ComponentTransition(transition),
|
||||
component: AnyComponent(
|
||||
MultilineTextComponent(
|
||||
text: .plain(
|
||||
NSAttributedString(
|
||||
string: self.presentationData.strings.Map_LocationTitle,
|
||||
font: Font.semibold(17.0),
|
||||
textColor: self.headerNode.mapNode.mapMode == .map ? self.presentationData.theme.rootController.navigationBar.primaryTextColor : .white
|
||||
)
|
||||
)
|
||||
)
|
||||
),
|
||||
environment: {},
|
||||
containerSize: CGSize(width: 200.0, height: 40.0)
|
||||
)
|
||||
let titleFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((layout.size.width - titleSize.width) / 2.0), y: floorToScreenPixels((navigationHeight - titleSize.height) / 2.0) + 3.0), size: titleSize)
|
||||
if let titleView = self.title.view {
|
||||
if titleView.superview == nil {
|
||||
self.view.addSubview(titleView)
|
||||
}
|
||||
transition.updateFrame(view: titleView, frame: titleFrame)
|
||||
}
|
||||
}
|
||||
|
||||
if isFirstLayout {
|
||||
while !self.enqueuedTransitions.isEmpty {
|
||||
self.dequeueTransition()
|
||||
}
|
||||
}
|
||||
|
||||
let optionsOffset: CGFloat = self.state.displayingMapModeOptions ? navigationHeight : navigationHeight - optionsHeight
|
||||
let optionsFrame = CGRect(x: 0.0, y: optionsOffset, width: layout.size.width, height: optionsHeight)
|
||||
transition.updateFrame(node: self.optionsNode, frame: optionsFrame)
|
||||
self.optionsNode.updateLayout(size: optionsFrame.size, leftInset: layout.safeInsets.left, rightInset: layout.safeInsets.right, transition: transition)
|
||||
self.optionsNode.isUserInteractionEnabled = self.state.displayingMapModeOptions
|
||||
}
|
||||
|
||||
var coordinate: Signal<CLLocationCoordinate2D, NoError> {
|
||||
|
|
|
|||
|
|
@ -240,8 +240,6 @@ public final class MediaPickerScreenImpl: ViewController, MediaPickerScreen, Att
|
|||
private let cancelButtonNode: WebAppCancelButtonNode
|
||||
|
||||
private var buttons: ComponentView<Empty>?
|
||||
private var cancelButton: ComponentView<Empty>?
|
||||
private var rightButton: ComponentView<Empty>?
|
||||
private let moreButtonPlayOnce = ActionSlot<Void>()
|
||||
|
||||
private let moreButtonNode: MoreButtonNode
|
||||
|
|
@ -2104,7 +2102,6 @@ public final class MediaPickerScreenImpl: ViewController, MediaPickerScreen, Att
|
|||
}
|
||||
|
||||
if case .glass = style {
|
||||
self.cancelButton = ComponentView()
|
||||
self.buttons = ComponentView()
|
||||
}
|
||||
self.cancelButtonNode = WebAppCancelButtonNode(theme: self.presentationData.theme, strings: self.presentationData.strings)
|
||||
|
|
|
|||
|
|
@ -250,6 +250,7 @@ static void initializeMapping()
|
|||
mimeToExtension[@"text/plain"] = @"text"; extensionToMime[@"text"] = @"text/plain";
|
||||
mimeToExtension[@"text/plain"] = @"diff"; extensionToMime[@"diff"] = @"text/plain";
|
||||
mimeToExtension[@"text/plain"] = @"po"; extensionToMime[@"po"] = @"text/plain"; // reserve "pot" for vnd.ms-powerpoint
|
||||
mimeToExtension[@"text/markdown"] = @"md"; extensionToMime[@"md"] = @"text/markdown";
|
||||
mimeToExtension[@"text/richtext"] = @"rtx"; extensionToMime[@"rtx"] = @"text/richtext";
|
||||
mimeToExtension[@"text/rtf"] = @"rtf"; extensionToMime[@"rtf"] = @"text/rtf";
|
||||
mimeToExtension[@"text/texmacs"] = @"ts"; extensionToMime[@"ts"] = @"text/texmacs";
|
||||
|
|
|
|||
|
|
@ -373,7 +373,7 @@ final class SharePeersContainerNode: ASDisplayNode, ShareContentContainerNode {
|
|||
self.contentOffsetUpdated = f
|
||||
}
|
||||
|
||||
private func calculateMetrics(size: CGSize, additionalBottomInset: CGFloat) -> (topInset: CGFloat, itemWidth: CGFloat) {
|
||||
private func calculateMetrics(size: CGSize, additionalBottomInset: CGFloat, isEmbedded: Bool) -> (topInset: CGFloat, itemWidth: CGFloat) {
|
||||
let itemCount = self.entries.count
|
||||
|
||||
let itemInsets = UIEdgeInsets(top: 0.0, left: 12.0, bottom: 0.0, right: 12.0)
|
||||
|
|
@ -394,7 +394,7 @@ final class SharePeersContainerNode: ASDisplayNode, ShareContentContainerNode {
|
|||
}
|
||||
let initiallyRevealedRowCount = min(minimallyRevealedRowCount, CGFloat(rowCount))
|
||||
|
||||
let gridTopInset = max(0.0, size.height - floor(initiallyRevealedRowCount * itemWidth) - 14.0 - additionalBottomInset)
|
||||
let gridTopInset = isEmbedded ? 136.0 : max(0.0, size.height - floor(initiallyRevealedRowCount * itemWidth) - 14.0 - additionalBottomInset)
|
||||
return (gridTopInset, itemWidth)
|
||||
}
|
||||
|
||||
|
|
@ -569,10 +569,15 @@ final class SharePeersContainerNode: ASDisplayNode, ShareContentContainerNode {
|
|||
}
|
||||
}
|
||||
|
||||
var isEmbedded = false
|
||||
func updateLayout(size: CGSize, isLandscape: Bool, bottomInset: CGFloat, transition: ContainedViewLayoutTransition) {
|
||||
let firstLayout = self.validLayout == nil
|
||||
self.validLayout = (size, bottomInset)
|
||||
|
||||
self.contentTitleNode.isHidden = self.isEmbedded
|
||||
self.contentSubtitleNode.isHidden = self.isEmbedded
|
||||
self.searchButtonNode.isHidden = self.isEmbedded
|
||||
|
||||
let gridLayoutTransition: ContainedViewLayoutTransition
|
||||
if firstLayout {
|
||||
gridLayoutTransition = .immediate
|
||||
|
|
@ -582,7 +587,7 @@ final class SharePeersContainerNode: ASDisplayNode, ShareContentContainerNode {
|
|||
self.overrideGridOffsetTransition = nil
|
||||
}
|
||||
|
||||
let (gridTopInset, itemWidth) = self.calculateMetrics(size: size, additionalBottomInset: bottomInset)
|
||||
let (gridTopInset, itemWidth) = self.calculateMetrics(size: size, additionalBottomInset: bottomInset, isEmbedded: self.isEmbedded)
|
||||
|
||||
var scrollToItem: GridNodeScrollToItem?
|
||||
if let ensurePeerVisibleOnLayout = self.ensurePeerVisibleOnLayout {
|
||||
|
|
@ -680,8 +685,8 @@ final class SharePeersContainerNode: ASDisplayNode, ShareContentContainerNode {
|
|||
self.contentTitleNode.isHidden = true
|
||||
self.contentSubtitleNode.isHidden = true
|
||||
} else {
|
||||
self.contentTitleNode.isHidden = false
|
||||
self.contentSubtitleNode.isHidden = false
|
||||
self.contentTitleNode.isHidden = self.isEmbedded
|
||||
self.contentSubtitleNode.isHidden = self.isEmbedded
|
||||
|
||||
var subtitleText = self.strings.ShareMenu_SelectChats
|
||||
if !self.controllerInteraction.selectedPeers.isEmpty {
|
||||
|
|
|
|||
|
|
@ -1413,7 +1413,7 @@ public final class SparseItemGridScrollingArea: ASDisplayNode {
|
|||
}
|
||||
|
||||
override public func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
|
||||
if self.dateIndicator.alpha <= 0.01 {
|
||||
if self.dateIndicator.alpha <= 0.01 || !self.isDateIndicatorVisible {
|
||||
return nil
|
||||
}
|
||||
if self.dateIndicator.frame.offsetBy(dx: self.dateIndicatorContainer.frame.minX, dy: self.dateIndicatorContainer.frame.minY).contains(point) {
|
||||
|
|
|
|||
|
|
@ -132,6 +132,12 @@ public enum PresentationResourceKey: Int32 {
|
|||
case chatListGiftIcon
|
||||
case chatListLocationIcon
|
||||
case chatListPollIcon
|
||||
case chatListTodoIcon
|
||||
case chatListGameIcon
|
||||
case chatListCallIncomingIcon
|
||||
case chatListCallOutgoingIcon
|
||||
case chatListCallVideoIncomingIcon
|
||||
case chatListCallVideoOutgoingIcon
|
||||
|
||||
case chatListGeneralTopicIcon
|
||||
case chatListGeneralTopicTemplateIcon
|
||||
|
|
|
|||
|
|
@ -313,6 +313,42 @@ public struct PresentationResourcesChatList {
|
|||
})
|
||||
}
|
||||
|
||||
public static func todoIcon(_ theme: PresentationTheme) -> UIImage? {
|
||||
return theme.image(PresentationResourceKey.chatListTodoIcon.rawValue, { theme in
|
||||
return generateTintedImage(image: UIImage(bundleImageName: "Chat List/TodoIcon"), color: theme.chatList.muteIconColor)
|
||||
})
|
||||
}
|
||||
|
||||
public static func gameIcon(_ theme: PresentationTheme) -> UIImage? {
|
||||
return theme.image(PresentationResourceKey.chatListGameIcon.rawValue, { theme in
|
||||
return generateTintedImage(image: UIImage(bundleImageName: "Chat List/GameIcon"), color: theme.chatList.muteIconColor)
|
||||
})
|
||||
}
|
||||
|
||||
public static func callIncomingIcon(_ theme: PresentationTheme) -> UIImage? {
|
||||
return theme.image(PresentationResourceKey.chatListCallIncomingIcon.rawValue, { theme in
|
||||
return generateTintedImage(image: UIImage(bundleImageName: "Chat List/CallIncomingIcon"), color: theme.chatList.muteIconColor)
|
||||
})
|
||||
}
|
||||
|
||||
public static func callOutgoingIcon(_ theme: PresentationTheme) -> UIImage? {
|
||||
return theme.image(PresentationResourceKey.chatListCallOutgoingIcon.rawValue, { theme in
|
||||
return generateTintedImage(image: UIImage(bundleImageName: "Chat List/CallOutgoingIcon"), color: theme.chatList.muteIconColor)
|
||||
})
|
||||
}
|
||||
|
||||
public static func callVideoIncomingIcon(_ theme: PresentationTheme) -> UIImage? {
|
||||
return theme.image(PresentationResourceKey.chatListCallVideoIncomingIcon.rawValue, { theme in
|
||||
return generateTintedImage(image: UIImage(bundleImageName: "Chat List/CallVideoIncomingIcon"), color: theme.chatList.muteIconColor)
|
||||
})
|
||||
}
|
||||
|
||||
public static func callVideoOutgoingIcon(_ theme: PresentationTheme) -> UIImage? {
|
||||
return theme.image(PresentationResourceKey.chatListCallVideoOutgoingIcon.rawValue, { theme in
|
||||
return generateTintedImage(image: UIImage(bundleImageName: "Chat List/CallVideoOutgoingIcon"), color: theme.chatList.muteIconColor)
|
||||
})
|
||||
}
|
||||
|
||||
public static func verifiedIcon(_ theme: PresentationTheme) -> UIImage? {
|
||||
return theme.image(PresentationResourceKey.chatListVerifiedIcon.rawValue, { theme in
|
||||
if let backgroundImage = UIImage(bundleImageName: "Chat List/PeerVerifiedIconBackground"), let foregroundImage = UIImage(bundleImageName: "Chat List/PeerVerifiedIconForeground") {
|
||||
|
|
|
|||
|
|
@ -526,6 +526,8 @@ swift_library(
|
|||
"//submodules/TelegramUI/Components/RankChatPreviewItem",
|
||||
"//submodules/TelegramUI/Components/TextProcessingScreen",
|
||||
"//submodules/TelegramUI/Components/CreateBotScreen",
|
||||
"//submodules/TelegramUI/Components/ShareScreen",
|
||||
"//submodules/Utils/AutomationBridge",
|
||||
] + select({
|
||||
"@build_bazel_rules_apple//apple:ios_arm64": appcenter_targets,
|
||||
"//build-system:ios_sim_arm64": [],
|
||||
|
|
|
|||
|
|
@ -17,6 +17,7 @@ swift_library(
|
|||
"//submodules/ComponentFlow",
|
||||
"//submodules/Components/ViewControllerComponent",
|
||||
"//submodules/Components/ComponentDisplayAdapters",
|
||||
"//submodules/Components/ResizableSheetComponent",
|
||||
"//submodules/Components/MultilineTextComponent",
|
||||
"//submodules/Components/BundleIconComponent",
|
||||
"//submodules/TelegramPresentationData",
|
||||
|
|
|
|||
|
|
@ -227,7 +227,7 @@ final class AdminUserActionsPeerComponent: Component {
|
|||
let titleSize = self.title.update(
|
||||
transition: .immediate,
|
||||
component: AnyComponent(MultilineTextComponent(
|
||||
text: .plain(NSAttributedString(string: component.title, font: Font.semibold(component.baseFontSize), textColor: component.theme.list.itemPrimaryTextColor))
|
||||
text: .plain(NSAttributedString(string: component.title, font: Font.medium(component.baseFontSize), textColor: component.theme.list.itemPrimaryTextColor))
|
||||
)),
|
||||
environment: {},
|
||||
containerSize: CGSize(width: maxTextSize, height: 100.0)
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
|
|
@ -465,7 +465,7 @@ public final class ButtonComponent: Component {
|
|||
private var glassShadowCornerRadius: CGFloat?
|
||||
private var glassHighlightContainerView: UIView?
|
||||
private let button: HighlightTrackingButton
|
||||
private let legacyGlassHighlightRecognizer: GlassHighlightGestureRecognizer
|
||||
private let glassHighlightRecognizer: GlassHighlightGestureRecognizer
|
||||
|
||||
private var shimmeringView: ButtonShimmeringView?
|
||||
private var chromeView: UIImageView?
|
||||
|
|
@ -480,7 +480,7 @@ public final class ButtonComponent: Component {
|
|||
self.containerView.isUserInteractionEnabled = false
|
||||
|
||||
self.button = HighlightTrackingButton()
|
||||
self.legacyGlassHighlightRecognizer = GlassHighlightGestureRecognizer(target: nil, action: nil)
|
||||
self.glassHighlightRecognizer = GlassHighlightGestureRecognizer(target: nil, action: nil)
|
||||
|
||||
super.init(frame: frame)
|
||||
|
||||
|
|
@ -489,8 +489,8 @@ public final class ButtonComponent: Component {
|
|||
|
||||
self.addSubview(self.containerView)
|
||||
self.addSubview(self.button)
|
||||
self.addGestureRecognizer(self.legacyGlassHighlightRecognizer)
|
||||
self.legacyGlassHighlightRecognizer.isEnabled = false
|
||||
self.addGestureRecognizer(self.glassHighlightRecognizer)
|
||||
self.glassHighlightRecognizer.isEnabled = false
|
||||
|
||||
self.button.addTarget(self, action: #selector(self.pressed), for: .touchUpInside)
|
||||
|
||||
|
|
@ -521,33 +521,18 @@ public final class ButtonComponent: Component {
|
|||
preconditionFailure()
|
||||
}
|
||||
|
||||
private func ensureGlassShadowView() -> UIImageView {
|
||||
if let glassShadowView = self.glassShadowView {
|
||||
return glassShadowView
|
||||
}
|
||||
let glassShadowView = UIImageView()
|
||||
glassShadowView.isUserInteractionEnabled = false
|
||||
self.glassShadowView = glassShadowView
|
||||
return glassShadowView
|
||||
}
|
||||
|
||||
private func ensureGlassHighlightContainerView() -> UIView {
|
||||
if let glassHighlightContainerView = self.glassHighlightContainerView {
|
||||
return glassHighlightContainerView
|
||||
}
|
||||
let glassHighlightContainerView = UIView()
|
||||
glassHighlightContainerView.isUserInteractionEnabled = false
|
||||
glassHighlightContainerView.clipsToBounds = true
|
||||
self.glassHighlightContainerView = glassHighlightContainerView
|
||||
return glassHighlightContainerView
|
||||
}
|
||||
|
||||
private func removeLegacyGlassEffectViews() {
|
||||
self.legacyGlassHighlightRecognizer.isEnabled = false
|
||||
self.legacyGlassHighlightRecognizer.highlightContainerView = nil
|
||||
private func removeGlassEffect(transition: ComponentTransition) {
|
||||
self.glassHighlightRecognizer.isEnabled = false
|
||||
self.glassHighlightRecognizer.highlightContainerView = nil
|
||||
|
||||
if let glassShadowView = self.glassShadowView, glassShadowView.superview != nil {
|
||||
glassShadowView.removeFromSuperview()
|
||||
if transition.animation.isImmediate {
|
||||
glassShadowView.removeFromSuperview()
|
||||
} else {
|
||||
transition.setAlpha(view: glassShadowView, alpha: 0.0, completion: { _ in
|
||||
glassShadowView.removeFromSuperview()
|
||||
})
|
||||
}
|
||||
}
|
||||
if let glassHighlightContainerView = self.glassHighlightContainerView, glassHighlightContainerView.superview != nil {
|
||||
glassHighlightContainerView.removeFromSuperview()
|
||||
|
|
@ -558,10 +543,17 @@ public final class ButtonComponent: Component {
|
|||
self.layer.sublayerTransform = CATransform3DIdentity
|
||||
}
|
||||
|
||||
private func updateLegacyGlassEffectViews(component: ButtonComponent, size: CGSize, cornerRadius: CGFloat, transition: ComponentTransition) {
|
||||
private func updateGlassEffect(component: ButtonComponent, size: CGSize, cornerRadius: CGFloat, transition: ComponentTransition) {
|
||||
let shadowInset: CGFloat = 48.0
|
||||
|
||||
let glassShadowView = self.ensureGlassShadowView()
|
||||
let glassShadowView: UIImageView
|
||||
if let current = self.glassShadowView {
|
||||
glassShadowView = current
|
||||
} else {
|
||||
glassShadowView = UIImageView()
|
||||
glassShadowView.isUserInteractionEnabled = false
|
||||
self.glassShadowView = glassShadowView
|
||||
}
|
||||
if glassShadowView.superview == nil {
|
||||
self.insertSubview(glassShadowView, at: 0)
|
||||
} else {
|
||||
|
|
@ -574,7 +566,15 @@ public final class ButtonComponent: Component {
|
|||
transition.setFrame(view: glassShadowView, frame: CGRect(origin: .zero, size: size).insetBy(dx: -shadowInset, dy: -shadowInset))
|
||||
transition.setAlpha(view: glassShadowView, alpha: 1.0)
|
||||
|
||||
let glassHighlightContainerView = self.ensureGlassHighlightContainerView()
|
||||
let glassHighlightContainerView: UIView
|
||||
if let current = self.glassHighlightContainerView {
|
||||
glassHighlightContainerView = current
|
||||
} else {
|
||||
glassHighlightContainerView = UIView()
|
||||
glassHighlightContainerView.isUserInteractionEnabled = false
|
||||
glassHighlightContainerView.clipsToBounds = true
|
||||
self.glassHighlightContainerView = glassHighlightContainerView
|
||||
}
|
||||
if glassHighlightContainerView.superview == nil {
|
||||
self.insertSubview(glassHighlightContainerView, aboveSubview: self.containerView)
|
||||
} else if self.button.superview === self {
|
||||
|
|
@ -585,8 +585,8 @@ public final class ButtonComponent: Component {
|
|||
transition.setFrame(view: glassHighlightContainerView, frame: CGRect(origin: .zero, size: size))
|
||||
transition.setCornerRadius(layer: glassHighlightContainerView.layer, cornerRadius: cornerRadius)
|
||||
|
||||
self.legacyGlassHighlightRecognizer.highlightContainerView = glassHighlightContainerView
|
||||
self.legacyGlassHighlightRecognizer.isEnabled = component.isEnabled && !component.displaysProgress
|
||||
self.glassHighlightRecognizer.highlightContainerView = glassHighlightContainerView
|
||||
self.glassHighlightRecognizer.isEnabled = component.isEnabled && !component.displaysProgress
|
||||
}
|
||||
|
||||
@objc private func pressed() {
|
||||
|
|
@ -694,10 +694,10 @@ public final class ButtonComponent: Component {
|
|||
transition.setCornerRadius(layer: self.containerView.layer, cornerRadius: cornerRadius)
|
||||
}
|
||||
|
||||
if component.background.style == .glass {
|
||||
self.updateLegacyGlassEffectViews(component: component, size: size, cornerRadius: cornerRadius, transition: transition)
|
||||
if component.background.style == .glass, component.background.color.alpha > 1.0 - .ulpOfOne {
|
||||
self.updateGlassEffect(component: component, size: size, cornerRadius: cornerRadius, transition: transition)
|
||||
} else {
|
||||
self.removeLegacyGlassEffectViews()
|
||||
self.removeGlassEffect(transition: transition)
|
||||
}
|
||||
|
||||
if let contentView = contentItem.view.view {
|
||||
|
|
|
|||
|
|
@ -2684,8 +2684,9 @@ public class ChatMessagePollBubbleContentNode: ChatMessageBubbleContentNode {
|
|||
var pollOptionsFinalizeLayouts: [(hasResult: Bool, layout: (CGFloat) -> (CGSize, (Bool, Bool, Bool) -> ChatMessagePollOptionNode))] = []
|
||||
var addOptionFinalizeLayout: ((CGFloat) -> (CGSize, (Bool, Bool) -> ChatMessagePollAddOptionNode))?
|
||||
var orderedPollOptions: [(Int, TelegramMediaPollOption)] = []
|
||||
|
||||
var isRestricted = false
|
||||
if let poll = poll {
|
||||
var isRestricted = false
|
||||
if !poll.countries.isEmpty, let accountCountry = item.associatedData.accountCountry, !poll.countries.contains(accountCountry) {
|
||||
isRestricted = true
|
||||
}
|
||||
|
|
@ -2842,430 +2843,428 @@ public class ChatMessagePollBubbleContentNode: ChatMessageBubbleContentNode {
|
|||
let buttonViewResultsTextFrame = CGRect(origin: CGPoint(x: floor((resultSize.width - buttonViewResultsTextLayout.size.width) / 2.0), y: optionsButtonSpacing), size: buttonViewResultsTextLayout.size)
|
||||
|
||||
return (resultSize, { [weak self] animation, synchronousLoad, _ in
|
||||
if let strongSelf = self {
|
||||
strongSelf.item = item
|
||||
strongSelf.poll = poll
|
||||
|
||||
let cachedLayout = strongSelf.textNode.textNode.cachedLayout
|
||||
if case .System = animation {
|
||||
if let cachedLayout = cachedLayout {
|
||||
if !cachedLayout.areLinesEqual(to: textLayout) {
|
||||
if let textContents = strongSelf.textNode.textNode.contents {
|
||||
let fadeNode = ASDisplayNode()
|
||||
fadeNode.displaysAsynchronously = false
|
||||
fadeNode.contents = textContents
|
||||
fadeNode.frame = strongSelf.textNode.textNode.frame
|
||||
fadeNode.isLayerBacked = true
|
||||
strongSelf.addSubnode(fadeNode)
|
||||
fadeNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { [weak fadeNode] _ in
|
||||
fadeNode?.removeFromSupernode()
|
||||
})
|
||||
strongSelf.textNode.textNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.15)
|
||||
}
|
||||
if let strongSelf = self {
|
||||
strongSelf.item = item
|
||||
strongSelf.poll = poll
|
||||
|
||||
let cachedLayout = strongSelf.textNode.textNode.cachedLayout
|
||||
if case .System = animation {
|
||||
if let cachedLayout = cachedLayout {
|
||||
if !cachedLayout.areLinesEqual(to: textLayout) {
|
||||
if let textContents = strongSelf.textNode.textNode.contents {
|
||||
let fadeNode = ASDisplayNode()
|
||||
fadeNode.displaysAsynchronously = false
|
||||
fadeNode.contents = textContents
|
||||
fadeNode.frame = strongSelf.textNode.textNode.frame
|
||||
fadeNode.isLayerBacked = true
|
||||
strongSelf.addSubnode(fadeNode)
|
||||
fadeNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { [weak fadeNode] _ in
|
||||
fadeNode?.removeFromSupernode()
|
||||
})
|
||||
strongSelf.textNode.textNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.15)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let _ = textApply(TextNodeWithEntities.Arguments(
|
||||
context: item.context,
|
||||
cache: item.context.animationCache,
|
||||
renderer: item.context.animationRenderer,
|
||||
placeholderColor: incoming ? item.presentationData.theme.theme.chat.message.incoming.mediaPlaceholderColor : item.presentationData.theme.theme.chat.message.outgoing.mediaPlaceholderColor,
|
||||
attemptSynchronous: synchronousLoad)
|
||||
)
|
||||
let _ = typeApply()
|
||||
|
||||
var verticalOffset = textFrame.maxY + titleTypeSpacing + typeLayout.size.height + typeOptionsSpacing
|
||||
var updatedOptionNodes: [ChatMessagePollOptionNode] = []
|
||||
for i in 0 ..< optionNodesSizesAndApply.count {
|
||||
let (size, apply) = optionNodesSizesAndApply[i]
|
||||
var isRequesting = false
|
||||
if i < orderedPollOptions.count {
|
||||
if let inProgressOpaqueIds = item.controllerInteraction.pollActionState.pollMessageIdsInProgress[item.message.id] {
|
||||
isRequesting = inProgressOpaqueIds.contains(orderedPollOptions[i].1.opaqueIdentifier)
|
||||
}
|
||||
}
|
||||
|
||||
let _ = textApply(TextNodeWithEntities.Arguments(
|
||||
context: item.context,
|
||||
cache: item.context.animationCache,
|
||||
renderer: item.context.animationRenderer,
|
||||
placeholderColor: incoming ? item.presentationData.theme.theme.chat.message.incoming.mediaPlaceholderColor : item.presentationData.theme.theme.chat.message.outgoing.mediaPlaceholderColor,
|
||||
attemptSynchronous: synchronousLoad)
|
||||
)
|
||||
let _ = typeApply()
|
||||
|
||||
var verticalOffset = textFrame.maxY + titleTypeSpacing + typeLayout.size.height + typeOptionsSpacing
|
||||
var updatedOptionNodes: [ChatMessagePollOptionNode] = []
|
||||
for i in 0 ..< optionNodesSizesAndApply.count {
|
||||
let (size, apply) = optionNodesSizesAndApply[i]
|
||||
var isRequesting = false
|
||||
if i < orderedPollOptions.count {
|
||||
if let inProgressOpaqueIds = item.controllerInteraction.pollActionState.pollMessageIdsInProgress[item.message.id] {
|
||||
isRequesting = inProgressOpaqueIds.contains(orderedPollOptions[i].1.opaqueIdentifier)
|
||||
}
|
||||
let optionNode = apply(animation.isAnimated, isRequesting, synchronousLoad)
|
||||
let optionNodeFrame = CGRect(origin: CGPoint(x: layoutConstants.bubble.borderInset, y: verticalOffset), size: size)
|
||||
if optionNode.supernode !== strongSelf {
|
||||
strongSelf.addSubnode(optionNode)
|
||||
let option = optionNode.option
|
||||
|
||||
optionNode.pressed = { [weak self] in
|
||||
guard let self,
|
||||
let item = self.item,
|
||||
let option else {
|
||||
return
|
||||
}
|
||||
item.controllerInteraction.requestSelectMessagePollOptions(item.message.id, [option.opaqueIdentifier])
|
||||
}
|
||||
let optionNode = apply(animation.isAnimated, isRequesting, synchronousLoad)
|
||||
let optionNodeFrame = CGRect(origin: CGPoint(x: layoutConstants.bubble.borderInset, y: verticalOffset), size: size)
|
||||
if optionNode.supernode !== strongSelf {
|
||||
strongSelf.addSubnode(optionNode)
|
||||
let option = optionNode.option
|
||||
|
||||
optionNode.pressed = { [weak self] in
|
||||
guard let self,
|
||||
let item = self.item,
|
||||
let option else {
|
||||
return
|
||||
}
|
||||
optionNode.resultPressed = { [weak self] in
|
||||
guard let self,
|
||||
let item = self.item,
|
||||
let option else {
|
||||
return
|
||||
}
|
||||
if let poll {
|
||||
if case .public = poll.publicity {
|
||||
item.controllerInteraction.openMessagePollResults(item.message.id, option.opaqueIdentifier)
|
||||
} else {
|
||||
if "".isEmpty {
|
||||
let locale = localeWithStrings(item.presentationData.strings)
|
||||
let countryNames = poll.countries.map { id in
|
||||
if let countryName = locale.localizedString(forRegionCode: id) {
|
||||
return countryName
|
||||
} else {
|
||||
return id
|
||||
}
|
||||
}
|
||||
var countries: String = ""
|
||||
if countryNames.count == 1, let country = countryNames.first {
|
||||
countries = "**\(country)**"
|
||||
} else {
|
||||
for i in 0 ..< countryNames.count {
|
||||
countries.append("**\(countryNames[i])**")
|
||||
if i == countryNames.count - 2 {
|
||||
countries.append(item.presentationData.strings.Chat_Poll_Restriction_Country_CountriesLastDelimiter)
|
||||
} else if i < countryNames.count - 2 {
|
||||
countries.append(item.presentationData.strings.Chat_Poll_Restriction_Country_CountriesDelimiter)
|
||||
}
|
||||
}
|
||||
}
|
||||
//TODO:localize
|
||||
let controller = UndoOverlayController(
|
||||
presentationData: item.context.sharedContext.currentPresentationData.with { $0 },
|
||||
content: .banned(text: "Only users from \(countries) can vote."),
|
||||
elevatedLayout: true,
|
||||
position: .bottom,
|
||||
action: { _ in return true }
|
||||
)
|
||||
item.controllerInteraction.presentController(controller, nil)
|
||||
item.controllerInteraction.requestSelectMessagePollOptions(item.message.id, [option.opaqueIdentifier])
|
||||
}
|
||||
optionNode.resultPressed = { [weak self] in
|
||||
guard let self,
|
||||
let item = self.item,
|
||||
let option else {
|
||||
return
|
||||
}
|
||||
if let poll {
|
||||
if case .public = poll.publicity {
|
||||
item.controllerInteraction.openMessagePollResults(item.message.id, option.opaqueIdentifier)
|
||||
} else if isRestricted {
|
||||
let locale = localeWithStrings(item.presentationData.strings)
|
||||
let countryNames = poll.countries.map { id in
|
||||
if let countryName = locale.localizedString(forRegionCode: id) {
|
||||
return countryName
|
||||
} else {
|
||||
return id
|
||||
}
|
||||
}
|
||||
var countries: String = ""
|
||||
if countryNames.count == 1, let country = countryNames.first {
|
||||
countries = "**\(country)**"
|
||||
} else {
|
||||
for i in 0 ..< countryNames.count {
|
||||
countries.append("**\(countryNames[i])**")
|
||||
if i == countryNames.count - 2 {
|
||||
countries.append(item.presentationData.strings.Chat_Poll_Restriction_Country_CountriesLastDelimiter)
|
||||
} else if i < countryNames.count - 2 {
|
||||
countries.append(item.presentationData.strings.Chat_Poll_Restriction_Country_CountriesDelimiter)
|
||||
}
|
||||
}
|
||||
}
|
||||
//TODO:localize
|
||||
let controller = UndoOverlayController(
|
||||
presentationData: item.context.sharedContext.currentPresentationData.with { $0 },
|
||||
content: .banned(text: "Only users from \(countries) can vote."),
|
||||
elevatedLayout: true,
|
||||
position: .bottom,
|
||||
action: { _ in return true }
|
||||
)
|
||||
item.controllerInteraction.presentController(controller, nil)
|
||||
}
|
||||
}
|
||||
optionNode.selectionUpdated = { [weak self] in
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
self.updateSelection()
|
||||
}
|
||||
optionNode.longTapped = { [weak self] in
|
||||
guard let self,
|
||||
let item = self.item,
|
||||
let option else {
|
||||
return
|
||||
}
|
||||
item.controllerInteraction.pollOptionLongTap(option.opaqueIdentifier, ChatControllerInteraction.LongTapParams(message: item.message, contentNode: optionNode.contextSourceNode, messageNode: strongSelf, progress: nil))
|
||||
}
|
||||
optionNode.frame = optionNodeFrame
|
||||
} else {
|
||||
animation.animator.updateFrame(layer: optionNode.layer, frame: optionNodeFrame, completion: nil)
|
||||
}
|
||||
|
||||
if optionNode.currentResult != nil {
|
||||
verticalOffset += size.height - 7.0
|
||||
} else {
|
||||
verticalOffset += size.height
|
||||
}
|
||||
updatedOptionNodes.append(optionNode)
|
||||
optionNode.isUserInteractionEnabled = !strongSelf.newOptionIsFocused && item.controllerInteraction.pollActionState.pollMessageIdsInProgress[item.message.id] == nil
|
||||
optionNode.alpha = strongSelf.newOptionIsFocused ? 0.5 : 1.0
|
||||
|
||||
if i > 0 {
|
||||
optionNode.previousOptionNode = updatedOptionNodes[i - 1]
|
||||
} else {
|
||||
optionNode.previousOptionNode = nil
|
||||
}
|
||||
}
|
||||
for optionNode in strongSelf.optionNodes {
|
||||
if !updatedOptionNodes.contains(where: { $0 === optionNode }) {
|
||||
optionNode.removeFromSupernode()
|
||||
}
|
||||
}
|
||||
strongSelf.optionNodes = updatedOptionNodes
|
||||
strongSelf.updatePollOptionsInteraction(animated: animation.isAnimated)
|
||||
|
||||
if let (size, apply) = addOptionSizeAndApply {
|
||||
let isRequesting = item.controllerInteraction.pollActionState.pollMessageIdsInProgress[item.message.id] != nil
|
||||
let addOptionNode = apply(animation.isAnimated, isRequesting)
|
||||
let addOptionNodeFrame = CGRect(origin: CGPoint(x: layoutConstants.bubble.borderInset, y: verticalOffset), size: size)
|
||||
if addOptionNode.supernode !== strongSelf {
|
||||
strongSelf.addSubnode(addOptionNode)
|
||||
} else {
|
||||
animation.animator.updateFrame(layer: addOptionNode.layer, frame: addOptionNodeFrame, completion: nil)
|
||||
}
|
||||
addOptionNode.frame = addOptionNodeFrame
|
||||
addOptionNode.isUserInteractionEnabled = !isRequesting
|
||||
addOptionNode.textUpdated = { [weak self] text in
|
||||
self?.updateNewOptionText(text)
|
||||
}
|
||||
addOptionNode.heightUpdated = { [weak self] in
|
||||
self?.requestNewOptionLayoutUpdate()
|
||||
}
|
||||
addOptionNode.attachPressed = { [weak self] in
|
||||
self?.openNewOptionAttachment()
|
||||
}
|
||||
addOptionNode.mediaPressed = { [weak self] in
|
||||
self?.openNewOptionAttachment()
|
||||
}
|
||||
addOptionNode.modeSelectorPressed = { [weak self] in
|
||||
self?.toggleNewOptionInputMode()
|
||||
}
|
||||
addOptionNode.requestSave = { [weak self] in
|
||||
self?.buttonPressed()
|
||||
}
|
||||
addOptionNode.focusUpdated = { [weak self] focused in
|
||||
optionNode.selectionUpdated = { [weak self] in
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
self.updatePollAddOptionFocused(focused)
|
||||
self.updateSelection()
|
||||
}
|
||||
strongSelf.addOptionNode = addOptionNode
|
||||
optionNode.longTapped = { [weak self] in
|
||||
guard let self,
|
||||
let item = self.item,
|
||||
let option else {
|
||||
return
|
||||
}
|
||||
item.controllerInteraction.pollOptionLongTap(option.opaqueIdentifier, ChatControllerInteraction.LongTapParams(message: item.message, contentNode: optionNode.contextSourceNode, messageNode: strongSelf, progress: nil))
|
||||
}
|
||||
optionNode.frame = optionNodeFrame
|
||||
} else {
|
||||
animation.animator.updateFrame(layer: optionNode.layer, frame: optionNodeFrame, completion: nil)
|
||||
}
|
||||
|
||||
if optionNode.currentResult != nil {
|
||||
verticalOffset += size.height - 7.0
|
||||
} else {
|
||||
verticalOffset += size.height
|
||||
} else if let addOptionNode = strongSelf.addOptionNode {
|
||||
strongSelf.updatePollAddOptionFocused(false)
|
||||
strongSelf.addOptionNode = nil
|
||||
addOptionNode.removeFromSupernode()
|
||||
}
|
||||
updatedOptionNodes.append(optionNode)
|
||||
optionNode.isUserInteractionEnabled = !strongSelf.newOptionIsFocused && item.controllerInteraction.pollActionState.pollMessageIdsInProgress[item.message.id] == nil
|
||||
optionNode.alpha = strongSelf.newOptionIsFocused ? 0.5 : 1.0
|
||||
|
||||
if let poll = poll, let pendingNewOptionSubmissionText, let pendingNewOptionOptionCount = strongSelf.pendingNewOptionOptionCount, poll.options.count > pendingNewOptionOptionCount, poll.options.contains(where: { $0.text == pendingNewOptionSubmissionText }) {
|
||||
strongSelf.clearNewOptionInput()
|
||||
}
|
||||
|
||||
if textLayout.hasRTL {
|
||||
strongSelf.textNode.textNode.frame = CGRect(origin: CGPoint(x: resultSize.width - textFrame.size.width - textInsets.left - layoutConstants.text.bubbleInsets.right - additionalTextRightInset, y: textFrame.origin.y), size: textFrame.size)
|
||||
if i > 0 {
|
||||
optionNode.previousOptionNode = updatedOptionNodes[i - 1]
|
||||
} else {
|
||||
strongSelf.textNode.textNode.frame = textFrame
|
||||
optionNode.previousOptionNode = nil
|
||||
}
|
||||
let typeFrame = CGRect(origin: CGPoint(x: textFrame.minX, y: textFrame.maxY + titleTypeSpacing), size: typeLayout.size)
|
||||
animation.animator.updateFrame(layer: strongSelf.typeNode.layer, frame: typeFrame, completion: nil)
|
||||
|
||||
let deadlineTimeout = poll?.deadlineTimeout
|
||||
var displayDeadlineTimer = true
|
||||
var hasSelected = false
|
||||
|
||||
if let poll {
|
||||
if let voters = poll.results.voters {
|
||||
for voter in voters {
|
||||
if voter.selected {
|
||||
if case .quiz = poll.kind {
|
||||
displayDeadlineTimer = false
|
||||
} else {
|
||||
displayDeadlineTimer = !poll.isClosed
|
||||
}
|
||||
hasSelected = true
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var endDate: Int32?
|
||||
if let deadlineTimeout,
|
||||
message.id.namespace == Namespaces.Message.Cloud {
|
||||
let startDate: Int32
|
||||
if let forwardInfo = message.forwardInfo {
|
||||
startDate = forwardInfo.date
|
||||
} else {
|
||||
startDate = message.timestamp
|
||||
}
|
||||
endDate = startDate + deadlineTimeout
|
||||
}
|
||||
|
||||
if let poll, case .quiz = poll.kind, let deadlineTimeout, !isClosed {
|
||||
let timerNode: PollBubbleTimerNode
|
||||
if let current = strongSelf.timerNode {
|
||||
timerNode = current
|
||||
let timerTransition: ContainedViewLayoutTransition
|
||||
if animation.isAnimated {
|
||||
timerTransition = .animated(duration: 0.25, curve: .easeInOut)
|
||||
} else {
|
||||
timerTransition = .immediate
|
||||
}
|
||||
if displayDeadlineTimer {
|
||||
timerTransition.updateAlpha(node: timerNode, alpha: 1.0)
|
||||
} else {
|
||||
timerTransition.updateAlpha(node: timerNode, alpha: 0.0)
|
||||
}
|
||||
} else {
|
||||
timerNode = PollBubbleTimerNode()
|
||||
strongSelf.timerNode = timerNode
|
||||
strongSelf.addSubnode(timerNode)
|
||||
timerNode.reachedTimeout = {
|
||||
guard let strongSelf = self,
|
||||
let _ = strongSelf.item else {
|
||||
return
|
||||
}
|
||||
//item.controllerInteraction.requestMessageUpdate(item.message.id)
|
||||
}
|
||||
|
||||
let timerTransition: ContainedViewLayoutTransition
|
||||
if animation.isAnimated {
|
||||
timerTransition = .animated(duration: 0.25, curve: .easeInOut)
|
||||
} else {
|
||||
timerTransition = .immediate
|
||||
}
|
||||
if displayDeadlineTimer {
|
||||
timerNode.alpha = 0.0
|
||||
timerTransition.updateAlpha(node: timerNode, alpha: 1.0)
|
||||
} else {
|
||||
timerNode.alpha = 0.0
|
||||
}
|
||||
}
|
||||
timerNode.update(regularColor: messageTheme.secondaryTextColor, proximityColor: messageTheme.scamColor, timeout: deadlineTimeout, deadlineTimestamp: endDate)
|
||||
timerNode.frame = CGRect(origin: CGPoint(x: resultSize.width - layoutConstants.text.bubbleInsets.right, y: typeFrame.minY), size: CGSize())
|
||||
} else if let timerNode = strongSelf.timerNode {
|
||||
strongSelf.timerNode = nil
|
||||
|
||||
let timerTransition: ContainedViewLayoutTransition
|
||||
if animation.isAnimated {
|
||||
timerTransition = .animated(duration: 0.25, curve: .easeInOut)
|
||||
} else {
|
||||
timerTransition = .immediate
|
||||
}
|
||||
timerTransition.updateAlpha(node: timerNode, alpha: 0.0, completion: { [weak timerNode] _ in
|
||||
timerNode?.removeFromSupernode()
|
||||
})
|
||||
timerTransition.updateTransformScale(node: timerNode, scale: 0.1)
|
||||
}
|
||||
|
||||
var statusOffset: CGFloat = 0.0
|
||||
if let poll, case .poll = poll.kind, let endDate, !isClosed {
|
||||
let timerNode: DeadlineTimerNode
|
||||
if let current = strongSelf.deadlineTimerNode {
|
||||
timerNode = current
|
||||
let timerTransition: ContainedViewLayoutTransition
|
||||
if animation.isAnimated {
|
||||
timerTransition = .animated(duration: 0.25, curve: .easeInOut)
|
||||
} else {
|
||||
timerTransition = .immediate
|
||||
}
|
||||
if displayDeadlineTimer {
|
||||
timerTransition.updateAlpha(node: timerNode, alpha: 1.0)
|
||||
} else {
|
||||
timerTransition.updateAlpha(node: timerNode, alpha: 0.0)
|
||||
}
|
||||
} else {
|
||||
timerNode = DeadlineTimerNode()
|
||||
strongSelf.deadlineTimerNode = timerNode
|
||||
strongSelf.addSubnode(timerNode)
|
||||
|
||||
let timerTransition: ContainedViewLayoutTransition
|
||||
if animation.isAnimated {
|
||||
timerTransition = .animated(duration: 0.25, curve: .easeInOut)
|
||||
} else {
|
||||
timerTransition = .immediate
|
||||
}
|
||||
if displayDeadlineTimer {
|
||||
timerNode.alpha = 0.0
|
||||
timerTransition.updateAlpha(node: timerNode, alpha: 1.0)
|
||||
} else {
|
||||
timerNode.alpha = 0.0
|
||||
}
|
||||
}
|
||||
timerNode.update(size: resultSize, color: messageTheme.secondaryTextColor, deadlineTimeout: endDate, resultsHidden: poll.hideResultsUntilClose, strings: item.presentationData.strings)
|
||||
timerNode.frame = CGRect(origin: CGPoint(x: 0.0, y: verticalOffset + 31.0), size: CGSize(width: resultSize.width, height: 20.0))
|
||||
statusOffset += 6.0
|
||||
} else if let timerNode = strongSelf.deadlineTimerNode {
|
||||
strongSelf.deadlineTimerNode = nil
|
||||
|
||||
let timerTransition: ContainedViewLayoutTransition
|
||||
if animation.isAnimated {
|
||||
timerTransition = .animated(duration: 0.25, curve: .easeInOut)
|
||||
} else {
|
||||
timerTransition = .immediate
|
||||
}
|
||||
timerTransition.updateAlpha(node: timerNode, alpha: 0.0, completion: { [weak timerNode] _ in
|
||||
timerNode?.removeFromSupernode()
|
||||
})
|
||||
timerTransition.updateTransformScale(node: timerNode, scale: 0.1)
|
||||
}
|
||||
|
||||
let solutionButtonSize = CGSize(width: 32.0, height: 32.0)
|
||||
let solutionButtonFrame = CGRect(origin: CGPoint(x: resultSize.width - layoutConstants.text.bubbleInsets.right - solutionButtonSize.width + 5.0, y: typeFrame.minY - 16.0), size: solutionButtonSize)
|
||||
strongSelf.solutionButtonNode.frame = solutionButtonFrame
|
||||
|
||||
if (strongSelf.timerNode == nil || !displayDeadlineTimer), let poll = poll, case .quiz = poll.kind, let _ = poll.results.solution, (isClosed || hasSelected) {
|
||||
if strongSelf.solutionButtonNode.alpha.isZero {
|
||||
let timerTransition: ContainedViewLayoutTransition
|
||||
if animation.isAnimated {
|
||||
timerTransition = .animated(duration: 0.25, curve: .easeInOut)
|
||||
} else {
|
||||
timerTransition = .immediate
|
||||
}
|
||||
timerTransition.updateAlpha(node: strongSelf.solutionButtonNode, alpha: 1.0)
|
||||
}
|
||||
strongSelf.solutionButtonNode.update(size: solutionButtonSize, theme: item.presentationData.theme.theme, incoming: incoming)
|
||||
} else if !strongSelf.solutionButtonNode.alpha.isZero {
|
||||
let timerTransition: ContainedViewLayoutTransition
|
||||
if animation.isAnimated {
|
||||
timerTransition = .animated(duration: 0.25, curve: .easeInOut)
|
||||
} else {
|
||||
timerTransition = .immediate
|
||||
}
|
||||
timerTransition.updateAlpha(node: strongSelf.solutionButtonNode, alpha: 0.0)
|
||||
}
|
||||
|
||||
let avatarsFrame = CGRect(origin: CGPoint(x: typeFrame.maxX + 6.0, y: typeFrame.minY + floor((typeFrame.height - MergedAvatarsNode.defaultMergedImageSize) / 2.0)), size: CGSize(width: MergedAvatarsNode.defaultMergedImageSize + MergedAvatarsNode.defaultMergedImageSpacing * 2.0, height: MergedAvatarsNode.defaultMergedImageSize))
|
||||
strongSelf.avatarsNode.frame = avatarsFrame
|
||||
strongSelf.avatarsNode.updateLayout(size: avatarsFrame.size)
|
||||
strongSelf.avatarsNode.update(context: item.context, peers: avatarPeers, synchronousLoad: synchronousLoad, imageSize: MergedAvatarsNode.defaultMergedImageSize, imageSpacing: MergedAvatarsNode.defaultMergedImageSpacing, borderWidth: MergedAvatarsNode.defaultBorderWidth)
|
||||
strongSelf.avatarsNode.isHidden = isBotChat
|
||||
let alphaTransition: ContainedViewLayoutTransition
|
||||
if animation.isAnimated {
|
||||
alphaTransition = .animated(duration: 0.25, curve: .easeInOut)
|
||||
alphaTransition.updateAlpha(node: strongSelf.avatarsNode, alpha: avatarPeers.isEmpty ? 0.0 : 1.0)
|
||||
} else {
|
||||
alphaTransition = .immediate
|
||||
}
|
||||
|
||||
let _ = votersApply()
|
||||
let votersFrame = CGRect(origin: CGPoint(x: floor((resultSize.width - votersLayout.size.width) / 2.0), y: verticalOffset + optionsVotersSpacing), size: votersLayout.size)
|
||||
animation.animator.updateFrame(layer: strongSelf.votersNode.layer, frame: votersFrame, completion: nil)
|
||||
|
||||
if animation.isAnimated, let previousPoll = previousPoll, let poll = poll {
|
||||
if previousPoll.results.totalVoters == nil && poll.results.totalVoters != nil {
|
||||
strongSelf.votersNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.25)
|
||||
}
|
||||
}
|
||||
|
||||
if let statusSizeAndApply = statusSizeAndApply {
|
||||
let statusFrame = CGRect(origin: CGPoint(x: resultSize.width - statusSizeAndApply.0.width - layoutConstants.text.bubbleInsets.right, y: votersFrame.maxY + statusOffset), size: statusSizeAndApply.0)
|
||||
|
||||
if strongSelf.statusNode.supernode == nil {
|
||||
statusSizeAndApply.1(.None)
|
||||
strongSelf.statusNode.frame = statusFrame
|
||||
strongSelf.addSubnode(strongSelf.statusNode)
|
||||
} else {
|
||||
statusSizeAndApply.1(animation)
|
||||
animation.animator.updateFrame(layer: strongSelf.statusNode.layer, frame: statusFrame, completion: nil)
|
||||
}
|
||||
} else if strongSelf.statusNode.supernode != nil {
|
||||
strongSelf.statusNode.removeFromSupernode()
|
||||
}
|
||||
|
||||
let _ = buttonSubmitInactiveTextApply()
|
||||
strongSelf.buttonSubmitInactiveTextNode.frame = buttonSubmitInactiveTextFrame.offsetBy(dx: 0.0, dy: verticalOffset)
|
||||
|
||||
let _ = buttonSubmitActiveTextApply()
|
||||
strongSelf.buttonSubmitActiveTextNode.frame = buttonSubmitActiveTextFrame.offsetBy(dx: 0.0, dy: verticalOffset)
|
||||
|
||||
let _ = buttonSaveTextApply()
|
||||
strongSelf.buttonSaveTextNode.frame = buttonSaveTextFrame.offsetBy(dx: 0.0, dy: verticalOffset)
|
||||
|
||||
let _ = buttonViewResultsTextApply()
|
||||
strongSelf.buttonViewResultsTextNode.frame = buttonViewResultsTextFrame.offsetBy(dx: 0.0, dy: verticalOffset)
|
||||
|
||||
strongSelf.updateSelection()
|
||||
strongSelf.updatePollTooltipMessageState(animated: false)
|
||||
|
||||
let buttonWidth: CGFloat = floor(max(strongSelf.buttonSaveTextNode.frame.width, max(strongSelf.buttonViewResultsTextNode.frame.width, strongSelf.buttonSubmitActiveTextNode.frame.width)) * 1.1)
|
||||
strongSelf.buttonNode.frame = CGRect(origin: CGPoint(x: floor((resultSize.width - buttonWidth) / 2.0), y: verticalOffset), size: CGSize(width: buttonWidth, height: 44.0))
|
||||
|
||||
strongSelf.updateIsTranslating(isTranslating)
|
||||
}
|
||||
})
|
||||
for optionNode in strongSelf.optionNodes {
|
||||
if !updatedOptionNodes.contains(where: { $0 === optionNode }) {
|
||||
optionNode.removeFromSupernode()
|
||||
}
|
||||
}
|
||||
strongSelf.optionNodes = updatedOptionNodes
|
||||
strongSelf.updatePollOptionsInteraction(animated: animation.isAnimated)
|
||||
|
||||
if let (size, apply) = addOptionSizeAndApply {
|
||||
let isRequesting = item.controllerInteraction.pollActionState.pollMessageIdsInProgress[item.message.id] != nil
|
||||
let addOptionNode = apply(animation.isAnimated, isRequesting)
|
||||
let addOptionNodeFrame = CGRect(origin: CGPoint(x: layoutConstants.bubble.borderInset, y: verticalOffset), size: size)
|
||||
if addOptionNode.supernode !== strongSelf {
|
||||
strongSelf.addSubnode(addOptionNode)
|
||||
} else {
|
||||
animation.animator.updateFrame(layer: addOptionNode.layer, frame: addOptionNodeFrame, completion: nil)
|
||||
}
|
||||
addOptionNode.frame = addOptionNodeFrame
|
||||
addOptionNode.isUserInteractionEnabled = !isRequesting
|
||||
addOptionNode.textUpdated = { [weak self] text in
|
||||
self?.updateNewOptionText(text)
|
||||
}
|
||||
addOptionNode.heightUpdated = { [weak self] in
|
||||
self?.requestNewOptionLayoutUpdate()
|
||||
}
|
||||
addOptionNode.attachPressed = { [weak self] in
|
||||
self?.openNewOptionAttachment()
|
||||
}
|
||||
addOptionNode.mediaPressed = { [weak self] in
|
||||
self?.openNewOptionAttachment()
|
||||
}
|
||||
addOptionNode.modeSelectorPressed = { [weak self] in
|
||||
self?.toggleNewOptionInputMode()
|
||||
}
|
||||
addOptionNode.requestSave = { [weak self] in
|
||||
self?.buttonPressed()
|
||||
}
|
||||
addOptionNode.focusUpdated = { [weak self] focused in
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
self.updatePollAddOptionFocused(focused)
|
||||
}
|
||||
strongSelf.addOptionNode = addOptionNode
|
||||
verticalOffset += size.height
|
||||
} else if let addOptionNode = strongSelf.addOptionNode {
|
||||
strongSelf.updatePollAddOptionFocused(false)
|
||||
strongSelf.addOptionNode = nil
|
||||
addOptionNode.removeFromSupernode()
|
||||
}
|
||||
|
||||
if let poll = poll, let pendingNewOptionSubmissionText, let pendingNewOptionOptionCount = strongSelf.pendingNewOptionOptionCount, poll.options.count > pendingNewOptionOptionCount, poll.options.contains(where: { $0.text == pendingNewOptionSubmissionText }) {
|
||||
strongSelf.clearNewOptionInput()
|
||||
}
|
||||
|
||||
if textLayout.hasRTL {
|
||||
strongSelf.textNode.textNode.frame = CGRect(origin: CGPoint(x: resultSize.width - textFrame.size.width - textInsets.left - layoutConstants.text.bubbleInsets.right - additionalTextRightInset, y: textFrame.origin.y), size: textFrame.size)
|
||||
} else {
|
||||
strongSelf.textNode.textNode.frame = textFrame
|
||||
}
|
||||
let typeFrame = CGRect(origin: CGPoint(x: textFrame.minX, y: textFrame.maxY + titleTypeSpacing), size: typeLayout.size)
|
||||
animation.animator.updateFrame(layer: strongSelf.typeNode.layer, frame: typeFrame, completion: nil)
|
||||
|
||||
let deadlineTimeout = poll?.deadlineTimeout
|
||||
var displayDeadlineTimer = true
|
||||
var hasSelected = false
|
||||
|
||||
if let poll {
|
||||
if let voters = poll.results.voters {
|
||||
for voter in voters {
|
||||
if voter.selected {
|
||||
if case .quiz = poll.kind {
|
||||
displayDeadlineTimer = false
|
||||
} else {
|
||||
displayDeadlineTimer = !poll.isClosed
|
||||
}
|
||||
hasSelected = true
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var endDate: Int32?
|
||||
if let deadlineTimeout,
|
||||
message.id.namespace == Namespaces.Message.Cloud {
|
||||
let startDate: Int32
|
||||
if let forwardInfo = message.forwardInfo {
|
||||
startDate = forwardInfo.date
|
||||
} else {
|
||||
startDate = message.timestamp
|
||||
}
|
||||
endDate = startDate + deadlineTimeout
|
||||
}
|
||||
|
||||
if let poll, case .quiz = poll.kind, let deadlineTimeout, !isClosed {
|
||||
let timerNode: PollBubbleTimerNode
|
||||
if let current = strongSelf.timerNode {
|
||||
timerNode = current
|
||||
let timerTransition: ContainedViewLayoutTransition
|
||||
if animation.isAnimated {
|
||||
timerTransition = .animated(duration: 0.25, curve: .easeInOut)
|
||||
} else {
|
||||
timerTransition = .immediate
|
||||
}
|
||||
if displayDeadlineTimer {
|
||||
timerTransition.updateAlpha(node: timerNode, alpha: 1.0)
|
||||
} else {
|
||||
timerTransition.updateAlpha(node: timerNode, alpha: 0.0)
|
||||
}
|
||||
} else {
|
||||
timerNode = PollBubbleTimerNode()
|
||||
strongSelf.timerNode = timerNode
|
||||
strongSelf.addSubnode(timerNode)
|
||||
timerNode.reachedTimeout = {
|
||||
guard let strongSelf = self,
|
||||
let _ = strongSelf.item else {
|
||||
return
|
||||
}
|
||||
//item.controllerInteraction.requestMessageUpdate(item.message.id)
|
||||
}
|
||||
|
||||
let timerTransition: ContainedViewLayoutTransition
|
||||
if animation.isAnimated {
|
||||
timerTransition = .animated(duration: 0.25, curve: .easeInOut)
|
||||
} else {
|
||||
timerTransition = .immediate
|
||||
}
|
||||
if displayDeadlineTimer {
|
||||
timerNode.alpha = 0.0
|
||||
timerTransition.updateAlpha(node: timerNode, alpha: 1.0)
|
||||
} else {
|
||||
timerNode.alpha = 0.0
|
||||
}
|
||||
}
|
||||
timerNode.update(regularColor: messageTheme.secondaryTextColor, proximityColor: messageTheme.scamColor, timeout: deadlineTimeout, deadlineTimestamp: endDate)
|
||||
timerNode.frame = CGRect(origin: CGPoint(x: resultSize.width - layoutConstants.text.bubbleInsets.right, y: typeFrame.minY), size: CGSize())
|
||||
} else if let timerNode = strongSelf.timerNode {
|
||||
strongSelf.timerNode = nil
|
||||
|
||||
let timerTransition: ContainedViewLayoutTransition
|
||||
if animation.isAnimated {
|
||||
timerTransition = .animated(duration: 0.25, curve: .easeInOut)
|
||||
} else {
|
||||
timerTransition = .immediate
|
||||
}
|
||||
timerTransition.updateAlpha(node: timerNode, alpha: 0.0, completion: { [weak timerNode] _ in
|
||||
timerNode?.removeFromSupernode()
|
||||
})
|
||||
timerTransition.updateTransformScale(node: timerNode, scale: 0.1)
|
||||
}
|
||||
|
||||
var statusOffset: CGFloat = 0.0
|
||||
if let poll, case .poll = poll.kind, let endDate, !isClosed {
|
||||
let timerNode: DeadlineTimerNode
|
||||
if let current = strongSelf.deadlineTimerNode {
|
||||
timerNode = current
|
||||
let timerTransition: ContainedViewLayoutTransition
|
||||
if animation.isAnimated {
|
||||
timerTransition = .animated(duration: 0.25, curve: .easeInOut)
|
||||
} else {
|
||||
timerTransition = .immediate
|
||||
}
|
||||
if displayDeadlineTimer {
|
||||
timerTransition.updateAlpha(node: timerNode, alpha: 1.0)
|
||||
} else {
|
||||
timerTransition.updateAlpha(node: timerNode, alpha: 0.0)
|
||||
}
|
||||
} else {
|
||||
timerNode = DeadlineTimerNode()
|
||||
strongSelf.deadlineTimerNode = timerNode
|
||||
strongSelf.addSubnode(timerNode)
|
||||
|
||||
let timerTransition: ContainedViewLayoutTransition
|
||||
if animation.isAnimated {
|
||||
timerTransition = .animated(duration: 0.25, curve: .easeInOut)
|
||||
} else {
|
||||
timerTransition = .immediate
|
||||
}
|
||||
if displayDeadlineTimer {
|
||||
timerNode.alpha = 0.0
|
||||
timerTransition.updateAlpha(node: timerNode, alpha: 1.0)
|
||||
} else {
|
||||
timerNode.alpha = 0.0
|
||||
}
|
||||
}
|
||||
timerNode.update(size: resultSize, color: messageTheme.secondaryTextColor, deadlineTimeout: endDate, resultsHidden: poll.hideResultsUntilClose, strings: item.presentationData.strings)
|
||||
timerNode.frame = CGRect(origin: CGPoint(x: 0.0, y: verticalOffset + 31.0), size: CGSize(width: resultSize.width, height: 20.0))
|
||||
statusOffset += 6.0
|
||||
} else if let timerNode = strongSelf.deadlineTimerNode {
|
||||
strongSelf.deadlineTimerNode = nil
|
||||
|
||||
let timerTransition: ContainedViewLayoutTransition
|
||||
if animation.isAnimated {
|
||||
timerTransition = .animated(duration: 0.25, curve: .easeInOut)
|
||||
} else {
|
||||
timerTransition = .immediate
|
||||
}
|
||||
timerTransition.updateAlpha(node: timerNode, alpha: 0.0, completion: { [weak timerNode] _ in
|
||||
timerNode?.removeFromSupernode()
|
||||
})
|
||||
timerTransition.updateTransformScale(node: timerNode, scale: 0.1)
|
||||
}
|
||||
|
||||
let solutionButtonSize = CGSize(width: 32.0, height: 32.0)
|
||||
let solutionButtonFrame = CGRect(origin: CGPoint(x: resultSize.width - layoutConstants.text.bubbleInsets.right - solutionButtonSize.width + 5.0, y: typeFrame.minY - 16.0), size: solutionButtonSize)
|
||||
strongSelf.solutionButtonNode.frame = solutionButtonFrame
|
||||
|
||||
if (strongSelf.timerNode == nil || !displayDeadlineTimer), let poll = poll, case .quiz = poll.kind, let _ = poll.results.solution, (isClosed || hasSelected) {
|
||||
if strongSelf.solutionButtonNode.alpha.isZero {
|
||||
let timerTransition: ContainedViewLayoutTransition
|
||||
if animation.isAnimated {
|
||||
timerTransition = .animated(duration: 0.25, curve: .easeInOut)
|
||||
} else {
|
||||
timerTransition = .immediate
|
||||
}
|
||||
timerTransition.updateAlpha(node: strongSelf.solutionButtonNode, alpha: 1.0)
|
||||
}
|
||||
strongSelf.solutionButtonNode.update(size: solutionButtonSize, theme: item.presentationData.theme.theme, incoming: incoming)
|
||||
} else if !strongSelf.solutionButtonNode.alpha.isZero {
|
||||
let timerTransition: ContainedViewLayoutTransition
|
||||
if animation.isAnimated {
|
||||
timerTransition = .animated(duration: 0.25, curve: .easeInOut)
|
||||
} else {
|
||||
timerTransition = .immediate
|
||||
}
|
||||
timerTransition.updateAlpha(node: strongSelf.solutionButtonNode, alpha: 0.0)
|
||||
}
|
||||
|
||||
let avatarsFrame = CGRect(origin: CGPoint(x: typeFrame.maxX + 6.0, y: typeFrame.minY + floor((typeFrame.height - MergedAvatarsNode.defaultMergedImageSize) / 2.0)), size: CGSize(width: MergedAvatarsNode.defaultMergedImageSize + MergedAvatarsNode.defaultMergedImageSpacing * 2.0, height: MergedAvatarsNode.defaultMergedImageSize))
|
||||
strongSelf.avatarsNode.frame = avatarsFrame
|
||||
strongSelf.avatarsNode.updateLayout(size: avatarsFrame.size)
|
||||
strongSelf.avatarsNode.update(context: item.context, peers: avatarPeers, synchronousLoad: synchronousLoad, imageSize: MergedAvatarsNode.defaultMergedImageSize, imageSpacing: MergedAvatarsNode.defaultMergedImageSpacing, borderWidth: MergedAvatarsNode.defaultBorderWidth)
|
||||
strongSelf.avatarsNode.isHidden = isBotChat
|
||||
let alphaTransition: ContainedViewLayoutTransition
|
||||
if animation.isAnimated {
|
||||
alphaTransition = .animated(duration: 0.25, curve: .easeInOut)
|
||||
alphaTransition.updateAlpha(node: strongSelf.avatarsNode, alpha: avatarPeers.isEmpty ? 0.0 : 1.0)
|
||||
} else {
|
||||
alphaTransition = .immediate
|
||||
}
|
||||
|
||||
let _ = votersApply()
|
||||
let votersFrame = CGRect(origin: CGPoint(x: floor((resultSize.width - votersLayout.size.width) / 2.0), y: verticalOffset + optionsVotersSpacing), size: votersLayout.size)
|
||||
animation.animator.updateFrame(layer: strongSelf.votersNode.layer, frame: votersFrame, completion: nil)
|
||||
|
||||
if animation.isAnimated, let previousPoll = previousPoll, let poll = poll {
|
||||
if previousPoll.results.totalVoters == nil && poll.results.totalVoters != nil {
|
||||
strongSelf.votersNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.25)
|
||||
}
|
||||
}
|
||||
|
||||
if let statusSizeAndApply = statusSizeAndApply {
|
||||
let statusFrame = CGRect(origin: CGPoint(x: resultSize.width - statusSizeAndApply.0.width - layoutConstants.text.bubbleInsets.right, y: votersFrame.maxY + statusOffset), size: statusSizeAndApply.0)
|
||||
|
||||
if strongSelf.statusNode.supernode == nil {
|
||||
statusSizeAndApply.1(.None)
|
||||
strongSelf.statusNode.frame = statusFrame
|
||||
strongSelf.addSubnode(strongSelf.statusNode)
|
||||
} else {
|
||||
statusSizeAndApply.1(animation)
|
||||
animation.animator.updateFrame(layer: strongSelf.statusNode.layer, frame: statusFrame, completion: nil)
|
||||
}
|
||||
} else if strongSelf.statusNode.supernode != nil {
|
||||
strongSelf.statusNode.removeFromSupernode()
|
||||
}
|
||||
|
||||
let _ = buttonSubmitInactiveTextApply()
|
||||
strongSelf.buttonSubmitInactiveTextNode.frame = buttonSubmitInactiveTextFrame.offsetBy(dx: 0.0, dy: verticalOffset)
|
||||
|
||||
let _ = buttonSubmitActiveTextApply()
|
||||
strongSelf.buttonSubmitActiveTextNode.frame = buttonSubmitActiveTextFrame.offsetBy(dx: 0.0, dy: verticalOffset)
|
||||
|
||||
let _ = buttonSaveTextApply()
|
||||
strongSelf.buttonSaveTextNode.frame = buttonSaveTextFrame.offsetBy(dx: 0.0, dy: verticalOffset)
|
||||
|
||||
let _ = buttonViewResultsTextApply()
|
||||
strongSelf.buttonViewResultsTextNode.frame = buttonViewResultsTextFrame.offsetBy(dx: 0.0, dy: verticalOffset)
|
||||
|
||||
strongSelf.updateSelection()
|
||||
strongSelf.updatePollTooltipMessageState(animated: false)
|
||||
|
||||
let buttonWidth: CGFloat = floor(max(strongSelf.buttonSaveTextNode.frame.width, max(strongSelf.buttonViewResultsTextNode.frame.width, strongSelf.buttonSubmitActiveTextNode.frame.width)) * 1.1)
|
||||
strongSelf.buttonNode.frame = CGRect(origin: CGPoint(x: floor((resultSize.width - buttonWidth) / 2.0), y: verticalOffset), size: CGSize(width: buttonWidth, height: 44.0))
|
||||
|
||||
strongSelf.updateIsTranslating(isTranslating)
|
||||
}
|
||||
})
|
||||
})
|
||||
})
|
||||
}
|
||||
|
|
|
|||
|
|
@ -769,10 +769,10 @@ private final class ChatMessageTodoItemNode: ASDisplayNode {
|
|||
linkColor: messageTheme.linkTextColor,
|
||||
baseFont: presentationData.messageFont,
|
||||
linkFont: presentationData.messageFont,
|
||||
boldFont: presentationData.messageFont,
|
||||
italicFont: presentationData.messageFont,
|
||||
boldItalicFont: presentationData.messageFont,
|
||||
fixedFont: presentationData.messageFont,
|
||||
boldFont: presentationData.messageBoldFont,
|
||||
italicFont: presentationData.messageItalicFont,
|
||||
boldItalicFont: presentationData.messageBoldItalicFont,
|
||||
fixedFont: presentationData.messageFixedFont,
|
||||
blockQuoteFont: presentationData.messageFont,
|
||||
underlineLinks: underlineLinks,
|
||||
message: message
|
||||
|
|
|
|||
|
|
@ -24,6 +24,7 @@ swift_library(
|
|||
"//submodules/TelegramUI/Components/MultiAnimationRenderer:MultiAnimationRenderer",
|
||||
"//submodules/TextFormat:TextFormat",
|
||||
"//submodules/AppBundle:AppBundle",
|
||||
"//submodules/SearchBarNode:SearchBarNode",
|
||||
"//submodules/TelegramUI/Components/ChatInputNode:ChatInputNode",
|
||||
"//submodules/Components/PagerComponent:PagerComponent",
|
||||
"//submodules/PremiumUI:PremiumUI",
|
||||
|
|
|
|||
|
|
@ -1,500 +0,0 @@
|
|||
import Foundation
|
||||
import UIKit
|
||||
import SwiftSignalKit
|
||||
import AsyncDisplayKit
|
||||
import Display
|
||||
import TelegramPresentationData
|
||||
import ActivityIndicator
|
||||
import AppBundle
|
||||
import FeaturedStickersScreen
|
||||
|
||||
private func generateLoupeIcon(color: UIColor) -> UIImage? {
|
||||
return generateTintedImage(image: UIImage(bundleImageName: "Components/Search Bar/Loupe"), color: color)
|
||||
}
|
||||
|
||||
private func generateClearIcon(color: UIColor) -> UIImage? {
|
||||
return generateTintedImage(image: UIImage(bundleImageName: "Components/Search Bar/Clear"), color: color)
|
||||
}
|
||||
|
||||
private func generateBackground(backgroundColor: UIColor, foregroundColor: UIColor) -> UIImage? {
|
||||
let diameter: CGFloat = 10.0
|
||||
return generateImage(CGSize(width: diameter, height: diameter), contextGenerator: { size, context in
|
||||
context.setFillColor(backgroundColor.cgColor)
|
||||
context.fill(CGRect(origin: CGPoint(), size: size))
|
||||
context.setFillColor(foregroundColor.cgColor)
|
||||
context.fillEllipse(in: CGRect(origin: CGPoint(), size: size))
|
||||
}, opaque: true)?.stretchableImage(withLeftCapWidth: Int(diameter / 2.0), topCapHeight: Int(diameter / 2.0))
|
||||
}
|
||||
|
||||
private class PaneSearchBarTextField: UITextField {
|
||||
public var didDeleteBackwardWhileEmpty: (() -> Void)?
|
||||
|
||||
let placeholderLabel: ImmediateTextNode
|
||||
var placeholderString: NSAttributedString? {
|
||||
didSet {
|
||||
self.placeholderLabel.attributedText = self.placeholderString
|
||||
}
|
||||
}
|
||||
|
||||
let prefixLabel: ASTextNode
|
||||
var prefixString: NSAttributedString? {
|
||||
didSet {
|
||||
self.prefixLabel.attributedText = self.prefixString
|
||||
}
|
||||
}
|
||||
|
||||
override init(frame: CGRect) {
|
||||
self.placeholderLabel = ImmediateTextNode()
|
||||
self.placeholderLabel.isUserInteractionEnabled = false
|
||||
self.placeholderLabel.displaysAsynchronously = false
|
||||
self.placeholderLabel.maximumNumberOfLines = 1
|
||||
|
||||
self.prefixLabel = ASTextNode()
|
||||
self.prefixLabel.isUserInteractionEnabled = false
|
||||
self.prefixLabel.displaysAsynchronously = false
|
||||
|
||||
super.init(frame: frame)
|
||||
|
||||
self.addSubnode(self.placeholderLabel)
|
||||
self.addSubnode(self.prefixLabel)
|
||||
}
|
||||
|
||||
required init?(coder aDecoder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
override var keyboardAppearance: UIKeyboardAppearance {
|
||||
get {
|
||||
return super.keyboardAppearance
|
||||
}
|
||||
set {
|
||||
let resigning = self.isFirstResponder
|
||||
if resigning {
|
||||
self.resignFirstResponder()
|
||||
}
|
||||
super.keyboardAppearance = newValue
|
||||
if resigning {
|
||||
self.becomeFirstResponder()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override func textRect(forBounds bounds: CGRect) -> CGRect {
|
||||
if bounds.size.width.isZero {
|
||||
return CGRect(origin: CGPoint(), size: CGSize())
|
||||
}
|
||||
var rect = bounds.insetBy(dx: 4.0, dy: 4.0)
|
||||
|
||||
let prefixSize = self.prefixLabel.measure(bounds.size)
|
||||
if !prefixSize.width.isZero {
|
||||
let prefixOffset = prefixSize.width
|
||||
rect.origin.x += prefixOffset
|
||||
rect.size.width -= prefixOffset
|
||||
}
|
||||
return rect
|
||||
}
|
||||
|
||||
override func editingRect(forBounds bounds: CGRect) -> CGRect {
|
||||
return self.textRect(forBounds: bounds)
|
||||
}
|
||||
|
||||
override func layoutSubviews() {
|
||||
super.layoutSubviews()
|
||||
|
||||
let bounds = self.bounds
|
||||
if bounds.size.width.isZero {
|
||||
return
|
||||
}
|
||||
|
||||
let constrainedSize = self.textRect(forBounds: self.bounds).size
|
||||
let labelSize = self.placeholderLabel.updateLayout(constrainedSize)
|
||||
self.placeholderLabel.frame = CGRect(origin: CGPoint(x: self.textRect(forBounds: bounds).minX, y: self.textRect(forBounds: bounds).minY + 4.0), size: labelSize)
|
||||
|
||||
let prefixSize = self.prefixLabel.measure(constrainedSize)
|
||||
let prefixBounds = bounds.insetBy(dx: 4.0, dy: 4.0)
|
||||
self.prefixLabel.frame = CGRect(origin: CGPoint(x: prefixBounds.minX, y: prefixBounds.minY + 1.0), size: prefixSize)
|
||||
}
|
||||
|
||||
override func deleteBackward() {
|
||||
if self.text == nil || self.text!.isEmpty {
|
||||
self.didDeleteBackwardWhileEmpty?()
|
||||
}
|
||||
super.deleteBackward()
|
||||
}
|
||||
}
|
||||
|
||||
class PaneSearchBarNode: ASDisplayNode, UITextFieldDelegate {
|
||||
var cancel: (() -> Void)?
|
||||
var textUpdated: ((String, String) -> Void)?
|
||||
var clearPrefix: (() -> Void)?
|
||||
|
||||
private let backgroundNode: ASDisplayNode
|
||||
private let separatorNode: ASDisplayNode
|
||||
private let textBackgroundNode: ASImageNode
|
||||
private var activityIndicator: ActivityIndicator?
|
||||
private let iconNode: ASImageNode
|
||||
private let textField: PaneSearchBarTextField
|
||||
private let clearButton: HighlightableButtonNode
|
||||
private let cancelButton: HighlightableButtonNode
|
||||
|
||||
var placeholderString: NSAttributedString? {
|
||||
get {
|
||||
return self.textField.placeholderString
|
||||
} set(value) {
|
||||
self.textField.placeholderString = value
|
||||
}
|
||||
}
|
||||
|
||||
var prefixString: NSAttributedString? {
|
||||
get {
|
||||
return self.textField.prefixString
|
||||
} set(value) {
|
||||
let previous = self.prefixString
|
||||
let updated: Bool
|
||||
if let previous = previous, let value = value {
|
||||
updated = !previous.isEqual(to: value)
|
||||
} else {
|
||||
updated = (previous != nil) != (value != nil)
|
||||
}
|
||||
if updated {
|
||||
self.textField.prefixString = value
|
||||
self.textField.setNeedsLayout()
|
||||
self.updateIsEmpty()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var text: String {
|
||||
get {
|
||||
return self.textField.text ?? ""
|
||||
} set(value) {
|
||||
if self.textField.text ?? "" != value {
|
||||
self.textField.text = value
|
||||
self.textFieldDidChange(self.textField)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var activity: Bool = false {
|
||||
didSet {
|
||||
if self.activity != oldValue {
|
||||
if self.activity {
|
||||
if self.activityIndicator == nil, let theme = self.theme {
|
||||
let activityIndicator = ActivityIndicator(type: .custom(theme.chat.inputMediaPanel.stickersSearchControlColor, 13.0, 1.0, false))
|
||||
self.activityIndicator = activityIndicator
|
||||
self.addSubnode(activityIndicator)
|
||||
if let (boundingSize, leftInset, rightInset) = self.validLayout {
|
||||
self.updateLayout(boundingSize: boundingSize, leftInset: leftInset, rightInset: rightInset, transition: .immediate)
|
||||
}
|
||||
}
|
||||
} else if let activityIndicator = self.activityIndicator {
|
||||
self.activityIndicator = nil
|
||||
activityIndicator.removeFromSupernode()
|
||||
}
|
||||
self.iconNode.isHidden = self.activity
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private var validLayout: (CGSize, CGFloat, CGFloat)?
|
||||
private var theme: PresentationTheme?
|
||||
|
||||
override init() {
|
||||
self.backgroundNode = ASDisplayNode()
|
||||
self.backgroundNode.isLayerBacked = true
|
||||
|
||||
self.separatorNode = ASDisplayNode()
|
||||
self.separatorNode.isLayerBacked = true
|
||||
|
||||
self.textBackgroundNode = ASImageNode()
|
||||
self.textBackgroundNode.isLayerBacked = false
|
||||
self.textBackgroundNode.displaysAsynchronously = false
|
||||
self.textBackgroundNode.displayWithoutProcessing = true
|
||||
|
||||
self.iconNode = ASImageNode()
|
||||
self.iconNode.isUserInteractionEnabled = false
|
||||
self.iconNode.displaysAsynchronously = false
|
||||
self.iconNode.displayWithoutProcessing = true
|
||||
|
||||
self.textField = PaneSearchBarTextField()
|
||||
self.textField.accessibilityTraits = .searchField
|
||||
self.textField.autocorrectionType = .no
|
||||
self.textField.returnKeyType = .search
|
||||
self.textField.font = Font.regular(17.0)
|
||||
|
||||
self.clearButton = HighlightableButtonNode()
|
||||
self.clearButton.imageNode.displaysAsynchronously = false
|
||||
self.clearButton.imageNode.displayWithoutProcessing = true
|
||||
self.clearButton.displaysAsynchronously = false
|
||||
self.clearButton.isHidden = true
|
||||
|
||||
self.cancelButton = HighlightableButtonNode()
|
||||
self.cancelButton.hitTestSlop = UIEdgeInsets(top: -8.0, left: -8.0, bottom: -8.0, right: -8.0)
|
||||
self.cancelButton.displaysAsynchronously = false
|
||||
|
||||
super.init()
|
||||
|
||||
self.addSubnode(self.backgroundNode)
|
||||
self.addSubnode(self.separatorNode)
|
||||
|
||||
self.addSubnode(self.textBackgroundNode)
|
||||
self.view.addSubview(self.textField)
|
||||
self.addSubnode(self.iconNode)
|
||||
self.addSubnode(self.clearButton)
|
||||
self.addSubnode(self.cancelButton)
|
||||
|
||||
self.textField.delegate = self
|
||||
self.textField.addTarget(self, action: #selector(self.textFieldDidChange(_:)), for: .editingChanged)
|
||||
|
||||
self.textField.didDeleteBackwardWhileEmpty = { [weak self] in
|
||||
self?.clearPressed()
|
||||
}
|
||||
|
||||
self.cancelButton.addTarget(self, action: #selector(self.cancelPressed), forControlEvents: .touchUpInside)
|
||||
self.clearButton.addTarget(self, action: #selector(self.clearPressed), forControlEvents: .touchUpInside)
|
||||
}
|
||||
|
||||
func updateThemeAndStrings(theme: PresentationTheme, strings: PresentationStrings) {
|
||||
self.theme = theme
|
||||
|
||||
if let activityIndicator = self.activityIndicator {
|
||||
activityIndicator.type = .custom(theme.chat.inputMediaPanel.stickersSearchControlColor, 13.0, 1.0, false)
|
||||
}
|
||||
self.separatorNode.backgroundColor = theme.chat.inputMediaPanel.panelSeparatorColor
|
||||
self.textBackgroundNode.image = generateStretchableFilledCircleImage(diameter: 36.0, color: theme.chat.inputMediaPanel.stickersSearchBackgroundColor)
|
||||
self.textField.textColor = theme.chat.inputMediaPanel.stickersSearchPrimaryColor
|
||||
self.iconNode.image = generateLoupeIcon(color: theme.chat.inputMediaPanel.stickersSearchControlColor)
|
||||
self.clearButton.setImage(generateClearIcon(color: theme.chat.inputMediaPanel.stickersSearchControlColor), for: [])
|
||||
self.cancelButton.setAttributedTitle(NSAttributedString(string: strings.Common_Cancel, font: Font.regular(17.0), textColor: theme.chat.inputPanel.panelControlAccentColor), for: [])
|
||||
self.textField.keyboardAppearance = theme.rootController.keyboardColor.keyboardAppearance
|
||||
self.textField.tintColor = theme.list.itemAccentColor
|
||||
|
||||
if let (boundingSize, leftInset, rightInset) = self.validLayout {
|
||||
self.updateLayout(boundingSize: boundingSize, leftInset: leftInset, rightInset: rightInset, transition: .immediate)
|
||||
}
|
||||
}
|
||||
|
||||
func updateLayout(boundingSize: CGSize, leftInset: CGFloat, rightInset: CGFloat, transition: ContainedViewLayoutTransition) {
|
||||
self.validLayout = (boundingSize, leftInset, rightInset)
|
||||
|
||||
self.backgroundNode.frame = self.bounds
|
||||
transition.updateFrame(node: self.separatorNode, frame: CGRect(origin: CGPoint(x: 0.0, y: self.bounds.size.height), size: CGSize(width: self.bounds.size.width, height: UIScreenPixel)))
|
||||
|
||||
let verticalOffset: CGFloat = -20.0
|
||||
|
||||
let contentFrame = CGRect(origin: CGPoint(x: leftInset, y: 0.0), size: CGSize(width: boundingSize.width - leftInset - rightInset, height: boundingSize.height))
|
||||
|
||||
let cancelButtonSize = self.cancelButton.measure(CGSize(width: 100.0, height: CGFloat.infinity))
|
||||
transition.updateFrame(node: self.cancelButton, frame: CGRect(origin: CGPoint(x: contentFrame.maxX - 8.0 - cancelButtonSize.width, y: verticalOffset + 36.0), size: cancelButtonSize))
|
||||
|
||||
let textBackgroundFrame = CGRect(origin: CGPoint(x: contentFrame.minX + 8.0, y: verticalOffset + 28.0), size: CGSize(width: contentFrame.width - 16.0 - cancelButtonSize.width - 11.0, height: 36.0))
|
||||
transition.updateFrame(node: self.textBackgroundNode, frame: textBackgroundFrame)
|
||||
|
||||
let textFrame = CGRect(origin: CGPoint(x: textBackgroundFrame.minX + 27.0, y: textBackgroundFrame.minY), size: CGSize(width: max(1.0, textBackgroundFrame.size.width - 27.0 - 20.0), height: textBackgroundFrame.size.height))
|
||||
|
||||
if let iconImage = self.iconNode.image {
|
||||
let iconSize = iconImage.size
|
||||
transition.updateFrame(node: self.iconNode, frame: CGRect(origin: CGPoint(x: textBackgroundFrame.minX + 5.0, y: textBackgroundFrame.minY + floor((textBackgroundFrame.size.height - iconSize.height) / 2.0)), size: iconSize))
|
||||
}
|
||||
|
||||
if let activityIndicator = self.activityIndicator {
|
||||
let indicatorSize = activityIndicator.measure(CGSize(width: 32.0, height: 32.0))
|
||||
transition.updateFrame(node: activityIndicator, frame: CGRect(origin: CGPoint(x: textBackgroundFrame.minX + 11.0, y: textBackgroundFrame.minY + floor((textBackgroundFrame.size.height - indicatorSize.height) / 2.0)), size: indicatorSize))
|
||||
}
|
||||
|
||||
let clearSize = self.clearButton.measure(CGSize(width: 100.0, height: 100.0))
|
||||
transition.updateFrame(node: self.clearButton, frame: CGRect(origin: CGPoint(x: textBackgroundFrame.maxX - 8.0 - clearSize.width, y: textBackgroundFrame.minY + floor((textBackgroundFrame.size.height - clearSize.height) / 2.0)), size: clearSize))
|
||||
|
||||
self.textField.frame = textFrame
|
||||
self.textField.layoutSubviews()
|
||||
}
|
||||
|
||||
@objc private func tapGesture(_ recognizer: UITapGestureRecognizer) {
|
||||
if case .ended = recognizer.state {
|
||||
if let cancel = self.cancel {
|
||||
cancel()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func activate() {
|
||||
self.textField.becomeFirstResponder()
|
||||
}
|
||||
|
||||
func animateIn(from node: PaneSearchBarPlaceholderNode, duration: Double, timingFunction: String, completion: @escaping () -> Void) {
|
||||
let initialTextBackgroundFrame = node.view.convert(node.backgroundNode.frame, to: self.view)
|
||||
|
||||
var backgroundCompleted = false
|
||||
var separatorCompleted = false
|
||||
var textBackgroundCompleted = false
|
||||
let intermediateCompletion: () -> Void = {
|
||||
if backgroundCompleted && separatorCompleted && textBackgroundCompleted {
|
||||
completion()
|
||||
}
|
||||
}
|
||||
|
||||
let initialBackgroundFrame = CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: self.bounds.size.width, height: max(0.0, initialTextBackgroundFrame.maxY + 8.0)))
|
||||
if let fromBackgroundColor = node.backgroundColor, let toBackgroundColor = self.backgroundNode.backgroundColor {
|
||||
self.backgroundNode.layer.animate(from: fromBackgroundColor.cgColor, to: toBackgroundColor.cgColor, keyPath: "backgroundColor", timingFunction: CAMediaTimingFunctionName.easeInEaseOut.rawValue, duration: duration * 0.7)
|
||||
} else {
|
||||
self.backgroundNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: duration)
|
||||
}
|
||||
self.backgroundNode.layer.animateFrame(from: initialBackgroundFrame, to: self.backgroundNode.frame, duration: duration, timingFunction: timingFunction, completion: { _ in
|
||||
backgroundCompleted = true
|
||||
intermediateCompletion()
|
||||
})
|
||||
|
||||
let initialSeparatorFrame = CGRect(origin: CGPoint(x: 0.0, y: max(0.0, initialTextBackgroundFrame.maxY + 8.0)), size: CGSize(width: self.bounds.size.width, height: UIScreenPixel))
|
||||
self.separatorNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: duration)
|
||||
self.separatorNode.layer.animateFrame(from: initialSeparatorFrame, to: self.separatorNode.frame, duration: duration, timingFunction: timingFunction, completion: { _ in
|
||||
separatorCompleted = true
|
||||
intermediateCompletion()
|
||||
})
|
||||
|
||||
self.textBackgroundNode.layer.animateFrame(from: initialTextBackgroundFrame, to: self.textBackgroundNode.frame, duration: duration, timingFunction: timingFunction, completion: { _ in
|
||||
textBackgroundCompleted = true
|
||||
intermediateCompletion()
|
||||
})
|
||||
|
||||
let labelFrame = self.textField.placeholderLabel.frame
|
||||
let initialLabelNodeFrame = CGRect(origin: node.labelNode.view.convert(node.labelNode.bounds, to: self.textField.superview).origin, size: labelFrame.size)
|
||||
self.textField.layer.animateFrame(from: CGRect(origin: initialLabelNodeFrame.origin.offsetBy(dx: -labelFrame.minX, dy: -labelFrame.minY), size: self.textField.frame.size), to: self.textField.frame, duration: duration, timingFunction: timingFunction)
|
||||
|
||||
let iconFrame = self.iconNode.frame
|
||||
let initialIconFrame = CGRect(origin: node.iconNode.view.convert(node.iconNode.bounds, to: self.iconNode.view.superview).origin, size: iconFrame.size)
|
||||
self.iconNode.layer.animateFrame(from: initialIconFrame, to: self.iconNode.frame, duration: duration, timingFunction: timingFunction)
|
||||
|
||||
let cancelButtonFrame = self.cancelButton.frame
|
||||
self.cancelButton.layer.animatePosition(from: CGPoint(x: self.bounds.size.width + cancelButtonFrame.size.width / 2.0, y: initialTextBackgroundFrame.minY + 2.0 + cancelButtonFrame.size.height / 2.0), to: self.cancelButton.layer.position, duration: duration, timingFunction: timingFunction)
|
||||
node.isHidden = true
|
||||
}
|
||||
|
||||
func deactivate(clear: Bool = true) {
|
||||
self.textField.resignFirstResponder()
|
||||
if clear {
|
||||
self.textField.text = nil
|
||||
self.textField.placeholderLabel.isHidden = false
|
||||
}
|
||||
}
|
||||
|
||||
func transitionOut(to node: PaneSearchBarPlaceholderNode, transition: ContainedViewLayoutTransition, completion: @escaping () -> Void) {
|
||||
let targetTextBackgroundFrame = node.view.convert(node.backgroundNode.view.frame, to: self.view)
|
||||
|
||||
let duration: Double = 0.5
|
||||
let timingFunction = kCAMediaTimingFunctionSpring
|
||||
|
||||
node.isHidden = true
|
||||
self.clearButton.isHidden = true
|
||||
self.textField.text = ""
|
||||
|
||||
var backgroundCompleted = false
|
||||
var separatorCompleted = false
|
||||
var textBackgroundCompleted = false
|
||||
let intermediateCompletion: () -> Void = { [weak node] in
|
||||
if backgroundCompleted && separatorCompleted && textBackgroundCompleted {
|
||||
completion()
|
||||
node?.isHidden = false
|
||||
}
|
||||
}
|
||||
|
||||
let targetBackgroundFrame = CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: self.bounds.size.width, height: max(0.0, targetTextBackgroundFrame.maxY + 8.0)))
|
||||
if let toBackgroundColor = node.backgroundColor, let fromBackgroundColor = self.backgroundNode.backgroundColor {
|
||||
self.backgroundNode.layer.animate(from: fromBackgroundColor.cgColor, to: toBackgroundColor.cgColor, keyPath: "backgroundColor", timingFunction: CAMediaTimingFunctionName.easeInEaseOut.rawValue, duration: duration * 0.5, removeOnCompletion: false)
|
||||
} else {
|
||||
self.backgroundNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: duration / 2.0, removeOnCompletion: false)
|
||||
}
|
||||
self.backgroundNode.layer.animateFrame(from: self.backgroundNode.frame, to: targetBackgroundFrame, duration: duration, timingFunction: timingFunction, removeOnCompletion: false, completion: { _ in
|
||||
backgroundCompleted = true
|
||||
intermediateCompletion()
|
||||
})
|
||||
|
||||
let targetSeparatorFrame = CGRect(origin: CGPoint(x: 0.0, y: max(0.0, targetTextBackgroundFrame.maxY + 8.0)), size: CGSize(width: self.bounds.size.width, height: UIScreenPixel))
|
||||
self.separatorNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: duration / 2.0, removeOnCompletion: false)
|
||||
self.separatorNode.layer.animateFrame(from: self.separatorNode.frame, to: targetSeparatorFrame, duration: duration, timingFunction: timingFunction, removeOnCompletion: false, completion: { _ in
|
||||
separatorCompleted = true
|
||||
intermediateCompletion()
|
||||
})
|
||||
|
||||
self.textBackgroundNode.layer.animateFrame(from: self.textBackgroundNode.frame, to: targetTextBackgroundFrame, duration: duration, timingFunction: timingFunction, removeOnCompletion: false, completion: { _ in
|
||||
textBackgroundCompleted = true
|
||||
intermediateCompletion()
|
||||
})
|
||||
|
||||
let transitionBackgroundNode = ASImageNode()
|
||||
transitionBackgroundNode.isLayerBacked = true
|
||||
transitionBackgroundNode.displaysAsynchronously = false
|
||||
transitionBackgroundNode.displayWithoutProcessing = true
|
||||
transitionBackgroundNode.image = node.backgroundNode.image
|
||||
self.insertSubnode(transitionBackgroundNode, aboveSubnode: self.textBackgroundNode)
|
||||
transitionBackgroundNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: duration / 2.0, removeOnCompletion: false)
|
||||
transitionBackgroundNode.layer.animateFrame(from: self.textBackgroundNode.frame, to: targetTextBackgroundFrame, duration: duration, timingFunction: timingFunction, removeOnCompletion: false)
|
||||
|
||||
let textFieldFrame = self.textField.frame
|
||||
let targetLabelNodeFrame = CGRect(origin: node.labelNode.view.convert(node.labelNode.bounds, to: self.textField.superview).origin, size: textFieldFrame.size)
|
||||
self.textField.layer.animateFrame(from: self.textField.frame, to: CGRect(origin: targetLabelNodeFrame.origin.offsetBy(dx: -self.textField.placeholderLabel.frame.minX, dy: -self.textField.placeholderLabel.frame.minY), size: self.textField.frame.size), duration: duration, timingFunction: timingFunction, removeOnCompletion: false)
|
||||
if let snapshot = node.labelNode.layer.snapshotContentTree() {
|
||||
snapshot.frame = CGRect(origin: self.textField.placeholderLabel.frame.origin, size: node.labelNode.frame.size)
|
||||
self.textField.layer.addSublayer(snapshot)
|
||||
snapshot.animateAlpha(from: 0.0, to: 1.0, duration: duration * 2.0 / 3.0, timingFunction: CAMediaTimingFunctionName.linear.rawValue)
|
||||
//self.textField.placeholderLabel.layer.animateAlpha(from: 1.0, to: 0.0, duration: duration * 3.0 / 2.0, timingFunction: CAMediaTimingFunctionName.linear.rawValue, removeOnCompletion: false)
|
||||
|
||||
}
|
||||
|
||||
let iconFrame = self.iconNode.frame
|
||||
let targetIconFrame = CGRect(origin: node.iconNode.view.convert(node.iconNode.bounds, to: self.iconNode.view.superview).origin, size: iconFrame.size)
|
||||
self.iconNode.image = node.iconNode.image
|
||||
self.iconNode.layer.animateFrame(from: self.iconNode.frame, to: targetIconFrame, duration: duration, timingFunction: timingFunction, removeOnCompletion: false)
|
||||
|
||||
let cancelButtonFrame = self.cancelButton.frame
|
||||
self.cancelButton.layer.animatePosition(from: self.cancelButton.layer.position, to: CGPoint(x: self.bounds.size.width + cancelButtonFrame.size.width / 2.0, y: targetTextBackgroundFrame.minY + 4.0 + cancelButtonFrame.size.height / 2.0), duration: duration, timingFunction: timingFunction, removeOnCompletion: false)
|
||||
}
|
||||
|
||||
func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool {
|
||||
if string.range(of: "\n") != nil {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func textFieldShouldReturn(_ textField: UITextField) -> Bool {
|
||||
self.textField.resignFirstResponder()
|
||||
return false
|
||||
}
|
||||
|
||||
@objc func textFieldDidChange(_ textField: UITextField) {
|
||||
self.updateIsEmpty()
|
||||
if let textUpdated = self.textUpdated {
|
||||
textUpdated(textField.text ?? "", self.textField.textInputMode?.primaryLanguage ?? "")
|
||||
}
|
||||
}
|
||||
|
||||
private func updateIsEmpty() {
|
||||
let isEmpty = !(textField.text?.isEmpty ?? true)
|
||||
if isEmpty != self.textField.placeholderLabel.isHidden {
|
||||
self.textField.placeholderLabel.isHidden = isEmpty
|
||||
}
|
||||
self.clearButton.isHidden = !isEmpty && self.prefixString == nil
|
||||
}
|
||||
|
||||
@objc func cancelPressed() {
|
||||
if let cancel = self.cancel {
|
||||
cancel()
|
||||
}
|
||||
}
|
||||
|
||||
@objc func clearPressed() {
|
||||
if (self.textField.text?.isEmpty ?? true) {
|
||||
if self.prefixString != nil {
|
||||
self.clearPrefix?()
|
||||
}
|
||||
} else {
|
||||
self.textField.text = ""
|
||||
self.textFieldDidChange(self.textField)
|
||||
}
|
||||
}
|
||||
|
||||
func updateQuery(_ query: String) {
|
||||
self.textField.text = query
|
||||
self.textFieldDidChange(self.textField)
|
||||
}
|
||||
}
|
||||
|
|
@ -2,6 +2,7 @@ import Foundation
|
|||
import UIKit
|
||||
import AsyncDisplayKit
|
||||
import Display
|
||||
import SearchBarNode
|
||||
import SwiftSignalKit
|
||||
import Postbox
|
||||
import TelegramCore
|
||||
|
|
@ -16,7 +17,23 @@ import StickerPeekUI
|
|||
import EntityKeyboardGifContent
|
||||
import BatchVideoRendering
|
||||
|
||||
private let searchBarHeight: CGFloat = 52.0
|
||||
private let searchBarHeight: CGFloat = 76.0
|
||||
private let searchBarTopInset: CGFloat = 16.0
|
||||
private let searchBarFieldHeight: CGFloat = 44.0
|
||||
|
||||
private func paneSearchBarTheme(_ theme: PresentationTheme) -> SearchBarNodeTheme {
|
||||
return SearchBarNodeTheme(
|
||||
background: .clear,
|
||||
separator: .clear,
|
||||
inputFill: .clear,
|
||||
primaryText: theme.chat.inputPanel.panelControlColor,
|
||||
placeholder: theme.chat.inputPanel.inputPlaceholderColor,
|
||||
inputIcon: theme.chat.inputPanel.inputControlColor,
|
||||
inputClear: theme.chat.inputPanel.panelControlColor,
|
||||
accent: theme.chat.inputPanel.panelControlAccentColor,
|
||||
keyboard: theme.rootController.keyboardColor
|
||||
)
|
||||
}
|
||||
|
||||
public protocol PaneSearchContentNode {
|
||||
var ready: Signal<Void, NoError> { get }
|
||||
|
|
@ -43,9 +60,10 @@ public final class PaneSearchContainerNode: ASDisplayNode, EntitySearchContainer
|
|||
private let peekBehavior: EmojiContentPeekBehavior?
|
||||
|
||||
private let backgroundNode: ASDisplayNode
|
||||
private let searchBar: PaneSearchBarNode
|
||||
private let searchBar: SearchBarNode
|
||||
|
||||
private var validLayout: CGSize?
|
||||
private weak var animatedPlaceholder: PaneSearchBarPlaceholderNode?
|
||||
|
||||
public var onCancel: (() -> Void)?
|
||||
|
||||
|
|
@ -69,7 +87,13 @@ public final class PaneSearchContainerNode: ASDisplayNode, EntitySearchContainer
|
|||
}
|
||||
self.backgroundNode = ASDisplayNode()
|
||||
|
||||
self.searchBar = PaneSearchBarNode()
|
||||
self.searchBar = SearchBarNode(
|
||||
theme: paneSearchBarTheme(theme),
|
||||
presentationTheme: theme,
|
||||
strings: strings,
|
||||
fieldStyle: .glass,
|
||||
displayBackground: false
|
||||
)
|
||||
|
||||
super.init()
|
||||
|
||||
|
|
@ -87,9 +111,8 @@ public final class PaneSearchContainerNode: ASDisplayNode, EntitySearchContainer
|
|||
}
|
||||
|
||||
self.searchBar.cancel = { [weak self] in
|
||||
self?.searchBar.deactivate(clear: false)
|
||||
cancel()
|
||||
|
||||
self?.searchBar.view.endEditing(true)
|
||||
self?.onCancel?()
|
||||
}
|
||||
self.searchBar.activate()
|
||||
|
|
@ -150,7 +173,7 @@ public final class PaneSearchContainerNode: ASDisplayNode, EntitySearchContainer
|
|||
public func updateThemeAndStrings(theme: PresentationTheme, strings: PresentationStrings) {
|
||||
self.backgroundNode.backgroundColor = theme.chat.inputMediaPanel.stickersBackgroundColor.withAlphaComponent(1.0)
|
||||
self.contentNode.updateThemeAndStrings(theme: theme, strings: strings)
|
||||
self.searchBar.updateThemeAndStrings(theme: theme, strings: strings)
|
||||
self.searchBar.updateThemeAndStrings(theme: paneSearchBarTheme(theme), presentationTheme: theme, strings: strings)
|
||||
|
||||
let placeholder: String
|
||||
switch mode {
|
||||
|
|
@ -159,11 +182,11 @@ public final class PaneSearchContainerNode: ASDisplayNode, EntitySearchContainer
|
|||
case .sticker, .trending:
|
||||
placeholder = strings.Stickers_Search
|
||||
}
|
||||
self.searchBar.placeholderString = NSAttributedString(string: placeholder, font: Font.regular(17.0), textColor: theme.chat.inputMediaPanel.stickersSearchPlaceholderColor)
|
||||
self.searchBar.placeholderString = NSAttributedString(string: placeholder, font: Font.regular(17.0), textColor: theme.rootController.navigationSearchBar.inputPlaceholderTextColor)
|
||||
}
|
||||
|
||||
public func updateQuery(_ query: String) {
|
||||
self.searchBar.updateQuery(query)
|
||||
self.searchBar.text = query
|
||||
}
|
||||
|
||||
public func itemAt(point: CGPoint) -> (ASDisplayNode, Any)? {
|
||||
|
|
@ -174,8 +197,9 @@ public final class PaneSearchContainerNode: ASDisplayNode, EntitySearchContainer
|
|||
self.validLayout = size
|
||||
transition.updateFrame(node: self.backgroundNode, frame: CGRect(origin: CGPoint(), size: size))
|
||||
|
||||
transition.updateFrame(node: self.searchBar, frame: CGRect(origin: CGPoint(), size: CGSize(width: size.width, height: searchBarHeight)))
|
||||
self.searchBar.updateLayout(boundingSize: CGSize(width: size.width, height: searchBarHeight), leftInset: leftInset, rightInset: rightInset, transition: transition)
|
||||
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)
|
||||
|
||||
let contentFrame = CGRect(origin: CGPoint(x: leftInset, y: searchBarHeight), size: CGSize(width: size.width - leftInset - rightInset, height: size.height - searchBarHeight))
|
||||
|
||||
|
|
@ -190,6 +214,9 @@ public final class PaneSearchContainerNode: ASDisplayNode, EntitySearchContainer
|
|||
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)
|
||||
|
|
@ -197,33 +224,65 @@ public final class PaneSearchContainerNode: ASDisplayNode, EntitySearchContainer
|
|||
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)
|
||||
if let placeholder = placeholder {
|
||||
self.searchBar.animateIn(from: placeholder, duration: duration, timingFunction: curve.timingFunction, completion: completion)
|
||||
} else {
|
||||
self.searchBar.alpha = 0.0
|
||||
transition.updateAlpha(node: self.searchBar, alpha: 1.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)))
|
||||
self.backgroundNode.layer.animateFrame(from: initialBackgroundFrame, to: self.backgroundNode.frame, duration: duration, timingFunction: curve.timingFunction)
|
||||
}
|
||||
case .immediate:
|
||||
completion()
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
public func animateOut(to placeholder: PaneSearchBarPlaceholderNode, animateOutSearchBar: Bool, transition: ContainedViewLayoutTransition, completion: @escaping () -> Void) {
|
||||
let finish: () -> Void = { [weak self] in
|
||||
placeholder.isHidden = false
|
||||
if let self, self.animatedPlaceholder === placeholder {
|
||||
self.animatedPlaceholder = nil
|
||||
}
|
||||
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 {
|
||||
let placeholderFrame = placeholder.view.convert(placeholder.bounds, to: self.view)
|
||||
let verticalOrigin = placeholderFrame.minY - 4.0
|
||||
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)
|
||||
}
|
||||
|
||||
self.searchBar.layer.animateFrame(from: self.searchBar.frame, to: targetSearchBarFrame, duration: duration, timingFunction: curve.timingFunction, removeOnCompletion: false)
|
||||
if animateOutSearchBar {
|
||||
self.searchBar.alpha = 0.0
|
||||
self.searchBar.layer.animateAlpha(from: 1.0, to: 0.0, duration: duration, timingFunction: curve.timingFunction, removeOnCompletion: false, completion: { _ in
|
||||
finish()
|
||||
})
|
||||
} else {
|
||||
self.searchBar.layer.animateAlpha(from: self.searchBar.alpha, to: self.searchBar.alpha, duration: duration, timingFunction: curve.timingFunction, removeOnCompletion: false, completion: { _ in
|
||||
finish()
|
||||
})
|
||||
}
|
||||
} else {
|
||||
if animateOutSearchBar {
|
||||
self.searchBar.alpha = 0.0
|
||||
}
|
||||
finish()
|
||||
}
|
||||
self.searchBar.transitionOut(to: placeholder, transition: transition, completion: completion)
|
||||
|
||||
transition.updateAlpha(node: self.backgroundNode, alpha: 0.0)
|
||||
if animateOutSearchBar {
|
||||
transition.updateAlpha(node: self.searchBar, alpha: 0.0)
|
||||
|
|
|
|||
|
|
@ -25,6 +25,7 @@ swift_library(
|
|||
"//submodules/TelegramUI/Components/ToastComponent",
|
||||
"//submodules/Markdown",
|
||||
"//submodules/UndoUI",
|
||||
"//submodules/TooltipUI",
|
||||
"//submodules/TelegramUI/Components/LottieComponent",
|
||||
"//submodules/Components/MultilineTextComponent",
|
||||
"//submodules/Components/BundleIconComponent",
|
||||
|
|
@ -36,6 +37,7 @@ swift_library(
|
|||
"//submodules/TelegramUI/Components/GlassBackgroundComponent",
|
||||
"//submodules/TelegramUI/Components/GlassBarButtonComponent",
|
||||
"//submodules/TelegramUI/Components/PlainButtonComponent",
|
||||
"//submodules/Components/LottieAnimationComponent",
|
||||
],
|
||||
visibility = [
|
||||
"//visibility:public",
|
||||
|
|
|
|||
|
|
@ -13,10 +13,12 @@ import SheetComponent
|
|||
import ButtonComponent
|
||||
import PlainButtonComponent
|
||||
import BundleIconComponent
|
||||
import LottieAnimationComponent
|
||||
import GlassBackgroundComponent
|
||||
import GlassBarButtonComponent
|
||||
import DatePickerNode
|
||||
import UndoUI
|
||||
import TooltipUI
|
||||
|
||||
private let calendar = Calendar(identifier: .gregorian)
|
||||
|
||||
|
|
@ -66,6 +68,8 @@ private final class ChatScheduleTimeSheetContentComponent: Component {
|
|||
|
||||
final class View: UIView {
|
||||
private let cancel = ComponentView<Empty>()
|
||||
private let silent = ComponentView<Empty>()
|
||||
|
||||
private let title = ComponentView<Empty>()
|
||||
private let button = ComponentView<Empty>()
|
||||
private let secondaryButton = ComponentView<Empty>()
|
||||
|
|
@ -93,13 +97,15 @@ private final class ChatScheduleTimeSheetContentComponent: Component {
|
|||
|
||||
private var monthHeight: CGFloat?
|
||||
|
||||
private var isSilentPosting = false
|
||||
|
||||
private var date: Date?
|
||||
private var minDate: Date?
|
||||
private var maxDate: Date?
|
||||
|
||||
private var isPickingTime = false
|
||||
private var isPickingRepeatPeriod = false
|
||||
|
||||
|
||||
private var repeatPeriod: Int32?
|
||||
|
||||
private let dateFormatter: DateFormatter
|
||||
|
|
@ -144,6 +150,65 @@ private final class ChatScheduleTimeSheetContentComponent: Component {
|
|||
}
|
||||
}
|
||||
|
||||
private func presentSilentPostingTooltip() {
|
||||
guard let component = self.component, let sourceView = self.silent.view else {
|
||||
return
|
||||
}
|
||||
|
||||
let peerId: EnginePeer.Id?
|
||||
switch component.mode {
|
||||
case let .scheduledMessages(peerIdValue, _):
|
||||
peerId = peerIdValue
|
||||
case .reminders:
|
||||
peerId = component.context.account.peerId
|
||||
default:
|
||||
peerId = nil
|
||||
}
|
||||
guard let peerId else {
|
||||
return
|
||||
}
|
||||
|
||||
let isSilentPosting = self.isSilentPosting
|
||||
let _ = (component.context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: peerId))
|
||||
|> deliverOnMainQueue).start(next: { [weak self] peer in
|
||||
guard let self, let peer, let controller = self.environment?.controller() else {
|
||||
return
|
||||
}
|
||||
|
||||
var isChannel = false
|
||||
if case let .channel(channel) = peer, case .broadcast = channel.info {
|
||||
isChannel = true
|
||||
}
|
||||
|
||||
//TODO:localize
|
||||
let text: String
|
||||
if case .user = peer {
|
||||
if isSilentPosting {
|
||||
text = "\(peer.compactDisplayTitle) will receive a silent notification"
|
||||
} else {
|
||||
text = "\(peer.compactDisplayTitle) will be notified"
|
||||
}
|
||||
} else if isChannel {
|
||||
if isSilentPosting {
|
||||
text = "Subscribers will receive a silent notification"
|
||||
} else {
|
||||
text = "Subscribers will be notified"
|
||||
}
|
||||
} else {
|
||||
if isSilentPosting {
|
||||
text = "Members will receive a silent notification"
|
||||
} else {
|
||||
text = "Members will be notified"
|
||||
}
|
||||
}
|
||||
|
||||
let sourceFrame = sourceView.convert(sourceView.bounds, to: nil)
|
||||
controller.present(TooltipScreen(account: component.context.account, sharedContext: component.context.sharedContext, text: .plain(text: text), style: .default, icon: .none, location: .point(sourceFrame, .bottom), shouldDismissOnTouch: { _, _ in
|
||||
return .dismiss(consume: false)
|
||||
}), in: .window(.root))
|
||||
})
|
||||
}
|
||||
|
||||
func update(component: ChatScheduleTimeSheetContentComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<EnvironmentType>, transition: ComponentTransition) -> CGSize {
|
||||
self.isUpdating = true
|
||||
defer {
|
||||
|
|
@ -184,40 +249,6 @@ private final class ChatScheduleTimeSheetContentComponent: Component {
|
|||
var contentHeight: CGFloat = 0.0
|
||||
contentHeight += 30.0
|
||||
|
||||
let barButtonSize = CGSize(width: 44.0, height: 44.0)
|
||||
let cancelSize = self.cancel.update(
|
||||
transition: transition,
|
||||
component: AnyComponent(
|
||||
GlassBarButtonComponent(
|
||||
size: barButtonSize,
|
||||
backgroundColor: nil,
|
||||
isDark: environment.theme.overallDarkAppearance,
|
||||
state: .glass,
|
||||
component: AnyComponentWithIdentity(id: "close", component: AnyComponent(
|
||||
BundleIconComponent(
|
||||
name: "Navigation/Close",
|
||||
tintColor: environment.theme.chat.inputPanel.panelControlColor
|
||||
)
|
||||
)),
|
||||
action: { [weak self] _ in
|
||||
guard let self, let component = self.component else {
|
||||
return
|
||||
}
|
||||
component.dismiss()
|
||||
}
|
||||
)
|
||||
),
|
||||
environment: {},
|
||||
containerSize: barButtonSize
|
||||
)
|
||||
let cancelFrame = CGRect(origin: CGPoint(x: 16.0, y: 16.0), size: cancelSize)
|
||||
if let cancelView = self.cancel.view {
|
||||
if cancelView.superview == nil {
|
||||
self.addSubview(cancelView)
|
||||
}
|
||||
transition.setFrame(view: cancelView, frame: cancelFrame)
|
||||
}
|
||||
|
||||
let title: String
|
||||
switch component.mode {
|
||||
case .scheduledMessages:
|
||||
|
|
@ -595,7 +626,7 @@ private final class ChatScheduleTimeSheetContentComponent: Component {
|
|||
transition.setFrame(view: buttonView, frame: buttonFrame)
|
||||
}
|
||||
contentHeight += buttonSize.height
|
||||
} else if case .scheduledMessages(true) = component.mode {
|
||||
} else if case .scheduledMessages(_, true) = component.mode {
|
||||
contentHeight += 8.0
|
||||
|
||||
let buttonSize = self.secondaryButton.update(
|
||||
|
|
@ -784,6 +815,87 @@ private final class ChatScheduleTimeSheetContentComponent: Component {
|
|||
|
||||
component.externalState.repeatValueFrame = repeatValueFrame
|
||||
|
||||
let barButtonSize = CGSize(width: 44.0, height: 44.0)
|
||||
let cancelSize = self.cancel.update(
|
||||
transition: transition,
|
||||
component: AnyComponent(
|
||||
GlassBarButtonComponent(
|
||||
size: barButtonSize,
|
||||
backgroundColor: nil,
|
||||
isDark: environment.theme.overallDarkAppearance,
|
||||
state: .glass,
|
||||
component: AnyComponentWithIdentity(id: "close", component: AnyComponent(
|
||||
BundleIconComponent(
|
||||
name: "Navigation/Close",
|
||||
tintColor: environment.theme.chat.inputPanel.panelControlColor
|
||||
)
|
||||
)),
|
||||
action: { [weak self] _ in
|
||||
guard let self, let component = self.component else {
|
||||
return
|
||||
}
|
||||
component.dismiss()
|
||||
}
|
||||
)
|
||||
),
|
||||
environment: {},
|
||||
containerSize: barButtonSize
|
||||
)
|
||||
let cancelFrame = CGRect(origin: CGPoint(x: 16.0, y: 16.0), size: cancelSize)
|
||||
if let cancelView = self.cancel.view {
|
||||
if cancelView.superview == nil {
|
||||
self.addSubview(cancelView)
|
||||
}
|
||||
transition.setFrame(view: cancelView, frame: cancelFrame)
|
||||
}
|
||||
|
||||
switch component.mode {
|
||||
case .scheduledMessages, .reminders:
|
||||
let silentSize = self.silent.update(
|
||||
transition: transition,
|
||||
component: AnyComponent(
|
||||
GlassBarButtonComponent(
|
||||
size: barButtonSize,
|
||||
backgroundColor: nil,
|
||||
isDark: environment.theme.overallDarkAppearance,
|
||||
state: .glass,
|
||||
component: AnyComponentWithIdentity(id: "silent", component: AnyComponent(
|
||||
LottieAnimationComponent(
|
||||
animation: LottieAnimationComponent.AnimationItem(
|
||||
name: self.isSilentPosting ? "NavigationMuteOn" : "NavigationMuteOff",
|
||||
mode: !transition.animation.isImmediate ? .animating(loop: false) : .still(position: .end),
|
||||
range: nil,
|
||||
waitForCompletion: false
|
||||
),
|
||||
colors: ["__allcolors__": environment.theme.chat.inputPanel.panelControlColor],
|
||||
size: CGSize(width: 30.0, height: 30.0)
|
||||
)
|
||||
)),
|
||||
action: { [weak self] _ in
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
self.isSilentPosting = !self.isSilentPosting
|
||||
self.state?.updated(transition: .easeInOut(duration: 0.2))
|
||||
|
||||
self.presentSilentPostingTooltip()
|
||||
}
|
||||
)
|
||||
),
|
||||
environment: {},
|
||||
containerSize: barButtonSize
|
||||
)
|
||||
let silentFrame = CGRect(origin: CGPoint(x: availableSize.width - 16.0 - silentSize.width, y: 16.0), size: silentSize)
|
||||
if let silentView = self.silent.view {
|
||||
if silentView.superview == nil {
|
||||
self.addSubview(silentView)
|
||||
}
|
||||
transition.setFrame(view: silentView, frame: silentFrame)
|
||||
}
|
||||
default:
|
||||
break
|
||||
}
|
||||
|
||||
return contentSize
|
||||
}
|
||||
}
|
||||
|
|
@ -958,7 +1070,7 @@ private final class ChatScheduleTimeScreenComponent: Component {
|
|||
|
||||
public class ChatScheduleTimeScreen: ViewControllerComponentContainer {
|
||||
public enum Mode: Equatable {
|
||||
case scheduledMessages(sendWhenOnlineAvailable: Bool)
|
||||
case scheduledMessages(peerId: EnginePeer.Id, sendWhenOnlineAvailable: Bool)
|
||||
case reminders
|
||||
case format
|
||||
case poll
|
||||
|
|
|
|||
|
|
@ -975,6 +975,12 @@ final class ComposeTodoScreenComponent: Component {
|
|||
}
|
||||
}
|
||||
},
|
||||
present: { [weak self] c in
|
||||
guard let controller = self?.environment?.controller() else {
|
||||
return
|
||||
}
|
||||
controller.present(c, in: .window(.root))
|
||||
},
|
||||
tag: todoItem.textFieldTag
|
||||
))))
|
||||
|
||||
|
|
|
|||
|
|
@ -424,6 +424,13 @@ final class InnerTextSelectionTipContainerNode: ASDisplayNode {
|
|||
self.text = self.presentationData.strings.Camera_CollageReorderingInfo
|
||||
self.targetSelectionIndex = nil
|
||||
icon = UIImage(bundleImageName: "Chat/Context Menu/Tip")
|
||||
case .deleteReaction:
|
||||
self.action = nil
|
||||
//TODO:localize
|
||||
self.text = "Tap and hold to delete reaction."
|
||||
self.targetSelectionIndex = nil
|
||||
icon = nil
|
||||
isUserInteractionEnabled = false
|
||||
}
|
||||
|
||||
self.iconNode = ASImageNode()
|
||||
|
|
|
|||
|
|
@ -93,15 +93,23 @@ public final class ListActionItemComponent: Component {
|
|||
|
||||
public enum LeftIcon: Equatable {
|
||||
public final class Check: Equatable {
|
||||
public enum Style {
|
||||
case round
|
||||
case rectangle
|
||||
}
|
||||
|
||||
public let style: Style
|
||||
public let isSelected: Bool
|
||||
public let isEnabled: Bool
|
||||
public let toggle: (() -> Void)?
|
||||
|
||||
public init(
|
||||
style: Style = .round,
|
||||
isSelected: Bool,
|
||||
isEnabled: Bool = true,
|
||||
toggle: (() -> Void)?
|
||||
) {
|
||||
self.style = style
|
||||
self.isSelected = isSelected
|
||||
self.isEnabled = isEnabled
|
||||
self.toggle = toggle
|
||||
|
|
@ -111,6 +119,9 @@ public final class ListActionItemComponent: Component {
|
|||
if lhs === rhs {
|
||||
return true
|
||||
}
|
||||
if lhs.style != rhs.style {
|
||||
return false
|
||||
}
|
||||
if lhs.isSelected != rhs.isSelected {
|
||||
return false
|
||||
}
|
||||
|
|
@ -318,12 +329,12 @@ public final class ListActionItemComponent: Component {
|
|||
self.action?()
|
||||
}
|
||||
|
||||
func update(size: CGSize, theme: PresentationTheme, isSelected: Bool, transition: ComponentTransition) {
|
||||
func update(size: CGSize, theme: PresentationTheme, isSelected: Bool, isRectangle: Bool = false, transition: ComponentTransition) {
|
||||
let checkLayer: CheckLayer
|
||||
if let current = self.checkLayer {
|
||||
checkLayer = current
|
||||
} else {
|
||||
checkLayer = CheckLayer(theme: CheckNodeTheme(theme: theme, style: .plain), content: .check(isRectangle: false))
|
||||
checkLayer = CheckLayer(theme: CheckNodeTheme(theme: theme, style: .plain), content: .check(isRectangle: isRectangle))
|
||||
self.checkLayer = checkLayer
|
||||
self.layer.addSublayer(checkLayer)
|
||||
}
|
||||
|
|
@ -625,11 +636,11 @@ public final class ListActionItemComponent: Component {
|
|||
leftCheckView.frame = CGRect(origin: CGPoint(x: -checkSize.width, y: self.bounds.height == 0.0 ? checkFrame.minY : floor((self.bounds.height - checkSize.height) * 0.5)), size: checkFrame.size)
|
||||
transition.setPosition(view: leftCheckView, position: checkFrame.center)
|
||||
transition.setBounds(view: leftCheckView, bounds: CGRect(origin: CGPoint(), size: checkFrame.size))
|
||||
leftCheckView.update(size: checkFrame.size, theme: component.theme, isSelected: check.isSelected, transition: .immediate)
|
||||
leftCheckView.update(size: checkFrame.size, theme: component.theme, isSelected: check.isSelected, isRectangle: check.style == .rectangle, transition: .immediate)
|
||||
} else {
|
||||
transition.setPosition(view: leftCheckView, position: checkFrame.center)
|
||||
transition.setBounds(view: leftCheckView, bounds: CGRect(origin: CGPoint(), size: checkFrame.size))
|
||||
leftCheckView.update(size: checkFrame.size, theme: component.theme, isSelected: check.isSelected, transition: transition)
|
||||
leftCheckView.update(size: checkFrame.size, theme: component.theme, isSelected: check.isSelected, isRectangle: check.style == .rectangle, transition: transition)
|
||||
}
|
||||
case let .custom(customLeftIcon, adjustLeftInset):
|
||||
var resetLeftIcon = false
|
||||
|
|
|
|||
|
|
@ -130,6 +130,7 @@ public final class ListComposePollOptionComponent: Component {
|
|||
public let canReorder: Bool
|
||||
public let canAdd: Bool
|
||||
public let attachment: Attachment?
|
||||
public let formattingAvailable: Bool
|
||||
public let emptyLineHandling: TextFieldComponent.EmptyLineHandling
|
||||
public let returnKeyType: UIReturnKeyType
|
||||
public let returnKeyAction: (() -> Void)?
|
||||
|
|
@ -141,6 +142,7 @@ public final class ListComposePollOptionComponent: Component {
|
|||
public let attachAction: (() -> Void)?
|
||||
public let deleteAction: (() -> Void)?
|
||||
public let paste: ((TextFieldComponent.PasteData) -> Void)?
|
||||
public let present: ((ViewController) -> Void)?
|
||||
public let tag: AnyObject?
|
||||
|
||||
public init(
|
||||
|
|
@ -159,6 +161,7 @@ public final class ListComposePollOptionComponent: Component {
|
|||
canReorder: Bool = false,
|
||||
canAdd: Bool = false,
|
||||
attachment: Attachment? = nil,
|
||||
formattingAvailable: Bool = false,
|
||||
emptyLineHandling: TextFieldComponent.EmptyLineHandling,
|
||||
returnKeyType: UIReturnKeyType = .next,
|
||||
returnKeyAction: (() -> Void)? = nil,
|
||||
|
|
@ -170,6 +173,7 @@ public final class ListComposePollOptionComponent: Component {
|
|||
attachAction: (() -> Void)? = nil,
|
||||
deleteAction: (() -> Void)? = nil,
|
||||
paste: ((TextFieldComponent.PasteData) -> Void)? = nil,
|
||||
present: ((ViewController) -> Void)? = nil,
|
||||
tag: AnyObject? = nil
|
||||
) {
|
||||
self.externalState = externalState
|
||||
|
|
@ -187,6 +191,7 @@ public final class ListComposePollOptionComponent: Component {
|
|||
self.canReorder = canReorder
|
||||
self.canAdd = canAdd
|
||||
self.attachment = attachment
|
||||
self.formattingAvailable = formattingAvailable
|
||||
self.emptyLineHandling = emptyLineHandling
|
||||
self.returnKeyType = returnKeyType
|
||||
self.returnKeyAction = returnKeyAction
|
||||
|
|
@ -198,6 +203,7 @@ public final class ListComposePollOptionComponent: Component {
|
|||
self.attachAction = attachAction
|
||||
self.deleteAction = deleteAction
|
||||
self.paste = paste
|
||||
self.present = present
|
||||
self.tag = tag
|
||||
}
|
||||
|
||||
|
|
@ -247,6 +253,9 @@ public final class ListComposePollOptionComponent: Component {
|
|||
if lhs.attachment != rhs.attachment {
|
||||
return false
|
||||
}
|
||||
if lhs.formattingAvailable != rhs.formattingAvailable {
|
||||
return false
|
||||
}
|
||||
if lhs.emptyLineHandling != rhs.emptyLineHandling {
|
||||
return false
|
||||
}
|
||||
|
|
@ -732,11 +741,15 @@ public final class ListComposePollOptionComponent: Component {
|
|||
enableInlineAnimations: component.enableInlineAnimations,
|
||||
emptyLineHandling: component.emptyLineHandling,
|
||||
externalHandlingForMultilinePaste: true,
|
||||
formatMenuAvailability: .none,
|
||||
formatMenuAvailability: component.formattingAvailable ? .available([.bold, .italic, .strikethrough, .underline, .monospace, .spoiler, .link]) : .none,
|
||||
returnKeyType: component.returnKeyType,
|
||||
lockedFormatAction: {
|
||||
},
|
||||
present: { _ in
|
||||
present: { [weak self] c in
|
||||
guard let self, let component = self.component else {
|
||||
return
|
||||
}
|
||||
component.present?(c)
|
||||
},
|
||||
paste: { [weak self] data in
|
||||
guard let self, let component = self.component else {
|
||||
|
|
|
|||
|
|
@ -167,7 +167,11 @@ private final class PeerInfoScreenCommentItemNode: PeerInfoScreenItemNode {
|
|||
|
||||
let height = textSize.height + verticalInset * 2.0
|
||||
|
||||
transition.updateFrame(node: self.textNode, frame: textFrame)
|
||||
if self.textNode.frame.size != textFrame.size {
|
||||
self.textNode.frame = textFrame
|
||||
} else {
|
||||
transition.updateFrame(node: self.textNode, frame: textFrame)
|
||||
}
|
||||
|
||||
self.activateArea.frame = CGRect(origin: CGPoint(x: safeInsets.left, y: 0.0), size: CGSize(width: width - safeInsets.left - safeInsets.right, height: height))
|
||||
|
||||
|
|
|
|||
|
|
@ -375,17 +375,30 @@ func settingsEditingItems(data: PeerInfoScreenData?, state: PeerInfoState, conte
|
|||
items[.help]!.append(PeerInfoScreenCommentItem(id: ItemNameHelp, text: presentationData.strings.EditProfile_NameAndPhotoOrVideoHelp))
|
||||
|
||||
if let cachedData = data.cachedData as? CachedUserData {
|
||||
items[.bio]!.append(PeerInfoScreenMultilineInputItem(id: ItemBio, text: state.updatingBio ?? (cachedData.about ?? ""), placeholder: presentationData.strings.UserInfo_About_Placeholder, textUpdated: { updatedText in
|
||||
let currentBio = state.updatingBio ?? (cachedData.about ?? "")
|
||||
items[.bio]!.append(PeerInfoScreenMultilineInputItem(id: ItemBio, text: currentBio, placeholder: presentationData.strings.UserInfo_About_Placeholder, textUpdated: { updatedText in
|
||||
interaction.updateBio(updatedText)
|
||||
}, action: {
|
||||
interaction.dismissInput()
|
||||
}, maxLength: Int(data.globalSettings?.userLimits.maxAboutLength ?? 70)))
|
||||
items[.bio]!.append(PeerInfoScreenCommentItem(id: ItemBioHelp, text: presentationData.strings.Settings_About_PrivacyHelp, linkAction: { _ in
|
||||
|
||||
|
||||
var bioPrivacyInfo = presentationData.strings.Settings_About_PrivacyHelpEmpty
|
||||
if let bioPrivacy = data.globalSettings?.privacySettings?.bio, !currentBio.isEmpty {
|
||||
switch bioPrivacy {
|
||||
case .enableEveryone:
|
||||
bioPrivacyInfo = presentationData.strings.Settings_About_PrivacyHelpEveryone
|
||||
case .enableContacts:
|
||||
bioPrivacyInfo = presentationData.strings.Settings_About_PrivacyHelpContacts
|
||||
case .disableEveryone:
|
||||
bioPrivacyInfo = presentationData.strings.Settings_About_PrivacyHelpNobody
|
||||
}
|
||||
}
|
||||
items[.bio]!.append(PeerInfoScreenCommentItem(id: ItemBioHelp, text: bioPrivacyInfo, linkAction: { _ in
|
||||
interaction.openBioPrivacy()
|
||||
}))
|
||||
}
|
||||
|
||||
|
||||
var birthday: TelegramBirthday?
|
||||
if let updatingBirthDate = state.updatingBirthDate {
|
||||
birthday = updatingBirthDate
|
||||
|
|
@ -414,14 +427,22 @@ func settingsEditingItems(data: PeerInfoScreenData?, state: PeerInfoState, conte
|
|||
}))
|
||||
}
|
||||
|
||||
|
||||
var birthdayIsForContactsOnly = false
|
||||
if let birthdayPrivacy = data.globalSettings?.privacySettings?.birthday, case .enableContacts = birthdayPrivacy {
|
||||
birthdayIsForContactsOnly = true
|
||||
var birthdayPrivacyInfo = ""
|
||||
if let birthdayPrivacy = data.globalSettings?.privacySettings?.birthday {
|
||||
switch birthdayPrivacy {
|
||||
case .enableEveryone:
|
||||
birthdayPrivacyInfo = presentationData.strings.Settings_Birthday_PrivacyHelpEveryone
|
||||
case .enableContacts:
|
||||
birthdayPrivacyInfo = presentationData.strings.Settings_Birthday_PrivacyHelpContacts
|
||||
case .disableEveryone:
|
||||
birthdayPrivacyInfo = presentationData.strings.Settings_Birthday_PrivacyHelpNobody
|
||||
}
|
||||
}
|
||||
if !birthdayPrivacyInfo.isEmpty {
|
||||
items[.birthday]!.append(PeerInfoScreenCommentItem(id: ItemBirthdayHelp, text: birthdayPrivacyInfo, linkAction: { _ in
|
||||
interaction.openBirthdatePrivacy()
|
||||
}))
|
||||
}
|
||||
items[.birthday]!.append(PeerInfoScreenCommentItem(id: ItemBirthdayHelp, text: birthdayIsForContactsOnly ? presentationData.strings.Settings_Birthday_ContactsHelp : presentationData.strings.Settings_Birthday_Help, linkAction: { _ in
|
||||
interaction.openBirthdatePrivacy()
|
||||
}))
|
||||
|
||||
if let user = data.peer as? TelegramUser {
|
||||
items[.info]!.append(PeerInfoScreenDisclosureItem(id: ItemPhoneNumber, label: .text(user.phone.flatMap({ formatPhoneNumber(context: context, number: $0) }) ?? ""), text: presentationData.strings.Settings_PhoneNumber, icon: PresentationResourcesSettings.recentCalls, action: {
|
||||
|
|
|
|||
12
submodules/TelegramUI/Images.xcassets/Chat List/CallIncomingIcon.imageset/Contents.json
vendored
Normal file
12
submodules/TelegramUI/Images.xcassets/Chat List/CallIncomingIcon.imageset/Contents.json
vendored
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
{
|
||||
"images" : [
|
||||
{
|
||||
"filename" : "incoming_call_20.pdf",
|
||||
"idiom" : "universal"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
BIN
submodules/TelegramUI/Images.xcassets/Chat List/CallIncomingIcon.imageset/incoming_call_20.pdf
vendored
Normal file
BIN
submodules/TelegramUI/Images.xcassets/Chat List/CallIncomingIcon.imageset/incoming_call_20.pdf
vendored
Normal file
Binary file not shown.
12
submodules/TelegramUI/Images.xcassets/Chat List/CallOutgoingIcon.imageset/Contents.json
vendored
Normal file
12
submodules/TelegramUI/Images.xcassets/Chat List/CallOutgoingIcon.imageset/Contents.json
vendored
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
{
|
||||
"images" : [
|
||||
{
|
||||
"filename" : "outgoing_call_20.pdf",
|
||||
"idiom" : "universal"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
BIN
submodules/TelegramUI/Images.xcassets/Chat List/CallOutgoingIcon.imageset/outgoing_call_20.pdf
vendored
Normal file
BIN
submodules/TelegramUI/Images.xcassets/Chat List/CallOutgoingIcon.imageset/outgoing_call_20.pdf
vendored
Normal file
Binary file not shown.
12
submodules/TelegramUI/Images.xcassets/Chat List/CallVideoIncomingIcon.imageset/Contents.json
vendored
Normal file
12
submodules/TelegramUI/Images.xcassets/Chat List/CallVideoIncomingIcon.imageset/Contents.json
vendored
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
{
|
||||
"images" : [
|
||||
{
|
||||
"filename" : "incoming_video_call_20.pdf",
|
||||
"idiom" : "universal"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
Binary file not shown.
12
submodules/TelegramUI/Images.xcassets/Chat List/CallVideoOutgoingIcon.imageset/Contents.json
vendored
Normal file
12
submodules/TelegramUI/Images.xcassets/Chat List/CallVideoOutgoingIcon.imageset/Contents.json
vendored
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
{
|
||||
"images" : [
|
||||
{
|
||||
"filename" : "outgoing_video_call_20.pdf",
|
||||
"idiom" : "universal"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
Binary file not shown.
12
submodules/TelegramUI/Images.xcassets/Chat List/GameIcon.imageset/Contents.json
vendored
Normal file
12
submodules/TelegramUI/Images.xcassets/Chat List/GameIcon.imageset/Contents.json
vendored
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
{
|
||||
"images" : [
|
||||
{
|
||||
"filename" : "gamepad_20.pdf",
|
||||
"idiom" : "universal"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
BIN
submodules/TelegramUI/Images.xcassets/Chat List/GameIcon.imageset/gamepad_20.pdf
vendored
Normal file
BIN
submodules/TelegramUI/Images.xcassets/Chat List/GameIcon.imageset/gamepad_20.pdf
vendored
Normal file
Binary file not shown.
12
submodules/TelegramUI/Images.xcassets/Chat List/TodoIcon.imageset/Contents.json
vendored
Normal file
12
submodules/TelegramUI/Images.xcassets/Chat List/TodoIcon.imageset/Contents.json
vendored
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
{
|
||||
"images" : [
|
||||
{
|
||||
"filename" : "checklist_20.pdf",
|
||||
"idiom" : "universal"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
BIN
submodules/TelegramUI/Images.xcassets/Chat List/TodoIcon.imageset/checklist_20.pdf
vendored
Normal file
BIN
submodules/TelegramUI/Images.xcassets/Chat List/TodoIcon.imageset/checklist_20.pdf
vendored
Normal file
Binary file not shown.
BIN
submodules/TelegramUI/Resources/Animations/NavigationMuteOff.tgs
Normal file
BIN
submodules/TelegramUI/Resources/Animations/NavigationMuteOff.tgs
Normal file
Binary file not shown.
BIN
submodules/TelegramUI/Resources/Animations/NavigationMuteOn.tgs
Normal file
BIN
submodules/TelegramUI/Resources/Animations/NavigationMuteOn.tgs
Normal file
Binary file not shown.
|
|
@ -45,6 +45,7 @@ import RecaptchaEnterprise
|
|||
import NavigationBarImpl
|
||||
import ContextUI
|
||||
import ContextControllerImpl
|
||||
import AutomationBridge
|
||||
|
||||
#if canImport(AppCenter)
|
||||
import AppCenter
|
||||
|
|
@ -1688,6 +1689,13 @@ private func extractAccountManagerState(records: AccountRecordsView<TelegramAcco
|
|||
}
|
||||
})
|
||||
|
||||
#if DEBUG
|
||||
AutomationRuntime.shared.setHierarchyProvider {
|
||||
AutomationPassiveHierarchyBuilder.build()
|
||||
}
|
||||
AutomationRuntime.shared.startIfNeeded(stateProvider: nil)
|
||||
#endif
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -10270,7 +10270,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
|
|||
if peerId == strongSelf.context.account.peerId {
|
||||
mode = .reminders
|
||||
} else {
|
||||
mode = .scheduledMessages(sendWhenOnlineAvailable: sendWhenOnlineAvailable)
|
||||
mode = .scheduledMessages(peerId: peer.id, sendWhenOnlineAvailable: sendWhenOnlineAvailable)
|
||||
}
|
||||
|
||||
let controller = ChatScheduleTimeScreen(
|
||||
|
|
|
|||
|
|
@ -238,7 +238,7 @@ extension ChatControllerImpl {
|
|||
}))
|
||||
}
|
||||
|
||||
func presentBanMessageOptions(accountPeerId: PeerId, author: Peer, messageIds: Set<MessageId>, options: ChatAvailableMessageActionOptions) {
|
||||
func presentBanMessageOptions(accountPeerId: PeerId, author: Peer, messageIds: Set<MessageId>, options: ChatAvailableMessageActionOptions, reaction: Bool = false) {
|
||||
guard let peerId = self.chatLocation.peerId else {
|
||||
return
|
||||
}
|
||||
|
|
@ -325,14 +325,17 @@ extension ChatControllerImpl {
|
|||
initialUserBannedRights[participant.peerId] = InitialBannedRights(value: nil)
|
||||
}
|
||||
}
|
||||
self.push(AdminUserActionsSheet(
|
||||
context: self.context,
|
||||
chatPeer: chatPeer,
|
||||
peers: [RenderedChannelParticipant(
|
||||
participant: participant,
|
||||
peer: authorPeer._asPeer()
|
||||
)],
|
||||
mode: .chat(
|
||||
|
||||
let mode: AdminUserActionsSheet.Mode
|
||||
if reaction {
|
||||
mode = .chatReaction(completion: { [weak self] result in
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
self.applyAdminUserActionsResult(messageIds: messageIds, result: result, initialUserBannedRights: initialUserBannedRights)
|
||||
})
|
||||
} else {
|
||||
mode = .chat(
|
||||
messageCount: messageIds.count,
|
||||
deleteAllMessageCount: deleteAllMessageCount,
|
||||
completion: { [weak self] result in
|
||||
|
|
@ -342,6 +345,16 @@ extension ChatControllerImpl {
|
|||
self.applyAdminUserActionsResult(messageIds: messageIds, result: result, initialUserBannedRights: initialUserBannedRights)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
self.push(AdminUserActionsSheet(
|
||||
context: self.context,
|
||||
chatPeer: chatPeer,
|
||||
peers: [RenderedChannelParticipant(
|
||||
participant: participant,
|
||||
peer: authorPeer._asPeer()
|
||||
)],
|
||||
mode: mode
|
||||
))
|
||||
})
|
||||
}))
|
||||
|
|
|
|||
|
|
@ -2232,7 +2232,7 @@ class ChatControllerNode: ASDisplayNode, ASScrollViewDelegate {
|
|||
additionalOffset = 80.0
|
||||
}
|
||||
if let _ = inputPanelSize {
|
||||
inputPanelHideOffset += -48.0 - additionalOffset
|
||||
inputPanelHideOffset += -56.0 - additionalOffset
|
||||
}
|
||||
if let accessoryPanelSize = accessoryPanelSize {
|
||||
inputPanelHideOffset += -accessoryPanelSize.height - additionalOffset
|
||||
|
|
|
|||
|
|
@ -198,7 +198,10 @@ extension ChatControllerImpl {
|
|||
animationCache: self.controllerInteraction!.presentationContext.animationCache,
|
||||
animationRenderer: self.controllerInteraction!.presentationContext.animationRenderer,
|
||||
message: EngineMessage(message),
|
||||
reaction: value, readStats: nil, back: nil, openPeer: { peer, hasReaction in
|
||||
reaction: value,
|
||||
readStats: nil,
|
||||
back: nil,
|
||||
openPeer: { peer, hasReaction in
|
||||
dismissController?({ [weak self] in
|
||||
guard let self else {
|
||||
return
|
||||
|
|
@ -206,6 +209,31 @@ extension ChatControllerImpl {
|
|||
|
||||
self.openPeer(peer: peer, navigation: .default, fromMessage: MessageReference(message), fromReactionMessageId: hasReaction ? message.id : nil)
|
||||
})
|
||||
},
|
||||
deleteReaction: { [weak self] peer, _ in
|
||||
dismissController?({ [weak self] in
|
||||
guard let self, self.chatLocation.peerId?.namespace == Namespaces.Peer.CloudChannel else {
|
||||
return
|
||||
}
|
||||
let _ = (self.context.sharedContext.chatAvailableMessageActions(
|
||||
engine: self.context.engine,
|
||||
accountPeerId: self.context.account.peerId,
|
||||
messageIds: Set([message.id]),
|
||||
keepUpdated: false
|
||||
)
|
||||
|> deliverOnMainQueue).startStandalone(next: { [weak self] actions in
|
||||
guard let self, !actions.options.isEmpty else {
|
||||
return
|
||||
}
|
||||
self.presentBanMessageOptions(
|
||||
accountPeerId: self.context.account.peerId,
|
||||
author: peer._asPeer(),
|
||||
messageIds: Set([message.id]),
|
||||
options: actions.options,
|
||||
reaction: true
|
||||
)
|
||||
})
|
||||
})
|
||||
}
|
||||
)))
|
||||
} else {
|
||||
|
|
@ -297,7 +325,9 @@ extension ChatControllerImpl {
|
|||
let presentationContext = self.controllerInteraction?.presentationContext
|
||||
let premiumConfiguration = PremiumConfiguration.with(appConfiguration: context.currentAppConfiguration.with { $0 })
|
||||
|
||||
if !packReferences.isEmpty && !premiumConfiguration.isPremiumDisabled {
|
||||
if "".isEmpty {
|
||||
items.tip = .deleteReaction
|
||||
} else if !packReferences.isEmpty && !premiumConfiguration.isPremiumDisabled {
|
||||
items.tip = .animatedEmoji(text: nil, arguments: nil, file: nil, action: nil)
|
||||
|
||||
if packReferences.count > 1 {
|
||||
|
|
|
|||
|
|
@ -92,6 +92,59 @@ func chatShareToSavedMessagesAdditionalView(_ chatController: ChatControllerImpl
|
|||
}
|
||||
|
||||
extension ChatControllerImpl {
|
||||
// func openMessageShareMenu(id: EngineMessage.Id) {
|
||||
// guard let messages = self.chatDisplayNode.historyNode.messageGroupInCurrentHistoryView(id), let message = messages.first else {
|
||||
// return
|
||||
// }
|
||||
//
|
||||
// let chatPresentationInterfaceState = self.presentationInterfaceState
|
||||
// var warnAboutPrivate = false
|
||||
// var canShareToStory = false
|
||||
// if case .peer = chatPresentationInterfaceState.chatLocation, let channel = message.peers[message.id.peerId] as? TelegramChannel {
|
||||
// if case .broadcast = channel.info {
|
||||
// canShareToStory = true
|
||||
// if let message = messages.first, message.media.contains(where: { media in
|
||||
// if media is TelegramMediaContact || media is TelegramMediaPoll || media is TelegramMediaTodo {
|
||||
// return true
|
||||
// } else if let file = media as? TelegramMediaFile, file.isSticker || file.isAnimatedSticker || file.isVideoSticker {
|
||||
// return true
|
||||
// } else {
|
||||
// return false
|
||||
// }
|
||||
// }) {
|
||||
// canShareToStory = false
|
||||
// }
|
||||
// if message.text.containsOnlyEmoji {
|
||||
// canShareToStory = false
|
||||
// }
|
||||
// }
|
||||
// if channel.addressName == nil {
|
||||
// warnAboutPrivate = true
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// let _ = warnAboutPrivate
|
||||
//
|
||||
//// let shareScreen = self.context.sharedContext.makeShareController(
|
||||
//// context: self.context,
|
||||
//// subject: .messages(messages),
|
||||
//// forceExternal: false,
|
||||
//// shareStory: canShareToStory ? { [weak self] in
|
||||
//// guard let self else {
|
||||
//// return
|
||||
//// }
|
||||
//// Queue.mainQueue().after(0.15) {
|
||||
//// let controller = self.context.sharedContext.makeStorySharingScreen(context: self.context, subject: .messages(messages), parentController: self)
|
||||
//// self.push(controller)
|
||||
//// }
|
||||
//// } : nil,
|
||||
//// enqueued: nil,
|
||||
//// actionCompleted: nil
|
||||
//// )
|
||||
// self.chatDisplayNode.dismissInput()
|
||||
// self.push(shareScreen)
|
||||
// }
|
||||
|
||||
func openMessageShareMenu(id: EngineMessage.Id) {
|
||||
guard let messages = self.chatDisplayNode.historyNode.messageGroupInCurrentHistoryView(id), let message = messages.first else {
|
||||
return
|
||||
|
|
|
|||
|
|
@ -2189,6 +2189,30 @@ func contextMenuForChatPresentationInterfaceState(chatPresentationInterfaceState
|
|||
c?.dismiss(completion: {
|
||||
controllerInteraction.openPeer(peer, .default, MessageReference(message), hasReaction ? .reaction : .default)
|
||||
})
|
||||
},
|
||||
deleteReaction: { [weak c] peer, _ in
|
||||
c?.dismiss(completion: {
|
||||
guard let chatController = interfaceInteraction.chatController() as? ChatControllerImpl, chatController.chatLocation.peerId?.namespace == Namespaces.Peer.CloudChannel else {
|
||||
return
|
||||
}
|
||||
let _ = (context.sharedContext.chatAvailableMessageActions(
|
||||
engine: context.engine,
|
||||
accountPeerId: context.account.peerId,
|
||||
messageIds: Set([message.id]),
|
||||
keepUpdated: false
|
||||
)
|
||||
|> deliverOnMainQueue).startStandalone(next: { actions in
|
||||
guard !actions.options.isEmpty else {
|
||||
return
|
||||
}
|
||||
chatController.presentBanMessageOptions(
|
||||
accountPeerId: context.account.peerId,
|
||||
author: peer._asPeer(),
|
||||
messageIds: Set([message.id]),
|
||||
options: actions.options
|
||||
)
|
||||
})
|
||||
})
|
||||
}
|
||||
)), tip: tip)))
|
||||
} else {
|
||||
|
|
|
|||
|
|
@ -256,11 +256,14 @@ func openChatMessageImpl(_ params: OpenChatMessageParams) -> Bool {
|
|||
}
|
||||
}
|
||||
|
||||
let fileReference: FileMediaReference = .message(message: MessageReference(params.message), media: file)
|
||||
let subject: BrowserScreen.Subject
|
||||
if file.mimeType == "application/pdf" {
|
||||
subject = .pdfDocument(file: .message(message: MessageReference(params.message), media: file), canShare: canShare)
|
||||
if file.mimeType.contains("markdown") {
|
||||
subject = .markdownDocument(file: fileReference, canShare: canShare)
|
||||
} else if file.mimeType.contains("pdf") {
|
||||
subject = .pdfDocument(file: fileReference, canShare: canShare)
|
||||
} else {
|
||||
subject = .document(file: .message(message: MessageReference(params.message), media: file), canShare: canShare)
|
||||
subject = .document(file: fileReference, canShare: canShare)
|
||||
}
|
||||
let controller = BrowserScreen(context: params.context, subject: subject)
|
||||
controller.openDocument = { [weak controller] file, canShare in
|
||||
|
|
|
|||
|
|
@ -696,7 +696,7 @@ func openResolvedUrlImpl(
|
|||
present(shareController, nil)
|
||||
context.sharedContext.applicationBindings.dismissNativeController()
|
||||
} else {
|
||||
let controller = context.sharedContext.makePeerSelectionController(PeerSelectionControllerParams(context: context, filter: [.onlyWriteable, .excludeDisabled], selectForumThreads: true))
|
||||
let controller = context.sharedContext.makePeerSelectionController(PeerSelectionControllerParams(context: context, filter: [.onlyWriteable, .excludeDisabled], hasFilters: true, selectForumThreads: true))
|
||||
controller.peerSelected = { peer, threadId in
|
||||
continueWithPeer(peer.id, threadId)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -109,7 +109,7 @@ import EntityKeyboard
|
|||
private final class AccountUserInterfaceInUseContext {
|
||||
let subscribers = Bag<(Bool) -> Void>()
|
||||
let tokens = Bag<Void>()
|
||||
|
||||
|
||||
var isEmpty: Bool {
|
||||
return self.tokens.isEmpty && self.subscribers.isEmpty
|
||||
}
|
||||
|
|
@ -295,7 +295,7 @@ public final class SharedAccountContextImpl: SharedAccountContext {
|
|||
|
||||
init(mainWindow: Window1?, sharedContainerPath: String, basePath: String, encryptionParameters: ValueBoxEncryptionParameters, accountManager: AccountManager<TelegramAccountManagerTypes>, appLockContext: AppLockContext, notificationController: NotificationContainerController?, applicationBindings: TelegramApplicationBindings, initialPresentationDataAndSettings: InitialPresentationDataAndSettings, networkArguments: NetworkInitializationArguments, hasInAppPurchases: Bool, rootPath: String, legacyBasePath: String?, apsNotificationToken: Signal<Data?, NoError>, voipNotificationToken: Signal<Data?, NoError>, firebaseSecretStream: Signal<[String: String], NoError>, setNotificationCall: @escaping (PresentationCall?) -> Void, navigateToChat: @escaping (AccountRecordId, PeerId, MessageId?, Bool) -> Void, displayUpgradeProgress: @escaping (Float?) -> Void = { _ in }, appDelegate: AppDelegate?, testingEnvironment: Bool = false) {
|
||||
assert(Queue.mainQueue().isCurrent())
|
||||
|
||||
|
||||
precondition(!testHasInstance)
|
||||
testHasInstance = true
|
||||
|
||||
|
|
|
|||
|
|
@ -83,6 +83,50 @@ public func chatInputStateStringWithAppliedEntities(_ text: String, entities: [M
|
|||
|
||||
private let syntaxHighlighter = Syntaxer()
|
||||
|
||||
private func isValidMessageSyntaxHighlightRange(_ range: Range<Int>, expectedLength: Int) -> Bool {
|
||||
return range.lowerBound >= 0 && range.upperBound <= expectedLength && range.lowerBound < range.upperBound
|
||||
}
|
||||
|
||||
private func validatedCachedMessageSyntaxHighlight(_ highlight: MessageSyntaxHighlight, expectedLength: Int, language: String) -> MessageSyntaxHighlight? {
|
||||
for entity in highlight.entities {
|
||||
if !isValidMessageSyntaxHighlightRange(entity.range, expectedLength: expectedLength) {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
return highlight
|
||||
}
|
||||
|
||||
private func generateMessageSyntaxHighlight(spec: CachedMessageSyntaxHighlight.Spec, theme: SyntaxterTheme) -> MessageSyntaxHighlight {
|
||||
let expectedLength = (spec.text as NSString).length
|
||||
guard let syntaxHighlighter else {
|
||||
return MessageSyntaxHighlight(entities: [])
|
||||
}
|
||||
guard let highlightedString = syntaxHighlighter.syntax(spec.text, language: spec.language, theme: theme) else {
|
||||
return MessageSyntaxHighlight(entities: [])
|
||||
}
|
||||
guard highlightedString.length == expectedLength else {
|
||||
return MessageSyntaxHighlight(entities: [])
|
||||
}
|
||||
|
||||
var entities: [MessageSyntaxHighlight.Entity] = []
|
||||
var hasInvalidRange = false
|
||||
highlightedString.enumerateAttribute(.foregroundColor, in: NSRange(location: 0, length: highlightedString.length), using: { value, subRange, stop in
|
||||
let range = subRange.lowerBound ..< subRange.upperBound
|
||||
if !isValidMessageSyntaxHighlightRange(range, expectedLength: expectedLength) {
|
||||
hasInvalidRange = true
|
||||
stop.pointee = true
|
||||
return
|
||||
}
|
||||
if let value = value as? UIColor, value != .black {
|
||||
entities.append(MessageSyntaxHighlight.Entity(color: Int32(bitPattern: value.rgb), range: range))
|
||||
}
|
||||
})
|
||||
if hasInvalidRange {
|
||||
return MessageSyntaxHighlight(entities: [])
|
||||
}
|
||||
return MessageSyntaxHighlight(entities: entities)
|
||||
}
|
||||
|
||||
public func stringWithAppliedEntities(_ text: String, entities: [MessageTextEntity], strings: PresentationStrings? = nil, dateTimeFormat: PresentationDateTimeFormat? = nil, baseColor: UIColor, linkColor: UIColor, baseQuoteTintColor: UIColor? = nil, baseQuoteSecondaryTintColor: UIColor? = nil, baseQuoteTertiaryTintColor: UIColor? = nil, codeBlockTitleColor: UIColor? = nil, codeBlockAccentColor: UIColor? = nil, codeBlockBackgroundColor: UIColor? = nil, baseFont: UIFont, linkFont: UIFont, boldFont: UIFont, italicFont: UIFont, boldItalicFont: UIFont, fixedFont: UIFont, blockQuoteFont: UIFont, underlineLinks: Bool = true, external: Bool = false, message: Message?, entityFiles: [MediaId: TelegramMediaFile] = [:], adjustQuoteFontSize: Bool = false, cachedMessageSyntaxHighlight: CachedMessageSyntaxHighlight? = nil, paragraphAlignment: NSTextAlignment? = nil) -> NSAttributedString {
|
||||
let baseQuoteTintColor = baseQuoteTintColor ?? baseColor
|
||||
|
||||
|
|
@ -399,8 +443,10 @@ public func stringWithAppliedEntities(_ text: String, entities: [MessageTextEnti
|
|||
|
||||
let codeText = (string.string as NSString).substring(with: range)
|
||||
if let cachedMessageSyntaxHighlight, let entry = cachedMessageSyntaxHighlight.values[CachedMessageSyntaxHighlight.Spec(language: language, text: codeText)] {
|
||||
for entity in entry.entities {
|
||||
string.addAttribute(.foregroundColor, value: UIColor(rgb: UInt32(bitPattern: entity.color)), range: NSRange(location: range.location + entity.range.lowerBound, length: entity.range.upperBound - entity.range.lowerBound))
|
||||
if let validatedEntry = validatedCachedMessageSyntaxHighlight(entry, expectedLength: range.length, language: language) {
|
||||
for entity in validatedEntry.entities {
|
||||
string.addAttribute(.foregroundColor, value: UIColor(rgb: UInt32(bitPattern: entity.color)), range: NSRange(location: range.location + entity.range.lowerBound, length: entity.range.upperBound - entity.range.lowerBound))
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
|
@ -588,8 +634,18 @@ public func extractMessageSyntaxHighlightSpecs(text: String, entities: [MessageT
|
|||
private let internalFixedCodeFont = Font.regular(17.0)
|
||||
|
||||
public func asyncUpdateMessageSyntaxHighlight(engine: TelegramEngine, messageId: EngineMessage.Id, current: CachedMessageSyntaxHighlight?, specs: [CachedMessageSyntaxHighlight.Spec]) -> Signal<Never, NoError> {
|
||||
if let current, !specs.contains(where: { current.values[$0] == nil }) {
|
||||
return .complete()
|
||||
if let current {
|
||||
var hasMissingOrInvalidSpec = false
|
||||
for spec in specs {
|
||||
let expectedLength = (spec.text as NSString).length
|
||||
guard let value = current.values[spec], validatedCachedMessageSyntaxHighlight(value, expectedLength: expectedLength, language: spec.language) != nil else {
|
||||
hasMissingOrInvalidSpec = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !hasMissingOrInvalidSpec {
|
||||
return .complete()
|
||||
}
|
||||
}
|
||||
|
||||
return Signal { subscriber in
|
||||
|
|
@ -598,22 +654,11 @@ public func asyncUpdateMessageSyntaxHighlight(engine: TelegramEngine, messageId:
|
|||
let theme = SyntaxterTheme(dark: false, textColor: .black, textFont: internalFixedCodeFont, italicFont: internalFixedCodeFont, mediumFont: internalFixedCodeFont)
|
||||
|
||||
for spec in specs {
|
||||
let expectedLength = (spec.text as NSString).length
|
||||
if let value = current?.values[spec] {
|
||||
updated[spec] = value
|
||||
} else {
|
||||
var entities: [MessageSyntaxHighlight.Entity] = []
|
||||
|
||||
if let syntaxHighlighter {
|
||||
if let highlightedString = syntaxHighlighter.syntax(spec.text, language: spec.language, theme: theme) {
|
||||
highlightedString.enumerateAttribute(.foregroundColor, in: NSRange(location: 0, length: highlightedString.length), using: { value, subRange, _ in
|
||||
if let value = value as? UIColor, value != .black {
|
||||
entities.append(MessageSyntaxHighlight.Entity(color: Int32(bitPattern: value.rgb), range: subRange.lowerBound ..< subRange.upperBound))
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
updated[spec] = MessageSyntaxHighlight(entities: entities)
|
||||
updated[spec] = validatedCachedMessageSyntaxHighlight(value, expectedLength: expectedLength, language: spec.language) ?? MessageSyntaxHighlight(entities: [])
|
||||
} else if let theme {
|
||||
updated[spec] = generateMessageSyntaxHighlight(spec: spec, theme: theme)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -629,8 +674,18 @@ public func asyncUpdateMessageSyntaxHighlight(engine: TelegramEngine, messageId:
|
|||
}
|
||||
|
||||
public func asyncStanaloneSyntaxHighlight(current: CachedMessageSyntaxHighlight?, specs: [CachedMessageSyntaxHighlight.Spec]) -> Signal<CachedMessageSyntaxHighlight, NoError> {
|
||||
if let current, !specs.contains(where: { current.values[$0] == nil }) {
|
||||
return .single(current)
|
||||
if let current {
|
||||
var hasMissingOrInvalidSpec = false
|
||||
for spec in specs {
|
||||
let expectedLength = (spec.text as NSString).length
|
||||
guard let value = current.values[spec], validatedCachedMessageSyntaxHighlight(value, expectedLength: expectedLength, language: spec.language) != nil else {
|
||||
hasMissingOrInvalidSpec = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !hasMissingOrInvalidSpec {
|
||||
return .single(current)
|
||||
}
|
||||
}
|
||||
|
||||
return Signal { subscriber in
|
||||
|
|
@ -639,22 +694,11 @@ public func asyncStanaloneSyntaxHighlight(current: CachedMessageSyntaxHighlight?
|
|||
let theme = SyntaxterTheme(dark: false, textColor: .black, textFont: internalFixedCodeFont, italicFont: internalFixedCodeFont, mediumFont: internalFixedCodeFont)
|
||||
|
||||
for spec in specs {
|
||||
let expectedLength = (spec.text as NSString).length
|
||||
if let value = current?.values[spec] {
|
||||
updated[spec] = value
|
||||
} else {
|
||||
var entities: [MessageSyntaxHighlight.Entity] = []
|
||||
|
||||
if let syntaxHighlighter {
|
||||
if let highlightedString = syntaxHighlighter.syntax(spec.text, language: spec.language, theme: theme) {
|
||||
highlightedString.enumerateAttribute(.foregroundColor, in: NSRange(location: 0, length: highlightedString.length), using: { value, subRange, _ in
|
||||
if let value = value as? UIColor, value != .black {
|
||||
entities.append(MessageSyntaxHighlight.Entity(color: Int32(bitPattern: value.rgb), range: subRange.lowerBound ..< subRange.upperBound))
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
updated[spec] = MessageSyntaxHighlight(entities: entities)
|
||||
updated[spec] = validatedCachedMessageSyntaxHighlight(value, expectedLength: expectedLength, language: spec.language) ?? MessageSyntaxHighlight(entities: [])
|
||||
} else if let theme {
|
||||
updated[spec] = generateMessageSyntaxHighlight(spec: spec, theme: theme)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue