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.PrivacyHelpEveryone" = "Everyone can see your bio. [Change >]()";
"Settings.About.PrivacyHelpContacts" = "Only uour contacts can see your bio. [Change >]()";
"Settings.About.PrivacyHelpContacts" = "Only your contacts can see your bio. [Change >]()";
"Settings.About.PrivacyHelpNobody" = "Nobody can see your bio. [Change >]()";
"Settings.Birthday.PrivacyHelpEveryone" = "Everyone can see your birthday. [Change >]()";
"Settings.Birthday.PrivacyHelpContacts" = "Only your contacts can see your birthday. [Change >]()";
"Settings.Birthday.PrivacyHelpNobody" = "Nobody can see your birthday. [Change >]()";

View file

@ -65,6 +65,8 @@ final class BrowserInstantPageContent: UIView, BrowserContent, UIScrollViewDeleg
var currentWebEmbedHeights: [Int : CGFloat] = [:]
var currentExpandedDetails: [Int : Bool]?
var currentDetailsItems: [InstantPageDetailsItem] = []
private var resolvedExternalMediaDimensions: [MediaId: PixelDimensions] = [:]
private var pendingResolvedExternalMediaDimensions = Set<MediaId>()
var currentAccessibilityAreas: [AccessibilityAreaNode] = []
@ -87,6 +89,7 @@ final class BrowserInstantPageContent: UIView, BrowserContent, UIScrollViewDeleg
private let loadWebpageDisposable = MetaDisposable()
private let resolveUrlDisposable = MetaDisposable()
private let updateLayoutDisposable = MetaDisposable()
private let updateExternalMediaDimensionsDisposable = MetaDisposable()
private let loadProgress = ValuePromise<CGFloat>(1.0, ignoreRepeated: true)
private let readingProgress = ValuePromise<CGFloat>(0.0, ignoreRepeated: true)
@ -179,12 +182,14 @@ final class BrowserInstantPageContent: UIView, BrowserContent, UIScrollViewDeleg
}
self.scrollNode.view.addGestureRecognizer(recognizer)
self.webpageDisposable = (actualizedWebpage(account: context.account, webpage: webPage) |> deliverOnMainQueue).start(next: { [weak self] result in
guard let self else {
return
}
self.updateWebPage(result, anchor: self.initialAnchor)
})
if case let .Loaded(content) = webPage.content, let scheme = URL(string: content.url)?.scheme?.lowercased(), scheme == "http" || scheme == "https" {
self.webpageDisposable = (actualizedWebpage(account: context.account, webpage: webPage) |> deliverOnMainQueue).start(next: { [weak self] result in
guard let self else {
return
}
self.updateWebPage(result, anchor: self.initialAnchor)
})
}
}
deinit {
@ -193,6 +198,7 @@ final class BrowserInstantPageContent: UIView, BrowserContent, UIScrollViewDeleg
self.loadWebpageDisposable.dispose()
self.resolveUrlDisposable.dispose()
self.updateLayoutDisposable.dispose()
self.updateExternalMediaDimensionsDisposable.dispose()
}
required init?(coder: NSCoder) {
@ -304,6 +310,8 @@ final class BrowserInstantPageContent: UIView, BrowserContent, UIScrollViewDeleg
} else {
self.webPage = nil
}
self.resolvedExternalMediaDimensions.removeAll()
self.pendingResolvedExternalMediaDimensions.removeAll()
if let anchor = anchor {
self.initialAnchor = anchor.removingPercentEncoding
} else if let state = state {
@ -478,17 +486,12 @@ final class BrowserInstantPageContent: UIView, BrowserContent, UIScrollViewDeleg
}
private func updatePageLayout() {
guard let (size, insets, _) = self.containerLayout, let (webPage, instantPage) = self.webPage else {
guard let (size, insets, _) = self.containerLayout, let (webPage, instantPage) = self.resolvedWebPage() else {
return
}
let currentLayout = instantPageLayoutForWebPage(webPage, instantPage: instantPage, userLocation: self.sourceLocation.userLocation, boundingWidth: size.width, safeInset: insets.left, strings: self.presentationData.strings, theme: self.theme, dateTimeFormat: self.presentationData.dateTimeFormat, webEmbedHeights: self.currentWebEmbedHeights)
for (_, tileNode) in self.visibleTiles {
tileNode.removeFromSupernode()
}
self.visibleTiles.removeAll()
let currentLayoutTiles = instantPageTilesFromLayout(currentLayout, boundingWidth: size.width)
var currentDetailsItems: [InstantPageDetailsItem] = []
@ -655,6 +658,7 @@ final class BrowserInstantPageContent: UIView, BrowserContent, UIScrollViewDeleg
topNode = newNode
self.visibleItemsWithNodes[itemIndex] = newNode
itemNode = newNode
self.configureExternalMediaDimensionsUpdates(for: newNode)
if let itemNode = itemNode as? InstantPageDetailsNode {
itemNode.requestLayoutUpdate = { [weak self] animated in
@ -678,6 +682,10 @@ final class BrowserInstantPageContent: UIView, BrowserContent, UIScrollViewDeleg
}
}
if let itemNode = itemNode {
self.configureExternalMediaDimensionsUpdates(for: itemNode)
}
if let itemNode = itemNode as? InstantPageDetailsNode {
itemNode.updateVisibleItems(visibleBounds: visibleBounds.offsetBy(dx: -itemNode.frame.minX, dy: -itemNode.frame.minY), animated: animated)
}
@ -708,8 +716,11 @@ final class BrowserInstantPageContent: UIView, BrowserContent, UIScrollViewDeleg
topNode = tileNode
self.visibleTiles[tileIndex] = tileNode
} else {
if visibleTiles[tileIndex]!.frame != tileFrame {
transition.updateFrame(node: self.visibleTiles[tileIndex]!, frame: tileFrame)
if let tileNode = self.visibleTiles[tileIndex] {
tileNode.update(tile: tile, backgroundColor: theme.pageBackgroundColor)
if tileNode.frame != tileFrame {
transition.updateFrame(node: tileNode, frame: tileFrame)
}
}
}
}
@ -929,6 +940,385 @@ final class BrowserInstantPageContent: UIView, BrowserContent, UIScrollViewDeleg
return nil
}
private func configureExternalMediaDimensionsUpdates(for itemNode: InstantPageNode) {
let update: (MediaId, PixelDimensions) -> Void = { [weak self] mediaId, dimensions in
self?.updateExternalMediaDimensions(mediaId, dimensions)
}
if let itemNode = itemNode as? InstantPageExternalMediaDimensionsNode {
itemNode.updateExternalMediaDimensions = update
}
if let itemNode = itemNode as? InstantPageDetailsNode {
itemNode.contentNode.updateExternalMediaDimensions = update
}
}
private func updateExternalMediaDimensions(_ mediaId: MediaId, _ dimensions: PixelDimensions) {
if self.resolvedExternalMediaDimensions[mediaId] == dimensions {
return
}
self.resolvedExternalMediaDimensions[mediaId] = dimensions
self.pendingResolvedExternalMediaDimensions.insert(mediaId)
let signal: Signal<Void, NoError> = (.complete() |> delay(0.08, queue: Queue.mainQueue()))
self.updateExternalMediaDimensionsDisposable.set(signal.start(completed: { [weak self] in
self?.relayoutForResolvedExternalMediaDimensions()
}))
}
private func relayoutForResolvedExternalMediaDimensions() {
guard !self.pendingResolvedExternalMediaDimensions.isEmpty else {
return
}
let mediaIds = Array(self.pendingResolvedExternalMediaDimensions)
self.pendingResolvedExternalMediaDimensions.removeAll()
let detailsStateMaps = self.captureExpandedDetailsStateMaps()
let viewportTop = self.scrollNode.view.contentOffset.y + self.scrollNode.view.contentInset.top
var oldFrames: [MediaId: CGRect] = [:]
for mediaId in mediaIds {
if let frame = self.effectiveFrameForMedia(mediaId, detailsStateMaps: detailsStateMaps) {
oldFrames[mediaId] = frame
}
}
self.updatePageLayout()
var newFrames: [MediaId: CGRect] = [:]
for mediaId in mediaIds {
if let frame = self.effectiveFrameForMedia(mediaId, detailsStateMaps: detailsStateMaps) {
newFrames[mediaId] = frame
}
}
if let compensatedViewportTop = self.compensatedViewportTop(oldFrames: oldFrames, newFrames: newFrames, viewportTop: viewportTop) {
self.setViewportTop(compensatedViewportTop)
}
self.updateVisibleItems(visibleBounds: self.scrollNode.view.bounds)
}
private func setViewportTop(_ viewportTop: CGFloat) {
let scrollView = self.scrollNode.view
let minOffsetY = -scrollView.contentInset.top
let maxOffsetY = max(minOffsetY, scrollView.contentSize.height - scrollView.bounds.height + scrollView.contentInset.bottom)
let contentOffsetY = min(max(viewportTop - scrollView.contentInset.top, minOffsetY), maxOffsetY)
if contentOffsetY.isFinite {
scrollView.contentOffset = CGPoint(x: scrollView.contentOffset.x, y: contentOffsetY)
}
}
private func compensatedViewportTop(oldFrames: [MediaId: CGRect], newFrames: [MediaId: CGRect], viewportTop: CGFloat) -> CGFloat? {
var pairedFrames: [(old: CGRect, new: CGRect)] = []
for (mediaId, oldFrame) in oldFrames {
if let newFrame = newFrames[mediaId] {
pairedFrames.append((oldFrame, newFrame))
}
}
if pairedFrames.isEmpty {
return nil
}
if let intersecting = pairedFrames
.filter({ $0.old.height > 0.0 && $0.new.height > 0.0 && viewportTop > $0.old.minY && viewportTop < $0.old.maxY })
.max(by: { $0.old.minY < $1.old.minY }) {
let ratio = min(max((viewportTop - intersecting.old.minY) / intersecting.old.height, 0.0), 1.0)
return intersecting.new.minY + ratio * intersecting.new.height
}
if let above = pairedFrames
.filter({ viewportTop >= $0.old.maxY })
.max(by: { $0.old.maxY < $1.old.maxY }) {
return viewportTop + (above.new.maxY - above.old.maxY)
}
return nil
}
private func captureExpandedDetailsStateMaps() -> [String: [Int: Bool]] {
guard let currentLayout = self.currentLayout else {
return [:]
}
var result: [String: [Int: Bool]] = [:]
self.captureExpandedDetailsStateMaps(items: currentLayout.items, visibleItemsWithNodes: self.visibleItemsWithNodes, path: [], result: &result)
return result
}
private func captureExpandedDetailsStateMaps(items: [InstantPageItem], visibleItemsWithNodes: [Int: InstantPageNode], path: [Int], result: inout [String: [Int: Bool]]) {
let detailsNodes = visibleItemsWithNodes.compactMap { $0.value as? InstantPageDetailsNode }
var detailsIndex = -1
for item in items {
guard let detailsItem = item as? InstantPageDetailsItem else {
continue
}
detailsIndex += 1
guard let detailsNode = detailsNodes.first(where: { $0.item === detailsItem }) else {
continue
}
let nextPath = path + [detailsIndex]
result[self.detailsStateKey(nextPath)] = detailsNode.contentNode.currentExpandedDetails ?? [:]
self.captureExpandedDetailsStateMaps(items: detailsItem.items, visibleItemsWithNodes: detailsNode.contentNode.visibleItemsWithNodes, path: nextPath, result: &result)
}
}
private func detailsStateKey(_ path: [Int]) -> String {
if path.isEmpty {
return ""
}
return path.map(String.init).joined(separator: ".")
}
private func effectiveFrameForMedia(_ mediaId: MediaId, detailsStateMaps: [String: [Int: Bool]]) -> CGRect? {
guard let currentLayout = self.currentLayout else {
return nil
}
return self.effectiveFrameForMedia(mediaId, items: currentLayout.items, origin: .zero, expandedDetails: self.currentExpandedDetails, path: [], detailsStateMaps: detailsStateMaps)
}
private func effectiveFrameForMedia(_ mediaId: MediaId, items: [InstantPageItem], origin: CGPoint, expandedDetails: [Int: Bool]?, path: [Int], detailsStateMaps: [String: [Int: Bool]]) -> CGRect? {
var collapseOffset: CGFloat = 0.0
var detailsIndex = -1
for item in items {
if item is InstantPageDetailsItem {
detailsIndex += 1
}
var itemFrame = item.frame.offsetBy(dx: origin.x, dy: origin.y - collapseOffset)
if let detailsItem = item as? InstantPageDetailsItem {
let nextPath = path + [detailsIndex]
let nestedExpandedDetails = detailsStateMaps[self.detailsStateKey(nextPath)]
let expanded = expandedDetails?[detailsIndex] ?? detailsItem.initiallyExpanded
let height = expanded ? detailsItem.titleHeight + self.effectiveContentHeight(items: detailsItem.items, baseHeight: detailsItem.frame.height - detailsItem.titleHeight, expandedDetails: nestedExpandedDetails, path: nextPath, detailsStateMaps: detailsStateMaps) : detailsItem.titleHeight
collapseOffset += item.frame.height - height
itemFrame.size.height = height
if expanded, let nestedFrame = self.effectiveFrameForMedia(mediaId, items: detailsItem.items, origin: CGPoint(x: itemFrame.minX, y: itemFrame.minY + detailsItem.titleHeight), expandedDetails: nestedExpandedDetails, path: nextPath, detailsStateMaps: detailsStateMaps) {
return nestedFrame
}
continue
}
if self.itemContainsMedia(item, mediaId: mediaId) {
return itemFrame
}
}
return nil
}
private func effectiveContentHeight(items: [InstantPageItem], baseHeight: CGFloat, expandedDetails: [Int: Bool]?, path: [Int], detailsStateMaps: [String: [Int: Bool]]) -> CGFloat {
var contentHeight = baseHeight
var detailsIndex = -1
for item in items {
guard let detailsItem = item as? InstantPageDetailsItem else {
continue
}
detailsIndex += 1
let nextPath = path + [detailsIndex]
let nestedExpandedDetails = detailsStateMaps[self.detailsStateKey(nextPath)]
let expanded = expandedDetails?[detailsIndex] ?? detailsItem.initiallyExpanded
let height = expanded ? detailsItem.titleHeight + self.effectiveContentHeight(items: detailsItem.items, baseHeight: detailsItem.frame.height - detailsItem.titleHeight, expandedDetails: nestedExpandedDetails, path: nextPath, detailsStateMaps: detailsStateMaps) : detailsItem.titleHeight
contentHeight += -detailsItem.frame.height + height
}
return contentHeight
}
private func itemContainsMedia(_ item: InstantPageItem, mediaId: MediaId) -> Bool {
for media in item.medias {
if media.media.id == mediaId {
return true
}
}
return false
}
private func resolvedWebPage() -> (webPage: TelegramMediaWebpage, instantPage: InstantPage?)? {
guard let (webPage, instantPage) = self.webPage else {
return nil
}
guard !self.resolvedExternalMediaDimensions.isEmpty, case let .Loaded(content) = webPage.content else {
return (webPage, instantPage)
}
var instantPageUpdated = false
var effectiveInstantPage = instantPage
if let instantPage {
var media = instantPage.media
for (mediaId, currentMedia) in instantPage.media {
if let updatedMedia = self.updatedMediaIfNeeded(currentMedia) {
media[mediaId] = updatedMedia
instantPageUpdated = true
}
}
if instantPageUpdated {
effectiveInstantPage = InstantPage(blocks: instantPage.blocks, media: media, isComplete: instantPage.isComplete, rtl: instantPage.rtl, url: instantPage.url, views: instantPage.views)
}
}
var imageUpdated = false
let effectiveImage = content.image.map { image -> TelegramMediaImage in
if let updated = self.updatedImageIfNeeded(image) {
imageUpdated = true
return updated
} else {
return image
}
}
var fileUpdated = false
let effectiveFile = content.file.map { file -> TelegramMediaFile in
if let updated = self.updatedFileIfNeeded(file) {
fileUpdated = true
return updated
} else {
return file
}
}
if !instantPageUpdated && !imageUpdated && !fileUpdated {
return (webPage, instantPage)
}
let effectiveContent = TelegramMediaWebpageLoadedContent(
url: content.url,
displayUrl: content.displayUrl,
hash: content.hash,
type: content.type,
websiteName: content.websiteName,
title: content.title,
text: content.text,
embedUrl: content.embedUrl,
embedType: content.embedType,
embedSize: content.embedSize,
duration: content.duration,
author: content.author,
isMediaLargeByDefault: content.isMediaLargeByDefault,
imageIsVideoCover: content.imageIsVideoCover,
image: effectiveImage,
file: effectiveFile,
story: content.story,
attributes: content.attributes,
instantPage: effectiveInstantPage
)
return (TelegramMediaWebpage(webpageId: webPage.webpageId, content: .Loaded(effectiveContent)), effectiveInstantPage)
}
private func updatedMediaIfNeeded(_ media: Media) -> Media? {
if let image = media as? TelegramMediaImage {
return self.updatedImageIfNeeded(image)
} else if let file = media as? TelegramMediaFile {
return self.updatedFileIfNeeded(file)
} else {
return nil
}
}
private func updatedImageIfNeeded(_ image: TelegramMediaImage) -> TelegramMediaImage? {
guard let dimensions = self.resolvedExternalMediaDimensions[image.imageId] else {
return nil
}
var updatedRepresentations = image.representations
var didUpdate = false
for i in 0 ..< updatedRepresentations.count {
let representation = updatedRepresentations[i]
guard representation.resource is InstantPageExternalMediaResource, representation.dimensions != dimensions else {
continue
}
updatedRepresentations[i] = TelegramMediaImageRepresentation(
dimensions: dimensions,
resource: representation.resource,
progressiveSizes: representation.progressiveSizes,
immediateThumbnailData: representation.immediateThumbnailData,
hasVideo: representation.hasVideo,
isPersonal: representation.isPersonal,
typeHint: representation.typeHint
)
didUpdate = true
}
guard didUpdate else {
return nil
}
return TelegramMediaImage(
imageId: image.imageId,
representations: updatedRepresentations,
videoRepresentations: image.videoRepresentations,
immediateThumbnailData: image.immediateThumbnailData,
emojiMarkup: image.emojiMarkup,
reference: image.reference,
partialReference: image.partialReference,
flags: image.flags,
video: image.video
)
}
private func updatedFileIfNeeded(_ file: TelegramMediaFile) -> TelegramMediaFile? {
guard let dimensions = self.resolvedExternalMediaDimensions[file.fileId], file.resource is InstantPageExternalMediaResource else {
return nil
}
let (attributes, didUpdate) = self.fileAttributesWithResolvedDimensions(file.attributes, dimensions: dimensions)
guard didUpdate else {
return nil
}
return TelegramMediaFile(
fileId: file.fileId,
partialReference: file.partialReference,
resource: file.resource,
previewRepresentations: file.previewRepresentations,
videoThumbnails: file.videoThumbnails,
videoCover: file.videoCover,
immediateThumbnailData: file.immediateThumbnailData,
mimeType: file.mimeType,
size: file.size,
attributes: attributes,
alternativeRepresentations: file.alternativeRepresentations
)
}
private func fileAttributesWithResolvedDimensions(_ attributes: [TelegramMediaFileAttribute], dimensions: PixelDimensions) -> ([TelegramMediaFileAttribute], Bool) {
var updatedAttributes: [TelegramMediaFileAttribute] = []
var didUpdate = false
var hasSizeAttribute = false
for attribute in attributes {
switch attribute {
case let .ImageSize(size):
hasSizeAttribute = true
if size != dimensions {
updatedAttributes.append(.ImageSize(size: dimensions))
didUpdate = true
} else {
updatedAttributes.append(attribute)
}
case let .Video(duration, size, flags, preloadSize, coverTime, videoCodec):
hasSizeAttribute = true
if size != dimensions {
updatedAttributes.append(.Video(duration: duration, size: dimensions, flags: flags, preloadSize: preloadSize, coverTime: coverTime, videoCodec: videoCodec))
didUpdate = true
} else {
updatedAttributes.append(attribute)
}
default:
updatedAttributes.append(attribute)
}
}
if !hasSizeAttribute {
updatedAttributes.append(.ImageSize(size: dimensions))
didUpdate = true
}
return (updatedAttributes, didUpdate)
}
private func openUrl(_ url: InstantPageUrlItem) {
var baseUrl = url.url
var anchor: String?

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(
transition: .immediate,
component: AnyComponent(
Text(text: "\(self.pageNumber?.0 ?? 1) of \(self.pageNumber?.1 ?? 1)", font: Font.with(size: 15.0, weight: .regular, traits: .monospacedNumbers), color: self.presentationData.theme.list.itemPrimaryTextColor)
Text(text: self.presentationData.strings.Items_NOfM("\(self.pageNumber?.0 ?? 1)", "\(self.pageNumber?.1 ?? 1)").string, font: Font.with(size: 15.0, weight: .regular, traits: .monospacedNumbers), color: self.presentationData.theme.list.itemPrimaryTextColor)
),
environment: {},
containerSize: size

View file

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

View file

@ -9,6 +9,13 @@ import ContextReferenceButtonComponent
import GlassBackgroundComponent
import EdgeEffect
enum BrowserToolbarMode: Equatable {
case webPage
case instantPage
case document
case markdown
}
final class BrowserToolbarComponent: CombinedComponent {
let theme: PresentationTheme
let bottomInset: CGFloat
@ -131,7 +138,7 @@ final class NavigationToolbarContentComponent: CombinedComponent {
let canGoForward: Bool
let canOpenIn: Bool
let canShare: Bool
let isDocument: Bool
let mode: BrowserToolbarMode
let performAction: ActionSlot<BrowserScreen.Action>
let performHoldAction: (UIView, ContextGesture?, BrowserScreen.Action) -> Void
@ -141,7 +148,7 @@ final class NavigationToolbarContentComponent: CombinedComponent {
canGoForward: Bool,
canOpenIn: Bool,
canShare: Bool,
isDocument: Bool,
mode: BrowserToolbarMode,
performAction: ActionSlot<BrowserScreen.Action>,
performHoldAction: @escaping (UIView, ContextGesture?, BrowserScreen.Action) -> Void
) {
@ -150,7 +157,7 @@ final class NavigationToolbarContentComponent: CombinedComponent {
self.canGoForward = canGoForward
self.canOpenIn = canOpenIn
self.canShare = canShare
self.isDocument = isDocument
self.mode = mode
self.performAction = performAction
self.performHoldAction = performHoldAction
}
@ -171,7 +178,7 @@ final class NavigationToolbarContentComponent: CombinedComponent {
if lhs.canShare != rhs.canShare {
return false
}
if lhs.isDocument != rhs.isDocument {
if lhs.mode != rhs.mode {
return false
}
return true
@ -217,7 +224,8 @@ final class NavigationToolbarContentComponent: CombinedComponent {
transition: .easeInOut(duration: 0.2)
)
if context.component.isDocument {
switch context.component.mode {
case .document:
var originX: CGFloat = sideInset
let search = search.update(
@ -270,7 +278,40 @@ final class NavigationToolbarContentComponent: CombinedComponent {
.position(CGPoint(x: originX, y: availableSize.height / 2.0))
)
size.width = originX + sideInset
} else {
case .markdown:
var originX: CGFloat = sideInset
if !context.component.canShare {
context.add(share
.position(CGPoint(x: availableSize.width / 2.0, y: 10000.0))
)
} else {
context.add(share
.position(CGPoint(x: originX, y: availableSize.height / 2.0))
)
originX += spacing
}
let quickLook = quickLook.update(
component: Button(
content: AnyComponent(
BundleIconComponent(
name: "Instant View/OpenDocument",
tintColor: textColor
)
),
action: {
performAction.invoke(.openIn)
}
).minSize(buttonSize),
availableSize: buttonSize,
transition: .easeInOut(duration: 0.2)
)
context.add(quickLook
.position(CGPoint(x: originX, y: availableSize.height / 2.0))
)
size.width = originX + sideInset
case .webPage, .instantPage:
var originX: CGFloat = sideInset
let canGoBack = context.component.canGoBack

View file

@ -23,8 +23,6 @@ func fetchFavicon(context: AccountContext, url: String, size: CGSize) -> Signal<
if let data {
if let image = UIImage(data: data) {
return image
} else if url.lowercased().contains(".svg"), let preparedData = prepareSvgImage(data, false), let image = renderPreparedImage(preparedData, size, .clear, UIScreenScale, false) {
return image
}
return nil
} else {

View file

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

View file

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

View file

@ -383,8 +383,15 @@ public final class ReactionListContextMenuContent: ContextControllerItemsContent
private var iconFrame: CGRect?
private var file: TelegramMediaFile?
private var fileDisposable: Disposable?
private var longTapRecognizer: UILongPressGestureRecognizer?
private var skipNextTapAction = false
let action: () -> Void
var longTapAction: (() -> Void)? {
didSet {
self.longTapRecognizer?.isEnabled = self.longTapAction != nil
}
}
private var item: EngineMessageReactionListContext.Item?
@ -422,6 +429,12 @@ public final class ReactionListContextMenuContent: ContextControllerItemsContent
self.view.addSubview(self.readIconView)
self.addTarget(self, action: #selector(self.pressed), forControlEvents: .touchUpInside)
let longTapRecognizer = UILongPressGestureRecognizer(target: self, action: #selector(self.longTapGesture(_:)))
longTapRecognizer.isEnabled = false
longTapRecognizer.cancelsTouchesInView = true
self.view.addGestureRecognizer(longTapRecognizer)
self.longTapRecognizer = longTapRecognizer
}
deinit {
@ -429,9 +442,30 @@ public final class ReactionListContextMenuContent: ContextControllerItemsContent
}
@objc private func pressed() {
if self.skipNextTapAction {
self.skipNextTapAction = false
return
}
self.action()
}
@objc private func longTapGesture(_ recognizer: UILongPressGestureRecognizer) {
switch recognizer.state {
case .began:
guard let item = self.item, item.reaction != nil else {
return
}
self.skipNextTapAction = true
self.longTapAction?()
case .cancelled, .ended, .failed:
DispatchQueue.main.async { [weak self] in
self?.skipNextTapAction = false
}
default:
break
}
}
private func updateReactionLayer() {
guard let file = self.file else {
return
@ -771,6 +805,7 @@ public final class ReactionListContextMenuContent: ContextControllerItemsContent
private let requestUpdate: (ReactionsTabNode, ContainedViewLayoutTransition) -> Void
private let requestUpdateApparentHeight: (ReactionsTabNode, ContainedViewLayoutTransition) -> Void
private let openPeer: (EnginePeer, Bool) -> Void
private let deleteReaction: ((EnginePeer, MessageReaction.Reaction) -> Void)?
private var hasMore: Bool = false
@ -804,7 +839,8 @@ public final class ReactionListContextMenuContent: ContextControllerItemsContent
readStats: MessageReadStats?,
requestUpdate: @escaping (ReactionsTabNode, ContainedViewLayoutTransition) -> Void,
requestUpdateApparentHeight: @escaping (ReactionsTabNode, ContainedViewLayoutTransition) -> Void,
openPeer: @escaping (EnginePeer, Bool) -> Void
openPeer: @escaping (EnginePeer, Bool) -> Void,
deleteReaction: ((EnginePeer, MessageReaction.Reaction) -> Void)?
) {
self.context = context
self.displayReadTimestamps = displayReadTimestamps
@ -816,6 +852,7 @@ public final class ReactionListContextMenuContent: ContextControllerItemsContent
self.requestUpdate = requestUpdate
self.requestUpdateApparentHeight = requestUpdateApparentHeight
self.openPeer = openPeer
self.deleteReaction = deleteReaction
self.listContext = context.engine.messages.messageReactionList(message: message, readStats: readStats, reaction: reaction)
self.state = ItemsState(listState: EngineMessageReactionListContext.State(message: message, readStats: readStats, reaction: reaction), readStats: readStats)
@ -928,6 +965,14 @@ public final class ReactionListContextMenuContent: ContextControllerItemsContent
}
itemNode.update(size: itemFrame.size, presentationData: presentationData, item: item, isLast: self.state.item(at: index + 1) == nil, syncronousLoad: syncronousLoad)
if let deleteReaction = self.deleteReaction, let reaction = item.reaction {
let peer = item.peer
itemNode.longTapAction = {
deleteReaction(peer, reaction)
}
} else {
itemNode.longTapAction = nil
}
itemNode.frame = itemFrame
} else if index < self.state.totalCount {
validPlaceholderIds.insert(index)
@ -1079,6 +1124,7 @@ public final class ReactionListContextMenuContent: ContextControllerItemsContent
private let reactions: [(MessageReaction.Reaction?, Int)]
private let requestUpdate: (ContainedViewLayoutTransition) -> Void
private let requestUpdateApparentHeight: (ContainedViewLayoutTransition) -> Void
private let deleteReaction: ((EnginePeer, MessageReaction.Reaction) -> Void)?
private var presentationData: PresentationData
@ -1111,7 +1157,8 @@ public final class ReactionListContextMenuContent: ContextControllerItemsContent
requestUpdate: @escaping (ContainedViewLayoutTransition) -> Void,
requestUpdateApparentHeight: @escaping (ContainedViewLayoutTransition) -> Void,
back: (() -> Void)?,
openPeer: @escaping (EnginePeer, Bool) -> Void
openPeer: @escaping (EnginePeer, Bool) -> Void,
deleteReaction: ((EnginePeer, MessageReaction.Reaction) -> Void)?
) {
self.context = context
self.displayReadTimestamps = displayReadTimestamps
@ -1126,6 +1173,7 @@ public final class ReactionListContextMenuContent: ContextControllerItemsContent
self.requestUpdate = requestUpdate
self.requestUpdateApparentHeight = requestUpdateApparentHeight
self.deleteReaction = deleteReaction
if let back = back {
self.backButtonNode = BackButtonNode()
@ -1332,7 +1380,8 @@ public final class ReactionListContextMenuContent: ContextControllerItemsContent
strongSelf.requestUpdateApparentHeight(transition)
}
},
openPeer: self.openPeer
openPeer: self.openPeer,
deleteReaction: self.deleteReaction
)
self.addSubnode(tabNode)
self.visibleTabNodes[index] = tabNode
@ -1426,6 +1475,7 @@ public final class ReactionListContextMenuContent: ContextControllerItemsContent
let readStats: MessageReadStats?
let back: (() -> Void)?
let openPeer: (EnginePeer, Bool) -> Void
let deleteReaction: ((EnginePeer, MessageReaction.Reaction) -> Void)?
public init(
context: AccountContext,
@ -1437,7 +1487,8 @@ public final class ReactionListContextMenuContent: ContextControllerItemsContent
reaction: MessageReaction.Reaction?,
readStats: MessageReadStats?,
back: (() -> Void)?,
openPeer: @escaping (EnginePeer, Bool) -> Void
openPeer: @escaping (EnginePeer, Bool) -> Void,
deleteReaction: ((EnginePeer, MessageReaction.Reaction) -> Void)? = nil
) {
self.context = context
self.displayReadTimestamps = displayReadTimestamps
@ -1449,6 +1500,7 @@ public final class ReactionListContextMenuContent: ContextControllerItemsContent
self.readStats = readStats
self.back = back
self.openPeer = openPeer
self.deleteReaction = deleteReaction
}
public func node(
@ -1467,7 +1519,8 @@ public final class ReactionListContextMenuContent: ContextControllerItemsContent
requestUpdate: requestUpdate,
requestUpdateApparentHeight: requestUpdateApparentHeight,
back: self.back,
openPeer: self.openPeer
openPeer: self.openPeer,
deleteReaction: self.deleteReaction
)
}
}

View file

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

View file

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

View file

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

View file

@ -65,6 +65,8 @@ final class InstantPageControllerNode: ASDisplayNode, ASScrollViewDelegate {
var currentWebEmbedHeights: [Int : CGFloat] = [:]
var currentExpandedDetails: [Int : Bool]?
var currentDetailsItems: [InstantPageDetailsItem] = []
private var resolvedExternalMediaDimensions: [MediaId: PixelDimensions] = [:]
private var pendingResolvedExternalMediaDimensions = Set<MediaId>()
var currentAccessibilityAreas: [AccessibilityAreaNode] = []
@ -79,6 +81,7 @@ final class InstantPageControllerNode: ASDisplayNode, ASScrollViewDelegate {
private let loadProgressDisposable = MetaDisposable()
private let updateLayoutDisposable = MetaDisposable()
private let updateExternalMediaDimensionsDisposable = MetaDisposable()
private var themeReferenceDate: Date?
@ -224,6 +227,7 @@ final class InstantPageControllerNode: ASDisplayNode, ASScrollViewDelegate {
self.resolveUrlDisposable.dispose()
self.loadWebpageDisposable.dispose()
self.loadProgressDisposable.dispose()
self.updateExternalMediaDimensionsDisposable.dispose()
}
func update(settings: InstantPagePresentationSettings, themeSettings: PresentationThemeSettings?, strings: PresentationStrings) {
@ -362,6 +366,8 @@ final class InstantPageControllerNode: ASDisplayNode, ASScrollViewDelegate {
} else {
self.webPage = nil
}
self.resolvedExternalMediaDimensions.removeAll()
self.pendingResolvedExternalMediaDimensions.removeAll()
if let anchor = anchor {
self.initialAnchor = anchor.removingPercentEncoding
} else if let state = state {
@ -465,17 +471,12 @@ final class InstantPageControllerNode: ASDisplayNode, ASScrollViewDelegate {
}
private func updateLayout() {
guard let containerLayout = self.containerLayout, let (webPage, instantPage) = self.webPage, let theme = self.theme else {
guard let containerLayout = self.containerLayout, let (webPage, instantPage) = self.resolvedWebPage(), let theme = self.theme else {
return
}
let currentLayout = instantPageLayoutForWebPage(webPage, instantPage: instantPage, userLocation: self.sourceLocation.userLocation, boundingWidth: containerLayout.size.width, safeInset: containerLayout.safeInsets.left, strings: self.strings, theme: theme, dateTimeFormat: self.dateTimeFormat, webEmbedHeights: self.currentWebEmbedHeights)
for (_, tileNode) in self.visibleTiles {
tileNode.removeFromSupernode()
}
self.visibleTiles.removeAll()
let currentLayoutTiles = instantPageTilesFromLayout(currentLayout, boundingWidth: containerLayout.size.width)
var currentDetailsItems: [InstantPageDetailsItem] = []
@ -665,6 +666,7 @@ final class InstantPageControllerNode: ASDisplayNode, ASScrollViewDelegate {
topNode = newNode
self.visibleItemsWithNodes[itemIndex] = newNode
itemNode = newNode
self.configureExternalMediaDimensionsUpdates(for: newNode)
if let itemNode = itemNode as? InstantPageDetailsNode {
itemNode.requestLayoutUpdate = { [weak self] animated in
@ -688,6 +690,10 @@ final class InstantPageControllerNode: ASDisplayNode, ASScrollViewDelegate {
}
}
if let itemNode = itemNode {
self.configureExternalMediaDimensionsUpdates(for: itemNode)
}
if let itemNode = itemNode as? InstantPageDetailsNode {
itemNode.updateVisibleItems(visibleBounds: visibleBounds.offsetBy(dx: -itemNode.frame.minX, dy: -itemNode.frame.minY), animated: animated)
}
@ -718,8 +724,11 @@ final class InstantPageControllerNode: ASDisplayNode, ASScrollViewDelegate {
topNode = tileNode
self.visibleTiles[tileIndex] = tileNode
} else {
if visibleTiles[tileIndex]!.frame != tileFrame {
transition.updateFrame(node: self.visibleTiles[tileIndex]!, frame: tileFrame)
if let tileNode = self.visibleTiles[tileIndex] {
tileNode.update(tile: tile, backgroundColor: theme.pageBackgroundColor)
if tileNode.frame != tileFrame {
transition.updateFrame(node: tileNode, frame: tileFrame)
}
}
}
}
@ -1020,6 +1029,388 @@ final class InstantPageControllerNode: ASDisplayNode, ASScrollViewDelegate {
return nil
}
private func configureExternalMediaDimensionsUpdates(for itemNode: InstantPageNode) {
let update: (MediaId, PixelDimensions) -> Void = { [weak self] mediaId, dimensions in
self?.updateExternalMediaDimensions(mediaId, dimensions)
}
if let itemNode = itemNode as? InstantPageExternalMediaDimensionsNode {
itemNode.updateExternalMediaDimensions = update
}
if let itemNode = itemNode as? InstantPageDetailsNode {
itemNode.contentNode.updateExternalMediaDimensions = update
}
}
private func updateExternalMediaDimensions(_ mediaId: MediaId, _ dimensions: PixelDimensions) {
if self.resolvedExternalMediaDimensions[mediaId] == dimensions {
return
}
self.resolvedExternalMediaDimensions[mediaId] = dimensions
self.pendingResolvedExternalMediaDimensions.insert(mediaId)
let signal: Signal<Void, NoError> = (.complete() |> delay(0.08, queue: Queue.mainQueue()))
self.updateExternalMediaDimensionsDisposable.set(signal.start(completed: { [weak self] in
self?.relayoutForResolvedExternalMediaDimensions()
}))
}
private func relayoutForResolvedExternalMediaDimensions() {
guard !self.pendingResolvedExternalMediaDimensions.isEmpty else {
return
}
let mediaIds = Array(self.pendingResolvedExternalMediaDimensions)
self.pendingResolvedExternalMediaDimensions.removeAll()
let detailsStateMaps = self.captureExpandedDetailsStateMaps()
let viewportTop = self.scrollNode.view.contentOffset.y + self.scrollNode.view.contentInset.top
var oldFrames: [MediaId: CGRect] = [:]
for mediaId in mediaIds {
if let frame = self.effectiveFrameForMedia(mediaId, detailsStateMaps: detailsStateMaps) {
oldFrames[mediaId] = frame
}
}
self.updateLayout()
var newFrames: [MediaId: CGRect] = [:]
for mediaId in mediaIds {
if let frame = self.effectiveFrameForMedia(mediaId, detailsStateMaps: detailsStateMaps) {
newFrames[mediaId] = frame
}
}
if let compensatedViewportTop = self.compensatedViewportTop(oldFrames: oldFrames, newFrames: newFrames, viewportTop: viewportTop) {
self.setViewportTop(compensatedViewportTop)
}
self.updateVisibleItems(visibleBounds: self.scrollNode.view.bounds)
}
private func setViewportTop(_ viewportTop: CGFloat) {
let scrollView = self.scrollNode.view
let minOffsetY = -scrollView.contentInset.top
let maxOffsetY = max(minOffsetY, scrollView.contentSize.height - scrollView.bounds.height + scrollView.contentInset.bottom)
let contentOffsetY = min(max(viewportTop - scrollView.contentInset.top, minOffsetY), maxOffsetY)
if contentOffsetY.isFinite {
let contentOffset = CGPoint(x: scrollView.contentOffset.x, y: contentOffsetY)
scrollView.contentOffset = contentOffset
self.previousContentOffset = contentOffset
self.updateNavigationBar()
}
}
private func compensatedViewportTop(oldFrames: [MediaId: CGRect], newFrames: [MediaId: CGRect], viewportTop: CGFloat) -> CGFloat? {
var pairedFrames: [(old: CGRect, new: CGRect)] = []
for (mediaId, oldFrame) in oldFrames {
if let newFrame = newFrames[mediaId] {
pairedFrames.append((oldFrame, newFrame))
}
}
if pairedFrames.isEmpty {
return nil
}
if let intersecting = pairedFrames
.filter({ $0.old.height > 0.0 && $0.new.height > 0.0 && viewportTop > $0.old.minY && viewportTop < $0.old.maxY })
.max(by: { $0.old.minY < $1.old.minY }) {
let ratio = min(max((viewportTop - intersecting.old.minY) / intersecting.old.height, 0.0), 1.0)
return intersecting.new.minY + ratio * intersecting.new.height
}
if let above = pairedFrames
.filter({ viewportTop >= $0.old.maxY })
.max(by: { $0.old.maxY < $1.old.maxY }) {
return viewportTop + (above.new.maxY - above.old.maxY)
}
return nil
}
private func captureExpandedDetailsStateMaps() -> [String: [Int: Bool]] {
guard let currentLayout = self.currentLayout else {
return [:]
}
var result: [String: [Int: Bool]] = [:]
self.captureExpandedDetailsStateMaps(items: currentLayout.items, visibleItemsWithNodes: self.visibleItemsWithNodes, path: [], result: &result)
return result
}
private func captureExpandedDetailsStateMaps(items: [InstantPageItem], visibleItemsWithNodes: [Int: InstantPageNode], path: [Int], result: inout [String: [Int: Bool]]) {
let detailsNodes = visibleItemsWithNodes.compactMap { $0.value as? InstantPageDetailsNode }
var detailsIndex = -1
for item in items {
guard let detailsItem = item as? InstantPageDetailsItem else {
continue
}
detailsIndex += 1
guard let detailsNode = detailsNodes.first(where: { $0.item === detailsItem }) else {
continue
}
let nextPath = path + [detailsIndex]
result[self.detailsStateKey(nextPath)] = detailsNode.contentNode.currentExpandedDetails ?? [:]
self.captureExpandedDetailsStateMaps(items: detailsItem.items, visibleItemsWithNodes: detailsNode.contentNode.visibleItemsWithNodes, path: nextPath, result: &result)
}
}
private func detailsStateKey(_ path: [Int]) -> String {
if path.isEmpty {
return ""
}
return path.map(String.init).joined(separator: ".")
}
private func effectiveFrameForMedia(_ mediaId: MediaId, detailsStateMaps: [String: [Int: Bool]]) -> CGRect? {
guard let currentLayout = self.currentLayout else {
return nil
}
return self.effectiveFrameForMedia(mediaId, items: currentLayout.items, origin: .zero, expandedDetails: self.currentExpandedDetails, path: [], detailsStateMaps: detailsStateMaps)
}
private func effectiveFrameForMedia(_ mediaId: MediaId, items: [InstantPageItem], origin: CGPoint, expandedDetails: [Int: Bool]?, path: [Int], detailsStateMaps: [String: [Int: Bool]]) -> CGRect? {
var collapseOffset: CGFloat = 0.0
var detailsIndex = -1
for item in items {
if item is InstantPageDetailsItem {
detailsIndex += 1
}
var itemFrame = item.frame.offsetBy(dx: origin.x, dy: origin.y - collapseOffset)
if let detailsItem = item as? InstantPageDetailsItem {
let nextPath = path + [detailsIndex]
let nestedExpandedDetails = detailsStateMaps[self.detailsStateKey(nextPath)]
let expanded = expandedDetails?[detailsIndex] ?? detailsItem.initiallyExpanded
let height = expanded ? detailsItem.titleHeight + self.effectiveContentHeight(items: detailsItem.items, baseHeight: detailsItem.frame.height - detailsItem.titleHeight, expandedDetails: nestedExpandedDetails, path: nextPath, detailsStateMaps: detailsStateMaps) : detailsItem.titleHeight
collapseOffset += item.frame.height - height
itemFrame.size.height = height
if expanded, let nestedFrame = self.effectiveFrameForMedia(mediaId, items: detailsItem.items, origin: CGPoint(x: itemFrame.minX, y: itemFrame.minY + detailsItem.titleHeight), expandedDetails: nestedExpandedDetails, path: nextPath, detailsStateMaps: detailsStateMaps) {
return nestedFrame
}
continue
}
if self.itemContainsMedia(item, mediaId: mediaId) {
return itemFrame
}
}
return nil
}
private func effectiveContentHeight(items: [InstantPageItem], baseHeight: CGFloat, expandedDetails: [Int: Bool]?, path: [Int], detailsStateMaps: [String: [Int: Bool]]) -> CGFloat {
var contentHeight = baseHeight
var detailsIndex = -1
for item in items {
guard let detailsItem = item as? InstantPageDetailsItem else {
continue
}
detailsIndex += 1
let nextPath = path + [detailsIndex]
let nestedExpandedDetails = detailsStateMaps[self.detailsStateKey(nextPath)]
let expanded = expandedDetails?[detailsIndex] ?? detailsItem.initiallyExpanded
let height = expanded ? detailsItem.titleHeight + self.effectiveContentHeight(items: detailsItem.items, baseHeight: detailsItem.frame.height - detailsItem.titleHeight, expandedDetails: nestedExpandedDetails, path: nextPath, detailsStateMaps: detailsStateMaps) : detailsItem.titleHeight
contentHeight += -detailsItem.frame.height + height
}
return contentHeight
}
private func itemContainsMedia(_ item: InstantPageItem, mediaId: MediaId) -> Bool {
for media in item.medias {
if media.media.id == mediaId {
return true
}
}
return false
}
private func resolvedWebPage() -> (webPage: TelegramMediaWebpage, instantPage: InstantPage?)? {
guard let (webPage, instantPage) = self.webPage else {
return nil
}
guard !self.resolvedExternalMediaDimensions.isEmpty, case let .Loaded(content) = webPage.content else {
return (webPage, instantPage)
}
var instantPageUpdated = false
var effectiveInstantPage = instantPage
if let instantPage {
var media = instantPage.media
for (mediaId, currentMedia) in instantPage.media {
if let updatedMedia = self.updatedMediaIfNeeded(currentMedia) {
media[mediaId] = updatedMedia
instantPageUpdated = true
}
}
if instantPageUpdated {
effectiveInstantPage = InstantPage(blocks: instantPage.blocks, media: media, isComplete: instantPage.isComplete, rtl: instantPage.rtl, url: instantPage.url, views: instantPage.views)
}
}
var imageUpdated = false
let effectiveImage = content.image.map { image -> TelegramMediaImage in
if let updated = self.updatedImageIfNeeded(image) {
imageUpdated = true
return updated
} else {
return image
}
}
var fileUpdated = false
let effectiveFile = content.file.map { file -> TelegramMediaFile in
if let updated = self.updatedFileIfNeeded(file) {
fileUpdated = true
return updated
} else {
return file
}
}
if !instantPageUpdated && !imageUpdated && !fileUpdated {
return (webPage, instantPage)
}
let effectiveContent = TelegramMediaWebpageLoadedContent(
url: content.url,
displayUrl: content.displayUrl,
hash: content.hash,
type: content.type,
websiteName: content.websiteName,
title: content.title,
text: content.text,
embedUrl: content.embedUrl,
embedType: content.embedType,
embedSize: content.embedSize,
duration: content.duration,
author: content.author,
isMediaLargeByDefault: content.isMediaLargeByDefault,
imageIsVideoCover: content.imageIsVideoCover,
image: effectiveImage,
file: effectiveFile,
story: content.story,
attributes: content.attributes,
instantPage: effectiveInstantPage
)
return (TelegramMediaWebpage(webpageId: webPage.webpageId, content: .Loaded(effectiveContent)), effectiveInstantPage)
}
private func updatedMediaIfNeeded(_ media: Media) -> Media? {
if let image = media as? TelegramMediaImage {
return self.updatedImageIfNeeded(image)
} else if let file = media as? TelegramMediaFile {
return self.updatedFileIfNeeded(file)
} else {
return nil
}
}
private func updatedImageIfNeeded(_ image: TelegramMediaImage) -> TelegramMediaImage? {
guard let dimensions = self.resolvedExternalMediaDimensions[image.imageId] else {
return nil
}
var updatedRepresentations = image.representations
var didUpdate = false
for i in 0 ..< updatedRepresentations.count {
let representation = updatedRepresentations[i]
guard representation.resource is InstantPageExternalMediaResource, representation.dimensions != dimensions else {
continue
}
updatedRepresentations[i] = TelegramMediaImageRepresentation(
dimensions: dimensions,
resource: representation.resource,
progressiveSizes: representation.progressiveSizes,
immediateThumbnailData: representation.immediateThumbnailData,
hasVideo: representation.hasVideo,
isPersonal: representation.isPersonal,
typeHint: representation.typeHint
)
didUpdate = true
}
guard didUpdate else {
return nil
}
return TelegramMediaImage(
imageId: image.imageId,
representations: updatedRepresentations,
videoRepresentations: image.videoRepresentations,
immediateThumbnailData: image.immediateThumbnailData,
emojiMarkup: image.emojiMarkup,
reference: image.reference,
partialReference: image.partialReference,
flags: image.flags,
video: image.video
)
}
private func updatedFileIfNeeded(_ file: TelegramMediaFile) -> TelegramMediaFile? {
guard let dimensions = self.resolvedExternalMediaDimensions[file.fileId], file.resource is InstantPageExternalMediaResource else {
return nil
}
let (attributes, didUpdate) = self.fileAttributesWithResolvedDimensions(file.attributes, dimensions: dimensions)
guard didUpdate else {
return nil
}
return TelegramMediaFile(
fileId: file.fileId,
partialReference: file.partialReference,
resource: file.resource,
previewRepresentations: file.previewRepresentations,
videoThumbnails: file.videoThumbnails,
videoCover: file.videoCover,
immediateThumbnailData: file.immediateThumbnailData,
mimeType: file.mimeType,
size: file.size,
attributes: attributes,
alternativeRepresentations: file.alternativeRepresentations
)
}
private func fileAttributesWithResolvedDimensions(_ attributes: [TelegramMediaFileAttribute], dimensions: PixelDimensions) -> ([TelegramMediaFileAttribute], Bool) {
var updatedAttributes: [TelegramMediaFileAttribute] = []
var didUpdate = false
var hasSizeAttribute = false
for attribute in attributes {
switch attribute {
case let .ImageSize(size):
hasSizeAttribute = true
if size != dimensions {
updatedAttributes.append(.ImageSize(size: dimensions))
didUpdate = true
} else {
updatedAttributes.append(attribute)
}
case let .Video(duration, size, flags, preloadSize, coverTime, videoCodec):
hasSizeAttribute = true
if size != dimensions {
updatedAttributes.append(.Video(duration: duration, size: dimensions, flags: flags, preloadSize: preloadSize, coverTime: coverTime, videoCodec: videoCodec))
didUpdate = true
} else {
updatedAttributes.append(attribute)
}
default:
updatedAttributes.append(attribute)
}
}
if !hasSizeAttribute {
updatedAttributes.append(.ImageSize(size: dimensions))
didUpdate = true
}
return (updatedAttributes, didUpdate)
}
private func longPressMedia(_ media: InstantPageMedia) {
let controller = makeContextMenuController(actions: [ContextMenuAction(content: .text(title: self.strings.Conversation_ContextMenuCopy, accessibilityLabel: self.strings.Conversation_ContextMenuCopy), action: { [weak self] in
if let strongSelf = self, case let .image(image) = media.media {

View file

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

View file

@ -1,5 +1,6 @@
import Foundation
import UIKit
import ImageIO
import AsyncDisplayKit
import Display
import TelegramCore
@ -21,7 +22,35 @@ private struct FetchControls {
let cancel: () -> Void
}
final class InstantPageImageNode: ASDisplayNode, InstantPageNode {
private enum ExternalImageLoadState {
case loading
case ready
case failed
}
private func externalImagePixelDimensions(data: Data) -> PixelDimensions? {
guard let source = CGImageSourceCreateWithData(data as CFData, nil) else {
return nil
}
guard let properties = CGImageSourceCopyPropertiesAtIndex(source, 0, nil) as? [CFString: Any] else {
return nil
}
guard let pixelWidth = (properties[kCGImagePropertyPixelWidth] as? NSNumber)?.int32Value,
let pixelHeight = (properties[kCGImagePropertyPixelHeight] as? NSNumber)?.int32Value,
pixelWidth > 0, pixelHeight > 0 else {
return nil
}
let orientation = imageOrientationFromSource(source)
switch orientation {
case .left, .right, .leftMirrored, .rightMirrored:
return PixelDimensions(width: pixelHeight, height: pixelWidth)
default:
return PixelDimensions(width: pixelWidth, height: pixelHeight)
}
}
final class InstantPageImageNode: ASDisplayNode, InstantPageNode, InstantPageExternalMediaDimensionsNode {
private let context: AccountContext
private let webPage: TelegramMediaWebpage
private var theme: InstantPageTheme
@ -32,8 +61,13 @@ final class InstantPageImageNode: ASDisplayNode, InstantPageNode {
private let fit: Bool
private let openMedia: (InstantPageMedia) -> Void
private let longPressMedia: (InstantPageMedia) -> Void
private let getPreloadedResource: (String) -> Data?
private var fetchControls: FetchControls?
private var externalImageLoadState: ExternalImageLoadState?
var updateExternalMediaDimensions: ((EngineMedia.Id, PixelDimensions) -> Void)?
private var externalMediaDimensions: PixelDimensions?
private var didReportExternalMediaDimensions = false
private let pinchContainerNode: PinchSourceContainerNode
private let imageNode: TransformImageNode
@ -48,6 +82,7 @@ final class InstantPageImageNode: ASDisplayNode, InstantPageNode {
private var statusDisposable = MetaDisposable()
private var themeUpdated: Bool = false
private var externalMediaDimensionsUpdated: Bool = false
init(context: AccountContext, sourceLocation: InstantPageSourceLocation, theme: InstantPageTheme, webPage: TelegramMediaWebpage, media: InstantPageMedia, attributes: [InstantPageImageAttribute], interactive: Bool, roundCorners: Bool, fit: Bool, openMedia: @escaping (InstantPageMedia) -> Void, longPressMedia: @escaping (InstantPageMedia) -> Void, activatePinchPreview: ((PinchSourceContainerNode) -> Void)?, pinchPreviewFinished: ((InstantPageNode) -> Void)?, getPreloadedResource: @escaping (String) -> Data?) {
self.context = context
@ -60,6 +95,7 @@ final class InstantPageImageNode: ASDisplayNode, InstantPageNode {
self.fit = fit
self.openMedia = openMedia
self.longPressMedia = longPressMedia
self.getPreloadedResource = getPreloadedResource
self.pinchContainerNode = PinchSourceContainerNode()
self.imageNode = TransformImageNode()
@ -72,33 +108,14 @@ final class InstantPageImageNode: ASDisplayNode, InstantPageNode {
self.pinchContainerNode.contentNode.addSubnode(self.imageNode)
self.addSubnode(self.pinchContainerNode)
if interactive, media.url != nil {
self.linkIconNode.image = UIImage(bundleImageName: "Instant View/ImageLink")
self.pinchContainerNode.contentNode.addSubnode(self.linkIconNode)
}
if case let .image(image) = media.media, let largest = largestImageRepresentation(image.representations) {
if let externalResource = largest.resource as? InstantPageExternalMediaResource {
var url = externalResource.url
if !url.hasPrefix("http") && !url.hasPrefix("https") && url.hasPrefix("//") {
url = "https:\(url)"
}
let photoData: Signal<Tuple4<Data?, Data?, ChatMessagePhotoQuality, Bool>, NoError>
if let preloadedData = getPreloadedResource(externalResource.url) {
photoData = .single(Tuple4(nil, preloadedData, .full, true))
} else {
photoData = context.engine.resources.httpData(url: url, preserveExactUrl: true)
|> map(Optional.init)
|> `catch` { _ -> Signal<Data?, NoError> in
return .single(nil)
}
|> map { data in
if let data {
return Tuple4(nil, data, .full, true)
} else {
return Tuple4(nil, nil, .full, false)
}
}
}
self.imageNode.setSignal(chatMessagePhotoInternal(photoData: photoData)
|> map { _, _, generate in
return generate
})
self.loadExternalImage(resourceUrl: externalResource.url)
} else {
let imageReference = ImageMediaReference.webPage(webPage: WebpageReference(webPage), media: image)
self.imageNode.setSignal(chatMessagePhoto(postbox: context.account.postbox, userLocation: sourceLocation.userLocation, photoReference: imageReference))
@ -124,38 +141,13 @@ final class InstantPageImageNode: ASDisplayNode, InstantPageNode {
}
}
}))
if media.url != nil {
self.linkIconNode.image = UIImage(bundleImageName: "Instant View/ImageLink")
self.pinchContainerNode.contentNode.addSubnode(self.linkIconNode)
}
self.pinchContainerNode.contentNode.addSubnode(self.statusNode)
}
}
} else if case let .file(file) = media.media {
if let externalResource = file.resource as? InstantPageExternalMediaResource {
let photoData: Signal<Tuple4<Data?, Data?, ChatMessagePhotoQuality, Bool>, NoError>
if let preloadedData = getPreloadedResource(externalResource.url) {
photoData = .single(Tuple4(nil, preloadedData, .full, true))
} else {
photoData = context.engine.resources.httpData(url: externalResource.url, preserveExactUrl: true)
|> map(Optional.init)
|> `catch` { _ -> Signal<Data?, NoError> in
return .single(nil)
}
|> map { data in
if let data {
return Tuple4(nil, data, .full, true)
} else {
return Tuple4(nil, nil, .full, false)
}
}
}
self.imageNode.setSignal(chatMessagePhotoInternal(photoData: photoData)
|> map { _, _, generate in
return generate
})
self.loadExternalImage(resourceUrl: externalResource.url)
} else {
let fileReference = FileMediaReference.webPage(webPage: WebpageReference(webPage), media: file)
if file.mimeType.hasPrefix("image/") {
@ -235,6 +227,114 @@ final class InstantPageImageNode: ASDisplayNode, InstantPageNode {
}
}
private func loadExternalImage(resourceUrl: String) {
self.externalImageLoadState = .loading
self.updateExternalImageLoadState()
var requestUrl = resourceUrl
if !requestUrl.hasPrefix("http") && !requestUrl.hasPrefix("https") && requestUrl.hasPrefix("//") {
requestUrl = "https:\(requestUrl)"
}
let photoData: Signal<Tuple4<Data?, Data?, ChatMessagePhotoQuality, Bool>, NoError>
if let preloadedData = self.getPreloadedResource(resourceUrl) {
photoData = .single(Tuple4(nil, preloadedData, .full, true))
} else {
photoData = self.context.engine.resources.httpData(url: requestUrl, preserveExactUrl: true)
|> map(Optional.init)
|> `catch` { _ -> Signal<Data?, NoError> in
return .single(nil)
}
|> map { data in
if let data {
return Tuple4(nil, data, .full, true)
} else {
return Tuple4(nil, nil, .full, false)
}
}
}
let stateAwarePhotoData = photoData
|> deliverOnMainQueue
|> afterNext { [weak self] value in
guard let strongSelf = self else {
return
}
if let data = value._1 ?? value._0, UIImage(data: data) != nil {
if let dimensions = externalImagePixelDimensions(data: data) {
strongSelf.externalMediaDimensions = dimensions
strongSelf.externalMediaDimensionsUpdated = true
strongSelf.maybeUpdateExternalMediaDimensions(dimensions)
}
strongSelf.externalImageLoadState = .ready
strongSelf.setNeedsLayout()
} else {
strongSelf.externalImageLoadState = .failed
}
strongSelf.updateExternalImageLoadState()
}
self.imageNode.setSignal(chatMessagePhotoInternal(photoData: stateAwarePhotoData)
|> map { _, _, generate in
return generate
})
self.fetchControls = FetchControls(fetch: { [weak self] _ in
self?.loadExternalImage(resourceUrl: resourceUrl)
}, cancel: {})
}
private func currentMediaDimensions() -> PixelDimensions? {
if case let .image(image) = self.media.media, let largest = largestImageRepresentation(image.representations) {
return largest.dimensions
} else if case let .file(file) = self.media.media {
return file.dimensions
} else {
return nil
}
}
private func effectiveMediaDimensions() -> PixelDimensions? {
return self.externalMediaDimensions ?? self.currentMediaDimensions()
}
private func maybeUpdateExternalMediaDimensions(_ dimensions: PixelDimensions) {
guard !self.didReportExternalMediaDimensions, let mediaId = self.media.media.id else {
return
}
if let currentDimensions = self.currentMediaDimensions(), currentDimensions == dimensions {
return
}
self.didReportExternalMediaDimensions = true
self.updateExternalMediaDimensions?(mediaId, dimensions)
}
private func updateExternalImageLoadState() {
guard let externalImageLoadState = self.externalImageLoadState else {
return
}
if self.statusNode.supernode == nil {
self.pinchContainerNode.contentNode.addSubnode(self.statusNode)
}
let state: RadialStatusNodeState
switch externalImageLoadState {
case .loading:
state = .progress(color: .white, lineWidth: nil, value: nil, cancelEnabled: false, animateRotation: true)
case .ready:
state = .none
case .failed:
state = .none
}
self.statusNode.transitionToState(state, completion: { [weak statusNode] in
if state == .none {
statusNode?.removeFromSupernode()
}
})
}
private func updateFetchStatus() {
var state: RadialStatusNodeState = .none
if let fetchStatus = self.fetchStatus {
@ -260,19 +360,20 @@ final class InstantPageImageNode: ASDisplayNode, InstantPageNode {
let size = self.bounds.size
if self.currentSize != size || self.themeUpdated {
if self.currentSize != size || self.themeUpdated || self.externalMediaDimensionsUpdated {
self.currentSize = size
self.themeUpdated = false
self.externalMediaDimensionsUpdated = false
self.pinchContainerNode.frame = CGRect(origin: CGPoint(), size: size)
self.pinchContainerNode.update(size: size, transition: .immediate)
self.imageNode.frame = CGRect(origin: CGPoint(), size: size)
let radialStatusSize: CGFloat = 50.0
let radialStatusSize: CGFloat = max(18.0, min(50.0, floor(min(size.width, size.height) * 0.7)))
self.statusNode.frame = CGRect(x: floorToScreenPixels((size.width - radialStatusSize) / 2.0), y: floorToScreenPixels((size.height - radialStatusSize) / 2.0), width: radialStatusSize, height: radialStatusSize)
if case let .image(image) = self.media.media, let largest = largestImageRepresentation(image.representations) {
let imageSize = largest.dimensions.cgSize.aspectFilled(size)
if case .image = self.media.media, let dimensions = self.effectiveMediaDimensions() {
let imageSize = dimensions.cgSize.aspectFilled(size)
let boundingSize = size
let radius: CGFloat = self.roundCorners ? floor(min(imageSize.width, imageSize.height) / 2.0) : 0.0
let makeLayout = self.imageNode.asyncLayout()
@ -280,7 +381,7 @@ final class InstantPageImageNode: ASDisplayNode, InstantPageNode {
apply()
self.linkIconNode.frame = CGRect(x: size.width - 38.0, y: 14.0, width: 24.0, height: 24.0)
} else if case let .file(file) = self.media.media, let dimensions = file.dimensions {
} else if case let .file(file) = self.media.media, let dimensions = self.effectiveMediaDimensions() {
let emptyColor = file.mimeType.hasPrefix("image/") ? self.theme.imageTintColor : nil
let imageSize = dimensions.cgSize.aspectFilled(size)
@ -318,7 +419,7 @@ final class InstantPageImageNode: ASDisplayNode, InstantPageNode {
}
func transitionNode(media: InstantPageMedia) -> (ASDisplayNode, CGRect, () -> (UIView?, UIView?))? {
if media == self.media {
if instantPageMediaMatchesNodeIdentity(media, self.media) {
let imageNode = self.imageNode
return (self.imageNode, self.imageNode.bounds, { [weak imageNode] in
return (imageNode?.view.snapshotContentTree(unhide: true), nil)
@ -329,14 +430,32 @@ final class InstantPageImageNode: ASDisplayNode, InstantPageNode {
}
func updateHiddenMedia(media: InstantPageMedia?) {
self.imageNode.isHidden = self.media == media
if let media {
self.imageNode.isHidden = instantPageMediaMatchesNodeIdentity(self.media, media)
} else {
self.imageNode.isHidden = false
}
self.statusNode.isHidden = self.imageNode.isHidden
self.linkIconNode.isHidden = self.imageNode.isHidden
}
@objc private func tapGesture(_ recognizer: TapLongTapOrDoubleTapGestureRecognizer) {
switch recognizer.state {
case .ended:
if let (gesture, _) = recognizer.lastRecognizedGestureAndLocation {
if let externalImageLoadState = self.externalImageLoadState {
switch externalImageLoadState {
case .loading:
return
case .failed:
if case .tap = gesture {
self.fetchControls?.fetch(true)
}
return
case .ready:
break
}
}
if let fetchStatus = self.fetchStatus {
switch fetchStatus {
case .Local:

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
}
}
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 AsyncDisplayKit
import Display
import TelegramCore
import TelegramPresentationData
public protocol InstantPageExternalMediaDimensionsNode: AnyObject {
var updateExternalMediaDimensions: ((EngineMedia.Id, PixelDimensions) -> Void)? { get set }
}
public protocol InstantPageNode: ASDisplayNode {
func updateIsVisible(_ isVisible: Bool)

View file

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

View file

@ -61,6 +61,12 @@ private final class InstantPageSlideshowItemNode: ASDisplayNode {
}
return nil
}
func updateExternalMediaDimensions(_ update: ((EngineMedia.Id, PixelDimensions) -> Void)?) {
if let node = self.contentNode as? InstantPageImageNode {
node.updateExternalMediaDimensions = update
}
}
}
private final class InstantPageSlideshowPagerNode: ASDisplayNode, ASScrollViewDelegate {
@ -88,6 +94,13 @@ private final class InstantPageSlideshowPagerNode: ASDisplayNode, ASScrollViewDe
}
private var containerLayout: ContainerViewLayout?
var updateExternalMediaDimensions: ((EngineMedia.Id, PixelDimensions) -> Void)? {
didSet {
for node in self.itemNodes {
node.updateExternalMediaDimensions(self.updateExternalMediaDimensions)
}
}
}
var centralItemIndexUpdated: (Int?) -> Void = { _ in }
@ -195,6 +208,7 @@ private final class InstantPageSlideshowPagerNode: ASDisplayNode, ASScrollViewDe
}
let node = InstantPageSlideshowItemNode(contentNode: contentNode)
node.updateExternalMediaDimensions(self.updateExternalMediaDimensions)
node.index = index
return node
@ -380,8 +394,13 @@ private final class InstantPageSlideshowPagerNode: ASDisplayNode, ASScrollViewDe
}
}
final class InstantPageSlideshowNode: ASDisplayNode, InstantPageNode {
final class InstantPageSlideshowNode: ASDisplayNode, InstantPageNode, InstantPageExternalMediaDimensionsNode {
var medias: [InstantPageMedia] = []
var updateExternalMediaDimensions: ((EngineMedia.Id, PixelDimensions) -> Void)? {
didSet {
self.pagerNode.updateExternalMediaDimensions = self.updateExternalMediaDimensions
}
}
private let pagerNode: InstantPageSlideshowPagerNode
private let pageControlNode: PageControlNode

View file

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

View file

@ -15,10 +15,12 @@ private final class InstantPageTileNodeParameters: NSObject {
}
public final class InstantPageTileNode: ASDisplayNode {
private let tile: InstantPageTile
private var tile: InstantPageTile
private var tileBackgroundColor: UIColor
public init(tile: InstantPageTile, backgroundColor: UIColor) {
self.tile = tile
self.tileBackgroundColor = backgroundColor
super.init()
@ -27,8 +29,15 @@ public final class InstantPageTileNode: ASDisplayNode {
self.backgroundColor = backgroundColor
}
public func update(tile: InstantPageTile, backgroundColor: UIColor) {
self.tile = tile
self.tileBackgroundColor = backgroundColor
self.backgroundColor = backgroundColor
self.setNeedsDisplay()
}
public override func drawParameters(forAsyncLayer layer: _ASDisplayLayer) -> NSObjectProtocol? {
return InstantPageTileNodeParameters(tile: self.tile, backgroundColor: self.backgroundColor ?? UIColor.white)
return InstantPageTileNodeParameters(tile: self.tile, backgroundColor: self.tileBackgroundColor)
}
@objc override public class func draw(_ bounds: CGRect, withParameters parameters: Any?, isCancelled: () -> Bool, isRasterizing: Bool) {

View file

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

View file

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

View file

@ -57,7 +57,7 @@ public final class LocationMapHeaderNode: ASDisplayNode {
private let options: ComponentView<Empty>?
private let optionsBackgroundView: GlassBackgroundView?
private let optionsBackgroundView: GlassContextExtractableContainer?
private let optionsBackgroundNode: ASImageNode
private let optionsSeparatorNode: ASDisplayNode
private let optionsSecondSeparatorNode: ASDisplayNode
@ -66,7 +66,7 @@ public final class LocationMapHeaderNode: ASDisplayNode {
private let notificationButtonNode: HighlightableButtonNode
private let placesBackgroundView: GlassBackgroundView?
private let placesBackgroundNode: ASImageNode
private let placesButtonNode: HighlightableButtonNode
private let placesButtonNode: HighlightTrackingButtonNode
private let shadowNode: ASImageNode
private var validLayout: (ContainerViewLayout, CGFloat, CGFloat, CGFloat, CGFloat, CGFloat, CGSize)?
@ -132,7 +132,7 @@ public final class LocationMapHeaderNode: ASDisplayNode {
self.placesBackgroundNode.image = generateBackgroundImage(theme: presentationData.theme)
self.placesBackgroundNode.isUserInteractionEnabled = true
self.placesButtonNode = HighlightableButtonNode()
self.placesButtonNode = HighlightTrackingButtonNode()
self.placesButtonNode.setTitle(presentationData.strings.Map_PlacesInThisArea, with: Font.medium(17.0), with: self.glass ? presentationData.theme.rootController.navigationBar.primaryTextColor : buttonColor, for: .normal)
self.shadowNode = ASImageNode()
@ -142,7 +142,7 @@ public final class LocationMapHeaderNode: ASDisplayNode {
self.shadowNode.image = generateShadowImage(theme: presentationData.theme, highlighted: false)
if glass {
self.optionsBackgroundView = GlassBackgroundView()
self.optionsBackgroundView = GlassContextExtractableContainer()
self.optionsBackgroundNode.image = nil
self.placesBackgroundView = GlassBackgroundView()
@ -169,16 +169,16 @@ public final class LocationMapHeaderNode: ASDisplayNode {
self.view.addSubview(optionsBackgroundView)
}
self.addSubnode(self.optionsBackgroundNode)
self.optionsBackgroundNode.addSubnode(self.optionsSeparatorNode)
self.optionsBackgroundNode.addSubnode(self.optionsSecondSeparatorNode)
self.optionsBackgroundNode.addSubnode(self.infoButtonNode)
self.optionsBackgroundNode.addSubnode(self.locationButtonNode)
self.optionsBackgroundNode.addSubnode(self.notificationButtonNode)
self.optionsBackgroundView?.contentView.addSubview(self.optionsSeparatorNode.view)
self.optionsBackgroundView?.contentView.addSubview(self.optionsSecondSeparatorNode.view)
self.optionsBackgroundView?.contentView.addSubview(self.infoButtonNode.view)
self.optionsBackgroundView?.contentView.addSubview(self.locationButtonNode.view)
self.optionsBackgroundView?.contentView.addSubview(self.notificationButtonNode.view)
}
}
self.addSubnode(self.placesBackgroundNode)
self.placesBackgroundNode.addSubnode(self.placesButtonNode)
self.placesBackgroundView?.contentView.addSubview(self.placesButtonNode.view)
//self.addSubnode(self.shadowNode)
self.infoButtonNode.addTarget(self, action: #selector(self.infoPressed), forControlEvents: .touchUpInside)
@ -252,16 +252,16 @@ public final class LocationMapHeaderNode: ASDisplayNode {
let inset: CGFloat = 6.0
let placesButtonSize = CGSize(width: 180.0 + panelInset * 2.0, height: 45.0 + panelInset * 2.0)
let placesButtonFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((size.width - placesButtonSize.width) / 2.0), y: navigationBarHeight + topPadding - 6.0), size: placesButtonSize)
let placesButtonFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((size.width - placesButtonSize.width) / 2.0), y: navigationBarHeight + topPadding - 6.0), size: placesButtonSize).insetBy(dx: 5.0, dy: 6.0)
transition.updateFrame(node: self.placesBackgroundNode, frame: placesButtonFrame)
if let placesBackgroundView = self.placesBackgroundView {
let backgroundViewFrame = CGRect(origin: .zero, size: placesButtonFrame.size).insetBy(dx: 5.0, dy: 6.0)
let backgroundViewFrame = CGRect(origin: .zero, size: placesButtonFrame.size)
transition.updateFrame(view: placesBackgroundView, frame: backgroundViewFrame)
placesBackgroundView.update(size: backgroundViewFrame.size, cornerRadius: backgroundViewFrame.height * 0.5, isDark: self.presentationData.theme.overallDarkAppearance, tintColor: .init(kind: .panel), transition: .immediate)
placesBackgroundView.update(size: backgroundViewFrame.size, cornerRadius: backgroundViewFrame.height * 0.5, isDark: self.presentationData.theme.overallDarkAppearance, tintColor: .init(kind: .panel), isInteractive: true, transition: .immediate)
}
transition.updateFrame(node: self.placesButtonNode, frame: CGRect(origin: CGPoint(), size: placesButtonSize))
transition.updateFrame(node: self.placesButtonNode, frame: CGRect(origin: CGPoint(), size: placesButtonFrame.size))
transition.updateAlpha(node: self.placesBackgroundNode, alpha: self.displayingPlacesButton ? 1.0 : 0.0)
transition.updateAlpha(node: self.placesButtonNode, alpha: self.displayingPlacesButton ? 1.0 : 0.0)
@ -327,7 +327,7 @@ public final class LocationMapHeaderNode: ASDisplayNode {
if let optionsBackgroundView = self.optionsBackgroundView {
let backgroundViewFrame = backgroundFrame.insetBy(dx: 4.0, dy: 4.0)
transition.updateFrame(view: optionsBackgroundView, frame: backgroundViewFrame)
optionsBackgroundView.update(size: backgroundViewFrame.size, cornerRadius: backgroundViewFrame.width * 0.5, isDark: self.presentationData.theme.overallDarkAppearance, tintColor: .init(kind: .panel), transition: .immediate)
optionsBackgroundView.update(size: backgroundViewFrame.size, cornerRadius: backgroundViewFrame.width * 0.5, isDark: self.presentationData.theme.overallDarkAppearance, tintColor: .init(kind: .panel), isInteractive: true, transition: .immediate)
}
let alphaTransition = ContainedViewLayoutTransition.animated(duration: 0.2, curve: .easeInOut)

View file

@ -11,7 +11,6 @@ import AppBundle
import CoreLocation
import PresentationDataUtils
import OpenInExternalAppUI
import DeviceAccess
import UndoUI
import MapKit
@ -32,12 +31,6 @@ public class LocationViewParams {
}
}
enum LocationViewRightBarButton {
case none
case share
case showAll
}
class LocationViewInteraction {
let toggleMapModeSelection: () -> Void
let updateMapMode: (LocationMapMode) -> Void
@ -49,10 +42,9 @@ class LocationViewInteraction {
let updateSendActionHighlight: (Bool) -> Void
let sendLiveLocation: (Int32?, Bool, EngineMessage.Id?) -> Void
let stopLiveLocation: () -> Void
let updateRightBarButton: (LocationViewRightBarButton) -> Void
let present: (ViewController) -> Void
init(toggleMapModeSelection: @escaping () -> Void, updateMapMode: @escaping (LocationMapMode) -> Void, toggleTrackingMode: @escaping () -> Void, goToCoordinate: @escaping (CLLocationCoordinate2D) -> Void, requestDirections: @escaping (TelegramMediaMap, String?, OpenInLocationDirections) -> Void, share: @escaping () -> Void, setupProximityNotification: @escaping (Bool, EngineMessage.Id?) -> Void, updateSendActionHighlight: @escaping (Bool) -> Void, sendLiveLocation: @escaping (Int32?, Bool, EngineMessage.Id?) -> Void, stopLiveLocation: @escaping () -> Void, updateRightBarButton: @escaping (LocationViewRightBarButton) -> Void, present: @escaping (ViewController) -> Void) {
init(toggleMapModeSelection: @escaping () -> Void, updateMapMode: @escaping (LocationMapMode) -> Void, toggleTrackingMode: @escaping () -> Void, goToCoordinate: @escaping (CLLocationCoordinate2D) -> Void, requestDirections: @escaping (TelegramMediaMap, String?, OpenInLocationDirections) -> Void, share: @escaping () -> Void, setupProximityNotification: @escaping (Bool, EngineMessage.Id?) -> Void, updateSendActionHighlight: @escaping (Bool) -> Void, sendLiveLocation: @escaping (Int32?, Bool, EngineMessage.Id?) -> Void, stopLiveLocation: @escaping () -> Void, present: @escaping (ViewController) -> Void) {
self.toggleMapModeSelection = toggleMapModeSelection
self.updateMapMode = updateMapMode
self.toggleTrackingMode = toggleTrackingMode
@ -63,7 +55,6 @@ class LocationViewInteraction {
self.updateSendActionHighlight = updateSendActionHighlight
self.sendLiveLocation = sendLiveLocation
self.stopLiveLocation = stopLiveLocation
self.updateRightBarButton = updateRightBarButton
self.present = present
}
}
@ -83,9 +74,7 @@ public final class LocationViewController: ViewController {
private let locationManager = LocationManager()
private var interaction: LocationViewInteraction?
private var rightBarButtonAction: LocationViewRightBarButton = .none
public var dismissed: () -> Void = {}
public init(context: AccountContext, updatedPresentationData: (initial: PresentationData, signal: Signal<PresentationData, NoError>)? = nil, subject: EngineMessage, isStoryLocation: Bool = false, isPreview: Bool = false, params: LocationViewParams) {
@ -97,33 +86,19 @@ public final class LocationViewController: ViewController {
self.presentationData = updatedPresentationData?.initial ?? context.sharedContext.currentPresentationData.with { $0 }
let navigationBarPresentationData: NavigationBarPresentationData?
if !isPreview {
navigationBarPresentationData = NavigationBarPresentationData(theme: NavigationBarTheme(rootControllerTheme: self.presentationData.theme).withUpdatedSeparatorColor(.clear), strings: NavigationBarStrings(presentationStrings: self.presentationData.strings))
} else {
navigationBarPresentationData = nil
}
super.init(navigationBarPresentationData: nil)
super.init(navigationBarPresentationData: navigationBarPresentationData)
self._hasGlassStyle = true
self.navigationPresentation = .modal
if !self.isPreview {
self.title = self.presentationData.strings.Map_LocationTitle
self.navigationItem.leftBarButtonItem = UIBarButtonItem(title: self.presentationData.strings.Common_Close, style: .plain, target: self, action: #selector(self.cancelPressed))
}
self.presentationDataDisposable = ((updatedPresentationData?.signal ?? context.sharedContext.presentationData)
|> deliverOnMainQueue).start(next: { [weak self] presentationData in
guard let strongSelf = self, strongSelf.presentationData.theme !== presentationData.theme else {
return
}
strongSelf.presentationData = presentationData
strongSelf.navigationBar?.updatePresentationData(NavigationBarPresentationData(theme: NavigationBarTheme(rootControllerTheme: strongSelf.presentationData.theme).withUpdatedSeparatorColor(.clear), strings: NavigationBarStrings(presentationStrings: strongSelf.presentationData.strings)), transition: .immediate)
strongSelf.updateRightBarButton()
if strongSelf.isNodeLoaded {
strongSelf.controllerNode.updatePresentationData(presentationData)
}
@ -466,15 +441,6 @@ public final class LocationViewController: ViewController {
}, stopLiveLocation: { [weak self] in
params.stopLiveLocation(nil)
self?.dismiss()
}, updateRightBarButton: { [weak self] action in
guard let strongSelf = self else {
return
}
if action != strongSelf.rightBarButtonAction {
strongSelf.rightBarButtonAction = action
strongSelf.updateRightBarButton()
}
}, present: { [weak self] c in
if let strongSelf = self {
strongSelf.present(c, in: .window(.root))
@ -515,7 +481,7 @@ public final class LocationViewController: ViewController {
return
}
self.displayNode = LocationViewControllerNode(context: self.context, presentationData: self.presentationData, subject: self.subject, interaction: interaction, locationManager: self.locationManager, isStoryLocation: self.isStoryLocation, isPreview: self.isPreview)
self.displayNode = LocationViewControllerNode(context: self.context, controller: self, presentationData: self.presentationData, subject: self.subject, interaction: interaction, locationManager: self.locationManager, isStoryLocation: self.isStoryLocation, isPreview: self.isPreview)
self.displayNodeDidLoad()
self.controllerNode.onAnnotationsReady = { [weak self] in
@ -528,39 +494,12 @@ public final class LocationViewController: ViewController {
self.controllerNode.headerNode.mapNode.disableHorizontalTransitionGesture = self.isStoryLocation
}
private func updateRightBarButton() {
guard !self.isPreview else {
return
}
switch self.rightBarButtonAction {
case .none:
self.navigationItem.rightBarButtonItem = nil
case .share:
self.navigationItem.rightBarButtonItem = UIBarButtonItem(image: PresentationResourcesRootController.navigationShareIcon(self.presentationData.theme), style: .plain, target: self, action: #selector(self.sharePressed))
self.navigationItem.rightBarButtonItem?.accessibilityLabel = self.presentationData.strings.VoiceOver_MessageContextShare
case .showAll:
self.navigationItem.rightBarButtonItem = UIBarButtonItem(title: self.presentationData.strings.Map_LiveLocationShowAll, style: .plain, target: self, action: #selector(self.showAllPressed))
}
}
override public func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) {
super.containerLayoutUpdated(layout, transition: transition)
self.controllerNode.containerLayoutUpdated(layout, navigationHeight: self.navigationLayout(layout: layout).navigationFrame.maxY, transition: transition)
}
@objc private func cancelPressed() {
self.dismiss()
}
@objc private func sharePressed() {
self.interaction?.share()
}
@objc private func showAllPressed() {
self.controllerNode.showAll()
}
private var didDismiss = false
public override func viewWillDisappear(_ animated: Bool) {
super.viewWillDisappear(animated)

View file

@ -17,6 +17,11 @@ import CoreLocation
import Geocoding
import DeviceAccess
import TooltipUI
import ComponentFlow
import GlassControls
import BundleIconComponent
import EdgeEffect
import MultilineTextComponent
func getLocation(from message: EngineMessage) -> TelegramMediaMap? {
if let poll = message.media.first(where: { $0 is TelegramMediaPoll } ) as? TelegramMediaPoll, let map = poll.attachedMedia as? TelegramMediaMap {
@ -212,6 +217,12 @@ private func preparedTransition(from fromEntries: [LocationViewEntry], to toEntr
return LocationViewTransaction(deletions: deletions, insertions: insertions, updates: updates, gotTravelTimes: gotTravelTimes, count: toEntries.count, animated: animated)
}
enum LocationViewRightBarButton {
case none
case share
case showAll
}
public enum LocationViewLocation: Equatable {
case initial
case user
@ -268,6 +279,7 @@ public struct LocationViewState {
final class LocationViewControllerNode: ViewControllerTracingNode, CLLocationManagerDelegate {
private let context: AccountContext
private weak var controller: LocationViewController?
private var presentationData: PresentationData
private let presentationDataPromise: Promise<PresentationData>
private var subject: EngineMessage
@ -276,9 +288,14 @@ final class LocationViewControllerNode: ViewControllerTracingNode, CLLocationMan
private let isStoryLocation: Bool
private let isPreview: Bool
private var rightBarButtonAction: LocationViewRightBarButton = .none
private let topEdgeEffectView = EdgeEffectView()
private let buttons = ComponentView<Empty>()
private let title = ComponentView<Empty>()
private let listNode: ListView
let headerNode: LocationMapHeaderNode
private let optionsNode: LocationOptionsNode
private var enqueuedTransitions: [LocationViewTransaction] = []
@ -302,8 +319,9 @@ final class LocationViewControllerNode: ViewControllerTracingNode, CLLocationMan
}
private let travelTimesPromise = Promise<[EngineMessage.Id: (Double, ExpectedTravelTime, ExpectedTravelTime, ExpectedTravelTime)]>([:])
init(context: AccountContext, presentationData: PresentationData, subject: EngineMessage, interaction: LocationViewInteraction, locationManager: LocationManager, isStoryLocation: Bool, isPreview: Bool) {
init(context: AccountContext, controller: LocationViewController, presentationData: PresentationData, subject: EngineMessage, interaction: LocationViewInteraction, locationManager: LocationManager, isStoryLocation: Bool, isPreview: Bool) {
self.context = context
self.controller = controller
self.presentationData = presentationData
self.presentationDataPromise = Promise(presentationData)
self.subject = subject
@ -316,7 +334,7 @@ final class LocationViewControllerNode: ViewControllerTracingNode, CLLocationMan
self.statePromise = Promise(self.state)
self.listNode = ListViewImpl()
self.listNode.backgroundColor = self.presentationData.theme.list.plainBackgroundColor
self.listNode.backgroundColor = .clear //self.presentationData.theme.list.plainBackgroundColor
self.listNode.verticalScrollIndicatorColor = UIColor(white: 0.0, alpha: 0.3)
self.listNode.verticalScrollIndicatorFollowsOverscroll = true
self.listNode.accessibilityPageScrolledString = { row, count in
@ -326,29 +344,24 @@ final class LocationViewControllerNode: ViewControllerTracingNode, CLLocationMan
var setupProximityNotificationImpl: ((Bool) -> Void)?
self.headerNode = LocationMapHeaderNode(
presentationData: presentationData,
glass: false,
isPreview: true,
glass: true,
isPreview: self.isPreview,
toggleMapModeSelection: interaction.toggleMapModeSelection,
updateMapMode: interaction.updateMapMode,
goToUserLocation: interaction.toggleTrackingMode,
setupProximityNotification: { reset in
setupProximityNotificationImpl?(reset)
})
//self.headerNode.mapNode.isRotateEnabled = false
self.optionsNode = LocationOptionsNode(presentationData: presentationData, updateMapMode: interaction.updateMapMode)
setupProximityNotificationImpl?(reset)
}
)
super.init()
self.backgroundColor = self.presentationData.theme.list.plainBackgroundColor
self.backgroundColor = .red // self.presentationData.theme.list.plainBackgroundColor
if !self.isPreview {
self.addSubnode(self.listNode)
}
self.addSubnode(self.headerNode)
if !self.isPreview {
self.addSubnode(self.optionsNode)
}
let userLocation: Signal<CLLocation?, NoError> = .single(nil)
|> then(
@ -707,7 +720,7 @@ final class LocationViewControllerNode: ViewControllerTracingNode, CLLocationMan
} else {
rightBarButtonAction = .share
}
strongSelf.interaction.updateRightBarButton(rightBarButtonAction)
strongSelf.rightBarButtonAction = rightBarButtonAction
if let (layout, navigationBarHeight) = strongSelf.validLayout {
var updateLayout = false
@ -800,10 +813,9 @@ final class LocationViewControllerNode: ViewControllerTracingNode, CLLocationMan
self.presentationData = presentationData
self.presentationDataPromise.set(.single(presentationData))
self.backgroundColor = self.presentationData.theme.list.plainBackgroundColor
self.listNode.backgroundColor = self.presentationData.theme.list.plainBackgroundColor
self.backgroundColor = .red //self.presentationData.theme.list.plainBackgroundColor
self.listNode.backgroundColor = .clear // self.presentationData.theme.list.plainBackgroundColor
self.headerNode.updatePresentationData(self.presentationData)
self.optionsNode.updatePresentationData(self.presentationData)
}
func updateState(_ f: (LocationViewState) -> LocationViewState) {
@ -970,10 +982,10 @@ final class LocationViewControllerNode: ViewControllerTracingNode, CLLocationMan
} else {
headerHeight = topInset + overlap
}
let headerFrame = CGRect(origin: CGPoint(), size: CGSize(width: layout.size.width, height: headerHeight))
let headerFrame = CGRect(origin: CGPoint(), size: CGSize(width: layout.size.width, height: layout.size.height))
transition.updateFrame(node: self.headerNode, frame: headerFrame)
self.headerNode.updateLayout(layout: layout, navigationBarHeight: navigationHeight, topPadding: self.state.displayingMapModeOptions ? optionsHeight : 0.0, controlsTopPadding: self.state.displayingMapModeOptions ? optionsHeight : 0.0, controlsBottomPadding: 0.0, offset: 0.0, size: headerFrame.size, transition: transition)
self.headerNode.updateLayout(layout: layout, navigationBarHeight: navigationHeight, topPadding: self.state.displayingMapModeOptions ? optionsHeight : 0.0, controlsTopPadding: 0.0, controlsBottomPadding: 0.0, offset: 0.0, size: CGSize(width: headerFrame.width, height: headerHeight), transition: transition)
let (duration, curve) = listViewAnimationDurationAndCurve(transition: transition)
@ -982,18 +994,118 @@ final class LocationViewControllerNode: ViewControllerTracingNode, CLLocationMan
let listFrame: CGRect = CGRect(origin: CGPoint(), size: layout.size)
transition.updateFrame(node: self.listNode, frame: listFrame)
if !self.isPreview {
let topEdgeEffectFrame = CGRect(origin: .zero, size: CGSize(width: layout.size.width, height: 80.0))
transition.updateFrame(view: self.topEdgeEffectView, frame: topEdgeEffectFrame)
self.topEdgeEffectView.update(content: self.headerNode.mapNode.mapMode == .map ? self.presentationData.theme.list.plainBackgroundColor : .clear, blur: true, alpha: 0.65, rect: topEdgeEffectFrame, edge: .top, edgeSize: topEdgeEffectFrame.height, transition: ComponentTransition(transition))
if self.topEdgeEffectView.superview == nil {
self.view.addSubview(self.topEdgeEffectView)
}
let leftControlItems: [GlassControlGroupComponent.Item] = [
GlassControlGroupComponent.Item(
id: AnyHashable("close"),
content: .icon("Navigation/Close"),
action: { [weak self] in
guard let self else {
return
}
self.controller?.dismiss()
}
)
]
var rightControlItems: [GlassControlGroupComponent.Item] = []
switch self.rightBarButtonAction {
case .none:
break
case .share:
rightControlItems.append(
GlassControlGroupComponent.Item(
id: AnyHashable("share"),
content: .icon("Navigation/Share"),
action: { [weak self] in
guard let self else {
return
}
self.interaction.share()
}
)
)
case .showAll:
rightControlItems.append(
GlassControlGroupComponent.Item(
id: AnyHashable("share"),
content: .text(self.presentationData.strings.Map_LiveLocationShowAll),
action: { [weak self] in
guard let self else {
return
}
self.showAll()
}
)
)
}
let barButtonSideInset: CGFloat = 16.0
let buttonsSize = self.buttons.update(
transition: ComponentTransition(transition),
component: AnyComponent(GlassControlPanelComponent(
theme: self.presentationData.theme,
leftItem: GlassControlPanelComponent.Item(
items: leftControlItems,
background: .panel
),
centralItem: nil,
rightItem: rightControlItems.isEmpty ? nil : GlassControlPanelComponent.Item(
items: rightControlItems,
background: .panel
),
centerAlignmentIfPossible: true,
isDark: self.presentationData.theme.overallDarkAppearance
)),
environment: {},
containerSize: CGSize(width: layout.size.width - barButtonSideInset * 2.0 - layout.safeInsets.left - layout.safeInsets.right, height: 44.0)
)
let buttonsFrame = CGRect(origin: CGPoint(x: barButtonSideInset + layout.safeInsets.left, y: barButtonSideInset), size: buttonsSize)
if let view = self.buttons.view {
if view.superview == nil {
self.view.addSubview(view)
}
view.bounds = CGRect(origin: .zero, size: buttonsFrame.size)
view.center = buttonsFrame.center
}
let titleSize = self.title.update(
transition: ComponentTransition(transition),
component: AnyComponent(
MultilineTextComponent(
text: .plain(
NSAttributedString(
string: self.presentationData.strings.Map_LocationTitle,
font: Font.semibold(17.0),
textColor: self.headerNode.mapNode.mapMode == .map ? self.presentationData.theme.rootController.navigationBar.primaryTextColor : .white
)
)
)
),
environment: {},
containerSize: CGSize(width: 200.0, height: 40.0)
)
let titleFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((layout.size.width - titleSize.width) / 2.0), y: floorToScreenPixels((navigationHeight - titleSize.height) / 2.0) + 3.0), size: titleSize)
if let titleView = self.title.view {
if titleView.superview == nil {
self.view.addSubview(titleView)
}
transition.updateFrame(view: titleView, frame: titleFrame)
}
}
if isFirstLayout {
while !self.enqueuedTransitions.isEmpty {
self.dequeueTransition()
}
}
let optionsOffset: CGFloat = self.state.displayingMapModeOptions ? navigationHeight : navigationHeight - optionsHeight
let optionsFrame = CGRect(x: 0.0, y: optionsOffset, width: layout.size.width, height: optionsHeight)
transition.updateFrame(node: self.optionsNode, frame: optionsFrame)
self.optionsNode.updateLayout(size: optionsFrame.size, leftInset: layout.safeInsets.left, rightInset: layout.safeInsets.right, transition: transition)
self.optionsNode.isUserInteractionEnabled = self.state.displayingMapModeOptions
}
var coordinate: Signal<CLLocationCoordinate2D, NoError> {

View file

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

View file

@ -250,6 +250,7 @@ static void initializeMapping()
mimeToExtension[@"text/plain"] = @"text"; extensionToMime[@"text"] = @"text/plain";
mimeToExtension[@"text/plain"] = @"diff"; extensionToMime[@"diff"] = @"text/plain";
mimeToExtension[@"text/plain"] = @"po"; extensionToMime[@"po"] = @"text/plain"; // reserve "pot" for vnd.ms-powerpoint
mimeToExtension[@"text/markdown"] = @"md"; extensionToMime[@"md"] = @"text/markdown";
mimeToExtension[@"text/richtext"] = @"rtx"; extensionToMime[@"rtx"] = @"text/richtext";
mimeToExtension[@"text/rtf"] = @"rtf"; extensionToMime[@"rtf"] = @"text/rtf";
mimeToExtension[@"text/texmacs"] = @"ts"; extensionToMime[@"ts"] = @"text/texmacs";

View file

@ -373,7 +373,7 @@ final class SharePeersContainerNode: ASDisplayNode, ShareContentContainerNode {
self.contentOffsetUpdated = f
}
private func calculateMetrics(size: CGSize, additionalBottomInset: CGFloat) -> (topInset: CGFloat, itemWidth: CGFloat) {
private func calculateMetrics(size: CGSize, additionalBottomInset: CGFloat, isEmbedded: Bool) -> (topInset: CGFloat, itemWidth: CGFloat) {
let itemCount = self.entries.count
let itemInsets = UIEdgeInsets(top: 0.0, left: 12.0, bottom: 0.0, right: 12.0)
@ -394,7 +394,7 @@ final class SharePeersContainerNode: ASDisplayNode, ShareContentContainerNode {
}
let initiallyRevealedRowCount = min(minimallyRevealedRowCount, CGFloat(rowCount))
let gridTopInset = max(0.0, size.height - floor(initiallyRevealedRowCount * itemWidth) - 14.0 - additionalBottomInset)
let gridTopInset = isEmbedded ? 136.0 : max(0.0, size.height - floor(initiallyRevealedRowCount * itemWidth) - 14.0 - additionalBottomInset)
return (gridTopInset, itemWidth)
}
@ -569,10 +569,15 @@ final class SharePeersContainerNode: ASDisplayNode, ShareContentContainerNode {
}
}
var isEmbedded = false
func updateLayout(size: CGSize, isLandscape: Bool, bottomInset: CGFloat, transition: ContainedViewLayoutTransition) {
let firstLayout = self.validLayout == nil
self.validLayout = (size, bottomInset)
self.contentTitleNode.isHidden = self.isEmbedded
self.contentSubtitleNode.isHidden = self.isEmbedded
self.searchButtonNode.isHidden = self.isEmbedded
let gridLayoutTransition: ContainedViewLayoutTransition
if firstLayout {
gridLayoutTransition = .immediate
@ -582,7 +587,7 @@ final class SharePeersContainerNode: ASDisplayNode, ShareContentContainerNode {
self.overrideGridOffsetTransition = nil
}
let (gridTopInset, itemWidth) = self.calculateMetrics(size: size, additionalBottomInset: bottomInset)
let (gridTopInset, itemWidth) = self.calculateMetrics(size: size, additionalBottomInset: bottomInset, isEmbedded: self.isEmbedded)
var scrollToItem: GridNodeScrollToItem?
if let ensurePeerVisibleOnLayout = self.ensurePeerVisibleOnLayout {
@ -680,8 +685,8 @@ final class SharePeersContainerNode: ASDisplayNode, ShareContentContainerNode {
self.contentTitleNode.isHidden = true
self.contentSubtitleNode.isHidden = true
} else {
self.contentTitleNode.isHidden = false
self.contentSubtitleNode.isHidden = false
self.contentTitleNode.isHidden = self.isEmbedded
self.contentSubtitleNode.isHidden = self.isEmbedded
var subtitleText = self.strings.ShareMenu_SelectChats
if !self.controllerInteraction.selectedPeers.isEmpty {

View file

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

View file

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

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? {
return theme.image(PresentationResourceKey.chatListVerifiedIcon.rawValue, { theme in
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/TextProcessingScreen",
"//submodules/TelegramUI/Components/CreateBotScreen",
"//submodules/TelegramUI/Components/ShareScreen",
"//submodules/Utils/AutomationBridge",
] + select({
"@build_bazel_rules_apple//apple:ios_arm64": appcenter_targets,
"//build-system:ios_sim_arm64": [],

View file

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

View file

@ -227,7 +227,7 @@ final class AdminUserActionsPeerComponent: Component {
let titleSize = self.title.update(
transition: .immediate,
component: AnyComponent(MultilineTextComponent(
text: .plain(NSAttributedString(string: component.title, font: Font.semibold(component.baseFontSize), textColor: component.theme.list.itemPrimaryTextColor))
text: .plain(NSAttributedString(string: component.title, font: Font.medium(component.baseFontSize), textColor: component.theme.list.itemPrimaryTextColor))
)),
environment: {},
containerSize: CGSize(width: maxTextSize, height: 100.0)

View file

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

View file

@ -2684,8 +2684,9 @@ public class ChatMessagePollBubbleContentNode: ChatMessageBubbleContentNode {
var pollOptionsFinalizeLayouts: [(hasResult: Bool, layout: (CGFloat) -> (CGSize, (Bool, Bool, Bool) -> ChatMessagePollOptionNode))] = []
var addOptionFinalizeLayout: ((CGFloat) -> (CGSize, (Bool, Bool) -> ChatMessagePollAddOptionNode))?
var orderedPollOptions: [(Int, TelegramMediaPollOption)] = []
var isRestricted = false
if let poll = poll {
var isRestricted = false
if !poll.countries.isEmpty, let accountCountry = item.associatedData.accountCountry, !poll.countries.contains(accountCountry) {
isRestricted = true
}
@ -2842,430 +2843,428 @@ public class ChatMessagePollBubbleContentNode: ChatMessageBubbleContentNode {
let buttonViewResultsTextFrame = CGRect(origin: CGPoint(x: floor((resultSize.width - buttonViewResultsTextLayout.size.width) / 2.0), y: optionsButtonSpacing), size: buttonViewResultsTextLayout.size)
return (resultSize, { [weak self] animation, synchronousLoad, _ in
if let strongSelf = self {
strongSelf.item = item
strongSelf.poll = poll
let cachedLayout = strongSelf.textNode.textNode.cachedLayout
if case .System = animation {
if let cachedLayout = cachedLayout {
if !cachedLayout.areLinesEqual(to: textLayout) {
if let textContents = strongSelf.textNode.textNode.contents {
let fadeNode = ASDisplayNode()
fadeNode.displaysAsynchronously = false
fadeNode.contents = textContents
fadeNode.frame = strongSelf.textNode.textNode.frame
fadeNode.isLayerBacked = true
strongSelf.addSubnode(fadeNode)
fadeNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { [weak fadeNode] _ in
fadeNode?.removeFromSupernode()
})
strongSelf.textNode.textNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.15)
}
if let strongSelf = self {
strongSelf.item = item
strongSelf.poll = poll
let cachedLayout = strongSelf.textNode.textNode.cachedLayout
if case .System = animation {
if let cachedLayout = cachedLayout {
if !cachedLayout.areLinesEqual(to: textLayout) {
if let textContents = strongSelf.textNode.textNode.contents {
let fadeNode = ASDisplayNode()
fadeNode.displaysAsynchronously = false
fadeNode.contents = textContents
fadeNode.frame = strongSelf.textNode.textNode.frame
fadeNode.isLayerBacked = true
strongSelf.addSubnode(fadeNode)
fadeNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { [weak fadeNode] _ in
fadeNode?.removeFromSupernode()
})
strongSelf.textNode.textNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.15)
}
}
}
let _ = textApply(TextNodeWithEntities.Arguments(
context: item.context,
cache: item.context.animationCache,
renderer: item.context.animationRenderer,
placeholderColor: incoming ? item.presentationData.theme.theme.chat.message.incoming.mediaPlaceholderColor : item.presentationData.theme.theme.chat.message.outgoing.mediaPlaceholderColor,
attemptSynchronous: synchronousLoad)
)
let _ = typeApply()
var verticalOffset = textFrame.maxY + titleTypeSpacing + typeLayout.size.height + typeOptionsSpacing
var updatedOptionNodes: [ChatMessagePollOptionNode] = []
for i in 0 ..< optionNodesSizesAndApply.count {
let (size, apply) = optionNodesSizesAndApply[i]
var isRequesting = false
if i < orderedPollOptions.count {
if let inProgressOpaqueIds = item.controllerInteraction.pollActionState.pollMessageIdsInProgress[item.message.id] {
isRequesting = inProgressOpaqueIds.contains(orderedPollOptions[i].1.opaqueIdentifier)
}
}
let _ = textApply(TextNodeWithEntities.Arguments(
context: item.context,
cache: item.context.animationCache,
renderer: item.context.animationRenderer,
placeholderColor: incoming ? item.presentationData.theme.theme.chat.message.incoming.mediaPlaceholderColor : item.presentationData.theme.theme.chat.message.outgoing.mediaPlaceholderColor,
attemptSynchronous: synchronousLoad)
)
let _ = typeApply()
var verticalOffset = textFrame.maxY + titleTypeSpacing + typeLayout.size.height + typeOptionsSpacing
var updatedOptionNodes: [ChatMessagePollOptionNode] = []
for i in 0 ..< optionNodesSizesAndApply.count {
let (size, apply) = optionNodesSizesAndApply[i]
var isRequesting = false
if i < orderedPollOptions.count {
if let inProgressOpaqueIds = item.controllerInteraction.pollActionState.pollMessageIdsInProgress[item.message.id] {
isRequesting = inProgressOpaqueIds.contains(orderedPollOptions[i].1.opaqueIdentifier)
}
let optionNode = apply(animation.isAnimated, isRequesting, synchronousLoad)
let optionNodeFrame = CGRect(origin: CGPoint(x: layoutConstants.bubble.borderInset, y: verticalOffset), size: size)
if optionNode.supernode !== strongSelf {
strongSelf.addSubnode(optionNode)
let option = optionNode.option
optionNode.pressed = { [weak self] in
guard let self,
let item = self.item,
let option else {
return
}
item.controllerInteraction.requestSelectMessagePollOptions(item.message.id, [option.opaqueIdentifier])
}
let optionNode = apply(animation.isAnimated, isRequesting, synchronousLoad)
let optionNodeFrame = CGRect(origin: CGPoint(x: layoutConstants.bubble.borderInset, y: verticalOffset), size: size)
if optionNode.supernode !== strongSelf {
strongSelf.addSubnode(optionNode)
let option = optionNode.option
optionNode.pressed = { [weak self] in
guard let self,
let item = self.item,
let option else {
return
}
optionNode.resultPressed = { [weak self] in
guard let self,
let item = self.item,
let option else {
return
}
if let poll {
if case .public = poll.publicity {
item.controllerInteraction.openMessagePollResults(item.message.id, option.opaqueIdentifier)
} else {
if "".isEmpty {
let locale = localeWithStrings(item.presentationData.strings)
let countryNames = poll.countries.map { id in
if let countryName = locale.localizedString(forRegionCode: id) {
return countryName
} else {
return id
}
}
var countries: String = ""
if countryNames.count == 1, let country = countryNames.first {
countries = "**\(country)**"
} else {
for i in 0 ..< countryNames.count {
countries.append("**\(countryNames[i])**")
if i == countryNames.count - 2 {
countries.append(item.presentationData.strings.Chat_Poll_Restriction_Country_CountriesLastDelimiter)
} else if i < countryNames.count - 2 {
countries.append(item.presentationData.strings.Chat_Poll_Restriction_Country_CountriesDelimiter)
}
}
}
//TODO:localize
let controller = UndoOverlayController(
presentationData: item.context.sharedContext.currentPresentationData.with { $0 },
content: .banned(text: "Only users from \(countries) can vote."),
elevatedLayout: true,
position: .bottom,
action: { _ in return true }
)
item.controllerInteraction.presentController(controller, nil)
item.controllerInteraction.requestSelectMessagePollOptions(item.message.id, [option.opaqueIdentifier])
}
optionNode.resultPressed = { [weak self] in
guard let self,
let item = self.item,
let option else {
return
}
if let poll {
if case .public = poll.publicity {
item.controllerInteraction.openMessagePollResults(item.message.id, option.opaqueIdentifier)
} else if isRestricted {
let locale = localeWithStrings(item.presentationData.strings)
let countryNames = poll.countries.map { id in
if let countryName = locale.localizedString(forRegionCode: id) {
return countryName
} else {
return id
}
}
var countries: String = ""
if countryNames.count == 1, let country = countryNames.first {
countries = "**\(country)**"
} else {
for i in 0 ..< countryNames.count {
countries.append("**\(countryNames[i])**")
if i == countryNames.count - 2 {
countries.append(item.presentationData.strings.Chat_Poll_Restriction_Country_CountriesLastDelimiter)
} else if i < countryNames.count - 2 {
countries.append(item.presentationData.strings.Chat_Poll_Restriction_Country_CountriesDelimiter)
}
}
}
//TODO:localize
let controller = UndoOverlayController(
presentationData: item.context.sharedContext.currentPresentationData.with { $0 },
content: .banned(text: "Only users from \(countries) can vote."),
elevatedLayout: true,
position: .bottom,
action: { _ in return true }
)
item.controllerInteraction.presentController(controller, nil)
}
}
optionNode.selectionUpdated = { [weak self] in
guard let self else {
return
}
self.updateSelection()
}
optionNode.longTapped = { [weak self] in
guard let self,
let item = self.item,
let option else {
return
}
item.controllerInteraction.pollOptionLongTap(option.opaqueIdentifier, ChatControllerInteraction.LongTapParams(message: item.message, contentNode: optionNode.contextSourceNode, messageNode: strongSelf, progress: nil))
}
optionNode.frame = optionNodeFrame
} else {
animation.animator.updateFrame(layer: optionNode.layer, frame: optionNodeFrame, completion: nil)
}
if optionNode.currentResult != nil {
verticalOffset += size.height - 7.0
} else {
verticalOffset += size.height
}
updatedOptionNodes.append(optionNode)
optionNode.isUserInteractionEnabled = !strongSelf.newOptionIsFocused && item.controllerInteraction.pollActionState.pollMessageIdsInProgress[item.message.id] == nil
optionNode.alpha = strongSelf.newOptionIsFocused ? 0.5 : 1.0
if i > 0 {
optionNode.previousOptionNode = updatedOptionNodes[i - 1]
} else {
optionNode.previousOptionNode = nil
}
}
for optionNode in strongSelf.optionNodes {
if !updatedOptionNodes.contains(where: { $0 === optionNode }) {
optionNode.removeFromSupernode()
}
}
strongSelf.optionNodes = updatedOptionNodes
strongSelf.updatePollOptionsInteraction(animated: animation.isAnimated)
if let (size, apply) = addOptionSizeAndApply {
let isRequesting = item.controllerInteraction.pollActionState.pollMessageIdsInProgress[item.message.id] != nil
let addOptionNode = apply(animation.isAnimated, isRequesting)
let addOptionNodeFrame = CGRect(origin: CGPoint(x: layoutConstants.bubble.borderInset, y: verticalOffset), size: size)
if addOptionNode.supernode !== strongSelf {
strongSelf.addSubnode(addOptionNode)
} else {
animation.animator.updateFrame(layer: addOptionNode.layer, frame: addOptionNodeFrame, completion: nil)
}
addOptionNode.frame = addOptionNodeFrame
addOptionNode.isUserInteractionEnabled = !isRequesting
addOptionNode.textUpdated = { [weak self] text in
self?.updateNewOptionText(text)
}
addOptionNode.heightUpdated = { [weak self] in
self?.requestNewOptionLayoutUpdate()
}
addOptionNode.attachPressed = { [weak self] in
self?.openNewOptionAttachment()
}
addOptionNode.mediaPressed = { [weak self] in
self?.openNewOptionAttachment()
}
addOptionNode.modeSelectorPressed = { [weak self] in
self?.toggleNewOptionInputMode()
}
addOptionNode.requestSave = { [weak self] in
self?.buttonPressed()
}
addOptionNode.focusUpdated = { [weak self] focused in
optionNode.selectionUpdated = { [weak self] in
guard let self else {
return
}
self.updatePollAddOptionFocused(focused)
self.updateSelection()
}
strongSelf.addOptionNode = addOptionNode
optionNode.longTapped = { [weak self] in
guard let self,
let item = self.item,
let option else {
return
}
item.controllerInteraction.pollOptionLongTap(option.opaqueIdentifier, ChatControllerInteraction.LongTapParams(message: item.message, contentNode: optionNode.contextSourceNode, messageNode: strongSelf, progress: nil))
}
optionNode.frame = optionNodeFrame
} else {
animation.animator.updateFrame(layer: optionNode.layer, frame: optionNodeFrame, completion: nil)
}
if optionNode.currentResult != nil {
verticalOffset += size.height - 7.0
} else {
verticalOffset += size.height
} else if let addOptionNode = strongSelf.addOptionNode {
strongSelf.updatePollAddOptionFocused(false)
strongSelf.addOptionNode = nil
addOptionNode.removeFromSupernode()
}
updatedOptionNodes.append(optionNode)
optionNode.isUserInteractionEnabled = !strongSelf.newOptionIsFocused && item.controllerInteraction.pollActionState.pollMessageIdsInProgress[item.message.id] == nil
optionNode.alpha = strongSelf.newOptionIsFocused ? 0.5 : 1.0
if let poll = poll, let pendingNewOptionSubmissionText, let pendingNewOptionOptionCount = strongSelf.pendingNewOptionOptionCount, poll.options.count > pendingNewOptionOptionCount, poll.options.contains(where: { $0.text == pendingNewOptionSubmissionText }) {
strongSelf.clearNewOptionInput()
}
if textLayout.hasRTL {
strongSelf.textNode.textNode.frame = CGRect(origin: CGPoint(x: resultSize.width - textFrame.size.width - textInsets.left - layoutConstants.text.bubbleInsets.right - additionalTextRightInset, y: textFrame.origin.y), size: textFrame.size)
if i > 0 {
optionNode.previousOptionNode = updatedOptionNodes[i - 1]
} else {
strongSelf.textNode.textNode.frame = textFrame
optionNode.previousOptionNode = nil
}
let typeFrame = CGRect(origin: CGPoint(x: textFrame.minX, y: textFrame.maxY + titleTypeSpacing), size: typeLayout.size)
animation.animator.updateFrame(layer: strongSelf.typeNode.layer, frame: typeFrame, completion: nil)
let deadlineTimeout = poll?.deadlineTimeout
var displayDeadlineTimer = true
var hasSelected = false
if let poll {
if let voters = poll.results.voters {
for voter in voters {
if voter.selected {
if case .quiz = poll.kind {
displayDeadlineTimer = false
} else {
displayDeadlineTimer = !poll.isClosed
}
hasSelected = true
break
}
}
}
}
var endDate: Int32?
if let deadlineTimeout,
message.id.namespace == Namespaces.Message.Cloud {
let startDate: Int32
if let forwardInfo = message.forwardInfo {
startDate = forwardInfo.date
} else {
startDate = message.timestamp
}
endDate = startDate + deadlineTimeout
}
if let poll, case .quiz = poll.kind, let deadlineTimeout, !isClosed {
let timerNode: PollBubbleTimerNode
if let current = strongSelf.timerNode {
timerNode = current
let timerTransition: ContainedViewLayoutTransition
if animation.isAnimated {
timerTransition = .animated(duration: 0.25, curve: .easeInOut)
} else {
timerTransition = .immediate
}
if displayDeadlineTimer {
timerTransition.updateAlpha(node: timerNode, alpha: 1.0)
} else {
timerTransition.updateAlpha(node: timerNode, alpha: 0.0)
}
} else {
timerNode = PollBubbleTimerNode()
strongSelf.timerNode = timerNode
strongSelf.addSubnode(timerNode)
timerNode.reachedTimeout = {
guard let strongSelf = self,
let _ = strongSelf.item else {
return
}
//item.controllerInteraction.requestMessageUpdate(item.message.id)
}
let timerTransition: ContainedViewLayoutTransition
if animation.isAnimated {
timerTransition = .animated(duration: 0.25, curve: .easeInOut)
} else {
timerTransition = .immediate
}
if displayDeadlineTimer {
timerNode.alpha = 0.0
timerTransition.updateAlpha(node: timerNode, alpha: 1.0)
} else {
timerNode.alpha = 0.0
}
}
timerNode.update(regularColor: messageTheme.secondaryTextColor, proximityColor: messageTheme.scamColor, timeout: deadlineTimeout, deadlineTimestamp: endDate)
timerNode.frame = CGRect(origin: CGPoint(x: resultSize.width - layoutConstants.text.bubbleInsets.right, y: typeFrame.minY), size: CGSize())
} else if let timerNode = strongSelf.timerNode {
strongSelf.timerNode = nil
let timerTransition: ContainedViewLayoutTransition
if animation.isAnimated {
timerTransition = .animated(duration: 0.25, curve: .easeInOut)
} else {
timerTransition = .immediate
}
timerTransition.updateAlpha(node: timerNode, alpha: 0.0, completion: { [weak timerNode] _ in
timerNode?.removeFromSupernode()
})
timerTransition.updateTransformScale(node: timerNode, scale: 0.1)
}
var statusOffset: CGFloat = 0.0
if let poll, case .poll = poll.kind, let endDate, !isClosed {
let timerNode: DeadlineTimerNode
if let current = strongSelf.deadlineTimerNode {
timerNode = current
let timerTransition: ContainedViewLayoutTransition
if animation.isAnimated {
timerTransition = .animated(duration: 0.25, curve: .easeInOut)
} else {
timerTransition = .immediate
}
if displayDeadlineTimer {
timerTransition.updateAlpha(node: timerNode, alpha: 1.0)
} else {
timerTransition.updateAlpha(node: timerNode, alpha: 0.0)
}
} else {
timerNode = DeadlineTimerNode()
strongSelf.deadlineTimerNode = timerNode
strongSelf.addSubnode(timerNode)
let timerTransition: ContainedViewLayoutTransition
if animation.isAnimated {
timerTransition = .animated(duration: 0.25, curve: .easeInOut)
} else {
timerTransition = .immediate
}
if displayDeadlineTimer {
timerNode.alpha = 0.0
timerTransition.updateAlpha(node: timerNode, alpha: 1.0)
} else {
timerNode.alpha = 0.0
}
}
timerNode.update(size: resultSize, color: messageTheme.secondaryTextColor, deadlineTimeout: endDate, resultsHidden: poll.hideResultsUntilClose, strings: item.presentationData.strings)
timerNode.frame = CGRect(origin: CGPoint(x: 0.0, y: verticalOffset + 31.0), size: CGSize(width: resultSize.width, height: 20.0))
statusOffset += 6.0
} else if let timerNode = strongSelf.deadlineTimerNode {
strongSelf.deadlineTimerNode = nil
let timerTransition: ContainedViewLayoutTransition
if animation.isAnimated {
timerTransition = .animated(duration: 0.25, curve: .easeInOut)
} else {
timerTransition = .immediate
}
timerTransition.updateAlpha(node: timerNode, alpha: 0.0, completion: { [weak timerNode] _ in
timerNode?.removeFromSupernode()
})
timerTransition.updateTransformScale(node: timerNode, scale: 0.1)
}
let solutionButtonSize = CGSize(width: 32.0, height: 32.0)
let solutionButtonFrame = CGRect(origin: CGPoint(x: resultSize.width - layoutConstants.text.bubbleInsets.right - solutionButtonSize.width + 5.0, y: typeFrame.minY - 16.0), size: solutionButtonSize)
strongSelf.solutionButtonNode.frame = solutionButtonFrame
if (strongSelf.timerNode == nil || !displayDeadlineTimer), let poll = poll, case .quiz = poll.kind, let _ = poll.results.solution, (isClosed || hasSelected) {
if strongSelf.solutionButtonNode.alpha.isZero {
let timerTransition: ContainedViewLayoutTransition
if animation.isAnimated {
timerTransition = .animated(duration: 0.25, curve: .easeInOut)
} else {
timerTransition = .immediate
}
timerTransition.updateAlpha(node: strongSelf.solutionButtonNode, alpha: 1.0)
}
strongSelf.solutionButtonNode.update(size: solutionButtonSize, theme: item.presentationData.theme.theme, incoming: incoming)
} else if !strongSelf.solutionButtonNode.alpha.isZero {
let timerTransition: ContainedViewLayoutTransition
if animation.isAnimated {
timerTransition = .animated(duration: 0.25, curve: .easeInOut)
} else {
timerTransition = .immediate
}
timerTransition.updateAlpha(node: strongSelf.solutionButtonNode, alpha: 0.0)
}
let avatarsFrame = CGRect(origin: CGPoint(x: typeFrame.maxX + 6.0, y: typeFrame.minY + floor((typeFrame.height - MergedAvatarsNode.defaultMergedImageSize) / 2.0)), size: CGSize(width: MergedAvatarsNode.defaultMergedImageSize + MergedAvatarsNode.defaultMergedImageSpacing * 2.0, height: MergedAvatarsNode.defaultMergedImageSize))
strongSelf.avatarsNode.frame = avatarsFrame
strongSelf.avatarsNode.updateLayout(size: avatarsFrame.size)
strongSelf.avatarsNode.update(context: item.context, peers: avatarPeers, synchronousLoad: synchronousLoad, imageSize: MergedAvatarsNode.defaultMergedImageSize, imageSpacing: MergedAvatarsNode.defaultMergedImageSpacing, borderWidth: MergedAvatarsNode.defaultBorderWidth)
strongSelf.avatarsNode.isHidden = isBotChat
let alphaTransition: ContainedViewLayoutTransition
if animation.isAnimated {
alphaTransition = .animated(duration: 0.25, curve: .easeInOut)
alphaTransition.updateAlpha(node: strongSelf.avatarsNode, alpha: avatarPeers.isEmpty ? 0.0 : 1.0)
} else {
alphaTransition = .immediate
}
let _ = votersApply()
let votersFrame = CGRect(origin: CGPoint(x: floor((resultSize.width - votersLayout.size.width) / 2.0), y: verticalOffset + optionsVotersSpacing), size: votersLayout.size)
animation.animator.updateFrame(layer: strongSelf.votersNode.layer, frame: votersFrame, completion: nil)
if animation.isAnimated, let previousPoll = previousPoll, let poll = poll {
if previousPoll.results.totalVoters == nil && poll.results.totalVoters != nil {
strongSelf.votersNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.25)
}
}
if let statusSizeAndApply = statusSizeAndApply {
let statusFrame = CGRect(origin: CGPoint(x: resultSize.width - statusSizeAndApply.0.width - layoutConstants.text.bubbleInsets.right, y: votersFrame.maxY + statusOffset), size: statusSizeAndApply.0)
if strongSelf.statusNode.supernode == nil {
statusSizeAndApply.1(.None)
strongSelf.statusNode.frame = statusFrame
strongSelf.addSubnode(strongSelf.statusNode)
} else {
statusSizeAndApply.1(animation)
animation.animator.updateFrame(layer: strongSelf.statusNode.layer, frame: statusFrame, completion: nil)
}
} else if strongSelf.statusNode.supernode != nil {
strongSelf.statusNode.removeFromSupernode()
}
let _ = buttonSubmitInactiveTextApply()
strongSelf.buttonSubmitInactiveTextNode.frame = buttonSubmitInactiveTextFrame.offsetBy(dx: 0.0, dy: verticalOffset)
let _ = buttonSubmitActiveTextApply()
strongSelf.buttonSubmitActiveTextNode.frame = buttonSubmitActiveTextFrame.offsetBy(dx: 0.0, dy: verticalOffset)
let _ = buttonSaveTextApply()
strongSelf.buttonSaveTextNode.frame = buttonSaveTextFrame.offsetBy(dx: 0.0, dy: verticalOffset)
let _ = buttonViewResultsTextApply()
strongSelf.buttonViewResultsTextNode.frame = buttonViewResultsTextFrame.offsetBy(dx: 0.0, dy: verticalOffset)
strongSelf.updateSelection()
strongSelf.updatePollTooltipMessageState(animated: false)
let buttonWidth: CGFloat = floor(max(strongSelf.buttonSaveTextNode.frame.width, max(strongSelf.buttonViewResultsTextNode.frame.width, strongSelf.buttonSubmitActiveTextNode.frame.width)) * 1.1)
strongSelf.buttonNode.frame = CGRect(origin: CGPoint(x: floor((resultSize.width - buttonWidth) / 2.0), y: verticalOffset), size: CGSize(width: buttonWidth, height: 44.0))
strongSelf.updateIsTranslating(isTranslating)
}
})
for optionNode in strongSelf.optionNodes {
if !updatedOptionNodes.contains(where: { $0 === optionNode }) {
optionNode.removeFromSupernode()
}
}
strongSelf.optionNodes = updatedOptionNodes
strongSelf.updatePollOptionsInteraction(animated: animation.isAnimated)
if let (size, apply) = addOptionSizeAndApply {
let isRequesting = item.controllerInteraction.pollActionState.pollMessageIdsInProgress[item.message.id] != nil
let addOptionNode = apply(animation.isAnimated, isRequesting)
let addOptionNodeFrame = CGRect(origin: CGPoint(x: layoutConstants.bubble.borderInset, y: verticalOffset), size: size)
if addOptionNode.supernode !== strongSelf {
strongSelf.addSubnode(addOptionNode)
} else {
animation.animator.updateFrame(layer: addOptionNode.layer, frame: addOptionNodeFrame, completion: nil)
}
addOptionNode.frame = addOptionNodeFrame
addOptionNode.isUserInteractionEnabled = !isRequesting
addOptionNode.textUpdated = { [weak self] text in
self?.updateNewOptionText(text)
}
addOptionNode.heightUpdated = { [weak self] in
self?.requestNewOptionLayoutUpdate()
}
addOptionNode.attachPressed = { [weak self] in
self?.openNewOptionAttachment()
}
addOptionNode.mediaPressed = { [weak self] in
self?.openNewOptionAttachment()
}
addOptionNode.modeSelectorPressed = { [weak self] in
self?.toggleNewOptionInputMode()
}
addOptionNode.requestSave = { [weak self] in
self?.buttonPressed()
}
addOptionNode.focusUpdated = { [weak self] focused in
guard let self else {
return
}
self.updatePollAddOptionFocused(focused)
}
strongSelf.addOptionNode = addOptionNode
verticalOffset += size.height
} else if let addOptionNode = strongSelf.addOptionNode {
strongSelf.updatePollAddOptionFocused(false)
strongSelf.addOptionNode = nil
addOptionNode.removeFromSupernode()
}
if let poll = poll, let pendingNewOptionSubmissionText, let pendingNewOptionOptionCount = strongSelf.pendingNewOptionOptionCount, poll.options.count > pendingNewOptionOptionCount, poll.options.contains(where: { $0.text == pendingNewOptionSubmissionText }) {
strongSelf.clearNewOptionInput()
}
if textLayout.hasRTL {
strongSelf.textNode.textNode.frame = CGRect(origin: CGPoint(x: resultSize.width - textFrame.size.width - textInsets.left - layoutConstants.text.bubbleInsets.right - additionalTextRightInset, y: textFrame.origin.y), size: textFrame.size)
} else {
strongSelf.textNode.textNode.frame = textFrame
}
let typeFrame = CGRect(origin: CGPoint(x: textFrame.minX, y: textFrame.maxY + titleTypeSpacing), size: typeLayout.size)
animation.animator.updateFrame(layer: strongSelf.typeNode.layer, frame: typeFrame, completion: nil)
let deadlineTimeout = poll?.deadlineTimeout
var displayDeadlineTimer = true
var hasSelected = false
if let poll {
if let voters = poll.results.voters {
for voter in voters {
if voter.selected {
if case .quiz = poll.kind {
displayDeadlineTimer = false
} else {
displayDeadlineTimer = !poll.isClosed
}
hasSelected = true
break
}
}
}
}
var endDate: Int32?
if let deadlineTimeout,
message.id.namespace == Namespaces.Message.Cloud {
let startDate: Int32
if let forwardInfo = message.forwardInfo {
startDate = forwardInfo.date
} else {
startDate = message.timestamp
}
endDate = startDate + deadlineTimeout
}
if let poll, case .quiz = poll.kind, let deadlineTimeout, !isClosed {
let timerNode: PollBubbleTimerNode
if let current = strongSelf.timerNode {
timerNode = current
let timerTransition: ContainedViewLayoutTransition
if animation.isAnimated {
timerTransition = .animated(duration: 0.25, curve: .easeInOut)
} else {
timerTransition = .immediate
}
if displayDeadlineTimer {
timerTransition.updateAlpha(node: timerNode, alpha: 1.0)
} else {
timerTransition.updateAlpha(node: timerNode, alpha: 0.0)
}
} else {
timerNode = PollBubbleTimerNode()
strongSelf.timerNode = timerNode
strongSelf.addSubnode(timerNode)
timerNode.reachedTimeout = {
guard let strongSelf = self,
let _ = strongSelf.item else {
return
}
//item.controllerInteraction.requestMessageUpdate(item.message.id)
}
let timerTransition: ContainedViewLayoutTransition
if animation.isAnimated {
timerTransition = .animated(duration: 0.25, curve: .easeInOut)
} else {
timerTransition = .immediate
}
if displayDeadlineTimer {
timerNode.alpha = 0.0
timerTransition.updateAlpha(node: timerNode, alpha: 1.0)
} else {
timerNode.alpha = 0.0
}
}
timerNode.update(regularColor: messageTheme.secondaryTextColor, proximityColor: messageTheme.scamColor, timeout: deadlineTimeout, deadlineTimestamp: endDate)
timerNode.frame = CGRect(origin: CGPoint(x: resultSize.width - layoutConstants.text.bubbleInsets.right, y: typeFrame.minY), size: CGSize())
} else if let timerNode = strongSelf.timerNode {
strongSelf.timerNode = nil
let timerTransition: ContainedViewLayoutTransition
if animation.isAnimated {
timerTransition = .animated(duration: 0.25, curve: .easeInOut)
} else {
timerTransition = .immediate
}
timerTransition.updateAlpha(node: timerNode, alpha: 0.0, completion: { [weak timerNode] _ in
timerNode?.removeFromSupernode()
})
timerTransition.updateTransformScale(node: timerNode, scale: 0.1)
}
var statusOffset: CGFloat = 0.0
if let poll, case .poll = poll.kind, let endDate, !isClosed {
let timerNode: DeadlineTimerNode
if let current = strongSelf.deadlineTimerNode {
timerNode = current
let timerTransition: ContainedViewLayoutTransition
if animation.isAnimated {
timerTransition = .animated(duration: 0.25, curve: .easeInOut)
} else {
timerTransition = .immediate
}
if displayDeadlineTimer {
timerTransition.updateAlpha(node: timerNode, alpha: 1.0)
} else {
timerTransition.updateAlpha(node: timerNode, alpha: 0.0)
}
} else {
timerNode = DeadlineTimerNode()
strongSelf.deadlineTimerNode = timerNode
strongSelf.addSubnode(timerNode)
let timerTransition: ContainedViewLayoutTransition
if animation.isAnimated {
timerTransition = .animated(duration: 0.25, curve: .easeInOut)
} else {
timerTransition = .immediate
}
if displayDeadlineTimer {
timerNode.alpha = 0.0
timerTransition.updateAlpha(node: timerNode, alpha: 1.0)
} else {
timerNode.alpha = 0.0
}
}
timerNode.update(size: resultSize, color: messageTheme.secondaryTextColor, deadlineTimeout: endDate, resultsHidden: poll.hideResultsUntilClose, strings: item.presentationData.strings)
timerNode.frame = CGRect(origin: CGPoint(x: 0.0, y: verticalOffset + 31.0), size: CGSize(width: resultSize.width, height: 20.0))
statusOffset += 6.0
} else if let timerNode = strongSelf.deadlineTimerNode {
strongSelf.deadlineTimerNode = nil
let timerTransition: ContainedViewLayoutTransition
if animation.isAnimated {
timerTransition = .animated(duration: 0.25, curve: .easeInOut)
} else {
timerTransition = .immediate
}
timerTransition.updateAlpha(node: timerNode, alpha: 0.0, completion: { [weak timerNode] _ in
timerNode?.removeFromSupernode()
})
timerTransition.updateTransformScale(node: timerNode, scale: 0.1)
}
let solutionButtonSize = CGSize(width: 32.0, height: 32.0)
let solutionButtonFrame = CGRect(origin: CGPoint(x: resultSize.width - layoutConstants.text.bubbleInsets.right - solutionButtonSize.width + 5.0, y: typeFrame.minY - 16.0), size: solutionButtonSize)
strongSelf.solutionButtonNode.frame = solutionButtonFrame
if (strongSelf.timerNode == nil || !displayDeadlineTimer), let poll = poll, case .quiz = poll.kind, let _ = poll.results.solution, (isClosed || hasSelected) {
if strongSelf.solutionButtonNode.alpha.isZero {
let timerTransition: ContainedViewLayoutTransition
if animation.isAnimated {
timerTransition = .animated(duration: 0.25, curve: .easeInOut)
} else {
timerTransition = .immediate
}
timerTransition.updateAlpha(node: strongSelf.solutionButtonNode, alpha: 1.0)
}
strongSelf.solutionButtonNode.update(size: solutionButtonSize, theme: item.presentationData.theme.theme, incoming: incoming)
} else if !strongSelf.solutionButtonNode.alpha.isZero {
let timerTransition: ContainedViewLayoutTransition
if animation.isAnimated {
timerTransition = .animated(duration: 0.25, curve: .easeInOut)
} else {
timerTransition = .immediate
}
timerTransition.updateAlpha(node: strongSelf.solutionButtonNode, alpha: 0.0)
}
let avatarsFrame = CGRect(origin: CGPoint(x: typeFrame.maxX + 6.0, y: typeFrame.minY + floor((typeFrame.height - MergedAvatarsNode.defaultMergedImageSize) / 2.0)), size: CGSize(width: MergedAvatarsNode.defaultMergedImageSize + MergedAvatarsNode.defaultMergedImageSpacing * 2.0, height: MergedAvatarsNode.defaultMergedImageSize))
strongSelf.avatarsNode.frame = avatarsFrame
strongSelf.avatarsNode.updateLayout(size: avatarsFrame.size)
strongSelf.avatarsNode.update(context: item.context, peers: avatarPeers, synchronousLoad: synchronousLoad, imageSize: MergedAvatarsNode.defaultMergedImageSize, imageSpacing: MergedAvatarsNode.defaultMergedImageSpacing, borderWidth: MergedAvatarsNode.defaultBorderWidth)
strongSelf.avatarsNode.isHidden = isBotChat
let alphaTransition: ContainedViewLayoutTransition
if animation.isAnimated {
alphaTransition = .animated(duration: 0.25, curve: .easeInOut)
alphaTransition.updateAlpha(node: strongSelf.avatarsNode, alpha: avatarPeers.isEmpty ? 0.0 : 1.0)
} else {
alphaTransition = .immediate
}
let _ = votersApply()
let votersFrame = CGRect(origin: CGPoint(x: floor((resultSize.width - votersLayout.size.width) / 2.0), y: verticalOffset + optionsVotersSpacing), size: votersLayout.size)
animation.animator.updateFrame(layer: strongSelf.votersNode.layer, frame: votersFrame, completion: nil)
if animation.isAnimated, let previousPoll = previousPoll, let poll = poll {
if previousPoll.results.totalVoters == nil && poll.results.totalVoters != nil {
strongSelf.votersNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.25)
}
}
if let statusSizeAndApply = statusSizeAndApply {
let statusFrame = CGRect(origin: CGPoint(x: resultSize.width - statusSizeAndApply.0.width - layoutConstants.text.bubbleInsets.right, y: votersFrame.maxY + statusOffset), size: statusSizeAndApply.0)
if strongSelf.statusNode.supernode == nil {
statusSizeAndApply.1(.None)
strongSelf.statusNode.frame = statusFrame
strongSelf.addSubnode(strongSelf.statusNode)
} else {
statusSizeAndApply.1(animation)
animation.animator.updateFrame(layer: strongSelf.statusNode.layer, frame: statusFrame, completion: nil)
}
} else if strongSelf.statusNode.supernode != nil {
strongSelf.statusNode.removeFromSupernode()
}
let _ = buttonSubmitInactiveTextApply()
strongSelf.buttonSubmitInactiveTextNode.frame = buttonSubmitInactiveTextFrame.offsetBy(dx: 0.0, dy: verticalOffset)
let _ = buttonSubmitActiveTextApply()
strongSelf.buttonSubmitActiveTextNode.frame = buttonSubmitActiveTextFrame.offsetBy(dx: 0.0, dy: verticalOffset)
let _ = buttonSaveTextApply()
strongSelf.buttonSaveTextNode.frame = buttonSaveTextFrame.offsetBy(dx: 0.0, dy: verticalOffset)
let _ = buttonViewResultsTextApply()
strongSelf.buttonViewResultsTextNode.frame = buttonViewResultsTextFrame.offsetBy(dx: 0.0, dy: verticalOffset)
strongSelf.updateSelection()
strongSelf.updatePollTooltipMessageState(animated: false)
let buttonWidth: CGFloat = floor(max(strongSelf.buttonSaveTextNode.frame.width, max(strongSelf.buttonViewResultsTextNode.frame.width, strongSelf.buttonSubmitActiveTextNode.frame.width)) * 1.1)
strongSelf.buttonNode.frame = CGRect(origin: CGPoint(x: floor((resultSize.width - buttonWidth) / 2.0), y: verticalOffset), size: CGSize(width: buttonWidth, height: 44.0))
strongSelf.updateIsTranslating(isTranslating)
}
})
})
})
}

View file

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

View file

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

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 AsyncDisplayKit
import Display
import SearchBarNode
import SwiftSignalKit
import Postbox
import TelegramCore
@ -16,7 +17,23 @@ import StickerPeekUI
import EntityKeyboardGifContent
import BatchVideoRendering
private let searchBarHeight: CGFloat = 52.0
private let searchBarHeight: CGFloat = 76.0
private let searchBarTopInset: CGFloat = 16.0
private let searchBarFieldHeight: CGFloat = 44.0
private func paneSearchBarTheme(_ theme: PresentationTheme) -> SearchBarNodeTheme {
return SearchBarNodeTheme(
background: .clear,
separator: .clear,
inputFill: .clear,
primaryText: theme.chat.inputPanel.panelControlColor,
placeholder: theme.chat.inputPanel.inputPlaceholderColor,
inputIcon: theme.chat.inputPanel.inputControlColor,
inputClear: theme.chat.inputPanel.panelControlColor,
accent: theme.chat.inputPanel.panelControlAccentColor,
keyboard: theme.rootController.keyboardColor
)
}
public protocol PaneSearchContentNode {
var ready: Signal<Void, NoError> { get }
@ -43,9 +60,10 @@ public final class PaneSearchContainerNode: ASDisplayNode, EntitySearchContainer
private let peekBehavior: EmojiContentPeekBehavior?
private let backgroundNode: ASDisplayNode
private let searchBar: PaneSearchBarNode
private let searchBar: SearchBarNode
private var validLayout: CGSize?
private weak var animatedPlaceholder: PaneSearchBarPlaceholderNode?
public var onCancel: (() -> Void)?
@ -69,7 +87,13 @@ public final class PaneSearchContainerNode: ASDisplayNode, EntitySearchContainer
}
self.backgroundNode = ASDisplayNode()
self.searchBar = PaneSearchBarNode()
self.searchBar = SearchBarNode(
theme: paneSearchBarTheme(theme),
presentationTheme: theme,
strings: strings,
fieldStyle: .glass,
displayBackground: false
)
super.init()
@ -87,9 +111,8 @@ public final class PaneSearchContainerNode: ASDisplayNode, EntitySearchContainer
}
self.searchBar.cancel = { [weak self] in
self?.searchBar.deactivate(clear: false)
cancel()
self?.searchBar.view.endEditing(true)
self?.onCancel?()
}
self.searchBar.activate()
@ -150,7 +173,7 @@ public final class PaneSearchContainerNode: ASDisplayNode, EntitySearchContainer
public func updateThemeAndStrings(theme: PresentationTheme, strings: PresentationStrings) {
self.backgroundNode.backgroundColor = theme.chat.inputMediaPanel.stickersBackgroundColor.withAlphaComponent(1.0)
self.contentNode.updateThemeAndStrings(theme: theme, strings: strings)
self.searchBar.updateThemeAndStrings(theme: theme, strings: strings)
self.searchBar.updateThemeAndStrings(theme: paneSearchBarTheme(theme), presentationTheme: theme, strings: strings)
let placeholder: String
switch mode {
@ -159,11 +182,11 @@ public final class PaneSearchContainerNode: ASDisplayNode, EntitySearchContainer
case .sticker, .trending:
placeholder = strings.Stickers_Search
}
self.searchBar.placeholderString = NSAttributedString(string: placeholder, font: Font.regular(17.0), textColor: theme.chat.inputMediaPanel.stickersSearchPlaceholderColor)
self.searchBar.placeholderString = NSAttributedString(string: placeholder, font: Font.regular(17.0), textColor: theme.rootController.navigationSearchBar.inputPlaceholderTextColor)
}
public func updateQuery(_ query: String) {
self.searchBar.updateQuery(query)
self.searchBar.text = query
}
public func itemAt(point: CGPoint) -> (ASDisplayNode, Any)? {
@ -174,8 +197,9 @@ public final class PaneSearchContainerNode: ASDisplayNode, EntitySearchContainer
self.validLayout = size
transition.updateFrame(node: self.backgroundNode, frame: CGRect(origin: CGPoint(), size: size))
transition.updateFrame(node: self.searchBar, frame: CGRect(origin: CGPoint(), size: CGSize(width: size.width, height: searchBarHeight)))
self.searchBar.updateLayout(boundingSize: CGSize(width: size.width, height: searchBarHeight), leftInset: leftInset, rightInset: rightInset, transition: transition)
let searchBarFrame = CGRect(origin: CGPoint(x: 0.0, y: searchBarTopInset), size: CGSize(width: size.width, height: searchBarFieldHeight))
transition.updateFrame(node: self.searchBar, frame: searchBarFrame)
self.searchBar.updateLayout(boundingSize: searchBarFrame.size, leftInset: leftInset, rightInset: rightInset, transition: transition)
let contentFrame = CGRect(origin: CGPoint(x: leftInset, y: searchBarHeight), size: CGSize(width: size.width - leftInset - rightInset, height: size.height - searchBarHeight))
@ -190,6 +214,9 @@ public final class PaneSearchContainerNode: ASDisplayNode, EntitySearchContainer
public func animateIn(from placeholder: PaneSearchBarPlaceholderNode?, anchorTop: CGPoint, anhorTopView: UIView, transition: ContainedViewLayoutTransition, completion: @escaping () -> Void) {
var verticalOrigin: CGFloat = anhorTopView.convert(anchorTop, to: self.view).y
if let placeholder = placeholder {
self.animatedPlaceholder = placeholder
placeholder.isHidden = true
let placeholderFrame = placeholder.view.convert(placeholder.bounds, to: self.view)
verticalOrigin = placeholderFrame.minY - 4.0
self.contentNode.animateIn(additivePosition: verticalOrigin, transition: transition)
@ -197,33 +224,65 @@ public final class PaneSearchContainerNode: ASDisplayNode, EntitySearchContainer
self.contentNode.animateIn(additivePosition: 0.0, transition: transition)
}
let searchBarFrame = self.searchBar.frame
let initialSearchBarFrame = CGRect(origin: CGPoint(x: searchBarFrame.minX, y: verticalOrigin), size: searchBarFrame.size)
switch transition {
case let .animated(duration, curve):
self.backgroundNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: duration / 2.0)
if let placeholder = placeholder {
self.searchBar.animateIn(from: placeholder, duration: duration, timingFunction: curve.timingFunction, completion: completion)
} else {
self.searchBar.alpha = 0.0
transition.updateAlpha(node: self.searchBar, alpha: 1.0)
}
self.searchBar.alpha = 1.0
self.searchBar.layer.animateAlpha(from: 0.0, to: 1.0, duration: duration, timingFunction: curve.timingFunction, completion: { _ in
completion()
})
self.searchBar.layer.animateFrame(from: initialSearchBarFrame, to: searchBarFrame, duration: duration, timingFunction: curve.timingFunction)
if let size = self.validLayout {
let initialBackgroundFrame = CGRect(origin: CGPoint(x: 0.0, y: verticalOrigin), size: CGSize(width: size.width, height: max(0.0, size.height - verticalOrigin)))
self.backgroundNode.layer.animateFrame(from: initialBackgroundFrame, to: self.backgroundNode.frame, duration: duration, timingFunction: curve.timingFunction)
}
case .immediate:
completion()
break
}
}
public func animateOut(to placeholder: PaneSearchBarPlaceholderNode, animateOutSearchBar: Bool, transition: ContainedViewLayoutTransition, completion: @escaping () -> Void) {
let finish: () -> Void = { [weak self] in
placeholder.isHidden = false
if let self, self.animatedPlaceholder === placeholder {
self.animatedPlaceholder = nil
}
completion()
}
if case let .animated(duration, curve) = transition {
let placeholderFrame = placeholder.view.convert(placeholder.bounds, to: self.view)
let verticalOrigin = placeholderFrame.minY - 4.0
let targetSearchBarFrame = CGRect(origin: CGPoint(x: self.searchBar.frame.minX, y: verticalOrigin), size: self.searchBar.frame.size)
if let size = self.validLayout {
let placeholderFrame = placeholder.view.convert(placeholder.bounds, to: self.view)
let verticalOrigin = placeholderFrame.minY - 4.0
self.backgroundNode.layer.animateFrame(from: self.backgroundNode.frame, to: CGRect(origin: CGPoint(x: 0.0, y: verticalOrigin), size: CGSize(width: size.width, height: max(0.0, size.height - verticalOrigin))), duration: duration, timingFunction: curve.timingFunction, removeOnCompletion: false)
}
self.searchBar.layer.animateFrame(from: self.searchBar.frame, to: targetSearchBarFrame, duration: duration, timingFunction: curve.timingFunction, removeOnCompletion: false)
if animateOutSearchBar {
self.searchBar.alpha = 0.0
self.searchBar.layer.animateAlpha(from: 1.0, to: 0.0, duration: duration, timingFunction: curve.timingFunction, removeOnCompletion: false, completion: { _ in
finish()
})
} else {
self.searchBar.layer.animateAlpha(from: self.searchBar.alpha, to: self.searchBar.alpha, duration: duration, timingFunction: curve.timingFunction, removeOnCompletion: false, completion: { _ in
finish()
})
}
} else {
if animateOutSearchBar {
self.searchBar.alpha = 0.0
}
finish()
}
self.searchBar.transitionOut(to: placeholder, transition: transition, completion: completion)
transition.updateAlpha(node: self.backgroundNode, alpha: 0.0)
if animateOutSearchBar {
transition.updateAlpha(node: self.searchBar, alpha: 0.0)

View file

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

View file

@ -13,10 +13,12 @@ import SheetComponent
import ButtonComponent
import PlainButtonComponent
import BundleIconComponent
import LottieAnimationComponent
import GlassBackgroundComponent
import GlassBarButtonComponent
import DatePickerNode
import UndoUI
import TooltipUI
private let calendar = Calendar(identifier: .gregorian)
@ -66,6 +68,8 @@ private final class ChatScheduleTimeSheetContentComponent: Component {
final class View: UIView {
private let cancel = ComponentView<Empty>()
private let silent = ComponentView<Empty>()
private let title = ComponentView<Empty>()
private let button = ComponentView<Empty>()
private let secondaryButton = ComponentView<Empty>()
@ -93,13 +97,15 @@ private final class ChatScheduleTimeSheetContentComponent: Component {
private var monthHeight: CGFloat?
private var isSilentPosting = false
private var date: Date?
private var minDate: Date?
private var maxDate: Date?
private var isPickingTime = false
private var isPickingRepeatPeriod = false
private var repeatPeriod: Int32?
private let dateFormatter: DateFormatter
@ -144,6 +150,65 @@ private final class ChatScheduleTimeSheetContentComponent: Component {
}
}
private func presentSilentPostingTooltip() {
guard let component = self.component, let sourceView = self.silent.view else {
return
}
let peerId: EnginePeer.Id?
switch component.mode {
case let .scheduledMessages(peerIdValue, _):
peerId = peerIdValue
case .reminders:
peerId = component.context.account.peerId
default:
peerId = nil
}
guard let peerId else {
return
}
let isSilentPosting = self.isSilentPosting
let _ = (component.context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: peerId))
|> deliverOnMainQueue).start(next: { [weak self] peer in
guard let self, let peer, let controller = self.environment?.controller() else {
return
}
var isChannel = false
if case let .channel(channel) = peer, case .broadcast = channel.info {
isChannel = true
}
//TODO:localize
let text: String
if case .user = peer {
if isSilentPosting {
text = "\(peer.compactDisplayTitle) will receive a silent notification"
} else {
text = "\(peer.compactDisplayTitle) will be notified"
}
} else if isChannel {
if isSilentPosting {
text = "Subscribers will receive a silent notification"
} else {
text = "Subscribers will be notified"
}
} else {
if isSilentPosting {
text = "Members will receive a silent notification"
} else {
text = "Members will be notified"
}
}
let sourceFrame = sourceView.convert(sourceView.bounds, to: nil)
controller.present(TooltipScreen(account: component.context.account, sharedContext: component.context.sharedContext, text: .plain(text: text), style: .default, icon: .none, location: .point(sourceFrame, .bottom), shouldDismissOnTouch: { _, _ in
return .dismiss(consume: false)
}), in: .window(.root))
})
}
func update(component: ChatScheduleTimeSheetContentComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<EnvironmentType>, transition: ComponentTransition) -> CGSize {
self.isUpdating = true
defer {
@ -184,40 +249,6 @@ private final class ChatScheduleTimeSheetContentComponent: Component {
var contentHeight: CGFloat = 0.0
contentHeight += 30.0
let barButtonSize = CGSize(width: 44.0, height: 44.0)
let cancelSize = self.cancel.update(
transition: transition,
component: AnyComponent(
GlassBarButtonComponent(
size: barButtonSize,
backgroundColor: nil,
isDark: environment.theme.overallDarkAppearance,
state: .glass,
component: AnyComponentWithIdentity(id: "close", component: AnyComponent(
BundleIconComponent(
name: "Navigation/Close",
tintColor: environment.theme.chat.inputPanel.panelControlColor
)
)),
action: { [weak self] _ in
guard let self, let component = self.component else {
return
}
component.dismiss()
}
)
),
environment: {},
containerSize: barButtonSize
)
let cancelFrame = CGRect(origin: CGPoint(x: 16.0, y: 16.0), size: cancelSize)
if let cancelView = self.cancel.view {
if cancelView.superview == nil {
self.addSubview(cancelView)
}
transition.setFrame(view: cancelView, frame: cancelFrame)
}
let title: String
switch component.mode {
case .scheduledMessages:
@ -595,7 +626,7 @@ private final class ChatScheduleTimeSheetContentComponent: Component {
transition.setFrame(view: buttonView, frame: buttonFrame)
}
contentHeight += buttonSize.height
} else if case .scheduledMessages(true) = component.mode {
} else if case .scheduledMessages(_, true) = component.mode {
contentHeight += 8.0
let buttonSize = self.secondaryButton.update(
@ -784,6 +815,87 @@ private final class ChatScheduleTimeSheetContentComponent: Component {
component.externalState.repeatValueFrame = repeatValueFrame
let barButtonSize = CGSize(width: 44.0, height: 44.0)
let cancelSize = self.cancel.update(
transition: transition,
component: AnyComponent(
GlassBarButtonComponent(
size: barButtonSize,
backgroundColor: nil,
isDark: environment.theme.overallDarkAppearance,
state: .glass,
component: AnyComponentWithIdentity(id: "close", component: AnyComponent(
BundleIconComponent(
name: "Navigation/Close",
tintColor: environment.theme.chat.inputPanel.panelControlColor
)
)),
action: { [weak self] _ in
guard let self, let component = self.component else {
return
}
component.dismiss()
}
)
),
environment: {},
containerSize: barButtonSize
)
let cancelFrame = CGRect(origin: CGPoint(x: 16.0, y: 16.0), size: cancelSize)
if let cancelView = self.cancel.view {
if cancelView.superview == nil {
self.addSubview(cancelView)
}
transition.setFrame(view: cancelView, frame: cancelFrame)
}
switch component.mode {
case .scheduledMessages, .reminders:
let silentSize = self.silent.update(
transition: transition,
component: AnyComponent(
GlassBarButtonComponent(
size: barButtonSize,
backgroundColor: nil,
isDark: environment.theme.overallDarkAppearance,
state: .glass,
component: AnyComponentWithIdentity(id: "silent", component: AnyComponent(
LottieAnimationComponent(
animation: LottieAnimationComponent.AnimationItem(
name: self.isSilentPosting ? "NavigationMuteOn" : "NavigationMuteOff",
mode: !transition.animation.isImmediate ? .animating(loop: false) : .still(position: .end),
range: nil,
waitForCompletion: false
),
colors: ["__allcolors__": environment.theme.chat.inputPanel.panelControlColor],
size: CGSize(width: 30.0, height: 30.0)
)
)),
action: { [weak self] _ in
guard let self else {
return
}
self.isSilentPosting = !self.isSilentPosting
self.state?.updated(transition: .easeInOut(duration: 0.2))
self.presentSilentPostingTooltip()
}
)
),
environment: {},
containerSize: barButtonSize
)
let silentFrame = CGRect(origin: CGPoint(x: availableSize.width - 16.0 - silentSize.width, y: 16.0), size: silentSize)
if let silentView = self.silent.view {
if silentView.superview == nil {
self.addSubview(silentView)
}
transition.setFrame(view: silentView, frame: silentFrame)
}
default:
break
}
return contentSize
}
}
@ -958,7 +1070,7 @@ private final class ChatScheduleTimeScreenComponent: Component {
public class ChatScheduleTimeScreen: ViewControllerComponentContainer {
public enum Mode: Equatable {
case scheduledMessages(sendWhenOnlineAvailable: Bool)
case scheduledMessages(peerId: EnginePeer.Id, sendWhenOnlineAvailable: Bool)
case reminders
case format
case poll

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

View file

@ -424,6 +424,13 @@ final class InnerTextSelectionTipContainerNode: ASDisplayNode {
self.text = self.presentationData.strings.Camera_CollageReorderingInfo
self.targetSelectionIndex = nil
icon = UIImage(bundleImageName: "Chat/Context Menu/Tip")
case .deleteReaction:
self.action = nil
//TODO:localize
self.text = "Tap and hold to delete reaction."
self.targetSelectionIndex = nil
icon = nil
isUserInteractionEnabled = false
}
self.iconNode = ASImageNode()

View file

@ -93,15 +93,23 @@ public final class ListActionItemComponent: Component {
public enum LeftIcon: Equatable {
public final class Check: Equatable {
public enum Style {
case round
case rectangle
}
public let style: Style
public let isSelected: Bool
public let isEnabled: Bool
public let toggle: (() -> Void)?
public init(
style: Style = .round,
isSelected: Bool,
isEnabled: Bool = true,
toggle: (() -> Void)?
) {
self.style = style
self.isSelected = isSelected
self.isEnabled = isEnabled
self.toggle = toggle
@ -111,6 +119,9 @@ public final class ListActionItemComponent: Component {
if lhs === rhs {
return true
}
if lhs.style != rhs.style {
return false
}
if lhs.isSelected != rhs.isSelected {
return false
}
@ -318,12 +329,12 @@ public final class ListActionItemComponent: Component {
self.action?()
}
func update(size: CGSize, theme: PresentationTheme, isSelected: Bool, transition: ComponentTransition) {
func update(size: CGSize, theme: PresentationTheme, isSelected: Bool, isRectangle: Bool = false, transition: ComponentTransition) {
let checkLayer: CheckLayer
if let current = self.checkLayer {
checkLayer = current
} else {
checkLayer = CheckLayer(theme: CheckNodeTheme(theme: theme, style: .plain), content: .check(isRectangle: false))
checkLayer = CheckLayer(theme: CheckNodeTheme(theme: theme, style: .plain), content: .check(isRectangle: isRectangle))
self.checkLayer = checkLayer
self.layer.addSublayer(checkLayer)
}
@ -625,11 +636,11 @@ public final class ListActionItemComponent: Component {
leftCheckView.frame = CGRect(origin: CGPoint(x: -checkSize.width, y: self.bounds.height == 0.0 ? checkFrame.minY : floor((self.bounds.height - checkSize.height) * 0.5)), size: checkFrame.size)
transition.setPosition(view: leftCheckView, position: checkFrame.center)
transition.setBounds(view: leftCheckView, bounds: CGRect(origin: CGPoint(), size: checkFrame.size))
leftCheckView.update(size: checkFrame.size, theme: component.theme, isSelected: check.isSelected, transition: .immediate)
leftCheckView.update(size: checkFrame.size, theme: component.theme, isSelected: check.isSelected, isRectangle: check.style == .rectangle, transition: .immediate)
} else {
transition.setPosition(view: leftCheckView, position: checkFrame.center)
transition.setBounds(view: leftCheckView, bounds: CGRect(origin: CGPoint(), size: checkFrame.size))
leftCheckView.update(size: checkFrame.size, theme: component.theme, isSelected: check.isSelected, transition: transition)
leftCheckView.update(size: checkFrame.size, theme: component.theme, isSelected: check.isSelected, isRectangle: check.style == .rectangle, transition: transition)
}
case let .custom(customLeftIcon, adjustLeftInset):
var resetLeftIcon = false

View file

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

View file

@ -167,7 +167,11 @@ private final class PeerInfoScreenCommentItemNode: PeerInfoScreenItemNode {
let height = textSize.height + verticalInset * 2.0
transition.updateFrame(node: self.textNode, frame: textFrame)
if self.textNode.frame.size != textFrame.size {
self.textNode.frame = textFrame
} else {
transition.updateFrame(node: self.textNode, frame: textFrame)
}
self.activateArea.frame = CGRect(origin: CGPoint(x: safeInsets.left, y: 0.0), size: CGSize(width: width - safeInsets.left - safeInsets.right, height: height))

View file

@ -375,17 +375,30 @@ func settingsEditingItems(data: PeerInfoScreenData?, state: PeerInfoState, conte
items[.help]!.append(PeerInfoScreenCommentItem(id: ItemNameHelp, text: presentationData.strings.EditProfile_NameAndPhotoOrVideoHelp))
if let cachedData = data.cachedData as? CachedUserData {
items[.bio]!.append(PeerInfoScreenMultilineInputItem(id: ItemBio, text: state.updatingBio ?? (cachedData.about ?? ""), placeholder: presentationData.strings.UserInfo_About_Placeholder, textUpdated: { updatedText in
let currentBio = state.updatingBio ?? (cachedData.about ?? "")
items[.bio]!.append(PeerInfoScreenMultilineInputItem(id: ItemBio, text: currentBio, placeholder: presentationData.strings.UserInfo_About_Placeholder, textUpdated: { updatedText in
interaction.updateBio(updatedText)
}, action: {
interaction.dismissInput()
}, maxLength: Int(data.globalSettings?.userLimits.maxAboutLength ?? 70)))
items[.bio]!.append(PeerInfoScreenCommentItem(id: ItemBioHelp, text: presentationData.strings.Settings_About_PrivacyHelp, linkAction: { _ in
var bioPrivacyInfo = presentationData.strings.Settings_About_PrivacyHelpEmpty
if let bioPrivacy = data.globalSettings?.privacySettings?.bio, !currentBio.isEmpty {
switch bioPrivacy {
case .enableEveryone:
bioPrivacyInfo = presentationData.strings.Settings_About_PrivacyHelpEveryone
case .enableContacts:
bioPrivacyInfo = presentationData.strings.Settings_About_PrivacyHelpContacts
case .disableEveryone:
bioPrivacyInfo = presentationData.strings.Settings_About_PrivacyHelpNobody
}
}
items[.bio]!.append(PeerInfoScreenCommentItem(id: ItemBioHelp, text: bioPrivacyInfo, linkAction: { _ in
interaction.openBioPrivacy()
}))
}
var birthday: TelegramBirthday?
if let updatingBirthDate = state.updatingBirthDate {
birthday = updatingBirthDate
@ -414,14 +427,22 @@ func settingsEditingItems(data: PeerInfoScreenData?, state: PeerInfoState, conte
}))
}
var birthdayIsForContactsOnly = false
if let birthdayPrivacy = data.globalSettings?.privacySettings?.birthday, case .enableContacts = birthdayPrivacy {
birthdayIsForContactsOnly = true
var birthdayPrivacyInfo = ""
if let birthdayPrivacy = data.globalSettings?.privacySettings?.birthday {
switch birthdayPrivacy {
case .enableEveryone:
birthdayPrivacyInfo = presentationData.strings.Settings_Birthday_PrivacyHelpEveryone
case .enableContacts:
birthdayPrivacyInfo = presentationData.strings.Settings_Birthday_PrivacyHelpContacts
case .disableEveryone:
birthdayPrivacyInfo = presentationData.strings.Settings_Birthday_PrivacyHelpNobody
}
}
if !birthdayPrivacyInfo.isEmpty {
items[.birthday]!.append(PeerInfoScreenCommentItem(id: ItemBirthdayHelp, text: birthdayPrivacyInfo, linkAction: { _ in
interaction.openBirthdatePrivacy()
}))
}
items[.birthday]!.append(PeerInfoScreenCommentItem(id: ItemBirthdayHelp, text: birthdayIsForContactsOnly ? presentationData.strings.Settings_Birthday_ContactsHelp : presentationData.strings.Settings_Birthday_Help, linkAction: { _ in
interaction.openBirthdatePrivacy()
}))
if let user = data.peer as? TelegramUser {
items[.info]!.append(PeerInfoScreenDisclosureItem(id: ItemPhoneNumber, label: .text(user.phone.flatMap({ formatPhoneNumber(context: context, number: $0) }) ?? ""), text: presentationData.strings.Settings_PhoneNumber, icon: PresentationResourcesSettings.recentCalls, action: {

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 ContextUI
import ContextControllerImpl
import AutomationBridge
#if canImport(AppCenter)
import AppCenter
@ -1688,6 +1689,13 @@ private func extractAccountManagerState(records: AccountRecordsView<TelegramAcco
}
})
#if DEBUG
AutomationRuntime.shared.setHierarchyProvider {
AutomationPassiveHierarchyBuilder.build()
}
AutomationRuntime.shared.startIfNeeded(stateProvider: nil)
#endif
return true
}

View file

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

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

View file

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

View file

@ -198,7 +198,10 @@ extension ChatControllerImpl {
animationCache: self.controllerInteraction!.presentationContext.animationCache,
animationRenderer: self.controllerInteraction!.presentationContext.animationRenderer,
message: EngineMessage(message),
reaction: value, readStats: nil, back: nil, openPeer: { peer, hasReaction in
reaction: value,
readStats: nil,
back: nil,
openPeer: { peer, hasReaction in
dismissController?({ [weak self] in
guard let self else {
return
@ -206,6 +209,31 @@ extension ChatControllerImpl {
self.openPeer(peer: peer, navigation: .default, fromMessage: MessageReference(message), fromReactionMessageId: hasReaction ? message.id : nil)
})
},
deleteReaction: { [weak self] peer, _ in
dismissController?({ [weak self] in
guard let self, self.chatLocation.peerId?.namespace == Namespaces.Peer.CloudChannel else {
return
}
let _ = (self.context.sharedContext.chatAvailableMessageActions(
engine: self.context.engine,
accountPeerId: self.context.account.peerId,
messageIds: Set([message.id]),
keepUpdated: false
)
|> deliverOnMainQueue).startStandalone(next: { [weak self] actions in
guard let self, !actions.options.isEmpty else {
return
}
self.presentBanMessageOptions(
accountPeerId: self.context.account.peerId,
author: peer._asPeer(),
messageIds: Set([message.id]),
options: actions.options,
reaction: true
)
})
})
}
)))
} else {
@ -297,7 +325,9 @@ extension ChatControllerImpl {
let presentationContext = self.controllerInteraction?.presentationContext
let premiumConfiguration = PremiumConfiguration.with(appConfiguration: context.currentAppConfiguration.with { $0 })
if !packReferences.isEmpty && !premiumConfiguration.isPremiumDisabled {
if "".isEmpty {
items.tip = .deleteReaction
} else if !packReferences.isEmpty && !premiumConfiguration.isPremiumDisabled {
items.tip = .animatedEmoji(text: nil, arguments: nil, file: nil, action: nil)
if packReferences.count > 1 {

View file

@ -92,6 +92,59 @@ func chatShareToSavedMessagesAdditionalView(_ chatController: ChatControllerImpl
}
extension ChatControllerImpl {
// func openMessageShareMenu(id: EngineMessage.Id) {
// guard let messages = self.chatDisplayNode.historyNode.messageGroupInCurrentHistoryView(id), let message = messages.first else {
// return
// }
//
// let chatPresentationInterfaceState = self.presentationInterfaceState
// var warnAboutPrivate = false
// var canShareToStory = false
// if case .peer = chatPresentationInterfaceState.chatLocation, let channel = message.peers[message.id.peerId] as? TelegramChannel {
// if case .broadcast = channel.info {
// canShareToStory = true
// if let message = messages.first, message.media.contains(where: { media in
// if media is TelegramMediaContact || media is TelegramMediaPoll || media is TelegramMediaTodo {
// return true
// } else if let file = media as? TelegramMediaFile, file.isSticker || file.isAnimatedSticker || file.isVideoSticker {
// return true
// } else {
// return false
// }
// }) {
// canShareToStory = false
// }
// if message.text.containsOnlyEmoji {
// canShareToStory = false
// }
// }
// if channel.addressName == nil {
// warnAboutPrivate = true
// }
// }
//
// let _ = warnAboutPrivate
//
//// let shareScreen = self.context.sharedContext.makeShareController(
//// context: self.context,
//// subject: .messages(messages),
//// forceExternal: false,
//// shareStory: canShareToStory ? { [weak self] in
//// guard let self else {
//// return
//// }
//// Queue.mainQueue().after(0.15) {
//// let controller = self.context.sharedContext.makeStorySharingScreen(context: self.context, subject: .messages(messages), parentController: self)
//// self.push(controller)
//// }
//// } : nil,
//// enqueued: nil,
//// actionCompleted: nil
//// )
// self.chatDisplayNode.dismissInput()
// self.push(shareScreen)
// }
func openMessageShareMenu(id: EngineMessage.Id) {
guard let messages = self.chatDisplayNode.historyNode.messageGroupInCurrentHistoryView(id), let message = messages.first else {
return

View file

@ -2189,6 +2189,30 @@ func contextMenuForChatPresentationInterfaceState(chatPresentationInterfaceState
c?.dismiss(completion: {
controllerInteraction.openPeer(peer, .default, MessageReference(message), hasReaction ? .reaction : .default)
})
},
deleteReaction: { [weak c] peer, _ in
c?.dismiss(completion: {
guard let chatController = interfaceInteraction.chatController() as? ChatControllerImpl, chatController.chatLocation.peerId?.namespace == Namespaces.Peer.CloudChannel else {
return
}
let _ = (context.sharedContext.chatAvailableMessageActions(
engine: context.engine,
accountPeerId: context.account.peerId,
messageIds: Set([message.id]),
keepUpdated: false
)
|> deliverOnMainQueue).startStandalone(next: { actions in
guard !actions.options.isEmpty else {
return
}
chatController.presentBanMessageOptions(
accountPeerId: context.account.peerId,
author: peer._asPeer(),
messageIds: Set([message.id]),
options: actions.options
)
})
})
}
)), tip: tip)))
} else {

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
if file.mimeType == "application/pdf" {
subject = .pdfDocument(file: .message(message: MessageReference(params.message), media: file), canShare: canShare)
if file.mimeType.contains("markdown") {
subject = .markdownDocument(file: fileReference, canShare: canShare)
} else if file.mimeType.contains("pdf") {
subject = .pdfDocument(file: fileReference, canShare: canShare)
} else {
subject = .document(file: .message(message: MessageReference(params.message), media: file), canShare: canShare)
subject = .document(file: fileReference, canShare: canShare)
}
let controller = BrowserScreen(context: params.context, subject: subject)
controller.openDocument = { [weak controller] file, canShare in

View file

@ -696,7 +696,7 @@ func openResolvedUrlImpl(
present(shareController, nil)
context.sharedContext.applicationBindings.dismissNativeController()
} else {
let controller = context.sharedContext.makePeerSelectionController(PeerSelectionControllerParams(context: context, filter: [.onlyWriteable, .excludeDisabled], selectForumThreads: true))
let controller = context.sharedContext.makePeerSelectionController(PeerSelectionControllerParams(context: context, filter: [.onlyWriteable, .excludeDisabled], hasFilters: true, selectForumThreads: true))
controller.peerSelected = { peer, threadId in
continueWithPeer(peer.id, threadId)
}

View file

@ -109,7 +109,7 @@ import EntityKeyboard
private final class AccountUserInterfaceInUseContext {
let subscribers = Bag<(Bool) -> Void>()
let tokens = Bag<Void>()
var isEmpty: Bool {
return self.tokens.isEmpty && self.subscribers.isEmpty
}
@ -295,7 +295,7 @@ public final class SharedAccountContextImpl: SharedAccountContext {
init(mainWindow: Window1?, sharedContainerPath: String, basePath: String, encryptionParameters: ValueBoxEncryptionParameters, accountManager: AccountManager<TelegramAccountManagerTypes>, appLockContext: AppLockContext, notificationController: NotificationContainerController?, applicationBindings: TelegramApplicationBindings, initialPresentationDataAndSettings: InitialPresentationDataAndSettings, networkArguments: NetworkInitializationArguments, hasInAppPurchases: Bool, rootPath: String, legacyBasePath: String?, apsNotificationToken: Signal<Data?, NoError>, voipNotificationToken: Signal<Data?, NoError>, firebaseSecretStream: Signal<[String: String], NoError>, setNotificationCall: @escaping (PresentationCall?) -> Void, navigateToChat: @escaping (AccountRecordId, PeerId, MessageId?, Bool) -> Void, displayUpgradeProgress: @escaping (Float?) -> Void = { _ in }, appDelegate: AppDelegate?, testingEnvironment: Bool = false) {
assert(Queue.mainQueue().isCurrent())
precondition(!testHasInstance)
testHasInstance = true

View file

@ -83,6 +83,50 @@ public func chatInputStateStringWithAppliedEntities(_ text: String, entities: [M
private let syntaxHighlighter = Syntaxer()
private func isValidMessageSyntaxHighlightRange(_ range: Range<Int>, expectedLength: Int) -> Bool {
return range.lowerBound >= 0 && range.upperBound <= expectedLength && range.lowerBound < range.upperBound
}
private func validatedCachedMessageSyntaxHighlight(_ highlight: MessageSyntaxHighlight, expectedLength: Int, language: String) -> MessageSyntaxHighlight? {
for entity in highlight.entities {
if !isValidMessageSyntaxHighlightRange(entity.range, expectedLength: expectedLength) {
return nil
}
}
return highlight
}
private func generateMessageSyntaxHighlight(spec: CachedMessageSyntaxHighlight.Spec, theme: SyntaxterTheme) -> MessageSyntaxHighlight {
let expectedLength = (spec.text as NSString).length
guard let syntaxHighlighter else {
return MessageSyntaxHighlight(entities: [])
}
guard let highlightedString = syntaxHighlighter.syntax(spec.text, language: spec.language, theme: theme) else {
return MessageSyntaxHighlight(entities: [])
}
guard highlightedString.length == expectedLength else {
return MessageSyntaxHighlight(entities: [])
}
var entities: [MessageSyntaxHighlight.Entity] = []
var hasInvalidRange = false
highlightedString.enumerateAttribute(.foregroundColor, in: NSRange(location: 0, length: highlightedString.length), using: { value, subRange, stop in
let range = subRange.lowerBound ..< subRange.upperBound
if !isValidMessageSyntaxHighlightRange(range, expectedLength: expectedLength) {
hasInvalidRange = true
stop.pointee = true
return
}
if let value = value as? UIColor, value != .black {
entities.append(MessageSyntaxHighlight.Entity(color: Int32(bitPattern: value.rgb), range: range))
}
})
if hasInvalidRange {
return MessageSyntaxHighlight(entities: [])
}
return MessageSyntaxHighlight(entities: entities)
}
public func stringWithAppliedEntities(_ text: String, entities: [MessageTextEntity], strings: PresentationStrings? = nil, dateTimeFormat: PresentationDateTimeFormat? = nil, baseColor: UIColor, linkColor: UIColor, baseQuoteTintColor: UIColor? = nil, baseQuoteSecondaryTintColor: UIColor? = nil, baseQuoteTertiaryTintColor: UIColor? = nil, codeBlockTitleColor: UIColor? = nil, codeBlockAccentColor: UIColor? = nil, codeBlockBackgroundColor: UIColor? = nil, baseFont: UIFont, linkFont: UIFont, boldFont: UIFont, italicFont: UIFont, boldItalicFont: UIFont, fixedFont: UIFont, blockQuoteFont: UIFont, underlineLinks: Bool = true, external: Bool = false, message: Message?, entityFiles: [MediaId: TelegramMediaFile] = [:], adjustQuoteFontSize: Bool = false, cachedMessageSyntaxHighlight: CachedMessageSyntaxHighlight? = nil, paragraphAlignment: NSTextAlignment? = nil) -> NSAttributedString {
let baseQuoteTintColor = baseQuoteTintColor ?? baseColor
@ -399,8 +443,10 @@ public func stringWithAppliedEntities(_ text: String, entities: [MessageTextEnti
let codeText = (string.string as NSString).substring(with: range)
if let cachedMessageSyntaxHighlight, let entry = cachedMessageSyntaxHighlight.values[CachedMessageSyntaxHighlight.Spec(language: language, text: codeText)] {
for entity in entry.entities {
string.addAttribute(.foregroundColor, value: UIColor(rgb: UInt32(bitPattern: entity.color)), range: NSRange(location: range.location + entity.range.lowerBound, length: entity.range.upperBound - entity.range.lowerBound))
if let validatedEntry = validatedCachedMessageSyntaxHighlight(entry, expectedLength: range.length, language: language) {
for entity in validatedEntry.entities {
string.addAttribute(.foregroundColor, value: UIColor(rgb: UInt32(bitPattern: entity.color)), range: NSRange(location: range.location + entity.range.lowerBound, length: entity.range.upperBound - entity.range.lowerBound))
}
}
}
})
@ -588,8 +634,18 @@ public func extractMessageSyntaxHighlightSpecs(text: String, entities: [MessageT
private let internalFixedCodeFont = Font.regular(17.0)
public func asyncUpdateMessageSyntaxHighlight(engine: TelegramEngine, messageId: EngineMessage.Id, current: CachedMessageSyntaxHighlight?, specs: [CachedMessageSyntaxHighlight.Spec]) -> Signal<Never, NoError> {
if let current, !specs.contains(where: { current.values[$0] == nil }) {
return .complete()
if let current {
var hasMissingOrInvalidSpec = false
for spec in specs {
let expectedLength = (spec.text as NSString).length
guard let value = current.values[spec], validatedCachedMessageSyntaxHighlight(value, expectedLength: expectedLength, language: spec.language) != nil else {
hasMissingOrInvalidSpec = true
break
}
}
if !hasMissingOrInvalidSpec {
return .complete()
}
}
return Signal { subscriber in
@ -598,22 +654,11 @@ public func asyncUpdateMessageSyntaxHighlight(engine: TelegramEngine, messageId:
let theme = SyntaxterTheme(dark: false, textColor: .black, textFont: internalFixedCodeFont, italicFont: internalFixedCodeFont, mediumFont: internalFixedCodeFont)
for spec in specs {
let expectedLength = (spec.text as NSString).length
if let value = current?.values[spec] {
updated[spec] = value
} else {
var entities: [MessageSyntaxHighlight.Entity] = []
if let syntaxHighlighter {
if let highlightedString = syntaxHighlighter.syntax(spec.text, language: spec.language, theme: theme) {
highlightedString.enumerateAttribute(.foregroundColor, in: NSRange(location: 0, length: highlightedString.length), using: { value, subRange, _ in
if let value = value as? UIColor, value != .black {
entities.append(MessageSyntaxHighlight.Entity(color: Int32(bitPattern: value.rgb), range: subRange.lowerBound ..< subRange.upperBound))
}
})
}
}
updated[spec] = MessageSyntaxHighlight(entities: entities)
updated[spec] = validatedCachedMessageSyntaxHighlight(value, expectedLength: expectedLength, language: spec.language) ?? MessageSyntaxHighlight(entities: [])
} else if let theme {
updated[spec] = generateMessageSyntaxHighlight(spec: spec, theme: theme)
}
}
@ -629,8 +674,18 @@ public func asyncUpdateMessageSyntaxHighlight(engine: TelegramEngine, messageId:
}
public func asyncStanaloneSyntaxHighlight(current: CachedMessageSyntaxHighlight?, specs: [CachedMessageSyntaxHighlight.Spec]) -> Signal<CachedMessageSyntaxHighlight, NoError> {
if let current, !specs.contains(where: { current.values[$0] == nil }) {
return .single(current)
if let current {
var hasMissingOrInvalidSpec = false
for spec in specs {
let expectedLength = (spec.text as NSString).length
guard let value = current.values[spec], validatedCachedMessageSyntaxHighlight(value, expectedLength: expectedLength, language: spec.language) != nil else {
hasMissingOrInvalidSpec = true
break
}
}
if !hasMissingOrInvalidSpec {
return .single(current)
}
}
return Signal { subscriber in
@ -639,22 +694,11 @@ public func asyncStanaloneSyntaxHighlight(current: CachedMessageSyntaxHighlight?
let theme = SyntaxterTheme(dark: false, textColor: .black, textFont: internalFixedCodeFont, italicFont: internalFixedCodeFont, mediumFont: internalFixedCodeFont)
for spec in specs {
let expectedLength = (spec.text as NSString).length
if let value = current?.values[spec] {
updated[spec] = value
} else {
var entities: [MessageSyntaxHighlight.Entity] = []
if let syntaxHighlighter {
if let highlightedString = syntaxHighlighter.syntax(spec.text, language: spec.language, theme: theme) {
highlightedString.enumerateAttribute(.foregroundColor, in: NSRange(location: 0, length: highlightedString.length), using: { value, subRange, _ in
if let value = value as? UIColor, value != .black {
entities.append(MessageSyntaxHighlight.Entity(color: Int32(bitPattern: value.rgb), range: subRange.lowerBound ..< subRange.upperBound))
}
})
}
}
updated[spec] = MessageSyntaxHighlight(entities: entities)
updated[spec] = validatedCachedMessageSyntaxHighlight(value, expectedLength: expectedLength, language: spec.language) ?? MessageSyntaxHighlight(entities: [])
} else if let theme {
updated[spec] = generateMessageSyntaxHighlight(spec: spec, theme: theme)
}
}