diff --git a/Telegram/NotificationService/Sources/NotificationService.swift b/Telegram/NotificationService/Sources/NotificationService.swift index c5cc9c8ccf..ff735b74ae 100644 --- a/Telegram/NotificationService/Sources/NotificationService.swift +++ b/Telegram/NotificationService/Sources/NotificationService.swift @@ -326,7 +326,7 @@ private let gradientColors: [NSArray] = [ [UIColor(rgb: 0xd669ed).cgColor, UIColor(rgb: 0xe0a2f3).cgColor], ] -private func avatarViewLettersImage(size: CGSize, peerId: Int64, accountPeerId: Int64, letters: [String]) -> UIImage? { +private func avatarViewLettersImage(size: CGSize, peerId: PeerId, letters: [String]) -> UIImage? { UIGraphicsBeginImageContextWithOptions(size, false, 2.0) let context = UIGraphicsGetCurrentContext() @@ -334,7 +334,12 @@ private func avatarViewLettersImage(size: CGSize, peerId: Int64, accountPeerId: context?.addEllipse(in: CGRect(x: 0.0, y: 0.0, width: size.width, height: size.height)) context?.clip() - let colorIndex = abs(Int(accountPeerId + peerId)) + let colorIndex: Int + if peerId.namespace == .max { + colorIndex = 0 + } else { + colorIndex = abs(Int(clamping: peerId.id._internalGetInt64Value())) + } let colorsArray = gradientColors[colorIndex % gradientColors.count] var locations: [CGFloat] = [1.0, 0.0] @@ -368,11 +373,11 @@ private func avatarViewLettersImage(size: CGSize, peerId: Int64, accountPeerId: return image } -private func avatarImage(path: String?, peerId: Int64, accountPeerId: Int64, letters: [String], size: CGSize) -> UIImage { +private func avatarImage(path: String?, peerId: PeerId, letters: [String], size: CGSize) -> UIImage { if let path = path, let image = UIImage(contentsOfFile: path), let roundImage = avatarRoundImage(size: size, source: image) { return roundImage } else { - return avatarViewLettersImage(size: size, peerId: peerId, accountPeerId: accountPeerId, letters: letters)! + return avatarViewLettersImage(size: size, peerId: peerId, letters: letters)! } } @@ -394,48 +399,27 @@ private func storeTemporaryImage(path: String) -> String { private func peerAvatar(mediaBox: MediaBox, accountPeerId: PeerId, peer: Peer) -> INImage? { if let resource = smallestImageRepresentation(peer.profileImageRepresentations)?.resource, let path = mediaBox.completedResourcePath(resource) { let cachedPath = mediaBox.cachedRepresentationPathForId(resource.id.stringRepresentation, representationId: "intents.png", keepDuration: .shortLived) - if let _ = fileSize(cachedPath), let data = try? Data(contentsOf: URL(fileURLWithPath: cachedPath), options: .alwaysMapped) { - do { - return INImage(url: URL(fileURLWithPath: storeTemporaryImage(path: cachedPath))) - } catch { - return nil - } + if let _ = fileSize(cachedPath) { + return INImage(url: URL(fileURLWithPath: storeTemporaryImage(path: cachedPath))) } else { - let image = avatarImage(path: path, peerId: peer.id.toInt64(), accountPeerId: accountPeerId.toInt64(), letters: peer.displayLetters, size: CGSize(width: 50.0, height: 50.0)) + let image = avatarImage(path: path, peerId: peer.id, letters: peer.displayLetters, size: CGSize(width: 50.0, height: 50.0)) if let data = image.pngData() { let _ = try? data.write(to: URL(fileURLWithPath: cachedPath), options: .atomic) } - do { - //let data = try Data(contentsOf: URL(fileURLWithPath: cachedPath), options: .alwaysMapped) - //return INImage(imageData: data) - return INImage(url: URL(fileURLWithPath: storeTemporaryImage(path: cachedPath))) - } catch { - return nil - } + + return INImage(url: URL(fileURLWithPath: storeTemporaryImage(path: cachedPath))) } } let cachedPath = mediaBox.cachedRepresentationPathForId("lettersAvatar-\(peer.displayLetters.joined(separator: ","))", representationId: "intents.png", keepDuration: .shortLived) if let _ = fileSize(cachedPath) { - do { - //let data = try Data(contentsOf: URL(fileURLWithPath: cachedPath), options: []) - //return INImage(imageData: data) - return INImage(url: URL(fileURLWithPath: storeTemporaryImage(path: cachedPath))) - } catch { - return nil - } + return INImage(url: URL(fileURLWithPath: storeTemporaryImage(path: cachedPath))) } else { - let image = avatarImage(path: nil, peerId: peer.id.toInt64(), accountPeerId: accountPeerId.toInt64(), letters: peer.displayLetters, size: CGSize(width: 50.0, height: 50.0)) + let image = avatarImage(path: nil, peerId: peer.id, letters: peer.displayLetters, size: CGSize(width: 50.0, height: 50.0)) if let data = image.pngData() { let _ = try? data.write(to: URL(fileURLWithPath: cachedPath), options: .atomic) } - do { - //let data = try Data(contentsOf: URL(fileURLWithPath: cachedPath), options: .alwaysMapped) - //return INImage(imageData: data) - return INImage(url: URL(fileURLWithPath: storeTemporaryImage(path: cachedPath))) - } catch { - return nil - } + return INImage(url: URL(fileURLWithPath: storeTemporaryImage(path: cachedPath))) } } diff --git a/submodules/AccountContext/Sources/FetchMediaUtils.swift b/submodules/AccountContext/Sources/FetchMediaUtils.swift index 3e7416c63f..96d91b784d 100644 --- a/submodules/AccountContext/Sources/FetchMediaUtils.swift +++ b/submodules/AccountContext/Sources/FetchMediaUtils.swift @@ -46,8 +46,8 @@ public func messageMediaFileCancelInteractiveFetch(context: AccountContext, mess context.fetchManager.cancelInteractiveFetches(category: fetchCategoryForFile(file), location: .chat(messageId.peerId), locationKey: .messageId(messageId), resource: file.resource) } -public func messageMediaImageInteractiveFetched(context: AccountContext, message: Message, image: TelegramMediaImage, resource: MediaResource, range: Range? = nil, storeToDownloadsPeerType: MediaAutoDownloadPeerType?) -> Signal { - return messageMediaImageInteractiveFetched(fetchManager: context.fetchManager, messageId: message.id, messageReference: MessageReference(message), image: image, resource: resource, range: range, userInitiated: true, priority: .userInitiated, storeToDownloadsPeerType: storeToDownloadsPeerType) +public func messageMediaImageInteractiveFetched(context: AccountContext, message: Message, image: TelegramMediaImage, resource: MediaResource, range: Range? = nil, userInitiated: Bool = true, storeToDownloadsPeerType: MediaAutoDownloadPeerType?) -> Signal { + return messageMediaImageInteractiveFetched(fetchManager: context.fetchManager, messageId: message.id, messageReference: MessageReference(message), image: image, resource: resource, range: range, userInitiated: userInitiated, priority: .userInitiated, storeToDownloadsPeerType: storeToDownloadsPeerType) } public func messageMediaImageInteractiveFetched(fetchManager: FetchManager, messageId: MessageId, messageReference: MessageReference, image: TelegramMediaImage, resource: MediaResource, range: Range? = nil, userInitiated: Bool, priority: FetchManagerPriority, storeToDownloadsPeerType: MediaAutoDownloadPeerType?) -> Signal { diff --git a/submodules/CalendarMessageScreen/Sources/CalendarMessageScreen.swift b/submodules/CalendarMessageScreen/Sources/CalendarMessageScreen.swift index 22b2c9fb59..467627a1bc 100644 --- a/submodules/CalendarMessageScreen/Sources/CalendarMessageScreen.swift +++ b/submodules/CalendarMessageScreen/Sources/CalendarMessageScreen.swift @@ -268,6 +268,8 @@ private final class DayComponent: Component { private var action: (() -> Void)? private var currentMedia: DayMedia? + private(set) var index: MessageIndex? + init() { self.button = HighlightableButton() self.highlightView = UIImageView() @@ -295,6 +297,7 @@ private final class DayComponent: Component { func update(component: DayComponent, availableSize: CGSize, environment: Environment, transition: Transition) -> CGSize { self.action = component.action + self.index = component.media?.message.index let diameter = min(availableSize.width, availableSize.height) let contentFrame = CGRect(origin: CGPoint(x: floor((availableSize.width - diameter) / 2.0), y: floor((availableSize.height - diameter) / 2.0)), size: CGSize(width: diameter, height: diameter)) @@ -601,16 +604,18 @@ public final class CalendarMessageScreen: ViewController { private final class Node: ViewControllerTracingNode, UIScrollViewDelegate { private let context: AccountContext private let peerId: PeerId + private let initialTimestamp: Int32 private let navigateToDay: (Int32) -> Void + private let previewDay: (MessageIndex, ASDisplayNode, CGRect, ContextGesture) -> Void private var presentationData: PresentationData private var scrollView: Scroller private let calendarSource: SparseMessageCalendar - private var initialMonthIndex: Int = 0 private var months: [MonthModel] = [] private var monthViews: [Int: ComponentHostView] = [:] + private let contextGestureContainerNode: ContextControllerSourceNode private let imageCache = ImageCache() @@ -622,14 +627,20 @@ public final class CalendarMessageScreen: ViewController { private var isLoadingMoreDisposable: Disposable? private var stateDisposable: Disposable? - init(context: AccountContext, peerId: PeerId, calendarSource: SparseMessageCalendar, initialTimestamp: Int32, navigateToDay: @escaping (Int32) -> Void) { + private weak var currentGestureDayView: DayComponent.View? + + init(context: AccountContext, peerId: PeerId, calendarSource: SparseMessageCalendar, initialTimestamp: Int32, navigateToDay: @escaping (Int32) -> Void, previewDay: @escaping (MessageIndex, ASDisplayNode, CGRect, ContextGesture) -> Void) { self.context = context self.peerId = peerId + self.initialTimestamp = initialTimestamp self.calendarSource = calendarSource self.navigateToDay = navigateToDay + self.previewDay = previewDay self.presentationData = context.sharedContext.currentPresentationData.with { $0 } + self.contextGestureContainerNode = ContextControllerSourceNode() + self.scrollView = Scroller() self.scrollView.showsVerticalScrollIndicator = true self.scrollView.showsHorizontalScrollIndicator = false @@ -644,6 +655,75 @@ public final class CalendarMessageScreen: ViewController { super.init() + self.contextGestureContainerNode.shouldBegin = { [weak self] point in + guard let strongSelf = self else { + return false + } + + guard let result = strongSelf.contextGestureContainerNode.view.hitTest(point, with: nil) as? HighlightableButton else { + return false + } + + guard let dayView = result.superview as? DayComponent.View else { + return false + } + + strongSelf.currentGestureDayView = dayView + + return true + } + + self.contextGestureContainerNode.customActivationProgress = { [weak self] progress, update in + guard let strongSelf = self, let currentGestureDayView = strongSelf.currentGestureDayView else { + return + } + let itemLayer = currentGestureDayView.layer + + let targetContentRect = CGRect(origin: CGPoint(), size: itemLayer.bounds.size) + + let scaleSide = itemLayer.bounds.width + let minScale: CGFloat = max(0.7, (scaleSide - 15.0) / scaleSide) + let currentScale = 1.0 * (1.0 - progress) + minScale * progress + + let originalCenterOffsetX: CGFloat = itemLayer.bounds.width / 2.0 - targetContentRect.midX + let scaledCenterOffsetX: CGFloat = originalCenterOffsetX * currentScale + + let originalCenterOffsetY: CGFloat = itemLayer.bounds.height / 2.0 - targetContentRect.midY + let scaledCenterOffsetY: CGFloat = originalCenterOffsetY * currentScale + + let scaleMidX: CGFloat = scaledCenterOffsetX - originalCenterOffsetX + let scaleMidY: CGFloat = scaledCenterOffsetY - originalCenterOffsetY + + switch update { + case .update: + let sublayerTransform = CATransform3DTranslate(CATransform3DScale(CATransform3DIdentity, currentScale, currentScale, 1.0), scaleMidX, scaleMidY, 0.0) + itemLayer.sublayerTransform = sublayerTransform + case .begin: + let sublayerTransform = CATransform3DTranslate(CATransform3DScale(CATransform3DIdentity, currentScale, currentScale, 1.0), scaleMidX, scaleMidY, 0.0) + itemLayer.sublayerTransform = sublayerTransform + case .ended: + let sublayerTransform = CATransform3DTranslate(CATransform3DScale(CATransform3DIdentity, currentScale, currentScale, 1.0), scaleMidX, scaleMidY, 0.0) + let previousTransform = itemLayer.sublayerTransform + itemLayer.sublayerTransform = sublayerTransform + + itemLayer.animate(from: NSValue(caTransform3D: previousTransform), to: NSValue(caTransform3D: sublayerTransform), keyPath: "sublayerTransform", timingFunction: CAMediaTimingFunctionName.easeOut.rawValue, duration: 0.2) + } + } + + self.contextGestureContainerNode.activated = { [weak self] gesture, _ in + guard let strongSelf = self, let currentGestureDayView = strongSelf.currentGestureDayView else { + return + } + strongSelf.currentGestureDayView = nil + + currentGestureDayView.isUserInteractionEnabled = false + currentGestureDayView.isUserInteractionEnabled = true + + if let index = currentGestureDayView.index { + strongSelf.previewDay(index, strongSelf, currentGestureDayView.convert(currentGestureDayView.bounds, to: strongSelf.view), gesture) + } + } + let calendar = Calendar(identifier: .gregorian) let baseDate = Date() @@ -651,8 +731,6 @@ public final class CalendarMessageScreen: ViewController { let currentMonth = calendar.component(.month, from: baseDate) let currentDayOfMonth = calendar.component(.day, from: baseDate) - let initialDate = Date(timeIntervalSince1970: TimeInterval(initialTimestamp)) - for i in 0 ..< 12 * 20 { guard let firstDayOfMonth = calendar.date(from: calendar.dateComponents([.year, .month], from: baseDate)) else { break @@ -660,10 +738,18 @@ public final class CalendarMessageScreen: ViewController { guard let monthBaseDate = calendar.date(byAdding: .month, value: -i, to: firstDayOfMonth) else { break } + guard let monthModel = monthMetadata(calendar: calendar, for: monthBaseDate, currentYear: currentYear, currentMonth: currentMonth, currentDayOfMonth: currentDayOfMonth) else { break } + let firstDayTimestamp = Int32(monthModel.firstDay.timeIntervalSince1970) + let lastDayTimestamp = firstDayTimestamp + 24 * 60 * 60 * Int32(monthModel.numberOfDays) + + if let minTimestamp = calendarSource.minTimestamp, minTimestamp > lastDayTimestamp { + break + } + if monthModel.year < 2013 { break } @@ -676,19 +762,11 @@ public final class CalendarMessageScreen: ViewController { self.months.append(monthModel) } - if self.months.count > 1 { - for i in 0 ..< self.months.count - 1 { - if initialDate >= self.months[i].firstDay { - self.initialMonthIndex = i - break - } - } - } - self.backgroundColor = self.presentationData.theme.list.plainBackgroundColor self.scrollView.delegate = self - self.view.addSubview(self.scrollView) + self.addSubnode(self.contextGestureContainerNode) + self.contextGestureContainerNode.view.addSubview(self.scrollView) self.isLoadingMoreDisposable = (self.calendarSource.isLoadingMore |> distinctUntilChanged @@ -722,20 +800,38 @@ public final class CalendarMessageScreen: ViewController { if self.updateScrollLayoutIfNeeded() { } - if isFirstLayout, let frame = self.scrollLayout?.frames[self.initialMonthIndex] { - var contentOffset = floor(frame.midY - self.scrollView.bounds.height / 2.0) - if contentOffset < 0 { - contentOffset = 0 + if isFirstLayout { + let initialDate = Date(timeIntervalSince1970: TimeInterval(self.initialTimestamp)) + var initialMonthIndex: Int? + + if self.months.count > 1 { + for i in 0 ..< self.months.count - 1 { + if initialDate >= self.months[i].firstDay { + initialMonthIndex = i + break + } + } } - if contentOffset > self.scrollView.contentSize.height - self.scrollView.bounds.height { - contentOffset = self.scrollView.contentSize.height - self.scrollView.bounds.height + + if isFirstLayout, let initialMonthIndex = initialMonthIndex, let frame = self.scrollLayout?.frames[initialMonthIndex] { + var contentOffset = floor(frame.midY - self.scrollView.bounds.height / 2.0) + if contentOffset < 0 { + contentOffset = 0 + } + if contentOffset > self.scrollView.contentSize.height - self.scrollView.bounds.height { + contentOffset = self.scrollView.contentSize.height - self.scrollView.bounds.height + } + self.scrollView.setContentOffset(CGPoint(x: 0.0, y: contentOffset), animated: false) } - self.scrollView.setContentOffset(CGPoint(x: 0.0, y: contentOffset), animated: false) } updateMonthViews() } + func scrollViewWillBeginDragging(_ scrollView: UIScrollView) { + self.contextGestureContainerNode.cancelGesture() + } + func scrollViewDidScroll(_ scrollView: UIScrollView) { if let indicator = scrollView.value(forKey: "_verticalScrollIndicator") as? UIView { indicator.transform = CGAffineTransform(scaleX: -1.0, y: 1.0) @@ -784,7 +880,8 @@ public final class CalendarMessageScreen: ViewController { self.scrollLayout = (layout.size.width, contentHeight, frames) - self.scrollView.frame = CGRect(origin: CGPoint(x: 0.0, y: navigationHeight), size: CGSize(width: layout.size.width, height: layout.size.height - navigationHeight)) + self.contextGestureContainerNode.frame = CGRect(origin: CGPoint(x: 0.0, y: navigationHeight), size: CGSize(width: layout.size.width, height: layout.size.height - navigationHeight)) + self.scrollView.frame = CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: layout.size.width, height: layout.size.height - navigationHeight)) self.scrollView.contentSize = CGSize(width: layout.size.width, height: contentHeight) self.scrollView.scrollIndicatorInsets = UIEdgeInsets(top: layout.intrinsicInsets.bottom, left: 0.0, bottom: 0.0, right: layout.size.width - 3.0 - 6.0) @@ -861,19 +958,11 @@ public final class CalendarMessageScreen: ViewController { messageMap.append(message) } - let _ = messageMap - var updatedMedia: [Int: [Int: DayMedia]] = [:] - var removeMonths: [Int] = [] for i in 0 ..< self.months.count { - let firstDayTimestamp = Int32(self.months[i].firstDay.timeIntervalSince1970) - let lastDayTimestamp = firstDayTimestamp + 24 * 60 * 60 * Int32(self.months[i].numberOfDays) - - if let minTimestamp = calendarState.minTimestamp, minTimestamp > lastDayTimestamp { - removeMonths.append(i) - } - for day in 0 ..< self.months[i].numberOfDays { + let firstDayTimestamp = Int32(self.months[i].firstDay.timeIntervalSince1970) + let dayTimestamp = firstDayTimestamp + 24 * 60 * 60 * Int32(day) let nextDayTimestamp = firstDayTimestamp + 24 * 60 * 60 * Int32(day - 1) @@ -901,57 +990,7 @@ public final class CalendarMessageScreen: ViewController { self.months[monthIndex].mediaByDay = mediaByDay } - for i in removeMonths.reversed() { - self.months.remove(at: i) - } - - if !removeMonths.isEmpty { - self.scrollLayout = nil - let _ = self.updateScrollLayoutIfNeeded() - } - self.updateMonthViews() - - /*let peerId = self.peerId - let months = self.months - let _ = (self.context.account.postbox.transaction { transaction -> [Int: [Int: DayMedia]] in - var updatedMedia: [Int: [Int: DayMedia]] = [:] - - for i in 0 ..< months.count { - for day in 0 ..< months[i].numberOfDays { - let dayTimestamp = Int32(months[i].firstDay.timeIntervalSince1970) + 24 * 60 * 60 * Int32(day) - let nextDayTimestamp = Int32(months[i].firstDay.timeIntervalSince1970) + 24 * 60 * 60 * Int32(day - 1) - if let message = transaction.firstMessageInRange(peerId: peerId, namespace: Namespaces.Message.Cloud, tag: .photoOrVideo, timestampMax: dayTimestamp, timestampMin: nextDayTimestamp - 1) { - /*if message.timestamp < nextDayTimestamp { - continue - }*/ - if updatedMedia[i] == nil { - updatedMedia[i] = [:] - } - mediaLoop: for media in message.media { - switch media { - case _ as TelegramMediaImage, _ as TelegramMediaFile: - updatedMedia[i]![day] = DayMedia(message: EngineMessage(message), media: EngineMedia(media)) - break mediaLoop - default: - break - } - } - } - } - } - - return updatedMedia - } - |> deliverOnMainQueue).start(next: { [weak self] updatedMedia in - guard let strongSelf = self else { - return - } - for (monthIndex, mediaByDay) in updatedMedia { - strongSelf.months[monthIndex].mediaByDay = mediaByDay - } - strongSelf.updateMonthViews() - })*/ } } @@ -964,13 +1003,15 @@ public final class CalendarMessageScreen: ViewController { private let calendarSource: SparseMessageCalendar private let initialTimestamp: Int32 private let navigateToDay: (CalendarMessageScreen, Int32) -> Void + private let previewDay: (MessageIndex, ASDisplayNode, CGRect, ContextGesture) -> Void - public init(context: AccountContext, peerId: PeerId, calendarSource: SparseMessageCalendar, initialTimestamp: Int32, navigateToDay: @escaping (CalendarMessageScreen, Int32) -> Void) { + public init(context: AccountContext, peerId: PeerId, calendarSource: SparseMessageCalendar, initialTimestamp: Int32, navigateToDay: @escaping (CalendarMessageScreen, Int32) -> Void, previewDay: @escaping (MessageIndex, ASDisplayNode, CGRect, ContextGesture) -> Void) { self.context = context self.peerId = peerId self.calendarSource = calendarSource self.initialTimestamp = initialTimestamp self.navigateToDay = navigateToDay + self.previewDay = previewDay let presentationData = self.context.sharedContext.currentPresentationData.with { $0 } @@ -997,7 +1038,7 @@ public final class CalendarMessageScreen: ViewController { return } strongSelf.navigateToDay(strongSelf, timestamp) - }) + }, previewDay: self.previewDay) self.displayNodeDidLoad() } diff --git a/submodules/LegacyMediaPickerUI/Sources/LegacyAttachmentMenu.swift b/submodules/LegacyMediaPickerUI/Sources/LegacyAttachmentMenu.swift index d0004cc945..ad3ee8953d 100644 --- a/submodules/LegacyMediaPickerUI/Sources/LegacyAttachmentMenu.swift +++ b/submodules/LegacyMediaPickerUI/Sources/LegacyAttachmentMenu.swift @@ -229,7 +229,7 @@ public func legacyAttachmentMenu(context: AccountContext, peer: Peer, chatLocati let intent: TGMediaAssetsControllerIntent = asFiles ? TGMediaAssetsControllerSendFileIntent : TGMediaAssetsControllerSendMediaIntent var hasHeic = false - var allItems = carouselItem.selectionContext.selectedItems() ?? [] + var allItems = carouselItem.selectionContext?.selectedItems() ?? [] if let currentItem = currentItem { allItems.append(currentItem) } diff --git a/submodules/PhotoResources/Sources/PhotoResources.swift b/submodules/PhotoResources/Sources/PhotoResources.swift index 14c832524b..35af7162d1 100644 --- a/submodules/PhotoResources/Sources/PhotoResources.swift +++ b/submodules/PhotoResources/Sources/PhotoResources.swift @@ -1699,13 +1699,41 @@ public func standaloneChatMessagePhotoInteractiveFetched(account: Account, photo } } -public func chatMessagePhotoInteractiveFetched(context: AccountContext, photoReference: ImageMediaReference, displayAtSize: Int?, storeToDownloadsPeerType: MediaAutoDownloadPeerType?) -> Signal { +public func chatMessagePhotoInteractiveFetched(context: AccountContext, photoReference: ImageMediaReference, displayAtSize: Int?, storeToDownloadsPeerType: MediaAutoDownloadPeerType?) -> Signal { if let largestRepresentation = largestRepresentationForPhoto(photoReference.media) { var fetchRange: (Range, MediaBoxFetchPriority)? if let displayAtSize = displayAtSize, let range = representationFetchRangeForDisplayAtSize(representation: largestRepresentation, dimension: displayAtSize) { fetchRange = (range, .default) } - + + /*switch photoReference { + case let .message(message, _): + if let id = message.id { + let ranges: IndexSet + if let (range, _) = fetchRange { + ranges = IndexSet(integersIn: range) + } else { + ranges = IndexSet(integersIn: 0 ..< Int(Int32.max) as Range) + } + return context.fetchManager.interactivelyFetched( + category: .image, + location: .chat(id.peerId), + locationKey: .messageId(id), + mediaReference: photoReference.abstract, + resourceReference: photoReference.resourceReference(largestRepresentation.resource), + ranges: ranges, + statsCategory: .image, + elevatedPriority: false, + userInitiated: false, + priority: .userInitiated, + storeToDownloadsPeerType: nil + ) + |> ignoreValues + } + default: + break + }*/ + return fetchedMediaResource(mediaBox: context.account.postbox.mediaBox, reference: photoReference.resourceReference(largestRepresentation.resource), range: fetchRange, statsCategory: .image, reportResultStatus: true) |> mapToSignal { type -> Signal in if case .remote = type, let peerType = storeToDownloadsPeerType { @@ -1717,6 +1745,10 @@ public func chatMessagePhotoInteractiveFetched(context: AccountContext, photoRef } return .single(type) } + |> ignoreValues + |> `catch` { _ -> Signal in + return .complete() + } } else { return .never() } diff --git a/submodules/Postbox/Sources/DeletedMessagesView.swift b/submodules/Postbox/Sources/DeletedMessagesView.swift new file mode 100644 index 0000000000..95c8e2d528 --- /dev/null +++ b/submodules/Postbox/Sources/DeletedMessagesView.swift @@ -0,0 +1,47 @@ +import Foundation + +final class MutableDeletedMessagesView: MutablePostboxView { + let peerId: PeerId + var currentDeletedMessages: [MessageId] = [] + + init(peerId: PeerId) { + self.peerId = peerId + } + + func replay(postbox: PostboxImpl, transaction: PostboxTransaction) -> Bool { + var updated = false + if let operations = transaction.currentOperationsByPeerId[self.peerId] { + var testMessageIds: [MessageId] = [] + for operation in operations { + switch operation { + case let .Remove(indices): + for (index, _) in indices { + testMessageIds.append(index.id) + } + default: + break + } + } + self.currentDeletedMessages.removeAll() + for id in testMessageIds { + if !postbox.messageHistoryIndexTable.exists(id) { + self.currentDeletedMessages.append(id) + updated = true + } + } + } + return updated + } + + func immutableView() -> PostboxView { + return DeletedMessagesView(self) + } +} + +public final class DeletedMessagesView: PostboxView { + public let currentDeletedMessages: [MessageId] + + init(_ view: MutableDeletedMessagesView) { + self.currentDeletedMessages = view.currentDeletedMessages + } +} diff --git a/submodules/Postbox/Sources/Views.swift b/submodules/Postbox/Sources/Views.swift index 3f47094ab6..9b7dbea1e9 100644 --- a/submodules/Postbox/Sources/Views.swift +++ b/submodules/Postbox/Sources/Views.swift @@ -32,6 +32,7 @@ public enum PostboxViewKey: Hashable { case historyTagInfo(peerId: PeerId, tag: MessageTags) case topChatMessage(peerIds: [PeerId]) case contacts(accountPeerId: PeerId?, includePresences: Bool) + case deletedMessages(peerId: PeerId) public func hash(into hasher: inout Hasher) { switch self { @@ -106,6 +107,8 @@ public enum PostboxViewKey: Hashable { hasher.combine(peerIds) case .contacts: hasher.combine(16) + case let .deletedMessages(peerId): + hasher.combine(peerId) } } @@ -297,6 +300,12 @@ public enum PostboxViewKey: Hashable { } else { return false } + case let .deletedMessages(peerId): + if case .deletedMessages(peerId) = rhs { + return true + } else { + return false + } } } } @@ -365,5 +374,7 @@ func postboxViewForKey(postbox: PostboxImpl, key: PostboxViewKey) -> MutablePost return MutableTopChatMessageView(postbox: postbox, peerIds: Set(peerIds)) case let .contacts(accountPeerId, includePresences): return MutableContactPeersView(postbox: postbox, accountPeerId: accountPeerId, includePresences: includePresences) + case let .deletedMessages(peerId): + return MutableDeletedMessagesView(peerId: peerId) } } diff --git a/submodules/SparseItemGrid/BUILD b/submodules/SparseItemGrid/BUILD index b7d80e82a0..62bcc3923f 100644 --- a/submodules/SparseItemGrid/BUILD +++ b/submodules/SparseItemGrid/BUILD @@ -15,6 +15,7 @@ swift_library( "//submodules/SSignalKit/SwiftSignalKit:SwiftSignalKit", "//submodules/ComponentFlow:ComponentFlow", "//submodules/AnimationUI:AnimationUI", + "//submodules/TelegramPresentationData:TelegramPresentationData", ], visibility = [ "//visibility:public", diff --git a/submodules/SparseItemGrid/Sources/SparseItemGrid.swift b/submodules/SparseItemGrid/Sources/SparseItemGrid.swift index 64349311b9..fe99a9197f 100644 --- a/submodules/SparseItemGrid/Sources/SparseItemGrid.swift +++ b/submodules/SparseItemGrid/Sources/SparseItemGrid.swift @@ -3,6 +3,7 @@ import UIKit import Display import AsyncDisplayKit import SwiftSignalKit +import TelegramPresentationData private final class NullActionClass: NSObject, CAAction { @objc func run(forKey event: String, object anObject: Any, arguments dict: [AnyHashable : Any]?) { @@ -129,8 +130,7 @@ private final class Shimmer { if let image = self.image { layer.contents = image.cgImage - let shiftedContentsRect = CGRect(origin: CGPoint(x: frame.minX / containerSize.width, y: frame.minY / containerSize.height), size: CGSize(width: frame.width / containerSize.width, height: frame.height / containerSize.height)) - let _ = shiftedContentsRect + let shiftedContentsRect = CGRect(origin: CGPoint(x: 0.0, y: frame.minY / containerSize.height), size: CGSize(width: 1.0, height: frame.height / containerSize.height)) layer.contentsRect = shiftedContentsRect if layer.animation(forKey: "shimmer") == nil { @@ -140,7 +140,7 @@ private final class Shimmer { animation.isAdditive = true animation.repeatCount = .infinity animation.duration = 0.8 - animation.beginTime = 1.0 + animation.beginTime = layer.convertTime(1.0, from: nil) layer.add(animation, forKey: "shimmer") } } @@ -429,6 +429,8 @@ public final class SparseItemGrid: ASDisplayNode { private let scrollView: UIScrollView private let shimmer: Shimmer + var theme: PresentationTheme + var layout: Layout? var items: Items? var visibleItems: [AnyHashable: VisibleItem] = [:] @@ -446,7 +448,8 @@ public final class SparseItemGrid: ASDisplayNode { let coveringOffsetUpdated: (Viewport, ContainedViewLayoutTransition) -> Void - init(zoomLevel: ZoomLevel, maybeLoadHoleAnchor: @escaping (HoleAnchor, HoleLocation) -> Void, coveringOffsetUpdated: @escaping (Viewport, ContainedViewLayoutTransition) -> Void) { + init(theme: PresentationTheme, zoomLevel: ZoomLevel, maybeLoadHoleAnchor: @escaping (HoleAnchor, HoleLocation) -> Void, coveringOffsetUpdated: @escaping (Viewport, ContainedViewLayoutTransition) -> Void) { + self.theme = theme self.zoomLevel = zoomLevel self.maybeLoadHoleAnchor = maybeLoadHoleAnchor self.coveringOffsetUpdated = coveringOffsetUpdated @@ -596,6 +599,23 @@ public final class SparseItemGrid: ASDisplayNode { return nil } + func visualItem(at index: Int) -> SparseItemGridDisplayItem? { + guard let items = self.items, !items.items.isEmpty else { + return nil + } + + guard let item = items.item(at: index) else { + return nil + } + for (id, visibleItem) in self.visibleItems { + if id == item.id { + return visibleItem + } + } + + return nil + } + func item(at point: CGPoint) -> Item? { guard let items = self.items, !items.items.isEmpty else { return nil @@ -749,6 +769,8 @@ public final class SparseItemGrid: ASDisplayNode { let visibleRange = layout.visibleItemRange(for: visibleBounds, count: items.count) for index in visibleRange.minIndex ... visibleRange.maxIndex { if let item = items.item(at: index) { + let itemFrame = layout.frame(at: index) + let itemLayer: VisibleItem if let current = self.visibleItems[item.id] { itemLayer = current @@ -777,7 +799,6 @@ public final class SparseItemGrid: ASDisplayNode { itemLayer.shimmerLayer = placeholderLayer } - let itemFrame = layout.frame(at: index) placeholderLayer.frame = itemFrame self.shimmer.update(colors: shimmerColors, layer: placeholderLayer, containerSize: layout.containerLayout.size, frame: itemFrame.offsetBy(dx: 0.0, dy: -visibleBounds.minY)) placeholderLayer.update(size: itemFrame.size) @@ -788,7 +809,7 @@ public final class SparseItemGrid: ASDisplayNode { validIds.insert(item.id) - itemLayer.frame = layout.frame(at: index) + itemLayer.frame = itemFrame } else { let placeholderLayer: SparseItemGridShimmerLayer if self.visiblePlaceholders.count > usedPlaceholderCount { @@ -834,6 +855,7 @@ public final class SparseItemGrid: ASDisplayNode { } else if let view = item.view { view.removeFromSuperview() } + item.shimmerLayer?.removeFromSuperlayer() } } @@ -931,6 +953,7 @@ public final class SparseItemGrid: ASDisplayNode { contentOffset: self.scrollView.bounds.minY, isScrolling: self.scrollView.isDragging || self.scrollView.isDecelerating, dateString: dateString ?? "", + theme: self.theme, transition: .immediate ) } @@ -1058,6 +1081,7 @@ public final class SparseItemGrid: ASDisplayNode { private var tapRecognizer: UITapGestureRecognizer? private var pinchRecognizer: UIPinchGestureRecognizer? + private var theme: PresentationTheme private var containerLayout: ContainerLayout? private var items: Items? @@ -1082,7 +1106,9 @@ public final class SparseItemGrid: ASDisplayNode { public var cancelExternalContentGestures: (() -> Void)? - override public init() { + public init(theme: PresentationTheme) { + self.theme = theme + self.scrollingArea = SparseItemGridScrollingArea() super.init() @@ -1135,7 +1161,7 @@ public final class SparseItemGrid: ASDisplayNode { let progress = (scale - interactiveState.initialScale) / (interactiveState.targetScale - interactiveState.initialScale) var replacedTransition = false - //print("progress: \(progress), scale: \(scale), initial: \(interactiveState.initialScale), target: \(interactiveState.targetScale)") + if progress < 0.0 || progress > 1.0 { let boundaryViewport = progress > 1.0 ? currentViewportTransition.toViewport : currentViewportTransition.fromViewport let zoomLevels = self.availableZoomLevels(width: containerLayout.size.width, startingAt: boundaryViewport.zoomLevel) @@ -1176,7 +1202,7 @@ public final class SparseItemGrid: ASDisplayNode { let restoreScrollPosition: (y: CGFloat, index: Int)? = (anchorItemFrame.minY, nextAnchorItemIndex) - let nextViewport = Viewport(zoomLevel: nextZoomLevel, maybeLoadHoleAnchor: { [weak self] holeAnchor, location in + let nextViewport = Viewport(theme: self.theme, zoomLevel: nextZoomLevel, maybeLoadHoleAnchor: { [weak self] holeAnchor, location in guard let strongSelf = self else { return } @@ -1226,7 +1252,7 @@ public final class SparseItemGrid: ASDisplayNode { let restoreScrollPosition: (y: CGFloat, index: Int)? = (anchorItemFrame.minY, anchorItem.index) let anchorItemIndex = anchorItem.index - let nextViewport = Viewport(zoomLevel: nextZoomLevel, maybeLoadHoleAnchor: { [weak self] holeAnchor, location in + let nextViewport = Viewport(theme: self.theme, zoomLevel: nextZoomLevel, maybeLoadHoleAnchor: { [weak self] holeAnchor, location in guard let strongSelf = self else { return } @@ -1285,7 +1311,8 @@ public final class SparseItemGrid: ASDisplayNode { } } - public func update(size: CGSize, insets: UIEdgeInsets, scrollIndicatorInsets: UIEdgeInsets, lockScrollingAtTop: Bool, fixedItemHeight: CGFloat?, items: Items, synchronous: Bool) { + public func update(size: CGSize, insets: UIEdgeInsets, scrollIndicatorInsets: UIEdgeInsets, lockScrollingAtTop: Bool, fixedItemHeight: CGFloat?, items: Items, theme: PresentationTheme, synchronous: Bool) { + self.theme = theme let containerLayout = ContainerLayout(size: size, insets: insets, scrollIndicatorInsets: scrollIndicatorInsets, lockScrollingAtTop: lockScrollingAtTop, fixedItemHeight: fixedItemHeight) self.containerLayout = containerLayout self.items = items @@ -1295,7 +1322,7 @@ public final class SparseItemGrid: ASDisplayNode { self.pinchRecognizer?.isEnabled = fixedItemHeight == nil && !lockScrollingAtTop if self.currentViewport == nil { - let currentViewport = Viewport(zoomLevel: self.initialZoomLevel ?? ZoomLevel(rawValue: 3), maybeLoadHoleAnchor: { [weak self] holeAnchor, location in + let currentViewport = Viewport(theme: self.theme, zoomLevel: self.initialZoomLevel ?? ZoomLevel(rawValue: 3), maybeLoadHoleAnchor: { [weak self] holeAnchor, location in guard let strongSelf = self else { return } @@ -1377,7 +1404,7 @@ public final class SparseItemGrid: ASDisplayNode { self.currentViewport = nil previousViewport.removeFromSupernode() - let currentViewport = Viewport(zoomLevel: level, maybeLoadHoleAnchor: { [weak self] holeAnchor, location in + let currentViewport = Viewport(theme: self.theme, zoomLevel: level, maybeLoadHoleAnchor: { [weak self] holeAnchor, location in guard let strongSelf = self else { return } @@ -1468,6 +1495,13 @@ public final class SparseItemGrid: ASDisplayNode { return currentViewport.visualItem(at: point) } + public func item(at index: Int) -> SparseItemGridDisplayItem? { + guard let currentViewport = self.currentViewport else { + return nil + } + return currentViewport.visualItem(at: index) + } + public func scrollToItem(at index: Int) { guard let currentViewport = self.currentViewport else { return diff --git a/submodules/SparseItemGrid/Sources/SparseItemGridScrollingArea.swift b/submodules/SparseItemGrid/Sources/SparseItemGridScrollingArea.swift index 6b41c8a938..139eea45b5 100644 --- a/submodules/SparseItemGrid/Sources/SparseItemGridScrollingArea.swift +++ b/submodules/SparseItemGrid/Sources/SparseItemGridScrollingArea.swift @@ -5,6 +5,7 @@ import AsyncDisplayKit import ComponentFlow import SwiftSignalKit import AnimationUI +import TelegramPresentationData public final class MultilineText: Component { public let text: String @@ -474,7 +475,7 @@ private final class SparseItemGridScrollingIndicatorComponent: CombinedComponent component: Text( text: context.component.dateString, font: Font.medium(13.0), - color: .black + color: context.component.foregroundColor ), availableSize: CGSize(width: 200.0, height: 100.0), transition: .immediate @@ -482,7 +483,7 @@ private final class SparseItemGridScrollingIndicatorComponent: CombinedComponent let rect = rect.update( component: ShadowRoundedRectangle( - color: .white + color: context.component.backgroundColor ), availableSize: CGSize(width: text.size.width + 26.0, height: 32.0), transition: .immediate @@ -646,6 +647,8 @@ public final class SparseItemGridScrollingArea: ASDisplayNode { public var displayTooltip: DisplayTooltip? + private var theme: PresentationTheme? + override public init() { self.dateIndicator = ComponentHostView() self.lineIndicator = ComponentHostView() @@ -781,9 +784,11 @@ public final class SparseItemGridScrollingArea: ASDisplayNode { contentOffset: CGFloat, isScrolling: Bool, dateString: String, + theme: PresentationTheme, transition: ContainedViewLayoutTransition ) { self.containerSize = containerSize + self.theme = theme if self.dateIndicator.alpha.isZero { let transition: ContainedViewLayoutTransition = .immediate @@ -797,9 +802,9 @@ public final class SparseItemGridScrollingArea: ASDisplayNode { let indicatorSize = self.dateIndicator.update( transition: .immediate, component: AnyComponent(SparseItemGridScrollingIndicatorComponent( - backgroundColor: .white, + backgroundColor: theme.list.itemHighlightedBackgroundColor, shadowColor: .black, - foregroundColor: .black, + foregroundColor: theme.list.itemPrimaryTextColor, dateString: dateString )), environment: {}, @@ -858,7 +863,7 @@ public final class SparseItemGridScrollingArea: ASDisplayNode { } private func updateLineIndicator(transition: ContainedViewLayoutTransition) { - guard let indicatorPosition = self.indicatorPosition, let scrollIndicatorHeight = self.scrollIndicatorHeight else { + guard let indicatorPosition = self.indicatorPosition, let scrollIndicatorHeight = self.scrollIndicatorHeight, let theme = self.theme else { return } @@ -873,7 +878,7 @@ public final class SparseItemGridScrollingArea: ASDisplayNode { let _ = self.lineIndicator.update( transition: mappedTransition, component: AnyComponent(RoundedRectangle( - color: UIColor(white: 0.0, alpha: 0.3) + color: theme.list.scrollIndicatorColor )), environment: {}, containerSize: lineIndicatorSize diff --git a/submodules/TelegramCore/Sources/SyncCore/SyncCore_MediaReference.swift b/submodules/TelegramCore/Sources/SyncCore/SyncCore_MediaReference.swift index 29fa4cf791..b6b3672ac4 100644 --- a/submodules/TelegramCore/Sources/SyncCore/SyncCore_MediaReference.swift +++ b/submodules/TelegramCore/Sources/SyncCore/SyncCore_MediaReference.swift @@ -11,6 +11,15 @@ public struct MessageReference: PostboxCoding, Hashable, Equatable { return peer } } + + public var id: MessageId? { + switch content { + case .none: + return nil + case let .message(_, id, _, _, _): + return id + } + } public var timestamp: Int32? { switch content { diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Messages/SparseMessageList.swift b/submodules/TelegramCore/Sources/TelegramEngine/Messages/SparseMessageList.swift index d066374fb5..48da3da24d 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Messages/SparseMessageList.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Messages/SparseMessageList.swift @@ -74,6 +74,8 @@ public final class SparseMessageList { private var topSection: TopSection? private var topItemsDisposable = MetaDisposable() + private var deletedMessagesDisposable: Disposable? + private var sparseItems: SparseItems? private var sparseItemsDisposable: Disposable? @@ -159,12 +161,24 @@ public final class SparseMessageList { strongSelf.updateState() } }) + + self.deletedMessagesDisposable = (account.postbox.combinedView(keys: [.deletedMessages(peerId: peerId)]) + |> deliverOn(self.queue)).start(next: { [weak self] views in + guard let strongSelf = self else { + return + } + guard let view = views.views[.deletedMessages(peerId: peerId)] as? DeletedMessagesView else { + return + } + strongSelf.processDeletedMessages(ids: view.currentDeletedMessages) + }) } deinit { self.topItemsDisposable.dispose() self.sparseItemsDisposable?.dispose() self.loadHoleDisposable.dispose() + self.deletedMessagesDisposable?.dispose() } private func resetTopSection() { @@ -188,162 +202,37 @@ public final class SparseMessageList { })) } + private func processDeletedMessages(ids: [MessageId]) { + if let sparseItems = self.sparseItems { + let idsSet = Set(ids) + + var removeIndices: [Int] = [] + for i in 0 ..< sparseItems.items.count { + switch sparseItems.items[i] { + case let .anchor(id, _, message): + if message != nil, idsSet.contains(id) { + removeIndices.append(i) + } + default: + break + } + } + + if !removeIndices.isEmpty { + for index in removeIndices.reversed() { + self.sparseItems?.items.remove(at: index) + } + + self.updateState() + } + } + } + func loadMoreFromTopSection() { self.topSectionItemRequestCount += 100 self.resetTopSection() } - func loadPlaceholders(ids: [MessageId]) { - var loadGlobalIds: [MessageId] = [] - var loadChannelIds: [PeerId: [MessageId]] = [:] - for id in ids { - if self.loadingPlaceholders[id] != nil { - continue - } - self.loadingPlaceholders[id] = MetaDisposable() - if id.peerId.namespace == Namespaces.Peer.CloudUser || id.peerId.namespace == Namespaces.Peer.CloudGroup { - loadGlobalIds.append(id) - } else if id.peerId.namespace == Namespaces.Peer.CloudChannel { - if loadChannelIds[id.peerId] == nil { - loadChannelIds[id.peerId] = [] - } - loadChannelIds[id.peerId]!.append(id) - } - } - - var loadSignals: [Signal<(messages: [Api.Message], chats: [Api.Chat], users: [Api.User]), NoError>] = [] - let account = self.account - - if !loadGlobalIds.isEmpty { - loadSignals.append(self.account.postbox.transaction { transaction -> [Api.InputMessage] in - var result: [Api.InputMessage] = [] - for id in loadGlobalIds { - let inputMessage: Api.InputMessage = .inputMessageID(id: id.id) - result.append(inputMessage) - } - return result - } - |> mapToSignal { inputMessages -> Signal<(messages: [Api.Message], chats: [Api.Chat], users: [Api.User]), NoError> in - return account.network.request(Api.functions.messages.getMessages(id: inputMessages)) - |> map { result -> (messages: [Api.Message], chats: [Api.Chat], users: [Api.User]) in - switch result { - case let .messages(messages, chats, users): - return (messages, chats, users) - case let .messagesSlice(_, _, _, _, messages, chats, users): - return (messages, chats, users) - case let .channelMessages(_, _, _, _, messages, chats, users): - return (messages, chats, users) - case .messagesNotModified: - return ([], [], []) - } - } - |> `catch` { _ -> Signal<(messages: [Api.Message], chats: [Api.Chat], users: [Api.User]), NoError> in - return .single(([], [], [])) - } - }) - } - - if !loadChannelIds.isEmpty { - for (channelId, ids) in loadChannelIds { - loadSignals.append(self.account.postbox.transaction { transaction -> Api.InputChannel? in - return transaction.getPeer(channelId).flatMap(apiInputChannel) - } - |> mapToSignal { inputChannel -> Signal<(messages: [Api.Message], chats: [Api.Chat], users: [Api.User]), NoError> in - guard let inputChannel = inputChannel else { - return .single(([], [], [])) - } - - return account.network.request(Api.functions.channels.getMessages(channel: inputChannel, id: ids.map { Api.InputMessage.inputMessageID(id: $0.id) })) - |> map { result -> (messages: [Api.Message], chats: [Api.Chat], users: [Api.User]) in - switch result { - case let .messages(messages, chats, users): - return (messages, chats, users) - case let .messagesSlice(_, _, _, _, messages, chats, users): - return (messages, chats, users) - case let .channelMessages(_, _, _, _, messages, chats, users): - return (messages, chats, users) - case .messagesNotModified: - return ([], [], []) - } - } - |> `catch` { _ -> Signal<(messages: [Api.Message], chats: [Api.Chat], users: [Api.User]), NoError> in - return .single(([], [], [])) - } - }) - } - } - - let _ = (combineLatest(queue: self.queue, loadSignals) - |> mapToSignal { messageLists -> Signal<[Message], NoError> in - return account.postbox.transaction { transaction -> [Message] in - var parsedMessages: [StoreMessage] = [] - var peers: [Peer] = [] - var peerPresences: [PeerId: PeerPresence] = [:] - - for (messages, chats, users) in messageLists { - for chat in chats { - if let groupOrChannel = parseTelegramGroupOrChannel(chat: chat) { - peers.append(groupOrChannel) - } - } - for user in users { - let telegramUser = TelegramUser(user: user) - peers.append(telegramUser) - if let presence = TelegramUserPresence(apiUser: user) { - peerPresences[telegramUser.id] = presence - } - } - - for message in messages { - if let parsedMessage = StoreMessage(apiMessage: message) { - parsedMessages.append(parsedMessage) - } - } - } - - updatePeers(transaction: transaction, peers: peers, update: { _, updated in updated }) - updatePeerPresences(transaction: transaction, accountPeerId: account.peerId, peerPresences: peerPresences) - let _ = transaction.addMessages(parsedMessages, location: .Random) - - var result: [Message] = [] - for parsedMessage in parsedMessages { - switch parsedMessage.id { - case let .Id(id): - if let message = transaction.getMessage(id) { - result.append(message) - } - case .Partial: - break - } - } - - return result - } - } - |> deliverOn(self.queue)).start(next: { [weak self] messages in - guard let strongSelf = self else { - return - } - for message in messages { - strongSelf.loadedPlaceholders[message.id] = message - } - if strongSelf.sparseItems != nil { - for i in 0 ..< strongSelf.sparseItems!.items.count { - switch strongSelf.sparseItems!.items[i] { - case let .anchor(id, timestamp, _): - if let message = strongSelf.loadedPlaceholders[id] { - strongSelf.sparseItems!.items[i] = .anchor(id: id, timestamp: timestamp, message: message) - } - case .range: - break - } - } - } - - strongSelf.updateState() - }) - } - func loadHole(anchor: MessageId, direction: LoadHoleDirection, completion: @escaping () -> Void) { let loadingHole = LoadingHole(anchor: anchor, direction: direction) if self.loadingHole == loadingHole { @@ -811,12 +700,23 @@ public final class SparseMessageCalendar { private let queue: Queue private let impl: QueueLocalObject + public var minTimestamp: Int32? + private var disposable: Disposable? + init(account: Account, peerId: PeerId, messageTag: MessageTags) { let queue = Queue() self.queue = queue self.impl = QueueLocalObject(queue: queue, generate: { return Impl(queue: queue, account: account, peerId: peerId, messageTag: messageTag) }) + + self.disposable = self.state.start(next: { [weak self] state in + self?.minTimestamp = state.minTimestamp + }) + } + + deinit { + self.disposable?.dispose() } public var state: Signal { diff --git a/submodules/TelegramUI/Sources/ChatController.swift b/submodules/TelegramUI/Sources/ChatController.swift index e6191694b7..24c5f553d0 100644 --- a/submodules/TelegramUI/Sources/ChatController.swift +++ b/submodules/TelegramUI/Sources/ChatController.swift @@ -1814,7 +1814,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G ActionSheetButtonItem(title: strongSelf.presentationData.strings.Conversation_LinkDialogOpen, color: .accent, action: { [weak actionSheet] in actionSheet?.dismissAnimated() if let strongSelf = self { - strongSelf.openPeerMention(mention) + strongSelf.openPeerMention(mention, sourceMessageId: message?.id) } }), ActionSheetButtonItem(title: strongSelf.presentationData.strings.Conversation_LinkDialogCopy, color: .accent, action: { [weak actionSheet] in @@ -9423,7 +9423,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G }, recognizedQRCode: { [weak self] code in if let strongSelf = self { if let (host, port, username, password, secret) = parseProxyUrl(code) { - strongSelf.openResolved(ResolvedUrl.proxy(host: host, port: port, username: username, password: password, secret: secret)) + strongSelf.openResolved(result: ResolvedUrl.proxy(host: host, port: port, username: username, password: password, secret: secret), sourceMessageId: nil) } } }, presentSchedulePicker: { [weak self] done in @@ -12019,7 +12019,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G }) } - private func openPeerMention(_ name: String, navigation: ChatControllerInteractionNavigateToPeer = .default) { + private func openPeerMention(_ name: String, navigation: ChatControllerInteractionNavigateToPeer = .default, sourceMessageId: MessageId? = nil) { let _ = self.presentVoiceMessageDiscardAlert(action: { let disposable: MetaDisposable if let resolvePeerByNameDisposable = self.resolvePeerByNameDisposable { @@ -12070,7 +12070,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G navigation = .chat(textInputState: nil, subject: nil, peekData: nil) } } - strongSelf.openResolved(.peer(peer.id, navigation)) + strongSelf.openResolved(result: .peer(peer.id, navigation), sourceMessageId: sourceMessageId) } else { strongSelf.present(textAlertController(context: strongSelf.context, updatedPresentationData: strongSelf.updatedPresentationData, title: nil, text: strongSelf.presentationData.strings.Resolve_ErrorNotFound, actions: [TextAlertAction(type: .defaultAction, title: strongSelf.presentationData.strings.Common_OK, action: {})]), in: .window(.root)) } @@ -12378,7 +12378,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G })) } - private func openResolved(_ result: ResolvedUrl) { + private func openResolved(result: ResolvedUrl, sourceMessageId: MessageId?) { self.context.sharedContext.openResolvedUrl(result, context: self.context, urlContext: .chat(updatedPresentationData: self.updatedPresentationData), navigationController: self.effectiveNavigationController, openPeer: { [weak self] peerId, navigation in guard let strongSelf = self else { return @@ -12387,7 +12387,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G case let .chat(_, subject, peekData): if case .peer(peerId) = strongSelf.chatLocation { if let subject = subject, case let .message(messageId, _, timecode) = subject { - strongSelf.navigateToMessage(from: nil, to: .id(messageId, timecode)) + strongSelf.navigateToMessage(from: sourceMessageId, to: .id(messageId, timecode)) } } else if let navigationController = strongSelf.effectiveNavigationController { strongSelf.context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, context: strongSelf.context, chatLocation: .peer(peerId), subject: subject, keepStack: .always, peekData: peekData)) @@ -12488,7 +12488,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G openUserGeneratedUrl(context: self.context, peerId: self.peerView?.peerId, url: url, concealed: concealed, skipUrlAuth: skipUrlAuth, present: { [weak self] c in self?.present(c, in: .window(.root)) }, openResolved: { [weak self] resolved in - self?.openResolved(resolved) + self?.openResolved(result: resolved, sourceMessageId: message?.id) }) }, performAction: true) } diff --git a/submodules/TelegramUI/Sources/ChatMessageAttachedContentNode.swift b/submodules/TelegramUI/Sources/ChatMessageAttachedContentNode.swift index 464453b54c..134f4977f5 100644 --- a/submodules/TelegramUI/Sources/ChatMessageAttachedContentNode.swift +++ b/submodules/TelegramUI/Sources/ChatMessageAttachedContentNode.swift @@ -285,7 +285,12 @@ final class ChatMessageAttachedContentNode: ASDisplayNode { return { presentationData, automaticDownloadSettings, associatedData, attributes, context, controllerInteraction, message, messageRead, chatLocation, title, subtitle, text, entities, mediaAndFlags, mediaBadge, actionIcon, actionTitle, displayLine, layoutConstants, preparePosition, constrainedSize in let isPreview = presentationData.isPreview - let fontSize: CGFloat = floor(presentationData.fontSize.baseDisplaySize * 15.0 / 17.0) + let fontSize: CGFloat + if message.adAttribute != nil { + fontSize = floor(presentationData.fontSize.baseDisplaySize) + } else { + fontSize = floor(presentationData.fontSize.baseDisplaySize * 15.0 / 17.0) + } let titleFont = Font.semibold(fontSize) let textFont = Font.regular(fontSize) diff --git a/submodules/TelegramUI/Sources/ChatMessageBubbleItemNode.swift b/submodules/TelegramUI/Sources/ChatMessageBubbleItemNode.swift index f077acfc5f..3b3d34b0d0 100644 --- a/submodules/TelegramUI/Sources/ChatMessageBubbleItemNode.swift +++ b/submodules/TelegramUI/Sources/ChatMessageBubbleItemNode.swift @@ -1104,19 +1104,20 @@ class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewItemNode if isPreview { needShareButton = false } - if item.content.firstMessage.adAttribute != nil { + let isAd = item.content.firstMessage.adAttribute != nil + if isAd { needShareButton = false } var tmpWidth: CGFloat if allowFullWidth { tmpWidth = baseWidth - if needShareButton { + if needShareButton || isAd { tmpWidth -= 38.0 } } else { tmpWidth = layoutConstants.bubble.maximumWidthFill.widthFor(baseWidth) - if needShareButton && tmpWidth + 32.0 > baseWidth { + if (needShareButton || isAd) && tmpWidth + 32.0 > baseWidth { tmpWidth = baseWidth - 32.0 } } @@ -2580,7 +2581,6 @@ class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewItemNode strongSelf.insertSubnode(shareButtonNode, belowSubnode: strongSelf.messageAccessibilityArea) shareButtonNode.addTarget(strongSelf, action: #selector(strongSelf.shareButtonPressed), forControlEvents: .touchUpInside) } - } else if let shareButtonNode = strongSelf.shareButtonNode { strongSelf.shareButtonNode = nil shareButtonNode.removeFromSupernode() diff --git a/submodules/TelegramUI/Sources/ChatMessageInteractiveMediaNode.swift b/submodules/TelegramUI/Sources/ChatMessageInteractiveMediaNode.swift index 323e38ba2e..8303684a1a 100644 --- a/submodules/TelegramUI/Sources/ChatMessageInteractiveMediaNode.swift +++ b/submodules/TelegramUI/Sources/ChatMessageInteractiveMediaNode.swift @@ -613,10 +613,8 @@ final class ChatMessageInteractiveMediaNode: ASDisplayNode, GalleryItemTransitio updatedFetchControls = FetchControls(fetch: { manual in if let strongSelf = self { - if !manual { - strongSelf.fetchDisposable.set(chatMessagePhotoInteractiveFetched(context: context, photoReference: .message(message: MessageReference(message), media: image), displayAtSize: isSecretMedia ? nil : 600, storeToDownloadsPeerType: storeToDownloadsPeerType).start()) - } else if let representation = largestRepresentationForPhoto(image) { - strongSelf.fetchDisposable.set(messageMediaImageInteractiveFetched(context: context, message: message, image: image, resource: representation.resource, range: representationFetchRangeForDisplayAtSize(representation: representation, dimension: isSecretMedia ? nil : 600), storeToDownloadsPeerType: storeToDownloadsPeerType).start()) + if let representation = largestRepresentationForPhoto(image) { + strongSelf.fetchDisposable.set(messageMediaImageInteractiveFetched(context: context, message: message, image: image, resource: representation.resource, range: representationFetchRangeForDisplayAtSize(representation: representation, dimension: isSecretMedia ? nil : 600), userInitiated: manual, storeToDownloadsPeerType: storeToDownloadsPeerType).start()) } } }, cancel: { @@ -816,6 +814,7 @@ final class ChatMessageInteractiveMediaNode: ASDisplayNode, GalleryItemTransitio let arguments = TransformImageArguments(corners: corners, imageSize: drawingSize, boundingSize: boundingSize, intrinsicInsets: UIEdgeInsets(), resizeMode: isInlinePlayableVideo ? .fill(.black) : .blurBackground, emptyColor: emptyColor, custom: patternArguments) let imageFrame = CGRect(origin: CGPoint(x: -arguments.insets.left, y: -arguments.insets.top), size: arguments.drawingSize).ensuredValid + let cleanImageFrame = CGRect(origin: imageFrame.origin, size: CGSize(width: imageFrame.width - arguments.corners.extendedEdges.right, height: imageFrame.height)) let imageApply = imageLayout(arguments) @@ -860,7 +859,7 @@ final class ChatMessageInteractiveMediaNode: ASDisplayNode, GalleryItemTransitio } statusApply(hasAnimation) - let dateAndStatusFrame = CGRect(origin: CGPoint(x: imageFrame.width - layoutConstants.image.statusInsets.right - statusSize.width, y: imageFrame.height - layoutConstants.image.statusInsets.bottom - statusSize.height), size: statusSize) + let dateAndStatusFrame = CGRect(origin: CGPoint(x: cleanImageFrame.width - layoutConstants.image.statusInsets.right - statusSize.width, y: cleanImageFrame.height - layoutConstants.image.statusInsets.bottom - statusSize.height), size: statusSize) strongSelf.dateAndStatusNode.frame = dateAndStatusFrame strongSelf.dateAndStatusNode.bounds = CGRect(origin: CGPoint(), size: dateAndStatusFrame.size) diff --git a/submodules/TelegramUI/Sources/FetchManager.swift b/submodules/TelegramUI/Sources/FetchManager.swift index 26fef02594..cf546fc69c 100644 --- a/submodules/TelegramUI/Sources/FetchManager.swift +++ b/submodules/TelegramUI/Sources/FetchManager.swift @@ -99,8 +99,10 @@ private final class FetchManagerStatusContext { if let originalStatus = self.originalStatus { if originalStatus == .Remote && self.hasEntry { return .Fetching(isActive: false, progress: 0.0) - } else { + } else if self.hasEntry { return originalStatus + } else { + return .Remote } } else { return nil @@ -238,7 +240,14 @@ private final class FetchManagerCategoryContext { if (previousPriorityKey != nil) != (updatedPriorityKey != nil) { if let statusContext = self.statusContexts[id] { - var hasForegroundPriorityKey = false + let previousStatus = statusContext.combinedStatus + statusContext.hasEntry = self.entries[id] != nil + if let combinedStatus = statusContext.combinedStatus, combinedStatus != previousStatus { + for f in statusContext.subscribers.copyItems() { + f(combinedStatus) + } + } + /*var hasForegroundPriorityKey = false if let updatedPriorityKey = updatedPriorityKey, let topReference = updatedPriorityKey.topReference { switch topReference { case .userInitiated: @@ -270,7 +279,7 @@ private final class FetchManagerCategoryContext { } } } - } + }*/ } } diff --git a/submodules/TelegramUI/Sources/PeerInfo/Panes/PeerInfoVisualMediaPaneNode.swift b/submodules/TelegramUI/Sources/PeerInfo/Panes/PeerInfoVisualMediaPaneNode.swift index ca61755cd7..5447b9aa0b 100644 --- a/submodules/TelegramUI/Sources/PeerInfo/Panes/PeerInfoVisualMediaPaneNode.swift +++ b/submodules/TelegramUI/Sources/PeerInfo/Panes/PeerInfoVisualMediaPaneNode.swift @@ -1384,7 +1384,7 @@ final class PeerInfoVisualMediaPaneNode: ASDisplayNode, PeerInfoPaneNode, UIScro self.stateTag = tagMaskForType(contentType) self.contextGestureContainerNode = ContextControllerSourceNode() - self.itemGrid = SparseItemGrid() + self.itemGrid = SparseItemGrid(theme: self.context.sharedContext.currentPresentationData.with({ $0 }).theme) self.directMediaImageCache = DirectMediaImageCache(account: context.account) let useListItems: Bool @@ -2160,7 +2160,7 @@ final class PeerInfoVisualMediaPaneNode: ASDisplayNode, PeerInfoPaneNode, UIScro fixedItemHeight = nil } - self.itemGrid.update(size: size, insets: UIEdgeInsets(top: topInset, left: sideInset, bottom: bottomInset, right: sideInset), scrollIndicatorInsets: UIEdgeInsets(top: 0.0, left: sideInset, bottom: bottomInset, right: sideInset), lockScrollingAtTop: isScrollingLockedAtTop, fixedItemHeight: fixedItemHeight, items: items, synchronous: wasFirstTime) + self.itemGrid.update(size: size, insets: UIEdgeInsets(top: topInset, left: sideInset, bottom: bottomInset, right: sideInset), scrollIndicatorInsets: UIEdgeInsets(top: 0.0, left: sideInset, bottom: bottomInset, right: sideInset), lockScrollingAtTop: isScrollingLockedAtTop, fixedItemHeight: fixedItemHeight, items: items, theme: self.itemGridBinding.chatPresentationData.theme.theme, synchronous: wasFirstTime) } } @@ -2201,6 +2201,24 @@ final class PeerInfoVisualMediaPaneNode: ASDisplayNode, PeerInfoPaneNode, UIScro } if let index = previousIndex { self.itemGrid.scrollToItem(at: index) + + if let item = self.itemGrid.item(at: index) { + if let layer = item.layer as? ItemLayer { + Queue.mainQueue().after(0.1, { [weak layer] in + guard let layer = layer else { + return + } + + let overlayLayer = ListShimmerLayer.OverlayLayer() + overlayLayer.backgroundColor = UIColor(white: 1.0, alpha: 0.6).cgColor + overlayLayer.frame = layer.bounds + layer.addSublayer(overlayLayer) + overlayLayer.animateAlpha(from: 1.0, to: 0.0, duration: 0.8, delay: 0.3, removeOnCompletion: false, completion: { [weak overlayLayer] _ in + overlayLayer?.removeFromSuperlayer() + }) + }) + } + } } } } diff --git a/submodules/TelegramUI/Sources/PeerInfo/PeerInfoScreen.swift b/submodules/TelegramUI/Sources/PeerInfo/PeerInfoScreen.swift index f1475a6f73..037428f3da 100644 --- a/submodules/TelegramUI/Sources/PeerInfo/PeerInfoScreen.swift +++ b/submodules/TelegramUI/Sources/PeerInfo/PeerInfoScreen.swift @@ -6027,6 +6027,7 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, UIScrollViewDelegate let _ = (context.account.postbox.combinedView(keys: summaryTags.map { tag in return PostboxViewKey.historyTagSummaryView(tag: tag, peerId: peerId, namespace: Namespaces.Message.Cloud) }) + |> take(1) |> deliverOnMainQueue).start(next: { [weak self] views in guard let strongSelf = self else { return @@ -6194,7 +6195,9 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, UIScrollViewDelegate } initialTimestamp = timestamp - self.controller?.push(CalendarMessageScreen(context: self.context, peerId: self.peerId, calendarSource: calendarSource, initialTimestamp: initialTimestamp, navigateToDay: { [weak self] c, timestamp in + var dismissCalendarScreen: (() -> Void)? + + let calendarScreen = CalendarMessageScreen(context: self.context, peerId: self.peerId, calendarSource: calendarSource, initialTimestamp: initialTimestamp, navigateToDay: { [weak self] c, timestamp in guard let strongSelf = self else { c.dismiss() return @@ -6207,7 +6210,60 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, UIScrollViewDelegate pane.scrollToTimestamp(timestamp: timestamp) c.dismiss() - })) + }, previewDay: { [weak self] index, sourceNode, sourceRect, gesture in + guard let strongSelf = self else { + return + } + + var items: [ContextMenuItem] = [] + + items.append(.action(ContextMenuActionItem(text: strongSelf.presentationData.strings.SharedMedia_ViewInChat, icon: { theme in + return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/GoToMessage"), color: theme.contextMenu.primaryColor) + }, action: { _, f in + f(.dismissWithoutContent) + dismissCalendarScreen?() + + guard let strongSelf = self, let controller = strongSelf.controller, let navigationController = controller.navigationController as? NavigationController else { + return + } + + strongSelf.context.sharedContext.navigateToChatController(NavigateToChatControllerParams( + navigationController: navigationController, + chatController: nil, + context: strongSelf.context, + chatLocation: .peer(strongSelf.peerId), + subject: .message(id: index.id, highlight: false, timecode: nil), + botStart: nil, + updateTextInputState: nil, + activateInput: false, + keepStack: .never, + useExisting: true, + purposefulAction: nil, + scrollToEndIfExists: false, + activateMessageSearch: nil, + peekData: nil, + peerNearbyData: nil, + reportReason: nil, + animated: true, + options: [], + parentGroupId: nil, + chatListFilter: nil, + changeColors: false, + completion: { _ in + } + )) + }))) + + let chatController = strongSelf.context.sharedContext.makeChatController(context: strongSelf.context, chatLocation: .peer(strongSelf.peerId), subject: .message(id: index.id, highlight: false, timecode: nil), botStart: nil, mode: .standard(previewing: true)) + chatController.canReadHistory.set(false) + let contextController = ContextController(account: strongSelf.context.account, presentationData: strongSelf.presentationData, source: .controller(ContextControllerContentSourceImpl(controller: chatController, sourceNode: sourceNode, sourceRect: sourceRect, passthroughTouches: true)), items: .single(ContextController.Items(items: items)), gesture: gesture) + strongSelf.controller?.presentInGlobalOverlay(contextController) + }) + + self.controller?.push(calendarScreen) + dismissCalendarScreen = { [weak calendarScreen] in + calendarScreen?.dismiss(completion: nil) + } } func updatePresentationData(_ presentationData: PresentationData) { @@ -7427,21 +7483,26 @@ private final class PeerInfoNavigationTransitionNode: ASDisplayNode, CustomNavig private final class ContextControllerContentSourceImpl: ContextControllerContentSource { let controller: ViewController weak var sourceNode: ASDisplayNode? + let sourceRect: CGRect let navigationController: NavigationController? = nil - let passthroughTouches: Bool = false + let passthroughTouches: Bool - init(controller: ViewController, sourceNode: ASDisplayNode?) { + init(controller: ViewController, sourceNode: ASDisplayNode?, sourceRect: CGRect = CGRect(origin: CGPoint(), size: CGSize()), passthroughTouches: Bool = false) { self.controller = controller self.sourceNode = sourceNode + self.sourceRect = sourceRect + self.passthroughTouches = passthroughTouches } func transitionInfo() -> ContextControllerTakeControllerInfo? { let sourceNode = self.sourceNode + let sourceRect = self.sourceRect return ContextControllerTakeControllerInfo(contentAreaInScreenSpace: CGRect(origin: CGPoint(), size: CGSize(width: 10.0, height: 10.0)), sourceNode: { [weak sourceNode] in if let sourceNode = sourceNode { - return (sourceNode, sourceNode.bounds) + let rect = sourceRect.isEmpty ? sourceNode.bounds : sourceRect + return (sourceNode, rect) } else { return nil }