Apply fixes

This commit is contained in:
Ilya Laktyushin 2020-07-30 12:23:25 +03:00
parent e28ec6b7ca
commit 214f5a682f
64 changed files with 1295 additions and 464 deletions

View File

@ -3,7 +3,7 @@
include Utils.makefile include Utils.makefile
APP_VERSION="6.3" APP_VERSION="6.3.1"
CORE_COUNT=$(shell sysctl -n hw.logicalcpu) CORE_COUNT=$(shell sysctl -n hw.logicalcpu)
CORE_COUNT_MINUS_ONE=$(shell expr ${CORE_COUNT} \- 1) CORE_COUNT_MINUS_ONE=$(shell expr ${CORE_COUNT} \- 1)

View File

@ -10,5 +10,5 @@ public protocol ChatListController: ViewController {
func activateSearch() func activateSearch()
func deactivateSearch(animated: Bool) func deactivateSearch(animated: Bool)
func activateCompose() 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)
} }

View File

@ -69,15 +69,29 @@ public struct PresentationCallState: Equatable {
} }
public final class PresentationCallVideoView { public final class PresentationCallVideoView {
public enum Orientation {
case rotation0
case rotation90
case rotation180
case rotation270
}
public let view: UIView public let view: UIView
public let setOnFirstFrameReceived: ((() -> Void)?) -> Void public let setOnFirstFrameReceived: ((() -> Void)?) -> Void
public let getOrientation: () -> Orientation
public let setOnOrientationUpdated: (((Orientation) -> Void)?) -> Void
public init( public init(
view: UIView, view: UIView,
setOnFirstFrameReceived: @escaping ((() -> Void)?) -> Void setOnFirstFrameReceived: @escaping ((() -> Void)?) -> Void,
getOrientation: @escaping () -> Orientation,
setOnOrientationUpdated: @escaping (((Orientation) -> Void)?) -> Void
) { ) {
self.view = view self.view = view
self.setOnFirstFrameReceived = setOnFirstFrameReceived self.setOnFirstFrameReceived = setOnFirstFrameReceived
self.getOrientation = getOrientation
self.setOnOrientationUpdated = setOnOrientationUpdated
} }
} }

View File

@ -52,6 +52,7 @@ public enum AnimatedStickerMode {
public enum AnimatedStickerPlaybackPosition { public enum AnimatedStickerPlaybackPosition {
case start case start
case end case end
case timestamp(Double)
} }
public enum AnimatedStickerPlaybackMode { public enum AnimatedStickerPlaybackMode {
@ -83,8 +84,9 @@ public final class AnimatedStickerFrame {
public protocol AnimatedStickerFrameSource: class { public protocol AnimatedStickerFrameSource: class {
var frameRate: Int { get } var frameRate: Int { get }
var frameCount: Int { get } var frameCount: Int { get }
var frameIndex: Int { get }
func takeFrame() -> AnimatedStickerFrame? func takeFrame(draw: Bool) -> AnimatedStickerFrame?
func skipToEnd() func skipToEnd()
} }
@ -109,7 +111,7 @@ public final class AnimatedStickerCachedFrameSource: AnimatedStickerFrameSource
let height: Int let height: Int
public let frameRate: Int public let frameRate: Int
public let frameCount: Int public let frameCount: Int
private var frameIndex: Int public var frameIndex: Int
private let initialOffset: Int private let initialOffset: Int
private var offset: Int private var offset: Int
var decodeBuffer: Data var decodeBuffer: Data
@ -179,7 +181,7 @@ public final class AnimatedStickerCachedFrameSource: AnimatedStickerFrameSource
assert(self.queue.isCurrent()) assert(self.queue.isCurrent())
} }
public func takeFrame() -> AnimatedStickerFrame? { public func takeFrame(draw: Bool) -> AnimatedStickerFrame? {
var frameData: Data? var frameData: Data?
var isLastFrame = false var isLastFrame = false
@ -210,27 +212,29 @@ public final class AnimatedStickerCachedFrameSource: AnimatedStickerFrameSource
self.offset += 4 self.offset += 4
self.scratchBuffer.withUnsafeMutableBytes { (scratchBytes: UnsafeMutablePointer<UInt8>) -> Void in if draw {
self.decodeBuffer.withUnsafeMutableBytes { (decodeBytes: UnsafeMutablePointer<UInt8>) -> Void in self.scratchBuffer.withUnsafeMutableBytes { (scratchBytes: UnsafeMutablePointer<UInt8>) -> Void in
self.frameBuffer.withUnsafeMutableBytes { (frameBytes: UnsafeMutablePointer<UInt8>) -> Void in self.decodeBuffer.withUnsafeMutableBytes { (decodeBytes: UnsafeMutablePointer<UInt8>) -> Void in
compression_decode_buffer(decodeBytes, decodeBufferLength, bytes.advanced(by: self.offset), Int(frameLength), UnsafeMutableRawPointer(scratchBytes), COMPRESSION_LZFSE) self.frameBuffer.withUnsafeMutableBytes { (frameBytes: UnsafeMutablePointer<UInt8>) -> 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) var lhs = UnsafeMutableRawPointer(frameBytes).assumingMemoryBound(to: UInt64.self)
for _ in 0 ..< decodeBufferLength / 8 { var rhs = UnsafeRawPointer(decodeBytes).assumingMemoryBound(to: UInt64.self)
lhs.pointee = lhs.pointee ^ rhs.pointee for _ in 0 ..< decodeBufferLength / 8 {
lhs = lhs.advanced(by: 1) lhs.pointee = lhs.pointee ^ rhs.pointee
rhs = rhs.advanced(by: 1) 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) return AnimatedStickerFrame(data: frameData, type: .yuva, width: self.width, height: self.height, bytesPerRow: self.bytesPerRow, index: frameIndex, isLastFrame: isLastFrame)
} else { } else {
return nil return nil
@ -271,9 +275,13 @@ private final class AnimatedStickerDirectFrameSource: AnimatedStickerFrameSource
private let bytesPerRow: Int private let bytesPerRow: Int
let frameCount: Int let frameCount: Int
let frameRate: Int let frameRate: Int
private var currentFrame: Int fileprivate var currentFrame: Int
private let animation: LottieInstance private let animation: LottieInstance
var frameIndex: Int {
return self.currentFrame % self.frameCount
}
init?(queue: Queue, data: Data, width: Int, height: Int) { init?(queue: Queue, data: Data, width: Int, height: Int) {
self.queue = queue self.queue = queue
self.data = data self.data = data
@ -294,15 +302,19 @@ private final class AnimatedStickerDirectFrameSource: AnimatedStickerFrameSource
assert(self.queue.isCurrent()) assert(self.queue.isCurrent())
} }
func takeFrame() -> AnimatedStickerFrame? { func takeFrame(draw: Bool) -> AnimatedStickerFrame? {
let frameIndex = self.currentFrame % self.frameCount let frameIndex = self.currentFrame % self.frameCount
self.currentFrame += 1 self.currentFrame += 1
var frameData = Data(count: self.bytesPerRow * self.height) if draw {
frameData.withUnsafeMutableBytes { (bytes: UnsafeMutablePointer<UInt8>) -> Void in var frameData = Data(count: self.bytesPerRow * self.height)
memset(bytes, 0, self.bytesPerRow * self.height) frameData.withUnsafeMutableBytes { (bytes: UnsafeMutablePointer<UInt8>) -> Void in
self.animation.renderFrame(with: Int32(frameIndex), into: bytes, width: Int32(self.width), height: Int32(self.height), bytesPerRow: Int32(self.bytesPerRow)) 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() { func skipToEnd() {
@ -326,9 +338,9 @@ public final class AnimatedStickerFrameQueue {
assert(self.queue.isCurrent()) assert(self.queue.isCurrent())
} }
public func take() -> AnimatedStickerFrame? { public func take(draw: Bool) -> AnimatedStickerFrame? {
if self.frames.isEmpty { if self.frames.isEmpty {
if let frame = self.source.takeFrame() { if let frame = self.source.takeFrame(draw: draw) {
self.frames.append(frame) self.frames.append(frame)
} }
} }
@ -342,7 +354,7 @@ public final class AnimatedStickerFrameQueue {
public func generateFramesIfNeeded() { public func generateFramesIfNeeded() {
if self.frames.isEmpty { if self.frames.isEmpty {
if let frame = self.source.takeFrame() { if let frame = self.source.takeFrame(draw: true) {
self.frames.append(frame) 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 timer = SwiftSignalKit.Timer(timeout: 1.0 / Double(frameRate), repeat: !firstFrame, completion: {
let maybeFrame = frameQueue.syncWith { frameQueue in let maybeFrame = frameQueue.syncWith { frameQueue in
return frameQueue.take() return frameQueue.take(draw: true)
} }
if let maybeFrame = maybeFrame, let frame = maybeFrame { if let maybeFrame = maybeFrame, let frame = maybeFrame {
Queue.mainQueue().async { 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 timer = SwiftSignalKit.Timer(timeout: 1.0 / Double(frameRate), repeat: !firstFrame, completion: {
let maybeFrame = frameQueue.syncWith { frameQueue in let maybeFrame = frameQueue.syncWith { frameQueue in
return frameQueue.take() return frameQueue.take(draw: true)
} }
if let maybeFrame = maybeFrame, let frame = maybeFrame { if let maybeFrame = maybeFrame, let frame = maybeFrame {
Queue.mainQueue().async { Queue.mainQueue().async {
@ -710,19 +722,25 @@ public final class AnimatedStickerNode: ASDisplayNode {
let directData = self.directData let directData = self.directData
let cachedData = self.cachedData let cachedData = self.cachedData
let queue = self.queue let queue = self.queue
let frameSourceHolder = self.frameSource
let timerHolder = self.timer let timerHolder = self.timer
self.queue.async { [weak self] in self.queue.async { [weak self] in
var maybeFrameSource: AnimatedStickerFrameSource? var maybeFrameSource: AnimatedStickerFrameSource? = frameSourceHolder.with { $0 }?.syncWith { $0 }?.value
if let directData = directData { if case .timestamp = position {
maybeFrameSource = AnimatedStickerDirectFrameSource(queue: queue, data: directData.0, width: directData.2, height: directData.3) } else {
if position == .end { var maybeFrameSource: AnimatedStickerFrameSource?
maybeFrameSource?.skipToEnd() if let directData = directData {
} maybeFrameSource = AnimatedStickerDirectFrameSource(queue: queue, data: directData.0, width: directData.2, height: directData.3)
} else if let (cachedData, cachedDataComplete) = cachedData { if case .end = position {
if #available(iOS 9.0, *) { maybeFrameSource?.skipToEnd()
maybeFrameSource = AnimatedStickerCachedFrameSource(queue: queue, data: cachedData, complete: cachedDataComplete, notifyUpdated: {}) }
} 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 { guard let frameSource = maybeFrameSource else {
return return
} }
@ -732,9 +750,31 @@ public final class AnimatedStickerNode: ASDisplayNode {
timerHolder.swap(nil)?.invalidate() timerHolder.swap(nil)?.invalidate()
let duration: Double = frameSource.frameRate > 0 ? Double(frameSource.frameCount) / Double(frameSource.frameRate) : 0 let duration: Double = frameSource.frameRate > 0 ? Double(frameSource.frameCount) / Double(frameSource.frameRate) : 0
let maybeFrame = frameQueue.syncWith { frameQueue in var maybeFrame: AnimatedStickerFrame??
return frameQueue.take() 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 { if let maybeFrame = maybeFrame, let frame = maybeFrame {
Queue.mainQueue().async { Queue.mainQueue().async {

View File

@ -46,7 +46,7 @@ enum ChatContextMenuSource {
case search(ChatListSearchContextActionSource) 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 presentationData = context.sharedContext.currentPresentationData.with({ $0 })
let strings = presentationData.strings let strings = presentationData.strings
return context.account.postbox.transaction { [weak chatListController] transaction -> [ContextMenuItem] in 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 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) return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Back"), color: theme.contextMenu.primaryColor)
}, action: { c, _ in }, 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 return updatedItems
@ -379,7 +379,7 @@ func chatContextMenuItems(context: AccountContext, peerId: PeerId, promoInfo: Ch
if case .chatList = source, groupAndIndex != nil { 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 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 { if let chatListController = chatListController {
chatListController.deletePeerChat(peerId: peerId) chatListController.deletePeerChat(peerId: peerId, joined: joined)
} }
f(.default) f(.default)
}))) })))

View File

@ -575,11 +575,11 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController,
strongSelf.hidePsa(peerId) 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 { guard let strongSelf = self else {
return return
} }
strongSelf.deletePeerChat(peerId: peerId) strongSelf.deletePeerChat(peerId: peerId, joined: joined)
} }
self.chatListDisplayNode.containerNode.peerSelected = { [weak self] peer, animated, promoInfo in self.chatListDisplayNode.containerNode.peerSelected = { [weak self] peer, animated, promoInfo in
@ -801,6 +801,16 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController,
gesture?.cancel() gesture?.cancel()
return 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 { switch item.content {
case let .groupReference(groupReference): case let .groupReference(groupReference):
let chatListController = ChatListControllerImpl(context: strongSelf.context, groupId: groupReference.groupId, controlsHistoryPreload: false, hideNetworkActivityStatus: true, previewing: true, enableDebugActions: false) 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, _, _, _): 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)) 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) 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) 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)) 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) 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) strongSelf.presentInGlobalOverlay(contextController)
} }
@ -2069,7 +2079,7 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController,
let _ = hideAccountPromoInfoChat(account: self.context.account, peerId: id).start() 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 let _ = (self.context.account.postbox.transaction { transaction -> RenderedPeer? in
guard let peer = transaction.getPeer(peerId) else { guard let peer = transaction.getPeer(peerId) else {
return nil return nil
@ -2099,7 +2109,7 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController,
} }
if let user = chatPeer as? TelegramUser, user.botInfo == nil, canRemoveGlobally { 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 { } else {
let actionSheet = ActionSheetController(presentationData: strongSelf.presentationData) let actionSheet = ActionSheetController(presentationData: strongSelf.presentationData)
var items: [ActionSheetItem] = [] var items: [ActionSheetItem] = []
@ -2189,16 +2199,24 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController,
if canRemoveGlobally { if canRemoveGlobally {
let actionSheet = ActionSheetController(presentationData: strongSelf.presentationData) let actionSheet = ActionSheetController(presentationData: strongSelf.presentationData)
var items: [ActionSheetItem] = [] 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(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) if joined || mainPeer.isDeleted {
actionSheet?.dismissAnimated() items.append(ActionSheetButtonItem(title: strongSelf.presentationData.strings.Common_Delete, color: .destructive, action: { [weak actionSheet] in
})) beginClear(.forEveryone)
items.append(ActionSheetButtonItem(title: strongSelf.presentationData.strings.ChatList_DeleteForCurrentUser, color: .destructive, action: { [weak actionSheet] in actionSheet?.dismissAnimated()
beginClear(.forLocalPeer) }))
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([ actionSheet.setItemGroups([
ActionSheetItemGroup(items: items), 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 { guard let chatPeer = peer.peers[peer.peerId], let mainPeer = peer.chatMainPeer else {
completion(false) completion(false)
return return
@ -2279,31 +2297,41 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController,
var items: [ActionSheetItem] = [] 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(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([ actionSheet.setItemGroups([
ActionSheetItemGroup(items: items), ActionSheetItemGroup(items: items),
ActionSheetItemGroup(items: [ ActionSheetItemGroup(items: [

View File

@ -195,7 +195,7 @@ private final class ChatListShimmerNode: ASDisplayNode {
let timestamp1: Int32 = 100000 let timestamp1: Int32 = 100000
let peers = SimpleDictionary<PeerId, Peer>() let peers = SimpleDictionary<PeerId, Peer>()
let interaction = ChatListNodeInteraction(activateSearch: {}, peerSelected: { _, _ in }, disabledPeerSelected: { _ in }, togglePeerSelected: { _ in }, additionalCategorySelected: { _ in 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() gesture?.cancel()
}, present: { _ in }) }, present: { _ in })
@ -478,8 +478,8 @@ final class ChatListContainerNode: ASDisplayNode, UIGestureRecognizerDelegate {
itemNode.listNode.hidePsa = { [weak self] peerId in itemNode.listNode.hidePsa = { [weak self] peerId in
self?.hidePsa?(peerId) self?.hidePsa?(peerId)
} }
itemNode.listNode.deletePeerChat = { [weak self] peerId in itemNode.listNode.deletePeerChat = { [weak self] peerId, joined in
self?.deletePeerChat?(peerId) self?.deletePeerChat?(peerId, joined)
} }
itemNode.listNode.peerSelected = { [weak self] peerId, a, b in itemNode.listNode.peerSelected = { [weak self] peerId, a, b in
self?.peerSelected?(peerId, a, b) self?.peerSelected?(peerId, a, b)
@ -527,7 +527,7 @@ final class ChatListContainerNode: ASDisplayNode, UIGestureRecognizerDelegate {
var present: ((ViewController) -> Void)? var present: ((ViewController) -> Void)?
var toggleArchivedFolderHiddenByDefault: (() -> Void)? var toggleArchivedFolderHiddenByDefault: (() -> Void)?
var hidePsa: ((PeerId) -> Void)? var hidePsa: ((PeerId) -> Void)?
var deletePeerChat: ((PeerId) -> Void)? var deletePeerChat: ((PeerId, Bool) -> Void)?
var peerSelected: ((Peer, Bool, ChatListNodeEntryPromoInfo?) -> Void)? var peerSelected: ((Peer, Bool, ChatListNodeEntryPromoInfo?) -> Void)?
var groupSelected: ((PeerGroupId) -> Void)? var groupSelected: ((PeerGroupId) -> Void)?
var updatePeerGrouping: ((PeerId, Bool) -> Void)? var updatePeerGrouping: ((PeerId, Bool) -> Void)?

View File

@ -1028,7 +1028,7 @@ public final class ChatListSearchContainerNode: SearchDisplayControllerContentNo
} }
}, setItemPinned: { _, _ in }, setItemPinned: { _, _ in
}, setPeerMuted: { _, _ in }, setPeerMuted: { _, _ in
}, deletePeer: { _ in }, deletePeer: { _, _ in
}, updatePeerGrouping: { _, _ in }, updatePeerGrouping: { _, _ in
}, togglePeerMarkedUnread: { _, _ in }, togglePeerMarkedUnread: { _, _ in
}, toggleArchivedFolderHiddenByDefault: { }, toggleArchivedFolderHiddenByDefault: {

View File

@ -1946,7 +1946,15 @@ class ChatListItemNode: ItemListRevealOptionsItemNode {
item.interaction.setPeerMuted(item.index.messageIndex.id.peerId, false) item.interaction.setPeerMuted(item.index.messageIndex.id.peerId, false)
close = false close = false
case RevealOptionKey.delete.rawValue: 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: case RevealOptionKey.archive.rawValue:
item.interaction.updatePeerGrouping(item.index.messageIndex.id.peerId, true) item.interaction.updatePeerGrouping(item.index.messageIndex.id.peerId, true)
close = false close = false

View File

@ -59,7 +59,7 @@ public final class ChatListNodeInteraction {
let setPeerIdWithRevealedOptions: (PeerId?, PeerId?) -> Void let setPeerIdWithRevealedOptions: (PeerId?, PeerId?) -> Void
let setItemPinned: (PinnedItemId, Bool) -> Void let setItemPinned: (PinnedItemId, Bool) -> Void
let setPeerMuted: (PeerId, Bool) -> Void let setPeerMuted: (PeerId, Bool) -> Void
let deletePeer: (PeerId) -> Void let deletePeer: (PeerId, Bool) -> Void
let updatePeerGrouping: (PeerId, Bool) -> Void let updatePeerGrouping: (PeerId, Bool) -> Void
let togglePeerMarkedUnread: (PeerId, Bool) -> Void let togglePeerMarkedUnread: (PeerId, Bool) -> Void
let toggleArchivedFolderHiddenByDefault: () -> Void let toggleArchivedFolderHiddenByDefault: () -> Void
@ -70,7 +70,7 @@ public final class ChatListNodeInteraction {
public var searchTextHighightState: String? public var searchTextHighightState: String?
var highlightedChatLocation: ChatListHighlightedLocation? 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.activateSearch = activateSearch
self.peerSelected = peerSelected self.peerSelected = peerSelected
self.disabledPeerSelected = disabledPeerSelected self.disabledPeerSelected = disabledPeerSelected
@ -430,7 +430,7 @@ public final class ChatListNode: ListView {
public var groupSelected: ((PeerGroupId) -> Void)? public var groupSelected: ((PeerGroupId) -> Void)?
public var addContact: ((String) -> Void)? public var addContact: ((String) -> Void)?
public var activateSearch: (() -> Void)? public var activateSearch: (() -> Void)?
public var deletePeerChat: ((PeerId) -> Void)? public var deletePeerChat: ((PeerId, Bool) -> Void)?
public var updatePeerGrouping: ((PeerId, Bool) -> Void)? public var updatePeerGrouping: ((PeerId, Bool) -> Void)?
public var presentAlert: ((String) -> Void)? public var presentAlert: ((String) -> Void)?
public var present: ((ViewController) -> Void)? public var present: ((ViewController) -> Void)?
@ -628,8 +628,8 @@ public final class ChatListNode: ListView {
} }
self?.setCurrentRemovingPeerId(nil) self?.setCurrentRemovingPeerId(nil)
}) })
}, deletePeer: { [weak self] peerId in }, deletePeer: { [weak self] peerId, joined in
self?.deletePeerChat?(peerId) self?.deletePeerChat?(peerId, joined)
}, updatePeerGrouping: { [weak self] peerId, group in }, updatePeerGrouping: { [weak self] peerId, group in
self?.updatePeerGrouping?(peerId, group) self?.updatePeerGrouping?(peerId, group)
}, togglePeerMarkedUnread: { [weak self, weak context] peerId, animated in }, togglePeerMarkedUnread: { [weak self, weak context] peerId, animated in

View File

@ -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) 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) { 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, completion: completion) 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) { 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) {

View File

@ -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 t = node.layer.transform
let currentScale = sqrt((t.m11 * t.m11) + (t.m12 * t.m12) + (t.m13 * t.m13)) let currentScale = sqrt((t.m11 * t.m11) + (t.m12 * t.m12) + (t.m13 * t.m13))
if currentScale.isEqual(to: fromScale) { if currentScale.isEqual(to: fromScale) {
@ -592,7 +592,16 @@ public extension ContainedViewLayoutTransition {
completion(true) completion(true)
} }
case let .animated(duration, curve): 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 { if let completion = completion {
completion(result) 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) #if os(iOS)

View File

@ -184,6 +184,8 @@ open class ListView: ASDisplayNode, UIScrollViewAccessibilityDelegate, UIGesture
public final var keepMinimalScrollHeightWithTopInset: CGFloat? public final var keepMinimalScrollHeightWithTopInset: CGFloat?
public final var itemNodeHitTest: ((CGPoint) -> Bool)?
public final var stackFromBottom: Bool = false public final var stackFromBottom: Bool = false
public final var stackFromBottomInsetItemFactor: CGFloat = 0.0 public final var stackFromBottomInsetItemFactor: CGFloat = 0.0
public final var limitHitTestToNodes: Bool = false public final var limitHitTestToNodes: Bool = false
@ -3876,67 +3878,73 @@ open class ListView: ASDisplayNode, UIScrollViewAccessibilityDelegate, UIGesture
} }
self.touchesPosition = touchesPosition self.touchesPosition = touchesPosition
self.selectionTouchLocation = touches.first!.location(in: self.view)
self.selectionTouchDelayTimer?.invalidate() var processSelection = true
self.selectionLongTapDelayTimer?.invalidate() if let itemNodeHitTest = self.itemNodeHitTest, !itemNodeHitTest(touchesPosition) {
self.selectionLongTapDelayTimer = nil processSelection = false
let timer = Timer(timeInterval: 0.08, target: ListViewTimerProxy { [weak self] in }
if let strongSelf = self, strongSelf.selectionTouchLocation != nil {
strongSelf.clearHighlightAnimated(false) if processSelection {
self.selectionTouchLocation = touches.first!.location(in: self.view)
if let index = strongSelf.itemIndexAtPoint(strongSelf.touchesPosition) { self.selectionTouchDelayTimer?.invalidate()
var canBeSelectedOrLongTapped = false self.selectionLongTapDelayTimer?.invalidate()
for itemNode in strongSelf.itemNodes { self.selectionLongTapDelayTimer = nil
if itemNode.index == index && (strongSelf.items[index].selectable && itemNode.canBeSelected) || itemNode.canBeLongTapped { let timer = Timer(timeInterval: 0.08, target: ListViewTimerProxy { [weak self] in
canBeSelectedOrLongTapped = true if let strongSelf = self, strongSelf.selectionTouchLocation != nil {
} strongSelf.clearHighlightAnimated(false)
}
if canBeSelectedOrLongTapped { if let index = strongSelf.itemIndexAtPoint(strongSelf.touchesPosition) {
strongSelf.highlightedItemIndex = index var canBeSelectedOrLongTapped = false
for itemNode in strongSelf.itemNodes { for itemNode in strongSelf.itemNodes {
if itemNode.index == index && itemNode.canBeSelected { if itemNode.index == index && (strongSelf.items[index].selectable && itemNode.canBeSelected) || itemNode.canBeLongTapped {
if true { canBeSelectedOrLongTapped = true
if !itemNode.isLayerBacked { }
strongSelf.reorderItemNodeToFront(itemNode) }
for (_, headerNode) in strongSelf.itemHeaderNodes {
strongSelf.reorderHeaderNodeToFront(headerNode) 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 itemNodeFrame = itemNode.frame let itemNodeBounds = itemNode.bounds
let itemNodeBounds = itemNode.bounds if strongSelf.items[index].selectable {
if strongSelf.items[index].selectable { itemNode.setHighlighted(true, at: strongSelf.touchesPosition.offsetBy(dx: -itemNodeFrame.minX + itemNodeBounds.minX, dy: -itemNodeFrame.minY + itemNodeBounds.minY), animated: false)
itemNode.setHighlighted(true, at: strongSelf.touchesPosition.offsetBy(dx: -itemNodeFrame.minX + itemNodeBounds.minX, dy: -itemNodeFrame.minY + itemNodeBounds.minY), animated: false) }
}
if itemNode.canBeLongTapped {
if itemNode.canBeLongTapped { let timer = Timer(timeInterval: 0.3, target: ListViewTimerProxy {
let timer = Timer(timeInterval: 0.3, target: ListViewTimerProxy { if let strongSelf = self, strongSelf.highlightedItemIndex == index {
if let strongSelf = self, strongSelf.highlightedItemIndex == index { for itemNode in strongSelf.itemNodes {
for itemNode in strongSelf.itemNodes { if itemNode.index == index && itemNode.canBeLongTapped {
if itemNode.index == index && itemNode.canBeLongTapped { itemNode.longTapped()
itemNode.longTapped() strongSelf.clearHighlightAnimated(true)
strongSelf.clearHighlightAnimated(true) strongSelf.selectionTouchLocation = nil
strongSelf.selectionTouchLocation = nil break
break }
} }
} }
} }, selector: #selector(ListViewTimerProxy.timerEvent), userInfo: nil, repeats: false)
}, selector: #selector(ListViewTimerProxy.timerEvent), userInfo: nil, repeats: false) strongSelf.selectionLongTapDelayTimer = timer
strongSelf.selectionLongTapDelayTimer = timer RunLoop.main.add(timer, forMode: RunLoop.Mode.common)
RunLoop.main.add(timer, forMode: RunLoop.Mode.common) }
} }
break
} }
break
} }
} }
} }
} }
} }, selector: #selector(ListViewTimerProxy.timerEvent), userInfo: nil, repeats: false)
}, selector: #selector(ListViewTimerProxy.timerEvent), userInfo: nil, repeats: false) self.selectionTouchDelayTimer = timer
self.selectionTouchDelayTimer = timer RunLoop.main.add(timer, forMode: RunLoop.Mode.common)
RunLoop.main.add(timer, forMode: RunLoop.Mode.common) }
super.touchesBegan(touches, with: event) super.touchesBegan(touches, with: event)
self.updateScroller(transition: .immediate) self.updateScroller(transition: .immediate)

View File

@ -579,6 +579,7 @@ open class NavigationController: UINavigationController, ContainableController,
var previousModalContainer: NavigationModalContainer? var previousModalContainer: NavigationModalContainer?
var visibleModalCount = 0 var visibleModalCount = 0
var topModalIsFlat = false var topModalIsFlat = false
var isLandscape = layout.orientation == .landscape
var hasVisibleStandaloneModal = false var hasVisibleStandaloneModal = false
var topModalDismissProgress: CGFloat = 0.0 var topModalDismissProgress: CGFloat = 0.0
@ -784,7 +785,7 @@ open class NavigationController: UINavigationController, ContainableController,
let visibleRootModalDismissProgress: CGFloat let visibleRootModalDismissProgress: CGFloat
var additionalModalFrameProgress: CGFloat var additionalModalFrameProgress: CGFloat
if visibleModalCount == 1 { if visibleModalCount == 1 {
effectiveRootModalDismissProgress = topModalIsFlat ? 1.0 : topModalDismissProgress effectiveRootModalDismissProgress = (topModalIsFlat || isLandscape) ? 1.0 : topModalDismissProgress
visibleRootModalDismissProgress = effectiveRootModalDismissProgress visibleRootModalDismissProgress = effectiveRootModalDismissProgress
additionalModalFrameProgress = 0.0 additionalModalFrameProgress = 0.0
} else if visibleModalCount >= 2 { } else if visibleModalCount >= 2 {
@ -851,7 +852,7 @@ open class NavigationController: UINavigationController, ContainableController,
} }
let maxScale: CGFloat let maxScale: CGFloat
let maxOffset: CGFloat let maxOffset: CGFloat
if topModalIsFlat { if topModalIsFlat || isLandscape {
maxScale = 1.0 maxScale = 1.0
maxOffset = 0.0 maxOffset = 0.0
} else if visibleModalCount <= 1 { } else if visibleModalCount <= 1 {

View File

@ -328,6 +328,7 @@ final class NavigationModalContainer: ASDisplayNode, UIScrollViewDelegate, UIGes
self.scrollNode.view.isScrollEnabled = !isStandaloneModal self.scrollNode.view.isScrollEnabled = !isStandaloneModal
let isLandscape = layout.orientation == .landscape
let containerLayout: ContainerViewLayout let containerLayout: ContainerViewLayout
let containerFrame: CGRect let containerFrame: CGRect
let containerScale: CGFloat let containerScale: CGFloat
@ -336,7 +337,7 @@ final class NavigationModalContainer: ASDisplayNode, UIScrollViewDelegate, UIGes
self.panRecognizer?.isEnabled = true self.panRecognizer?.isEnabled = true
self.dim.backgroundColor = UIColor(white: 0.0, alpha: 0.25) self.dim.backgroundColor = UIColor(white: 0.0, alpha: 0.25)
self.container.clipsToBounds = true self.container.clipsToBounds = true
if isStandaloneModal { if isStandaloneModal || isLandscape {
self.container.cornerRadius = 0.0 self.container.cornerRadius = 0.0
} else { } else {
self.container.cornerRadius = 10.0 self.container.cornerRadius = 10.0
@ -351,7 +352,7 @@ final class NavigationModalContainer: ASDisplayNode, UIScrollViewDelegate, UIGes
} }
var topInset: CGFloat var topInset: CGFloat
if isStandaloneModal { if isStandaloneModal || isLandscape {
topInset = 0.0 topInset = 0.0
containerLayout = layout containerLayout = layout

View File

@ -62,6 +62,16 @@ final class GameControllerNode: ViewControllerTracingNode {
}, name: "performAction") }, name: "performAction")
configuration.userContentController = userController 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) let webView = WKWebView(frame: CGRect(), configuration: configuration)
if #available(iOSApplicationExtension 9.0, iOS 9.0, *) { if #available(iOSApplicationExtension 9.0, iOS 9.0, *) {
webView.allowsLinkPreview = false webView.allowsLinkPreview = false

View File

@ -66,7 +66,7 @@ public final class HashtagSearchController: TelegramBaseController {
}, setPeerIdWithRevealedOptions: { _, _ in }, setPeerIdWithRevealedOptions: { _, _ in
}, setItemPinned: { _, _ in }, setItemPinned: { _, _ in
}, setPeerMuted: { _, _ in }, setPeerMuted: { _, _ in
}, deletePeer: { _ in }, deletePeer: { _, _ in
}, updatePeerGrouping: { _, _ in }, updatePeerGrouping: { _, _ in
}, togglePeerMarkedUnread: { _, _ in }, togglePeerMarkedUnread: { _, _ in
}, toggleArchivedFolderHiddenByDefault: { }, toggleArchivedFolderHiddenByDefault: {

View File

@ -238,7 +238,9 @@ open class ItemListControllerNode: ASDisplayNode, UIScrollViewDelegate {
self.listNode = ListView() self.listNode = ListView()
self.leftOverlayNode = ASDisplayNode() self.leftOverlayNode = ASDisplayNode()
self.leftOverlayNode.isUserInteractionEnabled = false
self.rightOverlayNode = ASDisplayNode() self.rightOverlayNode = ASDisplayNode()
self.rightOverlayNode.isUserInteractionEnabled = false
super.init() super.init()
@ -302,6 +304,14 @@ open class ItemListControllerNode: ASDisplayNode, UIScrollViewDelegate {
let _ = strongSelf.contentScrollingEnded?(strongSelf.listNode) 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<ItemListNodeState?>(value: nil) let previousState = Atomic<ItemListNodeState?>(value: nil)
self.transitionDisposable.set(((state self.transitionDisposable.set(((state

View File

@ -22,8 +22,6 @@
#import <LegacyComponents/PGCamera.h> #import <LegacyComponents/PGCamera.h>
#import <LegacyComponents/PGCameraCaptureSession.h> #import <LegacyComponents/PGCameraCaptureSession.h>
#import <LegacyComponents/PGCameraDeviceAngleSampler.h> #import <LegacyComponents/PGCameraDeviceAngleSampler.h>
#import <LegacyComponents/PGCameraMomentSegment.h>
#import <LegacyComponents/PGCameraMomentSession.h>
#import <LegacyComponents/PGCameraMovieWriter.h> #import <LegacyComponents/PGCameraMovieWriter.h>
#import <LegacyComponents/PGCameraShotMetadata.h> #import <LegacyComponents/PGCameraShotMetadata.h>
#import <LegacyComponents/PGCameraVolumeButtonHandler.h> #import <LegacyComponents/PGCameraVolumeButtonHandler.h>

View File

@ -1,13 +0,0 @@
#import <Foundation/Foundation.h>
#import <CoreMedia/CoreMedia.h>
#import <AVFoundation/AVFoundation.h>
@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

View File

@ -1,28 +0,0 @@
#import <LegacyComponents/PGCameraMomentSegment.h>
@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

View File

@ -85,4 +85,7 @@ typedef enum {
+ (TGPhotoEditorTab)defaultTabsForAvatarIntent; + (TGPhotoEditorTab)defaultTabsForAvatarIntent;
- (NSTimeInterval)currentTime;
- (void)setMinimalVideoDuration:(NSTimeInterval)duration;
@end @end

View File

@ -13,7 +13,13 @@
@protocol TGPhotoPaintStickerRenderView <NSObject> @protocol TGPhotoPaintStickerRenderView <NSObject>
@property (nonatomic, copy) void(^started)(double);
- (void)setIsVisible:(bool)isVisible; - (void)setIsVisible:(bool)isVisible;
- (void)seekTo:(double)timestamp;
- (void)play;
- (void)pause;
- (void)resetToStart;
- (int64_t)documentId; - (int64_t)documentId;
- (UIImage *)image; - (UIImage *)image;

View File

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

View File

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

View File

@ -71,6 +71,8 @@
bool _throttle; bool _throttle;
TGLocationPinAnnotationView *_ownLiveLocationView; TGLocationPinAnnotationView *_ownLiveLocationView;
__weak MKAnnotationView *_userLocationView; __weak MKAnnotationView *_userLocationView;
UIImageView *_headingArrowView;
} }
@end @end
@ -162,6 +164,8 @@
[_liveLocationsDisposable dispose]; [_liveLocationsDisposable dispose];
[_reloadDisposable dispose]; [_reloadDisposable dispose];
[_frequentUpdatesDisposable dispose]; [_frequentUpdatesDisposable dispose];
[_locationManager stopUpdatingHeading];
} }
- (void)tg_setRightBarButtonItem:(UIBarButtonItem *)barButtonItem action:(bool)action animated:(bool)animated { - (void)tg_setRightBarButtonItem:(UIBarButtonItem *)barButtonItem action:(bool)action animated:(bool)animated {
@ -438,6 +442,36 @@
{ {
[super loadView]; [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; _tableView.scrollsToTop = false;
_mapView.tapEnabled = false; _mapView.tapEnabled = false;
@ -495,6 +529,8 @@
{ {
[super viewWillAppear:animated]; [super viewWillAppear:animated];
[_locationManager startUpdatingHeading];
if (self.previewMode && !animated) if (self.previewMode && !animated)
{ {
UIView *contentView = [[_mapView subviews] firstObject]; UIView *contentView = [[_mapView subviews] firstObject];
@ -950,6 +986,9 @@
{ {
_userLocationView = view; _userLocationView = view;
[_userLocationView addSubview:_headingArrowView];
_headingArrowView.center = CGPointMake(view.frame.size.width / 2.0, view.frame.size.height / 2.0);
if (_ownLiveLocationView != nil) if (_ownLiveLocationView != nil)
{ {
[_userLocationView addSubview:_ownLiveLocationView]; [_userLocationView addSubview:_ownLiveLocationView];
@ -982,6 +1021,14 @@
return CLLocationCoordinate2DMake(_locationAttachment.latitude, _locationAttachment.longitude); 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 - #pragma mark -
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section - (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section

View File

@ -19,6 +19,7 @@
- (void)setDotVideoView:(UIView *)dotVideoView; - (void)setDotVideoView:(UIView *)dotVideoView;
- (void)setDotImage:(UIImage *)dotImage; - (void)setDotImage:(UIImage *)dotImage;
@property (nonatomic, assign) NSTimeInterval minimumLength;
@property (nonatomic, assign) NSTimeInterval maximumLength; @property (nonatomic, assign) NSTimeInterval maximumLength;
@property (nonatomic, assign) bool disableZoom; @property (nonatomic, assign) bool disableZoom;

View File

@ -95,6 +95,7 @@ typedef enum
if (self != nil) if (self != nil)
{ {
_allowsTrimming = true; _allowsTrimming = true;
_minimumLength = TGVideoScrubberMinimumTrimDuration;
_currentTimeLabel = [[UILabel alloc] initWithFrame:CGRectMake(8, 4, 100, 15)]; _currentTimeLabel = [[UILabel alloc] initWithFrame:CGRectMake(8, 4, 100, 15)];
_currentTimeLabel.font = TGSystemFontOfSize(12.0f); _currentTimeLabel.font = TGSystemFontOfSize(12.0f);
@ -237,7 +238,7 @@ typedef enum
NSTimeInterval duration = trimEndPosition - trimStartPosition; NSTimeInterval duration = trimEndPosition - trimStartPosition;
if (trimEndPosition - trimStartPosition < TGVideoScrubberMinimumTrimDuration) if (trimEndPosition - trimStartPosition < self.minimumLength)
return; return;
if (strongSelf.maximumLength > DBL_EPSILON && duration > strongSelf.maximumLength) if (strongSelf.maximumLength > DBL_EPSILON && duration > strongSelf.maximumLength)
@ -300,7 +301,7 @@ typedef enum
NSTimeInterval duration = trimEndPosition - trimStartPosition; NSTimeInterval duration = trimEndPosition - trimStartPosition;
if (trimEndPosition - trimStartPosition < TGVideoScrubberMinimumTrimDuration) if (trimEndPosition - trimStartPosition < self.minimumLength)
return; return;
if (strongSelf.maximumLength > DBL_EPSILON && duration > strongSelf.maximumLength) if (strongSelf.maximumLength > DBL_EPSILON && duration > strongSelf.maximumLength)

View File

@ -82,6 +82,7 @@
UIImage *_thumbnailImage; UIImage *_thumbnailImage;
CMTime _chaseTime; CMTime _chaseTime;
bool _chaseStart;
bool _chasingTime; bool _chasingTime;
bool _isPlaying; bool _isPlaying;
AVPlayerItem *_playerItem; AVPlayerItem *_playerItem;
@ -367,6 +368,7 @@
if ([self presentedForAvatarCreation] && _item.isVideo) { if ([self presentedForAvatarCreation] && _item.isVideo) {
_scrubberView = [[TGMediaPickerGalleryVideoScrubber alloc] initWithFrame:CGRectMake(0.0f, 0.0, _portraitToolbarView.frame.size.width, 68.0f)]; _scrubberView = [[TGMediaPickerGalleryVideoScrubber alloc] initWithFrame:CGRectMake(0.0f, 0.0, _portraitToolbarView.frame.size.width, 68.0f)];
_scrubberView.minimumLength = 3.0;
_scrubberView.layer.allowsGroupOpacity = true; _scrubberView.layer.allowsGroupOpacity = true;
_scrubberView.hasDotPicker = true; _scrubberView.hasDotPicker = true;
_scrubberView.dataSource = self; _scrubberView.dataSource = self;
@ -670,6 +672,9 @@
if (strongSelf != nil && !strongSelf->_dismissed) { if (strongSelf != nil && !strongSelf->_dismissed) {
[strongSelf->_player seekToTime:startTime]; [strongSelf->_player seekToTime:startTime];
[strongSelf->_scrubberView setValue:strongSelf.trimStartValue resetPosition:true]; [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]; [_player addObserver:self forKeyPath:@"rate" options:NSKeyValueObservingOptionNew context:nil];
_registeredKeypathObserver = true; _registeredKeypathObserver = true;
} }
[_fullEntitiesView seekTo:0.0];
[_fullEntitiesView play];
} else {
[_fullEntitiesView play];
} }
_isPlaying = true; _isPlaying = true;
@ -722,6 +732,8 @@
} }
[_scrubberView setIsPlaying:false]; [_scrubberView setIsPlaying:false];
} else {
[_fullEntitiesView pause];
} }
_isPlaying = false; _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 { - (void)seekVideo:(NSTimeInterval)position {
CMTime targetTime = CMTimeMakeWithSeconds(position, NSEC_PER_SEC); CMTime targetTime = CMTimeMakeWithSeconds(position, NSEC_PER_SEC);
@ -766,6 +786,11 @@
CMTime currentChasingTime = _chaseTime; CMTime currentChasingTime = _chaseTime;
[_player.currentItem seekToTime:currentChasingTime toleranceBefore:kCMTimeZero toleranceAfter:kCMTimeZero completionHandler:^(BOOL finished) { [_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)) { if (CMTIME_COMPARE_INLINE(currentChasingTime, ==, _chaseTime)) {
_chasingTime = false; _chasingTime = false;
_chaseTime = kCMTimeInvalid; _chaseTime = kCMTimeInvalid;
@ -1957,7 +1982,7 @@
generator.requestedTimeToleranceAfter = kCMTimeZero; generator.requestedTimeToleranceAfter = kCMTimeZero;
generator.requestedTimeToleranceBefore = 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]; UIImage *image = [UIImage imageWithCGImage:imageRef];
CGImageRelease(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]; [self seekVideo:position];
} }
@ -2780,6 +2805,8 @@
[self startVideoPlayback:true]; [self startVideoPlayback:true];
[self setPlayButtonHidden:true animated:false]; [self setPlayButtonHidden:true animated:false];
_chaseStart = false;
} }
- (void)videoScrubber:(TGMediaPickerGalleryVideoScrubber *)videoScrubber editingStartValueDidChange:(NSTimeInterval)startValue - (void)videoScrubber:(TGMediaPickerGalleryVideoScrubber *)videoScrubber editingStartValueDidChange:(NSTimeInterval)startValue
@ -2788,6 +2815,12 @@
_resetDotPosition = true; _resetDotPosition = true;
[self resetDotImage]; [self resetDotImage];
} }
if (!_chaseStart) {
_chaseStart = true;
[_fullEntitiesView resetToStart];
}
[self seekVideo:startValue]; [self seekVideo:startValue];
} }

View File

@ -14,6 +14,10 @@
@property (nonatomic, copy) void (^entityRemoved)(TGPhotoPaintEntityView *); @property (nonatomic, copy) void (^entityRemoved)(TGPhotoPaintEntityView *);
- (void)updateVisibility:(bool)visible; - (void)updateVisibility:(bool)visible;
- (void)seekTo:(double)timestamp;
- (void)play;
- (void)pause;
- (void)resetToStart;
- (UIColor *)colorAtPoint:(CGPoint)point; - (UIColor *)colorAtPoint:(CGPoint)point;

View File

@ -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 - (BOOL)gestureRecognizer:(UIGestureRecognizer *)__unused gestureRecognizer shouldRecognizeSimultaneouslyWithGestureRecognizer:(UIGestureRecognizer *)__unused otherGestureRecognizer
{ {
return false; return false;

View File

@ -1196,9 +1196,39 @@ const CGFloat TGPhotoPaintStickerKeyboardSize = 260.0f;
TGPhotoPaintStickerEntity *entity = [[TGPhotoPaintStickerEntity alloc] initWithDocument:document baseSize:[self _stickerBaseSizeForCurrentPainting] animated:animated]; TGPhotoPaintStickerEntity *entity = [[TGPhotoPaintStickerEntity alloc] initWithDocument:document baseSize:[self _stickerBaseSizeForCurrentPainting] animated:animated];
[self _setStickerEntityPosition:entity]; [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]; TGPhotoStickerEntityView *stickerView = (TGPhotoStickerEntityView *)[_entitiesContainerView createEntityViewWithEntity:entity];
[self _commonEntityViewSetup:stickerView]; [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]; [self selectEntityView:stickerView];
_entitySelectionView.alpha = 0.0f; _entitySelectionView.alpha = 0.0f;

View File

@ -9,6 +9,8 @@
@interface TGPhotoStickerEntityView : TGPhotoPaintEntityView @interface TGPhotoStickerEntityView : TGPhotoPaintEntityView
@property (nonatomic, copy) void(^started)(double);
@property (nonatomic, readonly) TGPhotoPaintStickerEntity *entity; @property (nonatomic, readonly) TGPhotoPaintStickerEntity *entity;
@property (nonatomic, readonly) bool isMirrored; @property (nonatomic, readonly) bool isMirrored;
@ -17,6 +19,10 @@
- (UIImage *)image; - (UIImage *)image;
- (void)updateVisibility:(bool)visible; - (void)updateVisibility:(bool)visible;
- (void)seekTo:(double)timestamp;
- (void)play;
- (void)pause;
- (void)resetToStart;
- (CGRect)realBounds; - (CGRect)realBounds;

View File

@ -55,6 +55,13 @@ const CGFloat TGPhotoStickerSelectionViewHandleSide = 30.0f;
_mirrored = entity.isMirrored; _mirrored = entity.isMirrored;
_stickerView = [context stickerViewForDocument:entity.document]; _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]; [self addSubview:_stickerView];
_document = entity.document; _document = entity.document;
@ -178,6 +185,22 @@ const CGFloat TGPhotoStickerSelectionViewHandleSide = 30.0f;
[_stickerView setIsVisible:visible]; [_stickerView setIsVisible:visible];
} }
- (void)seekTo:(double)timestamp {
[_stickerView seekTo:timestamp];
}
- (void)play {
[_stickerView play];
}
- (void)pause {
[_stickerView pause];
}
- (void)resetToStart {
[_stickerView resetToStart];
}
@end @end

View File

@ -10,6 +10,8 @@ import StickerResources
import LegacyComponents import LegacyComponents
class LegacyPaintStickerView: UIView, TGPhotoPaintStickerRenderView { class LegacyPaintStickerView: UIView, TGPhotoPaintStickerRenderView {
var started: ((Double) -> Void)?
private let context: AccountContext private let context: AccountContext
private let file: TelegramMediaFile private let file: TelegramMediaFile
private var currentSize: CGSize? private var currentSize: CGSize?
@ -63,8 +65,16 @@ class LegacyPaintStickerView: UIView, TGPhotoPaintStickerRenderView {
if self.animationNode == nil { if self.animationNode == nil {
let animationNode = AnimatedStickerNode() let animationNode = AnimatedStickerNode()
self.animationNode = animationNode self.animationNode = animationNode
animationNode.started = { [weak self] in animationNode.started = { [weak self, weak animationNode] in
self?.imageNode.isHidden = true 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) 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() { override func layoutSubviews() {
super.layoutSubviews() super.layoutSubviews()

View File

@ -188,8 +188,8 @@ private class LegacyPaintStickerEntity: LegacyPaintEntity {
let maybeFrame = frameQueue.syncWith { frameQueue -> AnimatedStickerFrame? in let maybeFrame = frameQueue.syncWith { frameQueue -> AnimatedStickerFrame? in
var frame: AnimatedStickerFrame? var frame: AnimatedStickerFrame?
for _ in 0 ..< delta { for i in 0 ..< delta {
frame = frameQueue.take() frame = frameQueue.take(draw: i == delta - 1)
} }
return frame return frame
} }

View File

@ -8,11 +8,18 @@ import TelegramPresentationData
import ItemListUI import ItemListUI
import AppBundle import AppBundle
enum LocationAttribution: Equatable {
case foursquare
case google
}
class LocationAttributionItem: ListViewItem { class LocationAttributionItem: ListViewItem {
let presentationData: ItemListPresentationData let presentationData: ItemListPresentationData
let attribution: LocationAttribution
public init(presentationData: ItemListPresentationData) { public init(presentationData: ItemListPresentationData, attribution: LocationAttribution) {
self.presentationData = presentationData 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<Void, NoError>?, (ListViewItemApply) -> Void)) -> Void) { public func nodeConfiguredForParams(async: @escaping (@escaping () -> Void) -> Void, params: ListViewItemLayoutParams, synchronousLoads: Bool, previousItem: ListViewItem?, nextItem: ListViewItem?, completion: @escaping (ListViewItemNode, @escaping () -> (Signal<Void, NoError>?, (ListViewItemApply) -> Void)) -> Void) {
@ -93,11 +100,20 @@ private class LocationAttributionItemNode: ListViewItemNode {
strongSelf.layoutParams = params strongSelf.layoutParams = params
if let _ = updatedTheme { 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 { 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)
} }
} }
}) })

View File

@ -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 { final class LocationMapNode: ASDisplayNode, MKMapViewDelegate {
private let locationPromise = Promise<CLLocation?>(nil) private let locationPromise = Promise<CLLocation?>(nil)
private let pickerAnnotationContainerView: PickerAnnotationContainerView private let pickerAnnotationContainerView: PickerAnnotationContainerView
private weak var userLocationAnnotationView: MKAnnotationView? private weak var userLocationAnnotationView: MKAnnotationView?
private var headingArrowView: UIImageView?
private let pinDisposable = MetaDisposable() private let pinDisposable = MetaDisposable()
@ -103,6 +122,10 @@ final class LocationMapNode: ASDisplayNode, MKMapViewDelegate {
override func didLoad() { override func didLoad() {
super.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 self.mapView?.interactiveTransitionGestureRecognizerTest = { p in
if p.x > 44.0 { if p.x > 44.0 {
return true return true
@ -232,6 +255,10 @@ final class LocationMapNode: ASDisplayNode, MKMapViewDelegate {
for view in views { for view in views {
if view.annotation is MKUserLocation { if view.annotation is MKUserLocation {
self.userLocationAnnotationView = view 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 { if let annotationView = self.customUserLocationAnnotationView {
view.addSubview(annotationView) 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] = [] { var annotations: [LocationPinAnnotation] = [] {
didSet { didSet {
guard let mapView = self.mapView else { guard let mapView = self.mapView else {

View File

@ -289,7 +289,7 @@ public final class LocationPickerController: ViewController {
return 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.displayNodeDidLoad()
self.permissionDisposable = (DeviceAccess.authorizationStatus(subject: .location(.send)) self.permissionDisposable = (DeviceAccess.authorizationStatus(subject: .location(.send))

View File

@ -17,6 +17,7 @@ import AppBundle
import CoreLocation import CoreLocation
import Geocoding import Geocoding
import PhoneNumberFormat import PhoneNumberFormat
import DeviceAccess
private struct LocationPickerTransaction { private struct LocationPickerTransaction {
let deletions: [ListViewDeleteItem] let deletions: [ListViewDeleteItem]
@ -40,7 +41,7 @@ private enum LocationPickerEntry: Comparable, Identifiable {
case liveLocation(PresentationTheme, String, String, CLLocationCoordinate2D?) case liveLocation(PresentationTheme, String, String, CLLocationCoordinate2D?)
case header(PresentationTheme, String) case header(PresentationTheme, String)
case venue(PresentationTheme, TelegramMediaMap, Int) case venue(PresentationTheme, TelegramMediaMap, Int)
case attribution(PresentationTheme) case attribution(PresentationTheme, LocationAttribution)
var stableId: LocationPickerEntryId { var stableId: LocationPickerEntryId {
switch self { switch self {
@ -83,8 +84,8 @@ private enum LocationPickerEntry: Comparable, Identifiable {
} else { } else {
return false return false
} }
case let .attribution(lhsTheme): case let .attribution(lhsTheme, lhsAttribution):
if case let .attribution(rhsTheme) = rhs, lhsTheme === rhsTheme { if case let .attribution(rhsTheme, rhsAttribution) = rhs, lhsTheme === rhsTheme, lhsAttribution == rhsAttribution {
return true return true
} else { } else {
return false return false
@ -131,7 +132,7 @@ private enum LocationPickerEntry: Comparable, Identifiable {
func item(account: Account, presentationData: PresentationData, interaction: LocationPickerInteraction?) -> ListViewItem { func item(account: Account, presentationData: PresentationData, interaction: LocationPickerInteraction?) -> ListViewItem {
switch self { switch self {
case let .location(theme, title, subtitle, venue, coordinate): case let .location(_, title, subtitle, venue, coordinate):
let icon: LocationActionListItemIcon let icon: LocationActionListItemIcon
if let venue = venue { if let venue = venue {
icon = .venue(venue) icon = .venue(venue)
@ -147,23 +148,23 @@ private enum LocationPickerEntry: Comparable, Identifiable {
}, highlighted: { highlighted in }, highlighted: { highlighted in
interaction?.updateSendActionHighlight(highlighted) 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: { return LocationActionListItem(presentationData: ItemListPresentationData(presentationData), account: account, title: title, subtitle: subtitle, icon: .liveLocation, action: {
if let coordinate = coordinate { if let coordinate = coordinate {
interaction?.sendLiveLocation(coordinate) interaction?.sendLiveLocation(coordinate)
} }
}) })
case let .header(theme, title): case let .header(_, title):
return LocationSectionHeaderItem(presentationData: ItemListPresentationData(presentationData), title: title) return LocationSectionHeaderItem(presentationData: ItemListPresentationData(presentationData), title: title)
case let .venue(theme, venue, _): case let .venue(_, venue, _):
let venueType = venue.venue?.type ?? "" let venueType = venue.venue?.type ?? ""
return ItemListVenueItem(presentationData: ItemListPresentationData(presentationData), account: account, venue: venue, style: .plain, action: { return ItemListVenueItem(presentationData: ItemListPresentationData(presentationData), account: account, venue: venue, style: .plain, action: {
interaction?.sendVenue(venue) interaction?.sendVenue(venue)
}, infoAction: ["home", "work"].contains(venueType) ? { }, infoAction: ["home", "work"].contains(venueType) ? {
interaction?.openHomeWorkInfo() interaction?.openHomeWorkInfo()
} : nil) } : nil)
case let .attribution(theme): case let .attribution(_, attribution):
return LocationAttributionItem(presentationData: ItemListPresentationData(presentationData)) 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 let context: AccountContext
private var presentationData: PresentationData private var presentationData: PresentationData
private let presentationDataPromise: Promise<PresentationData> private let presentationDataPromise: Promise<PresentationData>
private let mode: LocationPickerMode private let mode: LocationPickerMode
private let interaction: LocationPickerInteraction private let interaction: LocationPickerInteraction
private let locationManager: LocationManager
private let listNode: ListView private let listNode: ListView
private let emptyResultsTextNode: ImmediateTextNode private let emptyResultsTextNode: ImmediateTextNode
@ -269,12 +271,13 @@ final class LocationPickerControllerNode: ViewControllerTracingNode {
private var validLayout: (layout: ContainerViewLayout, navigationHeight: CGFloat)? private var validLayout: (layout: ContainerViewLayout, navigationHeight: CGFloat)?
private var listOffset: 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.context = context
self.presentationData = presentationData self.presentationData = presentationData
self.presentationDataPromise = Promise(presentationData) self.presentationDataPromise = Promise(presentationData)
self.mode = mode self.mode = mode
self.interaction = interaction self.interaction = interaction
self.locationManager = locationManager
self.state = LocationPickerState() self.state = LocationPickerState()
self.statePromise = Promise(self.state) self.statePromise = Promise(self.state)
@ -496,15 +499,21 @@ final class LocationPickerControllerNode: ViewControllerTracingNode {
entries.append(.header(presentationData.theme, presentationData.strings.Map_ChooseAPlace.uppercased())) 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 { if let venues = displayedVenues {
var index: Int = 0 var index: Int = 0
var attribution: LocationAttribution?
for venue in venues { 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)) entries.append(.venue(presentationData.theme, venue, index))
index += 1 index += 1
} }
if !venues.isEmpty { if let attribution = attribution {
entries.append(.attribution(presentationData.theme)) entries.append(.attribution(presentationData.theme, attribution))
} }
} }
let previousEntries = previousEntries.swap(entries) let previousEntries = previousEntries.swap(entries)
@ -533,7 +542,7 @@ final class LocationPickerControllerNode: ViewControllerTracingNode {
switch previousState.selectedLocation { switch previousState.selectedLocation {
case .none, .venue: case .none, .venue:
updateMap = true updateMap = true
case let .location(previousCoordinate, address): case let .location(previousCoordinate, _):
if previousCoordinate != coordinate { if previousCoordinate != coordinate {
updateMap = true updateMap = true
} }
@ -574,7 +583,7 @@ final class LocationPickerControllerNode: ViewControllerTracingNode {
if let (layout, navigationBarHeight) = strongSelf.validLayout { if let (layout, navigationBarHeight) = strongSelf.validLayout {
var updateLayout = false 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 { if previousState.displayingMapModeOptions != state.displayingMapModeOptions {
updateLayout = true updateLayout = true
@ -685,11 +694,20 @@ final class LocationPickerControllerNode: ViewControllerTracingNode {
strongSelf.goToUserLocation() strongSelf.goToUserLocation()
} }
} }
self.locationManager.manager.startUpdatingHeading()
self.locationManager.manager.delegate = self
} }
deinit { deinit {
self.disposable?.dispose() self.disposable?.dispose()
self.geocodingDisposable.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) { func updatePresentationData(_ presentationData: PresentationData) {
@ -721,7 +739,7 @@ final class LocationPickerControllerNode: ViewControllerTracingNode {
} }
private func dequeueTransition() { 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 return
} }
self.enqueuedTransitions.remove(at: 0) self.enqueuedTransitions.remove(at: 0)

View File

@ -181,7 +181,11 @@ private func allOpenInOptions(context: AccountContext, item: OpenInItem) -> [Ope
if withDirections { if withDirections {
return .openUrl(url: "comgooglemaps-x-callback://?daddr=\(coordinates)&directionsmode=driving&x-success=telegram://?resume=true&x-source=Telegram") return .openUrl(url: "comgooglemaps-x-callback://?daddr=\(coordinates)&directionsmode=driving&x-success=telegram://?resume=true&x-source=Telegram")
} else { } 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")
}
} }
})) }))

View File

@ -149,11 +149,11 @@ public func initialAvatarGalleryEntries(account: Account, peer: Peer) -> Signal<
} }
if let photo = initialPhoto { if let photo = initialPhoto {
if photo.immediateThumbnailData == nil { var representations = photo.representations.map({ ImageRepresentationWithReference(representation: $0, reference: MediaResourceReference.avatar(peer: peerReference, resource: $0.resource)) })
return initialEntries if photo.immediateThumbnailData == nil, let firstEntry = initialEntries.first, let firstRepresentation = firstEntry.representations.first {
} else { representations.insert(firstRepresentation, at: 0)
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)]
} }
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 { } else {
return [] return []
} }
@ -243,19 +243,24 @@ public func fetchedAvatarGalleryEntries(account: Account, peer: Peer, firstEntry
var photosCount = photos.count var photosCount = photos.count
for i in 0 ..< photos.count { for i in 0 ..< photos.count {
let photo = photos[i] let photo = photos[i]
if i == 0 && !initialMediaIds.contains(photo.image.imageId) { var representations = photo.image.representations.map({ ImageRepresentationWithReference(representation: $0, reference: MediaResourceReference.avatarList(peer: peerReference, resource: $0.resource)) })
photosCount += 1 if i == 0 {
for entry in initialEntries { if !initialMediaIds.contains(photo.image.imageId) {
let indexData = GalleryItemIndexData(position: index, totalCount: Int32(photosCount)) photosCount += 1
if case let .image(image) = entry { for entry in initialEntries {
result.append(.image(image.0, image.1, image.2, image.3, image.4, nil, indexData, nil, image.8, nil)) let indexData = GalleryItemIndexData(position: index, totalCount: Int32(photosCount))
index += 1 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)) 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 index += 1
} }
} else { } else {

View File

@ -1076,8 +1076,8 @@ public func channelInfoController(context: AccountContext, peerId: PeerId) -> Vi
} }
for childController in tabController.controllers { for childController in tabController.controllers {
if let chatListController = childController as? ChatListController { if let chatListController = childController as? ChatListController {
chatListController.maybeAskForPeerChatRemoval(peer: RenderedPeer(peer: peer), deleteGloballyIfPossible: deleteGloballyIfPossible, completion: { [weak navigationController] deleted in chatListController.maybeAskForPeerChatRemoval(peer: RenderedPeer(peer: peer), joined: false, deleteGloballyIfPossible: deleteGloballyIfPossible, completion: { [weak navigationController] removed in
if deleted { if removed {
navigationController?.popToRoot(animated: true) navigationController?.popToRoot(animated: true)
} }
}, removed: { }, removed: {

View File

@ -2439,7 +2439,7 @@ public func groupInfoController(context: AccountContext, peerId originalPeerId:
} }
for childController in tabController.controllers { for childController in tabController.controllers {
if let chatListController = childController as? ChatListController { 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 { if removed {
navigationController?.popToRoot(animated: true) navigationController?.popToRoot(animated: true)
} }

View File

@ -214,7 +214,7 @@ private final class TextSizeSelectionControllerNode: ASDisplayNode, UIScrollView
var items: [ChatListItem] = [] var items: [ChatListItem] = []
let interaction = ChatListNodeInteraction(activateSearch: {}, peerSelected: { _, _ in }, disabledPeerSelected: { _ in }, togglePeerSelected: { _ in }, additionalCategorySelected: { _ in 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 }, activateChatPreview: { _, _, gesture in
gesture?.cancel() gesture?.cancel()
}, present: { _ in }) }, present: { _ in })

View File

@ -767,7 +767,7 @@ final class ThemeAccentColorControllerNode: ASDisplayNode, UIScrollViewDelegate
var items: [ChatListItem] = [] var items: [ChatListItem] = []
let interaction = ChatListNodeInteraction(activateSearch: {}, peerSelected: { _, _ in }, disabledPeerSelected: { _ in }, togglePeerSelected: { _ in }, additionalCategorySelected: { _ in 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 }, activateChatPreview: { _, _, gesture in
gesture?.cancel() gesture?.cancel()
}, present: { _ in }, present: { _ in

View File

@ -351,7 +351,7 @@ final class ThemePreviewControllerNode: ASDisplayNode, UIScrollViewDelegate {
var items: [ChatListItem] = [] var items: [ChatListItem] = []
let interaction = ChatListNodeInteraction(activateSearch: {}, peerSelected: { _, _ in }, disabledPeerSelected: { _ in }, togglePeerSelected: { _ in }, additionalCategorySelected: { _ in 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 }, activateChatPreview: { _, _, gesture in
gesture?.cancel() gesture?.cancel()
}, present: { _ in }, present: { _ in

View File

@ -244,6 +244,20 @@ public final class ManagedAudioSession {
if let availableInputs = audioSession.availableInputs { if let availableInputs = audioSession.availableInputs {
var hasHeadphones = false 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 { for input in availableInputs {
var isActive = false var isActive = false
for currentInput in audioSession.currentRoute.inputs { for currentInput in audioSession.currentRoute.inputs {
@ -253,7 +267,7 @@ public final class ManagedAudioSession {
} }
if input.portType == .builtInMic { if input.portType == .builtInMic {
if isActive { if isActive && !headphonesAreActive {
activeOutput = .builtin activeOutput = .builtin
inner: for currentOutput in audioSession.currentRoute.outputs { inner: for currentOutput in audioSession.currentRoute.outputs {
if currentOutput.portType == .builtInSpeaker { if currentOutput.portType == .builtInSpeaker {
@ -739,13 +753,28 @@ public final class ManagedAudioSession {
case .voiceCall, .playWithPossiblePortOverride, .record(true): case .voiceCall, .playWithPossiblePortOverride, .record(true):
try AVAudioSession.sharedInstance().overrideOutputAudioPort(.none) try AVAudioSession.sharedInstance().overrideOutputAudioPort(.none)
if let routes = AVAudioSession.sharedInstance().availableInputs { if let routes = AVAudioSession.sharedInstance().availableInputs {
for route in routes { var alreadySet = false
if route.portType == .builtInMic { if self.isHeadsetPluggedInValue {
if case .record = updatedType, self.isHeadsetPluggedInValue { loop: for route in routes {
} else { switch route.portType {
case .headphones, .bluetoothA2DP, .bluetoothHFP:
let _ = try? AVAudioSession.sharedInstance().setPreferredInput(route) 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
} }
} }
} }

View File

@ -31,6 +31,13 @@ final class CallControllerButtonItemNode: HighlightTrackingButtonNode {
var appearance: Appearance var appearance: Appearance
var image: Image 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 private let contentContainer: ASDisplayNode
@ -107,6 +114,9 @@ final class CallControllerButtonItemNode: HighlightTrackingButtonNode {
self.effectView.isHidden = true 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 let contentImage = generateImage(CGSize(width: self.largeButtonSize, height: self.largeButtonSize), contextGenerator: { size, context in
context.clear(CGRect(origin: CGPoint(), size: size)) context.clear(CGRect(origin: CGPoint(), size: size))

View File

@ -17,7 +17,7 @@ enum CallControllerButtonsSpeakerMode {
enum CallControllerButtonsMode: Equatable { enum CallControllerButtonsMode: Equatable {
enum VideoState: Equatable { enum VideoState: Equatable {
case notAvailable case notAvailable
case possible case possible(Bool)
case outgoingRequested case outgoingRequested
case incomingRequested case incomingRequested
case active case active
@ -52,7 +52,7 @@ private enum ButtonDescription: Equatable {
case accept case accept
case end(EndType) case end(EndType)
case enableCamera(Bool) case enableCamera(Bool, Bool)
case switchCamera case switchCamera
case soundOutput(SoundOutput) case soundOutput(SoundOutput)
case mute(Bool) case mute(Bool)
@ -203,12 +203,15 @@ final class CallControllerButtonsNode: ASDisplayNode {
switch videoState { switch videoState {
case .active, .possible, .incomingRequested, .outgoingRequested: case .active, .possible, .incomingRequested, .outgoingRequested:
let isCameraActive: Bool let isCameraActive: Bool
if case .possible = videoState { let isCameraEnabled: Bool
if case let .possible(value) = videoState {
isCameraActive = false isCameraActive = false
isCameraEnabled = value
} else { } else {
isCameraActive = !self.isCameraPaused isCameraActive = !self.isCameraPaused
isCameraEnabled = true
} }
topButtons.append(.enableCamera(isCameraActive)) topButtons.append(.enableCamera(isCameraActive, isCameraEnabled))
topButtons.append(.mute(self.isMuted)) topButtons.append(.mute(self.isMuted))
if case .possible = videoState { if case .possible = videoState {
topButtons.append(.soundOutput(soundOutput)) topButtons.append(.soundOutput(soundOutput))
@ -252,10 +255,13 @@ final class CallControllerButtonsNode: ASDisplayNode {
switch videoState { switch videoState {
case .active, .incomingRequested, .outgoingRequested: case .active, .incomingRequested, .outgoingRequested:
let isCameraActive: Bool let isCameraActive: Bool
if case .possible = videoState { let isCameraEnabled: Bool
if case let .possible(value) = videoState {
isCameraActive = false isCameraActive = false
isCameraEnabled = value
} else { } else {
isCameraActive = !self.isCameraPaused isCameraActive = !self.isCameraPaused
isCameraEnabled = true
} }
var topButtons: [ButtonDescription] = [] var topButtons: [ButtonDescription] = []
@ -272,7 +278,7 @@ final class CallControllerButtonsNode: ASDisplayNode {
soundOutput = .bluetooth soundOutput = .bluetooth
} }
topButtons.append(.enableCamera(isCameraActive)) topButtons.append(.enableCamera(isCameraActive, isCameraEnabled))
topButtons.append(.mute(isMuted)) topButtons.append(.mute(isMuted))
topButtons.append(.switchCamera) topButtons.append(.switchCamera)
topButtons.append(.end(.end)) topButtons.append(.end(.end))
@ -304,7 +310,7 @@ final class CallControllerButtonsNode: ASDisplayNode {
soundOutput = .bluetooth soundOutput = .bluetooth
} }
topButtons.append(.enableCamera(false)) topButtons.append(.enableCamera(false, true))
topButtons.append(.mute(self.isMuted)) topButtons.append(.mute(self.isMuted))
topButtons.append(.soundOutput(soundOutput)) topButtons.append(.soundOutput(soundOutput))
@ -373,10 +379,11 @@ final class CallControllerButtonsNode: ASDisplayNode {
case .end: case .end:
buttonText = strings.Call_End buttonText = strings.Call_End
} }
case let .enableCamera(isEnabled): case let .enableCamera(isActivated, isEnabled):
buttonContent = CallControllerButtonItemNode.Content( buttonContent = CallControllerButtonItemNode.Content(
appearance: .blurred(isFilled: isEnabled), appearance: .blurred(isFilled: isActivated),
image: .camera image: .camera,
isEnabled: isEnabled
) )
buttonText = strings.Call_Camera buttonText = strings.Call_Camera
case .switchCamera: case .switchCamera:

View File

@ -34,16 +34,25 @@ private final class CallVideoNode: ASDisplayNode {
private(set) var isReady: Bool = false private(set) var isReady: Bool = false
private var isReadyTimer: SwiftSignalKit.Timer? 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.isReadyUpdated = isReadyUpdated
self.isFlippedUpdated = isFlippedUpdated
self.videoTransformContainer = ASDisplayNode() self.videoTransformContainer = ASDisplayNode()
self.videoTransformContainer.clipsToBounds = true self.videoTransformContainer.clipsToBounds = true
self.videoView = videoView 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() super.init()
self.backgroundColor = .black
self.videoTransformContainer.view.addSubview(self.videoView.view) self.videoTransformContainer.view.addSubview(self.videoView.view)
self.addSubnode(self.videoTransformContainer) 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 self.isReadyTimer = SwiftSignalKit.Timer(timeout: 3.0, repeat: false, completion: { [weak self] in
guard let strongSelf = self else { guard let strongSelf = self else {
return return
@ -75,28 +94,80 @@ private final class CallVideoNode: ASDisplayNode {
} }
func updateLayout(size: CGSize, cornerRadius: CGFloat, transition: ContainedViewLayoutTransition) { func updateLayout(size: CGSize, cornerRadius: CGFloat, transition: ContainedViewLayoutTransition) {
let videoFrame = CGRect(origin: CGPoint(), size: size)
self.currentCornerRadius = cornerRadius self.currentCornerRadius = cornerRadius
let previousVideoFrame = self.videoTransformContainer.frame var rotationAngle: CGFloat
self.videoTransformContainer.frame = videoFrame var rotateFrame: Bool
if transition.isAnimated && !videoFrame.height.isZero && !previousVideoFrame.height.isZero { switch self.currentOrientation {
transition.animatePositionAdditive(node: self.videoTransformContainer, offset: CGPoint(x: previousVideoFrame.midX - videoFrame.midX, y: previousVideoFrame.midY - videoFrame.midY)) case .rotation0:
transition.animateTransformScale(node: self.videoTransformContainer, from: previousVideoFrame.height / videoFrame.height) 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 { if let effectView = self.effectView {
effectView.frame = videoFrame transition.updateFrame(view: 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.updateCornerRadius(layer: self.videoTransformContainer.layer, cornerRadius: self.currentCornerRadius) transition.updateCornerRadius(layer: self.videoTransformContainer.layer, cornerRadius: self.currentCornerRadius)
if let effectView = self.effectView { if let effectView = self.effectView {
transition.updateCornerRadius(layer: effectView.layer, cornerRadius: self.currentCornerRadius) transition.updateCornerRadius(layer: effectView.layer, cornerRadius: self.currentCornerRadius)
} }
transition.updateCornerRadius(layer: self.layer, cornerRadius: self.currentCornerRadius)
} }
func updateIsBlurred(isBlurred: Bool) { func updateIsBlurred(isBlurred: Bool) {
@ -178,6 +249,7 @@ final class CallControllerNode: ViewControllerTracingNode, CallControllerNodePro
private let keyButtonNode: HighlightableButtonNode private let keyButtonNode: HighlightableButtonNode
private var validLayout: (ContainerViewLayout, CGFloat)? private var validLayout: (ContainerViewLayout, CGFloat)?
private var disableActionsUntilTimestamp: Double = 0.0
var isMuted: Bool = false { var isMuted: Bool = false {
didSet { didSet {
@ -318,25 +390,38 @@ final class CallControllerNode: ViewControllerTracingNode, CallControllerNodePro
} }
self.buttonsNode.toggleVideo = { [weak self] in self.buttonsNode.toggleVideo = { [weak self] in
guard let strongSelf = self else { guard let strongSelf = self, let callState = strongSelf.callState else {
return return
} }
if strongSelf.outgoingVideoNodeValue == nil { switch callState.state {
strongSelf.call.requestVideo() case .active:
} else { if strongSelf.outgoingVideoNodeValue == nil {
strongSelf.isVideoPaused = !strongSelf.isVideoPaused strongSelf.call.requestVideo()
strongSelf.outgoingVideoNodeValue?.updateIsBlurred(isBlurred: strongSelf.isVideoPaused) } else {
strongSelf.buttonsNode.isCameraPaused = strongSelf.isVideoPaused strongSelf.isVideoPaused = !strongSelf.isVideoPaused
strongSelf.setIsVideoPaused?(strongSelf.isVideoPaused) strongSelf.outgoingVideoNodeValue?.updateIsBlurred(isBlurred: strongSelf.isVideoPaused)
strongSelf.buttonsNode.isCameraPaused = strongSelf.isVideoPaused
if let (layout, navigationBarHeight) = strongSelf.validLayout { strongSelf.setIsVideoPaused?(strongSelf.isVideoPaused)
strongSelf.containerLayoutUpdated(layout, navigationBarHeight: navigationBarHeight, transition: .animated(duration: 0.3, curve: .easeInOut))
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.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) self.keyButtonNode.addTarget(self, action: #selector(self.keyPressed), forControlEvents: .touchUpInside)
@ -347,7 +432,16 @@ final class CallControllerNode: ViewControllerTracingNode, CallControllerNodePro
override func didLoad() { override func didLoad() {
super.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) self.view.addGestureRecognizer(panRecognizer)
let tapRecognizer = UITapGestureRecognizer(target: self, action: #selector(self.tapGesture(_:))) 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 { if self.audioOutputState?.0 != availableOutputs || self.audioOutputState?.1 != currentOutput {
self.audioOutputState = (availableOutputs, currentOutput) self.audioOutputState = (availableOutputs, currentOutput)
self.updateButtonsMode() 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 { if let (layout, navigationBarHeight) = strongSelf.validLayout {
strongSelf.containerLayoutUpdated(layout, navigationBarHeight: navigationBarHeight, transition: .animated(duration: 0.5, curve: .spring)) 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.incomingVideoNodeValue = incomingVideoNode
strongSelf.expandedVideoNode = incomingVideoNode strongSelf.expandedVideoNode = incomingVideoNode
@ -437,15 +562,21 @@ final class CallControllerNode: ViewControllerTracingNode, CallControllerNodePro
if let outgoingVideoView = outgoingVideoView { if let outgoingVideoView = outgoingVideoView {
outgoingVideoView.view.backgroundColor = .black outgoingVideoView.view.backgroundColor = .black
outgoingVideoView.view.clipsToBounds = true outgoingVideoView.view.clipsToBounds = true
if let audioOutputState = strongSelf.audioOutputState, let currentOutput = audioOutputState.currentOutput { let outgoingVideoNode = CallVideoNode(videoView: outgoingVideoView, isReadyUpdated: {}, orientationUpdated: {
switch currentOutput { guard let strongSelf = self else {
case .speaker, .builtin: return
break
default:
strongSelf.setCurrentAudioOutput?(.speaker)
} }
} if let (layout, navigationBarHeight) = strongSelf.validLayout {
let outgoingVideoNode = CallVideoNode(videoView: outgoingVideoView, isReadyUpdated: {}) 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.outgoingVideoNodeValue = outgoingVideoNode
strongSelf.minimizedVideoNode = outgoingVideoNode strongSelf.minimizedVideoNode = outgoingVideoNode
if let expandedVideoNode = strongSelf.expandedVideoNode { if let expandedVideoNode = strongSelf.expandedVideoNode {
@ -456,6 +587,7 @@ final class CallControllerNode: ViewControllerTracingNode, CallControllerNodePro
if let (layout, navigationBarHeight) = strongSelf.validLayout { if let (layout, navigationBarHeight) = strongSelf.validLayout {
strongSelf.containerLayoutUpdated(layout, navigationBarHeight: navigationBarHeight, transition: .animated(duration: 0.4, curve: .spring)) 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: case .notAvailable:
mappedVideoState = .notAvailable mappedVideoState = .notAvailable
case .possible: case .possible:
mappedVideoState = .possible var isEnabled = false
switch callState.state {
case .active:
isEnabled = true
default:
break
}
mappedVideoState = .possible(isEnabled)
case .outgoingRequested: case .outgoingRequested:
mappedVideoState = .outgoingRequested mappedVideoState = .outgoingRequested
case .incomingRequested: case .incomingRequested:
@ -654,8 +793,6 @@ final class CallControllerNode: ViewControllerTracingNode, CallControllerNodePro
} }
if let (layout, navigationHeight) = self.validLayout { if let (layout, navigationHeight) = self.validLayout {
self.pictureInPictureTransitionFraction = 0.0
self.containerLayoutUpdated(layout, navigationBarHeight: navigationHeight, transition: .animated(duration: 0.3, curve: .spring)) 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) { func animateOut(completion: @escaping () -> Void) {
self.statusBar.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3, removeOnCompletion: false) self.statusBar.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3, removeOnCompletion: false)
if !self.shouldStayHiddenUntilConnection || self.containerNode.alpha > 0.0 { 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 self.containerNode.layer.animateScale(from: 1.0, to: 1.04, duration: 0.3, removeOnCompletion: false, completion: { _ in
completion() completion()
}) })
@ -723,7 +863,15 @@ final class CallControllerNode: ViewControllerTracingNode, CallControllerNodePro
insets.right = interpolate(from: expandedInset, to: insets.right, value: 1.0 - self.pictureInPictureTransitionFraction) 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 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 previewVideoY: CGFloat
let previewVideoX: CGFloat let previewVideoX: CGFloat
@ -852,6 +1000,7 @@ final class CallControllerNode: ViewControllerTracingNode, CallControllerNodePro
transition.updateAlpha(node: self.buttonsNode, alpha: overlayAlpha) transition.updateAlpha(node: self.buttonsNode, alpha: overlayAlpha)
let fullscreenVideoFrame = CGRect(origin: CGPoint(), size: layout.size) let fullscreenVideoFrame = CGRect(origin: CGPoint(), size: layout.size)
let previewVideoFrame = self.calculatePreviewVideoRect(layout: layout, navigationHeight: navigationBarHeight) let previewVideoFrame = self.calculatePreviewVideoRect(layout: layout, navigationHeight: navigationBarHeight)
if let expandedVideoNode = self.expandedVideoNode { if let expandedVideoNode = self.expandedVideoNode {
@ -933,6 +1082,10 @@ final class CallControllerNode: ViewControllerTracingNode, CallControllerNodePro
private var debugTapCounter: (Double, Int) = (0.0, 0) private var debugTapCounter: (Double, Int) = (0.0, 0)
private func areUserActionsDisabledNow() -> Bool {
return CACurrentMediaTime() < self.disableActionsUntilTimestamp
}
@objc func tapGesture(_ recognizer: UITapGestureRecognizer) { @objc func tapGesture(_ recognizer: UITapGestureRecognizer) {
if case .ended = recognizer.state { if case .ended = recognizer.state {
if !self.pictureInPictureTransitionFraction.isZero { if !self.pictureInPictureTransitionFraction.isZero {
@ -947,17 +1100,20 @@ final class CallControllerNode: ViewControllerTracingNode, CallControllerNodePro
if let expandedVideoNode = self.expandedVideoNode, let minimizedVideoNode = self.minimizedVideoNode { if let expandedVideoNode = self.expandedVideoNode, let minimizedVideoNode = self.minimizedVideoNode {
let point = recognizer.location(in: recognizer.view) let point = recognizer.location(in: recognizer.view)
if minimizedVideoNode.frame.contains(point) { if minimizedVideoNode.frame.contains(point) {
let copyView = minimizedVideoNode.view.snapshotView(afterScreenUpdates: false) if !self.areUserActionsDisabledNow() {
copyView?.frame = minimizedVideoNode.frame let copyView = minimizedVideoNode.view.snapshotView(afterScreenUpdates: false)
self.expandedVideoNode = minimizedVideoNode copyView?.frame = minimizedVideoNode.frame
self.minimizedVideoNode = expandedVideoNode self.expandedVideoNode = minimizedVideoNode
if let supernode = expandedVideoNode.supernode { self.minimizedVideoNode = expandedVideoNode
supernode.insertSubnode(expandedVideoNode, aboveSubnode: minimizedVideoNode) if let supernode = expandedVideoNode.supernode {
} supernode.insertSubnode(expandedVideoNode, aboveSubnode: minimizedVideoNode)
if let (layout, navigationBarHeight) = self.validLayout { }
self.disableAnimationForExpandedVideoOnce = true self.disableActionsUntilTimestamp = CACurrentMediaTime() + 0.3
self.animationForExpandedVideoSnapshotView = copyView if let (layout, navigationBarHeight) = self.validLayout {
self.containerLayoutUpdated(layout, navigationBarHeight: navigationBarHeight, transition: .animated(duration: 0.3, curve: .easeInOut)) self.disableAnimationForExpandedVideoOnce = true
self.animationForExpandedVideoSnapshotView = copyView
self.containerLayoutUpdated(layout, navigationBarHeight: navigationBarHeight, transition: .animated(duration: 0.3, curve: .easeInOut))
}
} }
} else { } else {
var updated = false 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 { switch recognizer.state {
case .began: case .began:
let location = recognizer.location(in: self.view) guard let location = recognizer.firstLocation else {
if self.self.pictureInPictureTransitionFraction.isZero, let _ = self.expandedVideoNode, let minimizedVideoNode = self.minimizedVideoNode, minimizedVideoNode.frame.contains(location) { 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 self.minimizedVideoInitialPosition = minimizedVideoNode.position
} else { } else if let _ = self.expandedVideoNode, let _ = self.minimizedVideoNode {
self.minimizedVideoInitialPosition = nil self.minimizedVideoInitialPosition = nil
if !self.pictureInPictureTransitionFraction.isZero { if !self.pictureInPictureTransitionFraction.isZero {
self.pictureInPictureGestureState = .dragging(initialPosition: self.containerTransformationNode.position, draggingPosition: self.containerTransformationNode.position) self.pictureInPictureGestureState = .dragging(initialPosition: self.containerTransformationNode.position, draggingPosition: self.containerTransformationNode.position)
} else { } else {
self.pictureInPictureGestureState = .collapsing(didSelectCorner: false) self.pictureInPictureGestureState = .collapsing(didSelectCorner: false)
} }
} else {
self.pictureInPictureGestureState = .none
} }
case .changed: case .changed:
if let minimizedVideoNode = self.minimizedVideoNode, let minimizedVideoInitialPosition = self.minimizedVideoInitialPosition { if let minimizedVideoNode = self.minimizedVideoNode, let minimizedVideoInitialPosition = self.minimizedVideoInitialPosition {
@ -1266,3 +1426,38 @@ final class CallControllerNode: ViewControllerTracingNode, CallControllerNodePro
return nil 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<UITouch>, 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<UITouch>, with event: UIEvent) {
super.touchesMoved(touches, with: event)
}
}

View File

@ -810,10 +810,45 @@ public final class PresentationCallImpl: PresentationCall {
self.ongoingContext?.makeIncomingVideoView(completion: { view in self.ongoingContext?.makeIncomingVideoView(completion: { view in
if let view = view { if let view = view {
let setOnFirstFrameReceived = view.setOnFirstFrameReceived let setOnFirstFrameReceived = view.setOnFirstFrameReceived
let setOnOrientationUpdated = view.setOnOrientationUpdated
completion(PresentationCallVideoView( completion(PresentationCallVideoView(
view: view.view, view: view.view,
setOnFirstFrameReceived: { f in setOnFirstFrameReceived: { f in
setOnFirstFrameReceived(f) 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 { } else {
@ -831,11 +866,47 @@ public final class PresentationCallImpl: PresentationCall {
self.videoCapturer?.makeOutgoingVideoView(completion: { view in self.videoCapturer?.makeOutgoingVideoView(completion: { view in
if let view = view { if let view = view {
let setOnFirstFrameReceived = view.setOnFirstFrameReceived let setOnFirstFrameReceived = view.setOnFirstFrameReceived
let setOnOrientationUpdated = view.setOnOrientationUpdated
completion(PresentationCallVideoView( completion(PresentationCallVideoView(
view: view.view, view: view.view,
setOnFirstFrameReceived: { f in setOnFirstFrameReceived: { f in
setOnFirstFrameReceived(f) 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 { } else {
completion(nil) completion(nil)

View File

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.7 KiB

View File

@ -570,6 +570,9 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
if strongSelf.presentationInterfaceState.interfaceState.selectionState != nil { if strongSelf.presentationInterfaceState.interfaceState.selectionState != nil {
return return
} }
strongSelf.dismissAllTooltips()
let recognizer: TapLongTapOrDoubleTapGestureRecognizer? = anyRecognizer as? TapLongTapOrDoubleTapGestureRecognizer let recognizer: TapLongTapOrDoubleTapGestureRecognizer? = anyRecognizer as? TapLongTapOrDoubleTapGestureRecognizer
let gesture: ContextGesture? = anyRecognizer as? ContextGesture let gesture: ContextGesture? = anyRecognizer as? ContextGesture
if let messages = strongSelf.chatDisplayNode.historyNode.messageGroupInCurrentHistoryView(message.id) { if let messages = strongSelf.chatDisplayNode.historyNode.messageGroupInCurrentHistoryView(message.id) {
@ -1974,7 +1977,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
}) })
}, displaySwipeToReplyHint: { [weak self] in }, displaySwipeToReplyHint: { [weak self] in
if let strongSelf = self, let validLayout = strongSelf.validLayout, min(validLayout.size.width, validLayout.size.height) > 320.0 { 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 }, dismissReplyMarkupMessage: { [weak self] message in
guard let strongSelf = self, strongSelf.presentationInterfaceState.keyboardButtonsMessage?.id == message.id else { 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 { guard let strongSelf = self else {
return return
} }
if strongSelf.presentationInterfaceState.interfaceState.selectionState != nil {
return
}
strongSelf.dismissAllTooltips()
let context = strongSelf.context let context = strongSelf.context
let _ = (context.account.postbox.transaction { transaction -> Peer? in let _ = (context.account.postbox.transaction { transaction -> Peer? in
return transaction.getPeer(peer.id) return transaction.getPeer(peer.id)
@ -5217,18 +5227,6 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
self.dismissAllTooltips() 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() self.sendMessageActionsController?.dismiss()
if let _ = self.peekData { if let _ = self.peekData {
@ -6066,7 +6064,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
disposable.set((signal disposable.set((signal
|> deliverOnMainQueue).start(completed: { [weak self] in |> deliverOnMainQueue).start(completed: { [weak self] in
if let strongSelf = self, let _ = strongSelf.validLayout { 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 { 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 { 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)]) strongSelf.sendMessages([.message(text: "", attributes: [], mediaReference: AnyMediaReference.standalone(media: TelegramMediaDice(emoji: dice.emoji)), replyToMessageId: nil, localGroupingKey: nil)])
} }
return false return false
}), in: .window(.root)) }), in: .current)
} }
} }
@ -9168,6 +9166,21 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
self.silentPostTooltipController?.dismiss() self.silentPostTooltipController?.dismiss()
self.mediaRecordingModeTooltipController?.dismiss() self.mediaRecordingModeTooltipController?.dismiss()
self.mediaRestrictedTooltipController?.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() { private func commitPurposefulAction() {

View File

@ -974,10 +974,14 @@ func chatAvailableMessageActionsImpl(postbox: Postbox, accountPeerId: PeerId, me
optionsMap[id]!.insert(.deleteLocally) optionsMap[id]!.insert(.deleteLocally)
} else if let peer = transaction.getPeer(id.peerId) { } else if let peer = transaction.getPeer(id.peerId) {
var isAction = false var isAction = false
var isDice = false
for media in message.media { for media in message.media {
if media is TelegramMediaAction || media is TelegramMediaExpiredContent { if media is TelegramMediaAction || media is TelegramMediaExpiredContent {
isAction = true isAction = true
} }
if media is TelegramMediaDice {
isDice = true
}
} }
if let channel = peer as? TelegramChannel { if let channel = peer as? TelegramChannel {
if message.flags.contains(.Incoming) { if message.flags.contains(.Incoming) {
@ -1064,6 +1068,11 @@ func chatAvailableMessageActionsImpl(postbox: Postbox, accountPeerId: PeerId, me
} else if limitsConfiguration.canRemoveIncomingMessagesInPrivateChats { } else if limitsConfiguration.canRemoveIncomingMessagesInPrivateChats {
canDeleteGlobally = true canDeleteGlobally = true
} }
let timestamp = Int32(CFAbsoluteTimeGetCurrent() + NSTimeIntervalSince1970)
if isDice && Int64(message.timestamp) + 60 * 60 * 24 > Int64(timestamp) {
canDeleteGlobally = false
}
if message.flags.contains(.Incoming) { if message.flags.contains(.Incoming) {
hadPersonalIncoming = true hadPersonalIncoming = true
} }

View File

@ -183,7 +183,7 @@ class ChatSearchResultsControllerNode: ViewControllerTracingNode, UIScrollViewDe
}, setPeerIdWithRevealedOptions: { _, _ in }, setPeerIdWithRevealedOptions: { _, _ in
}, setItemPinned: { _, _ in }, setItemPinned: { _, _ in
}, setPeerMuted: { _, _ in }, setPeerMuted: { _, _ in
}, deletePeer: { _ in }, deletePeer: { _, _ in
}, updatePeerGrouping: { _, _ in }, updatePeerGrouping: { _, _ in
}, togglePeerMarkedUnread: { _, _ in }, togglePeerMarkedUnread: { _, _ in
}, toggleArchivedFolderHiddenByDefault: { }, toggleArchivedFolderHiddenByDefault: {

View File

@ -3837,7 +3837,7 @@ private final class PeerInfoScreenNode: ViewControllerTracingNode, UIScrollViewD
} }
for childController in tabController.controllers { for childController in tabController.controllers {
if let chatListController = childController as? ChatListController { 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 { if deleted {
navigationController?.popToRoot(animated: true) navigationController?.popToRoot(animated: true)
} }
@ -4032,7 +4032,7 @@ private final class PeerInfoScreenNode: ViewControllerTracingNode, UIScrollViewD
return mapResourceToAvatarSizes(postbox: account.postbox, resource: resource, representations: representations) return mapResourceToAvatarSizes(postbox: account.postbox, resource: resource, representations: representations)
}) })
} else { } 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) return mapResourceToAvatarSizes(postbox: account.postbox, resource: resource, representations: representations)
}) })
} }

View File

@ -88,24 +88,24 @@ final class WebEmbedPlayerNode: ASDisplayNode, WKNavigationDelegate {
let userContentController = WKUserContentController() 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)) 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() let configuration = WKWebViewConfiguration()
config.allowsInlineMediaPlayback = true configuration.allowsInlineMediaPlayback = true
config.userContentController = userContentController configuration.userContentController = userContentController
if #available(iOSApplicationExtension 10.0, iOS 10.0, *) { if #available(iOSApplicationExtension 10.0, iOS 10.0, *) {
config.mediaTypesRequiringUserActionForPlayback = [] configuration.mediaTypesRequiringUserActionForPlayback = []
} else if #available(iOSApplicationExtension 9.0, iOS 9.0, *) { } else if #available(iOSApplicationExtension 9.0, iOS 9.0, *) {
config.requiresUserActionForMediaPlayback = false configuration.requiresUserActionForMediaPlayback = false
} else { } else {
config.mediaPlaybackRequiresUserAction = false configuration.mediaPlaybackRequiresUserAction = false
} }
if #available(iOSApplicationExtension 9.0, iOS 9.0, *) { if #available(iOSApplicationExtension 9.0, iOS 9.0, *) {
config.allowsPictureInPictureMediaPlayback = false configuration.allowsPictureInPictureMediaPlayback = false
} }
let frame = CGRect(origin: CGPoint.zero, size: intrinsicDimensions) let frame = CGRect(origin: CGPoint.zero, size: intrinsicDimensions)
self.webView = WKWebView(frame: frame, configuration: config) self.webView = WKWebView(frame: frame, configuration: configuration)
super.init() super.init()
self.frame = frame self.frame = frame

View File

@ -318,6 +318,11 @@ public final class OngoingCallVideoCapturer {
view: view, view: view,
setOnFirstFrameReceived: { [weak view] f in setOnFirstFrameReceived: { [weak view] f in
view?.setOnFirstFrameReceived(f) view?.setOnFirstFrameReceived(f)
},
getOrientation: {
return .rotation0
},
setOnOrientationUpdated: { _ in
} }
)) ))
} else { } 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 final class OngoingCallContextPresentationCallVideoView {
public let view: UIView public let view: UIView
public let setOnFirstFrameReceived: ((() -> Void)?) -> Void public let setOnFirstFrameReceived: ((() -> Void)?) -> Void
public let getOrientation: () -> OngoingCallVideoOrientation
public let setOnOrientationUpdated: (((OngoingCallVideoOrientation) -> Void)?) -> Void
public init( public init(
view: UIView, view: UIView,
setOnFirstFrameReceived: @escaping ((() -> Void)?) -> Void setOnFirstFrameReceived: @escaping ((() -> Void)?) -> Void,
getOrientation: @escaping () -> OngoingCallVideoOrientation,
setOnOrientationUpdated: @escaping (((OngoingCallVideoOrientation) -> Void)?) -> Void
) { ) {
self.view = view self.view = view
self.setOnFirstFrameReceived = setOnFirstFrameReceived self.setOnFirstFrameReceived = setOnFirstFrameReceived
self.getOrientation = getOrientation
self.setOnOrientationUpdated = setOnOrientationUpdated
} }
} }
@ -721,6 +756,18 @@ public final class OngoingCallContext {
view: view, view: view,
setOnFirstFrameReceived: { [weak view] f in setOnFirstFrameReceived: { [weak view] f in
view?.setOnFirstFrameReceived(f) 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 { } else {

View File

@ -41,6 +41,13 @@ typedef NS_ENUM(int32_t, OngoingCallRemoteVideoStateWebrtc) {
OngoingCallRemoteVideoStateActive OngoingCallRemoteVideoStateActive
}; };
typedef NS_ENUM(int32_t, OngoingCallVideoOrientationWebrtc) {
OngoingCallVideoOrientation0,
OngoingCallVideoOrientation90,
OngoingCallVideoOrientation180,
OngoingCallVideoOrientation270
};
typedef NS_ENUM(int32_t, OngoingCallNetworkTypeWebrtc) { typedef NS_ENUM(int32_t, OngoingCallNetworkTypeWebrtc) {
OngoingCallNetworkTypeWifi, OngoingCallNetworkTypeWifi,
OngoingCallNetworkTypeCellularGprs, OngoingCallNetworkTypeCellularGprs,
@ -87,7 +94,10 @@ typedef NS_ENUM(int32_t, OngoingCallDataSavingWebrtc) {
@protocol OngoingCallThreadLocalContextWebrtcVideoView <NSObject> @protocol OngoingCallThreadLocalContextWebrtcVideoView <NSObject>
@property (nonatomic, readonly) OngoingCallVideoOrientationWebrtc orientation;
- (void)setOnFirstFrameReceived:(void (^ _Nullable)())onFirstFrameReceived; - (void)setOnFirstFrameReceived:(void (^ _Nullable)())onFirstFrameReceived;
- (void)setOnOrientationUpdated:(void (^ _Nullable)(OngoingCallVideoOrientationWebrtc))onOrientationUpdated;
@end @end

View File

@ -42,20 +42,66 @@
@end @end
@interface VideoMetalView (VideoViewImpl) <OngoingCallThreadLocalContextWebrtcVideoView> @protocol OngoingCallThreadLocalContextWebrtcVideoViewImpl <NSObject>
@property (nonatomic, readwrite) OngoingCallVideoOrientationWebrtc orientation;
@end
@interface VideoMetalView (VideoViewImpl) <OngoingCallThreadLocalContextWebrtcVideoView, OngoingCallThreadLocalContextWebrtcVideoViewImpl>
@property (nonatomic, readwrite) OngoingCallVideoOrientationWebrtc orientation;
@end @end
@implementation VideoMetalView (VideoViewImpl) @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 @end
@interface GLVideoView (VideoViewImpl) <OngoingCallThreadLocalContextWebrtcVideoView> @interface GLVideoView (VideoViewImpl) <OngoingCallThreadLocalContextWebrtcVideoView, OngoingCallThreadLocalContextWebrtcVideoViewImpl>
@property (nonatomic, readwrite) OngoingCallVideoOrientationWebrtc orientation;
@end @end
@implementation GLVideoView (VideoViewImpl) @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 @end
@implementation OngoingCallThreadLocalContextVideoCapturer @implementation OngoingCallThreadLocalContextVideoCapturer
@ -68,6 +114,9 @@
return self; return self;
} }
- (void)dealloc {
}
- (void)switchVideoCamera { - (void)switchVideoCamera {
_interface->switchCamera(); _interface->switchCamera();
} }
@ -140,6 +189,8 @@
OngoingCallVideoStateWebrtc _videoState; OngoingCallVideoStateWebrtc _videoState;
bool _connectedOnce; bool _connectedOnce;
OngoingCallRemoteVideoStateWebrtc _remoteVideoState; OngoingCallRemoteVideoStateWebrtc _remoteVideoState;
OngoingCallVideoOrientationWebrtc _remoteVideoOrientation;
__weak UIView<OngoingCallThreadLocalContextWebrtcVideoViewImpl> *_currentRemoteVideoRenderer;
OngoingCallThreadLocalContextVideoCapturer *_videoCapturer; OngoingCallThreadLocalContextVideoCapturer *_videoCapturer;
int32_t _signalBars; int32_t _signalBars;
@ -267,6 +318,8 @@ static void (*InternalVoipLoggingFunction)(NSString *) = NULL;
_remoteVideoState = OngoingCallRemoteVideoStateActive; _remoteVideoState = OngoingCallRemoteVideoStateActive;
} }
_remoteVideoOrientation = OngoingCallVideoOrientation0;
std::vector<uint8_t> derivedStateValue; std::vector<uint8_t> derivedStateValue;
derivedStateValue.resize(derivedState.length); derivedStateValue.resize(derivedState.length);
[derivedState getBytes:derivedStateValue.data() length:derivedState.length]; [derivedState getBytes:derivedStateValue.data() length:derivedState.length];
@ -568,6 +621,8 @@ static void (*InternalVoipLoggingFunction)(NSString *) = NULL;
std::shared_ptr<rtc::VideoSinkInterface<webrtc::VideoFrame>> sink = [remoteRenderer getSink]; std::shared_ptr<rtc::VideoSinkInterface<webrtc::VideoFrame>> sink = [remoteRenderer getSink];
__strong OngoingCallThreadLocalContextWebrtc *strongSelf = weakSelf; __strong OngoingCallThreadLocalContextWebrtc *strongSelf = weakSelf;
if (strongSelf) { if (strongSelf) {
[remoteRenderer setOrientation:strongSelf->_remoteVideoOrientation];
strongSelf->_currentRemoteVideoRenderer = remoteRenderer;
strongSelf->_tgVoip->setIncomingVideoOutput(sink); strongSelf->_tgVoip->setIncomingVideoOutput(sink);
} }
@ -578,6 +633,8 @@ static void (*InternalVoipLoggingFunction)(NSString *) = NULL;
std::shared_ptr<rtc::VideoSinkInterface<webrtc::VideoFrame>> sink = [remoteRenderer getSink]; std::shared_ptr<rtc::VideoSinkInterface<webrtc::VideoFrame>> sink = [remoteRenderer getSink];
__strong OngoingCallThreadLocalContextWebrtc *strongSelf = weakSelf; __strong OngoingCallThreadLocalContextWebrtc *strongSelf = weakSelf;
if (strongSelf) { if (strongSelf) {
[remoteRenderer setOrientation:strongSelf->_remoteVideoOrientation];
strongSelf->_currentRemoteVideoRenderer = remoteRenderer;
strongSelf->_tgVoip->setIncomingVideoOutput(sink); strongSelf->_tgVoip->setIncomingVideoOutput(sink);
} }

@ -1 +1 @@
Subproject commit 8e9d3e56d43ffa4ed9ababd5fe7a4b5df8ec94d1 Subproject commit c3345bb26aba541c99ff3c7075bda8024c7a8202