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