diff --git a/Makefile b/Makefile index 80c8001593..55d71031c0 100644 --- a/Makefile +++ b/Makefile @@ -3,7 +3,7 @@ include Utils.makefile -APP_VERSION="6.3" +APP_VERSION="6.3.1" CORE_COUNT=$(shell sysctl -n hw.logicalcpu) CORE_COUNT_MINUS_ONE=$(shell expr ${CORE_COUNT} \- 1) diff --git a/submodules/AccountContext/Sources/ChatListController.swift b/submodules/AccountContext/Sources/ChatListController.swift index ad9ac4ee69..ec9fb811e8 100644 --- a/submodules/AccountContext/Sources/ChatListController.swift +++ b/submodules/AccountContext/Sources/ChatListController.swift @@ -10,5 +10,5 @@ public protocol ChatListController: ViewController { func activateSearch() func deactivateSearch(animated: Bool) func activateCompose() - func maybeAskForPeerChatRemoval(peer: RenderedPeer, deleteGloballyIfPossible: Bool, completion: @escaping (Bool) -> Void, removed: @escaping () -> Void) + func maybeAskForPeerChatRemoval(peer: RenderedPeer, joined: Bool, deleteGloballyIfPossible: Bool, completion: @escaping (Bool) -> Void, removed: @escaping () -> Void) } diff --git a/submodules/AccountContext/Sources/PresentationCallManager.swift b/submodules/AccountContext/Sources/PresentationCallManager.swift index f620a90761..3cc62937f2 100644 --- a/submodules/AccountContext/Sources/PresentationCallManager.swift +++ b/submodules/AccountContext/Sources/PresentationCallManager.swift @@ -69,15 +69,29 @@ public struct PresentationCallState: Equatable { } public final class PresentationCallVideoView { + public enum Orientation { + case rotation0 + case rotation90 + case rotation180 + case rotation270 + } + public let view: UIView public let setOnFirstFrameReceived: ((() -> Void)?) -> Void + public let getOrientation: () -> Orientation + public let setOnOrientationUpdated: (((Orientation) -> Void)?) -> Void + public init( view: UIView, - setOnFirstFrameReceived: @escaping ((() -> Void)?) -> Void + setOnFirstFrameReceived: @escaping ((() -> Void)?) -> Void, + getOrientation: @escaping () -> Orientation, + setOnOrientationUpdated: @escaping (((Orientation) -> Void)?) -> Void ) { self.view = view self.setOnFirstFrameReceived = setOnFirstFrameReceived + self.getOrientation = getOrientation + self.setOnOrientationUpdated = setOnOrientationUpdated } } diff --git a/submodules/AnimatedStickerNode/Sources/AnimatedStickerNode.swift b/submodules/AnimatedStickerNode/Sources/AnimatedStickerNode.swift index 84445b572e..887a0cee0a 100644 --- a/submodules/AnimatedStickerNode/Sources/AnimatedStickerNode.swift +++ b/submodules/AnimatedStickerNode/Sources/AnimatedStickerNode.swift @@ -52,6 +52,7 @@ public enum AnimatedStickerMode { public enum AnimatedStickerPlaybackPosition { case start case end + case timestamp(Double) } public enum AnimatedStickerPlaybackMode { @@ -83,8 +84,9 @@ public final class AnimatedStickerFrame { public protocol AnimatedStickerFrameSource: class { var frameRate: Int { get } var frameCount: Int { get } + var frameIndex: Int { get } - func takeFrame() -> AnimatedStickerFrame? + func takeFrame(draw: Bool) -> AnimatedStickerFrame? func skipToEnd() } @@ -109,7 +111,7 @@ public final class AnimatedStickerCachedFrameSource: AnimatedStickerFrameSource let height: Int public let frameRate: Int public let frameCount: Int - private var frameIndex: Int + public var frameIndex: Int private let initialOffset: Int private var offset: Int var decodeBuffer: Data @@ -179,7 +181,7 @@ public final class AnimatedStickerCachedFrameSource: AnimatedStickerFrameSource assert(self.queue.isCurrent()) } - public func takeFrame() -> AnimatedStickerFrame? { + public func takeFrame(draw: Bool) -> AnimatedStickerFrame? { var frameData: Data? var isLastFrame = false @@ -210,27 +212,29 @@ public final class AnimatedStickerCachedFrameSource: AnimatedStickerFrameSource self.offset += 4 - self.scratchBuffer.withUnsafeMutableBytes { (scratchBytes: UnsafeMutablePointer) -> Void in - self.decodeBuffer.withUnsafeMutableBytes { (decodeBytes: UnsafeMutablePointer) -> Void in - self.frameBuffer.withUnsafeMutableBytes { (frameBytes: UnsafeMutablePointer) -> Void in - compression_decode_buffer(decodeBytes, decodeBufferLength, bytes.advanced(by: self.offset), Int(frameLength), UnsafeMutableRawPointer(scratchBytes), COMPRESSION_LZFSE) - - var lhs = UnsafeMutableRawPointer(frameBytes).assumingMemoryBound(to: UInt64.self) - var rhs = UnsafeRawPointer(decodeBytes).assumingMemoryBound(to: UInt64.self) - for _ in 0 ..< decodeBufferLength / 8 { - lhs.pointee = lhs.pointee ^ rhs.pointee - lhs = lhs.advanced(by: 1) - rhs = rhs.advanced(by: 1) + if draw { + self.scratchBuffer.withUnsafeMutableBytes { (scratchBytes: UnsafeMutablePointer) -> Void in + self.decodeBuffer.withUnsafeMutableBytes { (decodeBytes: UnsafeMutablePointer) -> Void in + self.frameBuffer.withUnsafeMutableBytes { (frameBytes: UnsafeMutablePointer) -> Void in + compression_decode_buffer(decodeBytes, decodeBufferLength, bytes.advanced(by: self.offset), Int(frameLength), UnsafeMutableRawPointer(scratchBytes), COMPRESSION_LZFSE) + + var lhs = UnsafeMutableRawPointer(frameBytes).assumingMemoryBound(to: UInt64.self) + var rhs = UnsafeRawPointer(decodeBytes).assumingMemoryBound(to: UInt64.self) + for _ in 0 ..< decodeBufferLength / 8 { + lhs.pointee = lhs.pointee ^ rhs.pointee + lhs = lhs.advanced(by: 1) + rhs = rhs.advanced(by: 1) + } + var lhsRest = UnsafeMutableRawPointer(frameBytes).assumingMemoryBound(to: UInt8.self).advanced(by: (decodeBufferLength / 8) * 8) + var rhsRest = UnsafeMutableRawPointer(decodeBytes).assumingMemoryBound(to: UInt8.self).advanced(by: (decodeBufferLength / 8) * 8) + for _ in (decodeBufferLength / 8) * 8 ..< decodeBufferLength { + lhsRest.pointee = rhsRest.pointee ^ lhsRest.pointee + lhsRest = lhsRest.advanced(by: 1) + rhsRest = rhsRest.advanced(by: 1) + } + + frameData = Data(bytes: frameBytes, count: decodeBufferLength) } - var lhsRest = UnsafeMutableRawPointer(frameBytes).assumingMemoryBound(to: UInt8.self).advanced(by: (decodeBufferLength / 8) * 8) - var rhsRest = UnsafeMutableRawPointer(decodeBytes).assumingMemoryBound(to: UInt8.self).advanced(by: (decodeBufferLength / 8) * 8) - for _ in (decodeBufferLength / 8) * 8 ..< decodeBufferLength { - lhsRest.pointee = rhsRest.pointee ^ lhsRest.pointee - lhsRest = lhsRest.advanced(by: 1) - rhsRest = rhsRest.advanced(by: 1) - } - - frameData = Data(bytes: frameBytes, count: decodeBufferLength) } } } @@ -247,7 +251,7 @@ public final class AnimatedStickerCachedFrameSource: AnimatedStickerFrameSource } } - if let frameData = frameData { + if let frameData = frameData, draw { return AnimatedStickerFrame(data: frameData, type: .yuva, width: self.width, height: self.height, bytesPerRow: self.bytesPerRow, index: frameIndex, isLastFrame: isLastFrame) } else { return nil @@ -271,9 +275,13 @@ private final class AnimatedStickerDirectFrameSource: AnimatedStickerFrameSource private let bytesPerRow: Int let frameCount: Int let frameRate: Int - private var currentFrame: Int + fileprivate var currentFrame: Int private let animation: LottieInstance + var frameIndex: Int { + return self.currentFrame % self.frameCount + } + init?(queue: Queue, data: Data, width: Int, height: Int) { self.queue = queue self.data = data @@ -294,15 +302,19 @@ private final class AnimatedStickerDirectFrameSource: AnimatedStickerFrameSource assert(self.queue.isCurrent()) } - func takeFrame() -> AnimatedStickerFrame? { + func takeFrame(draw: Bool) -> AnimatedStickerFrame? { let frameIndex = self.currentFrame % self.frameCount self.currentFrame += 1 - var frameData = Data(count: self.bytesPerRow * self.height) - frameData.withUnsafeMutableBytes { (bytes: UnsafeMutablePointer) -> Void in - memset(bytes, 0, self.bytesPerRow * self.height) - self.animation.renderFrame(with: Int32(frameIndex), into: bytes, width: Int32(self.width), height: Int32(self.height), bytesPerRow: Int32(self.bytesPerRow)) + if draw { + var frameData = Data(count: self.bytesPerRow * self.height) + frameData.withUnsafeMutableBytes { (bytes: UnsafeMutablePointer) -> Void in + memset(bytes, 0, self.bytesPerRow * self.height) + self.animation.renderFrame(with: Int32(frameIndex), into: bytes, width: Int32(self.width), height: Int32(self.height), bytesPerRow: Int32(self.bytesPerRow)) + } + return AnimatedStickerFrame(data: frameData, type: .argb, width: self.width, height: self.height, bytesPerRow: self.bytesPerRow, index: frameIndex, isLastFrame: frameIndex == self.frameCount - 1) + } else { + return nil } - return AnimatedStickerFrame(data: frameData, type: .argb, width: self.width, height: self.height, bytesPerRow: self.bytesPerRow, index: frameIndex, isLastFrame: frameIndex == self.frameCount - 1) } func skipToEnd() { @@ -326,9 +338,9 @@ public final class AnimatedStickerFrameQueue { assert(self.queue.isCurrent()) } - public func take() -> AnimatedStickerFrame? { + public func take(draw: Bool) -> AnimatedStickerFrame? { if self.frames.isEmpty { - if let frame = self.source.takeFrame() { + if let frame = self.source.takeFrame(draw: draw) { self.frames.append(frame) } } @@ -342,7 +354,7 @@ public final class AnimatedStickerFrameQueue { public func generateFramesIfNeeded() { if self.frames.isEmpty { - if let frame = self.source.takeFrame() { + if let frame = self.source.takeFrame(draw: true) { self.frames.append(frame) } } @@ -583,7 +595,7 @@ public final class AnimatedStickerNode: ASDisplayNode { let timer = SwiftSignalKit.Timer(timeout: 1.0 / Double(frameRate), repeat: !firstFrame, completion: { let maybeFrame = frameQueue.syncWith { frameQueue in - return frameQueue.take() + return frameQueue.take(draw: true) } if let maybeFrame = maybeFrame, let frame = maybeFrame { Queue.mainQueue().async { @@ -654,7 +666,7 @@ public final class AnimatedStickerNode: ASDisplayNode { let timer = SwiftSignalKit.Timer(timeout: 1.0 / Double(frameRate), repeat: !firstFrame, completion: { let maybeFrame = frameQueue.syncWith { frameQueue in - return frameQueue.take() + return frameQueue.take(draw: true) } if let maybeFrame = maybeFrame, let frame = maybeFrame { Queue.mainQueue().async { @@ -710,19 +722,25 @@ public final class AnimatedStickerNode: ASDisplayNode { let directData = self.directData let cachedData = self.cachedData let queue = self.queue + let frameSourceHolder = self.frameSource let timerHolder = self.timer self.queue.async { [weak self] in - var maybeFrameSource: AnimatedStickerFrameSource? - if let directData = directData { - maybeFrameSource = AnimatedStickerDirectFrameSource(queue: queue, data: directData.0, width: directData.2, height: directData.3) - if position == .end { - maybeFrameSource?.skipToEnd() - } - } else if let (cachedData, cachedDataComplete) = cachedData { - if #available(iOS 9.0, *) { - maybeFrameSource = AnimatedStickerCachedFrameSource(queue: queue, data: cachedData, complete: cachedDataComplete, notifyUpdated: {}) + var maybeFrameSource: AnimatedStickerFrameSource? = frameSourceHolder.with { $0 }?.syncWith { $0 }?.value + if case .timestamp = position { + } else { + var maybeFrameSource: AnimatedStickerFrameSource? + if let directData = directData { + maybeFrameSource = AnimatedStickerDirectFrameSource(queue: queue, data: directData.0, width: directData.2, height: directData.3) + if case .end = position { + maybeFrameSource?.skipToEnd() + } + } else if let (cachedData, cachedDataComplete) = cachedData { + if #available(iOS 9.0, *) { + maybeFrameSource = AnimatedStickerCachedFrameSource(queue: queue, data: cachedData, complete: cachedDataComplete, notifyUpdated: {}) + } } } + guard let frameSource = maybeFrameSource else { return } @@ -732,9 +750,31 @@ public final class AnimatedStickerNode: ASDisplayNode { timerHolder.swap(nil)?.invalidate() let duration: Double = frameSource.frameRate > 0 ? Double(frameSource.frameCount) / Double(frameSource.frameRate) : 0 - - let maybeFrame = frameQueue.syncWith { frameQueue in - return frameQueue.take() + + var maybeFrame: AnimatedStickerFrame?? + if case let .timestamp(timestamp) = position { + var stickerTimestamp = timestamp + while stickerTimestamp > duration { + stickerTimestamp -= duration + } + let targetFrame = Int(stickerTimestamp / duration * Double(frameSource.frameCount)) + if targetFrame == frameSource.frameIndex { + return + } + + var delta = targetFrame - frameSource.frameIndex + if delta < 0 { + delta = frameSource.frameCount + delta + } + for i in 0 ..< delta { + maybeFrame = frameQueue.syncWith { frameQueue in + return frameQueue.take(draw: i == delta - 1) + } + } + } else { + maybeFrame = frameQueue.syncWith { frameQueue in + return frameQueue.take(draw: true) + } } if let maybeFrame = maybeFrame, let frame = maybeFrame { Queue.mainQueue().async { diff --git a/submodules/ChatListUI/Sources/ChatContextMenus.swift b/submodules/ChatListUI/Sources/ChatContextMenus.swift index 0aaee436a0..7743bc1ad4 100644 --- a/submodules/ChatListUI/Sources/ChatContextMenus.swift +++ b/submodules/ChatListUI/Sources/ChatContextMenus.swift @@ -46,7 +46,7 @@ enum ChatContextMenuSource { case search(ChatListSearchContextActionSource) } -func chatContextMenuItems(context: AccountContext, peerId: PeerId, promoInfo: ChatListNodeEntryPromoInfo?, source: ChatContextMenuSource, chatListController: ChatListControllerImpl?) -> Signal<[ContextMenuItem], NoError> { +func chatContextMenuItems(context: AccountContext, peerId: PeerId, promoInfo: ChatListNodeEntryPromoInfo?, source: ChatContextMenuSource, chatListController: ChatListControllerImpl?, joined: Bool) -> Signal<[ContextMenuItem], NoError> { let presentationData = context.sharedContext.currentPresentationData.with({ $0 }) let strings = presentationData.strings return context.account.postbox.transaction { [weak chatListController] transaction -> [ContextMenuItem] in @@ -233,7 +233,7 @@ func chatContextMenuItems(context: AccountContext, peerId: PeerId, promoInfo: Ch updatedItems.append(.action(ContextMenuActionItem(text: strings.ChatList_Context_Back, icon: { theme in return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Back"), color: theme.contextMenu.primaryColor) }, action: { c, _ in - c.setItems(chatContextMenuItems(context: context, peerId: peerId, promoInfo: promoInfo, source: source, chatListController: chatListController)) + c.setItems(chatContextMenuItems(context: context, peerId: peerId, promoInfo: promoInfo, source: source, chatListController: chatListController, joined: joined)) }))) return updatedItems @@ -379,7 +379,7 @@ func chatContextMenuItems(context: AccountContext, peerId: PeerId, promoInfo: Ch if case .chatList = source, groupAndIndex != nil { items.append(.action(ContextMenuActionItem(text: strings.ChatList_Context_Delete, textColor: .destructive, icon: { theme in generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Delete"), color: theme.contextMenu.destructiveColor) }, action: { _, f in if let chatListController = chatListController { - chatListController.deletePeerChat(peerId: peerId) + chatListController.deletePeerChat(peerId: peerId, joined: joined) } f(.default) }))) diff --git a/submodules/ChatListUI/Sources/ChatListController.swift b/submodules/ChatListUI/Sources/ChatListController.swift index a949a737a9..44e06dca1e 100644 --- a/submodules/ChatListUI/Sources/ChatListController.swift +++ b/submodules/ChatListUI/Sources/ChatListController.swift @@ -575,11 +575,11 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController, strongSelf.hidePsa(peerId) } - self.chatListDisplayNode.containerNode.deletePeerChat = { [weak self] peerId in + self.chatListDisplayNode.containerNode.deletePeerChat = { [weak self] peerId, joined in guard let strongSelf = self else { return } - strongSelf.deletePeerChat(peerId: peerId) + strongSelf.deletePeerChat(peerId: peerId, joined: joined) } self.chatListDisplayNode.containerNode.peerSelected = { [weak self] peer, animated, promoInfo in @@ -801,6 +801,16 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController, gesture?.cancel() return } + + var joined = false + if case let .peer(messages, _, _, _, _, _, _, _, _, _, _, _) = item.content, let message = messages.first { + for media in message.media { + if let action = media as? TelegramMediaAction, action.action == .peerJoined { + joined = true + } + } + } + switch item.content { case let .groupReference(groupReference): let chatListController = ChatListControllerImpl(context: strongSelf.context, groupId: groupReference.groupId, controlsHistoryPreload: false, hideNetworkActivityStatus: true, previewing: true, enableDebugActions: false) @@ -810,7 +820,7 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController, case let .peer(_, peer, _, _, _, _, _, _, promoInfo, _, _, _): let chatController = strongSelf.context.sharedContext.makeChatController(context: strongSelf.context, chatLocation: .peer(peer.peerId), subject: 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: node, navigationController: strongSelf.navigationController as? NavigationController)), items: chatContextMenuItems(context: strongSelf.context, peerId: peer.peerId, promoInfo: promoInfo, source: .chatList(filter: strongSelf.chatListDisplayNode.containerNode.currentItemNode.chatListFilter), chatListController: strongSelf), reactionItems: [], gesture: gesture) + let contextController = ContextController(account: strongSelf.context.account, presentationData: strongSelf.presentationData, source: .controller(ContextControllerContentSourceImpl(controller: chatController, sourceNode: node, navigationController: strongSelf.navigationController as? NavigationController)), items: chatContextMenuItems(context: strongSelf.context, peerId: peer.peerId, promoInfo: promoInfo, source: .chatList(filter: strongSelf.chatListDisplayNode.containerNode.currentItemNode.chatListFilter), chatListController: strongSelf, joined: joined), reactionItems: [], gesture: gesture) strongSelf.presentInGlobalOverlay(contextController) } } @@ -823,7 +833,7 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController, let chatController = strongSelf.context.sharedContext.makeChatController(context: strongSelf.context, chatLocation: .peer(peer.id), subject: 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: node, navigationController: strongSelf.navigationController as? NavigationController)), items: chatContextMenuItems(context: strongSelf.context, peerId: peer.id, promoInfo: nil, source: .search(source), chatListController: strongSelf), reactionItems: [], gesture: gesture) + let contextController = ContextController(account: strongSelf.context.account, presentationData: strongSelf.presentationData, source: .controller(ContextControllerContentSourceImpl(controller: chatController, sourceNode: node, navigationController: strongSelf.navigationController as? NavigationController)), items: chatContextMenuItems(context: strongSelf.context, peerId: peer.id, promoInfo: nil, source: .search(source), chatListController: strongSelf, joined: false), reactionItems: [], gesture: gesture) strongSelf.presentInGlobalOverlay(contextController) } @@ -2069,7 +2079,7 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController, let _ = hideAccountPromoInfoChat(account: self.context.account, peerId: id).start() } - func deletePeerChat(peerId: PeerId) { + func deletePeerChat(peerId: PeerId, joined: Bool) { let _ = (self.context.account.postbox.transaction { transaction -> RenderedPeer? in guard let peer = transaction.getPeer(peerId) else { return nil @@ -2099,7 +2109,7 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController, } if let user = chatPeer as? TelegramUser, user.botInfo == nil, canRemoveGlobally { - strongSelf.maybeAskForPeerChatRemoval(peer: peer, completion: { _ in }, removed: {}) + strongSelf.maybeAskForPeerChatRemoval(peer: peer, joined: joined, completion: { _ in }, removed: {}) } else { let actionSheet = ActionSheetController(presentationData: strongSelf.presentationData) var items: [ActionSheetItem] = [] @@ -2189,16 +2199,24 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController, if canRemoveGlobally { let actionSheet = ActionSheetController(presentationData: strongSelf.presentationData) var items: [ActionSheetItem] = [] - + items.append(DeleteChatPeerActionSheetItem(context: strongSelf.context, peer: mainPeer, chatPeer: chatPeer, action: .clearHistory, strings: strongSelf.presentationData.strings, nameDisplayOrder: strongSelf.presentationData.nameDisplayOrder)) - items.append(ActionSheetButtonItem(title: strongSelf.presentationData.strings.ChatList_DeleteForEveryone(mainPeer.compactDisplayTitle).0, color: .destructive, action: { [weak actionSheet] in - beginClear(.forEveryone) - actionSheet?.dismissAnimated() - })) - items.append(ActionSheetButtonItem(title: strongSelf.presentationData.strings.ChatList_DeleteForCurrentUser, color: .destructive, action: { [weak actionSheet] in - beginClear(.forLocalPeer) - actionSheet?.dismissAnimated() - })) + + if joined || mainPeer.isDeleted { + items.append(ActionSheetButtonItem(title: strongSelf.presentationData.strings.Common_Delete, color: .destructive, action: { [weak actionSheet] in + beginClear(.forEveryone) + actionSheet?.dismissAnimated() + })) + } else { + items.append(ActionSheetButtonItem(title: strongSelf.presentationData.strings.ChatList_DeleteForEveryone(mainPeer.compactDisplayTitle).0, color: .destructive, action: { [weak actionSheet] in + beginClear(.forEveryone) + actionSheet?.dismissAnimated() + })) + items.append(ActionSheetButtonItem(title: strongSelf.presentationData.strings.ChatList_DeleteForCurrentUser, color: .destructive, action: { [weak actionSheet] in + beginClear(.forLocalPeer) + actionSheet?.dismissAnimated() + })) + } actionSheet.setItemGroups([ ActionSheetItemGroup(items: items), @@ -2258,7 +2276,7 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController, }) } - public func maybeAskForPeerChatRemoval(peer: RenderedPeer, deleteGloballyIfPossible: Bool = false, completion: @escaping (Bool) -> Void, removed: @escaping () -> Void) { + public func maybeAskForPeerChatRemoval(peer: RenderedPeer, joined: Bool = false, deleteGloballyIfPossible: Bool = false, completion: @escaping (Bool) -> Void, removed: @escaping () -> Void) { guard let chatPeer = peer.peers[peer.peerId], let mainPeer = peer.chatMainPeer else { completion(false) return @@ -2279,31 +2297,41 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController, var items: [ActionSheetItem] = [] items.append(DeleteChatPeerActionSheetItem(context: self.context, peer: mainPeer, chatPeer: chatPeer, action: .delete, strings: self.presentationData.strings, nameDisplayOrder: self.presentationData.nameDisplayOrder)) - items.append(ActionSheetButtonItem(title: self.presentationData.strings.ChatList_DeleteForEveryone(mainPeer.compactDisplayTitle).0, color: .destructive, action: { [weak self, weak actionSheet] in - actionSheet?.dismissAnimated() - guard let strongSelf = self else { - return - } - strongSelf.present(standardTextAlertController(theme: AlertControllerTheme(presentationData: strongSelf.presentationData), title: strongSelf.presentationData.strings.ChatList_DeleteForEveryoneConfirmationTitle, text: strongSelf.presentationData.strings.ChatList_DeleteForEveryoneConfirmationText, actions: [ - TextAlertAction(type: .genericAction, title: strongSelf.presentationData.strings.Common_Cancel, action: { - completion(false) - }), - TextAlertAction(type: .destructiveAction, title: strongSelf.presentationData.strings.ChatList_DeleteForEveryoneConfirmationAction, action: { - self?.schedulePeerChatRemoval(peer: peer, type: .forEveryone, deleteGloballyIfPossible: deleteGloballyIfPossible, completion: { - removed() - }) - completion(true) - }) - ], parseMarkdown: true), in: .window(.root)) - })) - items.append(ActionSheetButtonItem(title: self.presentationData.strings.ChatList_DeleteForCurrentUser, color: .destructive, action: { [weak self, weak actionSheet] in - actionSheet?.dismissAnimated() - self?.schedulePeerChatRemoval(peer: peer, type: .forLocalPeer, deleteGloballyIfPossible: deleteGloballyIfPossible, completion: { - removed() - }) - completion(true) - })) + if joined || mainPeer.isDeleted { + items.append(ActionSheetButtonItem(title: self.presentationData.strings.Common_Delete, color: .destructive, action: { [weak self, weak actionSheet] in + actionSheet?.dismissAnimated() + self?.schedulePeerChatRemoval(peer: peer, type: .forEveryone, deleteGloballyIfPossible: deleteGloballyIfPossible, completion: { + removed() + }) + completion(true) + })) + } else { + items.append(ActionSheetButtonItem(title: self.presentationData.strings.ChatList_DeleteForEveryone(mainPeer.compactDisplayTitle).0, color: .destructive, action: { [weak self, weak actionSheet] in + actionSheet?.dismissAnimated() + guard let strongSelf = self else { + return + } + strongSelf.present(standardTextAlertController(theme: AlertControllerTheme(presentationData: strongSelf.presentationData), title: strongSelf.presentationData.strings.ChatList_DeleteForEveryoneConfirmationTitle, text: strongSelf.presentationData.strings.ChatList_DeleteForEveryoneConfirmationText, actions: [ + TextAlertAction(type: .genericAction, title: strongSelf.presentationData.strings.Common_Cancel, action: { + completion(false) + }), + TextAlertAction(type: .destructiveAction, title: strongSelf.presentationData.strings.ChatList_DeleteForEveryoneConfirmationAction, action: { + self?.schedulePeerChatRemoval(peer: peer, type: .forEveryone, deleteGloballyIfPossible: deleteGloballyIfPossible, completion: { + removed() + }) + completion(true) + }) + ], parseMarkdown: true), in: .window(.root)) + })) + items.append(ActionSheetButtonItem(title: self.presentationData.strings.ChatList_DeleteForCurrentUser, color: .destructive, action: { [weak self, weak actionSheet] in + actionSheet?.dismissAnimated() + self?.schedulePeerChatRemoval(peer: peer, type: .forLocalPeer, deleteGloballyIfPossible: deleteGloballyIfPossible, completion: { + removed() + }) + completion(true) + })) + } actionSheet.setItemGroups([ ActionSheetItemGroup(items: items), ActionSheetItemGroup(items: [ diff --git a/submodules/ChatListUI/Sources/ChatListControllerNode.swift b/submodules/ChatListUI/Sources/ChatListControllerNode.swift index 6f4eb396bf..d336726443 100644 --- a/submodules/ChatListUI/Sources/ChatListControllerNode.swift +++ b/submodules/ChatListUI/Sources/ChatListControllerNode.swift @@ -195,7 +195,7 @@ private final class ChatListShimmerNode: ASDisplayNode { let timestamp1: Int32 = 100000 let peers = SimpleDictionary() let interaction = ChatListNodeInteraction(activateSearch: {}, peerSelected: { _, _ in }, disabledPeerSelected: { _ in }, togglePeerSelected: { _ in }, additionalCategorySelected: { _ in - }, messageSelected: { _, _, _ in}, groupSelected: { _ in }, addContact: { _ in }, setPeerIdWithRevealedOptions: { _, _ in }, setItemPinned: { _, _ in }, setPeerMuted: { _, _ in }, deletePeer: { _ in }, updatePeerGrouping: { _, _ in }, togglePeerMarkedUnread: { _, _ in}, toggleArchivedFolderHiddenByDefault: {}, hidePsa: { _ in }, activateChatPreview: { _, _, gesture in + }, messageSelected: { _, _, _ in}, groupSelected: { _ in }, addContact: { _ in }, setPeerIdWithRevealedOptions: { _, _ in }, setItemPinned: { _, _ in }, setPeerMuted: { _, _ in }, deletePeer: { _, _ in }, updatePeerGrouping: { _, _ in }, togglePeerMarkedUnread: { _, _ in}, toggleArchivedFolderHiddenByDefault: {}, hidePsa: { _ in }, activateChatPreview: { _, _, gesture in gesture?.cancel() }, present: { _ in }) @@ -478,8 +478,8 @@ final class ChatListContainerNode: ASDisplayNode, UIGestureRecognizerDelegate { itemNode.listNode.hidePsa = { [weak self] peerId in self?.hidePsa?(peerId) } - itemNode.listNode.deletePeerChat = { [weak self] peerId in - self?.deletePeerChat?(peerId) + itemNode.listNode.deletePeerChat = { [weak self] peerId, joined in + self?.deletePeerChat?(peerId, joined) } itemNode.listNode.peerSelected = { [weak self] peerId, a, b in self?.peerSelected?(peerId, a, b) @@ -527,7 +527,7 @@ final class ChatListContainerNode: ASDisplayNode, UIGestureRecognizerDelegate { var present: ((ViewController) -> Void)? var toggleArchivedFolderHiddenByDefault: (() -> Void)? var hidePsa: ((PeerId) -> Void)? - var deletePeerChat: ((PeerId) -> Void)? + var deletePeerChat: ((PeerId, Bool) -> Void)? var peerSelected: ((Peer, Bool, ChatListNodeEntryPromoInfo?) -> Void)? var groupSelected: ((PeerGroupId) -> Void)? var updatePeerGrouping: ((PeerId, Bool) -> Void)? diff --git a/submodules/ChatListUI/Sources/ChatListSearchContainerNode.swift b/submodules/ChatListUI/Sources/ChatListSearchContainerNode.swift index 33af650006..96189597db 100644 --- a/submodules/ChatListUI/Sources/ChatListSearchContainerNode.swift +++ b/submodules/ChatListUI/Sources/ChatListSearchContainerNode.swift @@ -1028,7 +1028,7 @@ public final class ChatListSearchContainerNode: SearchDisplayControllerContentNo } }, setItemPinned: { _, _ in }, setPeerMuted: { _, _ in - }, deletePeer: { _ in + }, deletePeer: { _, _ in }, updatePeerGrouping: { _, _ in }, togglePeerMarkedUnread: { _, _ in }, toggleArchivedFolderHiddenByDefault: { diff --git a/submodules/ChatListUI/Sources/Node/ChatListItem.swift b/submodules/ChatListUI/Sources/Node/ChatListItem.swift index 7edf5afe3b..f29befbd66 100644 --- a/submodules/ChatListUI/Sources/Node/ChatListItem.swift +++ b/submodules/ChatListUI/Sources/Node/ChatListItem.swift @@ -1946,7 +1946,15 @@ class ChatListItemNode: ItemListRevealOptionsItemNode { item.interaction.setPeerMuted(item.index.messageIndex.id.peerId, false) close = false case RevealOptionKey.delete.rawValue: - item.interaction.deletePeer(item.index.messageIndex.id.peerId) + var joined = false + if case let .peer(messages, _, _, _, _, _, _, _, _, _, _, _) = item.content, let message = messages.first { + for media in message.media { + if let action = media as? TelegramMediaAction, action.action == .peerJoined { + joined = true + } + } + } + item.interaction.deletePeer(item.index.messageIndex.id.peerId, joined) case RevealOptionKey.archive.rawValue: item.interaction.updatePeerGrouping(item.index.messageIndex.id.peerId, true) close = false diff --git a/submodules/ChatListUI/Sources/Node/ChatListNode.swift b/submodules/ChatListUI/Sources/Node/ChatListNode.swift index 326c5bcb5a..0506f6deb4 100644 --- a/submodules/ChatListUI/Sources/Node/ChatListNode.swift +++ b/submodules/ChatListUI/Sources/Node/ChatListNode.swift @@ -59,7 +59,7 @@ public final class ChatListNodeInteraction { let setPeerIdWithRevealedOptions: (PeerId?, PeerId?) -> Void let setItemPinned: (PinnedItemId, Bool) -> Void let setPeerMuted: (PeerId, Bool) -> Void - let deletePeer: (PeerId) -> Void + let deletePeer: (PeerId, Bool) -> Void let updatePeerGrouping: (PeerId, Bool) -> Void let togglePeerMarkedUnread: (PeerId, Bool) -> Void let toggleArchivedFolderHiddenByDefault: () -> Void @@ -70,7 +70,7 @@ public final class ChatListNodeInteraction { public var searchTextHighightState: String? var highlightedChatLocation: ChatListHighlightedLocation? - public init(activateSearch: @escaping () -> Void, peerSelected: @escaping (Peer, ChatListNodeEntryPromoInfo?) -> Void, disabledPeerSelected: @escaping (Peer) -> Void, togglePeerSelected: @escaping (PeerId) -> Void, additionalCategorySelected: @escaping (Int) -> Void, messageSelected: @escaping (Peer, Message, ChatListNodeEntryPromoInfo?) -> Void, groupSelected: @escaping (PeerGroupId) -> Void, addContact: @escaping (String) -> Void, setPeerIdWithRevealedOptions: @escaping (PeerId?, PeerId?) -> Void, setItemPinned: @escaping (PinnedItemId, Bool) -> Void, setPeerMuted: @escaping (PeerId, Bool) -> Void, deletePeer: @escaping (PeerId) -> Void, updatePeerGrouping: @escaping (PeerId, Bool) -> Void, togglePeerMarkedUnread: @escaping (PeerId, Bool) -> Void, toggleArchivedFolderHiddenByDefault: @escaping () -> Void, hidePsa: @escaping (PeerId) -> Void, activateChatPreview: @escaping (ChatListItem, ASDisplayNode, ContextGesture?) -> Void, present: @escaping (ViewController) -> Void) { + public init(activateSearch: @escaping () -> Void, peerSelected: @escaping (Peer, ChatListNodeEntryPromoInfo?) -> Void, disabledPeerSelected: @escaping (Peer) -> Void, togglePeerSelected: @escaping (PeerId) -> Void, additionalCategorySelected: @escaping (Int) -> Void, messageSelected: @escaping (Peer, Message, ChatListNodeEntryPromoInfo?) -> Void, groupSelected: @escaping (PeerGroupId) -> Void, addContact: @escaping (String) -> Void, setPeerIdWithRevealedOptions: @escaping (PeerId?, PeerId?) -> Void, setItemPinned: @escaping (PinnedItemId, Bool) -> Void, setPeerMuted: @escaping (PeerId, Bool) -> Void, deletePeer: @escaping (PeerId, Bool) -> Void, updatePeerGrouping: @escaping (PeerId, Bool) -> Void, togglePeerMarkedUnread: @escaping (PeerId, Bool) -> Void, toggleArchivedFolderHiddenByDefault: @escaping () -> Void, hidePsa: @escaping (PeerId) -> Void, activateChatPreview: @escaping (ChatListItem, ASDisplayNode, ContextGesture?) -> Void, present: @escaping (ViewController) -> Void) { self.activateSearch = activateSearch self.peerSelected = peerSelected self.disabledPeerSelected = disabledPeerSelected @@ -430,7 +430,7 @@ public final class ChatListNode: ListView { public var groupSelected: ((PeerGroupId) -> Void)? public var addContact: ((String) -> Void)? public var activateSearch: (() -> Void)? - public var deletePeerChat: ((PeerId) -> Void)? + public var deletePeerChat: ((PeerId, Bool) -> Void)? public var updatePeerGrouping: ((PeerId, Bool) -> Void)? public var presentAlert: ((String) -> Void)? public var present: ((ViewController) -> Void)? @@ -628,8 +628,8 @@ public final class ChatListNode: ListView { } self?.setCurrentRemovingPeerId(nil) }) - }, deletePeer: { [weak self] peerId in - self?.deletePeerChat?(peerId) + }, deletePeer: { [weak self] peerId, joined in + self?.deletePeerChat?(peerId, joined) }, updatePeerGrouping: { [weak self] peerId, group in self?.updatePeerGrouping?(peerId, group) }, togglePeerMarkedUnread: { [weak self, weak context] peerId, animated in diff --git a/submodules/Display/Source/CAAnimationUtils.swift b/submodules/Display/Source/CAAnimationUtils.swift index 1d37fa419b..8244276576 100644 --- a/submodules/Display/Source/CAAnimationUtils.swift +++ b/submodules/Display/Source/CAAnimationUtils.swift @@ -220,8 +220,8 @@ public extension CALayer { self.animate(from: NSNumber(value: Float(from)), to: NSNumber(value: Float(to)), keyPath: "opacity", timingFunction: timingFunction, duration: duration, delay: delay, mediaTimingFunction: mediaTimingFunction, removeOnCompletion: removeOnCompletion, completion: completion) } - func animateScale(from: CGFloat, to: CGFloat, duration: Double, delay: Double = 0.0, timingFunction: String = CAMediaTimingFunctionName.easeInEaseOut.rawValue, mediaTimingFunction: CAMediaTimingFunction? = nil, removeOnCompletion: Bool = true, completion: ((Bool) -> Void)? = nil) { - self.animate(from: NSNumber(value: Float(from)), to: NSNumber(value: Float(to)), keyPath: "transform.scale", timingFunction: timingFunction, duration: duration, delay: delay, mediaTimingFunction: mediaTimingFunction, removeOnCompletion: removeOnCompletion, completion: completion) + func animateScale(from: CGFloat, to: CGFloat, duration: Double, delay: Double = 0.0, timingFunction: String = CAMediaTimingFunctionName.easeInEaseOut.rawValue, mediaTimingFunction: CAMediaTimingFunction? = nil, removeOnCompletion: Bool = true, additive: Bool = false, completion: ((Bool) -> Void)? = nil) { + self.animate(from: NSNumber(value: Float(from)), to: NSNumber(value: Float(to)), keyPath: "transform.scale", timingFunction: timingFunction, duration: duration, delay: delay, mediaTimingFunction: mediaTimingFunction, removeOnCompletion: removeOnCompletion, additive: additive, completion: completion) } func animateScaleY(from: CGFloat, to: CGFloat, duration: Double, delay: Double = 0.0, timingFunction: String = CAMediaTimingFunctionName.easeInEaseOut.rawValue, mediaTimingFunction: CAMediaTimingFunction? = nil, removeOnCompletion: Bool = true, completion: ((Bool) -> Void)? = nil) { diff --git a/submodules/Display/Source/ContainedViewLayoutTransition.swift b/submodules/Display/Source/ContainedViewLayoutTransition.swift index 3134ef5c54..053f6ad60b 100644 --- a/submodules/Display/Source/ContainedViewLayoutTransition.swift +++ b/submodules/Display/Source/ContainedViewLayoutTransition.swift @@ -576,7 +576,7 @@ public extension ContainedViewLayoutTransition { } } - func animateTransformScale(node: ASDisplayNode, from fromScale: CGFloat, completion: ((Bool) -> Void)? = nil) { + func animateTransformScale(node: ASDisplayNode, from fromScale: CGFloat, additive: Bool = false, completion: ((Bool) -> Void)? = nil) { let t = node.layer.transform let currentScale = sqrt((t.m11 * t.m11) + (t.m12 * t.m12) + (t.m13 * t.m13)) if currentScale.isEqual(to: fromScale) { @@ -592,7 +592,16 @@ public extension ContainedViewLayoutTransition { completion(true) } case let .animated(duration, curve): - node.layer.animateScale(from: fromScale, to: currentScale, duration: duration, timingFunction: curve.timingFunction, mediaTimingFunction: curve.mediaTimingFunction, completion: { result in + let calculatedFrom: CGFloat + let calculatedTo: CGFloat + if additive { + calculatedFrom = fromScale - currentScale + calculatedTo = 0.0 + } else { + calculatedFrom = fromScale + calculatedTo = currentScale + } + node.layer.animateScale(from: calculatedFrom, to: calculatedTo, duration: duration, timingFunction: curve.timingFunction, mediaTimingFunction: curve.mediaTimingFunction, additive: additive, completion: { result in if let completion = completion { completion(result) } @@ -953,6 +962,73 @@ public extension ContainedViewLayoutTransition { }) } } + + func updateTransformRotation(view: UIView, angle: CGFloat, beginWithCurrentState: Bool = false, completion: ((Bool) -> Void)? = nil) { + let t = view.layer.transform + let currentAngle = atan2(t.m12, t.m11) + if currentAngle.isEqual(to: angle) { + if let completion = completion { + completion(true) + } + return + } + + switch self { + case .immediate: + view.layer.transform = CATransform3DMakeRotation(angle, 0.0, 0.0, 1.0) + if let completion = completion { + completion(true) + } + case let .animated(duration, curve): + let previousAngle: CGFloat + if beginWithCurrentState, let presentation = view.layer.presentation() { + let t = presentation.transform + previousAngle = atan2(t.m12, t.m11) + } else { + previousAngle = currentAngle + } + view.layer.transform = CATransform3DMakeRotation(angle, 0.0, 0.0, 1.0) + view.layer.animateRotation(from: previousAngle, to: angle, duration: duration, timingFunction: curve.timingFunction, mediaTimingFunction: curve.mediaTimingFunction, completion: { result in + if let completion = completion { + completion(result) + } + }) + } + } + + func updateTransformRotationAndScale(view: UIView, angle: CGFloat, scale: CGPoint, beginWithCurrentState: Bool = false, completion: ((Bool) -> Void)? = nil) { + let t = view.layer.transform + let currentAngle = atan2(t.m12, t.m11) + let currentScale = CGPoint(x: t.m11, y: t.m12) + if currentAngle.isEqual(to: angle) && currentScale == scale { + if let completion = completion { + completion(true) + } + return + } + + switch self { + case .immediate: + view.layer.transform = CATransform3DRotate(CATransform3DMakeScale(scale.x, scale.y, 1.0), angle, 0.0, 0.0, 1.0) + if let completion = completion { + completion(true) + } + case let .animated(duration, curve): + let previousAngle: CGFloat + if beginWithCurrentState, let presentation = view.layer.presentation() { + let t = presentation.transform + previousAngle = atan2(t.m12, t.m11) + } else { + previousAngle = currentAngle + } + view.layer.transform = CATransform3DRotate(CATransform3DMakeScale(scale.x, scale.y, 1.0), angle, 0.0, 0.0, 1.0) + view.layer.animateRotation(from: previousAngle, to: angle, duration: duration, timingFunction: curve.timingFunction, mediaTimingFunction: curve.mediaTimingFunction, completion: { result in + if let completion = completion { + completion(result) + } + }) + } + } } #if os(iOS) diff --git a/submodules/Display/Source/ListView.swift b/submodules/Display/Source/ListView.swift index a548fd1bf5..faf1fefcfb 100644 --- a/submodules/Display/Source/ListView.swift +++ b/submodules/Display/Source/ListView.swift @@ -184,6 +184,8 @@ open class ListView: ASDisplayNode, UIScrollViewAccessibilityDelegate, UIGesture public final var keepMinimalScrollHeightWithTopInset: CGFloat? + public final var itemNodeHitTest: ((CGPoint) -> Bool)? + public final var stackFromBottom: Bool = false public final var stackFromBottomInsetItemFactor: CGFloat = 0.0 public final var limitHitTestToNodes: Bool = false @@ -3876,67 +3878,73 @@ open class ListView: ASDisplayNode, UIScrollViewAccessibilityDelegate, UIGesture } self.touchesPosition = touchesPosition - self.selectionTouchLocation = touches.first!.location(in: self.view) - self.selectionTouchDelayTimer?.invalidate() - self.selectionLongTapDelayTimer?.invalidate() - self.selectionLongTapDelayTimer = nil - let timer = Timer(timeInterval: 0.08, target: ListViewTimerProxy { [weak self] in - if let strongSelf = self, strongSelf.selectionTouchLocation != nil { - strongSelf.clearHighlightAnimated(false) - - if let index = strongSelf.itemIndexAtPoint(strongSelf.touchesPosition) { - var canBeSelectedOrLongTapped = false - for itemNode in strongSelf.itemNodes { - if itemNode.index == index && (strongSelf.items[index].selectable && itemNode.canBeSelected) || itemNode.canBeLongTapped { - canBeSelectedOrLongTapped = true - } - } + var processSelection = true + if let itemNodeHitTest = self.itemNodeHitTest, !itemNodeHitTest(touchesPosition) { + processSelection = false + } + + if processSelection { + self.selectionTouchLocation = touches.first!.location(in: self.view) + self.selectionTouchDelayTimer?.invalidate() + self.selectionLongTapDelayTimer?.invalidate() + self.selectionLongTapDelayTimer = nil + let timer = Timer(timeInterval: 0.08, target: ListViewTimerProxy { [weak self] in + if let strongSelf = self, strongSelf.selectionTouchLocation != nil { + strongSelf.clearHighlightAnimated(false) - if canBeSelectedOrLongTapped { - strongSelf.highlightedItemIndex = index + if let index = strongSelf.itemIndexAtPoint(strongSelf.touchesPosition) { + var canBeSelectedOrLongTapped = false for itemNode in strongSelf.itemNodes { - if itemNode.index == index && itemNode.canBeSelected { - if true { - if !itemNode.isLayerBacked { - strongSelf.reorderItemNodeToFront(itemNode) - for (_, headerNode) in strongSelf.itemHeaderNodes { - strongSelf.reorderHeaderNodeToFront(headerNode) + if itemNode.index == index && (strongSelf.items[index].selectable && itemNode.canBeSelected) || itemNode.canBeLongTapped { + canBeSelectedOrLongTapped = true + } + } + + if canBeSelectedOrLongTapped { + strongSelf.highlightedItemIndex = index + for itemNode in strongSelf.itemNodes { + if itemNode.index == index && itemNode.canBeSelected { + if true { + if !itemNode.isLayerBacked { + strongSelf.reorderItemNodeToFront(itemNode) + for (_, headerNode) in strongSelf.itemHeaderNodes { + strongSelf.reorderHeaderNodeToFront(headerNode) + } } - } - let itemNodeFrame = itemNode.frame - let itemNodeBounds = itemNode.bounds - if strongSelf.items[index].selectable { - itemNode.setHighlighted(true, at: strongSelf.touchesPosition.offsetBy(dx: -itemNodeFrame.minX + itemNodeBounds.minX, dy: -itemNodeFrame.minY + itemNodeBounds.minY), animated: false) - } - - if itemNode.canBeLongTapped { - let timer = Timer(timeInterval: 0.3, target: ListViewTimerProxy { - if let strongSelf = self, strongSelf.highlightedItemIndex == index { - for itemNode in strongSelf.itemNodes { - if itemNode.index == index && itemNode.canBeLongTapped { - itemNode.longTapped() - strongSelf.clearHighlightAnimated(true) - strongSelf.selectionTouchLocation = nil - break + let itemNodeFrame = itemNode.frame + let itemNodeBounds = itemNode.bounds + if strongSelf.items[index].selectable { + itemNode.setHighlighted(true, at: strongSelf.touchesPosition.offsetBy(dx: -itemNodeFrame.minX + itemNodeBounds.minX, dy: -itemNodeFrame.minY + itemNodeBounds.minY), animated: false) + } + + if itemNode.canBeLongTapped { + let timer = Timer(timeInterval: 0.3, target: ListViewTimerProxy { + if let strongSelf = self, strongSelf.highlightedItemIndex == index { + for itemNode in strongSelf.itemNodes { + if itemNode.index == index && itemNode.canBeLongTapped { + itemNode.longTapped() + strongSelf.clearHighlightAnimated(true) + strongSelf.selectionTouchLocation = nil + break + } } } - } - }, selector: #selector(ListViewTimerProxy.timerEvent), userInfo: nil, repeats: false) - strongSelf.selectionLongTapDelayTimer = timer - RunLoop.main.add(timer, forMode: RunLoop.Mode.common) + }, selector: #selector(ListViewTimerProxy.timerEvent), userInfo: nil, repeats: false) + strongSelf.selectionLongTapDelayTimer = timer + RunLoop.main.add(timer, forMode: RunLoop.Mode.common) + } } + break } - break } } } } - } - }, selector: #selector(ListViewTimerProxy.timerEvent), userInfo: nil, repeats: false) - self.selectionTouchDelayTimer = timer - RunLoop.main.add(timer, forMode: RunLoop.Mode.common) - + }, selector: #selector(ListViewTimerProxy.timerEvent), userInfo: nil, repeats: false) + self.selectionTouchDelayTimer = timer + RunLoop.main.add(timer, forMode: RunLoop.Mode.common) + } super.touchesBegan(touches, with: event) self.updateScroller(transition: .immediate) diff --git a/submodules/Display/Source/Navigation/NavigationController.swift b/submodules/Display/Source/Navigation/NavigationController.swift index 26a12835c0..815b56521d 100644 --- a/submodules/Display/Source/Navigation/NavigationController.swift +++ b/submodules/Display/Source/Navigation/NavigationController.swift @@ -579,6 +579,7 @@ open class NavigationController: UINavigationController, ContainableController, var previousModalContainer: NavigationModalContainer? var visibleModalCount = 0 var topModalIsFlat = false + var isLandscape = layout.orientation == .landscape var hasVisibleStandaloneModal = false var topModalDismissProgress: CGFloat = 0.0 @@ -784,7 +785,7 @@ open class NavigationController: UINavigationController, ContainableController, let visibleRootModalDismissProgress: CGFloat var additionalModalFrameProgress: CGFloat if visibleModalCount == 1 { - effectiveRootModalDismissProgress = topModalIsFlat ? 1.0 : topModalDismissProgress + effectiveRootModalDismissProgress = (topModalIsFlat || isLandscape) ? 1.0 : topModalDismissProgress visibleRootModalDismissProgress = effectiveRootModalDismissProgress additionalModalFrameProgress = 0.0 } else if visibleModalCount >= 2 { @@ -851,7 +852,7 @@ open class NavigationController: UINavigationController, ContainableController, } let maxScale: CGFloat let maxOffset: CGFloat - if topModalIsFlat { + if topModalIsFlat || isLandscape { maxScale = 1.0 maxOffset = 0.0 } else if visibleModalCount <= 1 { diff --git a/submodules/Display/Source/Navigation/NavigationModalContainer.swift b/submodules/Display/Source/Navigation/NavigationModalContainer.swift index aa40c78ee3..247d077368 100644 --- a/submodules/Display/Source/Navigation/NavigationModalContainer.swift +++ b/submodules/Display/Source/Navigation/NavigationModalContainer.swift @@ -328,6 +328,7 @@ final class NavigationModalContainer: ASDisplayNode, UIScrollViewDelegate, UIGes self.scrollNode.view.isScrollEnabled = !isStandaloneModal + let isLandscape = layout.orientation == .landscape let containerLayout: ContainerViewLayout let containerFrame: CGRect let containerScale: CGFloat @@ -336,7 +337,7 @@ final class NavigationModalContainer: ASDisplayNode, UIScrollViewDelegate, UIGes self.panRecognizer?.isEnabled = true self.dim.backgroundColor = UIColor(white: 0.0, alpha: 0.25) self.container.clipsToBounds = true - if isStandaloneModal { + if isStandaloneModal || isLandscape { self.container.cornerRadius = 0.0 } else { self.container.cornerRadius = 10.0 @@ -351,7 +352,7 @@ final class NavigationModalContainer: ASDisplayNode, UIScrollViewDelegate, UIGes } var topInset: CGFloat - if isStandaloneModal { + if isStandaloneModal || isLandscape { topInset = 0.0 containerLayout = layout diff --git a/submodules/GameUI/Sources/GameControllerNode.swift b/submodules/GameUI/Sources/GameControllerNode.swift index 91d89e57bf..668c683ccc 100644 --- a/submodules/GameUI/Sources/GameControllerNode.swift +++ b/submodules/GameUI/Sources/GameControllerNode.swift @@ -62,6 +62,16 @@ final class GameControllerNode: ViewControllerTracingNode { }, name: "performAction") configuration.userContentController = userController + + configuration.allowsInlineMediaPlayback = true + if #available(iOSApplicationExtension 10.0, iOS 10.0, *) { + configuration.mediaTypesRequiringUserActionForPlayback = [] + } else if #available(iOSApplicationExtension 9.0, iOS 9.0, *) { + configuration.requiresUserActionForMediaPlayback = false + } else { + configuration.mediaPlaybackRequiresUserAction = false + } + let webView = WKWebView(frame: CGRect(), configuration: configuration) if #available(iOSApplicationExtension 9.0, iOS 9.0, *) { webView.allowsLinkPreview = false diff --git a/submodules/HashtagSearchUI/Sources/HashtagSearchController.swift b/submodules/HashtagSearchUI/Sources/HashtagSearchController.swift index a2fcb6fda9..78442b6a65 100644 --- a/submodules/HashtagSearchUI/Sources/HashtagSearchController.swift +++ b/submodules/HashtagSearchUI/Sources/HashtagSearchController.swift @@ -66,7 +66,7 @@ public final class HashtagSearchController: TelegramBaseController { }, setPeerIdWithRevealedOptions: { _, _ in }, setItemPinned: { _, _ in }, setPeerMuted: { _, _ in - }, deletePeer: { _ in + }, deletePeer: { _, _ in }, updatePeerGrouping: { _, _ in }, togglePeerMarkedUnread: { _, _ in }, toggleArchivedFolderHiddenByDefault: { diff --git a/submodules/ItemListUI/Sources/ItemListControllerNode.swift b/submodules/ItemListUI/Sources/ItemListControllerNode.swift index 21666e9305..59d3be2803 100644 --- a/submodules/ItemListUI/Sources/ItemListControllerNode.swift +++ b/submodules/ItemListUI/Sources/ItemListControllerNode.swift @@ -238,7 +238,9 @@ open class ItemListControllerNode: ASDisplayNode, UIScrollViewDelegate { self.listNode = ListView() self.leftOverlayNode = ASDisplayNode() + self.leftOverlayNode.isUserInteractionEnabled = false self.rightOverlayNode = ASDisplayNode() + self.rightOverlayNode.isUserInteractionEnabled = false super.init() @@ -302,6 +304,14 @@ open class ItemListControllerNode: ASDisplayNode, UIScrollViewDelegate { let _ = strongSelf.contentScrollingEnded?(strongSelf.listNode) } } + + self.listNode.itemNodeHitTest = { [weak self] point in + if let strongSelf = self { + return point.x > strongSelf.leftOverlayNode.frame.maxX && point.x < strongSelf.rightOverlayNode.frame.minX + } else { + return true + } + } let previousState = Atomic(value: nil) self.transitionDisposable.set(((state diff --git a/submodules/LegacyComponents/PublicHeaders/LegacyComponents/LegacyComponents.h b/submodules/LegacyComponents/PublicHeaders/LegacyComponents/LegacyComponents.h index fb42bcd69d..18f24fd501 100644 --- a/submodules/LegacyComponents/PublicHeaders/LegacyComponents/LegacyComponents.h +++ b/submodules/LegacyComponents/PublicHeaders/LegacyComponents/LegacyComponents.h @@ -22,8 +22,6 @@ #import #import #import -#import -#import #import #import #import diff --git a/submodules/LegacyComponents/PublicHeaders/LegacyComponents/PGCameraMomentSegment.h b/submodules/LegacyComponents/PublicHeaders/LegacyComponents/PGCameraMomentSegment.h deleted file mode 100644 index ab0699b79b..0000000000 --- a/submodules/LegacyComponents/PublicHeaders/LegacyComponents/PGCameraMomentSegment.h +++ /dev/null @@ -1,13 +0,0 @@ -#import -#import -#import - -@interface PGCameraMomentSegment : NSObject - -@property (nonatomic, readonly) NSURL *fileURL; -@property (nonatomic, readonly) AVAsset *asset; -@property (nonatomic, readonly) NSTimeInterval duration; - -- (instancetype)initWithURL:(NSURL *)url duration:(NSTimeInterval)duration; - -@end diff --git a/submodules/LegacyComponents/PublicHeaders/LegacyComponents/PGCameraMomentSession.h b/submodules/LegacyComponents/PublicHeaders/LegacyComponents/PGCameraMomentSession.h deleted file mode 100644 index a7f8269308..0000000000 --- a/submodules/LegacyComponents/PublicHeaders/LegacyComponents/PGCameraMomentSession.h +++ /dev/null @@ -1,28 +0,0 @@ -#import - -@class PGCamera; - -@interface PGCameraMomentSession : NSObject - -@property (nonatomic, copy) void (^beganCapture)(void); -@property (nonatomic, copy) void (^finishedCapture)(void); -@property (nonatomic, copy) bool (^captureIsAvailable)(void); -@property (nonatomic, copy) void (^durationChanged)(NSTimeInterval); - -@property (nonatomic, readonly) bool isCapturing; -@property (nonatomic, readonly) UIImage *previewImage; -@property (nonatomic, readonly) bool hasSegments; - -@property (nonatomic, readonly) PGCameraMomentSegment *lastSegment; - -- (instancetype)initWithCamera:(PGCamera *)camera; - -- (void)captureSegment; -- (void)commitSegment; - -- (void)addSegment:(PGCameraMomentSegment *)segment; -- (void)removeSegment:(PGCameraMomentSegment *)segment; -- (void)removeLastSegment; -- (void)removeAllSegments; - -@end diff --git a/submodules/LegacyComponents/PublicHeaders/LegacyComponents/TGPhotoEditorController.h b/submodules/LegacyComponents/PublicHeaders/LegacyComponents/TGPhotoEditorController.h index 889afb55cc..a9f491c4cb 100644 --- a/submodules/LegacyComponents/PublicHeaders/LegacyComponents/TGPhotoEditorController.h +++ b/submodules/LegacyComponents/PublicHeaders/LegacyComponents/TGPhotoEditorController.h @@ -85,4 +85,7 @@ typedef enum { + (TGPhotoEditorTab)defaultTabsForAvatarIntent; +- (NSTimeInterval)currentTime; +- (void)setMinimalVideoDuration:(NSTimeInterval)duration; + @end diff --git a/submodules/LegacyComponents/PublicHeaders/LegacyComponents/TGPhotoPaintStickersContext.h b/submodules/LegacyComponents/PublicHeaders/LegacyComponents/TGPhotoPaintStickersContext.h index 4c6e2e14ce..529d893be3 100644 --- a/submodules/LegacyComponents/PublicHeaders/LegacyComponents/TGPhotoPaintStickersContext.h +++ b/submodules/LegacyComponents/PublicHeaders/LegacyComponents/TGPhotoPaintStickersContext.h @@ -13,7 +13,13 @@ @protocol TGPhotoPaintStickerRenderView +@property (nonatomic, copy) void(^started)(double); + - (void)setIsVisible:(bool)isVisible; +- (void)seekTo:(double)timestamp; +- (void)play; +- (void)pause; +- (void)resetToStart; - (int64_t)documentId; - (UIImage *)image; diff --git a/submodules/LegacyComponents/Sources/PGCameraMomentSegment.m b/submodules/LegacyComponents/Sources/PGCameraMomentSegment.m deleted file mode 100644 index 50c9ed87a8..0000000000 --- a/submodules/LegacyComponents/Sources/PGCameraMomentSegment.m +++ /dev/null @@ -1,22 +0,0 @@ -#import "PGCameraMomentSegment.h" - -@interface PGCameraMomentSegment () -{ - -} -@end - -@implementation PGCameraMomentSegment - -- (instancetype)initWithURL:(NSURL *)url duration:(NSTimeInterval)duration -{ - self = [super init]; - if (self != nil) - { - _fileURL = url; - _duration = duration; - } - return self; -} - -@end diff --git a/submodules/LegacyComponents/Sources/PGCameraMomentSession.m b/submodules/LegacyComponents/Sources/PGCameraMomentSession.m deleted file mode 100644 index d83cbfc354..0000000000 --- a/submodules/LegacyComponents/Sources/PGCameraMomentSession.m +++ /dev/null @@ -1,89 +0,0 @@ -#import "PGCameraMomentSession.h" -#import "PGCamera.h" - -@interface PGCameraMomentSession () -{ - NSString *_uniqueIdentifier; - NSURL *_segmentsDirectory; - - PGCamera *_camera; - NSMutableArray *_segments; -} -@end - -@implementation PGCameraMomentSession - -- (instancetype)initWithCamera:(PGCamera *)camera -{ - self = [super init]; - if (self != nil) - { - _camera = camera; - _segments = [[NSMutableArray alloc] init]; - - int64_t uniqueId = 0; - arc4random_buf(&uniqueId, 8); - _uniqueIdentifier = [NSString stringWithFormat:@"%x", (int)arc4random()]; - } - return self; -} - -- (NSURL *)segmentsDirectory -{ - if (_segmentsDirectory == nil) - _segmentsDirectory = [NSURL fileURLWithPath:[NSTemporaryDirectory() stringByAppendingPathComponent:_uniqueIdentifier]]; - - return _segmentsDirectory; -} - -- (void)captureSegment -{ - if (self.isCapturing) - return; - - _isCapturing = true; - - if (self.beganCapture != nil) - self.beganCapture(); - - [_camera startVideoRecordingForMoment:true completion:^(NSURL *resultUrl, __unused CGAffineTransform transform, __unused CGSize dimensions, NSTimeInterval duration, bool success) - { - if (!success) - return; - - _isCapturing = false; - - if (self.finishedCapture != nil) - self.finishedCapture(); - - PGCameraMomentSegment *segment = [[PGCameraMomentSegment alloc] initWithURL:resultUrl duration:duration]; - [self addSegment:segment]; - }]; -} - -- (void)commitSegment -{ - [_camera stopVideoRecording]; -} - -- (void)addSegment:(PGCameraMomentSegment *)segment -{ - [_segments addObject:segment]; -} - -- (void)removeSegment:(PGCameraMomentSegment *)segment -{ - [_segments removeObject:segment]; -} - -- (void)removeLastSegment -{ - [_segments removeLastObject]; -} - -- (void)removeAllSegments -{ - [_segments removeAllObjects]; -} - -@end diff --git a/submodules/LegacyComponents/Sources/TGLocationViewController.m b/submodules/LegacyComponents/Sources/TGLocationViewController.m index 97489ab445..dd3ec6ea8c 100644 --- a/submodules/LegacyComponents/Sources/TGLocationViewController.m +++ b/submodules/LegacyComponents/Sources/TGLocationViewController.m @@ -71,6 +71,8 @@ bool _throttle; TGLocationPinAnnotationView *_ownLiveLocationView; __weak MKAnnotationView *_userLocationView; + + UIImageView *_headingArrowView; } @end @@ -162,6 +164,8 @@ [_liveLocationsDisposable dispose]; [_reloadDisposable dispose]; [_frequentUpdatesDisposable dispose]; + + [_locationManager stopUpdatingHeading]; } - (void)tg_setRightBarButtonItem:(UIBarButtonItem *)barButtonItem action:(bool)action animated:(bool)animated { @@ -438,6 +442,36 @@ { [super loadView]; + static UIImage *headingArrowImage = nil; + static dispatch_once_t onceToken; + + dispatch_once(&onceToken, ^ + { + UIGraphicsBeginImageContextWithOptions(CGSizeMake(28.0f, 28.0f), false, 0.0f); + CGContextRef context = UIGraphicsGetCurrentContext(); + + CGContextClearRect(context, CGRectMake(0, 0, 28, 28)); + + CGContextSetFillColorWithColor(context, UIColorRGB(0x3393fe).CGColor); + + CGContextMoveToPoint(context, 14, 0); + CGContextAddLineToPoint(context, 19, 7); + CGContextAddLineToPoint(context, 9, 7); + CGContextClosePath(context); + CGContextFillPath(context); + + CGContextSetBlendMode(context, kCGBlendModeClear); + CGContextFillEllipseInRect(context, CGRectMake(5.0, 5.0, 18.0, 18.0)); + + headingArrowImage = UIGraphicsGetImageFromCurrentImageContext(); + UIGraphicsEndImageContext(); + }); + + _headingArrowView = [[UIImageView alloc] init]; + _headingArrowView.hidden = true; + _headingArrowView.frame = CGRectMake(0.0, 0.0, 28.0, 28.0); + _headingArrowView.image = headingArrowImage; + _tableView.scrollsToTop = false; _mapView.tapEnabled = false; @@ -495,6 +529,8 @@ { [super viewWillAppear:animated]; + [_locationManager startUpdatingHeading]; + if (self.previewMode && !animated) { UIView *contentView = [[_mapView subviews] firstObject]; @@ -950,6 +986,9 @@ { _userLocationView = view; + [_userLocationView addSubview:_headingArrowView]; + _headingArrowView.center = CGPointMake(view.frame.size.width / 2.0, view.frame.size.height / 2.0); + if (_ownLiveLocationView != nil) { [_userLocationView addSubview:_ownLiveLocationView]; @@ -982,6 +1021,14 @@ return CLLocationCoordinate2DMake(_locationAttachment.latitude, _locationAttachment.longitude); } +- (void)locationManager:(CLLocationManager *)manager didUpdateHeading:(CLHeading *)newHeading +{ + if (newHeading != nil) { + _headingArrowView.hidden = false; + _headingArrowView.transform = CGAffineTransformMakeRotation(newHeading.magneticHeading / 180.0 * M_PI); + } +} + #pragma mark - - (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section diff --git a/submodules/LegacyComponents/Sources/TGMediaPickerGalleryVideoScrubber.h b/submodules/LegacyComponents/Sources/TGMediaPickerGalleryVideoScrubber.h index fcb8170187..1903dfcb0e 100644 --- a/submodules/LegacyComponents/Sources/TGMediaPickerGalleryVideoScrubber.h +++ b/submodules/LegacyComponents/Sources/TGMediaPickerGalleryVideoScrubber.h @@ -19,6 +19,7 @@ - (void)setDotVideoView:(UIView *)dotVideoView; - (void)setDotImage:(UIImage *)dotImage; +@property (nonatomic, assign) NSTimeInterval minimumLength; @property (nonatomic, assign) NSTimeInterval maximumLength; @property (nonatomic, assign) bool disableZoom; diff --git a/submodules/LegacyComponents/Sources/TGMediaPickerGalleryVideoScrubber.m b/submodules/LegacyComponents/Sources/TGMediaPickerGalleryVideoScrubber.m index 50dd34db80..476c89aae4 100644 --- a/submodules/LegacyComponents/Sources/TGMediaPickerGalleryVideoScrubber.m +++ b/submodules/LegacyComponents/Sources/TGMediaPickerGalleryVideoScrubber.m @@ -95,6 +95,7 @@ typedef enum if (self != nil) { _allowsTrimming = true; + _minimumLength = TGVideoScrubberMinimumTrimDuration; _currentTimeLabel = [[UILabel alloc] initWithFrame:CGRectMake(8, 4, 100, 15)]; _currentTimeLabel.font = TGSystemFontOfSize(12.0f); @@ -237,7 +238,7 @@ typedef enum NSTimeInterval duration = trimEndPosition - trimStartPosition; - if (trimEndPosition - trimStartPosition < TGVideoScrubberMinimumTrimDuration) + if (trimEndPosition - trimStartPosition < self.minimumLength) return; if (strongSelf.maximumLength > DBL_EPSILON && duration > strongSelf.maximumLength) @@ -300,7 +301,7 @@ typedef enum NSTimeInterval duration = trimEndPosition - trimStartPosition; - if (trimEndPosition - trimStartPosition < TGVideoScrubberMinimumTrimDuration) + if (trimEndPosition - trimStartPosition < self.minimumLength) return; if (strongSelf.maximumLength > DBL_EPSILON && duration > strongSelf.maximumLength) diff --git a/submodules/LegacyComponents/Sources/TGPhotoEditorController.m b/submodules/LegacyComponents/Sources/TGPhotoEditorController.m index ef921422b4..6a5e18795e 100644 --- a/submodules/LegacyComponents/Sources/TGPhotoEditorController.m +++ b/submodules/LegacyComponents/Sources/TGPhotoEditorController.m @@ -82,6 +82,7 @@ UIImage *_thumbnailImage; CMTime _chaseTime; + bool _chaseStart; bool _chasingTime; bool _isPlaying; AVPlayerItem *_playerItem; @@ -367,6 +368,7 @@ if ([self presentedForAvatarCreation] && _item.isVideo) { _scrubberView = [[TGMediaPickerGalleryVideoScrubber alloc] initWithFrame:CGRectMake(0.0f, 0.0, _portraitToolbarView.frame.size.width, 68.0f)]; + _scrubberView.minimumLength = 3.0; _scrubberView.layer.allowsGroupOpacity = true; _scrubberView.hasDotPicker = true; _scrubberView.dataSource = self; @@ -670,6 +672,9 @@ if (strongSelf != nil && !strongSelf->_dismissed) { [strongSelf->_player seekToTime:startTime]; [strongSelf->_scrubberView setValue:strongSelf.trimStartValue resetPosition:true]; + + [strongSelf->_fullEntitiesView seekTo:0.0]; + [strongSelf->_fullEntitiesView play]; } }]; } @@ -699,6 +704,11 @@ [_player addObserver:self forKeyPath:@"rate" options:NSKeyValueObservingOptionNew context:nil]; _registeredKeypathObserver = true; } + + [_fullEntitiesView seekTo:0.0]; + [_fullEntitiesView play]; + } else { + [_fullEntitiesView play]; } _isPlaying = true; @@ -722,6 +732,8 @@ } [_scrubberView setIsPlaying:false]; + } else { + [_fullEntitiesView pause]; } _isPlaying = false; @@ -748,6 +760,14 @@ } } +- (NSTimeInterval)currentTime { + return CMTimeGetSeconds(_player.currentItem.currentTime) - [self trimStartValue]; +} + +- (void)setMinimalVideoDuration:(NSTimeInterval)duration { + _scrubberView.minimumLength = duration; +} + - (void)seekVideo:(NSTimeInterval)position { CMTime targetTime = CMTimeMakeWithSeconds(position, NSEC_PER_SEC); @@ -766,6 +786,11 @@ CMTime currentChasingTime = _chaseTime; [_player.currentItem seekToTime:currentChasingTime toleranceBefore:kCMTimeZero toleranceAfter:kCMTimeZero completionHandler:^(BOOL finished) { + if (!_chaseStart) { + TGDispatchOnMainThread(^{ + [_fullEntitiesView seekTo:CMTimeGetSeconds(currentChasingTime) - _scrubberView.trimStartValue]; + }); + } if (CMTIME_COMPARE_INLINE(currentChasingTime, ==, _chaseTime)) { _chasingTime = false; _chaseTime = kCMTimeInvalid; @@ -1957,7 +1982,7 @@ generator.requestedTimeToleranceAfter = kCMTimeZero; generator.requestedTimeToleranceBefore = kCMTimeZero; - CGImageRef imageRef = [generator copyCGImageAtTime:CMTimeMakeWithSeconds(videoStartValue, NSEC_PER_SEC) actualTime:nil error:NULL]; + CGImageRef imageRef = [generator copyCGImageAtTime:CMTimeMakeWithSeconds(MIN(videoStartValue, CMTimeGetSeconds(asset.duration) - 0.05), NSEC_PER_SEC) actualTime:nil error:NULL]; UIImage *image = [UIImage imageWithCGImage:imageRef]; CGImageRelease(imageRef); @@ -2743,7 +2768,7 @@ }); } -- (void)videoScrubber:(TGMediaPickerGalleryVideoScrubber *)__unused videoScrubber valueDidChange:(NSTimeInterval)position +- (void)videoScrubber:(TGMediaPickerGalleryVideoScrubber *)videoScrubber valueDidChange:(NSTimeInterval)position { [self seekVideo:position]; } @@ -2780,6 +2805,8 @@ [self startVideoPlayback:true]; [self setPlayButtonHidden:true animated:false]; + + _chaseStart = false; } - (void)videoScrubber:(TGMediaPickerGalleryVideoScrubber *)videoScrubber editingStartValueDidChange:(NSTimeInterval)startValue @@ -2788,6 +2815,12 @@ _resetDotPosition = true; [self resetDotImage]; } + + if (!_chaseStart) { + _chaseStart = true; + [_fullEntitiesView resetToStart]; + } + [self seekVideo:startValue]; } diff --git a/submodules/LegacyComponents/Sources/TGPhotoEntitiesContainerView.h b/submodules/LegacyComponents/Sources/TGPhotoEntitiesContainerView.h index 0ebde1c945..00e2f53e88 100644 --- a/submodules/LegacyComponents/Sources/TGPhotoEntitiesContainerView.h +++ b/submodules/LegacyComponents/Sources/TGPhotoEntitiesContainerView.h @@ -14,6 +14,10 @@ @property (nonatomic, copy) void (^entityRemoved)(TGPhotoPaintEntityView *); - (void)updateVisibility:(bool)visible; +- (void)seekTo:(double)timestamp; +- (void)play; +- (void)pause; +- (void)resetToStart; - (UIColor *)colorAtPoint:(CGPoint)point; diff --git a/submodules/LegacyComponents/Sources/TGPhotoEntitiesContainerView.m b/submodules/LegacyComponents/Sources/TGPhotoEntitiesContainerView.m index f4e9b85126..7e6e7bcb04 100644 --- a/submodules/LegacyComponents/Sources/TGPhotoEntitiesContainerView.m +++ b/submodules/LegacyComponents/Sources/TGPhotoEntitiesContainerView.m @@ -40,6 +40,55 @@ } } +- (void)seekTo:(double)timestamp { + for (TGPhotoPaintEntityView *view in self.subviews) + { + if (![view isKindOfClass:[TGPhotoPaintEntityView class]]) + continue; + + if ([view isKindOfClass:[TGPhotoStickerEntityView class]]) { + [(TGPhotoStickerEntityView *)view seekTo:timestamp]; + } + } +} + +- (void)play { + for (TGPhotoPaintEntityView *view in self.subviews) + { + if (![view isKindOfClass:[TGPhotoPaintEntityView class]]) + continue; + + if ([view isKindOfClass:[TGPhotoStickerEntityView class]]) { + [(TGPhotoStickerEntityView *)view play]; + } + } +} + +- (void)pause { + for (TGPhotoPaintEntityView *view in self.subviews) + { + if (![view isKindOfClass:[TGPhotoPaintEntityView class]]) + continue; + + if ([view isKindOfClass:[TGPhotoStickerEntityView class]]) { + [(TGPhotoStickerEntityView *)view pause]; + } + } +} + + +- (void)resetToStart { + for (TGPhotoPaintEntityView *view in self.subviews) + { + if (![view isKindOfClass:[TGPhotoPaintEntityView class]]) + continue; + + if ([view isKindOfClass:[TGPhotoStickerEntityView class]]) { + [(TGPhotoStickerEntityView *)view resetToStart]; + } + } +} + - (BOOL)gestureRecognizer:(UIGestureRecognizer *)__unused gestureRecognizer shouldRecognizeSimultaneouslyWithGestureRecognizer:(UIGestureRecognizer *)__unused otherGestureRecognizer { return false; diff --git a/submodules/LegacyComponents/Sources/TGPhotoPaintController.m b/submodules/LegacyComponents/Sources/TGPhotoPaintController.m index a30a75dbfb..3fe8874b3e 100644 --- a/submodules/LegacyComponents/Sources/TGPhotoPaintController.m +++ b/submodules/LegacyComponents/Sources/TGPhotoPaintController.m @@ -1196,9 +1196,39 @@ const CGFloat TGPhotoPaintStickerKeyboardSize = 260.0f; TGPhotoPaintStickerEntity *entity = [[TGPhotoPaintStickerEntity alloc] initWithDocument:document baseSize:[self _stickerBaseSizeForCurrentPainting] animated:animated]; [self _setStickerEntityPosition:entity]; + bool hasStickers = false; + for (TGPhotoPaintEntityView *view in _entitiesContainerView.subviews) { + if ([view isKindOfClass:[TGPhotoStickerEntityView class]]) { + hasStickers = true; + break; + } + } + TGPhotoStickerEntityView *stickerView = (TGPhotoStickerEntityView *)[_entitiesContainerView createEntityViewWithEntity:entity]; [self _commonEntityViewSetup:stickerView]; + __weak TGPhotoPaintController *weakSelf = self; + __weak TGPhotoStickerEntityView *weakStickerView = stickerView; + stickerView.started = ^(double duration) { + __strong TGPhotoPaintController *strongSelf = weakSelf; + if (strongSelf != nil) { + TGPhotoEditorController *editorController = (TGPhotoEditorController *)self.parentViewController; + if (![editorController isKindOfClass:[TGPhotoEditorController class]]) + return; + + if (hasStickers) { + [editorController setMinimalVideoDuration:duration]; + } + + NSTimeInterval currentTime = editorController.currentTime; + __strong TGPhotoStickerEntityView *strongStickerView = weakStickerView; + if (strongStickerView != nil) { + [strongStickerView seekTo:currentTime]; + [strongStickerView play]; + } + } + }; + [self selectEntityView:stickerView]; _entitySelectionView.alpha = 0.0f; diff --git a/submodules/LegacyComponents/Sources/TGPhotoStickerEntityView.h b/submodules/LegacyComponents/Sources/TGPhotoStickerEntityView.h index 828588add6..e00badcf5b 100644 --- a/submodules/LegacyComponents/Sources/TGPhotoStickerEntityView.h +++ b/submodules/LegacyComponents/Sources/TGPhotoStickerEntityView.h @@ -9,6 +9,8 @@ @interface TGPhotoStickerEntityView : TGPhotoPaintEntityView +@property (nonatomic, copy) void(^started)(double); + @property (nonatomic, readonly) TGPhotoPaintStickerEntity *entity; @property (nonatomic, readonly) bool isMirrored; @@ -17,6 +19,10 @@ - (UIImage *)image; - (void)updateVisibility:(bool)visible; +- (void)seekTo:(double)timestamp; +- (void)play; +- (void)pause; +- (void)resetToStart; - (CGRect)realBounds; diff --git a/submodules/LegacyComponents/Sources/TGPhotoStickerEntityView.m b/submodules/LegacyComponents/Sources/TGPhotoStickerEntityView.m index a89a23735d..9e2712308d 100644 --- a/submodules/LegacyComponents/Sources/TGPhotoStickerEntityView.m +++ b/submodules/LegacyComponents/Sources/TGPhotoStickerEntityView.m @@ -55,6 +55,13 @@ const CGFloat TGPhotoStickerSelectionViewHandleSide = 30.0f; _mirrored = entity.isMirrored; _stickerView = [context stickerViewForDocument:entity.document]; + + __weak TGPhotoStickerEntityView *weakSelf = self; + _stickerView.started = ^(double duration) { + __strong TGPhotoStickerEntityView *strongSelf = weakSelf; + if (strongSelf != nil && strongSelf.started != nil) + strongSelf.started(duration); + }; [self addSubview:_stickerView]; _document = entity.document; @@ -178,6 +185,22 @@ const CGFloat TGPhotoStickerSelectionViewHandleSide = 30.0f; [_stickerView setIsVisible:visible]; } +- (void)seekTo:(double)timestamp { + [_stickerView seekTo:timestamp]; +} + +- (void)play { + [_stickerView play]; +} + +- (void)pause { + [_stickerView pause]; +} + +- (void)resetToStart { + [_stickerView resetToStart]; +} + @end diff --git a/submodules/LegacyMediaPickerUI/Sources/LegacyPaintStickerView.swift b/submodules/LegacyMediaPickerUI/Sources/LegacyPaintStickerView.swift index c6c9d72dac..595dea2b24 100644 --- a/submodules/LegacyMediaPickerUI/Sources/LegacyPaintStickerView.swift +++ b/submodules/LegacyMediaPickerUI/Sources/LegacyPaintStickerView.swift @@ -10,6 +10,8 @@ import StickerResources import LegacyComponents class LegacyPaintStickerView: UIView, TGPhotoPaintStickerRenderView { + var started: ((Double) -> Void)? + private let context: AccountContext private let file: TelegramMediaFile private var currentSize: CGSize? @@ -63,8 +65,16 @@ class LegacyPaintStickerView: UIView, TGPhotoPaintStickerRenderView { if self.animationNode == nil { let animationNode = AnimatedStickerNode() self.animationNode = animationNode - animationNode.started = { [weak self] in + animationNode.started = { [weak self, weak animationNode] in self?.imageNode.isHidden = true + + if let animationNode = animationNode { + let _ = (animationNode.status + |> take(1) + |> deliverOnMainQueue).start(next: { [weak self] status in + self?.started?(status.duration) + }) + } } self.addSubnode(animationNode) } @@ -115,6 +125,30 @@ class LegacyPaintStickerView: UIView, TGPhotoPaintStickerRenderView { } } + func seek(to timestamp: Double) { + self.isVisible = false + self.isPlaying = false + self.animationNode?.seekTo(.timestamp(timestamp)) + } + + func play() { + self.isVisible = true + self.isPlaying = true + self.animationNode?.play() + } + + func pause() { + self.isVisible = false + self.isPlaying = false + self.animationNode?.pause() + } + + func resetToStart() { + self.isVisible = false + self.isPlaying = false + self.animationNode?.seekTo(.timestamp(0.0)) + } + override func layoutSubviews() { super.layoutSubviews() diff --git a/submodules/LegacyMediaPickerUI/Sources/LegacyPaintStickersContext.swift b/submodules/LegacyMediaPickerUI/Sources/LegacyPaintStickersContext.swift index 0d8c690267..dbaebce170 100644 --- a/submodules/LegacyMediaPickerUI/Sources/LegacyPaintStickersContext.swift +++ b/submodules/LegacyMediaPickerUI/Sources/LegacyPaintStickersContext.swift @@ -188,8 +188,8 @@ private class LegacyPaintStickerEntity: LegacyPaintEntity { let maybeFrame = frameQueue.syncWith { frameQueue -> AnimatedStickerFrame? in var frame: AnimatedStickerFrame? - for _ in 0 ..< delta { - frame = frameQueue.take() + for i in 0 ..< delta { + frame = frameQueue.take(draw: i == delta - 1) } return frame } diff --git a/submodules/LocationUI/Sources/LocationAttributionItem.swift b/submodules/LocationUI/Sources/LocationAttributionItem.swift index 6f62515c4c..96f74ee56d 100644 --- a/submodules/LocationUI/Sources/LocationAttributionItem.swift +++ b/submodules/LocationUI/Sources/LocationAttributionItem.swift @@ -8,11 +8,18 @@ import TelegramPresentationData import ItemListUI import AppBundle +enum LocationAttribution: Equatable { + case foursquare + case google +} + class LocationAttributionItem: ListViewItem { let presentationData: ItemListPresentationData + let attribution: LocationAttribution - public init(presentationData: ItemListPresentationData) { + public init(presentationData: ItemListPresentationData, attribution: LocationAttribution) { self.presentationData = presentationData + self.attribution = attribution } public func nodeConfiguredForParams(async: @escaping (@escaping () -> Void) -> Void, params: ListViewItemLayoutParams, synchronousLoads: Bool, previousItem: ListViewItem?, nextItem: ListViewItem?, completion: @escaping (ListViewItemNode, @escaping () -> (Signal?, (ListViewItemApply) -> Void)) -> Void) { @@ -93,11 +100,20 @@ private class LocationAttributionItemNode: ListViewItemNode { strongSelf.layoutParams = params if let _ = updatedTheme { - strongSelf.imageNode.image = generateTintedImage(image: UIImage(bundleImageName: "Location/FoursquareAttribution"), color: item.presentationData.theme.list.itemSecondaryTextColor) + switch item.attribution { + case .foursquare: + strongSelf.imageNode.image = generateTintedImage(image: UIImage(bundleImageName: "Location/FoursquareAttribution"), color: item.presentationData.theme.list.itemSecondaryTextColor) + case .google: + if item.presentationData.theme.overallDarkAppearance { + strongSelf.imageNode.image = generateTintedImage(image: UIImage(bundleImageName: "Location/GoogleAttribution"), color: item.presentationData.theme.list.itemSecondaryTextColor) + } else { + strongSelf.imageNode.image = UIImage(bundleImageName: "Location/GoogleAttribution") + } + } } if let image = strongSelf.imageNode.image { - strongSelf.imageNode.frame = CGRect(x: floor((params.width - image.size.width) / 2.0), y: 0.0, width: image.size.width, height: image.size.height) + strongSelf.imageNode.frame = CGRect(x: floor((params.width - image.size.width) / 2.0), y: floor((contentSize.height - image.size.height) / 2.0), width: image.size.width, height: image.size.height) } } }) diff --git a/submodules/LocationUI/Sources/LocationMapNode.swift b/submodules/LocationUI/Sources/LocationMapNode.swift index 0576883bb4..c330c2017e 100644 --- a/submodules/LocationUI/Sources/LocationMapNode.swift +++ b/submodules/LocationUI/Sources/LocationMapNode.swift @@ -68,11 +68,30 @@ private class LocationMapView: MKMapView, UIGestureRecognizerDelegate { } } +private func generateHeadingArrowImage() -> UIImage? { + return generateImage(CGSize(width: 28.0, height: 28.0)) { size, context in + let bounds = CGRect(origin: CGPoint(), size: size) + context.clear(bounds) + + context.setFillColor(UIColor(rgb: 0x3393fe).cgColor) + + context.move(to: CGPoint(x: 14.0, y: 0.0)) + context.addLine(to: CGPoint(x: 19.0, y: 7.0)) + context.addLine(to: CGPoint(x: 9.0, y: 7.0)) + context.closePath() + context.fillPath() + + context.setBlendMode(.clear) + context.fillEllipse(in: bounds.insetBy(dx: 5.0, dy: 5.0)) + } +} + final class LocationMapNode: ASDisplayNode, MKMapViewDelegate { private let locationPromise = Promise(nil) private let pickerAnnotationContainerView: PickerAnnotationContainerView private weak var userLocationAnnotationView: MKAnnotationView? + private var headingArrowView: UIImageView? private let pinDisposable = MetaDisposable() @@ -103,6 +122,10 @@ final class LocationMapNode: ASDisplayNode, MKMapViewDelegate { override func didLoad() { super.didLoad() + self.headingArrowView = UIImageView() + self.headingArrowView?.frame = CGRect(origin: CGPoint(), size: CGSize(width: 28.0, height: 28.0)) + self.headingArrowView?.image = generateHeadingArrowImage() + self.mapView?.interactiveTransitionGestureRecognizerTest = { p in if p.x > 44.0 { return true @@ -232,6 +255,10 @@ final class LocationMapNode: ASDisplayNode, MKMapViewDelegate { for view in views { if view.annotation is MKUserLocation { self.userLocationAnnotationView = view + if let headingArrowView = self.headingArrowView { + view.addSubview(headingArrowView) + headingArrowView.center = CGPoint(x: view.frame.width / 2.0, y: view.frame.height / 2.0) + } if let annotationView = self.customUserLocationAnnotationView { view.addSubview(annotationView) } @@ -347,6 +374,18 @@ final class LocationMapNode: ASDisplayNode, MKMapViewDelegate { } } + var userHeading: CGFloat? = nil { + didSet { + if let heading = self.userHeading { + self.headingArrowView?.isHidden = false + self.headingArrowView?.transform = CGAffineTransform(rotationAngle: CGFloat(heading / 180.0 * CGFloat.pi)) + } else { + self.headingArrowView?.isHidden = true + self.headingArrowView?.transform = CGAffineTransform.identity + } + } + } + var annotations: [LocationPinAnnotation] = [] { didSet { guard let mapView = self.mapView else { diff --git a/submodules/LocationUI/Sources/LocationPickerController.swift b/submodules/LocationUI/Sources/LocationPickerController.swift index 1a150f9c18..3602c194f3 100644 --- a/submodules/LocationUI/Sources/LocationPickerController.swift +++ b/submodules/LocationUI/Sources/LocationPickerController.swift @@ -289,7 +289,7 @@ public final class LocationPickerController: ViewController { return } - self.displayNode = LocationPickerControllerNode(context: self.context, presentationData: self.presentationData, mode: self.mode, interaction: interaction) + self.displayNode = LocationPickerControllerNode(context: self.context, presentationData: self.presentationData, mode: self.mode, interaction: interaction, locationManager: self.locationManager) self.displayNodeDidLoad() self.permissionDisposable = (DeviceAccess.authorizationStatus(subject: .location(.send)) diff --git a/submodules/LocationUI/Sources/LocationPickerControllerNode.swift b/submodules/LocationUI/Sources/LocationPickerControllerNode.swift index 03b0ebcdb3..4aee790229 100644 --- a/submodules/LocationUI/Sources/LocationPickerControllerNode.swift +++ b/submodules/LocationUI/Sources/LocationPickerControllerNode.swift @@ -17,6 +17,7 @@ import AppBundle import CoreLocation import Geocoding import PhoneNumberFormat +import DeviceAccess private struct LocationPickerTransaction { let deletions: [ListViewDeleteItem] @@ -40,7 +41,7 @@ private enum LocationPickerEntry: Comparable, Identifiable { case liveLocation(PresentationTheme, String, String, CLLocationCoordinate2D?) case header(PresentationTheme, String) case venue(PresentationTheme, TelegramMediaMap, Int) - case attribution(PresentationTheme) + case attribution(PresentationTheme, LocationAttribution) var stableId: LocationPickerEntryId { switch self { @@ -83,8 +84,8 @@ private enum LocationPickerEntry: Comparable, Identifiable { } else { return false } - case let .attribution(lhsTheme): - if case let .attribution(rhsTheme) = rhs, lhsTheme === rhsTheme { + case let .attribution(lhsTheme, lhsAttribution): + if case let .attribution(rhsTheme, rhsAttribution) = rhs, lhsTheme === rhsTheme, lhsAttribution == rhsAttribution { return true } else { return false @@ -131,7 +132,7 @@ private enum LocationPickerEntry: Comparable, Identifiable { func item(account: Account, presentationData: PresentationData, interaction: LocationPickerInteraction?) -> ListViewItem { switch self { - case let .location(theme, title, subtitle, venue, coordinate): + case let .location(_, title, subtitle, venue, coordinate): let icon: LocationActionListItemIcon if let venue = venue { icon = .venue(venue) @@ -147,23 +148,23 @@ private enum LocationPickerEntry: Comparable, Identifiable { }, highlighted: { highlighted in interaction?.updateSendActionHighlight(highlighted) }) - case let .liveLocation(theme, title, subtitle, coordinate): + case let .liveLocation(_, title, subtitle, coordinate): return LocationActionListItem(presentationData: ItemListPresentationData(presentationData), account: account, title: title, subtitle: subtitle, icon: .liveLocation, action: { if let coordinate = coordinate { interaction?.sendLiveLocation(coordinate) } }) - case let .header(theme, title): + case let .header(_, title): return LocationSectionHeaderItem(presentationData: ItemListPresentationData(presentationData), title: title) - case let .venue(theme, venue, _): + case let .venue(_, venue, _): let venueType = venue.venue?.type ?? "" return ItemListVenueItem(presentationData: ItemListPresentationData(presentationData), account: account, venue: venue, style: .plain, action: { interaction?.sendVenue(venue) }, infoAction: ["home", "work"].contains(venueType) ? { interaction?.openHomeWorkInfo() } : nil) - case let .attribution(theme): - return LocationAttributionItem(presentationData: ItemListPresentationData(presentationData)) + case let .attribution(_, attribution): + return LocationAttributionItem(presentationData: ItemListPresentationData(presentationData), attribution: attribution) } } } @@ -240,12 +241,13 @@ struct LocationPickerState { } } -final class LocationPickerControllerNode: ViewControllerTracingNode { +final class LocationPickerControllerNode: ViewControllerTracingNode, CLLocationManagerDelegate { private let context: AccountContext private var presentationData: PresentationData private let presentationDataPromise: Promise private let mode: LocationPickerMode private let interaction: LocationPickerInteraction + private let locationManager: LocationManager private let listNode: ListView private let emptyResultsTextNode: ImmediateTextNode @@ -269,12 +271,13 @@ final class LocationPickerControllerNode: ViewControllerTracingNode { private var validLayout: (layout: ContainerViewLayout, navigationHeight: CGFloat)? private var listOffset: CGFloat? - init(context: AccountContext, presentationData: PresentationData, mode: LocationPickerMode, interaction: LocationPickerInteraction) { + init(context: AccountContext, presentationData: PresentationData, mode: LocationPickerMode, interaction: LocationPickerInteraction, locationManager: LocationManager) { self.context = context self.presentationData = presentationData self.presentationDataPromise = Promise(presentationData) self.mode = mode self.interaction = interaction + self.locationManager = locationManager self.state = LocationPickerState() self.statePromise = Promise(self.state) @@ -496,15 +499,21 @@ final class LocationPickerControllerNode: ViewControllerTracingNode { entries.append(.header(presentationData.theme, presentationData.strings.Map_ChooseAPlace.uppercased())) - var displayedVenues = foundVenues != nil || state.searchingVenuesAround ? foundVenues : venues + let displayedVenues = foundVenues != nil || state.searchingVenuesAround ? foundVenues : venues if let venues = displayedVenues { var index: Int = 0 + var attribution: LocationAttribution? for venue in venues { + if venue.venue?.provider == "foursquare" { + attribution = .foursquare + } else if venue.venue?.provider == "gplaces" { + attribution = .google + } entries.append(.venue(presentationData.theme, venue, index)) index += 1 } - if !venues.isEmpty { - entries.append(.attribution(presentationData.theme)) + if let attribution = attribution { + entries.append(.attribution(presentationData.theme, attribution)) } } let previousEntries = previousEntries.swap(entries) @@ -533,7 +542,7 @@ final class LocationPickerControllerNode: ViewControllerTracingNode { switch previousState.selectedLocation { case .none, .venue: updateMap = true - case let .location(previousCoordinate, address): + case let .location(previousCoordinate, _): if previousCoordinate != coordinate { updateMap = true } @@ -574,7 +583,7 @@ final class LocationPickerControllerNode: ViewControllerTracingNode { if let (layout, navigationBarHeight) = strongSelf.validLayout { var updateLayout = false - var transition: ContainedViewLayoutTransition = .animated(duration: 0.45, curve: .spring) + let transition: ContainedViewLayoutTransition = .animated(duration: 0.45, curve: .spring) if previousState.displayingMapModeOptions != state.displayingMapModeOptions { updateLayout = true @@ -685,11 +694,20 @@ final class LocationPickerControllerNode: ViewControllerTracingNode { strongSelf.goToUserLocation() } } + + self.locationManager.manager.startUpdatingHeading() + self.locationManager.manager.delegate = self } deinit { self.disposable?.dispose() self.geocodingDisposable.dispose() + + self.locationManager.manager.stopUpdatingHeading() + } + + func locationManager(_ manager: CLLocationManager, didUpdateHeading newHeading: CLHeading) { + self.headerNode.mapNode.userHeading = CGFloat(newHeading.magneticHeading) } func updatePresentationData(_ presentationData: PresentationData) { @@ -721,7 +739,7 @@ final class LocationPickerControllerNode: ViewControllerTracingNode { } private func dequeueTransition() { - guard let layout = self.validLayout, let transition = self.enqueuedTransitions.first else { + guard let _ = self.validLayout, let transition = self.enqueuedTransitions.first else { return } self.enqueuedTransitions.remove(at: 0) diff --git a/submodules/OpenInExternalAppUI/Sources/OpenInOptions.swift b/submodules/OpenInExternalAppUI/Sources/OpenInOptions.swift index 8d77aeeec4..e855eae738 100644 --- a/submodules/OpenInExternalAppUI/Sources/OpenInOptions.swift +++ b/submodules/OpenInExternalAppUI/Sources/OpenInOptions.swift @@ -181,7 +181,11 @@ private func allOpenInOptions(context: AccountContext, item: OpenInItem) -> [Ope if withDirections { return .openUrl(url: "comgooglemaps-x-callback://?daddr=\(coordinates)&directionsmode=driving&x-success=telegram://?resume=true&x-source=Telegram") } else { - return .openUrl(url: "comgooglemaps-x-callback://?center=\(coordinates)&q=\(coordinates)&x-success=telegram://?resume=true&x-source=Telegram") + if let venue = location.venue, let venueId = venue.id, let provider = venue.provider, provider == "gplaces" { + return .openUrl(url: "https://www.google.com/maps/search/?api=1&query=\(venue.address ?? "")&query_place_id=\(venueId)") + } else { + return .openUrl(url: "comgooglemaps-x-callback://?center=\(coordinates)&q=\(coordinates)&x-success=telegram://?resume=true&x-source=Telegram") + } } })) diff --git a/submodules/PeerAvatarGalleryUI/Sources/AvatarGalleryController.swift b/submodules/PeerAvatarGalleryUI/Sources/AvatarGalleryController.swift index 720e8d6bec..526c2943a1 100644 --- a/submodules/PeerAvatarGalleryUI/Sources/AvatarGalleryController.swift +++ b/submodules/PeerAvatarGalleryUI/Sources/AvatarGalleryController.swift @@ -149,11 +149,11 @@ public func initialAvatarGalleryEntries(account: Account, peer: Peer) -> Signal< } if let photo = initialPhoto { - if photo.immediateThumbnailData == nil { - return initialEntries - } else { - return [.image(photo.imageId, photo.reference, photo.representations.map({ ImageRepresentationWithReference(representation: $0, reference: MediaResourceReference.avatar(peer: peerReference, resource: $0.resource)) }), photo.videoRepresentations.map({ VideoRepresentationWithReference(representation: $0, reference: MediaResourceReference.avatarList(peer: peerReference, resource: $0.resource)) }), peer, nil, nil, nil, photo.immediateThumbnailData, nil)] + var representations = photo.representations.map({ ImageRepresentationWithReference(representation: $0, reference: MediaResourceReference.avatar(peer: peerReference, resource: $0.resource)) }) + if photo.immediateThumbnailData == nil, let firstEntry = initialEntries.first, let firstRepresentation = firstEntry.representations.first { + representations.insert(firstRepresentation, at: 0) } + return [.image(photo.imageId, photo.reference, representations, photo.videoRepresentations.map({ VideoRepresentationWithReference(representation: $0, reference: MediaResourceReference.avatarList(peer: peerReference, resource: $0.resource)) }), peer, nil, nil, nil, photo.immediateThumbnailData, nil)] } else { return [] } @@ -243,19 +243,24 @@ public func fetchedAvatarGalleryEntries(account: Account, peer: Peer, firstEntry var photosCount = photos.count for i in 0 ..< photos.count { let photo = photos[i] - if i == 0 && !initialMediaIds.contains(photo.image.imageId) { - photosCount += 1 - for entry in initialEntries { - let indexData = GalleryItemIndexData(position: index, totalCount: Int32(photosCount)) - if case let .image(image) = entry { - result.append(.image(image.0, image.1, image.2, image.3, image.4, nil, indexData, nil, image.8, nil)) - index += 1 + var representations = photo.image.representations.map({ ImageRepresentationWithReference(representation: $0, reference: MediaResourceReference.avatarList(peer: peerReference, resource: $0.resource)) }) + if i == 0 { + if !initialMediaIds.contains(photo.image.imageId) { + photosCount += 1 + for entry in initialEntries { + let indexData = GalleryItemIndexData(position: index, totalCount: Int32(photosCount)) + if case let .image(image) = entry { + result.append(.image(image.0, image.1, image.2, image.3, image.4, nil, indexData, nil, image.8, nil)) + index += 1 + } } + } else if photo.image.immediateThumbnailData == nil, let firstEntry = initialEntries.first, let firstRepresentation = firstEntry.representations.first { + representations.insert(firstRepresentation, at: 0) } } let indexData = GalleryItemIndexData(position: index, totalCount: Int32(photosCount)) - result.append(.image(photo.image.imageId, photo.image.reference, photo.image.representations.map({ ImageRepresentationWithReference(representation: $0, reference: MediaResourceReference.avatarList(peer: peerReference, resource: $0.resource)) }), photo.image.videoRepresentations.map({ VideoRepresentationWithReference(representation: $0, reference: MediaResourceReference.avatarList(peer: peerReference, resource: $0.resource)) }), peer, photo.date, indexData, photo.messageId, photo.image.immediateThumbnailData, nil)) + result.append(.image(photo.image.imageId, photo.image.reference, representations, photo.image.videoRepresentations.map({ VideoRepresentationWithReference(representation: $0, reference: MediaResourceReference.avatarList(peer: peerReference, resource: $0.resource)) }), peer, photo.date, indexData, photo.messageId, photo.image.immediateThumbnailData, nil)) index += 1 } } else { diff --git a/submodules/PeerInfoUI/Sources/ChannelInfoController.swift b/submodules/PeerInfoUI/Sources/ChannelInfoController.swift index 685be43fc3..369bbcc8e7 100644 --- a/submodules/PeerInfoUI/Sources/ChannelInfoController.swift +++ b/submodules/PeerInfoUI/Sources/ChannelInfoController.swift @@ -1076,8 +1076,8 @@ public func channelInfoController(context: AccountContext, peerId: PeerId) -> Vi } for childController in tabController.controllers { if let chatListController = childController as? ChatListController { - chatListController.maybeAskForPeerChatRemoval(peer: RenderedPeer(peer: peer), deleteGloballyIfPossible: deleteGloballyIfPossible, completion: { [weak navigationController] deleted in - if deleted { + chatListController.maybeAskForPeerChatRemoval(peer: RenderedPeer(peer: peer), joined: false, deleteGloballyIfPossible: deleteGloballyIfPossible, completion: { [weak navigationController] removed in + if removed { navigationController?.popToRoot(animated: true) } }, removed: { diff --git a/submodules/PeerInfoUI/Sources/GroupInfoController.swift b/submodules/PeerInfoUI/Sources/GroupInfoController.swift index 7b25fb885f..90bbff79e5 100644 --- a/submodules/PeerInfoUI/Sources/GroupInfoController.swift +++ b/submodules/PeerInfoUI/Sources/GroupInfoController.swift @@ -2439,7 +2439,7 @@ public func groupInfoController(context: AccountContext, peerId originalPeerId: } for childController in tabController.controllers { if let chatListController = childController as? ChatListController { - chatListController.maybeAskForPeerChatRemoval(peer: RenderedPeer(peer: peer), deleteGloballyIfPossible: deleteGloballyIfPossible, completion: { [weak navigationController] removed in + chatListController.maybeAskForPeerChatRemoval(peer: RenderedPeer(peer: peer), joined: false, deleteGloballyIfPossible: deleteGloballyIfPossible, completion: { [weak navigationController] removed in if removed { navigationController?.popToRoot(animated: true) } diff --git a/submodules/SettingsUI/Sources/Text Size/TextSizeSelectionController.swift b/submodules/SettingsUI/Sources/Text Size/TextSizeSelectionController.swift index f2b4aafdf5..9c01ad1baa 100644 --- a/submodules/SettingsUI/Sources/Text Size/TextSizeSelectionController.swift +++ b/submodules/SettingsUI/Sources/Text Size/TextSizeSelectionController.swift @@ -214,7 +214,7 @@ private final class TextSizeSelectionControllerNode: ASDisplayNode, UIScrollView var items: [ChatListItem] = [] let interaction = ChatListNodeInteraction(activateSearch: {}, peerSelected: { _, _ in }, disabledPeerSelected: { _ in }, togglePeerSelected: { _ in }, additionalCategorySelected: { _ in - }, messageSelected: { _, _, _ in}, groupSelected: { _ in }, addContact: { _ in }, setPeerIdWithRevealedOptions: { _, _ in }, setItemPinned: { _, _ in }, setPeerMuted: { _, _ in }, deletePeer: { _ in }, updatePeerGrouping: { _, _ in }, togglePeerMarkedUnread: { _, _ in}, toggleArchivedFolderHiddenByDefault: {}, hidePsa: { _ in + }, messageSelected: { _, _, _ in}, groupSelected: { _ in }, addContact: { _ in }, setPeerIdWithRevealedOptions: { _, _ in }, setItemPinned: { _, _ in }, setPeerMuted: { _, _ in }, deletePeer: { _, _ in }, updatePeerGrouping: { _, _ in }, togglePeerMarkedUnread: { _, _ in}, toggleArchivedFolderHiddenByDefault: {}, hidePsa: { _ in }, activateChatPreview: { _, _, gesture in gesture?.cancel() }, present: { _ in }) diff --git a/submodules/SettingsUI/Sources/Themes/ThemeAccentColorControllerNode.swift b/submodules/SettingsUI/Sources/Themes/ThemeAccentColorControllerNode.swift index d30215f04e..1c834b6617 100644 --- a/submodules/SettingsUI/Sources/Themes/ThemeAccentColorControllerNode.swift +++ b/submodules/SettingsUI/Sources/Themes/ThemeAccentColorControllerNode.swift @@ -767,7 +767,7 @@ final class ThemeAccentColorControllerNode: ASDisplayNode, UIScrollViewDelegate var items: [ChatListItem] = [] let interaction = ChatListNodeInteraction(activateSearch: {}, peerSelected: { _, _ in }, disabledPeerSelected: { _ in }, togglePeerSelected: { _ in }, additionalCategorySelected: { _ in - }, messageSelected: { _, _, _ in}, groupSelected: { _ in }, addContact: { _ in }, setPeerIdWithRevealedOptions: { _, _ in }, setItemPinned: { _, _ in }, setPeerMuted: { _, _ in }, deletePeer: { _ in }, updatePeerGrouping: { _, _ in }, togglePeerMarkedUnread: { _, _ in}, toggleArchivedFolderHiddenByDefault: {}, hidePsa: { _ in + }, messageSelected: { _, _, _ in}, groupSelected: { _ in }, addContact: { _ in }, setPeerIdWithRevealedOptions: { _, _ in }, setItemPinned: { _, _ in }, setPeerMuted: { _, _ in }, deletePeer: { _, _ in }, updatePeerGrouping: { _, _ in }, togglePeerMarkedUnread: { _, _ in}, toggleArchivedFolderHiddenByDefault: {}, hidePsa: { _ in }, activateChatPreview: { _, _, gesture in gesture?.cancel() }, present: { _ in diff --git a/submodules/SettingsUI/Sources/Themes/ThemePreviewControllerNode.swift b/submodules/SettingsUI/Sources/Themes/ThemePreviewControllerNode.swift index c273166376..7eb50d2710 100644 --- a/submodules/SettingsUI/Sources/Themes/ThemePreviewControllerNode.swift +++ b/submodules/SettingsUI/Sources/Themes/ThemePreviewControllerNode.swift @@ -351,7 +351,7 @@ final class ThemePreviewControllerNode: ASDisplayNode, UIScrollViewDelegate { var items: [ChatListItem] = [] let interaction = ChatListNodeInteraction(activateSearch: {}, peerSelected: { _, _ in }, disabledPeerSelected: { _ in }, togglePeerSelected: { _ in }, additionalCategorySelected: { _ in - }, messageSelected: { _, _, _ in}, groupSelected: { _ in }, addContact: { _ in }, setPeerIdWithRevealedOptions: { _, _ in }, setItemPinned: { _, _ in }, setPeerMuted: { _, _ in }, deletePeer: { _ in }, updatePeerGrouping: { _, _ in }, togglePeerMarkedUnread: { _, _ in}, toggleArchivedFolderHiddenByDefault: {}, hidePsa: { _ in + }, messageSelected: { _, _, _ in}, groupSelected: { _ in }, addContact: { _ in }, setPeerIdWithRevealedOptions: { _, _ in }, setItemPinned: { _, _ in }, setPeerMuted: { _, _ in }, deletePeer: { _, _ in }, updatePeerGrouping: { _, _ in }, togglePeerMarkedUnread: { _, _ in}, toggleArchivedFolderHiddenByDefault: {}, hidePsa: { _ in }, activateChatPreview: { _, _, gesture in gesture?.cancel() }, present: { _ in diff --git a/submodules/TelegramAudio/Sources/ManagedAudioSession.swift b/submodules/TelegramAudio/Sources/ManagedAudioSession.swift index 058077e14e..5624714423 100644 --- a/submodules/TelegramAudio/Sources/ManagedAudioSession.swift +++ b/submodules/TelegramAudio/Sources/ManagedAudioSession.swift @@ -244,6 +244,20 @@ public final class ManagedAudioSession { if let availableInputs = audioSession.availableInputs { var hasHeadphones = false + + var headphonesAreActive = false + loop: for currentOutput in audioSession.currentRoute.outputs { + switch currentOutput.portType { + case .headphones, .bluetoothA2DP, .bluetoothHFP: + headphonesAreActive = true + hasHeadphones = true + activeOutput = .headphones + break loop + default: + break + } + } + for input in availableInputs { var isActive = false for currentInput in audioSession.currentRoute.inputs { @@ -253,7 +267,7 @@ public final class ManagedAudioSession { } if input.portType == .builtInMic { - if isActive { + if isActive && !headphonesAreActive { activeOutput = .builtin inner: for currentOutput in audioSession.currentRoute.outputs { if currentOutput.portType == .builtInSpeaker { @@ -739,13 +753,28 @@ public final class ManagedAudioSession { case .voiceCall, .playWithPossiblePortOverride, .record(true): try AVAudioSession.sharedInstance().overrideOutputAudioPort(.none) if let routes = AVAudioSession.sharedInstance().availableInputs { - for route in routes { - if route.portType == .builtInMic { - if case .record = updatedType, self.isHeadsetPluggedInValue { - } else { + var alreadySet = false + if self.isHeadsetPluggedInValue { + loop: for route in routes { + switch route.portType { + case .headphones, .bluetoothA2DP, .bluetoothHFP: let _ = try? AVAudioSession.sharedInstance().setPreferredInput(route) + alreadySet = true + break loop + default: + break + } + } + } + if !alreadySet { + for route in routes { + if route.portType == .builtInMic { + if case .record = updatedType, self.isHeadsetPluggedInValue { + } else { + let _ = try? AVAudioSession.sharedInstance().setPreferredInput(route) + } + break } - break } } } diff --git a/submodules/TelegramCallsUI/Sources/CallControllerButton.swift b/submodules/TelegramCallsUI/Sources/CallControllerButton.swift index ad64ab15cc..5b324d7b39 100644 --- a/submodules/TelegramCallsUI/Sources/CallControllerButton.swift +++ b/submodules/TelegramCallsUI/Sources/CallControllerButton.swift @@ -31,6 +31,13 @@ final class CallControllerButtonItemNode: HighlightTrackingButtonNode { var appearance: Appearance var image: Image + var isEnabled: Bool + + init(appearance: Appearance, image: Image, isEnabled: Bool = true) { + self.appearance = appearance + self.image = image + self.isEnabled = isEnabled + } } private let contentContainer: ASDisplayNode @@ -107,6 +114,9 @@ final class CallControllerButtonItemNode: HighlightTrackingButtonNode { self.effectView.isHidden = true } + self.alpha = content.isEnabled ? 1.0 : 0.7 + self.isUserInteractionEnabled = content.isEnabled + let contentImage = generateImage(CGSize(width: self.largeButtonSize, height: self.largeButtonSize), contextGenerator: { size, context in context.clear(CGRect(origin: CGPoint(), size: size)) diff --git a/submodules/TelegramCallsUI/Sources/CallControllerButtonsNode.swift b/submodules/TelegramCallsUI/Sources/CallControllerButtonsNode.swift index 31a79f72ca..64d6445e4a 100644 --- a/submodules/TelegramCallsUI/Sources/CallControllerButtonsNode.swift +++ b/submodules/TelegramCallsUI/Sources/CallControllerButtonsNode.swift @@ -17,7 +17,7 @@ enum CallControllerButtonsSpeakerMode { enum CallControllerButtonsMode: Equatable { enum VideoState: Equatable { case notAvailable - case possible + case possible(Bool) case outgoingRequested case incomingRequested case active @@ -52,7 +52,7 @@ private enum ButtonDescription: Equatable { case accept case end(EndType) - case enableCamera(Bool) + case enableCamera(Bool, Bool) case switchCamera case soundOutput(SoundOutput) case mute(Bool) @@ -203,12 +203,15 @@ final class CallControllerButtonsNode: ASDisplayNode { switch videoState { case .active, .possible, .incomingRequested, .outgoingRequested: let isCameraActive: Bool - if case .possible = videoState { + let isCameraEnabled: Bool + if case let .possible(value) = videoState { isCameraActive = false + isCameraEnabled = value } else { isCameraActive = !self.isCameraPaused + isCameraEnabled = true } - topButtons.append(.enableCamera(isCameraActive)) + topButtons.append(.enableCamera(isCameraActive, isCameraEnabled)) topButtons.append(.mute(self.isMuted)) if case .possible = videoState { topButtons.append(.soundOutput(soundOutput)) @@ -252,10 +255,13 @@ final class CallControllerButtonsNode: ASDisplayNode { switch videoState { case .active, .incomingRequested, .outgoingRequested: let isCameraActive: Bool - if case .possible = videoState { + let isCameraEnabled: Bool + if case let .possible(value) = videoState { isCameraActive = false + isCameraEnabled = value } else { isCameraActive = !self.isCameraPaused + isCameraEnabled = true } var topButtons: [ButtonDescription] = [] @@ -272,7 +278,7 @@ final class CallControllerButtonsNode: ASDisplayNode { soundOutput = .bluetooth } - topButtons.append(.enableCamera(isCameraActive)) + topButtons.append(.enableCamera(isCameraActive, isCameraEnabled)) topButtons.append(.mute(isMuted)) topButtons.append(.switchCamera) topButtons.append(.end(.end)) @@ -304,7 +310,7 @@ final class CallControllerButtonsNode: ASDisplayNode { soundOutput = .bluetooth } - topButtons.append(.enableCamera(false)) + topButtons.append(.enableCamera(false, true)) topButtons.append(.mute(self.isMuted)) topButtons.append(.soundOutput(soundOutput)) @@ -373,10 +379,11 @@ final class CallControllerButtonsNode: ASDisplayNode { case .end: buttonText = strings.Call_End } - case let .enableCamera(isEnabled): + case let .enableCamera(isActivated, isEnabled): buttonContent = CallControllerButtonItemNode.Content( - appearance: .blurred(isFilled: isEnabled), - image: .camera + appearance: .blurred(isFilled: isActivated), + image: .camera, + isEnabled: isEnabled ) buttonText = strings.Call_Camera case .switchCamera: diff --git a/submodules/TelegramCallsUI/Sources/CallControllerNode.swift b/submodules/TelegramCallsUI/Sources/CallControllerNode.swift index 1b469a0dd4..87721711ae 100644 --- a/submodules/TelegramCallsUI/Sources/CallControllerNode.swift +++ b/submodules/TelegramCallsUI/Sources/CallControllerNode.swift @@ -34,16 +34,25 @@ private final class CallVideoNode: ASDisplayNode { private(set) var isReady: Bool = false private var isReadyTimer: SwiftSignalKit.Timer? - init(videoView: PresentationCallVideoView, isReadyUpdated: @escaping () -> Void) { + private let isFlippedUpdated: () -> Void + + private(set) var currentOrientation: PresentationCallVideoView.Orientation + + init(videoView: PresentationCallVideoView, isReadyUpdated: @escaping () -> Void, orientationUpdated: @escaping () -> Void, isFlippedUpdated: @escaping () -> Void) { self.isReadyUpdated = isReadyUpdated + self.isFlippedUpdated = isFlippedUpdated self.videoTransformContainer = ASDisplayNode() self.videoTransformContainer.clipsToBounds = true self.videoView = videoView - self.videoView.view.layer.transform = CATransform3DMakeScale(-1.0, 1.0, 1.0) + videoView.view.clipsToBounds = true + + self.currentOrientation = videoView.getOrientation() super.init() + self.backgroundColor = .black + self.videoTransformContainer.view.addSubview(self.videoView.view) self.addSubnode(self.videoTransformContainer) @@ -58,6 +67,16 @@ private final class CallVideoNode: ASDisplayNode { } } + self.videoView.setOnOrientationUpdated { [weak self] orientation in + guard let strongSelf = self else { + return + } + if strongSelf.currentOrientation != orientation { + strongSelf.currentOrientation = orientation + orientationUpdated() + } + } + self.isReadyTimer = SwiftSignalKit.Timer(timeout: 3.0, repeat: false, completion: { [weak self] in guard let strongSelf = self else { return @@ -75,28 +94,80 @@ private final class CallVideoNode: ASDisplayNode { } func updateLayout(size: CGSize, cornerRadius: CGFloat, transition: ContainedViewLayoutTransition) { - let videoFrame = CGRect(origin: CGPoint(), size: size) + self.currentCornerRadius = cornerRadius - let previousVideoFrame = self.videoTransformContainer.frame - self.videoTransformContainer.frame = videoFrame - if transition.isAnimated && !videoFrame.height.isZero && !previousVideoFrame.height.isZero { - transition.animatePositionAdditive(node: self.videoTransformContainer, offset: CGPoint(x: previousVideoFrame.midX - videoFrame.midX, y: previousVideoFrame.midY - videoFrame.midY)) - transition.animateTransformScale(node: self.videoTransformContainer, from: previousVideoFrame.height / videoFrame.height) + var rotationAngle: CGFloat + var rotateFrame: Bool + switch self.currentOrientation { + case .rotation0: + rotationAngle = 0.0 + rotateFrame = false + case .rotation90: + rotationAngle = -CGFloat.pi / 2.0 + rotateFrame = true + case .rotation180: + rotationAngle = -CGFloat.pi + rotateFrame = false + case .rotation270: + rotationAngle = -CGFloat.pi * 3.0 / 2.0 + rotateFrame = true + } + var originalRotateFrame = rotateFrame + if size.width > size.height { + rotateFrame = !rotateFrame + if rotateFrame { + originalRotateFrame = true + } + } else { + if rotateFrame { + originalRotateFrame = false + } + } + let videoFrame: CGRect + let scale: CGFloat + if rotateFrame { + let frameSize = CGSize(width: size.height, height: size.width).aspectFitted(size) + videoFrame = CGRect(origin: CGPoint(x: floor((size.width - frameSize.width) / 2.0), y: floor((size.height - frameSize.height) / 2.0)), size: frameSize) + if size.width > size.height { + scale = frameSize.height / size.width + } else { + scale = frameSize.width / size.height + } + } else { + videoFrame = CGRect(origin: CGPoint(), size: size) + if size.width > size.height { + scale = 1.0 + } else { + scale = 1.0 + } } - self.videoView.view.frame = videoFrame + let previousVideoFrame = self.videoTransformContainer.frame + self.videoTransformContainer.bounds = CGRect(origin: CGPoint(), size: size) + if transition.isAnimated && !videoFrame.height.isZero && !previousVideoFrame.height.isZero { + transition.animateTransformScale(node: self.videoTransformContainer, from: previousVideoFrame.height / size.height, additive: true) + } + transition.updatePosition(node: self.videoTransformContainer, position: videoFrame.center) + transition.updateSublayerTransformScale(node: self.videoTransformContainer, scale: scale) + + let localVideoSize = originalRotateFrame ? CGSize(width: size.height, height: size.width) : size + let localVideoFrame = CGRect(origin: CGPoint(x: floor((size.width - localVideoSize.width) / 2.0), y: floor((size.height - localVideoSize.height) / 2.0)), size: localVideoSize) + + self.videoView.view.bounds = localVideoFrame + self.videoView.view.center = localVideoFrame.center + transition.updateTransformRotation(view: self.videoView.view, angle: rotationAngle) if let effectView = self.effectView { - effectView.frame = videoFrame - transition.animatePositionAdditive(layer: effectView.layer, offset: CGPoint(x: previousVideoFrame.midX - videoFrame.midX, y: previousVideoFrame.midY - videoFrame.midY)) - transition.animateTransformScale(view: effectView, from: previousVideoFrame.height / videoFrame.height) + transition.updateFrame(view: effectView, frame: videoFrame) } transition.updateCornerRadius(layer: self.videoTransformContainer.layer, cornerRadius: self.currentCornerRadius) if let effectView = self.effectView { transition.updateCornerRadius(layer: effectView.layer, cornerRadius: self.currentCornerRadius) } + + transition.updateCornerRadius(layer: self.layer, cornerRadius: self.currentCornerRadius) } func updateIsBlurred(isBlurred: Bool) { @@ -178,6 +249,7 @@ final class CallControllerNode: ViewControllerTracingNode, CallControllerNodePro private let keyButtonNode: HighlightableButtonNode private var validLayout: (ContainerViewLayout, CGFloat)? + private var disableActionsUntilTimestamp: Double = 0.0 var isMuted: Bool = false { didSet { @@ -318,25 +390,38 @@ final class CallControllerNode: ViewControllerTracingNode, CallControllerNodePro } self.buttonsNode.toggleVideo = { [weak self] in - guard let strongSelf = self else { + guard let strongSelf = self, let callState = strongSelf.callState else { return } - if strongSelf.outgoingVideoNodeValue == nil { - strongSelf.call.requestVideo() - } else { - strongSelf.isVideoPaused = !strongSelf.isVideoPaused - strongSelf.outgoingVideoNodeValue?.updateIsBlurred(isBlurred: strongSelf.isVideoPaused) - strongSelf.buttonsNode.isCameraPaused = strongSelf.isVideoPaused - strongSelf.setIsVideoPaused?(strongSelf.isVideoPaused) - - if let (layout, navigationBarHeight) = strongSelf.validLayout { - strongSelf.containerLayoutUpdated(layout, navigationBarHeight: navigationBarHeight, transition: .animated(duration: 0.3, curve: .easeInOut)) + switch callState.state { + case .active: + if strongSelf.outgoingVideoNodeValue == nil { + strongSelf.call.requestVideo() + } else { + strongSelf.isVideoPaused = !strongSelf.isVideoPaused + strongSelf.outgoingVideoNodeValue?.updateIsBlurred(isBlurred: strongSelf.isVideoPaused) + strongSelf.buttonsNode.isCameraPaused = strongSelf.isVideoPaused + strongSelf.setIsVideoPaused?(strongSelf.isVideoPaused) + + if let (layout, navigationBarHeight) = strongSelf.validLayout { + strongSelf.containerLayoutUpdated(layout, navigationBarHeight: navigationBarHeight, transition: .animated(duration: 0.3, curve: .easeInOut)) + } } + default: + break } } self.buttonsNode.rotateCamera = { [weak self] in - self?.call.switchVideoCamera() + guard let strongSelf = self else { + return + } + strongSelf.call.switchVideoCamera() + if let outgoingVideoNode = strongSelf.outgoingVideoNodeValue { + if let (layout, navigationBarHeight) = strongSelf.validLayout { + strongSelf.containerLayoutUpdated(layout, navigationBarHeight: navigationBarHeight, transition: .immediate) + } + } } self.keyButtonNode.addTarget(self, action: #selector(self.keyPressed), forControlEvents: .touchUpInside) @@ -347,7 +432,16 @@ final class CallControllerNode: ViewControllerTracingNode, CallControllerNodePro override func didLoad() { super.didLoad() - let panRecognizer = UIPanGestureRecognizer(target: self, action: #selector(self.panGesture(_:))) + let panRecognizer = CallPanGestureRecognizer(target: self, action: #selector(self.panGesture(_:))) + panRecognizer.shouldBegin = { [weak self] _ in + guard let strongSelf = self else { + return false + } + if strongSelf.areUserActionsDisabledNow() { + return false + } + return true + } self.view.addGestureRecognizer(panRecognizer) let tapRecognizer = UITapGestureRecognizer(target: self, action: #selector(self.tapGesture(_:))) @@ -387,6 +481,23 @@ final class CallControllerNode: ViewControllerTracingNode, CallControllerNodePro if self.audioOutputState?.0 != availableOutputs || self.audioOutputState?.1 != currentOutput { self.audioOutputState = (availableOutputs, currentOutput) self.updateButtonsMode() + + self.setupAudioOutputs() + } + } + + private func setupAudioOutputs() { + if self.outgoingVideoNodeValue != nil { + if let audioOutputState = self.audioOutputState, let currentOutput = audioOutputState.currentOutput { + switch currentOutput { + case .headphones: + break + case let .port(port) where port.type == .bluetooth: + break + default: + self.setCurrentAudioOutput?(.speaker) + } + } } } @@ -412,6 +523,20 @@ final class CallControllerNode: ViewControllerTracingNode, CallControllerNodePro if let (layout, navigationBarHeight) = strongSelf.validLayout { strongSelf.containerLayoutUpdated(layout, navigationBarHeight: navigationBarHeight, transition: .animated(duration: 0.5, curve: .spring)) } + }, orientationUpdated: { + guard let strongSelf = self else { + return + } + if let (layout, navigationBarHeight) = strongSelf.validLayout { + strongSelf.containerLayoutUpdated(layout, navigationBarHeight: navigationBarHeight, transition: .animated(duration: 0.3, curve: .easeInOut)) + } + }, isFlippedUpdated: { + guard let strongSelf = self else { + return + } + if let (layout, navigationBarHeight) = strongSelf.validLayout { + strongSelf.containerLayoutUpdated(layout, navigationBarHeight: navigationBarHeight, transition: .immediate) + } }) strongSelf.incomingVideoNodeValue = incomingVideoNode strongSelf.expandedVideoNode = incomingVideoNode @@ -437,15 +562,21 @@ final class CallControllerNode: ViewControllerTracingNode, CallControllerNodePro if let outgoingVideoView = outgoingVideoView { outgoingVideoView.view.backgroundColor = .black outgoingVideoView.view.clipsToBounds = true - if let audioOutputState = strongSelf.audioOutputState, let currentOutput = audioOutputState.currentOutput { - switch currentOutput { - case .speaker, .builtin: - break - default: - strongSelf.setCurrentAudioOutput?(.speaker) + let outgoingVideoNode = CallVideoNode(videoView: outgoingVideoView, isReadyUpdated: {}, orientationUpdated: { + guard let strongSelf = self else { + return } - } - let outgoingVideoNode = CallVideoNode(videoView: outgoingVideoView, isReadyUpdated: {}) + if let (layout, navigationBarHeight) = strongSelf.validLayout { + strongSelf.containerLayoutUpdated(layout, navigationBarHeight: navigationBarHeight, transition: .animated(duration: 0.3, curve: .easeInOut)) + } + }, isFlippedUpdated: { + guard let strongSelf = self else { + return + } + if let (layout, navigationBarHeight) = strongSelf.validLayout { + strongSelf.containerLayoutUpdated(layout, navigationBarHeight: navigationBarHeight, transition: .immediate) + } + }) strongSelf.outgoingVideoNodeValue = outgoingVideoNode strongSelf.minimizedVideoNode = outgoingVideoNode if let expandedVideoNode = strongSelf.expandedVideoNode { @@ -456,6 +587,7 @@ final class CallControllerNode: ViewControllerTracingNode, CallControllerNodePro if let (layout, navigationBarHeight) = strongSelf.validLayout { strongSelf.containerLayoutUpdated(layout, navigationBarHeight: navigationBarHeight, transition: .animated(duration: 0.4, curve: .spring)) } + strongSelf.setupAudioOutputs() } }) } @@ -626,7 +758,14 @@ final class CallControllerNode: ViewControllerTracingNode, CallControllerNodePro case .notAvailable: mappedVideoState = .notAvailable case .possible: - mappedVideoState = .possible + var isEnabled = false + switch callState.state { + case .active: + isEnabled = true + default: + break + } + mappedVideoState = .possible(isEnabled) case .outgoingRequested: mappedVideoState = .outgoingRequested case .incomingRequested: @@ -654,8 +793,6 @@ final class CallControllerNode: ViewControllerTracingNode, CallControllerNodePro } if let (layout, navigationHeight) = self.validLayout { - self.pictureInPictureTransitionFraction = 0.0 - self.containerLayoutUpdated(layout, navigationBarHeight: navigationHeight, transition: .animated(duration: 0.3, curve: .spring)) } } @@ -678,7 +815,10 @@ final class CallControllerNode: ViewControllerTracingNode, CallControllerNodePro func animateOut(completion: @escaping () -> Void) { self.statusBar.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3, removeOnCompletion: false) if !self.shouldStayHiddenUntilConnection || self.containerNode.alpha > 0.0 { - self.containerNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3, removeOnCompletion: false) + self.containerNode.layer.allowsGroupOpacity = true + self.containerNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3, removeOnCompletion: false, completion: { [weak self] _ in + self?.containerNode.layer.allowsGroupOpacity = true + }) self.containerNode.layer.animateScale(from: 1.0, to: 1.04, duration: 0.3, removeOnCompletion: false, completion: { _ in completion() }) @@ -723,7 +863,15 @@ final class CallControllerNode: ViewControllerTracingNode, CallControllerNodePro insets.right = interpolate(from: expandedInset, to: insets.right, value: 1.0 - self.pictureInPictureTransitionFraction) let previewVideoSide = interpolate(from: 350.0, to: 200.0, value: 1.0 - self.pictureInPictureTransitionFraction) - let previewVideoSize = layout.size.aspectFitted(CGSize(width: previewVideoSide, height: previewVideoSide)) + var previewVideoSize = layout.size.aspectFitted(CGSize(width: previewVideoSide, height: previewVideoSide)) + if let minimizedVideoNode = minimizedVideoNode { + switch minimizedVideoNode.currentOrientation { + case .rotation90, .rotation270: + previewVideoSize = CGSize(width: previewVideoSize.height, height: previewVideoSize.width) + default: + break + } + } let previewVideoY: CGFloat let previewVideoX: CGFloat @@ -852,6 +1000,7 @@ final class CallControllerNode: ViewControllerTracingNode, CallControllerNodePro transition.updateAlpha(node: self.buttonsNode, alpha: overlayAlpha) let fullscreenVideoFrame = CGRect(origin: CGPoint(), size: layout.size) + let previewVideoFrame = self.calculatePreviewVideoRect(layout: layout, navigationHeight: navigationBarHeight) if let expandedVideoNode = self.expandedVideoNode { @@ -933,6 +1082,10 @@ final class CallControllerNode: ViewControllerTracingNode, CallControllerNodePro private var debugTapCounter: (Double, Int) = (0.0, 0) + private func areUserActionsDisabledNow() -> Bool { + return CACurrentMediaTime() < self.disableActionsUntilTimestamp + } + @objc func tapGesture(_ recognizer: UITapGestureRecognizer) { if case .ended = recognizer.state { if !self.pictureInPictureTransitionFraction.isZero { @@ -947,17 +1100,20 @@ final class CallControllerNode: ViewControllerTracingNode, CallControllerNodePro if let expandedVideoNode = self.expandedVideoNode, let minimizedVideoNode = self.minimizedVideoNode { let point = recognizer.location(in: recognizer.view) if minimizedVideoNode.frame.contains(point) { - let copyView = minimizedVideoNode.view.snapshotView(afterScreenUpdates: false) - copyView?.frame = minimizedVideoNode.frame - self.expandedVideoNode = minimizedVideoNode - self.minimizedVideoNode = expandedVideoNode - if let supernode = expandedVideoNode.supernode { - supernode.insertSubnode(expandedVideoNode, aboveSubnode: minimizedVideoNode) - } - if let (layout, navigationBarHeight) = self.validLayout { - self.disableAnimationForExpandedVideoOnce = true - self.animationForExpandedVideoSnapshotView = copyView - self.containerLayoutUpdated(layout, navigationBarHeight: navigationBarHeight, transition: .animated(duration: 0.3, curve: .easeInOut)) + if !self.areUserActionsDisabledNow() { + let copyView = minimizedVideoNode.view.snapshotView(afterScreenUpdates: false) + copyView?.frame = minimizedVideoNode.frame + self.expandedVideoNode = minimizedVideoNode + self.minimizedVideoNode = expandedVideoNode + if let supernode = expandedVideoNode.supernode { + supernode.insertSubnode(expandedVideoNode, aboveSubnode: minimizedVideoNode) + } + self.disableActionsUntilTimestamp = CACurrentMediaTime() + 0.3 + if let (layout, navigationBarHeight) = self.validLayout { + self.disableAnimationForExpandedVideoOnce = true + self.animationForExpandedVideoSnapshotView = copyView + self.containerLayoutUpdated(layout, navigationBarHeight: navigationBarHeight, transition: .animated(duration: 0.3, curve: .easeInOut)) + } } } else { var updated = false @@ -1135,19 +1291,23 @@ final class CallControllerNode: ViewControllerTracingNode, CallControllerNodePro } } - @objc private func panGesture(_ recognizer: UIPanGestureRecognizer) { + @objc private func panGesture(_ recognizer: CallPanGestureRecognizer) { switch recognizer.state { case .began: - let location = recognizer.location(in: self.view) - if self.self.pictureInPictureTransitionFraction.isZero, let _ = self.expandedVideoNode, let minimizedVideoNode = self.minimizedVideoNode, minimizedVideoNode.frame.contains(location) { + guard let location = recognizer.firstLocation else { + return + } + if self.pictureInPictureTransitionFraction.isZero, let expandedVideoNode = self.expandedVideoNode, let minimizedVideoNode = self.minimizedVideoNode, minimizedVideoNode.frame.contains(location), expandedVideoNode.frame != minimizedVideoNode.frame { self.minimizedVideoInitialPosition = minimizedVideoNode.position - } else { + } else if let _ = self.expandedVideoNode, let _ = self.minimizedVideoNode { self.minimizedVideoInitialPosition = nil if !self.pictureInPictureTransitionFraction.isZero { self.pictureInPictureGestureState = .dragging(initialPosition: self.containerTransformationNode.position, draggingPosition: self.containerTransformationNode.position) } else { self.pictureInPictureGestureState = .collapsing(didSelectCorner: false) } + } else { + self.pictureInPictureGestureState = .none } case .changed: if let minimizedVideoNode = self.minimizedVideoNode, let minimizedVideoInitialPosition = self.minimizedVideoInitialPosition { @@ -1266,3 +1426,38 @@ final class CallControllerNode: ViewControllerTracingNode, CallControllerNodePro return nil } } + +private final class CallPanGestureRecognizer: UIPanGestureRecognizer { + private(set) var firstLocation: CGPoint? + + public var shouldBegin: ((CGPoint) -> Bool)? + + override public init(target: Any?, action: Selector?) { + super.init(target: target, action: action) + + self.maximumNumberOfTouches = 1 + } + + override public func reset() { + super.reset() + + self.firstLocation = nil + } + + override public func touchesBegan(_ touches: Set, with event: UIEvent) { + super.touchesBegan(touches, with: event) + + let touch = touches.first! + let point = touch.location(in: self.view) + if let shouldBegin = self.shouldBegin, !shouldBegin(point) { + self.state = .failed + return + } + + self.firstLocation = point + } + + override public func touchesMoved(_ touches: Set, with event: UIEvent) { + super.touchesMoved(touches, with: event) + } +} diff --git a/submodules/TelegramCallsUI/Sources/PresentationCall.swift b/submodules/TelegramCallsUI/Sources/PresentationCall.swift index f23d965608..bb46fe074b 100644 --- a/submodules/TelegramCallsUI/Sources/PresentationCall.swift +++ b/submodules/TelegramCallsUI/Sources/PresentationCall.swift @@ -810,10 +810,45 @@ public final class PresentationCallImpl: PresentationCall { self.ongoingContext?.makeIncomingVideoView(completion: { view in if let view = view { let setOnFirstFrameReceived = view.setOnFirstFrameReceived + let setOnOrientationUpdated = view.setOnOrientationUpdated completion(PresentationCallVideoView( view: view.view, setOnFirstFrameReceived: { f in setOnFirstFrameReceived(f) + }, + getOrientation: { [weak view] in + if let view = view { + let mappedValue: PresentationCallVideoView.Orientation + switch view.getOrientation() { + case .rotation0: + mappedValue = .rotation0 + case .rotation90: + mappedValue = .rotation90 + case .rotation180: + mappedValue = .rotation180 + case .rotation270: + mappedValue = .rotation270 + } + return mappedValue + } else { + return .rotation0 + } + }, + setOnOrientationUpdated: { f in + setOnOrientationUpdated { value in + let mappedValue: PresentationCallVideoView.Orientation + switch value { + case .rotation0: + mappedValue = .rotation0 + case .rotation90: + mappedValue = .rotation90 + case .rotation180: + mappedValue = .rotation180 + case .rotation270: + mappedValue = .rotation270 + } + f?(mappedValue) + } } )) } else { @@ -831,11 +866,47 @@ public final class PresentationCallImpl: PresentationCall { self.videoCapturer?.makeOutgoingVideoView(completion: { view in if let view = view { let setOnFirstFrameReceived = view.setOnFirstFrameReceived + let setOnOrientationUpdated = view.setOnOrientationUpdated completion(PresentationCallVideoView( view: view.view, setOnFirstFrameReceived: { f in setOnFirstFrameReceived(f) + }, + getOrientation: { [weak view] in + if let view = view { + let mappedValue: PresentationCallVideoView.Orientation + switch view.getOrientation() { + case .rotation0: + mappedValue = .rotation0 + case .rotation90: + mappedValue = .rotation90 + case .rotation180: + mappedValue = .rotation180 + case .rotation270: + mappedValue = .rotation270 + } + return mappedValue + } else { + return .rotation0 + } + }, + setOnOrientationUpdated: { f in + setOnOrientationUpdated { value in + let mappedValue: PresentationCallVideoView.Orientation + switch value { + case .rotation0: + mappedValue = .rotation0 + case .rotation90: + mappedValue = .rotation90 + case .rotation180: + mappedValue = .rotation180 + case .rotation270: + mappedValue = .rotation270 + } + f?(mappedValue) + } } + )) } else { completion(nil) diff --git a/submodules/TelegramUI/Images.xcassets/Location/GoogleAttribution.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Location/GoogleAttribution.imageset/Contents.json new file mode 100644 index 0000000000..67b12b6b3f --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Location/GoogleAttribution.imageset/Contents.json @@ -0,0 +1,22 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "powered_by_google_on_white@2x.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "powered_by_google_on_white@3x.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/submodules/TelegramUI/Images.xcassets/Location/GoogleAttribution.imageset/powered_by_google_on_white@2x.png b/submodules/TelegramUI/Images.xcassets/Location/GoogleAttribution.imageset/powered_by_google_on_white@2x.png new file mode 100644 index 0000000000..4676cb59be Binary files /dev/null and b/submodules/TelegramUI/Images.xcassets/Location/GoogleAttribution.imageset/powered_by_google_on_white@2x.png differ diff --git a/submodules/TelegramUI/Images.xcassets/Location/GoogleAttribution.imageset/powered_by_google_on_white@3x.png b/submodules/TelegramUI/Images.xcassets/Location/GoogleAttribution.imageset/powered_by_google_on_white@3x.png new file mode 100644 index 0000000000..64b2cab5aa Binary files /dev/null and b/submodules/TelegramUI/Images.xcassets/Location/GoogleAttribution.imageset/powered_by_google_on_white@3x.png differ diff --git a/submodules/TelegramUI/Sources/ChatController.swift b/submodules/TelegramUI/Sources/ChatController.swift index 9a1d426759..9456cf9c90 100644 --- a/submodules/TelegramUI/Sources/ChatController.swift +++ b/submodules/TelegramUI/Sources/ChatController.swift @@ -570,6 +570,9 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G if strongSelf.presentationInterfaceState.interfaceState.selectionState != nil { return } + + strongSelf.dismissAllTooltips() + let recognizer: TapLongTapOrDoubleTapGestureRecognizer? = anyRecognizer as? TapLongTapOrDoubleTapGestureRecognizer let gesture: ContextGesture? = anyRecognizer as? ContextGesture if let messages = strongSelf.chatDisplayNode.historyNode.messageGroupInCurrentHistoryView(message.id) { @@ -1974,7 +1977,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G }) }, displaySwipeToReplyHint: { [weak self] in if let strongSelf = self, let validLayout = strongSelf.validLayout, min(validLayout.size.width, validLayout.size.height) > 320.0 { - strongSelf.present(UndoOverlayController(presentationData: strongSelf.presentationData, content: .swipeToReply(title: strongSelf.presentationData.strings.Conversation_SwipeToReplyHintTitle, text: strongSelf.presentationData.strings.Conversation_SwipeToReplyHintText), elevatedLayout: true, action: { _ in return false }), in: .window(.root)) + strongSelf.present(UndoOverlayController(presentationData: strongSelf.presentationData, content: .swipeToReply(title: strongSelf.presentationData.strings.Conversation_SwipeToReplyHintTitle, text: strongSelf.presentationData.strings.Conversation_SwipeToReplyHintText), elevatedLayout: false, action: { _ in return false }), in: .current) } }, dismissReplyMarkupMessage: { [weak self] message in guard let strongSelf = self, strongSelf.presentationInterfaceState.keyboardButtonsMessage?.id == message.id else { @@ -2033,6 +2036,13 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G guard let strongSelf = self else { return } + + if strongSelf.presentationInterfaceState.interfaceState.selectionState != nil { + return + } + + strongSelf.dismissAllTooltips() + let context = strongSelf.context let _ = (context.account.postbox.transaction { transaction -> Peer? in return transaction.getPeer(peer.id) @@ -5217,18 +5227,6 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G self.dismissAllTooltips() - self.window?.forEachController({ controller in - if let controller = controller as? UndoOverlayController { - controller.dismissWithCommitAction() - } - }) - self.forEachController({ controller in - if let controller = controller as? TooltipScreen { - controller.dismiss() - } - return true - }) - self.sendMessageActionsController?.dismiss() if let _ = self.peekData { @@ -6066,7 +6064,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G disposable.set((signal |> deliverOnMainQueue).start(completed: { [weak self] in if let strongSelf = self, let _ = strongSelf.validLayout { - strongSelf.present(UndoOverlayController(presentationData: presentationData, content: .succeed(text: presentationData.strings.ClearCache_Success("\(dataSizeString(selectedSize, decimalSeparator: presentationData.dateTimeFormat.decimalSeparator))", stringForDeviceType()).0), elevatedLayout: true, action: { _ in return false }), in: .current) + strongSelf.present(UndoOverlayController(presentationData: presentationData, content: .succeed(text: presentationData.strings.ClearCache_Success("\(dataSizeString(selectedSize, decimalSeparator: presentationData.dateTimeFormat.decimalSeparator))", stringForDeviceType()).0), elevatedLayout: false, action: { _ in return false }), in: .current) } })) @@ -7107,12 +7105,12 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G } } if let value = value { - self.present(UndoOverlayController(presentationData: self.presentationData, content: .dice(dice: dice, account: self.context.account, text: value, action: canSendMessagesToChat(self.presentationInterfaceState) ? self.presentationData.strings.Conversation_SendDice : nil), elevatedLayout: true, action: { [weak self] action in + self.present(UndoOverlayController(presentationData: self.presentationData, content: .dice(dice: dice, account: self.context.account, text: value, action: canSendMessagesToChat(self.presentationInterfaceState) ? self.presentationData.strings.Conversation_SendDice : nil), elevatedLayout: false, action: { [weak self] action in if let strongSelf = self, canSendMessagesToChat(strongSelf.presentationInterfaceState), action == .undo { strongSelf.sendMessages([.message(text: "", attributes: [], mediaReference: AnyMediaReference.standalone(media: TelegramMediaDice(emoji: dice.emoji)), replyToMessageId: nil, localGroupingKey: nil)]) } return false - }), in: .window(.root)) + }), in: .current) } } @@ -9168,6 +9166,21 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G self.silentPostTooltipController?.dismiss() self.mediaRecordingModeTooltipController?.dismiss() self.mediaRestrictedTooltipController?.dismiss() + + self.window?.forEachController({ controller in + if let controller = controller as? UndoOverlayController { + controller.dismissWithCommitAction() + } + }) + self.forEachController({ controller in + if let controller = controller as? UndoOverlayController { + controller.dismissWithCommitAction() + } + if let controller = controller as? TooltipScreen { + controller.dismiss() + } + return true + }) } private func commitPurposefulAction() { diff --git a/submodules/TelegramUI/Sources/ChatInterfaceStateContextMenus.swift b/submodules/TelegramUI/Sources/ChatInterfaceStateContextMenus.swift index ceaa5643f0..5d66223437 100644 --- a/submodules/TelegramUI/Sources/ChatInterfaceStateContextMenus.swift +++ b/submodules/TelegramUI/Sources/ChatInterfaceStateContextMenus.swift @@ -974,10 +974,14 @@ func chatAvailableMessageActionsImpl(postbox: Postbox, accountPeerId: PeerId, me optionsMap[id]!.insert(.deleteLocally) } else if let peer = transaction.getPeer(id.peerId) { var isAction = false + var isDice = false for media in message.media { if media is TelegramMediaAction || media is TelegramMediaExpiredContent { isAction = true } + if media is TelegramMediaDice { + isDice = true + } } if let channel = peer as? TelegramChannel { if message.flags.contains(.Incoming) { @@ -1064,6 +1068,11 @@ func chatAvailableMessageActionsImpl(postbox: Postbox, accountPeerId: PeerId, me } else if limitsConfiguration.canRemoveIncomingMessagesInPrivateChats { canDeleteGlobally = true } + + let timestamp = Int32(CFAbsoluteTimeGetCurrent() + NSTimeIntervalSince1970) + if isDice && Int64(message.timestamp) + 60 * 60 * 24 > Int64(timestamp) { + canDeleteGlobally = false + } if message.flags.contains(.Incoming) { hadPersonalIncoming = true } diff --git a/submodules/TelegramUI/Sources/ChatSearchResultsContollerNode.swift b/submodules/TelegramUI/Sources/ChatSearchResultsContollerNode.swift index 399e715d5a..a69e9c64c8 100644 --- a/submodules/TelegramUI/Sources/ChatSearchResultsContollerNode.swift +++ b/submodules/TelegramUI/Sources/ChatSearchResultsContollerNode.swift @@ -183,7 +183,7 @@ class ChatSearchResultsControllerNode: ViewControllerTracingNode, UIScrollViewDe }, setPeerIdWithRevealedOptions: { _, _ in }, setItemPinned: { _, _ in }, setPeerMuted: { _, _ in - }, deletePeer: { _ in + }, deletePeer: { _, _ in }, updatePeerGrouping: { _, _ in }, togglePeerMarkedUnread: { _, _ in }, toggleArchivedFolderHiddenByDefault: { diff --git a/submodules/TelegramUI/Sources/PeerInfo/PeerInfoScreen.swift b/submodules/TelegramUI/Sources/PeerInfo/PeerInfoScreen.swift index c112f505e9..66f5041b99 100644 --- a/submodules/TelegramUI/Sources/PeerInfo/PeerInfoScreen.swift +++ b/submodules/TelegramUI/Sources/PeerInfo/PeerInfoScreen.swift @@ -3837,7 +3837,7 @@ private final class PeerInfoScreenNode: ViewControllerTracingNode, UIScrollViewD } for childController in tabController.controllers { if let chatListController = childController as? ChatListController { - chatListController.maybeAskForPeerChatRemoval(peer: RenderedPeer(peer: peer), deleteGloballyIfPossible: globally, completion: { [weak navigationController] deleted in + chatListController.maybeAskForPeerChatRemoval(peer: RenderedPeer(peer: peer), joined: false, deleteGloballyIfPossible: globally, completion: { [weak navigationController] deleted in if deleted { navigationController?.popToRoot(animated: true) } @@ -4032,7 +4032,7 @@ private final class PeerInfoScreenNode: ViewControllerTracingNode, UIScrollViewD return mapResourceToAvatarSizes(postbox: account.postbox, resource: resource, representations: representations) }) } else { - return updatePeerPhoto(postbox: account.postbox, network: account.network, stateManager: account.stateManager, accountPeerId: account.peerId, peerId: peerId, photo: uploadedPeerPhoto(postbox: account.postbox, network: account.network, resource: photoResource), video: uploadedPeerVideo(postbox: account.postbox, network: account.network, messageMediaPreuploadManager: account.messageMediaPreuploadManager, resource: videoResource) |> map(Optional.init), mapResourceToAvatarSizes: { resource, representations in + return updatePeerPhoto(postbox: account.postbox, network: account.network, stateManager: account.stateManager, accountPeerId: account.peerId, peerId: peerId, photo: uploadedPeerPhoto(postbox: account.postbox, network: account.network, resource: photoResource), video: uploadedPeerVideo(postbox: account.postbox, network: account.network, messageMediaPreuploadManager: account.messageMediaPreuploadManager, resource: videoResource) |> map(Optional.init), videoStartTimestamp: videoStartTimestamp, mapResourceToAvatarSizes: { resource, representations in return mapResourceToAvatarSizes(postbox: account.postbox, resource: resource, representations: representations) }) } diff --git a/submodules/TelegramUniversalVideoContent/Sources/WebEmbedPlayerNode.swift b/submodules/TelegramUniversalVideoContent/Sources/WebEmbedPlayerNode.swift index aa1329692a..40cf0fec67 100644 --- a/submodules/TelegramUniversalVideoContent/Sources/WebEmbedPlayerNode.swift +++ b/submodules/TelegramUniversalVideoContent/Sources/WebEmbedPlayerNode.swift @@ -88,24 +88,24 @@ final class WebEmbedPlayerNode: ASDisplayNode, WKNavigationDelegate { let userContentController = WKUserContentController() userContentController.addUserScript(WKUserScript(source: "var meta = document.createElement('meta'); meta.setAttribute('name', 'viewport'); meta.setAttribute('content', 'width=device-width'); document.getElementsByTagName('head')[0].appendChild(meta)", injectionTime: .atDocumentEnd, forMainFrameOnly: true)) - let config = WKWebViewConfiguration() - config.allowsInlineMediaPlayback = true - config.userContentController = userContentController + let configuration = WKWebViewConfiguration() + configuration.allowsInlineMediaPlayback = true + configuration.userContentController = userContentController if #available(iOSApplicationExtension 10.0, iOS 10.0, *) { - config.mediaTypesRequiringUserActionForPlayback = [] + configuration.mediaTypesRequiringUserActionForPlayback = [] } else if #available(iOSApplicationExtension 9.0, iOS 9.0, *) { - config.requiresUserActionForMediaPlayback = false + configuration.requiresUserActionForMediaPlayback = false } else { - config.mediaPlaybackRequiresUserAction = false + configuration.mediaPlaybackRequiresUserAction = false } if #available(iOSApplicationExtension 9.0, iOS 9.0, *) { - config.allowsPictureInPictureMediaPlayback = false + configuration.allowsPictureInPictureMediaPlayback = false } let frame = CGRect(origin: CGPoint.zero, size: intrinsicDimensions) - self.webView = WKWebView(frame: frame, configuration: config) + self.webView = WKWebView(frame: frame, configuration: configuration) super.init() self.frame = frame diff --git a/submodules/TelegramVoip/Sources/OngoingCallContext.swift b/submodules/TelegramVoip/Sources/OngoingCallContext.swift index decd2602b9..c627757858 100644 --- a/submodules/TelegramVoip/Sources/OngoingCallContext.swift +++ b/submodules/TelegramVoip/Sources/OngoingCallContext.swift @@ -318,6 +318,11 @@ public final class OngoingCallVideoCapturer { view: view, setOnFirstFrameReceived: { [weak view] f in view?.setOnFirstFrameReceived(f) + }, + getOrientation: { + return .rotation0 + }, + setOnOrientationUpdated: { _ in } )) } else { @@ -403,16 +408,46 @@ private extension OngoingCallContextState.State { } } +public enum OngoingCallVideoOrientation { + case rotation0 + case rotation90 + case rotation180 + case rotation270 +} + +private extension OngoingCallVideoOrientation { + init(_ orientation: OngoingCallVideoOrientationWebrtc) { + switch orientation { + case .orientation0: + self = .rotation0 + case .orientation90: + self = .rotation90 + case .orientation180: + self = .rotation180 + case .orientation270: + self = .rotation270 + @unknown default: + self = .rotation0 + } + } +} + public final class OngoingCallContextPresentationCallVideoView { public let view: UIView public let setOnFirstFrameReceived: ((() -> Void)?) -> Void + public let getOrientation: () -> OngoingCallVideoOrientation + public let setOnOrientationUpdated: (((OngoingCallVideoOrientation) -> Void)?) -> Void public init( view: UIView, - setOnFirstFrameReceived: @escaping ((() -> Void)?) -> Void + setOnFirstFrameReceived: @escaping ((() -> Void)?) -> Void, + getOrientation: @escaping () -> OngoingCallVideoOrientation, + setOnOrientationUpdated: @escaping (((OngoingCallVideoOrientation) -> Void)?) -> Void ) { self.view = view self.setOnFirstFrameReceived = setOnFirstFrameReceived + self.getOrientation = getOrientation + self.setOnOrientationUpdated = setOnOrientationUpdated } } @@ -721,6 +756,18 @@ public final class OngoingCallContext { view: view, setOnFirstFrameReceived: { [weak view] f in view?.setOnFirstFrameReceived(f) + }, + getOrientation: { [weak view] in + if let view = view { + return OngoingCallVideoOrientation(view.orientation) + } else { + return .rotation0 + } + }, + setOnOrientationUpdated: { [weak view] f in + view?.setOnOrientationUpdated { value in + f?(OngoingCallVideoOrientation(value)) + } } )) } else { diff --git a/submodules/TgVoipWebrtc/PublicHeaders/TgVoip/OngoingCallThreadLocalContext.h b/submodules/TgVoipWebrtc/PublicHeaders/TgVoip/OngoingCallThreadLocalContext.h index aa26669cba..503f47828b 100644 --- a/submodules/TgVoipWebrtc/PublicHeaders/TgVoip/OngoingCallThreadLocalContext.h +++ b/submodules/TgVoipWebrtc/PublicHeaders/TgVoip/OngoingCallThreadLocalContext.h @@ -41,6 +41,13 @@ typedef NS_ENUM(int32_t, OngoingCallRemoteVideoStateWebrtc) { OngoingCallRemoteVideoStateActive }; +typedef NS_ENUM(int32_t, OngoingCallVideoOrientationWebrtc) { + OngoingCallVideoOrientation0, + OngoingCallVideoOrientation90, + OngoingCallVideoOrientation180, + OngoingCallVideoOrientation270 +}; + typedef NS_ENUM(int32_t, OngoingCallNetworkTypeWebrtc) { OngoingCallNetworkTypeWifi, OngoingCallNetworkTypeCellularGprs, @@ -87,7 +94,10 @@ typedef NS_ENUM(int32_t, OngoingCallDataSavingWebrtc) { @protocol OngoingCallThreadLocalContextWebrtcVideoView +@property (nonatomic, readonly) OngoingCallVideoOrientationWebrtc orientation; + - (void)setOnFirstFrameReceived:(void (^ _Nullable)())onFirstFrameReceived; +- (void)setOnOrientationUpdated:(void (^ _Nullable)(OngoingCallVideoOrientationWebrtc))onOrientationUpdated; @end diff --git a/submodules/TgVoipWebrtc/Sources/OngoingCallThreadLocalContext.mm b/submodules/TgVoipWebrtc/Sources/OngoingCallThreadLocalContext.mm index 6cc65df000..1fcc1289f0 100644 --- a/submodules/TgVoipWebrtc/Sources/OngoingCallThreadLocalContext.mm +++ b/submodules/TgVoipWebrtc/Sources/OngoingCallThreadLocalContext.mm @@ -42,20 +42,66 @@ @end -@interface VideoMetalView (VideoViewImpl) +@protocol OngoingCallThreadLocalContextWebrtcVideoViewImpl + +@property (nonatomic, readwrite) OngoingCallVideoOrientationWebrtc orientation; + +@end + +@interface VideoMetalView (VideoViewImpl) + +@property (nonatomic, readwrite) OngoingCallVideoOrientationWebrtc orientation; @end @implementation VideoMetalView (VideoViewImpl) +- (OngoingCallVideoOrientationWebrtc)orientation { + return (OngoingCallVideoOrientationWebrtc)self.internalOrientation; +} + +- (void)setOrientation:(OngoingCallVideoOrientationWebrtc)orientation { + [self setInternalOrientation:(int)orientation]; +} + +- (void)setOnOrientationUpdated:(void (^ _Nullable)(OngoingCallVideoOrientationWebrtc))onOrientationUpdated { + if (onOrientationUpdated) { + [self internalSetOnOrientationUpdated:^(int value) { + onOrientationUpdated((OngoingCallVideoOrientationWebrtc)value); + }]; + } else { + [self internalSetOnOrientationUpdated:nil]; + } +} + @end -@interface GLVideoView (VideoViewImpl) +@interface GLVideoView (VideoViewImpl) + +@property (nonatomic, readwrite) OngoingCallVideoOrientationWebrtc orientation; @end @implementation GLVideoView (VideoViewImpl) +- (OngoingCallVideoOrientationWebrtc)orientation { + return (OngoingCallVideoOrientationWebrtc)self.internalOrientation; +} + +- (void)setOrientation:(OngoingCallVideoOrientationWebrtc)orientation { + [self setInternalOrientation:(int)orientation]; +} + +- (void)setOnOrientationUpdated:(void (^ _Nullable)(OngoingCallVideoOrientationWebrtc))onOrientationUpdated { + if (onOrientationUpdated) { + [self internalSetOnOrientationUpdated:^(int value) { + onOrientationUpdated((OngoingCallVideoOrientationWebrtc)value); + }]; + } else { + [self internalSetOnOrientationUpdated:nil]; + } +} + @end @implementation OngoingCallThreadLocalContextVideoCapturer @@ -68,6 +114,9 @@ return self; } +- (void)dealloc { +} + - (void)switchVideoCamera { _interface->switchCamera(); } @@ -140,6 +189,8 @@ OngoingCallVideoStateWebrtc _videoState; bool _connectedOnce; OngoingCallRemoteVideoStateWebrtc _remoteVideoState; + OngoingCallVideoOrientationWebrtc _remoteVideoOrientation; + __weak UIView *_currentRemoteVideoRenderer; OngoingCallThreadLocalContextVideoCapturer *_videoCapturer; int32_t _signalBars; @@ -267,6 +318,8 @@ static void (*InternalVoipLoggingFunction)(NSString *) = NULL; _remoteVideoState = OngoingCallRemoteVideoStateActive; } + _remoteVideoOrientation = OngoingCallVideoOrientation0; + std::vector derivedStateValue; derivedStateValue.resize(derivedState.length); [derivedState getBytes:derivedStateValue.data() length:derivedState.length]; @@ -568,6 +621,8 @@ static void (*InternalVoipLoggingFunction)(NSString *) = NULL; std::shared_ptr> sink = [remoteRenderer getSink]; __strong OngoingCallThreadLocalContextWebrtc *strongSelf = weakSelf; if (strongSelf) { + [remoteRenderer setOrientation:strongSelf->_remoteVideoOrientation]; + strongSelf->_currentRemoteVideoRenderer = remoteRenderer; strongSelf->_tgVoip->setIncomingVideoOutput(sink); } @@ -578,6 +633,8 @@ static void (*InternalVoipLoggingFunction)(NSString *) = NULL; std::shared_ptr> sink = [remoteRenderer getSink]; __strong OngoingCallThreadLocalContextWebrtc *strongSelf = weakSelf; if (strongSelf) { + [remoteRenderer setOrientation:strongSelf->_remoteVideoOrientation]; + strongSelf->_currentRemoteVideoRenderer = remoteRenderer; strongSelf->_tgVoip->setIncomingVideoOutput(sink); } diff --git a/submodules/TgVoipWebrtc/tgcalls b/submodules/TgVoipWebrtc/tgcalls index 8e9d3e56d4..c3345bb26a 160000 --- a/submodules/TgVoipWebrtc/tgcalls +++ b/submodules/TgVoipWebrtc/tgcalls @@ -1 +1 @@ -Subproject commit 8e9d3e56d43ffa4ed9ababd5fe7a4b5df8ec94d1 +Subproject commit c3345bb26aba541c99ff3c7075bda8024c7a8202