From c2f111a5b84397ab2edc39a7f1916a2b876387a7 Mon Sep 17 00:00:00 2001 From: Ilya Laktyushin Date: Sun, 15 Nov 2020 23:04:24 +0400 Subject: [PATCH 1/8] Fix location view directions button subtitle --- submodules/LocationUI/Sources/LocationUtils.swift | 2 +- .../SolidRoundedButtonNode/Sources/SolidRoundedButtonNode.swift | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/submodules/LocationUI/Sources/LocationUtils.swift b/submodules/LocationUI/Sources/LocationUtils.swift index 30475fa14a..7d658b057c 100644 --- a/submodules/LocationUI/Sources/LocationUtils.swift +++ b/submodules/LocationUI/Sources/LocationUtils.swift @@ -81,7 +81,7 @@ func stringForEstimatedDuration(strings: PresentationStrings, eta: Double) -> St if hours == 1 && minutes == 0 { string = strings.Map_ETAHours(1) } else { - string = strings.Map_ETAHours(9999).replacingOccurrences(of: "9999", with: String(format: "%d:%02d", arguments: [hours, minutes])) + string = strings.Map_ETAHours(10).replacingOccurrences(of: "10", with: String(format: "%d:%02d", arguments: [hours, minutes])) } } else { string = strings.Map_ETAMinutes(minutes) diff --git a/submodules/SolidRoundedButtonNode/Sources/SolidRoundedButtonNode.swift b/submodules/SolidRoundedButtonNode/Sources/SolidRoundedButtonNode.swift index c5c6f42d3f..671f2a3690 100644 --- a/submodules/SolidRoundedButtonNode/Sources/SolidRoundedButtonNode.swift +++ b/submodules/SolidRoundedButtonNode/Sources/SolidRoundedButtonNode.swift @@ -129,7 +129,7 @@ public final class SolidRoundedButtonNode: ASDisplayNode { } public func updateLayout(width: CGFloat, transition: ContainedViewLayoutTransition) -> CGFloat { - return self.updateLayout(width: width, previousSubtitle: nil, transition: transition) + return self.updateLayout(width: width, previousSubtitle: self.subtitle, transition: transition) } private func updateLayout(width: CGFloat, previousSubtitle: String?, transition: ContainedViewLayoutTransition) -> CGFloat { From 433d52cc20feac433c56298d5f6bcb9b53c34de2 Mon Sep 17 00:00:00 2001 From: Ilya Laktyushin Date: Mon, 16 Nov 2020 18:26:06 +0400 Subject: [PATCH 2/8] Request authorization for Siri message announcements --- submodules/TelegramUI/Sources/AppDelegate.swift | 3 +++ 1 file changed, 3 insertions(+) diff --git a/submodules/TelegramUI/Sources/AppDelegate.swift b/submodules/TelegramUI/Sources/AppDelegate.swift index 526e5c6a8e..b21e39473c 100644 --- a/submodules/TelegramUI/Sources/AppDelegate.swift +++ b/submodules/TelegramUI/Sources/AppDelegate.swift @@ -2089,6 +2089,9 @@ final class SharedApplicationContext { if #available(iOS 12.0, *) { authorizationOptions.insert(.providesAppNotificationSettings) } + if #available(iOS 13.0, *) { + authorizationOptions.insert(.announcement) + } notificationCenter.requestAuthorization(options: authorizationOptions, completionHandler: { result, _ in completion(result) if result { From 3166742ddb551e093da3398af62b11f598b9ea41 Mon Sep 17 00:00:00 2001 From: Ilya Laktyushin Date: Mon, 16 Nov 2020 18:26:52 +0400 Subject: [PATCH 3/8] Fix animated sticker placeholders --- .../TelegramUI/Sources/ChatMediaInputNode.swift | 14 ++++++++------ .../Sources/ChatMediaInputStickerGridItem.swift | 2 +- .../TelegramUI/Sources/ChatMessageItem.swift | 2 +- .../TelegramUI/Sources/MultiplexedVideoNode.swift | 2 +- .../Sources/PaneSearchContainerNode.swift | 2 +- 5 files changed, 12 insertions(+), 10 deletions(-) diff --git a/submodules/TelegramUI/Sources/ChatMediaInputNode.swift b/submodules/TelegramUI/Sources/ChatMediaInputNode.swift index 1a3247afc9..3db7dff3f3 100644 --- a/submodules/TelegramUI/Sources/ChatMediaInputNode.swift +++ b/submodules/TelegramUI/Sources/ChatMediaInputNode.swift @@ -681,7 +681,7 @@ final class ChatMediaInputNode: ChatInputNode { return false } - self.backgroundColor = theme.chat.inputMediaPanel.stickersBackgroundColor + self.backgroundColor = theme.chat.inputMediaPanel.stickersBackgroundColor.withAlphaComponent(1.0) self.collectionListPanel.addSubnode(self.listView) self.collectionListPanel.addSubnode(self.gifListView) @@ -934,10 +934,12 @@ final class ChatMediaInputNode: ChatInputNode { if let currentView = strongSelf.currentView, let (topIndex, topItem) = visibleItems.top, let (bottomIndex, bottomItem) = visibleItems.bottom { if topIndex <= 10 && currentView.lower != nil { - let position: StickerPacksCollectionPosition = clipScrollPosition(.scroll(aroundIndex: (topItem as! ChatMediaInputStickerGridItem).index)) - if strongSelf.currentStickerPacksCollectionPosition != position { - strongSelf.currentStickerPacksCollectionPosition = position - strongSelf.itemCollectionsViewPosition.set(.single(position)) + if let topItem = topItem as? ChatMediaInputStickerGridItem { + let position: StickerPacksCollectionPosition = clipScrollPosition(.scroll(aroundIndex: topItem.index)) + if strongSelf.currentStickerPacksCollectionPosition != position { + strongSelf.currentStickerPacksCollectionPosition = position + strongSelf.itemCollectionsViewPosition.set(.single(position)) + } } } else if bottomIndex >= visibleItems.count - 10 && currentView.higher != nil { var position: StickerPacksCollectionPosition? @@ -1052,7 +1054,7 @@ final class ChatMediaInputNode: ChatInputNode { } self.collectionListSeparator.backgroundColor = theme.chat.inputMediaPanel.panelSeparatorColor - self.backgroundColor = theme.chat.inputMediaPanel.stickersBackgroundColor + self.backgroundColor = theme.chat.inputMediaPanel.stickersBackgroundColor.withAlphaComponent(1.0) self.searchContainerNode?.updateThemeAndStrings(theme: theme, strings: strings) diff --git a/submodules/TelegramUI/Sources/ChatMediaInputStickerGridItem.swift b/submodules/TelegramUI/Sources/ChatMediaInputStickerGridItem.swift index 9afde7bbd1..e2ed66dfc4 100644 --- a/submodules/TelegramUI/Sources/ChatMediaInputStickerGridItem.swift +++ b/submodules/TelegramUI/Sources/ChatMediaInputStickerGridItem.swift @@ -319,7 +319,7 @@ final class ChatMediaInputStickerGridItemNode: GridItemNode { placeholderNode.frame = placeholderFrame let theme = item.theme - placeholderNode.update(backgroundColor: theme.chat.inputMediaPanel.stickersBackgroundColor, foregroundColor: theme.chat.inputMediaPanel.stickersSectionTextColor.blitOver(theme.chat.inputMediaPanel.stickersBackgroundColor, alpha: 0.15), shimmeringColor: theme.list.itemBlocksBackgroundColor.withAlphaComponent(0.3), data: item.stickerItem.file.immediateThumbnailData, size: placeholderFrame.size) + placeholderNode.update(backgroundColor: theme.chat.inputMediaPanel.stickersBackgroundColor.withAlphaComponent(1.0), foregroundColor: theme.chat.inputMediaPanel.stickersSectionTextColor.blitOver(theme.chat.inputMediaPanel.stickersBackgroundColor, alpha: 0.15), shimmeringColor: theme.list.itemBlocksBackgroundColor.withAlphaComponent(0.3), data: item.stickerItem.file.immediateThumbnailData, size: placeholderFrame.size) } } diff --git a/submodules/TelegramUI/Sources/ChatMessageItem.swift b/submodules/TelegramUI/Sources/ChatMessageItem.swift index b704489ea5..546e288117 100644 --- a/submodules/TelegramUI/Sources/ChatMessageItem.swift +++ b/submodules/TelegramUI/Sources/ChatMessageItem.swift @@ -349,7 +349,7 @@ public final class ChatMessageItem: ListViewItem, CustomStringConvertible { loop: for media in self.message.media { if let telegramFile = media as? TelegramMediaFile { - if telegramFile.isAnimatedSticker, (self.message.id.peerId.namespace == Namespaces.Peer.SecretChat || !telegramFile.previewRepresentations.isEmpty), let size = telegramFile.size, size > 0 && size <= 128 * 1024 { + if telegramFile.isAnimatedSticker, let size = telegramFile.size, size > 0 && size <= 128 * 1024 { if self.message.id.peerId.namespace == Namespaces.Peer.SecretChat { if telegramFile.fileId.namespace == Namespaces.Media.CloudFile { var isValidated = false diff --git a/submodules/TelegramUI/Sources/MultiplexedVideoNode.swift b/submodules/TelegramUI/Sources/MultiplexedVideoNode.swift index 3d29539c2b..0718703b26 100644 --- a/submodules/TelegramUI/Sources/MultiplexedVideoNode.swift +++ b/submodules/TelegramUI/Sources/MultiplexedVideoNode.swift @@ -30,7 +30,7 @@ final class MultiplexedVideoPlaceholderNode: ASDisplayNode { } self.effectNode.frame = CGRect(origin: CGPoint(), size: size) - self.effectNode.update(backgroundColor: theme.chat.inputMediaPanel.stickersBackgroundColor, foregroundColor: theme.chat.inputMediaPanel.stickersSectionTextColor.blitOver(theme.chat.inputMediaPanel.stickersBackgroundColor, alpha: 0.2), shimmeringColor: theme.list.itemBlocksBackgroundColor.withAlphaComponent(0.3), shapes: [.rect(rect: CGRect(origin: CGPoint(), size: size))], size: bounds.size) + self.effectNode.update(backgroundColor: theme.chat.inputMediaPanel.stickersBackgroundColor.withAlphaComponent(1.0), foregroundColor: theme.chat.inputMediaPanel.stickersSectionTextColor.blitOver(theme.chat.inputMediaPanel.stickersBackgroundColor, alpha: 0.2), shimmeringColor: theme.list.itemBlocksBackgroundColor.withAlphaComponent(0.3), shapes: [.rect(rect: CGRect(origin: CGPoint(), size: size))], size: bounds.size) } func updateAbsoluteRect(_ absoluteRect: CGRect, within containerSize: CGSize) { diff --git a/submodules/TelegramUI/Sources/PaneSearchContainerNode.swift b/submodules/TelegramUI/Sources/PaneSearchContainerNode.swift index 9731d96385..1059cb07cb 100644 --- a/submodules/TelegramUI/Sources/PaneSearchContainerNode.swift +++ b/submodules/TelegramUI/Sources/PaneSearchContainerNode.swift @@ -97,7 +97,7 @@ final class PaneSearchContainerNode: ASDisplayNode { } func updateThemeAndStrings(theme: PresentationTheme, strings: PresentationStrings) { - self.backgroundNode.backgroundColor = theme.chat.inputMediaPanel.stickersBackgroundColor + self.backgroundNode.backgroundColor = theme.chat.inputMediaPanel.stickersBackgroundColor.withAlphaComponent(1.0) self.contentNode.updateThemeAndStrings(theme: theme, strings: strings) self.searchBar.updateThemeAndStrings(theme: theme, strings: strings) From 4e35d6e5bf51863888324316d9f7e80f1f227057 Mon Sep 17 00:00:00 2001 From: Ilya Laktyushin Date: Mon, 16 Nov 2020 22:45:24 +0400 Subject: [PATCH 4/8] Tune outgoing photo resizing & compression --- .../FetchPhotoLibraryImageResource.swift | 96 +++++++++++++++++-- .../MozjpegBinding/Sources/MozjpegBinding.m | 2 +- 2 files changed, 90 insertions(+), 8 deletions(-) diff --git a/submodules/LocalMediaResources/Sources/FetchPhotoLibraryImageResource.swift b/submodules/LocalMediaResources/Sources/FetchPhotoLibraryImageResource.swift index bf04a0038e..2e7a8b2e89 100644 --- a/submodules/LocalMediaResources/Sources/FetchPhotoLibraryImageResource.swift +++ b/submodules/LocalMediaResources/Sources/FetchPhotoLibraryImageResource.swift @@ -4,12 +4,84 @@ import Photos import Postbox import SwiftSignalKit import ImageCompression +import Accelerate.vImage private final class RequestId { var id: PHImageRequestID? var invalidated: Bool = false } +private func resizedImage(_ image: UIImage, for size: CGSize) -> UIImage? { + guard let cgImage = image.cgImage else { + return nil + } + + var format = vImage_CGImageFormat(bitsPerComponent: 8, + bitsPerPixel: 32, + colorSpace: nil, + bitmapInfo: CGBitmapInfo(rawValue: CGImageAlphaInfo.first.rawValue), + version: 0, + decode: nil, + renderingIntent: .defaultIntent) + + var error: vImage_Error + var sourceBuffer = vImage_Buffer() + defer { sourceBuffer.data.deallocate() } + error = vImageBuffer_InitWithCGImage(&sourceBuffer, + &format, + nil, + cgImage, + vImage_Flags(kvImageNoFlags)) + guard error == kvImageNoError else { return nil } + + var destinationBuffer = vImage_Buffer() + error = vImageBuffer_Init(&destinationBuffer, + vImagePixelCount(size.height), + vImagePixelCount(size.width), + format.bitsPerPixel, + vImage_Flags(kvImageNoFlags)) + guard error == kvImageNoError else { + return nil + } + + error = vImageScale_ARGB8888(&sourceBuffer, + &destinationBuffer, + nil, + vImage_Flags(kvImageHighQualityResampling)) + guard error == kvImageNoError else { + return nil + } + + guard let resizedImage = + vImageCreateCGImageFromBuffer(&destinationBuffer, + &format, + nil, + nil, + vImage_Flags(kvImageNoAllocate), + &error)?.takeRetainedValue(), + error == kvImageNoError + else { + return nil + } + + return UIImage(cgImage: resizedImage) +} + +extension UIImage.Orientation { + init(_ cgOrientation: CGImagePropertyOrientation) { + switch cgOrientation { + case .up: self = .up + case .upMirrored: self = .upMirrored + case .down: self = .down + case .downMirrored: self = .downMirrored + case .left: self = .left + case .leftMirrored: self = .leftMirrored + case .right: self = .right + case .rightMirrored: self = .rightMirrored + } + } +} + public func fetchPhotoLibraryResource(localIdentifier: String) -> Signal { return Signal { subscriber in let fetchResult = PHAsset.fetchAssets(withLocalIdentifiers: [localIdentifier], options: nil) @@ -28,7 +100,9 @@ public func fetchPhotoLibraryResource(localIdentifier: String) -> Signal Void in + let startTime = CACurrentMediaTime() + + let requestIdValue = PHImageManager.default().requestImage(for: asset, targetSize: PHImageManagerMaximumSize, contentMode: .aspectFit, options: option, resultHandler: { (image, info) -> Void in Queue.concurrentDefaultQueue().async { requestId.with { current -> Void in if !current.invalidated { @@ -42,17 +116,24 @@ public func fetchPhotoLibraryResource(localIdentifier: String) -> Signal } } } + diff --git a/submodules/MozjpegBinding/Sources/MozjpegBinding.m b/submodules/MozjpegBinding/Sources/MozjpegBinding.m index e3952205cc..fa2dd964e2 100644 --- a/submodules/MozjpegBinding/Sources/MozjpegBinding.m +++ b/submodules/MozjpegBinding/Sources/MozjpegBinding.m @@ -125,7 +125,7 @@ NSData * _Nullable compressJPEGData(UIImage * _Nonnull sourceImage) { cinfo.arith_code = FALSE; cinfo.dct_method = JDCT_ISLOW; cinfo.optimize_coding = TRUE; - jpeg_set_quality(&cinfo, 78, 1); + jpeg_set_quality(&cinfo, 72, 1); jpeg_simple_progression(&cinfo); jpeg_start_compress(&cinfo, 1); From 5d2724a09e94a3121920e116be77f16bef3e15cc Mon Sep 17 00:00:00 2001 From: Ilya Laktyushin Date: Mon, 16 Nov 2020 23:39:09 +0400 Subject: [PATCH 5/8] Fix animated stickers placeholders --- .../TelegramUI/Sources/ChatMessageAnimatedStickerItemNode.swift | 1 - 1 file changed, 1 deletion(-) diff --git a/submodules/TelegramUI/Sources/ChatMessageAnimatedStickerItemNode.swift b/submodules/TelegramUI/Sources/ChatMessageAnimatedStickerItemNode.swift index b1c6cdaa10..b4158193b8 100644 --- a/submodules/TelegramUI/Sources/ChatMessageAnimatedStickerItemNode.swift +++ b/submodules/TelegramUI/Sources/ChatMessageAnimatedStickerItemNode.swift @@ -966,7 +966,6 @@ class ChatMessageAnimatedStickerItemNode: ChatMessageItemView { if let immediateThumbnailData = file?.immediateThumbnailData, let placeholderNode = strongSelf.placeholderNode { placeholderNode.update(backgroundColor: nil, foregroundColor: UIColor(rgb: 0x748391, alpha: 0.2), shimmeringColor: UIColor(rgb: 0x748391, alpha: 0.35), data: immediateThumbnailData, size: animationNodeFrame.size) placeholderNode.frame = animationNodeFrame - strongSelf.animationNode?.isHidden = true } if let animationNode = strongSelf.animationNode, let parentNode = strongSelf.greetingStickerParentNode, strongSelf.animateGreeting { From 3b7ee797b301fc56c40aa39b6752e9ea9db46631 Mon Sep 17 00:00:00 2001 From: Ilya Laktyushin Date: Tue, 17 Nov 2020 13:55:34 +0400 Subject: [PATCH 6/8] Fix animated emoji haptics --- .../ChatMessageAnimatedStickerItemNode.swift | 44 +++++++++++-------- 1 file changed, 25 insertions(+), 19 deletions(-) diff --git a/submodules/TelegramUI/Sources/ChatMessageAnimatedStickerItemNode.swift b/submodules/TelegramUI/Sources/ChatMessageAnimatedStickerItemNode.swift index b4158193b8..e6c0accb84 100644 --- a/submodules/TelegramUI/Sources/ChatMessageAnimatedStickerItemNode.swift +++ b/submodules/TelegramUI/Sources/ChatMessageAnimatedStickerItemNode.swift @@ -1303,6 +1303,7 @@ class ChatMessageAnimatedStickerItemNode: ChatMessageItemView { } let beatingHearts: [UInt32] = [0x2764, 0x1F90E, 0x1F9E1, 0x1F499, 0x1F49A, 0x1F49C, 0x1F49B, 0x1F5A4, 0x1F90D] + let heart = 0x2764 let peach = 0x1F351 let coffin = 0x26B0 @@ -1312,7 +1313,12 @@ class ChatMessageAnimatedStickerItemNode: ChatMessageItemView { return view.values[PreferencesKeys.appConfiguration] as? AppConfiguration ?? .defaultValue } - if let text = self.item?.message.text, let firstScalar = text.unicodeScalars.first { + if let text = self.item?.message.text, var firstScalar = text.unicodeScalars.first { + var textEmoji = text.strippedEmoji + if beatingHearts.contains(firstScalar.value) { + textEmoji = "❤️" + firstScalar = UnicodeScalar(heart)! + } return .optionalAction({ if shouldPlay { let _ = (appConfiguration @@ -1322,7 +1328,7 @@ class ChatMessageAnimatedStickerItemNode: ChatMessageItemView { } let emojiSounds = AnimatedEmojiSoundsConfiguration.with(appConfiguration: appConfiguration, account: item.context.account) for (emoji, file) in emojiSounds.sounds { - if emoji.strippedEmoji == text.strippedEmoji { + if emoji.strippedEmoji == textEmoji.strippedEmoji { let mediaManager = item.context.sharedContext.mediaManager let mediaPlayer = MediaPlayer(audioSessionManager: mediaManager.audioSession, postbox: item.context.account.postbox, resourceReference: .standalone(resource: file.resource), streamable: .none, video: false, preferSoftwareDecoding: false, enableSound: true, fetchAutomatically: true, ambient: true) mediaPlayer.togglePlayPause() @@ -1334,24 +1340,24 @@ class ChatMessageAnimatedStickerItemNode: ChatMessageItemView { strongSelf.mediaStatusDisposable.set((mediaPlayer.status |> deliverOnMainQueue).start(next: { [weak self, weak animationNode] status in if let strongSelf = self { - if firstScalar.value == coffin || firstScalar.value == peach { - var haptic: EmojiHaptic - if let current = strongSelf.haptic { - haptic = current - } else { - if beatingHearts.contains(firstScalar.value) { - haptic = HeartbeatHaptic() - } else if firstScalar.value == coffin { - haptic = CoffinHaptic() - } else { - haptic = PeachHaptic() - } - haptic.enabled = true - strongSelf.haptic = haptic - } - if !haptic.active { - haptic.start(time: 0.0) + + var haptic: EmojiHaptic? + if let current = strongSelf.haptic { + haptic = current + } else { + if firstScalar.value == heart { + haptic = HeartbeatHaptic() + } else if firstScalar.value == coffin { + haptic = CoffinHaptic() + } else if firstScalar.value == peach { + haptic = PeachHaptic() } + haptic?.enabled = true + strongSelf.haptic = haptic + } + + if let haptic = haptic, !haptic.active { + haptic.start(time: 0.0) } switch status.status { From 191adb951bbece9e0c94f5e4cd314e539ddece1f Mon Sep 17 00:00:00 2001 From: Ilya Laktyushin Date: Tue, 17 Nov 2020 14:12:30 +0400 Subject: [PATCH 7/8] Add "Add Contact" options in contacts search --- .../Sources/ContactsController.swift | 13 + .../Sources/ContactsControllerNode.swift | 7 +- .../Sources/ContactsSearchContainerNode.swift | 242 ++++++++++++------ .../InviteContactsControllerNode.swift | 2 +- .../Sources/ComposeControllerNode.swift | 2 +- .../ContactSelectionControllerNode.swift | 2 +- .../Sources/PeerSelectionControllerNode.swift | 2 +- 7 files changed, 181 insertions(+), 89 deletions(-) diff --git a/submodules/ContactListUI/Sources/ContactsController.swift b/submodules/ContactListUI/Sources/ContactsController.swift index a4bf057f52..eee25e215d 100644 --- a/submodules/ContactListUI/Sources/ContactsController.swift +++ b/submodules/ContactListUI/Sources/ContactsController.swift @@ -279,6 +279,19 @@ public class ContactsController: ViewController { openPeer(peer, false) } + self.contactsNode.requestAddContact = { [weak self] phoneNumber in + if let strongSelf = self { + strongSelf.view.endEditing(true) + strongSelf.context.sharedContext.openAddContact(context: strongSelf.context, firstName: "", lastName: "", phoneNumber: phoneNumber, label: defaultContactLabel, present: { [weak self] controller, arguments in + self?.present(controller, in: .window(.root), with: arguments) + }, pushController: { [weak self] controller in + (self?.navigationController as? NavigationController)?.pushViewController(controller) + }, completed: { + self?.deactivateSearch(animated: false) + }) + } + } + self.contactsNode.openPeopleNearby = { [weak self] in let _ = (DeviceAccess.authorizationStatus(subject: .location(.tracking)) |> take(1) diff --git a/submodules/ContactListUI/Sources/ContactsControllerNode.swift b/submodules/ContactListUI/Sources/ContactsControllerNode.swift index 77c3cc9e16..4259dfb280 100644 --- a/submodules/ContactListUI/Sources/ContactsControllerNode.swift +++ b/submodules/ContactListUI/Sources/ContactsControllerNode.swift @@ -55,6 +55,7 @@ final class ContactsControllerNode: ASDisplayNode { var requestDeactivateSearch: (() -> Void)? var requestOpenPeerFromSearch: ((ContactListPeer) -> Void)? + var requestAddContact: ((String) -> Void)? var openPeopleNearby: (() -> Void)? var openInvite: (() -> Void)? @@ -184,7 +185,11 @@ final class ContactsControllerNode: ASDisplayNode { return } - self.searchDisplayController = SearchDisplayController(presentationData: self.presentationData, contentNode: ContactsSearchContainerNode(context: self.context, onlyWriteable: false, categories: [.cloudContacts, .global, .deviceContacts], openPeer: { [weak self] peer in + self.searchDisplayController = SearchDisplayController(presentationData: self.presentationData, contentNode: ContactsSearchContainerNode(context: self.context, onlyWriteable: false, categories: [.cloudContacts, .global, .deviceContacts], addContact: { [weak self] phoneNumber in + if let requestAddContact = self?.requestAddContact { + requestAddContact(phoneNumber) + } + }, openPeer: { [weak self] peer in if let requestOpenPeerFromSearch = self?.requestOpenPeerFromSearch { requestOpenPeerFromSearch(peer) } diff --git a/submodules/ContactListUI/Sources/ContactsSearchContainerNode.swift b/submodules/ContactListUI/Sources/ContactsSearchContainerNode.swift index c617854aba..6d4932bf54 100644 --- a/submodules/ContactListUI/Sources/ContactsSearchContainerNode.swift +++ b/submodules/ContactListUI/Sources/ContactsSearchContainerNode.swift @@ -23,93 +23,151 @@ private enum ContactListSearchGroup { case deviceContacts } -private struct ContactListSearchEntry: Identifiable, Comparable { - let index: Int - let theme: PresentationTheme - let strings: PresentationStrings - let peer: ContactListPeer - let presence: PeerPresence? - let group: ContactListSearchGroup - let enabled: Bool +private enum ContactListSearchEntryId: Hashable { + case addContact + case peerId(ContactListPeerId) - var stableId: ContactListPeerId { - return self.peer.id + static func <(lhs: ContactListSearchEntryId, rhs: ContactListSearchEntryId) -> Bool { + return lhs.hashValue < rhs.hashValue + } + + static func ==(lhs: ContactListSearchEntryId, rhs: ContactListSearchEntryId) -> Bool { + switch lhs { + case .addContact: + switch rhs { + case .addContact: + return true + default: + return false + } + case let .peerId(lhsId): + switch rhs { + case let .peerId(rhsId): + return lhsId == rhsId + default: + return false + } + } + } +} + +private enum ContactListSearchEntry: Comparable, Identifiable { + case addContact(PresentationTheme, PresentationStrings, String) + case peer(Int, PresentationTheme, PresentationStrings, ContactListPeer, PeerPresence?, ContactListSearchGroup, Bool) + + var stableId: ContactListSearchEntryId { + switch self { + case .addContact: + return .addContact + case let .peer(_, _, _, peer, _, _, _): + return .peerId(peer.id) + } } static func ==(lhs: ContactListSearchEntry, rhs: ContactListSearchEntry) -> Bool { - if lhs.index != rhs.index { - return false + switch lhs { + case let .addContact(lhsTheme, lhsStrings, lhsPhoneNumber): + if case let .addContact(rhsTheme, rhsStrings, rhsPhoneNumber) = rhs, lhsTheme === rhsTheme, lhsStrings === rhsStrings, lhsPhoneNumber == rhsPhoneNumber { + return true + } else { + return false + } + case let .peer(lhsIndex, lhsTheme, lhsStrings, lhsPeer, lhsPresence, lhsGroup, lhsEnabled): + switch rhs { + case let .peer(rhsIndex, rhsTheme, rhsStrings, rhsPeer, rhsPresence, rhsGroup, rhsEnabled): + if lhsIndex != rhsIndex { + return false + } + if lhsTheme !== rhsTheme { + return false + } + if lhsStrings !== rhsStrings { + return false + } + if lhsPeer != rhsPeer { + return false + } + if let lhsPresence = lhsPresence, let rhsPresence = rhsPresence { + if !lhsPresence.isEqual(to: rhsPresence) { + return false + } + } else if (lhsPresence != nil) != (rhsPresence != nil) { + return false + } + if lhsGroup != rhsGroup { + return false + } + if lhsEnabled != rhsEnabled { + return false + } + return true + default: + return false + } } - if lhs.theme !== rhs.theme { - return false - } - if lhs.strings !== rhs.strings { - return false - } - if lhs.peer != rhs.peer { - return false - } - if let lhsPresence = lhs.presence, let rhsPresence = rhs.presence { - if !lhsPresence.isEqual(to: rhsPresence) { - return false - } - } else if (lhs.presence != nil) != (rhs.presence != nil) { - return false - } - if lhs.group != rhs.group { - return false - } - if lhs.enabled != rhs.enabled { - return false - } - return true } - + static func <(lhs: ContactListSearchEntry, rhs: ContactListSearchEntry) -> Bool { - return lhs.index < rhs.index + switch lhs { + case .addContact: + return true + case let .peer(lhsIndex, _, _, _, _, _, _): + switch rhs { + case .addContact: + return false + case let .peer(rhsIndex, _, _, _, _, _, _): + return lhsIndex < rhsIndex + } + } } - func item(context: AccountContext, presentationData: PresentationData, nameSortOrder: PresentationPersonNameOrder, nameDisplayOrder: PresentationPersonNameOrder, timeFormat: PresentationDateTimeFormat, openPeer: @escaping (ContactListPeer) -> Void, contextAction: ((Peer, ASDisplayNode, ContextGesture?) -> Void)?) -> ListViewItem { - let header: ListViewItemHeader - let status: ContactsPeerItemStatus - switch self.group { - case .contacts: - header = ChatListSearchItemHeader(type: .contacts, theme: self.theme, strings: self.strings, actionTitle: nil, action: nil) - if let presence = self.presence { - status = .presence(presence, timeFormat) - } else { - status = .none + func item(context: AccountContext, presentationData: PresentationData, nameSortOrder: PresentationPersonNameOrder, nameDisplayOrder: PresentationPersonNameOrder, timeFormat: PresentationDateTimeFormat, addContact: ((String) -> Void)?, openPeer: @escaping (ContactListPeer) -> Void, contextAction: ((Peer, ASDisplayNode, ContextGesture?) -> Void)?) -> ListViewItem { + switch self { + case let .addContact(theme, strings, phoneNumber): + return ContactsAddItem(theme: theme, strings: strings, phoneNumber: phoneNumber, header: ChatListSearchItemHeader(type: .phoneNumber, theme: theme, strings: strings, actionTitle: nil, action: nil), action: { + addContact?(phoneNumber) + }) + case let .peer(_, theme, strings, peer, presence, group, enabled): + let header: ListViewItemHeader + let status: ContactsPeerItemStatus + switch group { + case .contacts: + header = ChatListSearchItemHeader(type: .contacts, theme: theme, strings: strings, actionTitle: nil, action: nil) + if let presence = presence { + status = .presence(presence, timeFormat) + } else { + status = .none + } + case .global: + header = ChatListSearchItemHeader(type: .globalPeers, theme: theme, strings: strings, actionTitle: nil, action: nil) + if case let .peer(peer, _, _) = peer, let _ = peer.addressName { + status = .addressName("") + } else { + status = .none + } + case .deviceContacts: + header = ChatListSearchItemHeader(type: .deviceContacts, theme: theme, strings: strings, actionTitle: nil, action: nil) + status = .none } - case .global: - header = ChatListSearchItemHeader(type: .globalPeers, theme: self.theme, strings: self.strings, actionTitle: nil, action: nil) - if case let .peer(peer, _, _) = self.peer, let _ = peer.addressName { - status = .addressName("") - } else { - status = .none + var nativePeer: Peer? + let peerItem: ContactsPeerItemPeer + switch peer { + case let .peer(peer, _, _): + peerItem = .peer(peer: peer, chatPeer: peer) + nativePeer = peer + case let .deviceContact(stableId, contact): + peerItem = .deviceContact(stableId: stableId, contact: contact) } - case .deviceContacts: - header = ChatListSearchItemHeader(type: .deviceContacts, theme: self.theme, strings: self.strings, actionTitle: nil, action: nil) - status = .none + return ContactsPeerItem(presentationData: ItemListPresentationData(presentationData), sortOrder: nameSortOrder, displayOrder: nameDisplayOrder, context: context, peerMode: .peer, peer: peerItem, status: status, enabled: enabled, selection: .none, editing: ContactsPeerItemEditing(editable: false, editing: false, revealed: false), index: nil, header: header, action: { _ in + openPeer(peer) + }, contextAction: contextAction.flatMap { contextAction in + return nativePeer.flatMap { nativePeer in + return { node, gesture in + contextAction(nativePeer, node, gesture) + } + } + }) } - let peer = self.peer - var nativePeer: Peer? - let peerItem: ContactsPeerItemPeer - switch peer { - case let .peer(peer, _, _): - peerItem = .peer(peer: peer, chatPeer: peer) - nativePeer = peer - case let .deviceContact(stableId, contact): - peerItem = .deviceContact(stableId: stableId, contact: contact) - } - return ContactsPeerItem(presentationData: ItemListPresentationData(presentationData), sortOrder: nameSortOrder, displayOrder: nameDisplayOrder, context: context, peerMode: .peer, peer: peerItem, status: status, enabled: self.enabled, selection: .none, editing: ContactsPeerItemEditing(editable: false, editing: false, revealed: false), index: nil, header: header, action: { _ in - openPeer(peer) - }, contextAction: contextAction.flatMap { contextAction in - return nativePeer.flatMap { nativePeer in - return { node, gesture in - contextAction(nativePeer, node, gesture) - } - } - }) } } @@ -120,12 +178,12 @@ struct ContactListSearchContainerTransition { let isSearching: Bool } -private func contactListSearchContainerPreparedRecentTransition(from fromEntries: [ContactListSearchEntry], to toEntries: [ContactListSearchEntry], isSearching: Bool, context: AccountContext, presentationData: PresentationData, nameSortOrder: PresentationPersonNameOrder, nameDisplayOrder: PresentationPersonNameOrder, timeFormat: PresentationDateTimeFormat, openPeer: @escaping (ContactListPeer) -> Void, contextAction: ((Peer, ASDisplayNode, ContextGesture?) -> Void)?) -> ContactListSearchContainerTransition { +private func contactListSearchContainerPreparedRecentTransition(from fromEntries: [ContactListSearchEntry], to toEntries: [ContactListSearchEntry], isSearching: Bool, context: AccountContext, presentationData: PresentationData, nameSortOrder: PresentationPersonNameOrder, nameDisplayOrder: PresentationPersonNameOrder, timeFormat: PresentationDateTimeFormat, addContact: ((String) -> Void)?, openPeer: @escaping (ContactListPeer) -> Void, contextAction: ((Peer, ASDisplayNode, ContextGesture?) -> Void)?) -> ContactListSearchContainerTransition { let (deleteIndices, indicesAndItems, updateIndices) = mergeListsStableWithUpdates(leftList: fromEntries, rightList: toEntries) let deletions = deleteIndices.map { ListViewDeleteItem(index: $0, directionHint: nil) } - let insertions = indicesAndItems.map { ListViewInsertItem(index: $0.0, previousIndex: $0.2, item: $0.1.item(context: context, presentationData: presentationData, nameSortOrder: nameSortOrder, nameDisplayOrder: nameDisplayOrder, timeFormat: timeFormat, openPeer: openPeer, contextAction: contextAction), directionHint: nil) } - let updates = updateIndices.map { ListViewUpdateItem(index: $0.0, previousIndex: $0.2, item: $0.1.item(context: context, presentationData: presentationData, nameSortOrder: nameSortOrder, nameDisplayOrder: nameDisplayOrder, timeFormat: timeFormat, openPeer: openPeer, contextAction: contextAction), directionHint: nil) } + let insertions = indicesAndItems.map { ListViewInsertItem(index: $0.0, previousIndex: $0.2, item: $0.1.item(context: context, presentationData: presentationData, nameSortOrder: nameSortOrder, nameDisplayOrder: nameDisplayOrder, timeFormat: timeFormat, addContact: addContact, openPeer: openPeer, contextAction: contextAction), directionHint: nil) } + let updates = updateIndices.map { ListViewUpdateItem(index: $0.0, previousIndex: $0.2, item: $0.1.item(context: context, presentationData: presentationData, nameSortOrder: nameSortOrder, nameDisplayOrder: nameDisplayOrder, timeFormat: timeFormat, addContact: addContact, openPeer: openPeer, contextAction: contextAction), directionHint: nil) } return ContactListSearchContainerTransition(deletions: deletions, insertions: insertions, updates: updates, isSearching: isSearching) } @@ -144,6 +202,7 @@ public struct ContactsSearchCategories: OptionSet { public final class ContactsSearchContainerNode: SearchDisplayControllerContentNode { private let context: AccountContext + private let addContact: ((String) -> Void)? private let openPeer: (ContactListPeer) -> Void private let contextAction: ((Peer, ASDisplayNode, ContextGesture?) -> Void)? @@ -163,8 +222,9 @@ public final class ContactsSearchContainerNode: SearchDisplayControllerContentNo return true } - public init(context: AccountContext, onlyWriteable: Bool, categories: ContactsSearchCategories, filters: [ContactListFilter] = [.excludeSelf], openPeer: @escaping (ContactListPeer) -> Void, contextAction: ((Peer, ASDisplayNode, ContextGesture?) -> Void)?) { + public init(context: AccountContext, onlyWriteable: Bool, categories: ContactsSearchCategories, filters: [ContactListFilter] = [.excludeSelf], addContact: ((String) -> Void)?, openPeer: @escaping (ContactListPeer) -> Void, contextAction: ((Peer, ASDisplayNode, ContextGesture?) -> Void)?) { self.context = context + self.addContact = addContact self.openPeer = openPeer self.contextAction = contextAction @@ -250,7 +310,7 @@ public final class ContactsSearchContainerNode: SearchDisplayControllerContentNo if onlyWriteable { enabled = canSendMessagesToPeer(peer) } - entries.append(ContactListSearchEntry(index: index, theme: themeAndStrings.0, strings: themeAndStrings.1, peer: .peer(peer: peer, isGlobal: false, participantCount: nil), presence: localPeersAndPresences.1[peer.id], group: .contacts, enabled: enabled)) + entries.append(.peer(index, themeAndStrings.0, themeAndStrings.1, .peer(peer: peer, isGlobal: false, participantCount: nil), localPeersAndPresences.1[peer.id], .contacts, enabled)) if searchDeviceContacts, let user = peer as? TelegramUser, let phone = user.phone { existingNormalizedPhoneNumbers.insert(DeviceContactNormalizedPhoneNumber(rawValue: formatPhoneNumber(phone))) } @@ -269,7 +329,7 @@ public final class ContactsSearchContainerNode: SearchDisplayControllerContentNo enabled = canSendMessagesToPeer(peer.peer) } - entries.append(ContactListSearchEntry(index: index, theme: themeAndStrings.0, strings: themeAndStrings.1, peer: .peer(peer: peer.peer, isGlobal: true, participantCount: peer.subscribers), presence: nil, group: .global, enabled: enabled)) + entries.append(.peer(index, themeAndStrings.0, themeAndStrings.1, .peer(peer: peer.peer, isGlobal: true, participantCount: peer.subscribers), nil, .global, enabled)) if searchDeviceContacts, let user = peer.peer as? TelegramUser, let phone = user.phone { existingNormalizedPhoneNumbers.insert(DeviceContactNormalizedPhoneNumber(rawValue: formatPhoneNumber(phone))) } @@ -288,7 +348,7 @@ public final class ContactsSearchContainerNode: SearchDisplayControllerContentNo enabled = canSendMessagesToPeer(peer.peer) } - entries.append(ContactListSearchEntry(index: index, theme: themeAndStrings.0, strings: themeAndStrings.1, peer: .peer(peer: peer.peer, isGlobal: true, participantCount: peer.subscribers), presence: nil, group: .global, enabled: enabled)) + entries.append(.peer(index, themeAndStrings.0, themeAndStrings.1, .peer(peer: peer.peer, isGlobal: true, participantCount: peer.subscribers), nil, .global, enabled)) if searchDeviceContacts, let user = peer.peer as? TelegramUser, let phone = user.phone { existingNormalizedPhoneNumbers.insert(DeviceContactNormalizedPhoneNumber(rawValue: formatPhoneNumber(phone))) } @@ -309,10 +369,15 @@ public final class ContactsSearchContainerNode: SearchDisplayControllerContentNo continue outer } } - entries.append(ContactListSearchEntry(index: index, theme: themeAndStrings.0, strings: themeAndStrings.1, peer: .deviceContact(stableId, contact.0), presence: nil, group: .deviceContacts, enabled: true)) + entries.append(.peer(index, themeAndStrings.0, themeAndStrings.1, .deviceContact(stableId, contact.0), nil, .deviceContacts, true)) index += 1 } } + + if let _ = addContact, isViablePhoneNumber(query) { + entries.append(.addContact(themeAndStrings.0, themeAndStrings.1, query)) + } + return entries } } else { @@ -328,7 +393,16 @@ public final class ContactsSearchContainerNode: SearchDisplayControllerContentNo if let strongSelf = self { let previousItems = previousSearchItems.swap(items ?? []) - let transition = contactListSearchContainerPreparedRecentTransition(from: previousItems, to: items ?? [], isSearching: items != nil, context: context, presentationData: strongSelf.presentationData, nameSortOrder: strongSelf.presentationData.nameSortOrder, nameDisplayOrder: strongSelf.presentationData.nameDisplayOrder, timeFormat: strongSelf.presentationData.dateTimeFormat, openPeer: { peer in self?.listNode.clearHighlightAnimated(true) + var addContact: ((String) -> Void)? + if let originalAddContact = strongSelf.addContact { + addContact = { [weak self] phoneNumber in + self?.listNode.clearHighlightAnimated(true) + originalAddContact(phoneNumber) + } + } + + let transition = contactListSearchContainerPreparedRecentTransition(from: previousItems, to: items ?? [], isSearching: items != nil, context: context, presentationData: strongSelf.presentationData, nameSortOrder: strongSelf.presentationData.nameSortOrder, nameDisplayOrder: strongSelf.presentationData.nameDisplayOrder, timeFormat: strongSelf.presentationData.dateTimeFormat, addContact: addContact, openPeer: { peer in + self?.listNode.clearHighlightAnimated(true) self?.openPeer(peer) }, contextAction: strongSelf.contextAction) diff --git a/submodules/ContactListUI/Sources/InviteContactsControllerNode.swift b/submodules/ContactListUI/Sources/InviteContactsControllerNode.swift index 5f06466c78..78ec2a62c8 100644 --- a/submodules/ContactListUI/Sources/InviteContactsControllerNode.swift +++ b/submodules/ContactListUI/Sources/InviteContactsControllerNode.swift @@ -505,7 +505,7 @@ final class InviteContactsControllerNode: ASDisplayNode { return } - self.searchDisplayController = SearchDisplayController(presentationData: self.presentationData, contentNode: ContactsSearchContainerNode(context: self.context, onlyWriteable: false, categories: [.deviceContacts], openPeer: { [weak self] peer in + self.searchDisplayController = SearchDisplayController(presentationData: self.presentationData, contentNode: ContactsSearchContainerNode(context: self.context, onlyWriteable: false, categories: [.deviceContacts], addContact: nil, openPeer: { [weak self] peer in if let strongSelf = self, case let .deviceContact(id, _) = peer { strongSelf.selectionState = strongSelf.selectionState.withSelectedContactId(id) strongSelf.requestDeactivateSearch?() diff --git a/submodules/TelegramUI/Sources/ComposeControllerNode.swift b/submodules/TelegramUI/Sources/ComposeControllerNode.swift index 473daf26c4..f73ebc7945 100644 --- a/submodules/TelegramUI/Sources/ComposeControllerNode.swift +++ b/submodules/TelegramUI/Sources/ComposeControllerNode.swift @@ -124,7 +124,7 @@ final class ComposeControllerNode: ASDisplayNode { return } - self.searchDisplayController = SearchDisplayController(presentationData: self.presentationData, contentNode: ContactsSearchContainerNode(context: self.context, onlyWriteable: false, categories: [.cloudContacts, .global], openPeer: { [weak self] peer in + self.searchDisplayController = SearchDisplayController(presentationData: self.presentationData, contentNode: ContactsSearchContainerNode(context: self.context, onlyWriteable: false, categories: [.cloudContacts, .global], addContact: nil, openPeer: { [weak self] peer in if let requestOpenPeerFromSearch = self?.requestOpenPeerFromSearch, case let .peer(peer, _, _) = peer { requestOpenPeerFromSearch(peer.id) } diff --git a/submodules/TelegramUI/Sources/ContactSelectionControllerNode.swift b/submodules/TelegramUI/Sources/ContactSelectionControllerNode.swift index baa6cf8b3d..da4349b7df 100644 --- a/submodules/TelegramUI/Sources/ContactSelectionControllerNode.swift +++ b/submodules/TelegramUI/Sources/ContactSelectionControllerNode.swift @@ -123,7 +123,7 @@ final class ContactSelectionControllerNode: ASDisplayNode { } else { categories.insert(.global) } - self.searchDisplayController = SearchDisplayController(presentationData: self.presentationData, contentNode: ContactsSearchContainerNode(context: self.context, onlyWriteable: false, categories: categories, openPeer: { [weak self] peer in + self.searchDisplayController = SearchDisplayController(presentationData: self.presentationData, contentNode: ContactsSearchContainerNode(context: self.context, onlyWriteable: false, categories: categories, addContact: nil, openPeer: { [weak self] peer in self?.requestOpenPeerFromSearch?(peer) }, contextAction: nil), cancel: { [weak self] in if let requestDeactivateSearch = self?.requestDeactivateSearch { diff --git a/submodules/TelegramUI/Sources/PeerSelectionControllerNode.swift b/submodules/TelegramUI/Sources/PeerSelectionControllerNode.swift index 49b38ade7b..9fbb131130 100644 --- a/submodules/TelegramUI/Sources/PeerSelectionControllerNode.swift +++ b/submodules/TelegramUI/Sources/PeerSelectionControllerNode.swift @@ -248,7 +248,7 @@ final class PeerSelectionControllerNode: ASDisplayNode { }, placeholder: placeholderNode) } else if let contactListNode = self.contactListNode, contactListNode.supernode != nil { - self.searchDisplayController = SearchDisplayController(presentationData: self.presentationData, contentNode: ContactsSearchContainerNode(context: self.context, onlyWriteable: true, categories: [.cloudContacts, .global], openPeer: { [weak self] peer in + self.searchDisplayController = SearchDisplayController(presentationData: self.presentationData, contentNode: ContactsSearchContainerNode(context: self.context, onlyWriteable: true, categories: [.cloudContacts, .global], addContact: nil, openPeer: { [weak self] peer in if let strongSelf = self { switch peer { case let .peer(peer, _, _): From cf913d65d8c7eed8d97b8b7d79beabe37d6059ba Mon Sep 17 00:00:00 2001 From: Ilya Laktyushin Date: Tue, 17 Nov 2020 16:16:12 +0400 Subject: [PATCH 8/8] Fix keyboard height calculation when Prefer Cross-Fade Transitions is enabled on iOS 14.2+ --- submodules/Display/Source/WindowContent.swift | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/submodules/Display/Source/WindowContent.swift b/submodules/Display/Source/WindowContent.swift index dba854e0fe..91db3affaa 100644 --- a/submodules/Display/Source/WindowContent.swift +++ b/submodules/Display/Source/WindowContent.swift @@ -442,12 +442,14 @@ public class Window1 { self.keyboardFrameChangeObserver = NotificationCenter.default.addObserver(forName: UIResponder.keyboardWillChangeFrameNotification, object: nil, queue: nil, using: { [weak self] notification in if let strongSelf = self { var keyboardFrame: CGRect = (notification.userInfo?[UIResponder.keyboardFrameEndUserInfoKey] as? NSValue)?.cgRectValue ?? CGRect() - if let keyboardView = strongSelf.statusBarHost?.keyboardView { + if #available(iOSApplicationExtension 14.2, iOS 14.2, *), UIAccessibility.prefersCrossFadeTransitions { + } else if let keyboardView = strongSelf.statusBarHost?.keyboardView { if keyboardFrame.width.isEqual(to: keyboardView.bounds.width) && keyboardFrame.height.isEqual(to: keyboardView.bounds.height) && keyboardFrame.minX.isEqual(to: keyboardView.frame.minX) { keyboardFrame.origin.y = keyboardView.frame.minY } } + var popoverDelta: CGFloat = 0.0 let screenHeight: CGFloat @@ -493,11 +495,7 @@ public class Window1 { } else { keyboardHeight = max(0.0, screenHeight - keyboardFrame.minY) if inPopover && !keyboardHeight.isZero { - if #available(iOSApplicationExtension 13.0, iOS 13.0, *) { - keyboardHeight = max(0.0, keyboardHeight - popoverDelta) - } else { - keyboardHeight = max(0.0, keyboardHeight - popoverDelta) - } + keyboardHeight = max(0.0, keyboardHeight - popoverDelta) } }