Various improvements

This commit is contained in:
Ilya Laktyushin 2026-04-22 23:03:51 +02:00
parent 1dd23f6641
commit dbd40fe7d3
76 changed files with 5186 additions and 2625 deletions

View file

@ -16202,5 +16202,9 @@ Error: %8$@";
"Settings.About.PrivacyHelpEmpty" = "A few words about you."; "Settings.About.PrivacyHelpEmpty" = "A few words about you.";
"Settings.About.PrivacyHelpEveryone" = "Everyone can see your bio. [Change >]()"; "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.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 >]()";

View file

@ -65,6 +65,8 @@ final class BrowserInstantPageContent: UIView, BrowserContent, UIScrollViewDeleg
var currentWebEmbedHeights: [Int : CGFloat] = [:] var currentWebEmbedHeights: [Int : CGFloat] = [:]
var currentExpandedDetails: [Int : Bool]? var currentExpandedDetails: [Int : Bool]?
var currentDetailsItems: [InstantPageDetailsItem] = [] var currentDetailsItems: [InstantPageDetailsItem] = []
private var resolvedExternalMediaDimensions: [MediaId: PixelDimensions] = [:]
private var pendingResolvedExternalMediaDimensions = Set<MediaId>()
var currentAccessibilityAreas: [AccessibilityAreaNode] = [] var currentAccessibilityAreas: [AccessibilityAreaNode] = []
@ -87,6 +89,7 @@ final class BrowserInstantPageContent: UIView, BrowserContent, UIScrollViewDeleg
private let loadWebpageDisposable = MetaDisposable() private let loadWebpageDisposable = MetaDisposable()
private let resolveUrlDisposable = MetaDisposable() private let resolveUrlDisposable = MetaDisposable()
private let updateLayoutDisposable = MetaDisposable() private let updateLayoutDisposable = MetaDisposable()
private let updateExternalMediaDimensionsDisposable = MetaDisposable()
private let loadProgress = ValuePromise<CGFloat>(1.0, ignoreRepeated: true) private let loadProgress = ValuePromise<CGFloat>(1.0, ignoreRepeated: true)
private let readingProgress = ValuePromise<CGFloat>(0.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.scrollNode.view.addGestureRecognizer(recognizer)
self.webpageDisposable = (actualizedWebpage(account: context.account, webpage: webPage) |> deliverOnMainQueue).start(next: { [weak self] result in if case let .Loaded(content) = webPage.content, let scheme = URL(string: content.url)?.scheme?.lowercased(), scheme == "http" || scheme == "https" {
guard let self else { self.webpageDisposable = (actualizedWebpage(account: context.account, webpage: webPage) |> deliverOnMainQueue).start(next: { [weak self] result in
return guard let self else {
} return
self.updateWebPage(result, anchor: self.initialAnchor) }
}) self.updateWebPage(result, anchor: self.initialAnchor)
})
}
} }
deinit { deinit {
@ -193,6 +198,7 @@ final class BrowserInstantPageContent: UIView, BrowserContent, UIScrollViewDeleg
self.loadWebpageDisposable.dispose() self.loadWebpageDisposable.dispose()
self.resolveUrlDisposable.dispose() self.resolveUrlDisposable.dispose()
self.updateLayoutDisposable.dispose() self.updateLayoutDisposable.dispose()
self.updateExternalMediaDimensionsDisposable.dispose()
} }
required init?(coder: NSCoder) { required init?(coder: NSCoder) {
@ -304,6 +310,8 @@ final class BrowserInstantPageContent: UIView, BrowserContent, UIScrollViewDeleg
} else { } else {
self.webPage = nil self.webPage = nil
} }
self.resolvedExternalMediaDimensions.removeAll()
self.pendingResolvedExternalMediaDimensions.removeAll()
if let anchor = anchor { if let anchor = anchor {
self.initialAnchor = anchor.removingPercentEncoding self.initialAnchor = anchor.removingPercentEncoding
} else if let state = state { } else if let state = state {
@ -478,17 +486,12 @@ final class BrowserInstantPageContent: UIView, BrowserContent, UIScrollViewDeleg
} }
private func updatePageLayout() { 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 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) 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) let currentLayoutTiles = instantPageTilesFromLayout(currentLayout, boundingWidth: size.width)
var currentDetailsItems: [InstantPageDetailsItem] = [] var currentDetailsItems: [InstantPageDetailsItem] = []
@ -655,6 +658,7 @@ final class BrowserInstantPageContent: UIView, BrowserContent, UIScrollViewDeleg
topNode = newNode topNode = newNode
self.visibleItemsWithNodes[itemIndex] = newNode self.visibleItemsWithNodes[itemIndex] = newNode
itemNode = newNode itemNode = newNode
self.configureExternalMediaDimensionsUpdates(for: newNode)
if let itemNode = itemNode as? InstantPageDetailsNode { if let itemNode = itemNode as? InstantPageDetailsNode {
itemNode.requestLayoutUpdate = { [weak self] animated in 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 { if let itemNode = itemNode as? InstantPageDetailsNode {
itemNode.updateVisibleItems(visibleBounds: visibleBounds.offsetBy(dx: -itemNode.frame.minX, dy: -itemNode.frame.minY), animated: animated) 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 topNode = tileNode
self.visibleTiles[tileIndex] = tileNode self.visibleTiles[tileIndex] = tileNode
} else { } else {
if visibleTiles[tileIndex]!.frame != tileFrame { if let tileNode = self.visibleTiles[tileIndex] {
transition.updateFrame(node: self.visibleTiles[tileIndex]!, frame: tileFrame) 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 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) { private func openUrl(_ url: InstantPageUrlItem) {
var baseUrl = url.url var baseUrl = url.url
var anchor: String? var anchor: String?

File diff suppressed because it is too large Load diff

View file

@ -364,7 +364,7 @@ final class BrowserPdfContent: UIView, BrowserContent, UIScrollViewDelegate, PDF
let pageIndicatorSize = self.pageIndicator.update( let pageIndicatorSize = self.pageIndicator.update(
transition: .immediate, transition: .immediate,
component: AnyComponent( 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: {}, environment: {},
containerSize: size containerSize: size

View file

@ -29,6 +29,7 @@ private final class BrowserScreenComponent: CombinedComponent {
let context: AccountContext let context: AccountContext
let contentState: BrowserContentState? let contentState: BrowserContentState?
let presentationState: BrowserPresentationState let presentationState: BrowserPresentationState
let toolbarMode: BrowserToolbarMode
let canShare: Bool let canShare: Bool
let performAction: ActionSlot<BrowserScreen.Action> let performAction: ActionSlot<BrowserScreen.Action>
let performHoldAction: (UIView, ContextGesture?, BrowserScreen.Action) -> Void let performHoldAction: (UIView, ContextGesture?, BrowserScreen.Action) -> Void
@ -38,6 +39,7 @@ private final class BrowserScreenComponent: CombinedComponent {
context: AccountContext, context: AccountContext,
contentState: BrowserContentState?, contentState: BrowserContentState?,
presentationState: BrowserPresentationState, presentationState: BrowserPresentationState,
toolbarMode: BrowserToolbarMode,
canShare: Bool, canShare: Bool,
performAction: ActionSlot<BrowserScreen.Action>, performAction: ActionSlot<BrowserScreen.Action>,
performHoldAction: @escaping (UIView, ContextGesture?, BrowserScreen.Action) -> Void, performHoldAction: @escaping (UIView, ContextGesture?, BrowserScreen.Action) -> Void,
@ -46,6 +48,7 @@ private final class BrowserScreenComponent: CombinedComponent {
self.context = context self.context = context
self.contentState = contentState self.contentState = contentState
self.presentationState = presentationState self.presentationState = presentationState
self.toolbarMode = toolbarMode
self.canShare = canShare self.canShare = canShare
self.performAction = performAction self.performAction = performAction
self.performHoldAction = performHoldAction self.performHoldAction = performHoldAction
@ -62,6 +65,9 @@ private final class BrowserScreenComponent: CombinedComponent {
if lhs.presentationState != rhs.presentationState { if lhs.presentationState != rhs.presentationState {
return false return false
} }
if lhs.toolbarMode != rhs.toolbarMode {
return false
}
if lhs.canShare != rhs.canShare { if lhs.canShare != rhs.canShare {
return false return false
} }
@ -112,8 +118,7 @@ private final class BrowserScreenComponent: CombinedComponent {
navigationLeftItems = [] navigationLeftItems = []
navigationRightItems = [] navigationRightItems = []
} else { } else {
let contentType = context.component.contentState?.contentType ?? .instantPage switch context.component.toolbarMode {
switch contentType {
case .webPage: case .webPage:
navigationContent = AnyComponentWithIdentity( navigationContent = AnyComponentWithIdentity(
id: "addressBar", id: "addressBar",
@ -131,7 +136,7 @@ private final class BrowserScreenComponent: CombinedComponent {
) )
) )
) )
case .instantPage, .document: case .instantPage, .document, .markdown:
let title = context.component.contentState?.title ?? "" let title = context.component.contentState?.title ?? ""
navigationContent = AnyComponentWithIdentity( navigationContent = AnyComponentWithIdentity(
id: "titleBar_\(title)", id: "titleBar_\(title)",
@ -190,47 +195,46 @@ private final class BrowserScreenComponent: CombinedComponent {
// ) // )
// ) // )
// #endif // #endif
if context.component.toolbarMode != .markdown {
let canGoBack = context.component.contentState?.canGoBack ?? false let canGoBack = context.component.contentState?.canGoBack ?? false
let canGoForward = context.component.contentState?.canGoForward ?? false let canGoForward = context.component.contentState?.canGoForward ?? false
navigationLeftItems.append(
navigationLeftItems.append( AnyComponentWithIdentity(
AnyComponentWithIdentity( id: "back",
id: "back", component: AnyComponent(
component: AnyComponent( Button(
Button( content: AnyComponent(
content: AnyComponent( BundleIconComponent(
BundleIconComponent( name: "Instant View/Back",
name: "Instant View/Back", tintColor: environment.theme.chat.inputPanel.panelControlColor.withAlphaComponent(canGoBack ? 1.0 : 0.4)
tintColor: environment.theme.chat.inputPanel.panelControlColor.withAlphaComponent(canGoBack ? 1.0 : 0.4) )
) ),
), action: {
action: { performAction.invoke(.navigateBack)
performAction.invoke(.navigateBack) }
} ).minSize(CGSize(width: 44.0, height: 44.0))
).minSize(CGSize(width: 44.0, height: 44.0)) )
) )
) )
) navigationLeftItems.append(
AnyComponentWithIdentity(
navigationLeftItems.append( id: "forward",
AnyComponentWithIdentity( component: AnyComponent(
id: "forward", Button(
component: AnyComponent( content: AnyComponent(
Button( BundleIconComponent(
content: AnyComponent( name: "Instant View/Forward",
BundleIconComponent( tintColor: environment.theme.chat.inputPanel.panelControlColor.withAlphaComponent(canGoForward ? 1.0 : 0.4)
name: "Instant View/Forward", )
tintColor: environment.theme.chat.inputPanel.panelControlColor.withAlphaComponent(canGoForward ? 1.0 : 0.4) ),
) action: {
), performAction.invoke(.navigateForward)
action: { }
performAction.invoke(.navigateForward) ).minSize(CGSize(width: 44.0, height: 44.0))
} )
).minSize(CGSize(width: 44.0, height: 44.0))
) )
) )
) }
} }
navigationRightItems = [ navigationRightItems = [
@ -258,25 +262,27 @@ private final class BrowserScreenComponent: CombinedComponent {
] ]
if isTablet { if isTablet {
navigationRightItems.insert( if context.component.toolbarMode != .markdown {
AnyComponentWithIdentity( navigationRightItems.insert(
id: "bookmarks", AnyComponentWithIdentity(
component: AnyComponent( id: "bookmarks",
Button( component: AnyComponent(
content: AnyComponent( Button(
BundleIconComponent( content: AnyComponent(
name: "Instant View/Bookmark", BundleIconComponent(
tintColor: environment.theme.chat.inputPanel.panelControlColor name: "Instant View/Bookmark",
) tintColor: environment.theme.chat.inputPanel.panelControlColor
), )
action: { ),
performAction.invoke(.openBookmarks) action: {
} performAction.invoke(.openBookmarks)
).minSize(CGSize(width: 44.0, height: 44.0)) }
) ).minSize(CGSize(width: 44.0, height: 44.0))
), )
at: 0 ),
) at: 0
)
}
if context.component.canShare { if context.component.canShare {
navigationRightItems.insert( navigationRightItems.insert(
AnyComponentWithIdentity( AnyComponentWithIdentity(
@ -369,7 +375,7 @@ private final class BrowserScreenComponent: CombinedComponent {
canGoForward: context.component.contentState?.canGoForward ?? false, canGoForward: context.component.contentState?.canGoForward ?? false,
canOpenIn: canOpenIn, canOpenIn: canOpenIn,
canShare: context.component.canShare, canShare: context.component.canShare,
isDocument: context.component.contentState?.contentType == .document, mode: context.component.toolbarMode,
performAction: performAction, performAction: performAction,
performHoldAction: performHoldAction performHoldAction: performHoldAction
) )
@ -517,7 +523,45 @@ public class BrowserScreen: ViewController, MinimizableController {
private var presentationData: PresentationData private var presentationData: PresentationData
private var presentationDataDisposable: Disposable? private var presentationDataDisposable: Disposable?
private var validLayout: (ContainerViewLayout, CGFloat)? 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) { init(controller: BrowserScreen) {
self.context = controller.context self.context = controller.context
self.controller = controller self.controller = controller
@ -535,7 +579,7 @@ public class BrowserScreen: ViewController, MinimizableController {
super.init() super.init()
self.pushContent(controller.subject, transition: .immediate) self.pushContent(controller.subject, transition: .immediate)
if let content = self.content.last { if let content = self.content.last, !self.isMarkdownDocument {
content.addToRecentlyVisited() content.addToRecentlyVisited()
} }
@ -562,7 +606,44 @@ public class BrowserScreen: ViewController, MinimizableController {
let presentationData = self.presentationData let presentationData = self.presentationData
let subject: ShareControllerSubject let subject: ShareControllerSubject
var isDocument = false 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 { if let documentContent = content as? BrowserDocumentContent {
subject = .media(documentContent.file.abstract, nil) subject = .media(documentContent.file.abstract, nil)
isDocument = true isDocument = true
@ -641,7 +722,10 @@ public class BrowserScreen: ViewController, MinimizableController {
var processed = false var processed = false
if let controller = self.controller { if let controller = self.controller {
switch controller.subject { 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 processed = true
controller.openDocument(file.media, canShare) controller.openDocument(file.media, canShare)
default: default:
@ -855,6 +939,20 @@ public class BrowserScreen: ViewController, MinimizableController {
browserContent = BrowserDocumentContent(context: self.context, presentationData: self.presentationData, file: file) browserContent = BrowserDocumentContent(context: self.context, presentationData: self.presentationData, file: file)
case let .pdfDocument(file, _): case let .pdfDocument(file, _):
browserContent = BrowserPdfContent(context: self.context, presentationData: self.presentationData, file: 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 browserContent.pushContent = { [weak self] content, additionalContent in
guard let self else { guard let self else {
@ -925,6 +1023,10 @@ public class BrowserScreen: ViewController, MinimizableController {
} }
func popContent(transition: ComponentTransition) { func popContent(transition: ComponentTransition) {
guard self.content.count > 1 else {
return
}
self.content.removeLast() self.content.removeLast()
self.requestLayout(transition: transition) self.requestLayout(transition: transition)
@ -1148,10 +1250,11 @@ public class BrowserScreen: ViewController, MinimizableController {
} }
let canOpenIn = !(self.contentState?.url.hasPrefix("tonsite") ?? false) let canOpenIn = !(self.contentState?.url.hasPrefix("tonsite") ?? false)
let toolbarMode = self.toolbarMode
var canShare = true var canShare = true
if let controller = self.controller { if let controller = self.controller {
switch controller.subject { switch controller.subject {
case let .document(_, canShareValue), let .pdfDocument(_, canShareValue): case let .document(_, canShareValue), let .pdfDocument(_, canShareValue), let .markdownDocument(_, canShareValue):
canShare = canShareValue canShare = canShareValue
default: default:
break 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 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) performAction.invoke(.addBookmark)
action(.default) action(.default)
@ -1384,7 +1487,7 @@ public class BrowserScreen: ViewController, MinimizableController {
var canShare = true var canShare = true
if let controller = self.controller { if let controller = self.controller {
switch controller.subject { switch controller.subject {
case let .document(_, canShareValue), let .pdfDocument(_, canShareValue): case let .document(_, canShareValue), let .pdfDocument(_, canShareValue), let .markdownDocument(_, canShareValue):
canShare = canShareValue canShare = canShareValue
default: default:
break break
@ -1398,6 +1501,7 @@ public class BrowserScreen: ViewController, MinimizableController {
context: self.context, context: self.context,
contentState: self.contentState, contentState: self.contentState,
presentationState: self.presentationState, presentationState: self.presentationState,
toolbarMode: self.toolbarMode,
canShare: canShare, canShare: canShare,
performAction: self.performAction, performAction: self.performAction,
performHoldAction: { [weak self] view, gesture, action in 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 instantPage(webPage: TelegramMediaWebpage, anchor: String?, sourceLocation: InstantPageSourceLocation, preloadedResources: [Any]?)
case document(file: FileMediaReference, canShare: Bool) case document(file: FileMediaReference, canShare: Bool)
case pdfDocument(file: FileMediaReference, canShare: Bool) case pdfDocument(file: FileMediaReference, canShare: Bool)
case markdownDocument(file: FileMediaReference, canShare: Bool)
public var fileId: MediaId? { public var fileId: MediaId? {
switch self { switch self {
case let .document(file, _), let .pdfDocument(file, _): case let .document(file, _), let .pdfDocument(file, _), let .markdownDocument(file, _):
return file.media.fileId return file.media.fileId
default: default:
return nil return nil
@ -1506,7 +1611,10 @@ public class BrowserScreen: ViewController, MinimizableController {
"application/vnd.ms-excel", "application/vnd.ms-excel",
"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
"application/vnd.openxmlformats-officedocument.spreadsheetml.template", "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] = [ public static let supportedDocumentExtensions: [String] = [
@ -1517,7 +1625,8 @@ public class BrowserScreen: ViewController, MinimizableController {
"docx", "docx",
"xls", "xls",
"xlsx", "xlsx",
"pptx" "pptx",
"md"
] ]
public init(context: AccountContext, subject: Subject, preferredConfiguration: WKWebViewConfiguration? = nil, openPreviousOnClose: Bool = false) { public init(context: AccountContext, subject: Subject, preferredConfiguration: WKWebViewConfiguration? = nil, openPreviousOnClose: Bool = false) {

View file

@ -9,6 +9,13 @@ import ContextReferenceButtonComponent
import GlassBackgroundComponent import GlassBackgroundComponent
import EdgeEffect import EdgeEffect
enum BrowserToolbarMode: Equatable {
case webPage
case instantPage
case document
case markdown
}
final class BrowserToolbarComponent: CombinedComponent { final class BrowserToolbarComponent: CombinedComponent {
let theme: PresentationTheme let theme: PresentationTheme
let bottomInset: CGFloat let bottomInset: CGFloat
@ -131,7 +138,7 @@ final class NavigationToolbarContentComponent: CombinedComponent {
let canGoForward: Bool let canGoForward: Bool
let canOpenIn: Bool let canOpenIn: Bool
let canShare: Bool let canShare: Bool
let isDocument: Bool let mode: BrowserToolbarMode
let performAction: ActionSlot<BrowserScreen.Action> let performAction: ActionSlot<BrowserScreen.Action>
let performHoldAction: (UIView, ContextGesture?, BrowserScreen.Action) -> Void let performHoldAction: (UIView, ContextGesture?, BrowserScreen.Action) -> Void
@ -141,7 +148,7 @@ final class NavigationToolbarContentComponent: CombinedComponent {
canGoForward: Bool, canGoForward: Bool,
canOpenIn: Bool, canOpenIn: Bool,
canShare: Bool, canShare: Bool,
isDocument: Bool, mode: BrowserToolbarMode,
performAction: ActionSlot<BrowserScreen.Action>, performAction: ActionSlot<BrowserScreen.Action>,
performHoldAction: @escaping (UIView, ContextGesture?, BrowserScreen.Action) -> Void performHoldAction: @escaping (UIView, ContextGesture?, BrowserScreen.Action) -> Void
) { ) {
@ -150,7 +157,7 @@ final class NavigationToolbarContentComponent: CombinedComponent {
self.canGoForward = canGoForward self.canGoForward = canGoForward
self.canOpenIn = canOpenIn self.canOpenIn = canOpenIn
self.canShare = canShare self.canShare = canShare
self.isDocument = isDocument self.mode = mode
self.performAction = performAction self.performAction = performAction
self.performHoldAction = performHoldAction self.performHoldAction = performHoldAction
} }
@ -171,7 +178,7 @@ final class NavigationToolbarContentComponent: CombinedComponent {
if lhs.canShare != rhs.canShare { if lhs.canShare != rhs.canShare {
return false return false
} }
if lhs.isDocument != rhs.isDocument { if lhs.mode != rhs.mode {
return false return false
} }
return true return true
@ -217,7 +224,8 @@ final class NavigationToolbarContentComponent: CombinedComponent {
transition: .easeInOut(duration: 0.2) transition: .easeInOut(duration: 0.2)
) )
if context.component.isDocument { switch context.component.mode {
case .document:
var originX: CGFloat = sideInset var originX: CGFloat = sideInset
let search = search.update( let search = search.update(
@ -270,7 +278,40 @@ final class NavigationToolbarContentComponent: CombinedComponent {
.position(CGPoint(x: originX, y: availableSize.height / 2.0)) .position(CGPoint(x: originX, y: availableSize.height / 2.0))
) )
size.width = originX + sideInset 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 var originX: CGFloat = sideInset
let canGoBack = context.component.canGoBack let canGoBack = context.component.canGoBack

View file

@ -23,8 +23,6 @@ func fetchFavicon(context: AccountContext, url: String, size: CGSize) -> Signal<
if let data { if let data {
if let image = UIImage(data: data) { if let image = UIImage(data: data) {
return image 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 return nil
} else { } else {

View file

@ -2384,11 +2384,8 @@ public class ChatListItemNode: ItemListRevealOptionsItemNode {
var currentStatusIconContent: EmojiStatusComponent.Content? var currentStatusIconContent: EmojiStatusComponent.Content?
var currentStatusIconParticleColor: UIColor? var currentStatusIconParticleColor: UIColor?
var currentSecretIconImage: UIImage? var currentSecretIconImage: UIImage?
var currentForwardedIcon: UIImage? var currentMessageTypeIcon: UIImage?
var currentStoryIcon: UIImage? var currentMessageTypeIconOffset: CGPoint = .zero
var currentGiftIcon: UIImage?
var currentLocationIcon: UIImage?
var currentPollIcon: UIImage?
var selectableControlSizeAndApply: (CGFloat, (CGSize, Bool) -> ItemListSelectableControlNode)? var selectableControlSizeAndApply: (CGFloat, (CGSize, Bool) -> ItemListSelectableControlNode)?
var reorderControlSizeAndApply: (CGFloat, (CGFloat, Bool, ContainedViewLayoutTransition) -> ItemListEditableReorderControlNode)? var reorderControlSizeAndApply: (CGFloat, (CGFloat, Bool, ContainedViewLayoutTransition) -> ItemListEditableReorderControlNode)?
@ -2559,12 +2556,26 @@ public class ChatListItemNode: ItemListRevealOptionsItemNode {
var contentImageSpecs: [ContentImageSpec] = [] var contentImageSpecs: [ContentImageSpec] = []
var avatarContentImageSpec: ContentImageSpec? var avatarContentImageSpec: ContentImageSpec?
var forumThread: (id: Int64, title: String, iconId: Int64?, iconColor: Int32, threadPeer: EnginePeer?, isUnread: Bool)? var forumThread: (id: Int64, title: String, iconId: Int64?, iconColor: Int32, threadPeer: EnginePeer?, isUnread: Bool)?
var displayForwardedIcon = false enum MessageTypeIcon {
var displayStoryReplyIcon = false enum CallType {
var displayGiftIcon = false case voice
var displayLocationIcon = false case video
var displayPollIcon = false }
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 var ignoreForwardedIcon = false
switch contentData { switch contentData {
@ -2874,24 +2885,29 @@ public class ChatListItemNode: ItemListRevealOptionsItemNode {
if !ignoreForwardedIcon { if !ignoreForwardedIcon {
if case .savedMessagesChats = item.chatListLocation { if case .savedMessagesChats = item.chatListLocation {
displayForwardedIcon = false
} else if let forwardInfo = message.forwardInfo, !forwardInfo.flags.contains(.isImported) && !message.id.peerId.isVerificationCodes { } 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 }) { } else if let _ = message.attributes.first(where: { $0 is ReplyStoryAttribute }) {
displayStoryReplyIcon = true messageTypeIcon = .story
} else { } else {
for media in message.media { for media in message.media {
if let _ = media as? TelegramMediaPoll { 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 { } else if let _ = media as? TelegramMediaMap {
displayLocationIcon = true messageTypeIcon = .location
} else if let action = media as? TelegramMediaAction { } else if let action = media as? TelegramMediaAction {
switch action.action { switch action.action {
case let .phoneCall(_, _, _, isVideo):
messageTypeIcon = .call(isVideo ? .video : .voice, message.flags.contains(.Incoming) ? .incoming : .outgoing)
case .giftPremium, .giftStars, .starGift, .starGiftUnique: case .giftPremium, .giftStars, .starGift, .starGiftUnique:
displayGiftIcon = true messageTypeIcon = .gift
case let .giftCode(_, _, _, boostPeerId, _, _, _, _, _, _, _): case let .giftCode(_, _, _, boostPeerId, _, _, _, _, _, _, _):
if boostPeerId == nil { if boostPeerId == nil {
displayGiftIcon = true messageTypeIcon = .gift
} }
default: default:
break break
@ -3048,64 +3064,50 @@ public class ChatListItemNode: ItemListRevealOptionsItemNode {
attributedText = textString attributedText = textString
} }
if displayForwardedIcon { switch messageTypeIcon {
currentForwardedIcon = PresentationResourcesChatList.forwardedIcon(item.presentationData.theme) case let .call(type, direction):
} switch type {
case .voice:
if displayStoryReplyIcon { switch direction {
currentStoryIcon = PresentationResourcesChatList.storyReplyIcon(item.presentationData.theme) case .incoming:
} currentMessageTypeIcon = PresentationResourcesChatList.callIncomingIcon(item.presentationData.theme)
case .outgoing:
if displayGiftIcon { currentMessageTypeIcon = PresentationResourcesChatList.callOutgoingIcon(item.presentationData.theme)
currentGiftIcon = PresentationResourcesChatList.giftIcon(item.presentationData.theme) }
} case .video:
switch direction {
if displayLocationIcon { case .incoming:
currentLocationIcon = PresentationResourcesChatList.locationIcon(item.presentationData.theme) currentMessageTypeIcon = PresentationResourcesChatList.callVideoIncomingIcon(item.presentationData.theme)
} case .outgoing:
currentMessageTypeIcon = PresentationResourcesChatList.callVideoOutgoingIcon(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
} }
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 { if let currentMessageTypeIcon {
textLeftCutout += currentStoryIcon.size.width textLeftCutout += currentMessageTypeIcon.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 !contentImageSpecs.isEmpty { if !contentImageSpecs.isEmpty {
textLeftCutout += forwardedIconSpacing textLeftCutout += forwardedIconSpacing
} else { } 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 mediaPreviewOffset = textNodeFrame.origin.offsetBy(dx: 1.0, dy: 1.0 + floor((measureLayout.size.height - contentImageSize.height) / 2.0))
var messageTypeIcon: UIImage? let messageTypeIconImage = currentMessageTypeIcon
var messageTypeIconOffset = mediaPreviewOffset let messageTypeIconOffset = CGPoint(x: mediaPreviewOffset.x + currentMessageTypeIconOffset.x, y: mediaPreviewOffset.y + currentMessageTypeIconOffset.y)
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
}
if let messageTypeIcon { if let messageTypeIconImage {
strongSelf.forwardedIconNode.image = messageTypeIcon strongSelf.forwardedIconNode.image = messageTypeIconImage
if strongSelf.forwardedIconNode.supernode == nil { if strongSelf.forwardedIconNode.supernode == nil {
strongSelf.mainContentContainerNode.addSubnode(strongSelf.forwardedIconNode) strongSelf.mainContentContainerNode.addSubnode(strongSelf.forwardedIconNode)
} }
transition.updateFrame(node: strongSelf.forwardedIconNode, frame: CGRect(origin: messageTypeIconOffset, size: messageTypeIcon.size)) transition.updateFrame(node: strongSelf.forwardedIconNode, frame: CGRect(origin: messageTypeIconOffset, size: messageTypeIconImage.size))
mediaPreviewOffset.x += messageTypeIcon.size.width + forwardedIconSpacing mediaPreviewOffset.x += messageTypeIconImage.size.width + forwardedIconSpacing
} else if strongSelf.forwardedIconNode.supernode != nil { } else if strongSelf.forwardedIconNode.supernode != nil {
strongSelf.forwardedIconNode.removeFromSupernode() strongSelf.forwardedIconNode.removeFromSupernode()
} }

View file

@ -311,7 +311,7 @@ public func chatListItemStrings(strings: PresentationStrings, nameDisplayOrder:
case _ as TelegramMediaContact: case _ as TelegramMediaContact:
messageText = strings.Message_Contact messageText = strings.Message_Contact
case let game as TelegramMediaGame: case let game as TelegramMediaGame:
messageText = "🎮 \(game.title)" messageText = game.title
case let invoice as TelegramMediaInvoice: case let invoice as TelegramMediaInvoice:
messageText = invoice.title messageText = invoice.title
case let action as TelegramMediaAction: case let action as TelegramMediaAction:
@ -438,15 +438,11 @@ public func chatListItemStrings(strings: PresentationStrings, nameDisplayOrder:
messageText = content.displayUrl messageText = content.displayUrl
} }
case let todo as TelegramMediaTodo: case let todo as TelegramMediaTodo:
let pollPrefix = "☑️ " messageText = todo.text
let entityOffset = (pollPrefix as NSString).length customEmojiRanges = []
messageText = "\(pollPrefix)\(todo.text)"
for entity in todo.textEntities { for entity in todo.textEntities {
if case let .CustomEmoji(_, fileId) = entity.type { if case let .CustomEmoji(_, fileId) = entity.type {
if customEmojiRanges == nil { let range = NSRange(location: entity.range.lowerBound, length: entity.range.upperBound - entity.range.lowerBound)
customEmojiRanges = []
}
let range = NSRange(location: entityOffset + 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) let attribute = ChatTextInputTextCustomEmojiAttribute(interactivelySelectedFromPackId: nil, fileId: fileId, file: message.associatedMedia[EngineMedia.Id(namespace: Namespaces.Media.CloudFile, id: fileId)] as? TelegramMediaFile)
customEmojiRanges?.append((range, attribute)) customEmojiRanges?.append((range, attribute))
} }

View file

@ -383,8 +383,15 @@ public final class ReactionListContextMenuContent: ContextControllerItemsContent
private var iconFrame: CGRect? private var iconFrame: CGRect?
private var file: TelegramMediaFile? private var file: TelegramMediaFile?
private var fileDisposable: Disposable? private var fileDisposable: Disposable?
private var longTapRecognizer: UILongPressGestureRecognizer?
private var skipNextTapAction = false
let action: () -> Void let action: () -> Void
var longTapAction: (() -> Void)? {
didSet {
self.longTapRecognizer?.isEnabled = self.longTapAction != nil
}
}
private var item: EngineMessageReactionListContext.Item? private var item: EngineMessageReactionListContext.Item?
@ -422,6 +429,12 @@ public final class ReactionListContextMenuContent: ContextControllerItemsContent
self.view.addSubview(self.readIconView) self.view.addSubview(self.readIconView)
self.addTarget(self, action: #selector(self.pressed), forControlEvents: .touchUpInside) 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 { deinit {
@ -429,9 +442,30 @@ public final class ReactionListContextMenuContent: ContextControllerItemsContent
} }
@objc private func pressed() { @objc private func pressed() {
if self.skipNextTapAction {
self.skipNextTapAction = false
return
}
self.action() 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() { private func updateReactionLayer() {
guard let file = self.file else { guard let file = self.file else {
return return
@ -771,6 +805,7 @@ public final class ReactionListContextMenuContent: ContextControllerItemsContent
private let requestUpdate: (ReactionsTabNode, ContainedViewLayoutTransition) -> Void private let requestUpdate: (ReactionsTabNode, ContainedViewLayoutTransition) -> Void
private let requestUpdateApparentHeight: (ReactionsTabNode, ContainedViewLayoutTransition) -> Void private let requestUpdateApparentHeight: (ReactionsTabNode, ContainedViewLayoutTransition) -> Void
private let openPeer: (EnginePeer, Bool) -> Void private let openPeer: (EnginePeer, Bool) -> Void
private let deleteReaction: ((EnginePeer, MessageReaction.Reaction) -> Void)?
private var hasMore: Bool = false private var hasMore: Bool = false
@ -804,7 +839,8 @@ public final class ReactionListContextMenuContent: ContextControllerItemsContent
readStats: MessageReadStats?, readStats: MessageReadStats?,
requestUpdate: @escaping (ReactionsTabNode, ContainedViewLayoutTransition) -> Void, requestUpdate: @escaping (ReactionsTabNode, ContainedViewLayoutTransition) -> Void,
requestUpdateApparentHeight: @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.context = context
self.displayReadTimestamps = displayReadTimestamps self.displayReadTimestamps = displayReadTimestamps
@ -816,6 +852,7 @@ public final class ReactionListContextMenuContent: ContextControllerItemsContent
self.requestUpdate = requestUpdate self.requestUpdate = requestUpdate
self.requestUpdateApparentHeight = requestUpdateApparentHeight self.requestUpdateApparentHeight = requestUpdateApparentHeight
self.openPeer = openPeer self.openPeer = openPeer
self.deleteReaction = deleteReaction
self.listContext = context.engine.messages.messageReactionList(message: message, readStats: readStats, reaction: reaction) 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) 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) 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 itemNode.frame = itemFrame
} else if index < self.state.totalCount { } else if index < self.state.totalCount {
validPlaceholderIds.insert(index) validPlaceholderIds.insert(index)
@ -1079,6 +1124,7 @@ public final class ReactionListContextMenuContent: ContextControllerItemsContent
private let reactions: [(MessageReaction.Reaction?, Int)] private let reactions: [(MessageReaction.Reaction?, Int)]
private let requestUpdate: (ContainedViewLayoutTransition) -> Void private let requestUpdate: (ContainedViewLayoutTransition) -> Void
private let requestUpdateApparentHeight: (ContainedViewLayoutTransition) -> Void private let requestUpdateApparentHeight: (ContainedViewLayoutTransition) -> Void
private let deleteReaction: ((EnginePeer, MessageReaction.Reaction) -> Void)?
private var presentationData: PresentationData private var presentationData: PresentationData
@ -1111,7 +1157,8 @@ public final class ReactionListContextMenuContent: ContextControllerItemsContent
requestUpdate: @escaping (ContainedViewLayoutTransition) -> Void, requestUpdate: @escaping (ContainedViewLayoutTransition) -> Void,
requestUpdateApparentHeight: @escaping (ContainedViewLayoutTransition) -> Void, requestUpdateApparentHeight: @escaping (ContainedViewLayoutTransition) -> Void,
back: (() -> Void)?, back: (() -> Void)?,
openPeer: @escaping (EnginePeer, Bool) -> Void openPeer: @escaping (EnginePeer, Bool) -> Void,
deleteReaction: ((EnginePeer, MessageReaction.Reaction) -> Void)?
) { ) {
self.context = context self.context = context
self.displayReadTimestamps = displayReadTimestamps self.displayReadTimestamps = displayReadTimestamps
@ -1126,6 +1173,7 @@ public final class ReactionListContextMenuContent: ContextControllerItemsContent
self.requestUpdate = requestUpdate self.requestUpdate = requestUpdate
self.requestUpdateApparentHeight = requestUpdateApparentHeight self.requestUpdateApparentHeight = requestUpdateApparentHeight
self.deleteReaction = deleteReaction
if let back = back { if let back = back {
self.backButtonNode = BackButtonNode() self.backButtonNode = BackButtonNode()
@ -1332,7 +1380,8 @@ public final class ReactionListContextMenuContent: ContextControllerItemsContent
strongSelf.requestUpdateApparentHeight(transition) strongSelf.requestUpdateApparentHeight(transition)
} }
}, },
openPeer: self.openPeer openPeer: self.openPeer,
deleteReaction: self.deleteReaction
) )
self.addSubnode(tabNode) self.addSubnode(tabNode)
self.visibleTabNodes[index] = tabNode self.visibleTabNodes[index] = tabNode
@ -1426,6 +1475,7 @@ public final class ReactionListContextMenuContent: ContextControllerItemsContent
let readStats: MessageReadStats? let readStats: MessageReadStats?
let back: (() -> Void)? let back: (() -> Void)?
let openPeer: (EnginePeer, Bool) -> Void let openPeer: (EnginePeer, Bool) -> Void
let deleteReaction: ((EnginePeer, MessageReaction.Reaction) -> Void)?
public init( public init(
context: AccountContext, context: AccountContext,
@ -1437,7 +1487,8 @@ public final class ReactionListContextMenuContent: ContextControllerItemsContent
reaction: MessageReaction.Reaction?, reaction: MessageReaction.Reaction?,
readStats: MessageReadStats?, readStats: MessageReadStats?,
back: (() -> Void)?, back: (() -> Void)?,
openPeer: @escaping (EnginePeer, Bool) -> Void openPeer: @escaping (EnginePeer, Bool) -> Void,
deleteReaction: ((EnginePeer, MessageReaction.Reaction) -> Void)? = nil
) { ) {
self.context = context self.context = context
self.displayReadTimestamps = displayReadTimestamps self.displayReadTimestamps = displayReadTimestamps
@ -1449,6 +1500,7 @@ public final class ReactionListContextMenuContent: ContextControllerItemsContent
self.readStats = readStats self.readStats = readStats
self.back = back self.back = back
self.openPeer = openPeer self.openPeer = openPeer
self.deleteReaction = deleteReaction
} }
public func node( public func node(
@ -1467,7 +1519,8 @@ public final class ReactionListContextMenuContent: ContextControllerItemsContent
requestUpdate: requestUpdate, requestUpdate: requestUpdate,
requestUpdateApparentHeight: requestUpdateApparentHeight, requestUpdateApparentHeight: requestUpdateApparentHeight,
back: self.back, back: self.back,
openPeer: self.openPeer openPeer: self.openPeer,
deleteReaction: self.deleteReaction
) )
} }
} }

View file

@ -13,7 +13,7 @@ public final class ResizableSheetComponentEnvironment: Equatable {
public let bounds: CGRect public let bounds: CGRect
public let isInteractive: Bool public let isInteractive: Bool
} }
public let theme: PresentationTheme public let theme: PresentationTheme
public let statusBarHeight: CGFloat public let statusBarHeight: CGFloat
public let safeInsets: UIEdgeInsets public let safeInsets: UIEdgeInsets
@ -26,7 +26,7 @@ public final class ResizableSheetComponentEnvironment: Equatable {
public let regularMetricsSize: CGSize? public let regularMetricsSize: CGSize?
public let dismiss: (Bool) -> Void public let dismiss: (Bool) -> Void
public let boundsUpdated: ActionSlot<BoundsUpdate> public let boundsUpdated: ActionSlot<BoundsUpdate>
public init( public init(
theme: PresentationTheme, theme: PresentationTheme,
statusBarHeight: CGFloat, statusBarHeight: CGFloat,
@ -54,7 +54,7 @@ public final class ResizableSheetComponentEnvironment: Equatable {
self.dismiss = dismiss self.dismiss = dismiss
self.boundsUpdated = boundsUpdated self.boundsUpdated = boundsUpdated
} }
public static func ==(lhs: ResizableSheetComponentEnvironment, rhs: ResizableSheetComponentEnvironment) -> Bool { public static func ==(lhs: ResizableSheetComponentEnvironment, rhs: ResizableSheetComponentEnvironment) -> Bool {
if lhs.theme != rhs.theme { if lhs.theme != rhs.theme {
return false return false
@ -92,19 +92,24 @@ public final class ResizableSheetComponentEnvironment: Equatable {
public final class ResizableSheetComponent<ChildEnvironmentType: Sendable & Equatable>: Component { public final class ResizableSheetComponent<ChildEnvironmentType: Sendable & Equatable>: Component {
public typealias EnvironmentType = (ChildEnvironmentType, ResizableSheetComponentEnvironment) public typealias EnvironmentType = (ChildEnvironmentType, ResizableSheetComponentEnvironment)
public class ExternalState { public class ExternalState {
public fileprivate(set) var contentHeight: CGFloat public fileprivate(set) var contentHeight: CGFloat
fileprivate var trackedScrollViewUpdated: ((UIScrollView?) -> Void)?
public init() { public init() {
self.contentHeight = 0.0 self.contentHeight = 0.0
} }
public func setTrackedScrollView(_ scrollView: UIScrollView?) {
self.trackedScrollViewUpdated?(scrollView)
}
} }
public enum BackgroundColor: Equatable { public enum BackgroundColor: Equatable {
case color(UIColor) case color(UIColor)
} }
public let content: AnyComponent<ChildEnvironmentType> public let content: AnyComponent<ChildEnvironmentType>
public let titleItem: AnyComponent<Empty>? public let titleItem: AnyComponent<Empty>?
public let leftItem: 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 bottomItem: AnyComponent<Empty>?
public let backgroundColor: BackgroundColor public let backgroundColor: BackgroundColor
public let isFullscreen: Bool public let isFullscreen: Bool
public let defaultHeight: CGFloat?
public let externalState: ExternalState? public let externalState: ExternalState?
public let animateOut: ActionSlot<Action<()>> public let animateOut: ActionSlot<Action<()>>
public init( public init(
content: AnyComponent<ChildEnvironmentType>, content: AnyComponent<ChildEnvironmentType>,
titleItem: AnyComponent<Empty>? = nil, titleItem: AnyComponent<Empty>? = nil,
@ -125,6 +131,7 @@ public final class ResizableSheetComponent<ChildEnvironmentType: Sendable & Equa
bottomItem: AnyComponent<Empty>? = nil, bottomItem: AnyComponent<Empty>? = nil,
backgroundColor: BackgroundColor, backgroundColor: BackgroundColor,
isFullscreen: Bool = false, isFullscreen: Bool = false,
defaultHeight: CGFloat? = nil,
externalState: ExternalState? = nil, externalState: ExternalState? = nil,
animateOut: ActionSlot<Action<()>>, animateOut: ActionSlot<Action<()>>,
) { ) {
@ -136,10 +143,11 @@ public final class ResizableSheetComponent<ChildEnvironmentType: Sendable & Equa
self.bottomItem = bottomItem self.bottomItem = bottomItem
self.backgroundColor = backgroundColor self.backgroundColor = backgroundColor
self.isFullscreen = isFullscreen self.isFullscreen = isFullscreen
self.defaultHeight = defaultHeight
self.externalState = externalState self.externalState = externalState
self.animateOut = animateOut self.animateOut = animateOut
} }
public static func ==(lhs: ResizableSheetComponent, rhs: ResizableSheetComponent) -> Bool { public static func ==(lhs: ResizableSheetComponent, rhs: ResizableSheetComponent) -> Bool {
if lhs.content != rhs.content { if lhs.content != rhs.content {
return false return false
@ -165,12 +173,15 @@ public final class ResizableSheetComponent<ChildEnvironmentType: Sendable & Equa
if lhs.isFullscreen != rhs.isFullscreen { if lhs.isFullscreen != rhs.isFullscreen {
return false return false
} }
if lhs.defaultHeight != rhs.defaultHeight {
return false
}
if lhs.animateOut != rhs.animateOut { if lhs.animateOut != rhs.animateOut {
return false return false
} }
return true return true
} }
private struct ItemLayout: Equatable { private struct ItemLayout: Equatable {
var containerSize: CGSize var containerSize: CGSize
var containerInset: CGFloat var containerInset: CGFloat
@ -179,7 +190,7 @@ public final class ResizableSheetComponent<ChildEnvironmentType: Sendable & Equa
var topInset: CGFloat var topInset: CGFloat
var fillingSize: CGFloat var fillingSize: CGFloat
let isTablet: Bool let isTablet: Bool
init(containerSize: CGSize, containerInset: CGFloat, containerCornerRadius: CGFloat, bottomInset: CGFloat, topInset: CGFloat, fillingSize: CGFloat, isTablet: Bool) { init(containerSize: CGSize, containerInset: CGFloat, containerCornerRadius: CGFloat, bottomInset: CGFloat, topInset: CGFloat, fillingSize: CGFloat, isTablet: Bool) {
self.containerSize = containerSize self.containerSize = containerSize
self.containerInset = containerInset self.containerInset = containerInset
@ -190,26 +201,26 @@ public final class ResizableSheetComponent<ChildEnvironmentType: Sendable & Equa
self.isTablet = isTablet self.isTablet = isTablet
} }
} }
private final class ScrollView: UIScrollView { private final class ScrollView: UIScrollView {
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
return super.hitTest(point, with: event) return super.hitTest(point, with: event)
} }
} }
public final class View: UIView, UIScrollViewDelegate, ComponentTaggedView, UIGestureRecognizerDelegate { public final class View: UIView, UIScrollViewDelegate, ComponentTaggedView, UIGestureRecognizerDelegate {
public final class Tag { public final class Tag {
public init() { public init() {
} }
} }
public func matches(tag: Any) -> Bool { public func matches(tag: Any) -> Bool {
if let _ = tag as? Tag { if let _ = tag as? Tag {
return true return true
} }
return false return false
} }
private let dimView: UIView private let dimView: UIView
public let containerView: UIView public let containerView: UIView
private let backgroundLayer: SimpleLayer private let backgroundLayer: SimpleLayer
@ -218,54 +229,57 @@ public final class ResizableSheetComponent<ChildEnvironmentType: Sendable & Equa
private let scrollView: ScrollView private let scrollView: ScrollView
private let scrollContentClippingView: SparseContainerView private let scrollContentClippingView: SparseContainerView
private let scrollContentView: UIView private let scrollContentView: UIView
private let topEdgeEffectView: EdgeEffectView private let topEdgeEffectView: EdgeEffectView
private let bottomEdgeEffectView: EdgeEffectView private let bottomEdgeEffectView: EdgeEffectView
private let contentView: ComponentView<ChildEnvironmentType> private let contentView: ComponentView<ChildEnvironmentType>
private var titleItemView: ComponentView<Empty>? private var titleItemView: ComponentView<Empty>?
private var leftItemView: ComponentView<Empty>? private var leftItemView: ComponentView<Empty>?
private var rightItemView: ComponentView<Empty>? private var rightItemView: ComponentView<Empty>?
private var bottomItemView: ComponentView<Empty>? private var bottomItemView: ComponentView<Empty>?
private let backgroundHandleView: UIImageView private let backgroundHandleView: UIImageView
private var ignoreScrolling: Bool = false private var ignoreScrolling: Bool = false
private var isDismissingInteractively: Bool = false private var isDismissingInteractively: Bool = false
private var dismissTranslation: CGFloat = 0.0 private var dismissTranslation: CGFloat = 0.0
private var dismissStartTranslation: CGFloat? private var dismissStartTranslation: CGFloat?
private var dismissPanGesture: UIPanGestureRecognizer? private var dismissPanGesture: UIPanGestureRecognizer?
private var component: ResizableSheetComponent? private var component: ResizableSheetComponent?
private weak var state: EmptyComponentState? private weak var state: EmptyComponentState?
private var isUpdating: Bool = false private var isUpdating: Bool = false
private var environment: ResizableSheetComponentEnvironment? private var environment: ResizableSheetComponentEnvironment?
private var itemLayout: ItemLayout? private var itemLayout: ItemLayout?
private var registeredExternalState: ExternalState?
private weak var trackedScrollView: UIScrollView?
private var trackedScrollViewWasAtTopOnGestureBegan = false
override init(frame: CGRect) { override init(frame: CGRect) {
self.dimView = UIView() self.dimView = UIView()
self.containerView = UIView() self.containerView = UIView()
self.containerView.clipsToBounds = true self.containerView.clipsToBounds = true
self.containerView.layer.cornerRadius = 40.0 self.containerView.layer.cornerRadius = 40.0
self.containerView.layer.maskedCorners = [.layerMinXMaxYCorner, .layerMaxXMaxYCorner] self.containerView.layer.maskedCorners = [.layerMinXMaxYCorner, .layerMaxXMaxYCorner]
self.backgroundLayer = SimpleLayer() self.backgroundLayer = SimpleLayer()
self.backgroundLayer.maskedCorners = [.layerMinXMinYCorner, .layerMaxXMinYCorner] self.backgroundLayer.maskedCorners = [.layerMinXMinYCorner, .layerMaxXMinYCorner]
self.backgroundLayer.cornerRadius = 40.0 self.backgroundLayer.cornerRadius = 40.0
self.backgroundHandleView = UIImageView() self.backgroundHandleView = UIImageView()
self.navigationBarContainer = SparseContainerView() self.navigationBarContainer = SparseContainerView()
self.bottomContainer = SparseContainerView() self.bottomContainer = SparseContainerView()
self.scrollView = ScrollView() self.scrollView = ScrollView()
self.scrollContentClippingView = SparseContainerView() self.scrollContentClippingView = SparseContainerView()
self.scrollContentClippingView.clipsToBounds = true self.scrollContentClippingView.clipsToBounds = true
self.scrollContentView = UIView() self.scrollContentView = UIView()
self.topEdgeEffectView = EdgeEffectView() self.topEdgeEffectView = EdgeEffectView()
self.topEdgeEffectView.clipsToBounds = true self.topEdgeEffectView.clipsToBounds = true
self.topEdgeEffectView.layer.maskedCorners = [.layerMinXMinYCorner, .layerMaxXMinYCorner] self.topEdgeEffectView.layer.maskedCorners = [.layerMinXMinYCorner, .layerMaxXMinYCorner]
@ -275,15 +289,16 @@ public final class ResizableSheetComponent<ChildEnvironmentType: Sendable & Equa
self.bottomEdgeEffectView.clipsToBounds = true self.bottomEdgeEffectView.clipsToBounds = true
self.bottomEdgeEffectView.layer.maskedCorners = [.layerMinXMaxYCorner, .layerMaxXMaxYCorner] self.bottomEdgeEffectView.layer.maskedCorners = [.layerMinXMaxYCorner, .layerMaxXMaxYCorner]
self.bottomEdgeEffectView.layer.cornerRadius = 40.0 self.bottomEdgeEffectView.layer.cornerRadius = 40.0
self.bottomEdgeEffectView.isUserInteractionEnabled = false
self.contentView = ComponentView() self.contentView = ComponentView()
super.init(frame: frame) super.init(frame: frame)
self.addSubview(self.dimView) self.addSubview(self.dimView)
self.addSubview(self.containerView) self.addSubview(self.containerView)
self.containerView.layer.addSublayer(self.backgroundLayer) self.containerView.layer.addSublayer(self.backgroundLayer)
self.scrollView.delaysContentTouches = true self.scrollView.delaysContentTouches = true
self.scrollView.canCancelContentTouches = true self.scrollView.canCancelContentTouches = true
self.scrollView.clipsToBounds = false self.scrollView.clipsToBounds = false
@ -296,34 +311,39 @@ public final class ResizableSheetComponent<ChildEnvironmentType: Sendable & Equa
self.scrollView.scrollsToTop = false self.scrollView.scrollsToTop = false
self.scrollView.delegate = self self.scrollView.delegate = self
self.scrollView.clipsToBounds = true self.scrollView.clipsToBounds = true
self.containerView.addSubview(self.scrollContentClippingView) self.containerView.addSubview(self.scrollContentClippingView)
self.scrollContentClippingView.addSubview(self.scrollView) self.scrollContentClippingView.addSubview(self.scrollView)
self.scrollView.addSubview(self.scrollContentView) self.scrollView.addSubview(self.scrollContentView)
self.containerView.addSubview(self.navigationBarContainer) self.containerView.addSubview(self.navigationBarContainer)
self.containerView.addSubview(self.bottomContainer) self.containerView.addSubview(self.bottomContainer)
self.dimView.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.dimTapGesture(_:)))) self.dimView.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.dimTapGesture(_:))))
let dismissPanGesture = UIPanGestureRecognizer(target: self, action: #selector(self.dismissPanGesture(_:))) let dismissPanGesture = UIPanGestureRecognizer(target: self, action: #selector(self.dismissPanGesture(_:)))
dismissPanGesture.maximumNumberOfTouches = 1 dismissPanGesture.maximumNumberOfTouches = 1
dismissPanGesture.delegate = self dismissPanGesture.delegate = self
self.addGestureRecognizer(dismissPanGesture) self.addGestureRecognizer(dismissPanGesture)
self.dismissPanGesture = dismissPanGesture self.dismissPanGesture = dismissPanGesture
} }
required init?(coder: NSCoder) { required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented") fatalError("init(coder:) has not been implemented")
} }
deinit {
self.registeredExternalState?.trackedScrollViewUpdated = nil
self.setTrackedScrollView(nil)
}
public func scrollViewDidScroll(_ scrollView: UIScrollView) { public func scrollViewDidScroll(_ scrollView: UIScrollView) {
if !self.ignoreScrolling { if !self.ignoreScrolling {
self.updateScrolling(transition: .immediate) self.updateScrolling(transition: .immediate)
} }
} }
public override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { public override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
if !self.bounds.contains(point) { if !self.bounds.contains(point) {
return nil return nil
@ -331,7 +351,7 @@ public final class ResizableSheetComponent<ChildEnvironmentType: Sendable & Equa
if !self.backgroundLayer.frame.contains(point) { if !self.backgroundLayer.frame.contains(point) {
return self.dimView return self.dimView
} }
if let result = self.navigationBarContainer.hitTest(self.convert(point, to: self.navigationBarContainer), with: event) { if let result = self.navigationBarContainer.hitTest(self.convert(point, to: self.navigationBarContainer), with: event) {
return result return result
} }
@ -341,7 +361,7 @@ public final class ResizableSheetComponent<ChildEnvironmentType: Sendable & Equa
let result = super.hitTest(point, with: event) let result = super.hitTest(point, with: event)
return result return result
} }
override public func gestureRecognizerShouldBegin(_ gestureRecognizer: UIGestureRecognizer) -> Bool { override public func gestureRecognizerShouldBegin(_ gestureRecognizer: UIGestureRecognizer) -> Bool {
if gestureRecognizer === self.dismissPanGesture { if gestureRecognizer === self.dismissPanGesture {
let pan = gestureRecognizer as! UIPanGestureRecognizer let pan = gestureRecognizer as! UIPanGestureRecognizer
@ -352,22 +372,25 @@ public final class ResizableSheetComponent<ChildEnvironmentType: Sendable & Equa
} }
return true return true
} }
public func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool { public func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool {
if gestureRecognizer === self.dismissPanGesture { if gestureRecognizer === self.dismissPanGesture {
if otherGestureRecognizer === self.scrollView.panGestureRecognizer { if otherGestureRecognizer === self.scrollView.panGestureRecognizer {
return true return true
} }
if otherGestureRecognizer === self.trackedScrollView?.panGestureRecognizer {
return true
}
} }
return false return false
} }
@objc private func dimTapGesture(_ recognizer: UITapGestureRecognizer) { @objc private func dimTapGesture(_ recognizer: UITapGestureRecognizer) {
if case .ended = recognizer.state { if case .ended = recognizer.state {
self.dismissAnimated() self.dismissAnimated()
} }
} }
public func dismissAnimated() { public func dismissAnimated() {
guard let environment = self.environment else { guard let environment = self.environment else {
return return
@ -375,16 +398,16 @@ public final class ResizableSheetComponent<ChildEnvironmentType: Sendable & Equa
self.endEditing(true) self.endEditing(true)
environment.dismiss(true) environment.dismiss(true)
} }
private func updateDismissTranslation(_ translation: CGFloat) { private func updateDismissTranslation(_ translation: CGFloat) {
self.dismissTranslation = translation self.dismissTranslation = translation
self.updateScrolling(transition: .immediate) self.updateScrolling(transition: .immediate)
let maxAlphaDistance = max(1.0, self.bounds.height * 0.9) let maxAlphaDistance = max(1.0, self.bounds.height * 0.9)
let alpha = 1.0 - min(1.0, translation / maxAlphaDistance) let alpha = 1.0 - min(1.0, translation / maxAlphaDistance)
self.dimView.alpha = alpha self.dimView.alpha = alpha
} }
private func resetDismissTranslation(animated: Bool) { private func resetDismissTranslation(animated: Bool) {
self.dismissTranslation = 0.0 self.dismissTranslation = 0.0
if animated { if animated {
@ -396,12 +419,12 @@ public final class ResizableSheetComponent<ChildEnvironmentType: Sendable & Equa
self.updateScrolling(transition: .immediate) self.updateScrolling(transition: .immediate)
} }
} }
@objc private func dismissPanGesture(_ recognizer: UIPanGestureRecognizer) { @objc private func dismissPanGesture(_ recognizer: UIPanGestureRecognizer) {
guard let component = self.component else { guard let component = self.component else {
return return
} }
let translation = recognizer.translation(in: self) let translation = recognizer.translation(in: self)
switch recognizer.state { switch recognizer.state {
case .began: case .began:
@ -414,7 +437,7 @@ public final class ResizableSheetComponent<ChildEnvironmentType: Sendable & Equa
self.dismissStartTranslation = translation.y self.dismissStartTranslation = translation.y
self.scrollView.isScrollEnabled = false self.scrollView.isScrollEnabled = false
} }
let start = self.dismissStartTranslation ?? translation.y let start = self.dismissStartTranslation ?? translation.y
let dismissOffset = max(0.0, translation.y - start) let dismissOffset = max(0.0, translation.y - start)
self.scrollView.contentOffset = .zero self.scrollView.contentOffset = .zero
@ -430,10 +453,10 @@ public final class ResizableSheetComponent<ChildEnvironmentType: Sendable & Equa
let currentOffset = self.dismissTranslation let currentOffset = self.dismissTranslation
let threshold = min(180.0, self.bounds.height * 0.25) let threshold = min(180.0, self.bounds.height * 0.25)
let shouldDismiss = currentOffset > threshold || velocityY > 1000.0 let shouldDismiss = currentOffset > threshold || velocityY > 1000.0
self.isDismissingInteractively = false self.isDismissingInteractively = false
self.scrollView.isScrollEnabled = !component.isFullscreen self.scrollView.isScrollEnabled = !component.isFullscreen
if shouldDismiss { if shouldDismiss {
let animateOffset = self.bounds.height - self.backgroundLayer.frame.minY let animateOffset = self.bounds.height - self.backgroundLayer.frame.minY
let initialVelocity = animateOffset > 0.0 ? max(0.0, velocityY) / animateOffset : 0.0 let initialVelocity = animateOffset > 0.0 ? max(0.0, velocityY) / animateOffset : 0.0
@ -448,73 +471,170 @@ public final class ResizableSheetComponent<ChildEnvironmentType: Sendable & Equa
break 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) { 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 return
} }
var topOffset = -self.scrollView.bounds.minY + itemLayout.topInset var topOffset = -self.scrollView.bounds.minY + itemLayout.topInset
topOffset = max(0.0, topOffset) topOffset = max(0.0, topOffset)
transition.setTransform(layer: self.backgroundLayer, transform: CATransform3DMakeTranslation(0.0, topOffset + itemLayout.containerInset, 0.0)) 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)) transition.setPosition(view: self.navigationBarContainer, position: CGPoint(x: 0.0, y: topOffset + itemLayout.containerInset))
var topOffsetFraction = self.scrollView.bounds.minY / 100.0 var topOffsetFraction = self.scrollView.bounds.minY / 100.0
topOffsetFraction = max(0.0, min(1.0, topOffsetFraction)) topOffsetFraction = max(0.0, min(1.0, topOffsetFraction))
if component.isFullscreen { if component.isFullscreen || environment.inputHeight > 0.0 {
topOffsetFraction = 1.0 topOffsetFraction = 1.0
} }
#if DEBUG// && false // #if DEBUG// && false
if "".isEmpty { // if "".isEmpty {
topOffsetFraction = 1.0 // topOffsetFraction = 1.0
} // }
#endif // #endif
let minScale: CGFloat = itemLayout.isTablet ? 1.0 : (itemLayout.containerSize.width - 6.0 * 2.0) / itemLayout.containerSize.width 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 minScaledTranslation: CGFloat = itemLayout.isTablet ? 0.0 : (itemLayout.containerSize.height - itemLayout.containerSize.height * minScale) * 0.5 - 6.0
let minScaledCornerRadius: CGFloat = itemLayout.containerCornerRadius let minScaledCornerRadius: CGFloat = itemLayout.containerCornerRadius
let scale = minScale * (1.0 - topOffsetFraction) + 1.0 * topOffsetFraction let scale = minScale * (1.0 - topOffsetFraction) + 1.0 * topOffsetFraction
let scaledTranslation = minScaledTranslation * (1.0 - topOffsetFraction) let scaledTranslation = minScaledTranslation * (1.0 - topOffsetFraction)
let scaledCornerRadius = minScaledCornerRadius * (1.0 - topOffsetFraction) + itemLayout.containerCornerRadius * topOffsetFraction let scaledCornerRadius = minScaledCornerRadius * (1.0 - topOffsetFraction) + itemLayout.containerCornerRadius * topOffsetFraction
var containerTransform = CATransform3DIdentity var containerTransform = CATransform3DIdentity
containerTransform = CATransform3DTranslate(containerTransform, 0.0, scaledTranslation, 0.0) containerTransform = CATransform3DTranslate(containerTransform, 0.0, scaledTranslation, 0.0)
containerTransform = CATransform3DScale(containerTransform, scale, scale, scale) containerTransform = CATransform3DScale(containerTransform, scale, scale, scale)
containerTransform = CATransform3DTranslate(containerTransform, 0.0, self.dismissTranslation, 0.0) containerTransform = CATransform3DTranslate(containerTransform, 0.0, self.dismissTranslation, 0.0)
transition.setTransform(view: self.containerView, transform: containerTransform) transition.setTransform(view: self.containerView, transform: containerTransform)
transition.setCornerRadius(layer: self.containerView.layer, cornerRadius: scaledCornerRadius) transition.setCornerRadius(layer: self.containerView.layer, cornerRadius: scaledCornerRadius)
if component.isFullscreen { if component.isFullscreen {
transition.setBounds(view: self.scrollView, bounds: CGRect(origin: .zero, size: self.scrollView.bounds.size)) transition.setBounds(view: self.scrollView, bounds: CGRect(origin: .zero, size: self.scrollView.bounds.size))
self.scrollView.isScrollEnabled = false self.scrollView.isScrollEnabled = false
} else { } else {
self.scrollView.isScrollEnabled = !self.isDismissingInteractively self.scrollView.isScrollEnabled = !self.isDismissingInteractively
} }
var bounds = self.scrollView.bounds var bounds = self.scrollView.bounds
bounds.size.width = itemLayout.fillingSize bounds.size.width = itemLayout.fillingSize
self.environment?.boundsUpdated.invoke(ResizableSheetComponentEnvironment.BoundsUpdate(bounds: bounds, isInteractive: self.scrollView.isTracking)) self.environment?.boundsUpdated.invoke(ResizableSheetComponentEnvironment.BoundsUpdate(bounds: bounds, isInteractive: self.scrollView.isTracking))
self.updateTrackedScrollViewLock()
} }
private var didPlayAppearanceAnimation = false private var didPlayAppearanceAnimation = false
func animateIn() { func animateIn() {
self.didPlayAppearanceAnimation = true self.didPlayAppearanceAnimation = true
self.dimView.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.3) self.dimView.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.3)
let animateOffset: CGFloat = self.bounds.height - self.backgroundLayer.frame.minY 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) 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) { func animateOut(initialVelocity: CGFloat? = nil, completion: @escaping () -> Void) {
let animateOffset: CGFloat = self.bounds.height - self.backgroundLayer.frame.minY 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) self.dimView.layer.animateAlpha(from: self.dimView.alpha, to: 0.0, duration: 0.3, removeOnCompletion: false)
if let initialVelocity = initialVelocity { if let initialVelocity = initialVelocity {
let transition = ContainedViewLayoutTransition.animated(duration: 0.35, curve: .customSpring(damping: 124.0, 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 transition.updatePosition(layer: self.containerView.layer, position: CGPoint(x: self.containerView.layer.position.x, y: self.containerView.layer.position.y + animateOffset), completion: { _ in
completion() 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 { func update(component: ResizableSheetComponent<ChildEnvironmentType>, availableSize: CGSize, state: EmptyComponentState, environment: Environment<EnvironmentType>, transition: ComponentTransition) -> CGSize {
self.isUpdating = true self.isUpdating = true
defer { defer {
self.isUpdating = false self.isUpdating = false
} }
let sheetEnvironment = environment[ResizableSheetComponentEnvironment.self].value let sheetEnvironment = environment[ResizableSheetComponentEnvironment.self].value
component.animateOut.connect { [weak self] completion in component.animateOut.connect { [weak self] completion in
guard let self else { guard let self else {
@ -541,9 +661,9 @@ public final class ResizableSheetComponent<ChildEnvironmentType: Sendable & Equa
completion(Void()) completion(Void())
} }
} }
let resetScrolling = self.scrollView.bounds.width != availableSize.width let resetScrolling = self.scrollView.bounds.width != availableSize.width
let fillingSize: CGFloat let fillingSize: CGFloat
if case .regular = sheetEnvironment.metrics.widthClass { if case .regular = sheetEnvironment.metrics.widthClass {
fillingSize = min(availableSize.width, 414.0) - sheetEnvironment.safeInsets.left * 2.0 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.component = component
self.state = state self.state = state
self.environment = sheetEnvironment 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 let backgroundColor: UIColor
switch component.backgroundColor { switch component.backgroundColor {
case let .color(color): case let .color(color):
backgroundColor = color backgroundColor = color
self.backgroundLayer.backgroundColor = backgroundColor.cgColor self.backgroundLayer.backgroundColor = backgroundColor.cgColor
} }
transition.setFrame(view: self.dimView, frame: CGRect(origin: CGPoint(), size: availableSize)) transition.setFrame(view: self.dimView, frame: CGRect(origin: CGPoint(), size: availableSize))
var containerSize: CGSize var containerSize: CGSize
if !"".isEmpty, sheetEnvironment.isCentered { if !"".isEmpty, sheetEnvironment.isCentered {
let verticalInset: CGFloat = 44.0 let verticalInset: CGFloat = 44.0
@ -579,13 +711,13 @@ public final class ResizableSheetComponent<ChildEnvironmentType: Sendable & Equa
} else { } else {
containerSize = CGSize(width: fillingSize, height: .greatestFiniteMagnitude) containerSize = CGSize(width: fillingSize, height: .greatestFiniteMagnitude)
} }
var containerInset: CGFloat = sheetEnvironment.statusBarHeight + 10.0 var containerInset: CGFloat = sheetEnvironment.statusBarHeight + 10.0
if component.isFullscreen { if component.isFullscreen {
containerInset = 0.0 containerInset = 0.0
} }
let clippingY: CGFloat let clippingY: CGFloat
self.contentView.parentState = state self.contentView.parentState = state
let contentViewSize = self.contentView.update( let contentViewSize = self.contentView.update(
transition: transition, transition: transition,
@ -596,17 +728,24 @@ public final class ResizableSheetComponent<ChildEnvironmentType: Sendable & Equa
containerSize: containerSize containerSize: containerSize
) )
component.externalState?.contentHeight = contentViewSize.height component.externalState?.contentHeight = contentViewSize.height
if let contentView = self.contentView.view { if let contentView = self.contentView.view {
if contentView.superview == nil { if contentView.superview == nil {
self.scrollContentView.addSubview(contentView) self.scrollContentView.addSubview(contentView)
} }
transition.setFrame(view: contentView, frame: CGRect(origin: CGPoint(x: rawSideInset, y: 0.0), size: contentViewSize)) transition.setFrame(view: contentView, frame: CGRect(origin: CGPoint(x: rawSideInset, y: 0.0), size: contentViewSize))
} }
let contentHeight = contentViewSize.height 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 edgeEffectHeight: CGFloat = 80.0
let edgeEffectFrame = CGRect(origin: CGPoint(x: rawSideInset, y: 0.0), size: CGSize(width: fillingSize, height: edgeEffectHeight)) let edgeEffectFrame = CGRect(origin: CGPoint(x: rawSideInset, y: 0.0), size: CGSize(width: fillingSize, height: edgeEffectHeight))
transition.setFrame(view: self.topEdgeEffectView, frame: edgeEffectFrame) 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.navigationBarContainer.insertSubview(self.topEdgeEffectView, at: 0)
} }
self.topEdgeEffectView.isHidden = !component.hasTopEdgeEffect self.topEdgeEffectView.isHidden = !component.hasTopEdgeEffect
if let titleItem = component.titleItem { if let titleItem = component.titleItem {
let titleItemView: ComponentView<Empty> let titleItemView: ComponentView<Empty>
if let current = self.titleItemView { if let current = self.titleItemView {
@ -624,12 +763,12 @@ public final class ResizableSheetComponent<ChildEnvironmentType: Sendable & Equa
titleItemView = ComponentView<Empty>() titleItemView = ComponentView<Empty>()
self.titleItemView = titleItemView self.titleItemView = titleItemView
} }
let titleItemSize = titleItemView.update( let titleItemSize = titleItemView.update(
transition: transition, transition: transition,
component: titleItem, component: titleItem,
environment: {}, 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) 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 { if let view = titleItemView.view {
@ -642,7 +781,7 @@ public final class ResizableSheetComponent<ChildEnvironmentType: Sendable & Equa
self.titleItemView = nil self.titleItemView = nil
titleItemView.view?.removeFromSuperview() titleItemView.view?.removeFromSuperview()
} }
if let leftItem = component.leftItem { if let leftItem = component.leftItem {
var leftItemTransition = transition var leftItemTransition = transition
let leftItemView: ComponentView<Empty> let leftItemView: ComponentView<Empty>
@ -653,7 +792,7 @@ public final class ResizableSheetComponent<ChildEnvironmentType: Sendable & Equa
leftItemView = ComponentView<Empty>() leftItemView = ComponentView<Empty>()
self.leftItemView = leftItemView self.leftItemView = leftItemView
} }
let leftItemSize = leftItemView.update( let leftItemSize = leftItemView.update(
transition: leftItemTransition, transition: leftItemTransition,
component: leftItem, component: leftItem,
@ -664,7 +803,7 @@ public final class ResizableSheetComponent<ChildEnvironmentType: Sendable & Equa
if let view = leftItemView.view { if let view = leftItemView.view {
if view.superview == nil { if view.superview == nil {
self.navigationBarContainer.addSubview(view) self.navigationBarContainer.addSubview(view)
if !transition.animation.isImmediate { if !transition.animation.isImmediate {
view.layer.animateScale(from: 0.01, to: 1.0, duration: 0.25) view.layer.animateScale(from: 0.01, to: 1.0, duration: 0.25)
view.layer.animateAlpha(from: 0.0, 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() leftItemView.view?.removeFromSuperview()
} }
} }
if let rightItem = component.rightItem { if let rightItem = component.rightItem {
var rightItemTransition = transition var rightItemTransition = transition
let rightItemView: ComponentView<Empty> let rightItemView: ComponentView<Empty>
@ -694,7 +833,7 @@ public final class ResizableSheetComponent<ChildEnvironmentType: Sendable & Equa
rightItemView = ComponentView<Empty>() rightItemView = ComponentView<Empty>()
self.rightItemView = rightItemView self.rightItemView = rightItemView
} }
let rightItemSize = rightItemView.update( let rightItemSize = rightItemView.update(
transition: rightItemTransition, transition: rightItemTransition,
component: rightItem, component: rightItem,
@ -705,7 +844,7 @@ public final class ResizableSheetComponent<ChildEnvironmentType: Sendable & Equa
if let view = rightItemView.view { if let view = rightItemView.view {
if view.superview == nil { if view.superview == nil {
self.navigationBarContainer.addSubview(view) self.navigationBarContainer.addSubview(view)
if !transition.animation.isImmediate { if !transition.animation.isImmediate {
view.layer.animateScale(from: 0.01, to: 1.0, duration: 0.25) view.layer.animateScale(from: 0.01, to: 1.0, duration: 0.25)
view.layer.animateAlpha(from: 0.0, 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() rightItemView.view?.removeFromSuperview()
} }
} }
var bottomInsets = ContainerViewLayout.concentricInsets(bottomInset: sheetEnvironment.safeInsets.bottom, innerDiameter: 52.0, sideInset: 30.0) var bottomInsets = ContainerViewLayout.concentricInsets(bottomInset: sheetEnvironment.safeInsets.bottom, innerDiameter: 52.0, sideInset: 30.0)
if sheetEnvironment.inputHeight > 0.0 { if sheetEnvironment.inputHeight > 0.0 {
bottomInsets.left = 16.0 bottomInsets.left = 16.0
bottomInsets.right = 16.0 bottomInsets.right = 16.0
bottomInsets.bottom = sheetEnvironment.inputHeight + 8.0 bottomInsets.bottom = sheetEnvironment.inputHeight + 8.0
} }
var bottomEdgeEffectHeight = edgeEffectHeight
if let bottomItem = component.bottomItem { if let bottomItem = component.bottomItem {
var bottomItemTransition = transition var bottomItemTransition = transition
let bottomItemView: ComponentView<Empty> let bottomItemView: ComponentView<Empty>
@ -742,7 +882,7 @@ public final class ResizableSheetComponent<ChildEnvironmentType: Sendable & Equa
bottomItemView = ComponentView<Empty>() bottomItemView = ComponentView<Empty>()
self.bottomItemView = bottomItemView self.bottomItemView = bottomItemView
} }
let bottomItemSize = bottomItemView.update( let bottomItemSize = bottomItemView.update(
transition: bottomItemTransition, transition: bottomItemTransition,
component: bottomItem, component: bottomItem,
@ -753,7 +893,7 @@ public final class ResizableSheetComponent<ChildEnvironmentType: Sendable & Equa
if let view = bottomItemView.view { if let view = bottomItemView.view {
if view.superview == nil { if view.superview == nil {
self.bottomContainer.addSubview(view) self.bottomContainer.addSubview(view)
if !transition.animation.isImmediate { if !transition.animation.isImmediate {
view.layer.animateScale(from: 0.01, to: 1.0, duration: 0.25) view.layer.animateScale(from: 0.01, to: 1.0, duration: 0.25)
view.layer.animateAlpha(from: 0.0, 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) bottomItemTransition.setFrame(view: view, frame: bottomItemFrame)
} }
bottomEdgeEffectHeight = bottomItemSize.height + 36.0
} else if let bottomItemView = self.bottomItemView { } else if let bottomItemView = self.bottomItemView {
self.bottomItemView = nil self.bottomItemView = nil
if !transition.animation.isImmediate { if !transition.animation.isImmediate {
@ -772,16 +913,16 @@ public final class ResizableSheetComponent<ChildEnvironmentType: Sendable & Equa
bottomItemView.view?.removeFromSuperview() 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) 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 { if self.bottomEdgeEffectView.superview == nil {
self.bottomContainer.insertSubview(self.bottomEdgeEffectView, at: 0) self.bottomContainer.insertSubview(self.bottomEdgeEffectView, at: 0)
} }
transition.setAlpha(view: self.bottomContainer, alpha: component.bottomItem != nil ? 1.0 : 0.0) transition.setAlpha(view: self.bottomContainer, alpha: component.bottomItem != nil ? 1.0 : 0.0)
clippingY = availableSize.height clippingY = availableSize.height
var topInset: CGFloat = max(0.0, availableSize.height - containerInset - initialContentHeight - sheetEnvironment.inputHeight) 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) let scrollContentHeight = max(topInset + contentHeight + containerInset + sheetEnvironment.inputHeight, availableSize.height - containerInset)
self.scrollContentClippingView.layer.cornerRadius = 38.0 self.scrollContentClippingView.layer.cornerRadius = 38.0
let containerCornerRadius = max(22.0, sheetEnvironment.deviceMetrics.screenCornerRadius) 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) 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.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.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))) 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)) 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.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)) transition.setBounds(view: self.scrollContentClippingView, bounds: CGRect(origin: CGPoint(x: scrollClippingFrame.minX, y: scrollClippingFrame.minY), size: scrollClippingFrame.size))
self.ignoreScrolling = true 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))) 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) let contentSize = CGSize(width: availableSize.width, height: scrollContentHeight)
@ -816,22 +957,22 @@ public final class ResizableSheetComponent<ChildEnvironmentType: Sendable & Equa
} }
self.ignoreScrolling = false self.ignoreScrolling = false
self.updateScrolling(transition: transition) self.updateScrolling(transition: transition)
transition.setPosition(view: self.containerView, position: CGRect(origin: CGPoint(), size: availableSize).center) transition.setPosition(view: self.containerView, position: CGRect(origin: CGPoint(), size: availableSize).center)
transition.setBounds(view: self.containerView, bounds: CGRect(origin: CGPoint(), size: availableSize)) transition.setBounds(view: self.containerView, bounds: CGRect(origin: CGPoint(), size: availableSize))
if sheetEnvironment.isDisplaying && !self.didPlayAppearanceAnimation { if sheetEnvironment.isDisplaying && !self.didPlayAppearanceAnimation {
self.animateIn() self.animateIn()
} }
return availableSize return availableSize
} }
} }
public func makeView() -> View { public func makeView() -> View {
return View(frame: CGRect()) return View(frame: CGRect())
} }
public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment<EnvironmentType>, transition: ComponentTransition) -> CGSize { 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) return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition)
} }

View file

@ -602,6 +602,7 @@ public enum ContextControllerTip: Equatable {
case starsReactions(topCount: Int) case starsReactions(topCount: Int)
case videoProcessing case videoProcessing
case collageReordering case collageReordering
case deleteReaction
public static func ==(lhs: ContextControllerTip, rhs: ContextControllerTip) -> Bool { public static func ==(lhs: ContextControllerTip, rhs: ContextControllerTip) -> Bool {
switch lhs { switch lhs {
@ -611,6 +612,12 @@ public enum ContextControllerTip: Equatable {
} else { } else {
return false return false
} }
case .deleteReaction:
if case .deleteReaction = rhs {
return true
} else {
return false
}
case .quoteSelection: case .quoteSelection:
if case .quoteSelection = rhs { if case .quoteSelection = rhs {
return true return true

View file

@ -9,7 +9,7 @@ import TelegramUIPreferences
import AccountContext import AccountContext
import ContextUI import ContextUI
public final class InstantPageContentNode : ASDisplayNode { public final class InstantPageContentNode : ASDisplayNode, InstantPageExternalMediaDimensionsNode {
private let context: AccountContext private let context: AccountContext
private let strings: PresentationStrings private let strings: PresentationStrings
private let nameDisplayOrder: PresentationPersonNameOrder private let nameDisplayOrder: PresentationPersonNameOrder
@ -29,13 +29,20 @@ public final class InstantPageContentNode : ASDisplayNode {
var distanceThresholdGroupCount: [Int: Int] = [:] var distanceThresholdGroupCount: [Int: Int] = [:]
var visibleTiles: [Int: InstantPageTileNode] = [:] var visibleTiles: [Int: InstantPageTileNode] = [:]
var visibleItemsWithNodes: [Int: InstantPageNode] = [:] public var visibleItemsWithNodes: [Int: InstantPageNode] = [:]
var currentWebEmbedHeights: [Int : CGFloat] = [:] var currentWebEmbedHeights: [Int : CGFloat] = [:]
var currentExpandedDetails: [Int : Bool]? public var currentExpandedDetails: [Int : Bool]?
var currentDetailsItems: [InstantPageDetailsItem] = [] var currentDetailsItems: [InstantPageDetailsItem] = []
var requestLayoutUpdate: ((Bool) -> Void)? var requestLayoutUpdate: ((Bool) -> Void)?
public var updateExternalMediaDimensions: ((EngineMedia.Id, PixelDimensions) -> Void)? {
didSet {
for (_, itemNode) in self.visibleItemsWithNodes {
self.applyExternalMediaDimensionsUpdater(to: itemNode)
}
}
}
var currentLayout: InstantPageLayout var currentLayout: InstantPageLayout
let contentSize: CGSize let contentSize: CGSize
@ -223,6 +230,7 @@ public final class InstantPageContentNode : ASDisplayNode {
topNode = newNode topNode = newNode
self.visibleItemsWithNodes[itemIndex] = newNode self.visibleItemsWithNodes[itemIndex] = newNode
itemNode = newNode itemNode = newNode
self.applyExternalMediaDimensionsUpdater(to: newNode)
if let itemNode = itemNode as? InstantPageDetailsNode { if let itemNode = itemNode as? InstantPageDetailsNode {
itemNode.requestLayoutUpdate = { [weak self] animated in 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) { private func updateWebEmbedHeight(_ index: Int, _ height: CGFloat) {
// let currentHeight = self.currentWebEmbedHeights[index] // let currentHeight = self.currentWebEmbedHeights[index]
// if height != currentHeight { // if height != currentHeight {

View file

@ -65,6 +65,8 @@ final class InstantPageControllerNode: ASDisplayNode, ASScrollViewDelegate {
var currentWebEmbedHeights: [Int : CGFloat] = [:] var currentWebEmbedHeights: [Int : CGFloat] = [:]
var currentExpandedDetails: [Int : Bool]? var currentExpandedDetails: [Int : Bool]?
var currentDetailsItems: [InstantPageDetailsItem] = [] var currentDetailsItems: [InstantPageDetailsItem] = []
private var resolvedExternalMediaDimensions: [MediaId: PixelDimensions] = [:]
private var pendingResolvedExternalMediaDimensions = Set<MediaId>()
var currentAccessibilityAreas: [AccessibilityAreaNode] = [] var currentAccessibilityAreas: [AccessibilityAreaNode] = []
@ -79,6 +81,7 @@ final class InstantPageControllerNode: ASDisplayNode, ASScrollViewDelegate {
private let loadProgressDisposable = MetaDisposable() private let loadProgressDisposable = MetaDisposable()
private let updateLayoutDisposable = MetaDisposable() private let updateLayoutDisposable = MetaDisposable()
private let updateExternalMediaDimensionsDisposable = MetaDisposable()
private var themeReferenceDate: Date? private var themeReferenceDate: Date?
@ -224,6 +227,7 @@ final class InstantPageControllerNode: ASDisplayNode, ASScrollViewDelegate {
self.resolveUrlDisposable.dispose() self.resolveUrlDisposable.dispose()
self.loadWebpageDisposable.dispose() self.loadWebpageDisposable.dispose()
self.loadProgressDisposable.dispose() self.loadProgressDisposable.dispose()
self.updateExternalMediaDimensionsDisposable.dispose()
} }
func update(settings: InstantPagePresentationSettings, themeSettings: PresentationThemeSettings?, strings: PresentationStrings) { func update(settings: InstantPagePresentationSettings, themeSettings: PresentationThemeSettings?, strings: PresentationStrings) {
@ -362,6 +366,8 @@ final class InstantPageControllerNode: ASDisplayNode, ASScrollViewDelegate {
} else { } else {
self.webPage = nil self.webPage = nil
} }
self.resolvedExternalMediaDimensions.removeAll()
self.pendingResolvedExternalMediaDimensions.removeAll()
if let anchor = anchor { if let anchor = anchor {
self.initialAnchor = anchor.removingPercentEncoding self.initialAnchor = anchor.removingPercentEncoding
} else if let state = state { } else if let state = state {
@ -465,17 +471,12 @@ final class InstantPageControllerNode: ASDisplayNode, ASScrollViewDelegate {
} }
private func updateLayout() { 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 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) 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) let currentLayoutTiles = instantPageTilesFromLayout(currentLayout, boundingWidth: containerLayout.size.width)
var currentDetailsItems: [InstantPageDetailsItem] = [] var currentDetailsItems: [InstantPageDetailsItem] = []
@ -665,6 +666,7 @@ final class InstantPageControllerNode: ASDisplayNode, ASScrollViewDelegate {
topNode = newNode topNode = newNode
self.visibleItemsWithNodes[itemIndex] = newNode self.visibleItemsWithNodes[itemIndex] = newNode
itemNode = newNode itemNode = newNode
self.configureExternalMediaDimensionsUpdates(for: newNode)
if let itemNode = itemNode as? InstantPageDetailsNode { if let itemNode = itemNode as? InstantPageDetailsNode {
itemNode.requestLayoutUpdate = { [weak self] animated in 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 { if let itemNode = itemNode as? InstantPageDetailsNode {
itemNode.updateVisibleItems(visibleBounds: visibleBounds.offsetBy(dx: -itemNode.frame.minX, dy: -itemNode.frame.minY), animated: animated) 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 topNode = tileNode
self.visibleTiles[tileIndex] = tileNode self.visibleTiles[tileIndex] = tileNode
} else { } else {
if visibleTiles[tileIndex]!.frame != tileFrame { if let tileNode = self.visibleTiles[tileIndex] {
transition.updateFrame(node: self.visibleTiles[tileIndex]!, frame: tileFrame) 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 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) { 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 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 { if let strongSelf = self, case let .image(image) = media.media {

View file

@ -54,7 +54,7 @@ public final class InstantPageImageItem: InstantPageItem {
public func matchesNode(_ node: InstantPageNode) -> Bool { public func matchesNode(_ node: InstantPageNode) -> Bool {
if let node = node as? InstantPageImageNode { if let node = node as? InstantPageImageNode {
return node.media == self.media return instantPageMediaMatchesNodeIdentity(node.media, self.media)
} else { } else {
return false return false
} }

View file

@ -1,5 +1,6 @@
import Foundation import Foundation
import UIKit import UIKit
import ImageIO
import AsyncDisplayKit import AsyncDisplayKit
import Display import Display
import TelegramCore import TelegramCore
@ -21,7 +22,35 @@ private struct FetchControls {
let cancel: () -> Void 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 context: AccountContext
private let webPage: TelegramMediaWebpage private let webPage: TelegramMediaWebpage
private var theme: InstantPageTheme private var theme: InstantPageTheme
@ -32,8 +61,13 @@ final class InstantPageImageNode: ASDisplayNode, InstantPageNode {
private let fit: Bool private let fit: Bool
private let openMedia: (InstantPageMedia) -> Void private let openMedia: (InstantPageMedia) -> Void
private let longPressMedia: (InstantPageMedia) -> Void private let longPressMedia: (InstantPageMedia) -> Void
private let getPreloadedResource: (String) -> Data?
private var fetchControls: FetchControls? 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 pinchContainerNode: PinchSourceContainerNode
private let imageNode: TransformImageNode private let imageNode: TransformImageNode
@ -48,6 +82,7 @@ final class InstantPageImageNode: ASDisplayNode, InstantPageNode {
private var statusDisposable = MetaDisposable() private var statusDisposable = MetaDisposable()
private var themeUpdated: Bool = false 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?) { 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 self.context = context
@ -60,6 +95,7 @@ final class InstantPageImageNode: ASDisplayNode, InstantPageNode {
self.fit = fit self.fit = fit
self.openMedia = openMedia self.openMedia = openMedia
self.longPressMedia = longPressMedia self.longPressMedia = longPressMedia
self.getPreloadedResource = getPreloadedResource
self.pinchContainerNode = PinchSourceContainerNode() self.pinchContainerNode = PinchSourceContainerNode()
self.imageNode = TransformImageNode() self.imageNode = TransformImageNode()
@ -72,33 +108,14 @@ final class InstantPageImageNode: ASDisplayNode, InstantPageNode {
self.pinchContainerNode.contentNode.addSubnode(self.imageNode) self.pinchContainerNode.contentNode.addSubnode(self.imageNode)
self.addSubnode(self.pinchContainerNode) 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 case let .image(image) = media.media, let largest = largestImageRepresentation(image.representations) {
if let externalResource = largest.resource as? InstantPageExternalMediaResource { if let externalResource = largest.resource as? InstantPageExternalMediaResource {
var url = externalResource.url self.loadExternalImage(resourceUrl: 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
})
} else { } else {
let imageReference = ImageMediaReference.webPage(webPage: WebpageReference(webPage), media: image) let imageReference = ImageMediaReference.webPage(webPage: WebpageReference(webPage), media: image)
self.imageNode.setSignal(chatMessagePhoto(postbox: context.account.postbox, userLocation: sourceLocation.userLocation, photoReference: imageReference)) 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) self.pinchContainerNode.contentNode.addSubnode(self.statusNode)
} }
} }
} else if case let .file(file) = media.media { } else if case let .file(file) = media.media {
if let externalResource = file.resource as? InstantPageExternalMediaResource { if let externalResource = file.resource as? InstantPageExternalMediaResource {
let photoData: Signal<Tuple4<Data?, Data?, ChatMessagePhotoQuality, Bool>, NoError> self.loadExternalImage(resourceUrl: externalResource.url)
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
})
} else { } else {
let fileReference = FileMediaReference.webPage(webPage: WebpageReference(webPage), media: file) let fileReference = FileMediaReference.webPage(webPage: WebpageReference(webPage), media: file)
if file.mimeType.hasPrefix("image/") { 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() { private func updateFetchStatus() {
var state: RadialStatusNodeState = .none var state: RadialStatusNodeState = .none
if let fetchStatus = self.fetchStatus { if let fetchStatus = self.fetchStatus {
@ -260,19 +360,20 @@ final class InstantPageImageNode: ASDisplayNode, InstantPageNode {
let size = self.bounds.size let size = self.bounds.size
if self.currentSize != size || self.themeUpdated { if self.currentSize != size || self.themeUpdated || self.externalMediaDimensionsUpdated {
self.currentSize = size self.currentSize = size
self.themeUpdated = false self.themeUpdated = false
self.externalMediaDimensionsUpdated = false
self.pinchContainerNode.frame = CGRect(origin: CGPoint(), size: size) self.pinchContainerNode.frame = CGRect(origin: CGPoint(), size: size)
self.pinchContainerNode.update(size: size, transition: .immediate) self.pinchContainerNode.update(size: size, transition: .immediate)
self.imageNode.frame = CGRect(origin: CGPoint(), size: size) 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) 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) { if case .image = self.media.media, let dimensions = self.effectiveMediaDimensions() {
let imageSize = largest.dimensions.cgSize.aspectFilled(size) let imageSize = dimensions.cgSize.aspectFilled(size)
let boundingSize = size let boundingSize = size
let radius: CGFloat = self.roundCorners ? floor(min(imageSize.width, imageSize.height) / 2.0) : 0.0 let radius: CGFloat = self.roundCorners ? floor(min(imageSize.width, imageSize.height) / 2.0) : 0.0
let makeLayout = self.imageNode.asyncLayout() let makeLayout = self.imageNode.asyncLayout()
@ -280,7 +381,7 @@ final class InstantPageImageNode: ASDisplayNode, InstantPageNode {
apply() apply()
self.linkIconNode.frame = CGRect(x: size.width - 38.0, y: 14.0, width: 24.0, height: 24.0) 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 emptyColor = file.mimeType.hasPrefix("image/") ? self.theme.imageTintColor : nil
let imageSize = dimensions.cgSize.aspectFilled(size) let imageSize = dimensions.cgSize.aspectFilled(size)
@ -318,7 +419,7 @@ final class InstantPageImageNode: ASDisplayNode, InstantPageNode {
} }
func transitionNode(media: InstantPageMedia) -> (ASDisplayNode, CGRect, () -> (UIView?, UIView?))? { func transitionNode(media: InstantPageMedia) -> (ASDisplayNode, CGRect, () -> (UIView?, UIView?))? {
if media == self.media { if instantPageMediaMatchesNodeIdentity(media, self.media) {
let imageNode = self.imageNode let imageNode = self.imageNode
return (self.imageNode, self.imageNode.bounds, { [weak imageNode] in return (self.imageNode, self.imageNode.bounds, { [weak imageNode] in
return (imageNode?.view.snapshotContentTree(unhide: true), nil) return (imageNode?.view.snapshotContentTree(unhide: true), nil)
@ -329,14 +430,32 @@ final class InstantPageImageNode: ASDisplayNode, InstantPageNode {
} }
func updateHiddenMedia(media: InstantPageMedia?) { 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.statusNode.isHidden = self.imageNode.isHidden
self.linkIconNode.isHidden = self.imageNode.isHidden
} }
@objc private func tapGesture(_ recognizer: TapLongTapOrDoubleTapGestureRecognizer) { @objc private func tapGesture(_ recognizer: TapLongTapOrDoubleTapGestureRecognizer) {
switch recognizer.state { switch recognizer.state {
case .ended: case .ended:
if let (gesture, _) = recognizer.lastRecognizedGestureAndLocation { 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 { if let fetchStatus = self.fetchStatus {
switch fetchStatus { switch fetchStatus {
case .Local: case .Local:

View file

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

View file

@ -2,8 +2,13 @@ import Foundation
import UIKit import UIKit
import AsyncDisplayKit import AsyncDisplayKit
import Display import Display
import TelegramCore
import TelegramPresentationData import TelegramPresentationData
public protocol InstantPageExternalMediaDimensionsNode: AnyObject {
var updateExternalMediaDimensions: ((EngineMedia.Id, PixelDimensions) -> Void)? { get set }
}
public protocol InstantPageNode: ASDisplayNode { public protocol InstantPageNode: ASDisplayNode {
func updateIsVisible(_ isVisible: Bool) func updateIsVisible(_ isVisible: Bool)

View file

@ -30,7 +30,7 @@ final class InstantPageSlideshowItem: InstantPageItem {
func matchesNode(_ node: InstantPageNode) -> Bool { func matchesNode(_ node: InstantPageNode) -> Bool {
if let node = node as? InstantPageSlideshowNode { if let node = node as? InstantPageSlideshowNode {
return self.medias == node.medias return instantPageMediaArraysMatchNodeIdentity(self.medias, node.medias)
} else { } else {
return false return false
} }
@ -55,4 +55,3 @@ final class InstantPageSlideshowItem: InstantPageItem {
func drawInTile(context: CGContext) { func drawInTile(context: CGContext) {
} }
} }

View file

@ -61,6 +61,12 @@ private final class InstantPageSlideshowItemNode: ASDisplayNode {
} }
return nil 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 { private final class InstantPageSlideshowPagerNode: ASDisplayNode, ASScrollViewDelegate {
@ -88,6 +94,13 @@ private final class InstantPageSlideshowPagerNode: ASDisplayNode, ASScrollViewDe
} }
private var containerLayout: ContainerViewLayout? 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 } var centralItemIndexUpdated: (Int?) -> Void = { _ in }
@ -195,6 +208,7 @@ private final class InstantPageSlideshowPagerNode: ASDisplayNode, ASScrollViewDe
} }
let node = InstantPageSlideshowItemNode(contentNode: contentNode) let node = InstantPageSlideshowItemNode(contentNode: contentNode)
node.updateExternalMediaDimensions(self.updateExternalMediaDimensions)
node.index = index node.index = index
return node 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 medias: [InstantPageMedia] = []
var updateExternalMediaDimensions: ((EngineMedia.Id, PixelDimensions) -> Void)? {
didSet {
self.pagerNode.updateExternalMediaDimensions = self.updateExternalMediaDimensions
}
}
private let pagerNode: InstantPageSlideshowPagerNode private let pagerNode: InstantPageSlideshowPagerNode
private let pageControlNode: PageControlNode private let pageControlNode: PageControlNode

View file

@ -8,7 +8,7 @@ import TelegramPresentationData
import TelegramUIPreferences import TelegramUIPreferences
import AccountContext import AccountContext
final class InstantPageSubContentNode : ASDisplayNode { final class InstantPageSubContentNode : ASDisplayNode, InstantPageExternalMediaDimensionsNode {
private let context: AccountContext private let context: AccountContext
private let strings: PresentationStrings private let strings: PresentationStrings
private let nameDisplayOrder: PresentationPersonNameOrder private let nameDisplayOrder: PresentationPersonNameOrder
@ -32,6 +32,13 @@ final class InstantPageSubContentNode : ASDisplayNode {
var currentDetailsItems: [InstantPageDetailsItem] = [] var currentDetailsItems: [InstantPageDetailsItem] = []
var requestLayoutUpdate: ((Bool) -> Void)? var requestLayoutUpdate: ((Bool) -> Void)?
var updateExternalMediaDimensions: ((EngineMedia.Id, PixelDimensions) -> Void)? {
didSet {
for (_, itemNode) in self.visibleItemsWithNodes {
self.applyExternalMediaDimensionsUpdater(to: itemNode)
}
}
}
var currentLayout: InstantPageLayout var currentLayout: InstantPageLayout
let contentSize: CGSize let contentSize: CGSize
@ -209,6 +216,7 @@ final class InstantPageSubContentNode : ASDisplayNode {
topNode = newNode topNode = newNode
self.visibleItemsWithNodes[itemIndex] = newNode self.visibleItemsWithNodes[itemIndex] = newNode
itemNode = newNode itemNode = newNode
self.applyExternalMediaDimensionsUpdater(to: newNode)
if let itemNode = itemNode as? InstantPageDetailsNode { if let itemNode = itemNode as? InstantPageDetailsNode {
itemNode.requestLayoutUpdate = { [weak self] animated in 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) { private func updateWebEmbedHeight(_ index: Int, _ height: CGFloat) {
// let currentHeight = self.currentWebEmbedHeights[index] // let currentHeight = self.currentWebEmbedHeights[index]
// if height != currentHeight { // if height != currentHeight {

View file

@ -15,10 +15,12 @@ private final class InstantPageTileNodeParameters: NSObject {
} }
public final class InstantPageTileNode: ASDisplayNode { public final class InstantPageTileNode: ASDisplayNode {
private let tile: InstantPageTile private var tile: InstantPageTile
private var tileBackgroundColor: UIColor
public init(tile: InstantPageTile, backgroundColor: UIColor) { public init(tile: InstantPageTile, backgroundColor: UIColor) {
self.tile = tile self.tile = tile
self.tileBackgroundColor = backgroundColor
super.init() super.init()
@ -27,8 +29,15 @@ public final class InstantPageTileNode: ASDisplayNode {
self.backgroundColor = backgroundColor 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? { 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) { @objc override public class func draw(_ bounds: CGRect, withParameters parameters: Any?, isCancelled: () -> Bool, isRasterizing: Bool) {

View file

@ -48,6 +48,7 @@ swift_library(
"//submodules/Components/BundleIconComponent", "//submodules/Components/BundleIconComponent",
"//submodules/TelegramUI/Components/GlassBackgroundComponent", "//submodules/TelegramUI/Components/GlassBackgroundComponent",
"//submodules/TelegramUI/Components/GlassBarButtonComponent", "//submodules/TelegramUI/Components/GlassBarButtonComponent",
"//submodules/TelegramUI/Components/GlassControls",
"//submodules/TelegramUI/Components/EdgeEffect", "//submodules/TelegramUI/Components/EdgeEffect",
"//submodules/TelegramUI/Components/SearchInputPanelComponent", "//submodules/TelegramUI/Components/SearchInputPanelComponent",
"//submodules/TelegramUI/Components/ButtonComponent", "//submodules/TelegramUI/Components/ButtonComponent",

View file

@ -100,7 +100,7 @@ public final class LocationInfoListItemNode: ListViewItemNode {
super.init(layerBacked: false, rotated: false, seeThrough: false) super.init(layerBacked: false, rotated: false, seeThrough: false)
self.addSubnode(self.backgroundNode) //self.addSubnode(self.backgroundNode)
self.addSubnode(self.buttonNode) self.addSubnode(self.buttonNode)
self.addSubnode(self.venueIconNode) self.addSubnode(self.venueIconNode)

View file

@ -57,7 +57,7 @@ public final class LocationMapHeaderNode: ASDisplayNode {
private let options: ComponentView<Empty>? private let options: ComponentView<Empty>?
private let optionsBackgroundView: GlassBackgroundView? private let optionsBackgroundView: GlassContextExtractableContainer?
private let optionsBackgroundNode: ASImageNode private let optionsBackgroundNode: ASImageNode
private let optionsSeparatorNode: ASDisplayNode private let optionsSeparatorNode: ASDisplayNode
private let optionsSecondSeparatorNode: ASDisplayNode private let optionsSecondSeparatorNode: ASDisplayNode
@ -66,7 +66,7 @@ public final class LocationMapHeaderNode: ASDisplayNode {
private let notificationButtonNode: HighlightableButtonNode private let notificationButtonNode: HighlightableButtonNode
private let placesBackgroundView: GlassBackgroundView? private let placesBackgroundView: GlassBackgroundView?
private let placesBackgroundNode: ASImageNode private let placesBackgroundNode: ASImageNode
private let placesButtonNode: HighlightableButtonNode private let placesButtonNode: HighlightTrackingButtonNode
private let shadowNode: ASImageNode private let shadowNode: ASImageNode
private var validLayout: (ContainerViewLayout, CGFloat, CGFloat, CGFloat, CGFloat, CGFloat, CGSize)? 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.image = generateBackgroundImage(theme: presentationData.theme)
self.placesBackgroundNode.isUserInteractionEnabled = true 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.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() self.shadowNode = ASImageNode()
@ -142,7 +142,7 @@ public final class LocationMapHeaderNode: ASDisplayNode {
self.shadowNode.image = generateShadowImage(theme: presentationData.theme, highlighted: false) self.shadowNode.image = generateShadowImage(theme: presentationData.theme, highlighted: false)
if glass { if glass {
self.optionsBackgroundView = GlassBackgroundView() self.optionsBackgroundView = GlassContextExtractableContainer()
self.optionsBackgroundNode.image = nil self.optionsBackgroundNode.image = nil
self.placesBackgroundView = GlassBackgroundView() self.placesBackgroundView = GlassBackgroundView()
@ -169,16 +169,16 @@ public final class LocationMapHeaderNode: ASDisplayNode {
self.view.addSubview(optionsBackgroundView) self.view.addSubview(optionsBackgroundView)
} }
self.addSubnode(self.optionsBackgroundNode) self.addSubnode(self.optionsBackgroundNode)
self.optionsBackgroundNode.addSubnode(self.optionsSeparatorNode) self.optionsBackgroundView?.contentView.addSubview(self.optionsSeparatorNode.view)
self.optionsBackgroundNode.addSubnode(self.optionsSecondSeparatorNode) self.optionsBackgroundView?.contentView.addSubview(self.optionsSecondSeparatorNode.view)
self.optionsBackgroundNode.addSubnode(self.infoButtonNode) self.optionsBackgroundView?.contentView.addSubview(self.infoButtonNode.view)
self.optionsBackgroundNode.addSubnode(self.locationButtonNode) self.optionsBackgroundView?.contentView.addSubview(self.locationButtonNode.view)
self.optionsBackgroundNode.addSubnode(self.notificationButtonNode) self.optionsBackgroundView?.contentView.addSubview(self.notificationButtonNode.view)
} }
} }
self.addSubnode(self.placesBackgroundNode) self.addSubnode(self.placesBackgroundNode)
self.placesBackgroundNode.addSubnode(self.placesButtonNode) self.placesBackgroundView?.contentView.addSubview(self.placesButtonNode.view)
//self.addSubnode(self.shadowNode) //self.addSubnode(self.shadowNode)
self.infoButtonNode.addTarget(self, action: #selector(self.infoPressed), forControlEvents: .touchUpInside) 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 inset: CGFloat = 6.0
let placesButtonSize = CGSize(width: 180.0 + panelInset * 2.0, height: 45.0 + panelInset * 2.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) transition.updateFrame(node: self.placesBackgroundNode, frame: placesButtonFrame)
if let placesBackgroundView = self.placesBackgroundView { 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) 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.placesBackgroundNode, alpha: self.displayingPlacesButton ? 1.0 : 0.0)
transition.updateAlpha(node: self.placesButtonNode, 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 { if let optionsBackgroundView = self.optionsBackgroundView {
let backgroundViewFrame = backgroundFrame.insetBy(dx: 4.0, dy: 4.0) let backgroundViewFrame = backgroundFrame.insetBy(dx: 4.0, dy: 4.0)
transition.updateFrame(view: optionsBackgroundView, frame: backgroundViewFrame) 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) let alphaTransition = ContainedViewLayoutTransition.animated(duration: 0.2, curve: .easeInOut)

View file

@ -11,7 +11,6 @@ import AppBundle
import CoreLocation import CoreLocation
import PresentationDataUtils import PresentationDataUtils
import OpenInExternalAppUI import OpenInExternalAppUI
import DeviceAccess import DeviceAccess
import UndoUI import UndoUI
import MapKit import MapKit
@ -32,12 +31,6 @@ public class LocationViewParams {
} }
} }
enum LocationViewRightBarButton {
case none
case share
case showAll
}
class LocationViewInteraction { class LocationViewInteraction {
let toggleMapModeSelection: () -> Void let toggleMapModeSelection: () -> Void
let updateMapMode: (LocationMapMode) -> Void let updateMapMode: (LocationMapMode) -> Void
@ -49,10 +42,9 @@ class LocationViewInteraction {
let updateSendActionHighlight: (Bool) -> Void let updateSendActionHighlight: (Bool) -> Void
let sendLiveLocation: (Int32?, Bool, EngineMessage.Id?) -> Void let sendLiveLocation: (Int32?, Bool, EngineMessage.Id?) -> Void
let stopLiveLocation: () -> Void let stopLiveLocation: () -> Void
let updateRightBarButton: (LocationViewRightBarButton) -> Void
let present: (ViewController) -> 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.toggleMapModeSelection = toggleMapModeSelection
self.updateMapMode = updateMapMode self.updateMapMode = updateMapMode
self.toggleTrackingMode = toggleTrackingMode self.toggleTrackingMode = toggleTrackingMode
@ -63,7 +55,6 @@ class LocationViewInteraction {
self.updateSendActionHighlight = updateSendActionHighlight self.updateSendActionHighlight = updateSendActionHighlight
self.sendLiveLocation = sendLiveLocation self.sendLiveLocation = sendLiveLocation
self.stopLiveLocation = stopLiveLocation self.stopLiveLocation = stopLiveLocation
self.updateRightBarButton = updateRightBarButton
self.present = present self.present = present
} }
} }
@ -83,9 +74,7 @@ public final class LocationViewController: ViewController {
private let locationManager = LocationManager() private let locationManager = LocationManager()
private var interaction: LocationViewInteraction? private var interaction: LocationViewInteraction?
private var rightBarButtonAction: LocationViewRightBarButton = .none
public var dismissed: () -> Void = {} 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) { 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 } self.presentationData = updatedPresentationData?.initial ?? context.sharedContext.currentPresentationData.with { $0 }
let navigationBarPresentationData: NavigationBarPresentationData? super.init(navigationBarPresentationData: nil)
if !isPreview {
navigationBarPresentationData = NavigationBarPresentationData(theme: NavigationBarTheme(rootControllerTheme: self.presentationData.theme).withUpdatedSeparatorColor(.clear), strings: NavigationBarStrings(presentationStrings: self.presentationData.strings))
} else {
navigationBarPresentationData = nil
}
super.init(navigationBarPresentationData: navigationBarPresentationData) self._hasGlassStyle = true
self.navigationPresentation = .modal 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) self.presentationDataDisposable = ((updatedPresentationData?.signal ?? context.sharedContext.presentationData)
|> deliverOnMainQueue).start(next: { [weak self] presentationData in |> deliverOnMainQueue).start(next: { [weak self] presentationData in
guard let strongSelf = self, strongSelf.presentationData.theme !== presentationData.theme else { guard let strongSelf = self, strongSelf.presentationData.theme !== presentationData.theme else {
return return
} }
strongSelf.presentationData = presentationData 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 { if strongSelf.isNodeLoaded {
strongSelf.controllerNode.updatePresentationData(presentationData) strongSelf.controllerNode.updatePresentationData(presentationData)
} }
@ -466,15 +441,6 @@ public final class LocationViewController: ViewController {
}, stopLiveLocation: { [weak self] in }, stopLiveLocation: { [weak self] in
params.stopLiveLocation(nil) params.stopLiveLocation(nil)
self?.dismiss() 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 }, present: { [weak self] c in
if let strongSelf = self { if let strongSelf = self {
strongSelf.present(c, in: .window(.root)) strongSelf.present(c, in: .window(.root))
@ -515,7 +481,7 @@ public final class LocationViewController: ViewController {
return 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.displayNodeDidLoad()
self.controllerNode.onAnnotationsReady = { [weak self] in self.controllerNode.onAnnotationsReady = { [weak self] in
@ -528,39 +494,12 @@ public final class LocationViewController: ViewController {
self.controllerNode.headerNode.mapNode.disableHorizontalTransitionGesture = self.isStoryLocation 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) { override public func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) {
super.containerLayoutUpdated(layout, transition: transition) super.containerLayoutUpdated(layout, transition: transition)
self.controllerNode.containerLayoutUpdated(layout, navigationHeight: self.navigationLayout(layout: layout).navigationFrame.maxY, 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 private var didDismiss = false
public override func viewWillDisappear(_ animated: Bool) { public override func viewWillDisappear(_ animated: Bool) {
super.viewWillDisappear(animated) super.viewWillDisappear(animated)

View file

@ -17,6 +17,11 @@ import CoreLocation
import Geocoding import Geocoding
import DeviceAccess import DeviceAccess
import TooltipUI import TooltipUI
import ComponentFlow
import GlassControls
import BundleIconComponent
import EdgeEffect
import MultilineTextComponent
func getLocation(from message: EngineMessage) -> TelegramMediaMap? { func getLocation(from message: EngineMessage) -> TelegramMediaMap? {
if let poll = message.media.first(where: { $0 is TelegramMediaPoll } ) as? TelegramMediaPoll, let map = poll.attachedMedia as? 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) 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 { public enum LocationViewLocation: Equatable {
case initial case initial
case user case user
@ -268,6 +279,7 @@ public struct LocationViewState {
final class LocationViewControllerNode: ViewControllerTracingNode, CLLocationManagerDelegate { final class LocationViewControllerNode: ViewControllerTracingNode, CLLocationManagerDelegate {
private let context: AccountContext private let context: AccountContext
private weak var controller: LocationViewController?
private var presentationData: PresentationData private var presentationData: PresentationData
private let presentationDataPromise: Promise<PresentationData> private let presentationDataPromise: Promise<PresentationData>
private var subject: EngineMessage private var subject: EngineMessage
@ -276,9 +288,14 @@ final class LocationViewControllerNode: ViewControllerTracingNode, CLLocationMan
private let isStoryLocation: Bool private let isStoryLocation: Bool
private let isPreview: 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 private let listNode: ListView
let headerNode: LocationMapHeaderNode let headerNode: LocationMapHeaderNode
private let optionsNode: LocationOptionsNode
private var enqueuedTransitions: [LocationViewTransaction] = [] private var enqueuedTransitions: [LocationViewTransaction] = []
@ -302,8 +319,9 @@ final class LocationViewControllerNode: ViewControllerTracingNode, CLLocationMan
} }
private let travelTimesPromise = Promise<[EngineMessage.Id: (Double, ExpectedTravelTime, ExpectedTravelTime, ExpectedTravelTime)]>([:]) 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.context = context
self.controller = controller
self.presentationData = presentationData self.presentationData = presentationData
self.presentationDataPromise = Promise(presentationData) self.presentationDataPromise = Promise(presentationData)
self.subject = subject self.subject = subject
@ -316,7 +334,7 @@ final class LocationViewControllerNode: ViewControllerTracingNode, CLLocationMan
self.statePromise = Promise(self.state) self.statePromise = Promise(self.state)
self.listNode = ListViewImpl() 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.verticalScrollIndicatorColor = UIColor(white: 0.0, alpha: 0.3)
self.listNode.verticalScrollIndicatorFollowsOverscroll = true self.listNode.verticalScrollIndicatorFollowsOverscroll = true
self.listNode.accessibilityPageScrolledString = { row, count in self.listNode.accessibilityPageScrolledString = { row, count in
@ -326,29 +344,24 @@ final class LocationViewControllerNode: ViewControllerTracingNode, CLLocationMan
var setupProximityNotificationImpl: ((Bool) -> Void)? var setupProximityNotificationImpl: ((Bool) -> Void)?
self.headerNode = LocationMapHeaderNode( self.headerNode = LocationMapHeaderNode(
presentationData: presentationData, presentationData: presentationData,
glass: false, glass: true,
isPreview: true, isPreview: self.isPreview,
toggleMapModeSelection: interaction.toggleMapModeSelection, toggleMapModeSelection: interaction.toggleMapModeSelection,
updateMapMode: interaction.updateMapMode, updateMapMode: interaction.updateMapMode,
goToUserLocation: interaction.toggleTrackingMode, goToUserLocation: interaction.toggleTrackingMode,
setupProximityNotification: { reset in setupProximityNotification: { reset in
setupProximityNotificationImpl?(reset) setupProximityNotificationImpl?(reset)
}) }
//self.headerNode.mapNode.isRotateEnabled = false )
self.optionsNode = LocationOptionsNode(presentationData: presentationData, updateMapMode: interaction.updateMapMode)
super.init() super.init()
self.backgroundColor = self.presentationData.theme.list.plainBackgroundColor self.backgroundColor = .red // self.presentationData.theme.list.plainBackgroundColor
if !self.isPreview { if !self.isPreview {
self.addSubnode(self.listNode) self.addSubnode(self.listNode)
} }
self.addSubnode(self.headerNode) self.addSubnode(self.headerNode)
if !self.isPreview {
self.addSubnode(self.optionsNode)
}
let userLocation: Signal<CLLocation?, NoError> = .single(nil) let userLocation: Signal<CLLocation?, NoError> = .single(nil)
|> then( |> then(
@ -707,7 +720,7 @@ final class LocationViewControllerNode: ViewControllerTracingNode, CLLocationMan
} else { } else {
rightBarButtonAction = .share rightBarButtonAction = .share
} }
strongSelf.interaction.updateRightBarButton(rightBarButtonAction) strongSelf.rightBarButtonAction = rightBarButtonAction
if let (layout, navigationBarHeight) = strongSelf.validLayout { if let (layout, navigationBarHeight) = strongSelf.validLayout {
var updateLayout = false var updateLayout = false
@ -800,10 +813,9 @@ final class LocationViewControllerNode: ViewControllerTracingNode, CLLocationMan
self.presentationData = presentationData self.presentationData = presentationData
self.presentationDataPromise.set(.single(presentationData)) self.presentationDataPromise.set(.single(presentationData))
self.backgroundColor = self.presentationData.theme.list.plainBackgroundColor self.backgroundColor = .red //self.presentationData.theme.list.plainBackgroundColor
self.listNode.backgroundColor = self.presentationData.theme.list.plainBackgroundColor self.listNode.backgroundColor = .clear // self.presentationData.theme.list.plainBackgroundColor
self.headerNode.updatePresentationData(self.presentationData) self.headerNode.updatePresentationData(self.presentationData)
self.optionsNode.updatePresentationData(self.presentationData)
} }
func updateState(_ f: (LocationViewState) -> LocationViewState) { func updateState(_ f: (LocationViewState) -> LocationViewState) {
@ -970,10 +982,10 @@ final class LocationViewControllerNode: ViewControllerTracingNode, CLLocationMan
} else { } else {
headerHeight = topInset + overlap 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) 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) let (duration, curve) = listViewAnimationDurationAndCurve(transition: transition)
@ -982,18 +994,118 @@ final class LocationViewControllerNode: ViewControllerTracingNode, CLLocationMan
let listFrame: CGRect = CGRect(origin: CGPoint(), size: layout.size) let listFrame: CGRect = CGRect(origin: CGPoint(), size: layout.size)
transition.updateFrame(node: self.listNode, frame: listFrame) 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 { if isFirstLayout {
while !self.enqueuedTransitions.isEmpty { while !self.enqueuedTransitions.isEmpty {
self.dequeueTransition() 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> { var coordinate: Signal<CLLocationCoordinate2D, NoError> {

View file

@ -240,8 +240,6 @@ public final class MediaPickerScreenImpl: ViewController, MediaPickerScreen, Att
private let cancelButtonNode: WebAppCancelButtonNode private let cancelButtonNode: WebAppCancelButtonNode
private var buttons: ComponentView<Empty>? private var buttons: ComponentView<Empty>?
private var cancelButton: ComponentView<Empty>?
private var rightButton: ComponentView<Empty>?
private let moreButtonPlayOnce = ActionSlot<Void>() private let moreButtonPlayOnce = ActionSlot<Void>()
private let moreButtonNode: MoreButtonNode private let moreButtonNode: MoreButtonNode
@ -2104,7 +2102,6 @@ public final class MediaPickerScreenImpl: ViewController, MediaPickerScreen, Att
} }
if case .glass = style { if case .glass = style {
self.cancelButton = ComponentView()
self.buttons = ComponentView() self.buttons = ComponentView()
} }
self.cancelButtonNode = WebAppCancelButtonNode(theme: self.presentationData.theme, strings: self.presentationData.strings) self.cancelButtonNode = WebAppCancelButtonNode(theme: self.presentationData.theme, strings: self.presentationData.strings)

View file

@ -250,6 +250,7 @@ static void initializeMapping()
mimeToExtension[@"text/plain"] = @"text"; extensionToMime[@"text"] = @"text/plain"; mimeToExtension[@"text/plain"] = @"text"; extensionToMime[@"text"] = @"text/plain";
mimeToExtension[@"text/plain"] = @"diff"; extensionToMime[@"diff"] = @"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/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/richtext"] = @"rtx"; extensionToMime[@"rtx"] = @"text/richtext";
mimeToExtension[@"text/rtf"] = @"rtf"; extensionToMime[@"rtf"] = @"text/rtf"; mimeToExtension[@"text/rtf"] = @"rtf"; extensionToMime[@"rtf"] = @"text/rtf";
mimeToExtension[@"text/texmacs"] = @"ts"; extensionToMime[@"ts"] = @"text/texmacs"; mimeToExtension[@"text/texmacs"] = @"ts"; extensionToMime[@"ts"] = @"text/texmacs";

View file

@ -373,7 +373,7 @@ final class SharePeersContainerNode: ASDisplayNode, ShareContentContainerNode {
self.contentOffsetUpdated = f 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 itemCount = self.entries.count
let itemInsets = UIEdgeInsets(top: 0.0, left: 12.0, bottom: 0.0, right: 12.0) 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 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) 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) { func updateLayout(size: CGSize, isLandscape: Bool, bottomInset: CGFloat, transition: ContainedViewLayoutTransition) {
let firstLayout = self.validLayout == nil let firstLayout = self.validLayout == nil
self.validLayout = (size, bottomInset) self.validLayout = (size, bottomInset)
self.contentTitleNode.isHidden = self.isEmbedded
self.contentSubtitleNode.isHidden = self.isEmbedded
self.searchButtonNode.isHidden = self.isEmbedded
let gridLayoutTransition: ContainedViewLayoutTransition let gridLayoutTransition: ContainedViewLayoutTransition
if firstLayout { if firstLayout {
gridLayoutTransition = .immediate gridLayoutTransition = .immediate
@ -582,7 +587,7 @@ final class SharePeersContainerNode: ASDisplayNode, ShareContentContainerNode {
self.overrideGridOffsetTransition = nil 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? var scrollToItem: GridNodeScrollToItem?
if let ensurePeerVisibleOnLayout = self.ensurePeerVisibleOnLayout { if let ensurePeerVisibleOnLayout = self.ensurePeerVisibleOnLayout {
@ -680,8 +685,8 @@ final class SharePeersContainerNode: ASDisplayNode, ShareContentContainerNode {
self.contentTitleNode.isHidden = true self.contentTitleNode.isHidden = true
self.contentSubtitleNode.isHidden = true self.contentSubtitleNode.isHidden = true
} else { } else {
self.contentTitleNode.isHidden = false self.contentTitleNode.isHidden = self.isEmbedded
self.contentSubtitleNode.isHidden = false self.contentSubtitleNode.isHidden = self.isEmbedded
var subtitleText = self.strings.ShareMenu_SelectChats var subtitleText = self.strings.ShareMenu_SelectChats
if !self.controllerInteraction.selectedPeers.isEmpty { if !self.controllerInteraction.selectedPeers.isEmpty {

View file

@ -1413,7 +1413,7 @@ public final class SparseItemGridScrollingArea: ASDisplayNode {
} }
override public func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { 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 return nil
} }
if self.dateIndicator.frame.offsetBy(dx: self.dateIndicatorContainer.frame.minX, dy: self.dateIndicatorContainer.frame.minY).contains(point) { if self.dateIndicator.frame.offsetBy(dx: self.dateIndicatorContainer.frame.minX, dy: self.dateIndicatorContainer.frame.minY).contains(point) {

View file

@ -132,6 +132,12 @@ public enum PresentationResourceKey: Int32 {
case chatListGiftIcon case chatListGiftIcon
case chatListLocationIcon case chatListLocationIcon
case chatListPollIcon case chatListPollIcon
case chatListTodoIcon
case chatListGameIcon
case chatListCallIncomingIcon
case chatListCallOutgoingIcon
case chatListCallVideoIncomingIcon
case chatListCallVideoOutgoingIcon
case chatListGeneralTopicIcon case chatListGeneralTopicIcon
case chatListGeneralTopicTemplateIcon case chatListGeneralTopicTemplateIcon

View file

@ -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? { public static func verifiedIcon(_ theme: PresentationTheme) -> UIImage? {
return theme.image(PresentationResourceKey.chatListVerifiedIcon.rawValue, { theme in return theme.image(PresentationResourceKey.chatListVerifiedIcon.rawValue, { theme in
if let backgroundImage = UIImage(bundleImageName: "Chat List/PeerVerifiedIconBackground"), let foregroundImage = UIImage(bundleImageName: "Chat List/PeerVerifiedIconForeground") { if let backgroundImage = UIImage(bundleImageName: "Chat List/PeerVerifiedIconBackground"), let foregroundImage = UIImage(bundleImageName: "Chat List/PeerVerifiedIconForeground") {

View file

@ -526,6 +526,8 @@ swift_library(
"//submodules/TelegramUI/Components/RankChatPreviewItem", "//submodules/TelegramUI/Components/RankChatPreviewItem",
"//submodules/TelegramUI/Components/TextProcessingScreen", "//submodules/TelegramUI/Components/TextProcessingScreen",
"//submodules/TelegramUI/Components/CreateBotScreen", "//submodules/TelegramUI/Components/CreateBotScreen",
"//submodules/TelegramUI/Components/ShareScreen",
"//submodules/Utils/AutomationBridge",
] + select({ ] + select({
"@build_bazel_rules_apple//apple:ios_arm64": appcenter_targets, "@build_bazel_rules_apple//apple:ios_arm64": appcenter_targets,
"//build-system:ios_sim_arm64": [], "//build-system:ios_sim_arm64": [],

View file

@ -17,6 +17,7 @@ swift_library(
"//submodules/ComponentFlow", "//submodules/ComponentFlow",
"//submodules/Components/ViewControllerComponent", "//submodules/Components/ViewControllerComponent",
"//submodules/Components/ComponentDisplayAdapters", "//submodules/Components/ComponentDisplayAdapters",
"//submodules/Components/ResizableSheetComponent",
"//submodules/Components/MultilineTextComponent", "//submodules/Components/MultilineTextComponent",
"//submodules/Components/BundleIconComponent", "//submodules/Components/BundleIconComponent",
"//submodules/TelegramPresentationData", "//submodules/TelegramPresentationData",

View file

@ -227,7 +227,7 @@ final class AdminUserActionsPeerComponent: Component {
let titleSize = self.title.update( let titleSize = self.title.update(
transition: .immediate, transition: .immediate,
component: AnyComponent(MultilineTextComponent( 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: {}, environment: {},
containerSize: CGSize(width: maxTextSize, height: 100.0) containerSize: CGSize(width: maxTextSize, height: 100.0)

View file

@ -465,7 +465,7 @@ public final class ButtonComponent: Component {
private var glassShadowCornerRadius: CGFloat? private var glassShadowCornerRadius: CGFloat?
private var glassHighlightContainerView: UIView? private var glassHighlightContainerView: UIView?
private let button: HighlightTrackingButton private let button: HighlightTrackingButton
private let legacyGlassHighlightRecognizer: GlassHighlightGestureRecognizer private let glassHighlightRecognizer: GlassHighlightGestureRecognizer
private var shimmeringView: ButtonShimmeringView? private var shimmeringView: ButtonShimmeringView?
private var chromeView: UIImageView? private var chromeView: UIImageView?
@ -480,7 +480,7 @@ public final class ButtonComponent: Component {
self.containerView.isUserInteractionEnabled = false self.containerView.isUserInteractionEnabled = false
self.button = HighlightTrackingButton() self.button = HighlightTrackingButton()
self.legacyGlassHighlightRecognizer = GlassHighlightGestureRecognizer(target: nil, action: nil) self.glassHighlightRecognizer = GlassHighlightGestureRecognizer(target: nil, action: nil)
super.init(frame: frame) super.init(frame: frame)
@ -489,8 +489,8 @@ public final class ButtonComponent: Component {
self.addSubview(self.containerView) self.addSubview(self.containerView)
self.addSubview(self.button) self.addSubview(self.button)
self.addGestureRecognizer(self.legacyGlassHighlightRecognizer) self.addGestureRecognizer(self.glassHighlightRecognizer)
self.legacyGlassHighlightRecognizer.isEnabled = false self.glassHighlightRecognizer.isEnabled = false
self.button.addTarget(self, action: #selector(self.pressed), for: .touchUpInside) self.button.addTarget(self, action: #selector(self.pressed), for: .touchUpInside)
@ -521,33 +521,18 @@ public final class ButtonComponent: Component {
preconditionFailure() preconditionFailure()
} }
private func ensureGlassShadowView() -> UIImageView { private func removeGlassEffect(transition: ComponentTransition) {
if let glassShadowView = self.glassShadowView { self.glassHighlightRecognizer.isEnabled = false
return glassShadowView self.glassHighlightRecognizer.highlightContainerView = nil
}
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
if let glassShadowView = self.glassShadowView, glassShadowView.superview != 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 { if let glassHighlightContainerView = self.glassHighlightContainerView, glassHighlightContainerView.superview != nil {
glassHighlightContainerView.removeFromSuperview() glassHighlightContainerView.removeFromSuperview()
@ -558,10 +543,17 @@ public final class ButtonComponent: Component {
self.layer.sublayerTransform = CATransform3DIdentity 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 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 { if glassShadowView.superview == nil {
self.insertSubview(glassShadowView, at: 0) self.insertSubview(glassShadowView, at: 0)
} else { } 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.setFrame(view: glassShadowView, frame: CGRect(origin: .zero, size: size).insetBy(dx: -shadowInset, dy: -shadowInset))
transition.setAlpha(view: glassShadowView, alpha: 1.0) 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 { if glassHighlightContainerView.superview == nil {
self.insertSubview(glassHighlightContainerView, aboveSubview: self.containerView) self.insertSubview(glassHighlightContainerView, aboveSubview: self.containerView)
} else if self.button.superview === self { } 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.setFrame(view: glassHighlightContainerView, frame: CGRect(origin: .zero, size: size))
transition.setCornerRadius(layer: glassHighlightContainerView.layer, cornerRadius: cornerRadius) transition.setCornerRadius(layer: glassHighlightContainerView.layer, cornerRadius: cornerRadius)
self.legacyGlassHighlightRecognizer.highlightContainerView = glassHighlightContainerView self.glassHighlightRecognizer.highlightContainerView = glassHighlightContainerView
self.legacyGlassHighlightRecognizer.isEnabled = component.isEnabled && !component.displaysProgress self.glassHighlightRecognizer.isEnabled = component.isEnabled && !component.displaysProgress
} }
@objc private func pressed() { @objc private func pressed() {
@ -694,10 +694,10 @@ public final class ButtonComponent: Component {
transition.setCornerRadius(layer: self.containerView.layer, cornerRadius: cornerRadius) transition.setCornerRadius(layer: self.containerView.layer, cornerRadius: cornerRadius)
} }
if component.background.style == .glass { if component.background.style == .glass, component.background.color.alpha > 1.0 - .ulpOfOne {
self.updateLegacyGlassEffectViews(component: component, size: size, cornerRadius: cornerRadius, transition: transition) self.updateGlassEffect(component: component, size: size, cornerRadius: cornerRadius, transition: transition)
} else { } else {
self.removeLegacyGlassEffectViews() self.removeGlassEffect(transition: transition)
} }
if let contentView = contentItem.view.view { if let contentView = contentItem.view.view {

View file

@ -2684,8 +2684,9 @@ public class ChatMessagePollBubbleContentNode: ChatMessageBubbleContentNode {
var pollOptionsFinalizeLayouts: [(hasResult: Bool, layout: (CGFloat) -> (CGSize, (Bool, Bool, Bool) -> ChatMessagePollOptionNode))] = [] var pollOptionsFinalizeLayouts: [(hasResult: Bool, layout: (CGFloat) -> (CGSize, (Bool, Bool, Bool) -> ChatMessagePollOptionNode))] = []
var addOptionFinalizeLayout: ((CGFloat) -> (CGSize, (Bool, Bool) -> ChatMessagePollAddOptionNode))? var addOptionFinalizeLayout: ((CGFloat) -> (CGSize, (Bool, Bool) -> ChatMessagePollAddOptionNode))?
var orderedPollOptions: [(Int, TelegramMediaPollOption)] = [] var orderedPollOptions: [(Int, TelegramMediaPollOption)] = []
var isRestricted = false
if let poll = poll { if let poll = poll {
var isRestricted = false
if !poll.countries.isEmpty, let accountCountry = item.associatedData.accountCountry, !poll.countries.contains(accountCountry) { if !poll.countries.isEmpty, let accountCountry = item.associatedData.accountCountry, !poll.countries.contains(accountCountry) {
isRestricted = true 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) 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 return (resultSize, { [weak self] animation, synchronousLoad, _ in
if let strongSelf = self { if let strongSelf = self {
strongSelf.item = item strongSelf.item = item
strongSelf.poll = poll strongSelf.poll = poll
let cachedLayout = strongSelf.textNode.textNode.cachedLayout let cachedLayout = strongSelf.textNode.textNode.cachedLayout
if case .System = animation { if case .System = animation {
if let cachedLayout = cachedLayout { if let cachedLayout = cachedLayout {
if !cachedLayout.areLinesEqual(to: textLayout) { if !cachedLayout.areLinesEqual(to: textLayout) {
if let textContents = strongSelf.textNode.textNode.contents { if let textContents = strongSelf.textNode.textNode.contents {
let fadeNode = ASDisplayNode() let fadeNode = ASDisplayNode()
fadeNode.displaysAsynchronously = false fadeNode.displaysAsynchronously = false
fadeNode.contents = textContents fadeNode.contents = textContents
fadeNode.frame = strongSelf.textNode.textNode.frame fadeNode.frame = strongSelf.textNode.textNode.frame
fadeNode.isLayerBacked = true fadeNode.isLayerBacked = true
strongSelf.addSubnode(fadeNode) strongSelf.addSubnode(fadeNode)
fadeNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { [weak fadeNode] _ in fadeNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { [weak fadeNode] _ in
fadeNode?.removeFromSupernode() fadeNode?.removeFromSupernode()
}) })
strongSelf.textNode.textNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.15) strongSelf.textNode.textNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.15)
}
} }
} }
} }
}
let _ = textApply(TextNodeWithEntities.Arguments(
context: item.context, let _ = textApply(TextNodeWithEntities.Arguments(
cache: item.context.animationCache, context: item.context,
renderer: item.context.animationRenderer, cache: item.context.animationCache,
placeholderColor: incoming ? item.presentationData.theme.theme.chat.message.incoming.mediaPlaceholderColor : item.presentationData.theme.theme.chat.message.outgoing.mediaPlaceholderColor, renderer: item.context.animationRenderer,
attemptSynchronous: synchronousLoad) placeholderColor: incoming ? item.presentationData.theme.theme.chat.message.incoming.mediaPlaceholderColor : item.presentationData.theme.theme.chat.message.outgoing.mediaPlaceholderColor,
) attemptSynchronous: synchronousLoad)
let _ = typeApply() )
let _ = typeApply()
var verticalOffset = textFrame.maxY + titleTypeSpacing + typeLayout.size.height + typeOptionsSpacing
var updatedOptionNodes: [ChatMessagePollOptionNode] = [] var verticalOffset = textFrame.maxY + titleTypeSpacing + typeLayout.size.height + typeOptionsSpacing
for i in 0 ..< optionNodesSizesAndApply.count { var updatedOptionNodes: [ChatMessagePollOptionNode] = []
let (size, apply) = optionNodesSizesAndApply[i] for i in 0 ..< optionNodesSizesAndApply.count {
var isRequesting = false let (size, apply) = optionNodesSizesAndApply[i]
if i < orderedPollOptions.count { var isRequesting = false
if let inProgressOpaqueIds = item.controllerInteraction.pollActionState.pollMessageIdsInProgress[item.message.id] { if i < orderedPollOptions.count {
isRequesting = inProgressOpaqueIds.contains(orderedPollOptions[i].1.opaqueIdentifier) 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) let optionNode = apply(animation.isAnimated, isRequesting, synchronousLoad)
if optionNode.supernode !== strongSelf { let optionNodeFrame = CGRect(origin: CGPoint(x: layoutConstants.bubble.borderInset, y: verticalOffset), size: size)
strongSelf.addSubnode(optionNode) if optionNode.supernode !== strongSelf {
let option = optionNode.option strongSelf.addSubnode(optionNode)
let option = optionNode.option
optionNode.pressed = { [weak self] in
guard let self, optionNode.pressed = { [weak self] in
let item = self.item, guard let self,
let option else { let item = self.item,
return let option else {
} return
item.controllerInteraction.requestSelectMessagePollOptions(item.message.id, [option.opaqueIdentifier])
} }
optionNode.resultPressed = { [weak self] in item.controllerInteraction.requestSelectMessagePollOptions(item.message.id, [option.opaqueIdentifier])
guard let self, }
let item = self.item, optionNode.resultPressed = { [weak self] in
let option else { guard let self,
return let item = self.item,
} let option else {
if let poll { return
if case .public = poll.publicity { }
item.controllerInteraction.openMessagePollResults(item.message.id, option.opaqueIdentifier) if let poll {
} else { if case .public = poll.publicity {
if "".isEmpty { item.controllerInteraction.openMessagePollResults(item.message.id, option.opaqueIdentifier)
let locale = localeWithStrings(item.presentationData.strings) } else if isRestricted {
let countryNames = poll.countries.map { id in let locale = localeWithStrings(item.presentationData.strings)
if let countryName = locale.localizedString(forRegionCode: id) { let countryNames = poll.countries.map { id in
return countryName if let countryName = locale.localizedString(forRegionCode: id) {
} else { return countryName
return id } 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)
} }
} }
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)
} }
optionNode.selectionUpdated = { [weak self] in
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
guard let self else { guard let self else {
return 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 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 }) { if i > 0 {
strongSelf.clearNewOptionInput() optionNode.previousOptionNode = updatedOptionNodes[i - 1]
}
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 { } 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)
}
})
}) })
}) })
} }

View file

@ -769,10 +769,10 @@ private final class ChatMessageTodoItemNode: ASDisplayNode {
linkColor: messageTheme.linkTextColor, linkColor: messageTheme.linkTextColor,
baseFont: presentationData.messageFont, baseFont: presentationData.messageFont,
linkFont: presentationData.messageFont, linkFont: presentationData.messageFont,
boldFont: presentationData.messageFont, boldFont: presentationData.messageBoldFont,
italicFont: presentationData.messageFont, italicFont: presentationData.messageItalicFont,
boldItalicFont: presentationData.messageFont, boldItalicFont: presentationData.messageBoldItalicFont,
fixedFont: presentationData.messageFont, fixedFont: presentationData.messageFixedFont,
blockQuoteFont: presentationData.messageFont, blockQuoteFont: presentationData.messageFont,
underlineLinks: underlineLinks, underlineLinks: underlineLinks,
message: message message: message

View file

@ -24,6 +24,7 @@ swift_library(
"//submodules/TelegramUI/Components/MultiAnimationRenderer:MultiAnimationRenderer", "//submodules/TelegramUI/Components/MultiAnimationRenderer:MultiAnimationRenderer",
"//submodules/TextFormat:TextFormat", "//submodules/TextFormat:TextFormat",
"//submodules/AppBundle:AppBundle", "//submodules/AppBundle:AppBundle",
"//submodules/SearchBarNode:SearchBarNode",
"//submodules/TelegramUI/Components/ChatInputNode:ChatInputNode", "//submodules/TelegramUI/Components/ChatInputNode:ChatInputNode",
"//submodules/Components/PagerComponent:PagerComponent", "//submodules/Components/PagerComponent:PagerComponent",
"//submodules/PremiumUI:PremiumUI", "//submodules/PremiumUI:PremiumUI",

View file

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

View file

@ -2,6 +2,7 @@ import Foundation
import UIKit import UIKit
import AsyncDisplayKit import AsyncDisplayKit
import Display import Display
import SearchBarNode
import SwiftSignalKit import SwiftSignalKit
import Postbox import Postbox
import TelegramCore import TelegramCore
@ -16,7 +17,23 @@ import StickerPeekUI
import EntityKeyboardGifContent import EntityKeyboardGifContent
import BatchVideoRendering 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 { public protocol PaneSearchContentNode {
var ready: Signal<Void, NoError> { get } var ready: Signal<Void, NoError> { get }
@ -43,9 +60,10 @@ public final class PaneSearchContainerNode: ASDisplayNode, EntitySearchContainer
private let peekBehavior: EmojiContentPeekBehavior? private let peekBehavior: EmojiContentPeekBehavior?
private let backgroundNode: ASDisplayNode private let backgroundNode: ASDisplayNode
private let searchBar: PaneSearchBarNode private let searchBar: SearchBarNode
private var validLayout: CGSize? private var validLayout: CGSize?
private weak var animatedPlaceholder: PaneSearchBarPlaceholderNode?
public var onCancel: (() -> Void)? public var onCancel: (() -> Void)?
@ -69,7 +87,13 @@ public final class PaneSearchContainerNode: ASDisplayNode, EntitySearchContainer
} }
self.backgroundNode = ASDisplayNode() self.backgroundNode = ASDisplayNode()
self.searchBar = PaneSearchBarNode() self.searchBar = SearchBarNode(
theme: paneSearchBarTheme(theme),
presentationTheme: theme,
strings: strings,
fieldStyle: .glass,
displayBackground: false
)
super.init() super.init()
@ -87,9 +111,8 @@ public final class PaneSearchContainerNode: ASDisplayNode, EntitySearchContainer
} }
self.searchBar.cancel = { [weak self] in self.searchBar.cancel = { [weak self] in
self?.searchBar.deactivate(clear: false)
cancel() cancel()
self?.searchBar.view.endEditing(true)
self?.onCancel?() self?.onCancel?()
} }
self.searchBar.activate() self.searchBar.activate()
@ -150,7 +173,7 @@ public final class PaneSearchContainerNode: ASDisplayNode, EntitySearchContainer
public func updateThemeAndStrings(theme: PresentationTheme, strings: PresentationStrings) { public func updateThemeAndStrings(theme: PresentationTheme, strings: PresentationStrings) {
self.backgroundNode.backgroundColor = theme.chat.inputMediaPanel.stickersBackgroundColor.withAlphaComponent(1.0) self.backgroundNode.backgroundColor = theme.chat.inputMediaPanel.stickersBackgroundColor.withAlphaComponent(1.0)
self.contentNode.updateThemeAndStrings(theme: theme, strings: strings) 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 let placeholder: String
switch mode { switch mode {
@ -159,11 +182,11 @@ public final class PaneSearchContainerNode: ASDisplayNode, EntitySearchContainer
case .sticker, .trending: case .sticker, .trending:
placeholder = strings.Stickers_Search 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) { public func updateQuery(_ query: String) {
self.searchBar.updateQuery(query) self.searchBar.text = query
} }
public func itemAt(point: CGPoint) -> (ASDisplayNode, Any)? { public func itemAt(point: CGPoint) -> (ASDisplayNode, Any)? {
@ -174,8 +197,9 @@ public final class PaneSearchContainerNode: ASDisplayNode, EntitySearchContainer
self.validLayout = size self.validLayout = size
transition.updateFrame(node: self.backgroundNode, frame: CGRect(origin: CGPoint(), size: 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))) let searchBarFrame = CGRect(origin: CGPoint(x: 0.0, y: searchBarTopInset), size: CGSize(width: size.width, height: searchBarFieldHeight))
self.searchBar.updateLayout(boundingSize: CGSize(width: size.width, height: searchBarHeight), leftInset: leftInset, rightInset: rightInset, transition: transition) 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)) 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) { 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 var verticalOrigin: CGFloat = anhorTopView.convert(anchorTop, to: self.view).y
if let placeholder = placeholder { if let placeholder = placeholder {
self.animatedPlaceholder = placeholder
placeholder.isHidden = true
let placeholderFrame = placeholder.view.convert(placeholder.bounds, to: self.view) let placeholderFrame = placeholder.view.convert(placeholder.bounds, to: self.view)
verticalOrigin = placeholderFrame.minY - 4.0 verticalOrigin = placeholderFrame.minY - 4.0
self.contentNode.animateIn(additivePosition: verticalOrigin, transition: transition) 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) 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 { switch transition {
case let .animated(duration, curve): case let .animated(duration, curve):
self.backgroundNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: duration / 2.0) 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) self.searchBar.alpha = 1.0
} else { self.searchBar.layer.animateAlpha(from: 0.0, to: 1.0, duration: duration, timingFunction: curve.timingFunction, completion: { _ in
self.searchBar.alpha = 0.0 completion()
transition.updateAlpha(node: self.searchBar, alpha: 1.0) })
} self.searchBar.layer.animateFrame(from: initialSearchBarFrame, to: searchBarFrame, duration: duration, timingFunction: curve.timingFunction)
if let size = self.validLayout { 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))) 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) self.backgroundNode.layer.animateFrame(from: initialBackgroundFrame, to: self.backgroundNode.frame, duration: duration, timingFunction: curve.timingFunction)
} }
case .immediate: case .immediate:
completion()
break break
} }
} }
public func animateOut(to placeholder: PaneSearchBarPlaceholderNode, animateOutSearchBar: Bool, transition: ContainedViewLayoutTransition, completion: @escaping () -> Void) { 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 { 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 { 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.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) transition.updateAlpha(node: self.backgroundNode, alpha: 0.0)
if animateOutSearchBar { if animateOutSearchBar {
transition.updateAlpha(node: self.searchBar, alpha: 0.0) transition.updateAlpha(node: self.searchBar, alpha: 0.0)

View file

@ -25,6 +25,7 @@ swift_library(
"//submodules/TelegramUI/Components/ToastComponent", "//submodules/TelegramUI/Components/ToastComponent",
"//submodules/Markdown", "//submodules/Markdown",
"//submodules/UndoUI", "//submodules/UndoUI",
"//submodules/TooltipUI",
"//submodules/TelegramUI/Components/LottieComponent", "//submodules/TelegramUI/Components/LottieComponent",
"//submodules/Components/MultilineTextComponent", "//submodules/Components/MultilineTextComponent",
"//submodules/Components/BundleIconComponent", "//submodules/Components/BundleIconComponent",
@ -36,6 +37,7 @@ swift_library(
"//submodules/TelegramUI/Components/GlassBackgroundComponent", "//submodules/TelegramUI/Components/GlassBackgroundComponent",
"//submodules/TelegramUI/Components/GlassBarButtonComponent", "//submodules/TelegramUI/Components/GlassBarButtonComponent",
"//submodules/TelegramUI/Components/PlainButtonComponent", "//submodules/TelegramUI/Components/PlainButtonComponent",
"//submodules/Components/LottieAnimationComponent",
], ],
visibility = [ visibility = [
"//visibility:public", "//visibility:public",

View file

@ -13,10 +13,12 @@ import SheetComponent
import ButtonComponent import ButtonComponent
import PlainButtonComponent import PlainButtonComponent
import BundleIconComponent import BundleIconComponent
import LottieAnimationComponent
import GlassBackgroundComponent import GlassBackgroundComponent
import GlassBarButtonComponent import GlassBarButtonComponent
import DatePickerNode import DatePickerNode
import UndoUI import UndoUI
import TooltipUI
private let calendar = Calendar(identifier: .gregorian) private let calendar = Calendar(identifier: .gregorian)
@ -66,6 +68,8 @@ private final class ChatScheduleTimeSheetContentComponent: Component {
final class View: UIView { final class View: UIView {
private let cancel = ComponentView<Empty>() private let cancel = ComponentView<Empty>()
private let silent = ComponentView<Empty>()
private let title = ComponentView<Empty>() private let title = ComponentView<Empty>()
private let button = ComponentView<Empty>() private let button = ComponentView<Empty>()
private let secondaryButton = ComponentView<Empty>() private let secondaryButton = ComponentView<Empty>()
@ -93,13 +97,15 @@ private final class ChatScheduleTimeSheetContentComponent: Component {
private var monthHeight: CGFloat? private var monthHeight: CGFloat?
private var isSilentPosting = false
private var date: Date? private var date: Date?
private var minDate: Date? private var minDate: Date?
private var maxDate: Date? private var maxDate: Date?
private var isPickingTime = false private var isPickingTime = false
private var isPickingRepeatPeriod = false private var isPickingRepeatPeriod = false
private var repeatPeriod: Int32? private var repeatPeriod: Int32?
private let dateFormatter: DateFormatter 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 { func update(component: ChatScheduleTimeSheetContentComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<EnvironmentType>, transition: ComponentTransition) -> CGSize {
self.isUpdating = true self.isUpdating = true
defer { defer {
@ -184,40 +249,6 @@ private final class ChatScheduleTimeSheetContentComponent: Component {
var contentHeight: CGFloat = 0.0 var contentHeight: CGFloat = 0.0
contentHeight += 30.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 let title: String
switch component.mode { switch component.mode {
case .scheduledMessages: case .scheduledMessages:
@ -595,7 +626,7 @@ private final class ChatScheduleTimeSheetContentComponent: Component {
transition.setFrame(view: buttonView, frame: buttonFrame) transition.setFrame(view: buttonView, frame: buttonFrame)
} }
contentHeight += buttonSize.height contentHeight += buttonSize.height
} else if case .scheduledMessages(true) = component.mode { } else if case .scheduledMessages(_, true) = component.mode {
contentHeight += 8.0 contentHeight += 8.0
let buttonSize = self.secondaryButton.update( let buttonSize = self.secondaryButton.update(
@ -784,6 +815,87 @@ private final class ChatScheduleTimeSheetContentComponent: Component {
component.externalState.repeatValueFrame = repeatValueFrame 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 return contentSize
} }
} }
@ -958,7 +1070,7 @@ private final class ChatScheduleTimeScreenComponent: Component {
public class ChatScheduleTimeScreen: ViewControllerComponentContainer { public class ChatScheduleTimeScreen: ViewControllerComponentContainer {
public enum Mode: Equatable { public enum Mode: Equatable {
case scheduledMessages(sendWhenOnlineAvailable: Bool) case scheduledMessages(peerId: EnginePeer.Id, sendWhenOnlineAvailable: Bool)
case reminders case reminders
case format case format
case poll case poll

View file

@ -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 tag: todoItem.textFieldTag
)))) ))))

View file

@ -424,6 +424,13 @@ final class InnerTextSelectionTipContainerNode: ASDisplayNode {
self.text = self.presentationData.strings.Camera_CollageReorderingInfo self.text = self.presentationData.strings.Camera_CollageReorderingInfo
self.targetSelectionIndex = nil self.targetSelectionIndex = nil
icon = UIImage(bundleImageName: "Chat/Context Menu/Tip") 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() self.iconNode = ASImageNode()

View file

@ -93,15 +93,23 @@ public final class ListActionItemComponent: Component {
public enum LeftIcon: Equatable { public enum LeftIcon: Equatable {
public final class Check: Equatable { public final class Check: Equatable {
public enum Style {
case round
case rectangle
}
public let style: Style
public let isSelected: Bool public let isSelected: Bool
public let isEnabled: Bool public let isEnabled: Bool
public let toggle: (() -> Void)? public let toggle: (() -> Void)?
public init( public init(
style: Style = .round,
isSelected: Bool, isSelected: Bool,
isEnabled: Bool = true, isEnabled: Bool = true,
toggle: (() -> Void)? toggle: (() -> Void)?
) { ) {
self.style = style
self.isSelected = isSelected self.isSelected = isSelected
self.isEnabled = isEnabled self.isEnabled = isEnabled
self.toggle = toggle self.toggle = toggle
@ -111,6 +119,9 @@ public final class ListActionItemComponent: Component {
if lhs === rhs { if lhs === rhs {
return true return true
} }
if lhs.style != rhs.style {
return false
}
if lhs.isSelected != rhs.isSelected { if lhs.isSelected != rhs.isSelected {
return false return false
} }
@ -318,12 +329,12 @@ public final class ListActionItemComponent: Component {
self.action?() 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 let checkLayer: CheckLayer
if let current = self.checkLayer { if let current = self.checkLayer {
checkLayer = current checkLayer = current
} else { } 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.checkLayer = checkLayer
self.layer.addSublayer(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) 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.setPosition(view: leftCheckView, position: checkFrame.center)
transition.setBounds(view: leftCheckView, bounds: CGRect(origin: CGPoint(), size: checkFrame.size)) 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 { } else {
transition.setPosition(view: leftCheckView, position: checkFrame.center) transition.setPosition(view: leftCheckView, position: checkFrame.center)
transition.setBounds(view: leftCheckView, bounds: CGRect(origin: CGPoint(), size: checkFrame.size)) 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): case let .custom(customLeftIcon, adjustLeftInset):
var resetLeftIcon = false var resetLeftIcon = false

View file

@ -130,6 +130,7 @@ public final class ListComposePollOptionComponent: Component {
public let canReorder: Bool public let canReorder: Bool
public let canAdd: Bool public let canAdd: Bool
public let attachment: Attachment? public let attachment: Attachment?
public let formattingAvailable: Bool
public let emptyLineHandling: TextFieldComponent.EmptyLineHandling public let emptyLineHandling: TextFieldComponent.EmptyLineHandling
public let returnKeyType: UIReturnKeyType public let returnKeyType: UIReturnKeyType
public let returnKeyAction: (() -> Void)? public let returnKeyAction: (() -> Void)?
@ -141,6 +142,7 @@ public final class ListComposePollOptionComponent: Component {
public let attachAction: (() -> Void)? public let attachAction: (() -> Void)?
public let deleteAction: (() -> Void)? public let deleteAction: (() -> Void)?
public let paste: ((TextFieldComponent.PasteData) -> Void)? public let paste: ((TextFieldComponent.PasteData) -> Void)?
public let present: ((ViewController) -> Void)?
public let tag: AnyObject? public let tag: AnyObject?
public init( public init(
@ -159,6 +161,7 @@ public final class ListComposePollOptionComponent: Component {
canReorder: Bool = false, canReorder: Bool = false,
canAdd: Bool = false, canAdd: Bool = false,
attachment: Attachment? = nil, attachment: Attachment? = nil,
formattingAvailable: Bool = false,
emptyLineHandling: TextFieldComponent.EmptyLineHandling, emptyLineHandling: TextFieldComponent.EmptyLineHandling,
returnKeyType: UIReturnKeyType = .next, returnKeyType: UIReturnKeyType = .next,
returnKeyAction: (() -> Void)? = nil, returnKeyAction: (() -> Void)? = nil,
@ -170,6 +173,7 @@ public final class ListComposePollOptionComponent: Component {
attachAction: (() -> Void)? = nil, attachAction: (() -> Void)? = nil,
deleteAction: (() -> Void)? = nil, deleteAction: (() -> Void)? = nil,
paste: ((TextFieldComponent.PasteData) -> Void)? = nil, paste: ((TextFieldComponent.PasteData) -> Void)? = nil,
present: ((ViewController) -> Void)? = nil,
tag: AnyObject? = nil tag: AnyObject? = nil
) { ) {
self.externalState = externalState self.externalState = externalState
@ -187,6 +191,7 @@ public final class ListComposePollOptionComponent: Component {
self.canReorder = canReorder self.canReorder = canReorder
self.canAdd = canAdd self.canAdd = canAdd
self.attachment = attachment self.attachment = attachment
self.formattingAvailable = formattingAvailable
self.emptyLineHandling = emptyLineHandling self.emptyLineHandling = emptyLineHandling
self.returnKeyType = returnKeyType self.returnKeyType = returnKeyType
self.returnKeyAction = returnKeyAction self.returnKeyAction = returnKeyAction
@ -198,6 +203,7 @@ public final class ListComposePollOptionComponent: Component {
self.attachAction = attachAction self.attachAction = attachAction
self.deleteAction = deleteAction self.deleteAction = deleteAction
self.paste = paste self.paste = paste
self.present = present
self.tag = tag self.tag = tag
} }
@ -247,6 +253,9 @@ public final class ListComposePollOptionComponent: Component {
if lhs.attachment != rhs.attachment { if lhs.attachment != rhs.attachment {
return false return false
} }
if lhs.formattingAvailable != rhs.formattingAvailable {
return false
}
if lhs.emptyLineHandling != rhs.emptyLineHandling { if lhs.emptyLineHandling != rhs.emptyLineHandling {
return false return false
} }
@ -732,11 +741,15 @@ public final class ListComposePollOptionComponent: Component {
enableInlineAnimations: component.enableInlineAnimations, enableInlineAnimations: component.enableInlineAnimations,
emptyLineHandling: component.emptyLineHandling, emptyLineHandling: component.emptyLineHandling,
externalHandlingForMultilinePaste: true, externalHandlingForMultilinePaste: true,
formatMenuAvailability: .none, formatMenuAvailability: component.formattingAvailable ? .available([.bold, .italic, .strikethrough, .underline, .monospace, .spoiler, .link]) : .none,
returnKeyType: component.returnKeyType, returnKeyType: component.returnKeyType,
lockedFormatAction: { 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 paste: { [weak self] data in
guard let self, let component = self.component else { guard let self, let component = self.component else {

View file

@ -167,7 +167,11 @@ private final class PeerInfoScreenCommentItemNode: PeerInfoScreenItemNode {
let height = textSize.height + verticalInset * 2.0 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)) self.activateArea.frame = CGRect(origin: CGPoint(x: safeInsets.left, y: 0.0), size: CGSize(width: width - safeInsets.left - safeInsets.right, height: height))

View file

@ -375,17 +375,30 @@ func settingsEditingItems(data: PeerInfoScreenData?, state: PeerInfoState, conte
items[.help]!.append(PeerInfoScreenCommentItem(id: ItemNameHelp, text: presentationData.strings.EditProfile_NameAndPhotoOrVideoHelp)) items[.help]!.append(PeerInfoScreenCommentItem(id: ItemNameHelp, text: presentationData.strings.EditProfile_NameAndPhotoOrVideoHelp))
if let cachedData = data.cachedData as? CachedUserData { 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) interaction.updateBio(updatedText)
}, action: { }, action: {
interaction.dismissInput() interaction.dismissInput()
}, maxLength: Int(data.globalSettings?.userLimits.maxAboutLength ?? 70))) }, 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() interaction.openBioPrivacy()
})) }))
} }
var birthday: TelegramBirthday? var birthday: TelegramBirthday?
if let updatingBirthDate = state.updatingBirthDate { if let updatingBirthDate = state.updatingBirthDate {
birthday = updatingBirthDate birthday = updatingBirthDate
@ -414,14 +427,22 @@ func settingsEditingItems(data: PeerInfoScreenData?, state: PeerInfoState, conte
})) }))
} }
var birthdayPrivacyInfo = ""
var birthdayIsForContactsOnly = false if let birthdayPrivacy = data.globalSettings?.privacySettings?.birthday {
if let birthdayPrivacy = data.globalSettings?.privacySettings?.birthday, case .enableContacts = birthdayPrivacy { switch birthdayPrivacy {
birthdayIsForContactsOnly = true 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 { 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: { 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: {

View file

@ -0,0 +1,12 @@
{
"images" : [
{
"filename" : "incoming_call_20.pdf",
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

View file

@ -0,0 +1,12 @@
{
"images" : [
{
"filename" : "outgoing_call_20.pdf",
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

View file

@ -0,0 +1,12 @@
{
"images" : [
{
"filename" : "incoming_video_call_20.pdf",
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

View file

@ -0,0 +1,12 @@
{
"images" : [
{
"filename" : "outgoing_video_call_20.pdf",
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

View file

@ -0,0 +1,12 @@
{
"images" : [
{
"filename" : "gamepad_20.pdf",
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

View file

@ -0,0 +1,12 @@
{
"images" : [
{
"filename" : "checklist_20.pdf",
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

View file

@ -45,6 +45,7 @@ import RecaptchaEnterprise
import NavigationBarImpl import NavigationBarImpl
import ContextUI import ContextUI
import ContextControllerImpl import ContextControllerImpl
import AutomationBridge
#if canImport(AppCenter) #if canImport(AppCenter)
import 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 return true
} }

View file

@ -10270,7 +10270,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
if peerId == strongSelf.context.account.peerId { if peerId == strongSelf.context.account.peerId {
mode = .reminders mode = .reminders
} else { } else {
mode = .scheduledMessages(sendWhenOnlineAvailable: sendWhenOnlineAvailable) mode = .scheduledMessages(peerId: peer.id, sendWhenOnlineAvailable: sendWhenOnlineAvailable)
} }
let controller = ChatScheduleTimeScreen( let controller = ChatScheduleTimeScreen(

View file

@ -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 { guard let peerId = self.chatLocation.peerId else {
return return
} }
@ -325,14 +325,17 @@ extension ChatControllerImpl {
initialUserBannedRights[participant.peerId] = InitialBannedRights(value: nil) initialUserBannedRights[participant.peerId] = InitialBannedRights(value: nil)
} }
} }
self.push(AdminUserActionsSheet(
context: self.context, let mode: AdminUserActionsSheet.Mode
chatPeer: chatPeer, if reaction {
peers: [RenderedChannelParticipant( mode = .chatReaction(completion: { [weak self] result in
participant: participant, guard let self else {
peer: authorPeer._asPeer() return
)], }
mode: .chat( self.applyAdminUserActionsResult(messageIds: messageIds, result: result, initialUserBannedRights: initialUserBannedRights)
})
} else {
mode = .chat(
messageCount: messageIds.count, messageCount: messageIds.count,
deleteAllMessageCount: deleteAllMessageCount, deleteAllMessageCount: deleteAllMessageCount,
completion: { [weak self] result in completion: { [weak self] result in
@ -342,6 +345,16 @@ extension ChatControllerImpl {
self.applyAdminUserActionsResult(messageIds: messageIds, result: result, initialUserBannedRights: initialUserBannedRights) 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
)) ))
}) })
})) }))

View file

@ -2232,7 +2232,7 @@ class ChatControllerNode: ASDisplayNode, ASScrollViewDelegate {
additionalOffset = 80.0 additionalOffset = 80.0
} }
if let _ = inputPanelSize { if let _ = inputPanelSize {
inputPanelHideOffset += -48.0 - additionalOffset inputPanelHideOffset += -56.0 - additionalOffset
} }
if let accessoryPanelSize = accessoryPanelSize { if let accessoryPanelSize = accessoryPanelSize {
inputPanelHideOffset += -accessoryPanelSize.height - additionalOffset inputPanelHideOffset += -accessoryPanelSize.height - additionalOffset

View file

@ -198,7 +198,10 @@ extension ChatControllerImpl {
animationCache: self.controllerInteraction!.presentationContext.animationCache, animationCache: self.controllerInteraction!.presentationContext.animationCache,
animationRenderer: self.controllerInteraction!.presentationContext.animationRenderer, animationRenderer: self.controllerInteraction!.presentationContext.animationRenderer,
message: EngineMessage(message), 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 dismissController?({ [weak self] in
guard let self else { guard let self else {
return return
@ -206,6 +209,31 @@ extension ChatControllerImpl {
self.openPeer(peer: peer, navigation: .default, fromMessage: MessageReference(message), fromReactionMessageId: hasReaction ? message.id : nil) 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 { } else {
@ -297,7 +325,9 @@ extension ChatControllerImpl {
let presentationContext = self.controllerInteraction?.presentationContext let presentationContext = self.controllerInteraction?.presentationContext
let premiumConfiguration = PremiumConfiguration.with(appConfiguration: context.currentAppConfiguration.with { $0 }) 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) items.tip = .animatedEmoji(text: nil, arguments: nil, file: nil, action: nil)
if packReferences.count > 1 { if packReferences.count > 1 {

View file

@ -92,6 +92,59 @@ func chatShareToSavedMessagesAdditionalView(_ chatController: ChatControllerImpl
} }
extension 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) { func openMessageShareMenu(id: EngineMessage.Id) {
guard let messages = self.chatDisplayNode.historyNode.messageGroupInCurrentHistoryView(id), let message = messages.first else { guard let messages = self.chatDisplayNode.historyNode.messageGroupInCurrentHistoryView(id), let message = messages.first else {
return return

View file

@ -2189,6 +2189,30 @@ func contextMenuForChatPresentationInterfaceState(chatPresentationInterfaceState
c?.dismiss(completion: { c?.dismiss(completion: {
controllerInteraction.openPeer(peer, .default, MessageReference(message), hasReaction ? .reaction : .default) 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))) )), tip: tip)))
} else { } else {

View file

@ -256,11 +256,14 @@ func openChatMessageImpl(_ params: OpenChatMessageParams) -> Bool {
} }
} }
let fileReference: FileMediaReference = .message(message: MessageReference(params.message), media: file)
let subject: BrowserScreen.Subject let subject: BrowserScreen.Subject
if file.mimeType == "application/pdf" { if file.mimeType.contains("markdown") {
subject = .pdfDocument(file: .message(message: MessageReference(params.message), media: file), canShare: canShare) subject = .markdownDocument(file: fileReference, canShare: canShare)
} else if file.mimeType.contains("pdf") {
subject = .pdfDocument(file: fileReference, canShare: canShare)
} else { } 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) let controller = BrowserScreen(context: params.context, subject: subject)
controller.openDocument = { [weak controller] file, canShare in controller.openDocument = { [weak controller] file, canShare in

View file

@ -696,7 +696,7 @@ func openResolvedUrlImpl(
present(shareController, nil) present(shareController, nil)
context.sharedContext.applicationBindings.dismissNativeController() context.sharedContext.applicationBindings.dismissNativeController()
} else { } 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 controller.peerSelected = { peer, threadId in
continueWithPeer(peer.id, threadId) continueWithPeer(peer.id, threadId)
} }

View file

@ -109,7 +109,7 @@ import EntityKeyboard
private final class AccountUserInterfaceInUseContext { private final class AccountUserInterfaceInUseContext {
let subscribers = Bag<(Bool) -> Void>() let subscribers = Bag<(Bool) -> Void>()
let tokens = Bag<Void>() let tokens = Bag<Void>()
var isEmpty: Bool { var isEmpty: Bool {
return self.tokens.isEmpty && self.subscribers.isEmpty 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) { 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()) assert(Queue.mainQueue().isCurrent())
precondition(!testHasInstance) precondition(!testHasInstance)
testHasInstance = true testHasInstance = true

View file

@ -83,6 +83,50 @@ public func chatInputStateStringWithAppliedEntities(_ text: String, entities: [M
private let syntaxHighlighter = Syntaxer() 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 { 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 let baseQuoteTintColor = baseQuoteTintColor ?? baseColor
@ -399,8 +443,10 @@ public func stringWithAppliedEntities(_ text: String, entities: [MessageTextEnti
let codeText = (string.string as NSString).substring(with: range) let codeText = (string.string as NSString).substring(with: range)
if let cachedMessageSyntaxHighlight, let entry = cachedMessageSyntaxHighlight.values[CachedMessageSyntaxHighlight.Spec(language: language, text: codeText)] { if let cachedMessageSyntaxHighlight, let entry = cachedMessageSyntaxHighlight.values[CachedMessageSyntaxHighlight.Spec(language: language, text: codeText)] {
for entity in entry.entities { if let validatedEntry = validatedCachedMessageSyntaxHighlight(entry, expectedLength: range.length, language: language) {
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)) 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) private let internalFixedCodeFont = Font.regular(17.0)
public func asyncUpdateMessageSyntaxHighlight(engine: TelegramEngine, messageId: EngineMessage.Id, current: CachedMessageSyntaxHighlight?, specs: [CachedMessageSyntaxHighlight.Spec]) -> Signal<Never, NoError> { 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 }) { if let current {
return .complete() 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 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) let theme = SyntaxterTheme(dark: false, textColor: .black, textFont: internalFixedCodeFont, italicFont: internalFixedCodeFont, mediumFont: internalFixedCodeFont)
for spec in specs { for spec in specs {
let expectedLength = (spec.text as NSString).length
if let value = current?.values[spec] { if let value = current?.values[spec] {
updated[spec] = value updated[spec] = validatedCachedMessageSyntaxHighlight(value, expectedLength: expectedLength, language: spec.language) ?? MessageSyntaxHighlight(entities: [])
} else { } else if let theme {
var entities: [MessageSyntaxHighlight.Entity] = [] updated[spec] = generateMessageSyntaxHighlight(spec: spec, theme: theme)
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)
} }
} }
@ -629,8 +674,18 @@ public func asyncUpdateMessageSyntaxHighlight(engine: TelegramEngine, messageId:
} }
public func asyncStanaloneSyntaxHighlight(current: CachedMessageSyntaxHighlight?, specs: [CachedMessageSyntaxHighlight.Spec]) -> Signal<CachedMessageSyntaxHighlight, NoError> { public func asyncStanaloneSyntaxHighlight(current: CachedMessageSyntaxHighlight?, specs: [CachedMessageSyntaxHighlight.Spec]) -> Signal<CachedMessageSyntaxHighlight, NoError> {
if let current, !specs.contains(where: { current.values[$0] == nil }) { if let current {
return .single(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 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) let theme = SyntaxterTheme(dark: false, textColor: .black, textFont: internalFixedCodeFont, italicFont: internalFixedCodeFont, mediumFont: internalFixedCodeFont)
for spec in specs { for spec in specs {
let expectedLength = (spec.text as NSString).length
if let value = current?.values[spec] { if let value = current?.values[spec] {
updated[spec] = value updated[spec] = validatedCachedMessageSyntaxHighlight(value, expectedLength: expectedLength, language: spec.language) ?? MessageSyntaxHighlight(entities: [])
} else { } else if let theme {
var entities: [MessageSyntaxHighlight.Entity] = [] updated[spec] = generateMessageSyntaxHighlight(spec: spec, theme: theme)
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)
} }
} }