mirror of
https://github.com/TelegramMessenger/Telegram-iOS.git
synced 2026-07-06 03:33:41 +02:00
Merge branch 'master' of gitlab.com:peter-iakovlev/telegram-ios
This commit is contained in:
commit
eac6d297d0
95 changed files with 6089 additions and 4686 deletions
|
|
@ -156,7 +156,7 @@ private func callWithTelegramMessage(_ telegramMessage: Message, account: Accoun
|
|||
|
||||
var duration: Int32?
|
||||
for media in telegramMessage.media {
|
||||
if let action = media as? TelegramMediaAction, case let .phoneCall(_, _, callDuration) = action.action {
|
||||
if let action = media as? TelegramMediaAction, case let .phoneCall(_, _, callDuration, _) = action.action {
|
||||
duration = callDuration
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2289,7 +2289,9 @@ Unused sets are archived when you add more.";
|
|||
|
||||
"Notification.CallTimeFormat" = "%1$@ (%2$@)"; // 1 - type, 2 - duration
|
||||
"Notification.CallOutgoing" = "Outgoing Call";
|
||||
"Notification.VideoCallOutgoing" = "Outgoing Video Call";
|
||||
"Notification.CallIncoming" = "Incoming Call";
|
||||
"Notification.VideoCallIncoming" = "Incoming Video Call";
|
||||
"Notification.CallMissed" = "Missed Call";
|
||||
"Notification.CallCanceled" = "Cancelled Call";
|
||||
"Notification.CallOutgoingShort" = "Outgoing";
|
||||
|
|
@ -5328,6 +5330,7 @@ Any member of this group will be able to see messages in the channel.";
|
|||
"PeerInfo.ButtonMessage" = "Message";
|
||||
"PeerInfo.ButtonDiscuss" = "Discuss";
|
||||
"PeerInfo.ButtonCall" = "Call";
|
||||
"PeerInfo.ButtonVideoCall" = "Video Call";
|
||||
"PeerInfo.ButtonMute" = "Mute";
|
||||
"PeerInfo.ButtonUnmute" = "Unmute";
|
||||
"PeerInfo.ButtonMore" = "More";
|
||||
|
|
@ -5620,3 +5623,9 @@ Any member of this group will be able to see messages in the channel.";
|
|||
|
||||
"Stats.GroupTopInviter.History" = "History";
|
||||
"Stats.GroupTopInviter.Promote" = "Promote";
|
||||
|
||||
"PrivacySettings.AutoArchiveTitle" = "NEW CHATS FROM UNKNOWN USERS";
|
||||
"PrivacySettings.AutoArchive" = "Archive and Mute";
|
||||
"PrivacySettings.AutoArchiveInfo" = "Automatically archive and mute new chats, groups and channels from non-contacts.";
|
||||
|
||||
"Call.RemoteVideoPaused" = "%@'s video paused";
|
||||
|
|
|
|||
|
|
@ -30,7 +30,7 @@ public final class OpenChatMessageParams {
|
|||
public let addToTransitionSurface: (UIView) -> Void
|
||||
public let openUrl: (String) -> Void
|
||||
public let openPeer: (Peer, ChatControllerInteractionNavigateToPeer) -> Void
|
||||
public let callPeer: (PeerId) -> Void
|
||||
public let callPeer: (PeerId, Bool) -> Void
|
||||
public let enqueueMessage: (EnqueueMessage) -> Void
|
||||
public let sendSticker: ((FileMediaReference, ASDisplayNode, CGRect) -> Bool)?
|
||||
public let setupTemporaryHiddenMedia: (Signal<Any?, NoError>, Int, Media) -> Void
|
||||
|
|
@ -51,7 +51,7 @@ public final class OpenChatMessageParams {
|
|||
addToTransitionSurface: @escaping (UIView) -> Void,
|
||||
openUrl: @escaping (String) -> Void,
|
||||
openPeer: @escaping (Peer, ChatControllerInteractionNavigateToPeer) -> Void,
|
||||
callPeer: @escaping (PeerId) -> Void,
|
||||
callPeer: @escaping (PeerId, Bool) -> Void,
|
||||
enqueueMessage: @escaping (EnqueueMessage) -> Void,
|
||||
sendSticker: ((FileMediaReference, ASDisplayNode, CGRect) -> Bool)?,
|
||||
setupTemporaryHiddenMedia: @escaping (Signal<Any?, NoError>, Int, Media) -> Void,
|
||||
|
|
|
|||
|
|
@ -1,7 +1,24 @@
|
|||
import Foundation
|
||||
import UIKit
|
||||
import Display
|
||||
|
||||
public final class OverlayMediaControllerEmbeddingItem {
|
||||
public let position: CGPoint
|
||||
public let itemNode: OverlayMediaItemNode
|
||||
|
||||
public init(
|
||||
position: CGPoint,
|
||||
itemNode: OverlayMediaItemNode
|
||||
) {
|
||||
self.position = position
|
||||
self.itemNode = itemNode
|
||||
}
|
||||
}
|
||||
|
||||
public protocol OverlayMediaController: class {
|
||||
var updatePossibleEmbeddingItem: ((OverlayMediaControllerEmbeddingItem?) -> Void)? { get set }
|
||||
var embedPossibleEmbeddingItem: ((OverlayMediaControllerEmbeddingItem) -> Bool)? { get set }
|
||||
|
||||
var hasNodes: Bool { get }
|
||||
func addNode(_ node: OverlayMediaItemNode, customTransition: Bool)
|
||||
func removeNode(_ node: OverlayMediaItemNode, customTransition: Bool)
|
||||
|
|
@ -10,10 +27,21 @@ public protocol OverlayMediaController: class {
|
|||
public final class OverlayMediaManager {
|
||||
public var controller: (OverlayMediaController & ViewController)?
|
||||
|
||||
public var updatePossibleEmbeddingItem: ((OverlayMediaControllerEmbeddingItem?) -> Void)?
|
||||
public var embedPossibleEmbeddingItem: ((OverlayMediaControllerEmbeddingItem) -> Bool)?
|
||||
|
||||
public init() {
|
||||
}
|
||||
|
||||
public func attachOverlayMediaController(_ controller: OverlayMediaController & ViewController) {
|
||||
self.controller = controller
|
||||
|
||||
controller.updatePossibleEmbeddingItem = { [weak self] item in
|
||||
self?.updatePossibleEmbeddingItem?(item)
|
||||
}
|
||||
|
||||
controller.embedPossibleEmbeddingItem = { [weak self] item in
|
||||
return self?.embedPossibleEmbeddingItem?(item) ?? false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -11,6 +11,27 @@ public enum RequestCallResult {
|
|||
case alreadyInProgress(PeerId)
|
||||
}
|
||||
|
||||
public struct CallAuxiliaryServer {
|
||||
public enum Connection {
|
||||
case stun
|
||||
case turn(username: String, password: String)
|
||||
}
|
||||
|
||||
public let host: String
|
||||
public let port: Int
|
||||
public let connection: Connection
|
||||
|
||||
public init(
|
||||
host: String,
|
||||
port: Int,
|
||||
connection: Connection
|
||||
) {
|
||||
self.host = host
|
||||
self.port = port
|
||||
self.connection = connection
|
||||
}
|
||||
}
|
||||
|
||||
public struct PresentationCallState: Equatable {
|
||||
public enum State: Equatable {
|
||||
case waiting
|
||||
|
|
@ -27,14 +48,22 @@ public struct PresentationCallState: Equatable {
|
|||
case notAvailable
|
||||
case available(Bool)
|
||||
case active
|
||||
case activeOutgoing
|
||||
}
|
||||
|
||||
public enum RemoteVideoState: Equatable {
|
||||
case inactive
|
||||
case active
|
||||
}
|
||||
|
||||
public var state: State
|
||||
public var videoState: VideoState
|
||||
public var remoteVideoState: RemoteVideoState
|
||||
|
||||
public init(state: State, videoState: VideoState) {
|
||||
public init(state: State, videoState: VideoState, remoteVideoState: RemoteVideoState) {
|
||||
self.state = state
|
||||
self.videoState = videoState
|
||||
self.remoteVideoState = remoteVideoState
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -72,5 +101,5 @@ public protocol PresentationCall: class {
|
|||
public protocol PresentationCallManager: class {
|
||||
var currentCallSignal: Signal<PresentationCall?, NoError> { get }
|
||||
|
||||
func requestCall(account: Account, peerId: PeerId, endCurrentIfAny: Bool) -> RequestCallResult
|
||||
func requestCall(account: Account, peerId: PeerId, isVideo: Bool, endCurrentIfAny: Bool) -> RequestCallResult
|
||||
}
|
||||
|
|
|
|||
|
|
@ -135,7 +135,16 @@ class CallListCallItem: ListViewItem {
|
|||
|
||||
func selected(listView: ListView) {
|
||||
listView.clearHighlightAnimated(true)
|
||||
self.interaction.call(self.topMessage.id.peerId)
|
||||
var isVideo = false
|
||||
for media in self.topMessage.media {
|
||||
if let action = media as? TelegramMediaAction {
|
||||
if case let .phoneCall(_, _, _, isVideoValue) = action.action {
|
||||
break
|
||||
isVideo = isVideoValue
|
||||
}
|
||||
}
|
||||
}
|
||||
self.interaction.call(self.topMessage.id.peerId, isVideo)
|
||||
}
|
||||
|
||||
static func mergeType(item: CallListCallItem, previousItem: ListViewItem?, nextItem: ListViewItem?) -> (first: Bool, last: Bool, firstWithHeader: Bool) {
|
||||
|
|
@ -237,7 +246,16 @@ class CallListCallItemNode: ItemListRevealOptionsItemNode {
|
|||
guard let item = self?.layoutParams?.0 else {
|
||||
return false
|
||||
}
|
||||
item.interaction.call(item.topMessage.id.peerId)
|
||||
var isVideo = false
|
||||
for media in item.topMessage.media {
|
||||
if let action = media as? TelegramMediaAction {
|
||||
if case let .phoneCall(_, _, _, isVideoValue) = action.action {
|
||||
break
|
||||
isVideo = isVideoValue
|
||||
}
|
||||
}
|
||||
}
|
||||
item.interaction.call(item.topMessage.id.peerId, isVideo)
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
|
@ -357,7 +375,7 @@ class CallListCallItemNode: ItemListRevealOptionsItemNode {
|
|||
for message in item.messages {
|
||||
inner: for media in message.media {
|
||||
if let action = media as? TelegramMediaAction {
|
||||
if case let .phoneCall(_, discardReason, duration) = action.action {
|
||||
if case let .phoneCall(_, discardReason, duration, _) = action.action {
|
||||
if message.flags.contains(.Incoming) {
|
||||
hasIncoming = true
|
||||
|
||||
|
|
|
|||
|
|
@ -145,9 +145,9 @@ public final class CallListController: ViewController {
|
|||
}
|
||||
|
||||
override public func loadDisplayNode() {
|
||||
self.displayNode = CallListControllerNode(context: self.context, mode: self.mode, presentationData: self.presentationData, call: { [weak self] peerId in
|
||||
self.displayNode = CallListControllerNode(context: self.context, mode: self.mode, presentationData: self.presentationData, call: { [weak self] peerId, isVideo in
|
||||
if let strongSelf = self {
|
||||
strongSelf.call(peerId)
|
||||
strongSelf.call(peerId, isVideo: isVideo)
|
||||
}
|
||||
}, openInfo: { [weak self] peerId, messages in
|
||||
if let strongSelf = self {
|
||||
|
|
@ -201,6 +201,10 @@ public final class CallListController: ViewController {
|
|||
}
|
||||
|
||||
@objc func callPressed() {
|
||||
self.beginCallImpl(isVideo: false)
|
||||
}
|
||||
|
||||
private func beginCallImpl(isVideo: Bool) {
|
||||
let controller = self.context.sharedContext.makeContactSelectionController(ContactSelectionControllerParams(context: self.context, title: { $0.Calls_NewCall }))
|
||||
controller.navigationPresentation = .modal
|
||||
self.createActionDisposable.set((controller.result
|
||||
|
|
@ -208,7 +212,7 @@ public final class CallListController: ViewController {
|
|||
|> deliverOnMainQueue).start(next: { [weak controller, weak self] peer in
|
||||
controller?.dismissSearch()
|
||||
if let strongSelf = self, let contactPeer = peer, case let .peer(peer, _, _) = contactPeer {
|
||||
strongSelf.call(peer.id, began: {
|
||||
strongSelf.call(peer.id, isVideo: isVideo, began: {
|
||||
if let strongSelf = self {
|
||||
let _ = (strongSelf.context.sharedContext.hasOngoingCall.get()
|
||||
|> filter { $0 }
|
||||
|
|
@ -257,7 +261,7 @@ public final class CallListController: ViewController {
|
|||
}
|
||||
}
|
||||
|
||||
private func call(_ peerId: PeerId, began: (() -> Void)? = nil) {
|
||||
private func call(_ peerId: PeerId, isVideo: Bool, began: (() -> Void)? = nil) {
|
||||
self.peerViewDisposable.set((self.context.account.viewTracker.peerView(peerId)
|
||||
|> take(1)
|
||||
|> deliverOnMainQueue).start(next: { [weak self] view in
|
||||
|
|
@ -273,7 +277,7 @@ public final class CallListController: ViewController {
|
|||
return
|
||||
}
|
||||
|
||||
let callResult = strongSelf.context.sharedContext.callManager?.requestCall(account: strongSelf.context.account, peerId: peerId, endCurrentIfAny: false)
|
||||
let callResult = strongSelf.context.sharedContext.callManager?.requestCall(account: strongSelf.context.account, peerId: peerId, isVideo: isVideo, endCurrentIfAny: false)
|
||||
if let callResult = callResult {
|
||||
if case let .alreadyInProgress(currentPeerId) = callResult {
|
||||
if currentPeerId == peerId {
|
||||
|
|
@ -287,7 +291,7 @@ public final class CallListController: ViewController {
|
|||
if let strongSelf = self, let peer = peer, let current = current {
|
||||
strongSelf.present(textAlertController(context: strongSelf.context, title: presentationData.strings.Call_CallInProgressTitle, text: presentationData.strings.Call_CallInProgressMessage(current.compactDisplayTitle, peer.compactDisplayTitle).0, actions: [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_Cancel, action: {}), TextAlertAction(type: .genericAction, title: presentationData.strings.Common_OK, action: {
|
||||
if let strongSelf = self {
|
||||
let _ = strongSelf.context.sharedContext.callManager?.requestCall(account: strongSelf.context.account, peerId: peerId, endCurrentIfAny: true)
|
||||
let _ = strongSelf.context.sharedContext.callManager?.requestCall(account: strongSelf.context.account, peerId: peerId, isVideo: isVideo, endCurrentIfAny: true)
|
||||
began?()
|
||||
}
|
||||
})]), in: .window(.root))
|
||||
|
|
|
|||
|
|
@ -59,12 +59,12 @@ private extension CallListViewEntry {
|
|||
|
||||
final class CallListNodeInteraction {
|
||||
let setMessageIdWithRevealedOptions: (MessageId?, MessageId?) -> Void
|
||||
let call: (PeerId) -> Void
|
||||
let call: (PeerId, Bool) -> Void
|
||||
let openInfo: (PeerId, [Message]) -> Void
|
||||
let delete: ([MessageId]) -> Void
|
||||
let updateShowCallsTab: (Bool) -> Void
|
||||
|
||||
init(setMessageIdWithRevealedOptions: @escaping (MessageId?, MessageId?) -> Void, call: @escaping (PeerId) -> Void, openInfo: @escaping (PeerId, [Message]) -> Void, delete: @escaping ([MessageId]) -> Void, updateShowCallsTab: @escaping (Bool) -> Void) {
|
||||
init(setMessageIdWithRevealedOptions: @escaping (MessageId?, MessageId?) -> Void, call: @escaping (PeerId, Bool) -> Void, openInfo: @escaping (PeerId, [Message]) -> Void, delete: @escaping ([MessageId]) -> Void, updateShowCallsTab: @escaping (Bool) -> Void) {
|
||||
self.setMessageIdWithRevealedOptions = setMessageIdWithRevealedOptions
|
||||
self.call = call
|
||||
self.openInfo = openInfo
|
||||
|
|
@ -190,14 +190,14 @@ final class CallListControllerNode: ASDisplayNode {
|
|||
private let rightOverlayNode: ASDisplayNode
|
||||
private let emptyTextNode: ASTextNode
|
||||
|
||||
private let call: (PeerId) -> Void
|
||||
private let call: (PeerId, Bool) -> Void
|
||||
private let openInfo: (PeerId, [Message]) -> Void
|
||||
private let emptyStateUpdated: (Bool) -> Void
|
||||
|
||||
private let emptyStatePromise = Promise<Bool>()
|
||||
private let emptyStateDisposable = MetaDisposable()
|
||||
|
||||
init(context: AccountContext, mode: CallListControllerMode, presentationData: PresentationData, call: @escaping (PeerId) -> Void, openInfo: @escaping (PeerId, [Message]) -> Void, emptyStateUpdated: @escaping (Bool) -> Void) {
|
||||
init(context: AccountContext, mode: CallListControllerMode, presentationData: PresentationData, call: @escaping (PeerId, Bool) -> Void, openInfo: @escaping (PeerId, [Message]) -> Void, emptyStateUpdated: @escaping (Bool) -> Void) {
|
||||
self.context = context
|
||||
self.mode = mode
|
||||
self.presentationData = presentationData
|
||||
|
|
@ -248,8 +248,8 @@ final class CallListControllerNode: ASDisplayNode {
|
|||
}
|
||||
}
|
||||
}
|
||||
}, call: { [weak self] peerId in
|
||||
self?.call(peerId)
|
||||
}, call: { [weak self] peerId, isVideo in
|
||||
self?.call(peerId, isVideo)
|
||||
}, openInfo: { [weak self] peerId, messages in
|
||||
self?.openInfo(peerId, messages)
|
||||
}, delete: { [weak self] messageIds in
|
||||
|
|
|
|||
|
|
@ -189,7 +189,7 @@ public func chatListItemStrings(strings: PresentationStrings, nameDisplayOrder:
|
|||
messageText = invoice.title
|
||||
case let action as TelegramMediaAction:
|
||||
switch action.action {
|
||||
case let .phoneCall(_, discardReason, _):
|
||||
case let .phoneCall(_, discardReason, _, isVideo):
|
||||
hideAuthor = !isPeerGroup
|
||||
let incoming = message.flags.contains(.Incoming)
|
||||
if let discardReason = discardReason {
|
||||
|
|
@ -205,9 +205,17 @@ public func chatListItemStrings(strings: PresentationStrings, nameDisplayOrder:
|
|||
|
||||
if messageText.isEmpty {
|
||||
if incoming {
|
||||
messageText = strings.Notification_CallIncoming
|
||||
if isVideo {
|
||||
messageText = strings.Notification_VideoCallIncoming
|
||||
} else {
|
||||
messageText = strings.Notification_CallIncoming
|
||||
}
|
||||
} else {
|
||||
messageText = strings.Notification_CallOutgoing
|
||||
if isVideo {
|
||||
messageText = strings.Notification_VideoCallOutgoing
|
||||
} else {
|
||||
messageText = strings.Notification_CallOutgoing
|
||||
}
|
||||
}
|
||||
}
|
||||
default:
|
||||
|
|
|
|||
|
|
@ -111,11 +111,17 @@ func contactContextMenuItems(context: AccountContext, peerId: PeerId, contactsCo
|
|||
if let user = peer as? TelegramUser, let cachedUserData = transaction.getPeerCachedData(peerId: peerId) as? CachedUserData, user.flags.contains(.isSupport) || cachedUserData.callsPrivate {
|
||||
canCall = false
|
||||
}
|
||||
var canVideoCall = false
|
||||
if canCall {
|
||||
if context.sharedContext.immediateExperimentalUISettings.videoCalls {
|
||||
canVideoCall = true
|
||||
}
|
||||
}
|
||||
|
||||
if canCall {
|
||||
items.append(.action(ContextMenuActionItem(text: strings.ContactList_Context_Call, icon: { theme in generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Call"), color: theme.contextMenu.primaryColor) }, action: { _, f in
|
||||
if let contactsController = contactsController {
|
||||
let callResult = context.sharedContext.callManager?.requestCall(account: context.account, peerId: peerId, endCurrentIfAny: false)
|
||||
let callResult = context.sharedContext.callManager?.requestCall(account: context.account, peerId: peerId, isVideo: false, endCurrentIfAny: false)
|
||||
if let callResult = callResult, case let .alreadyInProgress(currentPeerId) = callResult {
|
||||
if currentPeerId == peerId {
|
||||
context.sharedContext.navigateToCurrentCall()
|
||||
|
|
@ -127,7 +133,33 @@ func contactContextMenuItems(context: AccountContext, peerId: PeerId, contactsCo
|
|||
|> deliverOnMainQueue).start(next: { [weak contactsController] peer, current in
|
||||
if let contactsController = contactsController, let peer = peer, let current = current {
|
||||
contactsController.present(textAlertController(context: context, title: presentationData.strings.Call_CallInProgressTitle, text: presentationData.strings.Call_CallInProgressMessage(current.compactDisplayTitle, peer.compactDisplayTitle).0, actions: [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_Cancel, action: {}), TextAlertAction(type: .genericAction, title: presentationData.strings.Common_OK, action: {
|
||||
let _ = context.sharedContext.callManager?.requestCall(account: context.account, peerId: peerId, endCurrentIfAny: true)
|
||||
let _ = context.sharedContext.callManager?.requestCall(account: context.account, peerId: peerId, isVideo: false, endCurrentIfAny: true)
|
||||
})]), in: .window(.root))
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
f(.default)
|
||||
})))
|
||||
}
|
||||
if canVideoCall {
|
||||
//TODO:localize
|
||||
items.append(.action(ContextMenuActionItem(text: "Video Call", icon: { theme in generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Call"), color: theme.contextMenu.primaryColor) }, action: { _, f in
|
||||
if let contactsController = contactsController {
|
||||
let callResult = context.sharedContext.callManager?.requestCall(account: context.account, peerId: peerId, isVideo: true, endCurrentIfAny: false)
|
||||
if let callResult = callResult, case let .alreadyInProgress(currentPeerId) = callResult {
|
||||
if currentPeerId == peerId {
|
||||
context.sharedContext.navigateToCurrentCall()
|
||||
} else {
|
||||
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
|
||||
let _ = (context.account.postbox.transaction { transaction -> (Peer?, Peer?) in
|
||||
return (transaction.getPeer(peerId), transaction.getPeer(currentPeerId))
|
||||
}
|
||||
|> deliverOnMainQueue).start(next: { [weak contactsController] peer, current in
|
||||
if let contactsController = contactsController, let peer = peer, let current = current {
|
||||
contactsController.present(textAlertController(context: context, title: presentationData.strings.Call_CallInProgressTitle, text: presentationData.strings.Call_CallInProgressMessage(current.compactDisplayTitle, peer.compactDisplayTitle).0, actions: [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_Cancel, action: {}), TextAlertAction(type: .genericAction, title: presentationData.strings.Common_OK, action: {
|
||||
let _ = context.sharedContext.callManager?.requestCall(account: context.account, peerId: peerId, isVideo: true, endCurrentIfAny: true)
|
||||
})]), in: .window(.root))
|
||||
}
|
||||
})
|
||||
|
|
|
|||
|
|
@ -544,6 +544,31 @@ public extension ContainedViewLayoutTransition {
|
|||
}
|
||||
}
|
||||
|
||||
func updateCornerRadius(layer: CALayer, cornerRadius: CGFloat, completion: ((Bool) -> Void)? = nil) {
|
||||
if layer.cornerRadius.isEqual(to: cornerRadius) {
|
||||
if let completion = completion {
|
||||
completion(true)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
switch self {
|
||||
case .immediate:
|
||||
layer.cornerRadius = cornerRadius
|
||||
if let completion = completion {
|
||||
completion(true)
|
||||
}
|
||||
case let .animated(duration, curve):
|
||||
let previousCornerRadius = layer.cornerRadius
|
||||
layer.cornerRadius = cornerRadius
|
||||
layer.animate(from: NSNumber(value: Float(previousCornerRadius)), to: NSNumber(value: Float(cornerRadius)), keyPath: "cornerRadius", timingFunction: curve.timingFunction, duration: duration, mediaTimingFunction: curve.mediaTimingFunction, completion: { result in
|
||||
if let completion = completion {
|
||||
completion(result)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func animateTransformScale(node: ASDisplayNode, from fromScale: CGFloat, completion: ((Bool) -> Void)? = nil) {
|
||||
let t = node.layer.transform
|
||||
let currentScale = sqrt((t.m11 * t.m11) + (t.m12 * t.m12) + (t.m13 * t.m13))
|
||||
|
|
|
|||
|
|
@ -102,6 +102,19 @@ private final class NavigationControllerNode: ASDisplayNode {
|
|||
}
|
||||
}
|
||||
|
||||
public protocol NavigationControllerDropContentItem: class {
|
||||
}
|
||||
|
||||
public final class NavigationControllerDropContent {
|
||||
public let position: CGPoint
|
||||
public let item: NavigationControllerDropContentItem
|
||||
|
||||
public init(position: CGPoint, item: NavigationControllerDropContentItem) {
|
||||
self.position = position
|
||||
self.item = item
|
||||
}
|
||||
}
|
||||
|
||||
open class NavigationController: UINavigationController, ContainableController, UIGestureRecognizerDelegate {
|
||||
public var isOpaqueWhenInOverlay: Bool = true
|
||||
public var blocksBackgroundWhenInOverlay: Bool = true
|
||||
|
|
@ -1221,6 +1234,35 @@ open class NavigationController: UINavigationController, ContainableController,
|
|||
}
|
||||
}
|
||||
|
||||
public func updatePossibleControllerDropContent(content: NavigationControllerDropContent?) {
|
||||
if let rootContainer = self.rootContainer {
|
||||
switch rootContainer {
|
||||
case let .flat(container):
|
||||
if let controller = container.controllers.last {
|
||||
controller.updatePossibleControllerDropContent(content: content)
|
||||
}
|
||||
case .split:
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public func acceptPossibleControllerDropContent(content: NavigationControllerDropContent) -> Bool {
|
||||
if let rootContainer = self.rootContainer {
|
||||
switch rootContainer {
|
||||
case let .flat(container):
|
||||
if let controller = container.controllers.last {
|
||||
if controller.acceptPossibleControllerDropContent(content: content) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
case .split:
|
||||
break
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
override open func present(_ viewControllerToPresent: UIViewController, animated flag: Bool, completion: (() -> Void)? = nil) {
|
||||
preconditionFailure()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -45,6 +45,7 @@ public final class NavigationBarBadgeNode: ASDisplayNode {
|
|||
self.textColor = textColor
|
||||
self.backgroundNode.image = generateStretchableFilledCircleImage(diameter: 18.0, color: fillColor, strokeColor: strokeColor, strokeWidth: 1.0)
|
||||
self.textNode.attributedText = NSAttributedString(string: self.text, font: self.font, textColor: self.textColor)
|
||||
self.textNode.redrawIfPossible()
|
||||
}
|
||||
|
||||
override public func calculateSizeThatFits(_ constrainedSize: CGSize) -> CGSize {
|
||||
|
|
|
|||
|
|
@ -644,6 +644,13 @@ public enum TabBarItemContextActionType {
|
|||
|
||||
open func tabBarItemSwipeAction(direction: TabBarItemSwipeDirection) {
|
||||
}
|
||||
|
||||
open func updatePossibleControllerDropContent(content: NavigationControllerDropContent?) {
|
||||
}
|
||||
|
||||
open func acceptPossibleControllerDropContent(content: NavigationControllerDropContent) -> Bool {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
func traceIsOpaque(layer: CALayer, rect: CGRect) -> Bool {
|
||||
|
|
|
|||
|
|
@ -1287,7 +1287,7 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode {
|
|||
mediaManager?.setOverlayVideoNode(nil)
|
||||
})
|
||||
expandImpl = { [weak overlayNode] in
|
||||
guard let contentInfo = item.contentInfo else {
|
||||
guard let contentInfo = item.contentInfo, let overlayNode = overlayNode else {
|
||||
return
|
||||
}
|
||||
|
||||
|
|
@ -1302,7 +1302,7 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode {
|
|||
|
||||
baseNavigationController?.view.endEditing(true)
|
||||
|
||||
(baseNavigationController?.topViewController as? ViewController)?.present(gallery, in: .window(.root), with: GalleryControllerPresentationArguments(transitionArguments: { id, media in
|
||||
(baseNavigationController?.topViewController as? ViewController)?.present(gallery, in: .window(.root), with: GalleryControllerPresentationArguments(transitionArguments: { [weak overlayNode] id, media in
|
||||
if let overlayNode = overlayNode, let overlaySupernode = overlayNode.supernode {
|
||||
return GalleryTransitionArguments(transitionNode: (overlayNode, overlayNode.bounds, { [weak overlayNode] in
|
||||
return (overlayNode?.view.snapshotContentTree(), nil)
|
||||
|
|
|
|||
|
|
@ -877,7 +877,7 @@ public func deviceContactInfoController(context: AccountContext, subject: Device
|
|||
ActionSheetItemGroup(items: [
|
||||
ActionSheetButtonItem(title: presentationData.strings.UserInfo_TelegramCall, action: {
|
||||
dismissAction()
|
||||
let callResult = context.sharedContext.callManager?.requestCall(account: context.account, peerId: user.id, endCurrentIfAny: false)
|
||||
let callResult = context.sharedContext.callManager?.requestCall(account: context.account, peerId: user.id, isVideo: false, endCurrentIfAny: false)
|
||||
if let callResult = callResult, case let .alreadyInProgress(currentPeerId) = callResult {
|
||||
if currentPeerId == user.id {
|
||||
context.sharedContext.navigateToCurrentCall()
|
||||
|
|
@ -888,7 +888,7 @@ public func deviceContactInfoController(context: AccountContext, subject: Device
|
|||
} |> deliverOnMainQueue).start(next: { peer, current in
|
||||
if let peer = peer, let current = current {
|
||||
presentControllerImpl?(textAlertController(context: context, title: presentationData.strings.Call_CallInProgressTitle, text: presentationData.strings.Call_CallInProgressMessage(current.compactDisplayTitle, peer.compactDisplayTitle).0, actions: [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_Cancel, action: {}), TextAlertAction(type: .genericAction, title: presentationData.strings.Common_OK, action: {
|
||||
let _ = context.sharedContext.callManager?.requestCall(account: context.account, peerId: peer.id, endCurrentIfAny: true)
|
||||
let _ = context.sharedContext.callManager?.requestCall(account: context.account, peerId: peer.id, isVideo: false, endCurrentIfAny: true)
|
||||
})]), nil)
|
||||
}
|
||||
})
|
||||
|
|
|
|||
|
|
@ -68,7 +68,7 @@ private func stringForCallType(message: Message, strings: PresentationStrings) -
|
|||
switch media {
|
||||
case let action as TelegramMediaAction:
|
||||
switch action.action {
|
||||
case let .phoneCall(_, discardReason, _):
|
||||
case let .phoneCall(_, discardReason, _, _):
|
||||
let incoming = message.flags.contains(.Incoming)
|
||||
if let discardReason = discardReason {
|
||||
switch discardReason {
|
||||
|
|
|
|||
|
|
@ -859,7 +859,7 @@ public func userInfoController(context: AccountContext, peerId: PeerId, mode: Pe
|
|||
return .single((view, view.cachedData))
|
||||
}))
|
||||
|
||||
let requestCallImpl: () -> Void = {
|
||||
let requestCallImpl: (Bool) -> Void = { isVideo in
|
||||
let _ = (peerView.get()
|
||||
|> take(1)
|
||||
|> deliverOnMainQueue).start(next: { view in
|
||||
|
|
@ -873,7 +873,7 @@ public func userInfoController(context: AccountContext, peerId: PeerId, mode: Pe
|
|||
return
|
||||
}
|
||||
|
||||
let callResult = context.sharedContext.callManager?.requestCall(account: context.account, peerId: peer.id, endCurrentIfAny: false)
|
||||
let callResult = context.sharedContext.callManager?.requestCall(account: context.account, peerId: peer.id, isVideo: isVideo, endCurrentIfAny: false)
|
||||
if let callResult = callResult, case let .alreadyInProgress(currentPeerId) = callResult {
|
||||
if currentPeerId == peer.id {
|
||||
context.sharedContext.navigateToCurrentCall()
|
||||
|
|
@ -884,7 +884,7 @@ public func userInfoController(context: AccountContext, peerId: PeerId, mode: Pe
|
|||
} |> deliverOnMainQueue).start(next: { peer, current in
|
||||
if let peer = peer, let current = current {
|
||||
presentControllerImpl?(textAlertController(context: context, title: presentationData.strings.Call_CallInProgressTitle, text: presentationData.strings.Call_CallInProgressMessage(current.compactDisplayTitle, peer.compactDisplayTitle).0, actions: [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_Cancel, action: {}), TextAlertAction(type: .genericAction, title: presentationData.strings.Common_OK, action: {
|
||||
let _ = context.sharedContext.callManager?.requestCall(account: context.account, peerId: peer.id, endCurrentIfAny: true)
|
||||
let _ = context.sharedContext.callManager?.requestCall(account: context.account, peerId: peer.id, isVideo: isVideo, endCurrentIfAny: true)
|
||||
})]), nil)
|
||||
}
|
||||
})
|
||||
|
|
@ -1111,7 +1111,7 @@ public func userInfoController(context: AccountContext, peerId: PeerId, mode: Pe
|
|||
}, displayCopyContextMenu: { tag, phone in
|
||||
displayCopyContextMenuImpl?(tag, phone)
|
||||
}, call: {
|
||||
requestCallImpl()
|
||||
requestCallImpl(false)
|
||||
}, openCallMenu: { number in
|
||||
let _ = (getUserPeer(postbox: context.account.postbox, peerId: peerId)
|
||||
|> deliverOnMainQueue).start(next: { peer, _ in
|
||||
|
|
@ -1125,7 +1125,7 @@ public func userInfoController(context: AccountContext, peerId: PeerId, mode: Pe
|
|||
ActionSheetItemGroup(items: [
|
||||
ActionSheetButtonItem(title: presentationData.strings.UserInfo_TelegramCall, action: {
|
||||
dismissAction()
|
||||
requestCallImpl()
|
||||
requestCallImpl(false)
|
||||
}),
|
||||
ActionSheetButtonItem(title: presentationData.strings.UserInfo_PhoneCall, action: {
|
||||
dismissAction()
|
||||
|
|
|
|||
|
|
@ -42,6 +42,7 @@ private enum DebugControllerSection: Int32 {
|
|||
case logs
|
||||
case logging
|
||||
case experiments
|
||||
case videoExperiments
|
||||
case info
|
||||
}
|
||||
|
||||
|
|
@ -70,6 +71,7 @@ private enum DebugControllerEntry: ItemListNodeEntry {
|
|||
case knockoutWallpaper(PresentationTheme, Bool)
|
||||
case alternativeFolderTabs(Bool)
|
||||
case videoCalls(Bool)
|
||||
case videoCallsInfo(PresentationTheme, String)
|
||||
case hostInfo(PresentationTheme, String)
|
||||
case versionInfo(PresentationTheme)
|
||||
|
||||
|
|
@ -83,8 +85,10 @@ private enum DebugControllerEntry: ItemListNodeEntry {
|
|||
return DebugControllerSection.logging.rawValue
|
||||
case .enableRaiseToSpeak, .keepChatNavigationStack, .skipReadHistory, .crashOnSlowQueries:
|
||||
return DebugControllerSection.experiments.rawValue
|
||||
case .clearTips, .reimport, .resetData, .resetDatabase, .resetHoles, .reindexUnread, .resetBiometricsData, .optimizeDatabase, .photoPreview, .knockoutWallpaper, .alternativeFolderTabs, .videoCalls:
|
||||
case .clearTips, .reimport, .resetData, .resetDatabase, .resetHoles, .reindexUnread, .resetBiometricsData, .optimizeDatabase, .photoPreview, .knockoutWallpaper, .alternativeFolderTabs:
|
||||
return DebugControllerSection.experiments.rawValue
|
||||
case .videoCalls, .videoCallsInfo:
|
||||
return DebugControllerSection.videoExperiments.rawValue
|
||||
case .hostInfo, .versionInfo:
|
||||
return DebugControllerSection.info.rawValue
|
||||
}
|
||||
|
|
@ -140,10 +144,12 @@ private enum DebugControllerEntry: ItemListNodeEntry {
|
|||
return 23
|
||||
case .videoCalls:
|
||||
return 24
|
||||
case .hostInfo:
|
||||
case .videoCallsInfo:
|
||||
return 25
|
||||
case .versionInfo:
|
||||
case .hostInfo:
|
||||
return 26
|
||||
case .versionInfo:
|
||||
return 27
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -542,7 +548,7 @@ private enum DebugControllerEntry: ItemListNodeEntry {
|
|||
}).start()
|
||||
})
|
||||
case let .videoCalls(value):
|
||||
return ItemListSwitchItem(presentationData: presentationData, title: "Video", value: value, sectionId: self.section, style: .blocks, updated: { value in
|
||||
return ItemListSwitchItem(presentationData: presentationData, title: "Experimental Feature", value: value, sectionId: self.section, style: .blocks, updated: { value in
|
||||
let _ = arguments.sharedContext.accountManager.transaction ({ transaction in
|
||||
transaction.updateSharedData(ApplicationSpecificSharedDataKeys.experimentalUISettings, { settings in
|
||||
var settings = settings as? ExperimentalUISettings ?? ExperimentalUISettings.defaultSettings
|
||||
|
|
@ -551,6 +557,8 @@ private enum DebugControllerEntry: ItemListNodeEntry {
|
|||
})
|
||||
}).start()
|
||||
})
|
||||
case let .videoCallsInfo(_, text):
|
||||
return ItemListTextItem(presentationData: presentationData, text: .plain(text), sectionId: self.section)
|
||||
case let .hostInfo(theme, string):
|
||||
return ItemListTextItem(presentationData: presentationData, text: .plain(string), sectionId: self.section)
|
||||
case let .versionInfo(theme):
|
||||
|
|
@ -595,6 +603,7 @@ private func debugControllerEntries(presentationData: PresentationData, loggingS
|
|||
entries.append(.knockoutWallpaper(presentationData.theme, experimentalSettings.knockoutWallpaper))
|
||||
entries.append(.alternativeFolderTabs(experimentalSettings.foldersTabAtBottom))
|
||||
entries.append(.videoCalls(experimentalSettings.videoCalls))
|
||||
entries.append(.videoCallsInfo(presentationData.theme, "Enables experimental transmission of electromagnetic radiation synchronized with pressure waves. Needs to be enabled on both sides."))
|
||||
|
||||
if let backupHostOverride = networkSettings?.backupHostOverride {
|
||||
entries.append(.hostInfo(presentationData.theme, "Host: \(backupHostOverride)"))
|
||||
|
|
|
|||
|
|
@ -28,10 +28,11 @@ private final class PrivacyAndSecurityControllerArguments {
|
|||
let openPasscode: () -> Void
|
||||
let openTwoStepVerification: (TwoStepVerificationAccessConfiguration?) -> Void
|
||||
let openActiveSessions: () -> Void
|
||||
let toggleArchiveAndMuteNonContacts: (Bool) -> Void
|
||||
let setupAccountAutoremove: () -> Void
|
||||
let openDataSettings: () -> Void
|
||||
|
||||
init(account: Account, openBlockedUsers: @escaping () -> Void, openLastSeenPrivacy: @escaping () -> Void, openGroupsPrivacy: @escaping () -> Void, openVoiceCallPrivacy: @escaping () -> Void, openProfilePhotoPrivacy: @escaping () -> Void, openForwardPrivacy: @escaping () -> Void, openPhoneNumberPrivacy: @escaping () -> Void, openPasscode: @escaping () -> Void, openTwoStepVerification: @escaping (TwoStepVerificationAccessConfiguration?) -> Void, openActiveSessions: @escaping () -> Void, setupAccountAutoremove: @escaping () -> Void, openDataSettings: @escaping () -> Void) {
|
||||
init(account: Account, openBlockedUsers: @escaping () -> Void, openLastSeenPrivacy: @escaping () -> Void, openGroupsPrivacy: @escaping () -> Void, openVoiceCallPrivacy: @escaping () -> Void, openProfilePhotoPrivacy: @escaping () -> Void, openForwardPrivacy: @escaping () -> Void, openPhoneNumberPrivacy: @escaping () -> Void, openPasscode: @escaping () -> Void, openTwoStepVerification: @escaping (TwoStepVerificationAccessConfiguration?) -> Void, openActiveSessions: @escaping () -> Void, toggleArchiveAndMuteNonContacts: @escaping (Bool) -> Void, setupAccountAutoremove: @escaping () -> Void, openDataSettings: @escaping () -> Void) {
|
||||
self.account = account
|
||||
self.openBlockedUsers = openBlockedUsers
|
||||
self.openLastSeenPrivacy = openLastSeenPrivacy
|
||||
|
|
@ -43,6 +44,7 @@ private final class PrivacyAndSecurityControllerArguments {
|
|||
self.openPasscode = openPasscode
|
||||
self.openTwoStepVerification = openTwoStepVerification
|
||||
self.openActiveSessions = openActiveSessions
|
||||
self.toggleArchiveAndMuteNonContacts = toggleArchiveAndMuteNonContacts
|
||||
self.setupAccountAutoremove = setupAccountAutoremove
|
||||
self.openDataSettings = openDataSettings
|
||||
}
|
||||
|
|
@ -51,6 +53,7 @@ private final class PrivacyAndSecurityControllerArguments {
|
|||
private enum PrivacyAndSecuritySection: Int32 {
|
||||
case general
|
||||
case privacy
|
||||
case autoArchive
|
||||
case account
|
||||
case dataSettings
|
||||
}
|
||||
|
|
@ -80,6 +83,9 @@ private enum PrivacyAndSecurityEntry: ItemListNodeEntry {
|
|||
case passcode(PresentationTheme, String, Bool, String)
|
||||
case twoStepVerification(PresentationTheme, String, String, TwoStepVerificationAccessConfiguration?)
|
||||
case activeSessions(PresentationTheme, String, String)
|
||||
case autoArchiveHeader(String)
|
||||
case autoArchive(String, Bool)
|
||||
case autoArchiveInfo(String)
|
||||
case accountHeader(PresentationTheme, String)
|
||||
case accountTimeout(PresentationTheme, String, String)
|
||||
case accountInfo(PresentationTheme, String)
|
||||
|
|
@ -92,6 +98,8 @@ private enum PrivacyAndSecurityEntry: ItemListNodeEntry {
|
|||
return PrivacyAndSecuritySection.general.rawValue
|
||||
case .privacyHeader, .phoneNumberPrivacy, .lastSeenPrivacy, .profilePhotoPrivacy, .forwardPrivacy, .groupPrivacy, .selectivePrivacyInfo, .voiceCallPrivacy:
|
||||
return PrivacyAndSecuritySection.privacy.rawValue
|
||||
case .autoArchiveHeader, .autoArchive, .autoArchiveInfo:
|
||||
return PrivacyAndSecuritySection.autoArchive.rawValue
|
||||
case .accountHeader, .accountTimeout, .accountInfo:
|
||||
return PrivacyAndSecuritySection.account.rawValue
|
||||
case .dataSettings, .dataSettingsInfo:
|
||||
|
|
@ -125,16 +133,22 @@ private enum PrivacyAndSecurityEntry: ItemListNodeEntry {
|
|||
return 11
|
||||
case .selectivePrivacyInfo:
|
||||
return 12
|
||||
case .accountHeader:
|
||||
case .autoArchiveHeader:
|
||||
return 13
|
||||
case .accountTimeout:
|
||||
case .autoArchive:
|
||||
return 14
|
||||
case .accountInfo:
|
||||
case .autoArchiveInfo:
|
||||
return 15
|
||||
case .dataSettings:
|
||||
case .accountHeader:
|
||||
return 16
|
||||
case .dataSettingsInfo:
|
||||
case .accountTimeout:
|
||||
return 17
|
||||
case .accountInfo:
|
||||
return 18
|
||||
case .dataSettings:
|
||||
return 19
|
||||
case .dataSettingsInfo:
|
||||
return 20
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -212,6 +226,24 @@ private enum PrivacyAndSecurityEntry: ItemListNodeEntry {
|
|||
} else {
|
||||
return false
|
||||
}
|
||||
case let .autoArchiveHeader(text):
|
||||
if case .autoArchiveHeader(text) = rhs {
|
||||
return true
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
case let .autoArchive(text, value):
|
||||
if case .autoArchive(text, value) = rhs {
|
||||
return true
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
case let .autoArchiveInfo(text):
|
||||
if case .autoArchiveInfo(text) = rhs {
|
||||
return true
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
case let .accountHeader(lhsTheme, lhsText):
|
||||
if case let .accountHeader(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText {
|
||||
return true
|
||||
|
|
@ -296,6 +328,14 @@ private enum PrivacyAndSecurityEntry: ItemListNodeEntry {
|
|||
return ItemListDisclosureItem(presentationData: presentationData, icon: UIImage(bundleImageName: "Settings/MenuIcons/Websites")?.precomposed(), title: text, label: value, sectionId: self.section, style: .blocks, action: {
|
||||
arguments.openActiveSessions()
|
||||
})
|
||||
case let .autoArchiveHeader(text):
|
||||
return ItemListSectionHeaderItem(presentationData: presentationData, text: text, sectionId: self.section)
|
||||
case let .autoArchive(text, value):
|
||||
return ItemListSwitchItem(presentationData: presentationData, title: text, value: value, sectionId: self.section, style: .blocks, updated: { value in
|
||||
arguments.toggleArchiveAndMuteNonContacts(value)
|
||||
})
|
||||
case let .autoArchiveInfo(text):
|
||||
return ItemListTextItem(presentationData: presentationData, text: .plain(text), sectionId: self.section)
|
||||
case let .accountHeader(theme, text):
|
||||
return ItemListSectionHeaderItem(presentationData: presentationData, text: text, sectionId: self.section)
|
||||
case let .accountTimeout(theme, text, value):
|
||||
|
|
@ -316,6 +356,7 @@ private enum PrivacyAndSecurityEntry: ItemListNodeEntry {
|
|||
|
||||
private struct PrivacyAndSecurityControllerState: Equatable {
|
||||
var updatingAccountTimeoutValue: Int32? = nil
|
||||
var updatingAutomaticallyArchiveAndMuteNonContacts: Bool? = nil
|
||||
}
|
||||
|
||||
private func countForSelectivePeers(_ peers: [PeerId: SelectivePrivacyPeer]) -> Int {
|
||||
|
|
@ -404,6 +445,21 @@ private func privacyAndSecurityControllerEntries(presentationData: PresentationD
|
|||
entries.append(.selectivePrivacyInfo(presentationData.theme, presentationData.strings.PrivacyLastSeenSettings_GroupsAndChannelsHelp))
|
||||
}
|
||||
|
||||
entries.append(.autoArchiveHeader(presentationData.strings.PrivacySettings_AutoArchiveTitle.uppercased()))
|
||||
if let privacySettings = privacySettings {
|
||||
let automaticallyArchiveAndMuteNonContactsValue: Bool
|
||||
if let automaticallyArchiveAndMuteNonContacts = state.updatingAutomaticallyArchiveAndMuteNonContacts {
|
||||
automaticallyArchiveAndMuteNonContactsValue = automaticallyArchiveAndMuteNonContacts
|
||||
} else {
|
||||
automaticallyArchiveAndMuteNonContactsValue = privacySettings.automaticallyArchiveAndMuteNonContacts
|
||||
}
|
||||
|
||||
entries.append(.autoArchive(presentationData.strings.PrivacySettings_AutoArchive, automaticallyArchiveAndMuteNonContactsValue))
|
||||
} else {
|
||||
entries.append(.autoArchive(presentationData.strings.PrivacySettings_AutoArchive, false))
|
||||
}
|
||||
entries.append(.autoArchiveInfo(presentationData.strings.PrivacySettings_AutoArchiveInfo))
|
||||
|
||||
entries.append(.accountHeader(presentationData.theme, presentationData.strings.PrivacySettings_DeleteAccountTitle.uppercased()))
|
||||
if let privacySettings = privacySettings {
|
||||
let value: Int32
|
||||
|
|
@ -443,6 +499,9 @@ public func privacyAndSecurityController(context: AccountContext, initialSetting
|
|||
let updateAccountTimeoutDisposable = MetaDisposable()
|
||||
actionsDisposable.add(updateAccountTimeoutDisposable)
|
||||
|
||||
let updateAutoArchiveDisposable = MetaDisposable()
|
||||
actionsDisposable.add(updateAutoArchiveDisposable)
|
||||
|
||||
let privacySettingsPromise = Promise<AccountPrivacySettings?>()
|
||||
privacySettingsPromise.set(.single(initialSettings) |> then(requestAccountPrivacySettings(account: context.account) |> map(Optional.init)))
|
||||
|
||||
|
|
@ -516,7 +575,7 @@ public func privacyAndSecurityController(context: AccountContext, initialSetting
|
|||
|> deliverOnMainQueue
|
||||
|> mapToSignal { value -> Signal<Void, NoError> in
|
||||
if let value = value {
|
||||
privacySettingsPromise.set(.single(AccountPrivacySettings(presence: updated, groupInvitations: value.groupInvitations, voiceCalls: value.voiceCalls, voiceCallsP2P: value.voiceCallsP2P, profilePhoto: value.profilePhoto, forwards: value.forwards, phoneNumber: value.phoneNumber, phoneDiscoveryEnabled: value.phoneDiscoveryEnabled, accountRemovalTimeout: value.accountRemovalTimeout)))
|
||||
privacySettingsPromise.set(.single(AccountPrivacySettings(presence: updated, groupInvitations: value.groupInvitations, voiceCalls: value.voiceCalls, voiceCallsP2P: value.voiceCallsP2P, profilePhoto: value.profilePhoto, forwards: value.forwards, phoneNumber: value.phoneNumber, phoneDiscoveryEnabled: value.phoneDiscoveryEnabled, automaticallyArchiveAndMuteNonContacts: value.automaticallyArchiveAndMuteNonContacts, accountRemovalTimeout: value.accountRemovalTimeout)))
|
||||
}
|
||||
return .complete()
|
||||
}
|
||||
|
|
@ -539,7 +598,7 @@ public func privacyAndSecurityController(context: AccountContext, initialSetting
|
|||
|> deliverOnMainQueue
|
||||
|> mapToSignal { value -> Signal<Void, NoError> in
|
||||
if let value = value {
|
||||
privacySettingsPromise.set(.single(AccountPrivacySettings(presence: value.presence, groupInvitations: updated, voiceCalls: value.voiceCalls, voiceCallsP2P: value.voiceCallsP2P, profilePhoto: value.profilePhoto, forwards: value.forwards, phoneNumber: value.phoneNumber, phoneDiscoveryEnabled: value.phoneDiscoveryEnabled, accountRemovalTimeout: value.accountRemovalTimeout)))
|
||||
privacySettingsPromise.set(.single(AccountPrivacySettings(presence: value.presence, groupInvitations: updated, voiceCalls: value.voiceCalls, voiceCallsP2P: value.voiceCallsP2P, profilePhoto: value.profilePhoto, forwards: value.forwards, phoneNumber: value.phoneNumber, phoneDiscoveryEnabled: value.phoneDiscoveryEnabled, automaticallyArchiveAndMuteNonContacts: value.automaticallyArchiveAndMuteNonContacts, accountRemovalTimeout: value.accountRemovalTimeout)))
|
||||
}
|
||||
return .complete()
|
||||
}
|
||||
|
|
@ -576,7 +635,7 @@ public func privacyAndSecurityController(context: AccountContext, initialSetting
|
|||
|> deliverOnMainQueue
|
||||
|> mapToSignal { value -> Signal<Void, NoError> in
|
||||
if let value = value {
|
||||
privacySettingsPromise.set(.single(AccountPrivacySettings(presence: value.presence, groupInvitations: value.groupInvitations, voiceCalls: updated, voiceCallsP2P: updatedCallsPrivacy, profilePhoto: value.profilePhoto, forwards: value.forwards, phoneNumber: value.phoneNumber, phoneDiscoveryEnabled: value.phoneDiscoveryEnabled, accountRemovalTimeout: value.accountRemovalTimeout)))
|
||||
privacySettingsPromise.set(.single(AccountPrivacySettings(presence: value.presence, groupInvitations: value.groupInvitations, voiceCalls: updated, voiceCallsP2P: updatedCallsPrivacy, profilePhoto: value.profilePhoto, forwards: value.forwards, phoneNumber: value.phoneNumber, phoneDiscoveryEnabled: value.phoneDiscoveryEnabled, automaticallyArchiveAndMuteNonContacts: value.automaticallyArchiveAndMuteNonContacts, accountRemovalTimeout: value.accountRemovalTimeout)))
|
||||
}
|
||||
return .complete()
|
||||
}
|
||||
|
|
@ -599,7 +658,7 @@ public func privacyAndSecurityController(context: AccountContext, initialSetting
|
|||
|> deliverOnMainQueue
|
||||
|> mapToSignal { value -> Signal<Void, NoError> in
|
||||
if let value = value {
|
||||
privacySettingsPromise.set(.single(AccountPrivacySettings(presence: value.presence, groupInvitations: value.groupInvitations, voiceCalls: value.voiceCalls, voiceCallsP2P: value.voiceCallsP2P, profilePhoto: updated, forwards: value.forwards, phoneNumber: value.phoneNumber, phoneDiscoveryEnabled: value.phoneDiscoveryEnabled, accountRemovalTimeout: value.accountRemovalTimeout)))
|
||||
privacySettingsPromise.set(.single(AccountPrivacySettings(presence: value.presence, groupInvitations: value.groupInvitations, voiceCalls: value.voiceCalls, voiceCallsP2P: value.voiceCallsP2P, profilePhoto: updated, forwards: value.forwards, phoneNumber: value.phoneNumber, phoneDiscoveryEnabled: value.phoneDiscoveryEnabled, automaticallyArchiveAndMuteNonContacts: value.automaticallyArchiveAndMuteNonContacts, accountRemovalTimeout: value.accountRemovalTimeout)))
|
||||
}
|
||||
return .complete()
|
||||
}
|
||||
|
|
@ -622,7 +681,7 @@ public func privacyAndSecurityController(context: AccountContext, initialSetting
|
|||
|> deliverOnMainQueue
|
||||
|> mapToSignal { value -> Signal<Void, NoError> in
|
||||
if let value = value {
|
||||
privacySettingsPromise.set(.single(AccountPrivacySettings(presence: value.presence, groupInvitations: value.groupInvitations, voiceCalls: value.voiceCalls, voiceCallsP2P: value.voiceCallsP2P, profilePhoto: value.profilePhoto, forwards: updated, phoneNumber: value.phoneNumber, phoneDiscoveryEnabled: value.phoneDiscoveryEnabled, accountRemovalTimeout: value.accountRemovalTimeout)))
|
||||
privacySettingsPromise.set(.single(AccountPrivacySettings(presence: value.presence, groupInvitations: value.groupInvitations, voiceCalls: value.voiceCalls, voiceCallsP2P: value.voiceCallsP2P, profilePhoto: value.profilePhoto, forwards: updated, phoneNumber: value.phoneNumber, phoneDiscoveryEnabled: value.phoneDiscoveryEnabled, automaticallyArchiveAndMuteNonContacts: value.automaticallyArchiveAndMuteNonContacts, accountRemovalTimeout: value.accountRemovalTimeout)))
|
||||
}
|
||||
return .complete()
|
||||
}
|
||||
|
|
@ -645,7 +704,7 @@ public func privacyAndSecurityController(context: AccountContext, initialSetting
|
|||
|> deliverOnMainQueue
|
||||
|> mapToSignal { value -> Signal<Void, NoError> in
|
||||
if let value = value {
|
||||
privacySettingsPromise.set(.single(AccountPrivacySettings(presence: value.presence, groupInvitations: value.groupInvitations, voiceCalls: value.voiceCalls, voiceCallsP2P: value.voiceCallsP2P, profilePhoto: value.profilePhoto, forwards: value.forwards, phoneNumber: updated, phoneDiscoveryEnabled: updatedDiscoveryEnabled ?? value.phoneDiscoveryEnabled, accountRemovalTimeout: value.accountRemovalTimeout)))
|
||||
privacySettingsPromise.set(.single(AccountPrivacySettings(presence: value.presence, groupInvitations: value.groupInvitations, voiceCalls: value.voiceCalls, voiceCallsP2P: value.voiceCallsP2P, profilePhoto: value.profilePhoto, forwards: value.forwards, phoneNumber: updated, phoneDiscoveryEnabled: updatedDiscoveryEnabled ?? value.phoneDiscoveryEnabled, automaticallyArchiveAndMuteNonContacts: value.automaticallyArchiveAndMuteNonContacts, accountRemovalTimeout: value.accountRemovalTimeout)))
|
||||
}
|
||||
return .complete()
|
||||
}
|
||||
|
|
@ -700,6 +759,33 @@ public func privacyAndSecurityController(context: AccountContext, initialSetting
|
|||
}
|
||||
}, openActiveSessions: {
|
||||
pushControllerImpl?(recentSessionsController(context: context, activeSessionsContext: activeSessionsContext, webSessionsContext: webSessionsContext, websitesOnly: true), true)
|
||||
}, toggleArchiveAndMuteNonContacts: { archiveValue in
|
||||
updateState { state in
|
||||
var state = state
|
||||
state.updatingAutomaticallyArchiveAndMuteNonContacts = archiveValue
|
||||
return state
|
||||
}
|
||||
let applyTimeout: Signal<Void, NoError> = privacySettingsPromise.get()
|
||||
|> filter { $0 != nil }
|
||||
|> take(1)
|
||||
|> deliverOnMainQueue
|
||||
|> mapToSignal { value -> Signal<Void, NoError> in
|
||||
if let value = value {
|
||||
privacySettingsPromise.set(.single(AccountPrivacySettings(presence: value.presence, groupInvitations: value.groupInvitations, voiceCalls: value.voiceCalls, voiceCallsP2P: value.voiceCallsP2P, profilePhoto: value.profilePhoto, forwards: value.forwards, phoneNumber: value.phoneNumber, phoneDiscoveryEnabled: value.phoneDiscoveryEnabled, automaticallyArchiveAndMuteNonContacts: archiveValue, accountRemovalTimeout: value.accountRemovalTimeout)))
|
||||
}
|
||||
return .complete()
|
||||
}
|
||||
|
||||
updateAutoArchiveDisposable.set((updateAccountAutoArchiveChats(account: context.account, value: archiveValue)
|
||||
|> mapToSignal { _ -> Signal<Void, NoError> in }
|
||||
|> then(applyTimeout)
|
||||
|> deliverOnMainQueue).start(completed: {
|
||||
updateState { state in
|
||||
var state = state
|
||||
state.updatingAutomaticallyArchiveAndMuteNonContacts = nil
|
||||
return state
|
||||
}
|
||||
}))
|
||||
}, setupAccountAutoremove: {
|
||||
let signal = privacySettingsPromise.get()
|
||||
|> take(1)
|
||||
|
|
@ -724,7 +810,7 @@ public func privacyAndSecurityController(context: AccountContext, initialSetting
|
|||
|> deliverOnMainQueue
|
||||
|> mapToSignal { value -> Signal<Void, NoError> in
|
||||
if let value = value {
|
||||
privacySettingsPromise.set(.single(AccountPrivacySettings(presence: value.presence, groupInvitations: value.groupInvitations, voiceCalls: value.voiceCalls, voiceCallsP2P: value.voiceCallsP2P, profilePhoto: value.profilePhoto, forwards: value.forwards, phoneNumber: value.phoneNumber, phoneDiscoveryEnabled: value.phoneDiscoveryEnabled, accountRemovalTimeout: timeout)))
|
||||
privacySettingsPromise.set(.single(AccountPrivacySettings(presence: value.presence, groupInvitations: value.groupInvitations, voiceCalls: value.voiceCalls, voiceCallsP2P: value.voiceCallsP2P, profilePhoto: value.profilePhoto, forwards: value.forwards, phoneNumber: value.phoneNumber, phoneDiscoveryEnabled: value.phoneDiscoveryEnabled, automaticallyArchiveAndMuteNonContacts: value.automaticallyArchiveAndMuteNonContacts, accountRemovalTimeout: timeout)))
|
||||
}
|
||||
return .complete()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -508,13 +508,17 @@ public class LocalFileMediaResource: TelegramMediaResource {
|
|||
public let fileId: Int64
|
||||
public let size: Int?
|
||||
|
||||
public init(fileId: Int64, size: Int? = nil) {
|
||||
public let isSecretRelated: Bool
|
||||
|
||||
public init(fileId: Int64, size: Int? = nil, isSecretRelated: Bool = false) {
|
||||
self.fileId = fileId
|
||||
self.size = size
|
||||
self.isSecretRelated = isSecretRelated
|
||||
}
|
||||
|
||||
public required init(decoder: PostboxDecoder) {
|
||||
self.fileId = decoder.decodeInt64ForKey("f", orElse: 0)
|
||||
self.isSecretRelated = decoder.decodeBoolForKey("sr", orElse: false)
|
||||
if let size = decoder.decodeOptionalInt32ForKey("s") {
|
||||
self.size = Int(size)
|
||||
} else {
|
||||
|
|
@ -524,6 +528,7 @@ public class LocalFileMediaResource: TelegramMediaResource {
|
|||
|
||||
public func encode(_ encoder: PostboxEncoder) {
|
||||
encoder.encodeInt64(self.fileId, forKey: "f")
|
||||
encoder.encodeBool(self.isSecretRelated, forKey: "sr")
|
||||
if let size = self.size {
|
||||
encoder.encodeInt32(Int32(size), forKey: "s")
|
||||
} else {
|
||||
|
|
@ -537,7 +542,7 @@ public class LocalFileMediaResource: TelegramMediaResource {
|
|||
|
||||
public func isEqual(to: MediaResource) -> Bool {
|
||||
if let to = to as? LocalFileMediaResource {
|
||||
return self.fileId == to.fileId && self.size == to.size
|
||||
return self.fileId == to.fileId && self.size == to.size && self.isSecretRelated == to.isSecretRelated
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
|
|
|
|||
|
|
@ -14,6 +14,8 @@ public struct PeerStatusSettings: PostboxCoding, Equatable {
|
|||
public static let canAddContact = Flags(rawValue: 1 << 4)
|
||||
public static let addExceptionWhenAddingContact = Flags(rawValue: 1 << 5)
|
||||
public static let canReportIrrelevantGeoLocation = Flags(rawValue: 1 << 6)
|
||||
public static let autoArchived = Flags(rawValue: 1 << 7)
|
||||
|
||||
}
|
||||
|
||||
public var flags: PeerStatusSettings.Flags
|
||||
|
|
|
|||
|
|
@ -38,7 +38,7 @@ public enum TelegramMediaActionType: PostboxCoding, Equatable {
|
|||
case historyScreenshot
|
||||
case messageAutoremoveTimeoutUpdated(Int32)
|
||||
case gameScore(gameId: Int64, score: Int32)
|
||||
case phoneCall(callId: Int64, discardReason: PhoneCallDiscardReason?, duration: Int32?)
|
||||
case phoneCall(callId: Int64, discardReason: PhoneCallDiscardReason?, duration: Int32?, isVideo: Bool)
|
||||
case paymentSent(currency: String, totalAmount: Int64)
|
||||
case customText(text: String, entities: [MessageTextEntity])
|
||||
case botDomainAccessGranted(domain: String)
|
||||
|
|
@ -80,7 +80,7 @@ public enum TelegramMediaActionType: PostboxCoding, Equatable {
|
|||
if let value = decoder.decodeOptionalInt32ForKey("dr") {
|
||||
discardReason = PhoneCallDiscardReason(rawValue: value)
|
||||
}
|
||||
self = .phoneCall(callId: decoder.decodeInt64ForKey("i", orElse: 0), discardReason: discardReason, duration: decoder.decodeInt32ForKey("d", orElse: 0))
|
||||
self = .phoneCall(callId: decoder.decodeInt64ForKey("i", orElse: 0), discardReason: discardReason, duration: decoder.decodeInt32ForKey("d", orElse: 0), isVideo: decoder.decodeInt32ForKey("vc", orElse: 0) != 0)
|
||||
case 15:
|
||||
self = .paymentSent(currency: decoder.decodeStringForKey("currency", orElse: ""), totalAmount: decoder.decodeInt64ForKey("ta", orElse: 0))
|
||||
case 16:
|
||||
|
|
@ -152,7 +152,7 @@ public enum TelegramMediaActionType: PostboxCoding, Equatable {
|
|||
encoder.encodeInt32(15, forKey: "_rawValue")
|
||||
encoder.encodeString(currency, forKey: "currency")
|
||||
encoder.encodeInt64(totalAmount, forKey: "ta")
|
||||
case let .phoneCall(callId, discardReason, duration):
|
||||
case let .phoneCall(callId, discardReason, duration, isVideo):
|
||||
encoder.encodeInt32(14, forKey: "_rawValue")
|
||||
encoder.encodeInt64(callId, forKey: "i")
|
||||
if let discardReason = discardReason {
|
||||
|
|
@ -165,6 +165,7 @@ public enum TelegramMediaActionType: PostboxCoding, Equatable {
|
|||
} else {
|
||||
encoder.encodeNil(forKey: "d")
|
||||
}
|
||||
encoder.encodeInt32(isVideo ? 1 : 0, forKey: "vc")
|
||||
case let .customText(text, entities):
|
||||
encoder.encodeInt32(16, forKey: "_rawValue")
|
||||
encoder.encodeString(text, forKey: "text")
|
||||
|
|
|
|||
|
|
@ -292,6 +292,7 @@ fileprivate let parsers: [Int32 : (BufferReader) -> Any?] = {
|
|||
dict[-525288402] = { return Api.PhotoSize.parse_photoStrippedSize($0) }
|
||||
dict[-244016606] = { return Api.messages.Stickers.parse_stickersNotModified($0) }
|
||||
dict[-463889475] = { return Api.messages.Stickers.parse_stickers($0) }
|
||||
dict[-1096616924] = { return Api.GlobalPrivacySettings.parse_globalPrivacySettings($0) }
|
||||
dict[1008755359] = { return Api.InlineBotSwitchPM.parse_inlineBotSwitchPM($0) }
|
||||
dict[223655517] = { return Api.messages.FoundStickerSets.parse_foundStickerSetsNotModified($0) }
|
||||
dict[1359533640] = { return Api.messages.FoundStickerSets.parse_foundStickerSets($0) }
|
||||
|
|
@ -1024,6 +1025,8 @@ public struct Api {
|
|||
_1.serialize(buffer, boxed)
|
||||
case let _1 as Api.messages.Stickers:
|
||||
_1.serialize(buffer, boxed)
|
||||
case let _1 as Api.GlobalPrivacySettings:
|
||||
_1.serialize(buffer, boxed)
|
||||
case let _1 as Api.InlineBotSwitchPM:
|
||||
_1.serialize(buffer, boxed)
|
||||
case let _1 as Api.messages.FoundStickerSets:
|
||||
|
|
|
|||
|
|
@ -9210,6 +9210,46 @@ public extension Api {
|
|||
}
|
||||
}
|
||||
|
||||
}
|
||||
public enum GlobalPrivacySettings: TypeConstructorDescription {
|
||||
case globalPrivacySettings(flags: Int32, archiveAndMuteNewNoncontactPeers: Api.Bool?)
|
||||
|
||||
public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) {
|
||||
switch self {
|
||||
case .globalPrivacySettings(let flags, let archiveAndMuteNewNoncontactPeers):
|
||||
if boxed {
|
||||
buffer.appendInt32(-1096616924)
|
||||
}
|
||||
serializeInt32(flags, buffer: buffer, boxed: false)
|
||||
if Int(flags) & Int(1 << 0) != 0 {archiveAndMuteNewNoncontactPeers!.serialize(buffer, true)}
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
public func descriptionFields() -> (String, [(String, Any)]) {
|
||||
switch self {
|
||||
case .globalPrivacySettings(let flags, let archiveAndMuteNewNoncontactPeers):
|
||||
return ("globalPrivacySettings", [("flags", flags), ("archiveAndMuteNewNoncontactPeers", archiveAndMuteNewNoncontactPeers)])
|
||||
}
|
||||
}
|
||||
|
||||
public static func parse_globalPrivacySettings(_ reader: BufferReader) -> GlobalPrivacySettings? {
|
||||
var _1: Int32?
|
||||
_1 = reader.readInt32()
|
||||
var _2: Api.Bool?
|
||||
if Int(_1!) & Int(1 << 0) != 0 {if let signature = reader.readInt32() {
|
||||
_2 = Api.parse(reader, signature: signature) as? Api.Bool
|
||||
} }
|
||||
let _c1 = _1 != nil
|
||||
let _c2 = (Int(_1!) & Int(1 << 0) == 0) || _2 != nil
|
||||
if _c1 && _c2 {
|
||||
return Api.GlobalPrivacySettings.globalPrivacySettings(flags: _1!, archiveAndMuteNewNoncontactPeers: _2)
|
||||
}
|
||||
else {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
public enum InlineBotSwitchPM: TypeConstructorDescription {
|
||||
case inlineBotSwitchPM(text: String, startParam: String)
|
||||
|
|
|
|||
|
|
@ -4421,12 +4421,13 @@ public extension Api {
|
|||
})
|
||||
}
|
||||
|
||||
public static func getBroadcastStats(flags: Int32, channel: Api.InputChannel) -> (FunctionDescription, Buffer, DeserializeFunctionResponse<Api.stats.BroadcastStats>) {
|
||||
public static func getBroadcastStats(flags: Int32, channel: Api.InputChannel, tzOffset: Int32) -> (FunctionDescription, Buffer, DeserializeFunctionResponse<Api.stats.BroadcastStats>) {
|
||||
let buffer = Buffer()
|
||||
buffer.appendInt32(-1421720550)
|
||||
buffer.appendInt32(-433058374)
|
||||
serializeInt32(flags, buffer: buffer, boxed: false)
|
||||
channel.serialize(buffer, true)
|
||||
return (FunctionDescription(name: "stats.getBroadcastStats", parameters: [("flags", flags), ("channel", channel)]), buffer, DeserializeFunctionResponse { (buffer: Buffer) -> Api.stats.BroadcastStats? in
|
||||
serializeInt32(tzOffset, buffer: buffer, boxed: false)
|
||||
return (FunctionDescription(name: "stats.getBroadcastStats", parameters: [("flags", flags), ("channel", channel), ("tzOffset", tzOffset)]), buffer, DeserializeFunctionResponse { (buffer: Buffer) -> Api.stats.BroadcastStats? in
|
||||
let reader = BufferReader(buffer)
|
||||
var result: Api.stats.BroadcastStats?
|
||||
if let signature = reader.readInt32() {
|
||||
|
|
@ -6614,6 +6615,34 @@ public extension Api {
|
|||
return result
|
||||
})
|
||||
}
|
||||
|
||||
public static func getGlobalPrivacySettings() -> (FunctionDescription, Buffer, DeserializeFunctionResponse<Api.GlobalPrivacySettings>) {
|
||||
let buffer = Buffer()
|
||||
buffer.appendInt32(-349483786)
|
||||
|
||||
return (FunctionDescription(name: "account.getGlobalPrivacySettings", parameters: []), buffer, DeserializeFunctionResponse { (buffer: Buffer) -> Api.GlobalPrivacySettings? in
|
||||
let reader = BufferReader(buffer)
|
||||
var result: Api.GlobalPrivacySettings?
|
||||
if let signature = reader.readInt32() {
|
||||
result = Api.parse(reader, signature: signature) as? Api.GlobalPrivacySettings
|
||||
}
|
||||
return result
|
||||
})
|
||||
}
|
||||
|
||||
public static func setGlobalPrivacySettings(settings: Api.GlobalPrivacySettings) -> (FunctionDescription, Buffer, DeserializeFunctionResponse<Api.GlobalPrivacySettings>) {
|
||||
let buffer = Buffer()
|
||||
buffer.appendInt32(517647042)
|
||||
settings.serialize(buffer, true)
|
||||
return (FunctionDescription(name: "account.setGlobalPrivacySettings", parameters: [("settings", settings)]), buffer, DeserializeFunctionResponse { (buffer: Buffer) -> Api.GlobalPrivacySettings? in
|
||||
let reader = BufferReader(buffer)
|
||||
var result: Api.GlobalPrivacySettings?
|
||||
if let signature = reader.readInt32() {
|
||||
result = Api.parse(reader, signature: signature) as? Api.GlobalPrivacySettings
|
||||
}
|
||||
return result
|
||||
})
|
||||
}
|
||||
}
|
||||
public struct wallet {
|
||||
public static func sendLiteRequest(body: Buffer) -> (FunctionDescription, Buffer, DeserializeFunctionResponse<Api.wallet.LiteResponse>) {
|
||||
|
|
|
|||
|
|
@ -37,7 +37,7 @@ private func presentLiveLocationController(context: AccountContext, peerId: Peer
|
|||
}, addToTransitionSurface: { _ in
|
||||
}, openUrl: { _ in
|
||||
}, openPeer: { peer, navigation in
|
||||
}, callPeer: { _ in
|
||||
}, callPeer: { _, _ in
|
||||
}, enqueueMessage: { _ in
|
||||
}, sendSticker: nil,
|
||||
setupTemporaryHiddenMedia: { _, _, _ in
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ enum CallControllerButtonType {
|
|||
case accept
|
||||
case speaker
|
||||
case bluetooth
|
||||
case video
|
||||
case switchCamera
|
||||
}
|
||||
|
||||
private let buttonSize = CGSize(width: 75.0, height: 75.0)
|
||||
|
|
@ -124,8 +124,8 @@ final class CallControllerButtonNode: HighlightTrackingButtonNode {
|
|||
regularImage = generateEmptyButtonImage(icon: UIImage(bundleImageName: "Call/CallBluetoothButton"), strokeColor: emptyStroke, fillColor: .clear)
|
||||
highlightedImage = generateEmptyButtonImage(icon: UIImage(bundleImageName: "Call/CallBluetoothButton"), strokeColor: emptyStroke, fillColor: emptyHighlightedFill)
|
||||
filledImage = generateEmptyButtonImage(icon: UIImage(bundleImageName: "Call/CallBluetoothButton"), strokeColor: nil, fillColor: invertedFill, knockout: true)
|
||||
case .video:
|
||||
let patternImage = generateTintedImage(image: UIImage(bundleImageName: "Chat/Input/Text/IconVideo"), color: .white)
|
||||
case .switchCamera:
|
||||
let patternImage = generateTintedImage(image: UIImage(bundleImageName: "Call/CallSwitchCameraButton"), color: .white)
|
||||
regularImage = generateEmptyButtonImage(icon: patternImage, strokeColor: emptyStroke, fillColor: .clear)
|
||||
highlightedImage = generateEmptyButtonImage(icon: patternImage, strokeColor: emptyStroke, fillColor: emptyHighlightedFill)
|
||||
filledImage = generateEmptyButtonImage(icon: patternImage, strokeColor: nil, fillColor: invertedFill, knockout: true)
|
||||
|
|
@ -215,8 +215,8 @@ final class CallControllerButtonNode: HighlightTrackingButtonNode {
|
|||
regularImage = generateEmptyButtonImage(icon: UIImage(bundleImageName: "Call/CallBluetoothButton"), strokeColor: emptyStroke, fillColor: .clear)
|
||||
highlightedImage = generateEmptyButtonImage(icon: UIImage(bundleImageName: "Call/CallBluetoothButton"), strokeColor: emptyStroke, fillColor: emptyHighlightedFill)
|
||||
filledImage = generateEmptyButtonImage(icon: UIImage(bundleImageName: "Call/CallBluetoothButton"), strokeColor: nil, fillColor: invertedFill, knockout: true)
|
||||
case .video:
|
||||
let patternImage = generateTintedImage(image: UIImage(bundleImageName: "Chat/Input/Text/IconVideo"), color: .white)
|
||||
case .switchCamera:
|
||||
let patternImage = generateTintedImage(image: UIImage(bundleImageName: "Call/CallSwitchCameraButton"), color: .white)
|
||||
regularImage = generateEmptyButtonImage(icon: patternImage, strokeColor: emptyStroke, fillColor: .clear)
|
||||
highlightedImage = generateEmptyButtonImage(icon: patternImage, strokeColor: emptyStroke, fillColor: emptyHighlightedFill)
|
||||
filledImage = generateEmptyButtonImage(icon: patternImage, strokeColor: nil, fillColor: invertedFill, knockout: true)
|
||||
|
|
|
|||
|
|
@ -32,8 +32,7 @@ final class CallControllerButtonsNode: ASDisplayNode {
|
|||
private let muteButton: CallControllerButtonNode
|
||||
private let endButton: CallControllerButtonNode
|
||||
private let speakerButton: CallControllerButtonNode
|
||||
|
||||
private let videoButton: CallControllerButtonNode
|
||||
private let swichCameraButton: CallControllerButtonNode
|
||||
|
||||
private var mode: CallControllerButtonsMode?
|
||||
|
||||
|
|
@ -50,6 +49,7 @@ final class CallControllerButtonsNode: ASDisplayNode {
|
|||
var end: (() -> Void)?
|
||||
var speaker: (() -> Void)?
|
||||
var toggleVideo: (() -> Void)?
|
||||
var rotateCamera: (() -> Void)?
|
||||
|
||||
init(strings: PresentationStrings) {
|
||||
self.acceptButton = CallControllerButtonNode(type: .accept, label: strings.Call_Accept)
|
||||
|
|
@ -63,9 +63,8 @@ final class CallControllerButtonsNode: ASDisplayNode {
|
|||
self.endButton.alpha = 0.0
|
||||
self.speakerButton = CallControllerButtonNode(type: .speaker, label: nil)
|
||||
self.speakerButton.alpha = 0.0
|
||||
|
||||
self.videoButton = CallControllerButtonNode(type: .video, label: nil)
|
||||
self.videoButton.alpha = 0.0
|
||||
self.swichCameraButton = CallControllerButtonNode(type: .switchCamera, label: nil)
|
||||
self.swichCameraButton.alpha = 0.0
|
||||
|
||||
super.init()
|
||||
|
||||
|
|
@ -74,14 +73,14 @@ final class CallControllerButtonsNode: ASDisplayNode {
|
|||
self.addSubnode(self.muteButton)
|
||||
self.addSubnode(self.endButton)
|
||||
self.addSubnode(self.speakerButton)
|
||||
self.addSubnode(self.videoButton)
|
||||
self.addSubnode(self.swichCameraButton)
|
||||
|
||||
self.acceptButton.addTarget(self, action: #selector(self.buttonPressed(_:)), forControlEvents: .touchUpInside)
|
||||
self.declineButton.addTarget(self, action: #selector(self.buttonPressed(_:)), forControlEvents: .touchUpInside)
|
||||
self.muteButton.addTarget(self, action: #selector(self.buttonPressed(_:)), forControlEvents: .touchUpInside)
|
||||
self.endButton.addTarget(self, action: #selector(self.buttonPressed(_:)), forControlEvents: .touchUpInside)
|
||||
self.speakerButton.addTarget(self, action: #selector(self.buttonPressed(_:)), forControlEvents: .touchUpInside)
|
||||
self.videoButton.addTarget(self, action: #selector(self.buttonPressed(_:)), forControlEvents: .touchUpInside)
|
||||
self.swichCameraButton.addTarget(self, action: #selector(self.buttonPressed(_:)), forControlEvents: .touchUpInside)
|
||||
}
|
||||
|
||||
func updateLayout(constrainedWidth: CGFloat, transition: ContainedViewLayoutTransition) {
|
||||
|
|
@ -119,11 +118,11 @@ final class CallControllerButtonsNode: ASDisplayNode {
|
|||
let twoButtonsWidth = 2.0 * buttonSize.width + 1.0 * twoButtonSpacing
|
||||
|
||||
var origin = CGPoint(x: floor((width - threeButtonsWidth) / 2.0), y: 0.0)
|
||||
|
||||
for button in [self.muteButton, self.endButton, self.speakerButton] {
|
||||
transition.updateFrame(node: button, frame: CGRect(origin: origin, size: buttonSize))
|
||||
|
||||
if button === self.endButton {
|
||||
transition.updateFrame(node: self.videoButton, frame: CGRect(origin: CGPoint(x: origin.x, y: origin.y - buttonSize.height - 20.0), size: buttonSize))
|
||||
if button === self.speakerButton {
|
||||
transition.updateFrame(node: self.swichCameraButton, frame: CGRect(origin: origin, size: buttonSize))
|
||||
}
|
||||
|
||||
origin.x += buttonSize.width + threeButtonSpacing
|
||||
|
|
@ -140,16 +139,44 @@ final class CallControllerButtonsNode: ASDisplayNode {
|
|||
for button in [self.declineButton, self.acceptButton] {
|
||||
button.alpha = 1.0
|
||||
}
|
||||
for button in [self.muteButton, self.endButton, self.speakerButton, self.videoButton] {
|
||||
for button in [self.muteButton, self.endButton, self.speakerButton, self.swichCameraButton] {
|
||||
button.alpha = 0.0
|
||||
}
|
||||
case let .active(speakerMode, videoState):
|
||||
for button in [self.muteButton, self.speakerButton] {
|
||||
for button in [self.muteButton] {
|
||||
if animated && button.alpha.isZero {
|
||||
button.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.3)
|
||||
}
|
||||
button.alpha = 1.0
|
||||
}
|
||||
switch videoState {
|
||||
case .active, .available:
|
||||
for button in [self.speakerButton] {
|
||||
if animated && !button.alpha.isZero {
|
||||
button.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3)
|
||||
}
|
||||
button.alpha = 0.0
|
||||
}
|
||||
for button in [self.swichCameraButton] {
|
||||
if animated && button.alpha.isZero {
|
||||
button.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.3)
|
||||
}
|
||||
button.alpha = 1.0
|
||||
}
|
||||
case .notAvailable:
|
||||
for button in [self.swichCameraButton] {
|
||||
if animated && !button.alpha.isZero {
|
||||
button.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3)
|
||||
}
|
||||
button.alpha = 0.0
|
||||
}
|
||||
for button in [self.speakerButton] {
|
||||
if animated && button.alpha.isZero {
|
||||
button.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.3)
|
||||
}
|
||||
button.alpha = 1.0
|
||||
}
|
||||
}
|
||||
var animatingAcceptButton = false
|
||||
if self.endButton.alpha.isZero {
|
||||
if animated {
|
||||
|
|
@ -171,23 +198,6 @@ final class CallControllerButtonsNode: ASDisplayNode {
|
|||
self.endButton.alpha = 1.0
|
||||
}
|
||||
|
||||
switch videoState {
|
||||
case .notAvailable:
|
||||
self.videoButton.alpha = 0.0
|
||||
case let .available(isEnabled):
|
||||
self.videoButton.isUserInteractionEnabled = isEnabled
|
||||
if animated {
|
||||
self.videoButton.alpha = isEnabled ? 1.0 : 0.5
|
||||
self.videoButton.layer.animateAlpha(from: 0.0, to: self.videoButton.alpha, duration: 0.2)
|
||||
} else {
|
||||
self.videoButton.alpha = isEnabled ? 1.0 : 0.5
|
||||
}
|
||||
case .active:
|
||||
self.videoButton.isUserInteractionEnabled = true
|
||||
self.videoButton.alpha = 0.0
|
||||
}
|
||||
|
||||
|
||||
if !self.declineButton.alpha.isZero {
|
||||
if animated {
|
||||
self.declineButton.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2)
|
||||
|
|
@ -223,8 +233,8 @@ final class CallControllerButtonsNode: ASDisplayNode {
|
|||
self.speaker?()
|
||||
} else if button === self.acceptButton {
|
||||
self.accept?()
|
||||
} else if button === self.videoButton {
|
||||
self.toggleVideo?()
|
||||
} else if button === self.swichCameraButton {
|
||||
self.rotateCamera?()
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -235,9 +245,12 @@ final class CallControllerButtonsNode: ASDisplayNode {
|
|||
self.muteButton,
|
||||
self.endButton,
|
||||
self.speakerButton,
|
||||
self.videoButton
|
||||
self.swichCameraButton
|
||||
]
|
||||
for button in buttons {
|
||||
if button.isHidden || button.alpha.isZero {
|
||||
continue
|
||||
}
|
||||
if let result = button.view.hitTest(self.view.convert(point, to: button.view), with: event) {
|
||||
return result
|
||||
}
|
||||
|
|
|
|||
|
|
@ -16,6 +16,8 @@ import CallsEmoji
|
|||
|
||||
private final class IncomingVideoNode: ASDisplayNode {
|
||||
private let videoView: UIView
|
||||
private var effectView: UIVisualEffectView?
|
||||
private var isBlurred: Bool = false
|
||||
|
||||
init(videoView: UIView) {
|
||||
self.videoView = videoView
|
||||
|
|
@ -28,6 +30,29 @@ private final class IncomingVideoNode: ASDisplayNode {
|
|||
func updateLayout(size: CGSize) {
|
||||
self.videoView.frame = CGRect(origin: CGPoint(), size: size)
|
||||
}
|
||||
|
||||
func updateIsBlurred(isBlurred: Bool) {
|
||||
if self.isBlurred == isBlurred {
|
||||
return
|
||||
}
|
||||
self.isBlurred = isBlurred
|
||||
|
||||
if isBlurred {
|
||||
if self.effectView == nil {
|
||||
let effectView = UIVisualEffectView()
|
||||
self.effectView = effectView
|
||||
effectView.frame = self.videoView.frame
|
||||
self.view.addSubview(effectView)
|
||||
}
|
||||
UIView.animate(withDuration: 0.3, animations: {
|
||||
self.effectView?.effect = UIBlurEffect(style: .dark)
|
||||
})
|
||||
} else if let effectView = self.effectView {
|
||||
UIView.animate(withDuration: 0.3, animations: {
|
||||
effectView.effect = nil
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private final class OutgoingVideoNode: ASDisplayNode {
|
||||
|
|
@ -51,8 +76,9 @@ private final class OutgoingVideoNode: ASDisplayNode {
|
|||
self.switchCamera()
|
||||
}
|
||||
|
||||
func updateLayout(size: CGSize) {
|
||||
self.videoView.frame = CGRect(origin: CGPoint(), size: size)
|
||||
func updateLayout(size: CGSize, isExpanded: Bool, transition: ContainedViewLayoutTransition) {
|
||||
transition.updateFrame(view: self.videoView, frame: CGRect(origin: CGPoint(), size: size))
|
||||
transition.updateCornerRadius(layer: self.videoView.layer, cornerRadius: isExpanded ? 0.0 : 16.0)
|
||||
self.switchCameraButton.frame = CGRect(origin: CGPoint(), size: size)
|
||||
}
|
||||
}
|
||||
|
|
@ -75,11 +101,13 @@ final class CallControllerNode: ASDisplayNode {
|
|||
private let imageNode: TransformImageNode
|
||||
private let dimNode: ASDisplayNode
|
||||
private var incomingVideoNode: IncomingVideoNode?
|
||||
private var incomingVideoViewRequested: Bool = false
|
||||
private var outgoingVideoNode: OutgoingVideoNode?
|
||||
private var videoViewsRequested: Bool = false
|
||||
private var outgoingVideoViewRequested: Bool = false
|
||||
private let backButtonArrowNode: ASImageNode
|
||||
private let backButtonNode: HighlightableButtonNode
|
||||
private let statusNode: CallControllerStatusNode
|
||||
private let videoPausedNode: ImmediateTextNode
|
||||
private let buttonsNode: CallControllerButtonsNode
|
||||
private var keyPreviewNode: CallControllerKeyPreviewNode?
|
||||
|
||||
|
|
@ -140,6 +168,10 @@ final class CallControllerNode: ASDisplayNode {
|
|||
self.backButtonNode = HighlightableButtonNode()
|
||||
|
||||
self.statusNode = CallControllerStatusNode()
|
||||
|
||||
self.videoPausedNode = ImmediateTextNode()
|
||||
self.videoPausedNode.alpha = 0.0
|
||||
|
||||
self.buttonsNode = CallControllerButtonsNode(strings: self.presentationData.strings)
|
||||
self.keyButtonNode = HighlightableButtonNode()
|
||||
|
||||
|
|
@ -174,6 +206,7 @@ final class CallControllerNode: ASDisplayNode {
|
|||
self.containerNode.addSubnode(self.imageNode)
|
||||
self.containerNode.addSubnode(self.dimNode)
|
||||
self.containerNode.addSubnode(self.statusNode)
|
||||
self.containerNode.addSubnode(self.videoPausedNode)
|
||||
self.containerNode.addSubnode(self.buttonsNode)
|
||||
self.containerNode.addSubnode(self.keyButtonNode)
|
||||
self.containerNode.addSubnode(self.backButtonArrowNode)
|
||||
|
|
@ -199,6 +232,10 @@ final class CallControllerNode: ASDisplayNode {
|
|||
self?.toggleVideo?()
|
||||
}
|
||||
|
||||
self.buttonsNode.rotateCamera = { [weak self] in
|
||||
self?.call.switchVideoCamera()
|
||||
}
|
||||
|
||||
self.keyButtonNode.addTarget(self, action: #selector(self.keyPressed), forControlEvents: .touchUpInside)
|
||||
|
||||
self.backButtonNode.addTarget(self, action: #selector(self.backPressed), forControlEvents: .touchUpInside)
|
||||
|
|
@ -235,6 +272,8 @@ final class CallControllerNode: ASDisplayNode {
|
|||
}
|
||||
}
|
||||
|
||||
self.videoPausedNode.attributedText = NSAttributedString(string: self.presentationData.strings.Call_RemoteVideoPaused(peer.compactDisplayTitle).0, font: Font.regular(17.0), textColor: .white)
|
||||
|
||||
if let (layout, navigationBarHeight) = self.validLayout {
|
||||
self.containerLayoutUpdated(layout, navigationBarHeight: navigationBarHeight, transition: .immediate)
|
||||
}
|
||||
|
|
@ -256,8 +295,8 @@ final class CallControllerNode: ASDisplayNode {
|
|||
|
||||
switch callState.videoState {
|
||||
case .active:
|
||||
if !self.videoViewsRequested {
|
||||
self.videoViewsRequested = true
|
||||
if !self.incomingVideoViewRequested {
|
||||
self.incomingVideoViewRequested = true
|
||||
self.call.makeIncomingVideoView(completion: { [weak self] incomingVideoView in
|
||||
guard let strongSelf = self else {
|
||||
return
|
||||
|
|
@ -273,7 +312,38 @@ final class CallControllerNode: ASDisplayNode {
|
|||
}
|
||||
}
|
||||
})
|
||||
|
||||
}
|
||||
if !self.outgoingVideoViewRequested {
|
||||
self.outgoingVideoViewRequested = true
|
||||
self.call.makeOutgoingVideoView(completion: { [weak self] outgoingVideoView in
|
||||
guard let strongSelf = self else {
|
||||
return
|
||||
}
|
||||
if let outgoingVideoView = outgoingVideoView {
|
||||
outgoingVideoView.backgroundColor = .black
|
||||
outgoingVideoView.clipsToBounds = true
|
||||
strongSelf.setCurrentAudioOutput?(.speaker)
|
||||
let outgoingVideoNode = OutgoingVideoNode(videoView: outgoingVideoView, switchCamera: {
|
||||
guard let strongSelf = self else {
|
||||
return
|
||||
}
|
||||
strongSelf.call.switchVideoCamera()
|
||||
})
|
||||
strongSelf.outgoingVideoNode = outgoingVideoNode
|
||||
if let incomingVideoNode = strongSelf.incomingVideoNode {
|
||||
strongSelf.containerNode.insertSubnode(outgoingVideoNode, aboveSubnode: incomingVideoNode)
|
||||
} else {
|
||||
strongSelf.containerNode.insertSubnode(outgoingVideoNode, aboveSubnode: strongSelf.dimNode)
|
||||
}
|
||||
if let (layout, navigationBarHeight) = strongSelf.validLayout {
|
||||
strongSelf.containerLayoutUpdated(layout, navigationBarHeight: navigationBarHeight, transition: .immediate)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
case .activeOutgoing:
|
||||
if !self.outgoingVideoViewRequested {
|
||||
self.outgoingVideoViewRequested = true
|
||||
self.call.makeOutgoingVideoView(completion: { [weak self] outgoingVideoView in
|
||||
guard let strongSelf = self else {
|
||||
return
|
||||
|
|
@ -305,6 +375,26 @@ final class CallControllerNode: ASDisplayNode {
|
|||
break
|
||||
}
|
||||
|
||||
if let incomingVideoNode = self.incomingVideoNode {
|
||||
let isActive: Bool
|
||||
switch callState.remoteVideoState {
|
||||
case .inactive:
|
||||
isActive = false
|
||||
case .active:
|
||||
isActive = true
|
||||
}
|
||||
incomingVideoNode.updateIsBlurred(isBlurred: !isActive)
|
||||
if isActive != self.videoPausedNode.alpha.isZero {
|
||||
if isActive {
|
||||
self.videoPausedNode.alpha = 0.0
|
||||
self.videoPausedNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3)
|
||||
} else {
|
||||
self.videoPausedNode.alpha = 1.0
|
||||
self.videoPausedNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.3)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
switch callState.state {
|
||||
case .waiting, .connecting:
|
||||
statusValue = .text(self.presentationData.strings.Call_StatusConnecting)
|
||||
|
|
@ -444,6 +534,8 @@ final class CallControllerNode: ASDisplayNode {
|
|||
mappedVideoState = .available(true)
|
||||
case .active:
|
||||
mappedVideoState = .active
|
||||
case .activeOutgoing:
|
||||
mappedVideoState = .active
|
||||
}
|
||||
self.buttonsNode.updateMode(.active(speakerMode: mode, videoState: mappedVideoState))
|
||||
}
|
||||
|
|
@ -534,19 +626,31 @@ final class CallControllerNode: ASDisplayNode {
|
|||
let statusHeight = self.statusNode.updateLayout(constrainedWidth: layout.size.width, transition: transition)
|
||||
transition.updateFrame(node: self.statusNode, frame: CGRect(origin: CGPoint(x: 0.0, y: statusOffset), size: CGSize(width: layout.size.width, height: statusHeight)))
|
||||
|
||||
let videoPausedSize = self.videoPausedNode.updateLayout(CGSize(width: layout.size.width - 16.0, height: 100.0))
|
||||
transition.updateFrame(node: self.videoPausedNode, frame: CGRect(origin: CGPoint(x: floor((layout.size.width - videoPausedSize.width) / 2.0), y: floor((layout.size.height - videoPausedSize.height) / 2.0)), size: videoPausedSize))
|
||||
|
||||
self.buttonsNode.updateLayout(constrainedWidth: layout.size.width, transition: transition)
|
||||
let buttonsOriginY: CGFloat = layout.size.height - (buttonsOffset - 40.0) - buttonsHeight - layout.intrinsicInsets.bottom
|
||||
transition.updateFrame(node: self.buttonsNode, frame: CGRect(origin: CGPoint(x: 0.0, y: buttonsOriginY), size: CGSize(width: layout.size.width, height: buttonsHeight)))
|
||||
|
||||
var outgoingVideoTransition = transition
|
||||
if let incomingVideoNode = self.incomingVideoNode {
|
||||
if incomingVideoNode.frame.width.isZero, let outgoingVideoNode = self.outgoingVideoNode, !outgoingVideoNode.frame.width.isZero, !transition.isAnimated {
|
||||
outgoingVideoTransition = .animated(duration: 0.3, curve: .easeInOut)
|
||||
}
|
||||
incomingVideoNode.frame = CGRect(origin: CGPoint(), size: layout.size)
|
||||
incomingVideoNode.updateLayout(size: layout.size)
|
||||
}
|
||||
if let outgoingVideoNode = self.outgoingVideoNode {
|
||||
let outgoingSize = layout.size.aspectFitted(CGSize(width: 200.0, height: 200.0))
|
||||
let outgoingFrame = CGRect(origin: CGPoint(x: layout.size.width - 16.0 - outgoingSize.width, y: buttonsOriginY - 32.0 - outgoingSize.height), size: outgoingSize)
|
||||
outgoingVideoNode.frame = outgoingFrame
|
||||
outgoingVideoNode.updateLayout(size: outgoingFrame.size)
|
||||
if self.incomingVideoNode == nil {
|
||||
outgoingVideoNode.frame = CGRect(origin: CGPoint(), size: layout.size)
|
||||
outgoingVideoNode.updateLayout(size: layout.size, isExpanded: true, transition: transition)
|
||||
} else {
|
||||
let outgoingSize = layout.size.aspectFitted(CGSize(width: 200.0, height: 200.0))
|
||||
let outgoingFrame = CGRect(origin: CGPoint(x: layout.size.width - 16.0 - outgoingSize.width, y: buttonsOriginY - 32.0 - outgoingSize.height), size: outgoingSize)
|
||||
outgoingVideoTransition.updateFrame(node: outgoingVideoNode, frame: outgoingFrame)
|
||||
outgoingVideoNode.updateLayout(size: outgoingFrame.size, isExpanded: false, transition: outgoingVideoTransition)
|
||||
}
|
||||
}
|
||||
|
||||
let keyTextSize = self.keyButtonNode.frame.size
|
||||
|
|
|
|||
|
|
@ -15,13 +15,13 @@ public final class CallKitIntegration {
|
|||
public static var isAvailable: Bool {
|
||||
#if targetEnvironment(simulator)
|
||||
return false
|
||||
#endif
|
||||
|
||||
#else
|
||||
if #available(iOSApplicationExtension 10.0, iOS 10.0, *) {
|
||||
return Locale.current.regionCode?.lowercased() != "cn"
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
private let audioSessionActivePromise = ValuePromise<Bool>(false, ignoreRepeated: true)
|
||||
|
|
@ -29,7 +29,7 @@ public final class CallKitIntegration {
|
|||
return self.audioSessionActivePromise.get()
|
||||
}
|
||||
|
||||
init?(startCall: @escaping (Account, UUID, String) -> Signal<Bool, NoError>, answerCall: @escaping (UUID) -> Void, endCall: @escaping (UUID) -> Signal<Bool, NoError>, setCallMuted: @escaping (UUID, Bool) -> Void, audioSessionActivationChanged: @escaping (Bool) -> Void) {
|
||||
init?(enableVideoCalls: Bool, startCall: @escaping (Account, UUID, String, Bool) -> Signal<Bool, NoError>, answerCall: @escaping (UUID) -> Void, endCall: @escaping (UUID) -> Signal<Bool, NoError>, setCallMuted: @escaping (UUID, Bool) -> Void, audioSessionActivationChanged: @escaping (Bool) -> Void) {
|
||||
if !CallKitIntegration.isAvailable {
|
||||
return nil
|
||||
}
|
||||
|
|
@ -40,7 +40,7 @@ public final class CallKitIntegration {
|
|||
|
||||
if #available(iOSApplicationExtension 10.0, iOS 10.0, *) {
|
||||
if sharedProviderDelegate == nil {
|
||||
sharedProviderDelegate = CallKitProviderDelegate()
|
||||
sharedProviderDelegate = CallKitProviderDelegate(enableVideoCalls: enableVideoCalls)
|
||||
}
|
||||
(sharedProviderDelegate as? CallKitProviderDelegate)?.setup(audioSessionActivePromise: self.audioSessionActivePromise, startCall: startCall, answerCall: answerCall, endCall: endCall, setCallMuted: setCallMuted, audioSessionActivationChanged: audioSessionActivationChanged)
|
||||
} else {
|
||||
|
|
@ -49,9 +49,9 @@ public final class CallKitIntegration {
|
|||
#endif
|
||||
}
|
||||
|
||||
func startCall(account: Account, peerId: PeerId, displayTitle: String) {
|
||||
func startCall(account: Account, peerId: PeerId, isVideo: Bool, displayTitle: String) {
|
||||
if #available(iOSApplicationExtension 10.0, iOS 10.0, *) {
|
||||
(sharedProviderDelegate as? CallKitProviderDelegate)?.startCall(account: account, peerId: peerId, displayTitle: displayTitle)
|
||||
(sharedProviderDelegate as? CallKitProviderDelegate)?.startCall(account: account, peerId: peerId, isVideo: isVideo, displayTitle: displayTitle)
|
||||
self.donateIntent(peerId: peerId, displayTitle: displayTitle)
|
||||
}
|
||||
}
|
||||
|
|
@ -68,9 +68,9 @@ public final class CallKitIntegration {
|
|||
}
|
||||
}
|
||||
|
||||
func reportIncomingCall(uuid: UUID, handle: String, displayTitle: String, completion: ((NSError?) -> Void)?) {
|
||||
func reportIncomingCall(uuid: UUID, handle: String, isVideo: Bool, displayTitle: String, completion: ((NSError?) -> Void)?) {
|
||||
if #available(iOSApplicationExtension 10.0, iOS 10.0, *) {
|
||||
(sharedProviderDelegate as? CallKitProviderDelegate)?.reportIncomingCall(uuid: uuid, handle: handle, displayTitle: displayTitle, completion: completion)
|
||||
(sharedProviderDelegate as? CallKitProviderDelegate)?.reportIncomingCall(uuid: uuid, handle: handle, isVideo: isVideo, displayTitle: displayTitle, completion: completion)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -102,7 +102,7 @@ class CallKitProviderDelegate: NSObject, CXProviderDelegate {
|
|||
|
||||
private var currentStartCallAccount: (UUID, Account)?
|
||||
|
||||
private var startCall: ((Account, UUID, String) -> Signal<Bool, NoError>)?
|
||||
private var startCall: ((Account, UUID, String, Bool) -> Signal<Bool, NoError>)?
|
||||
private var answerCall: ((UUID) -> Void)?
|
||||
private var endCall: ((UUID) -> Signal<Bool, NoError>)?
|
||||
private var setCallMuted: ((UUID, Bool) -> Void)?
|
||||
|
|
@ -112,15 +112,15 @@ class CallKitProviderDelegate: NSObject, CXProviderDelegate {
|
|||
|
||||
fileprivate var audioSessionActivePromise: ValuePromise<Bool>?
|
||||
|
||||
override init() {
|
||||
self.provider = CXProvider(configuration: CallKitProviderDelegate.providerConfiguration)
|
||||
init(enableVideoCalls: Bool) {
|
||||
self.provider = CXProvider(configuration: CallKitProviderDelegate.providerConfiguration(enableVideoCalls: enableVideoCalls))
|
||||
|
||||
super.init()
|
||||
|
||||
self.provider.setDelegate(self, queue: nil)
|
||||
}
|
||||
|
||||
func setup(audioSessionActivePromise: ValuePromise<Bool>, startCall: @escaping (Account, UUID, String) -> Signal<Bool, NoError>, answerCall: @escaping (UUID) -> Void, endCall: @escaping (UUID) -> Signal<Bool, NoError>, setCallMuted: @escaping (UUID, Bool) -> Void, audioSessionActivationChanged: @escaping (Bool) -> Void) {
|
||||
func setup(audioSessionActivePromise: ValuePromise<Bool>, startCall: @escaping (Account, UUID, String, Bool) -> Signal<Bool, NoError>, answerCall: @escaping (UUID) -> Void, endCall: @escaping (UUID) -> Signal<Bool, NoError>, setCallMuted: @escaping (UUID, Bool) -> Void, audioSessionActivationChanged: @escaping (Bool) -> Void) {
|
||||
self.audioSessionActivePromise = audioSessionActivePromise
|
||||
self.startCall = startCall
|
||||
self.answerCall = answerCall
|
||||
|
|
@ -129,7 +129,7 @@ class CallKitProviderDelegate: NSObject, CXProviderDelegate {
|
|||
self.audioSessionActivationChanged = audioSessionActivationChanged
|
||||
}
|
||||
|
||||
static var providerConfiguration: CXProviderConfiguration {
|
||||
private static func providerConfiguration(enableVideoCalls: Bool) -> CXProviderConfiguration {
|
||||
let providerConfiguration = CXProviderConfiguration(localizedName: "Telegram")
|
||||
|
||||
providerConfiguration.supportsVideo = false
|
||||
|
|
@ -166,14 +166,14 @@ class CallKitProviderDelegate: NSObject, CXProviderDelegate {
|
|||
|
||||
}
|
||||
|
||||
func startCall(account: Account, peerId: PeerId, displayTitle: String) {
|
||||
func startCall(account: Account, peerId: PeerId, isVideo: Bool, displayTitle: String) {
|
||||
let uuid = UUID()
|
||||
self.currentStartCallAccount = (uuid, account)
|
||||
let handle = CXHandle(type: .generic, value: "\(peerId.id)")
|
||||
let startCallAction = CXStartCallAction(call: uuid, handle: handle)
|
||||
startCallAction.contactIdentifier = displayTitle
|
||||
|
||||
startCallAction.isVideo = false
|
||||
startCallAction.isVideo = isVideo
|
||||
let transaction = CXTransaction(action: startCallAction)
|
||||
|
||||
self.requestTransaction(transaction, completion: { _ in
|
||||
|
|
@ -189,7 +189,7 @@ class CallKitProviderDelegate: NSObject, CXProviderDelegate {
|
|||
})
|
||||
}
|
||||
|
||||
func reportIncomingCall(uuid: UUID, handle: String, displayTitle: String, completion: ((NSError?) -> Void)?) {
|
||||
func reportIncomingCall(uuid: UUID, handle: String, isVideo: Bool, displayTitle: String, completion: ((NSError?) -> Void)?) {
|
||||
let update = CXCallUpdate()
|
||||
update.remoteHandle = CXHandle(type: .generic, value: handle)
|
||||
update.localizedCallerName = displayTitle
|
||||
|
|
@ -197,6 +197,7 @@ class CallKitProviderDelegate: NSObject, CXProviderDelegate {
|
|||
update.supportsGrouping = false
|
||||
update.supportsUngrouping = false
|
||||
update.supportsDTMF = false
|
||||
update.hasVideo = isVideo
|
||||
|
||||
self.provider.reportNewIncomingCall(with: uuid, update: update, completion: { error in
|
||||
completion?(error as NSError?)
|
||||
|
|
@ -222,7 +223,7 @@ class CallKitProviderDelegate: NSObject, CXProviderDelegate {
|
|||
self.currentStartCallAccount = nil
|
||||
let disposable = MetaDisposable()
|
||||
self.disposableSet.add(disposable)
|
||||
disposable.set((startCall(account, action.callUUID, action.handle.value)
|
||||
disposable.set((startCall(account, action.callUUID, action.handle.value, action.isVideo)
|
||||
|> deliverOnMainQueue
|
||||
|> afterDisposed { [weak self, weak disposable] in
|
||||
if let strongSelf = self, let disposable = disposable {
|
||||
|
|
|
|||
|
|
@ -166,12 +166,14 @@ public final class PresentationCallImpl: PresentationCall {
|
|||
public let internalId: CallSessionInternalId
|
||||
public let peerId: PeerId
|
||||
public let isOutgoing: Bool
|
||||
private var isVideo: Bool
|
||||
public let peer: Peer?
|
||||
|
||||
private let serializedData: String?
|
||||
private let dataSaving: VoiceCallDataSaving
|
||||
private let derivedState: VoipDerivedState
|
||||
private let proxyServer: ProxyServerSettings?
|
||||
private let auxiliaryServers: [OngoingCallContext.AuxiliaryServer]
|
||||
private let currentNetworkType: NetworkType
|
||||
private let updatedNetworkType: Signal<NetworkType, NoError>
|
||||
|
||||
|
|
@ -188,7 +190,7 @@ public final class PresentationCallImpl: PresentationCall {
|
|||
|
||||
private var sessionStateDisposable: Disposable?
|
||||
|
||||
private let statePromise = ValuePromise<PresentationCallState>(PresentationCallState(state: .waiting, videoState: .notAvailable), ignoreRepeated: true)
|
||||
private let statePromise = ValuePromise<PresentationCallState>(PresentationCallState(state: .waiting, videoState: .notAvailable, remoteVideoState: .inactive), ignoreRepeated: true)
|
||||
public var state: Signal<PresentationCallState, NoError> {
|
||||
return self.statePromise.get()
|
||||
}
|
||||
|
|
@ -231,16 +233,31 @@ public final class PresentationCallImpl: PresentationCall {
|
|||
private var droppedCall = false
|
||||
private var dropCallKitCallTimer: SwiftSignalKit.Timer?
|
||||
|
||||
init(account: Account, audioSession: ManagedAudioSession, callSessionManager: CallSessionManager, callKitIntegration: CallKitIntegration?, serializedData: String?, dataSaving: VoiceCallDataSaving, derivedState: VoipDerivedState, getDeviceAccessData: @escaping () -> (presentationData: PresentationData, present: (ViewController, Any?) -> Void, openSettings: () -> Void), initialState: CallSession?, internalId: CallSessionInternalId, peerId: PeerId, isOutgoing: Bool, peer: Peer?, proxyServer: ProxyServerSettings?, currentNetworkType: NetworkType, updatedNetworkType: Signal<NetworkType, NoError>) {
|
||||
init(account: Account, audioSession: ManagedAudioSession, callSessionManager: CallSessionManager, callKitIntegration: CallKitIntegration?, serializedData: String?, dataSaving: VoiceCallDataSaving, derivedState: VoipDerivedState, getDeviceAccessData: @escaping () -> (presentationData: PresentationData, present: (ViewController, Any?) -> Void, openSettings: () -> Void), initialState: CallSession?, internalId: CallSessionInternalId, peerId: PeerId, isOutgoing: Bool, peer: Peer?, proxyServer: ProxyServerSettings?, auxiliaryServers: [CallAuxiliaryServer], currentNetworkType: NetworkType, updatedNetworkType: Signal<NetworkType, NoError>) {
|
||||
self.account = account
|
||||
self.audioSession = audioSession
|
||||
self.callSessionManager = callSessionManager
|
||||
self.callKitIntegration = callKitIntegration
|
||||
self.getDeviceAccessData = getDeviceAccessData
|
||||
self.auxiliaryServers = auxiliaryServers.map { server -> OngoingCallContext.AuxiliaryServer in
|
||||
let mappedConnection: OngoingCallContext.AuxiliaryServer.Connection
|
||||
switch server.connection {
|
||||
case .stun:
|
||||
mappedConnection = .stun
|
||||
case let .turn(username, password):
|
||||
mappedConnection = .turn(username: username, password: password)
|
||||
}
|
||||
return OngoingCallContext.AuxiliaryServer(
|
||||
host: server.host,
|
||||
port: server.port,
|
||||
connection: mappedConnection
|
||||
)
|
||||
}
|
||||
|
||||
self.internalId = internalId
|
||||
self.peerId = peerId
|
||||
self.isOutgoing = isOutgoing
|
||||
self.isVideo = initialState?.type == .video
|
||||
self.peer = peer
|
||||
|
||||
self.serializedData = serializedData
|
||||
|
|
@ -369,6 +386,9 @@ public final class PresentationCallImpl: PresentationCall {
|
|||
}
|
||||
|
||||
private func updateSessionState(sessionState: CallSession, callContextState: OngoingCallContextState?, reception: Int32?, audioSessionControl: ManagedAudioSessionControl?) {
|
||||
if case .video = sessionState.type {
|
||||
self.isVideo = true
|
||||
}
|
||||
let previous = self.sessionState
|
||||
let previousControl = self.audioSessionControl
|
||||
self.sessionState = sessionState
|
||||
|
|
@ -400,13 +420,37 @@ public final class PresentationCallImpl: PresentationCall {
|
|||
audioSessionControl.setup(synchronous: true)
|
||||
}
|
||||
|
||||
let mappedVideoState: PresentationCallState.VideoState
|
||||
let mappedRemoteVideoState: PresentationCallState.RemoteVideoState
|
||||
if let callContextState = callContextState {
|
||||
switch callContextState.videoState {
|
||||
case .notAvailable:
|
||||
mappedVideoState = .notAvailable
|
||||
case let .available(enabled):
|
||||
mappedVideoState = .available(enabled)
|
||||
case .active:
|
||||
mappedVideoState = .active
|
||||
case .activeOutgoing:
|
||||
mappedVideoState = .activeOutgoing
|
||||
}
|
||||
switch callContextState.remoteVideoState {
|
||||
case .inactive:
|
||||
mappedRemoteVideoState = .inactive
|
||||
case .active:
|
||||
mappedRemoteVideoState = .active
|
||||
}
|
||||
} else {
|
||||
mappedVideoState = .notAvailable
|
||||
mappedRemoteVideoState = .inactive
|
||||
}
|
||||
|
||||
switch sessionState.state {
|
||||
case .ringing:
|
||||
presentationState = PresentationCallState(state: .ringing, videoState: .notAvailable)
|
||||
presentationState = PresentationCallState(state: .ringing, videoState: .notAvailable, remoteVideoState: .inactive)
|
||||
if previous == nil || previousControl == nil {
|
||||
if !self.reportedIncomingCall {
|
||||
self.reportedIncomingCall = true
|
||||
self.callKitIntegration?.reportIncomingCall(uuid: self.internalId, handle: "\(self.peerId.id)", displayTitle: self.peer?.debugDisplayTitle ?? "Unknown", completion: { [weak self] error in
|
||||
self.callKitIntegration?.reportIncomingCall(uuid: self.internalId, handle: "\(self.peerId.id)", isVideo: sessionState.type == .video, displayTitle: self.peer?.debugDisplayTitle ?? "Unknown", completion: { [weak self] error in
|
||||
if let error = error {
|
||||
if error.domain == "com.apple.CallKit.error.incomingcall" && (error.code == -3 || error.code == 3) {
|
||||
Logger.shared.log("PresentationCall", "reportIncomingCall device in DND mode")
|
||||
|
|
@ -429,28 +473,19 @@ public final class PresentationCallImpl: PresentationCall {
|
|||
}
|
||||
case .accepting:
|
||||
self.callWasActive = true
|
||||
presentationState = PresentationCallState(state: .connecting(nil), videoState: .notAvailable)
|
||||
presentationState = PresentationCallState(state: .connecting(nil), videoState: mappedVideoState, remoteVideoState: mappedRemoteVideoState)
|
||||
case .dropping:
|
||||
presentationState = PresentationCallState(state: .terminating, videoState: .notAvailable)
|
||||
presentationState = PresentationCallState(state: .terminating, videoState: mappedVideoState, remoteVideoState: mappedRemoteVideoState)
|
||||
case let .terminated(id, reason, options):
|
||||
presentationState = PresentationCallState(state: .terminated(id, reason, self.callWasActive && (options.contains(.reportRating) || self.shouldPresentCallRating)), videoState: .notAvailable)
|
||||
presentationState = PresentationCallState(state: .terminated(id, reason, self.callWasActive && (options.contains(.reportRating) || self.shouldPresentCallRating)), videoState: mappedVideoState, remoteVideoState: mappedRemoteVideoState)
|
||||
case let .requesting(ringing):
|
||||
presentationState = PresentationCallState(state: .requesting(ringing), videoState: .notAvailable)
|
||||
presentationState = PresentationCallState(state: .requesting(ringing), videoState: mappedVideoState, remoteVideoState: mappedRemoteVideoState)
|
||||
case let .active(_, _, keyVisualHash, _, _, _, _):
|
||||
self.callWasActive = true
|
||||
if let callContextState = callContextState {
|
||||
let mappedVideoState: PresentationCallState.VideoState
|
||||
switch callContextState.videoState {
|
||||
case .notAvailable:
|
||||
mappedVideoState = .notAvailable
|
||||
case let .available(enabled):
|
||||
mappedVideoState = .available(enabled)
|
||||
case .active:
|
||||
mappedVideoState = .active
|
||||
}
|
||||
switch callContextState.state {
|
||||
case .initializing:
|
||||
presentationState = PresentationCallState(state: .connecting(keyVisualHash), videoState: mappedVideoState)
|
||||
presentationState = PresentationCallState(state: .connecting(keyVisualHash), videoState: mappedVideoState, remoteVideoState: mappedRemoteVideoState)
|
||||
case .failed:
|
||||
presentationState = nil
|
||||
self.callSessionManager.drop(internalId: self.internalId, reason: .disconnect, debugLog: .single(nil))
|
||||
|
|
@ -462,7 +497,7 @@ public final class PresentationCallImpl: PresentationCall {
|
|||
timestamp = CFAbsoluteTimeGetCurrent()
|
||||
self.activeTimestamp = timestamp
|
||||
}
|
||||
presentationState = PresentationCallState(state: .active(timestamp, reception, keyVisualHash), videoState: mappedVideoState)
|
||||
presentationState = PresentationCallState(state: .active(timestamp, reception, keyVisualHash), videoState: mappedVideoState, remoteVideoState: mappedRemoteVideoState)
|
||||
case .reconnecting:
|
||||
let timestamp: Double
|
||||
if let activeTimestamp = self.activeTimestamp {
|
||||
|
|
@ -471,10 +506,10 @@ public final class PresentationCallImpl: PresentationCall {
|
|||
timestamp = CFAbsoluteTimeGetCurrent()
|
||||
self.activeTimestamp = timestamp
|
||||
}
|
||||
presentationState = PresentationCallState(state: .reconnecting(timestamp, reception, keyVisualHash), videoState: mappedVideoState)
|
||||
presentationState = PresentationCallState(state: .reconnecting(timestamp, reception, keyVisualHash), videoState: mappedVideoState, remoteVideoState: mappedRemoteVideoState)
|
||||
}
|
||||
} else {
|
||||
presentationState = PresentationCallState(state: .connecting(keyVisualHash), videoState: .notAvailable)
|
||||
presentationState = PresentationCallState(state: .connecting(keyVisualHash), videoState: .notAvailable, remoteVideoState: .inactive)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -488,7 +523,7 @@ public final class PresentationCallImpl: PresentationCall {
|
|||
if let _ = audioSessionControl, !wasActive || previousControl == nil {
|
||||
let logName = "\(id.id)_\(id.accessHash)"
|
||||
|
||||
let ongoingContext = OngoingCallContext(account: account, callSessionManager: self.callSessionManager, internalId: self.internalId, proxyServer: proxyServer, initialNetworkType: self.currentNetworkType, updatedNetworkType: self.updatedNetworkType, serializedData: self.serializedData, dataSaving: dataSaving, derivedState: self.derivedState, key: key, isOutgoing: sessionState.isOutgoing, connections: connections, maxLayer: maxLayer, version: version, allowP2P: allowsP2P, audioSessionActive: self.audioSessionActive.get(), logName: logName)
|
||||
let ongoingContext = OngoingCallContext(account: account, callSessionManager: self.callSessionManager, internalId: self.internalId, proxyServer: proxyServer, auxiliaryServers: auxiliaryServers, initialNetworkType: self.currentNetworkType, updatedNetworkType: self.updatedNetworkType, serializedData: self.serializedData, dataSaving: dataSaving, derivedState: self.derivedState, key: key, isOutgoing: sessionState.isOutgoing, isVideo: sessionState.type == .video, connections: connections, maxLayer: maxLayer, version: version, allowP2P: allowsP2P, audioSessionActive: self.audioSessionActive.get(), logName: logName)
|
||||
self.ongoingContext = ongoingContext
|
||||
|
||||
self.debugInfoValue.set(ongoingContext.debugInfo())
|
||||
|
|
@ -629,8 +664,26 @@ public final class PresentationCallImpl: PresentationCall {
|
|||
return
|
||||
}
|
||||
if value {
|
||||
strongSelf.callSessionManager.accept(internalId: strongSelf.internalId)
|
||||
strongSelf.callKitIntegration?.answerCall(uuid: strongSelf.internalId)
|
||||
if strongSelf.isVideo {
|
||||
DeviceAccess.authorizeAccess(to: .camera, presentationData: presentationData, present: { c, a in
|
||||
present(c, a)
|
||||
}, openSettings: {
|
||||
openSettings()
|
||||
}, { [weak self] value in
|
||||
guard let strongSelf = self else {
|
||||
return
|
||||
}
|
||||
if value {
|
||||
strongSelf.callSessionManager.accept(internalId: strongSelf.internalId)
|
||||
strongSelf.callKitIntegration?.answerCall(uuid: strongSelf.internalId)
|
||||
} else {
|
||||
let _ = strongSelf.hangUp().start()
|
||||
}
|
||||
})
|
||||
} else {
|
||||
strongSelf.callSessionManager.accept(internalId: strongSelf.internalId)
|
||||
strongSelf.callKitIntegration?.answerCall(uuid: strongSelf.internalId)
|
||||
}
|
||||
} else {
|
||||
let _ = strongSelf.hangUp().start()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -16,6 +16,47 @@ private func callKitIntegrationIfEnabled(_ integration: CallKitIntegration?, set
|
|||
return enabled ? integration : nil
|
||||
}
|
||||
|
||||
private func auxiliaryServers(appConfiguration: AppConfiguration) -> [CallAuxiliaryServer] {
|
||||
guard let data = appConfiguration.data else {
|
||||
return []
|
||||
}
|
||||
guard let servers = data["rtc_servers"] as? [[String: Any]] else {
|
||||
return []
|
||||
}
|
||||
var result: [CallAuxiliaryServer] = []
|
||||
for server in servers {
|
||||
guard let host = server["host"] as? String else {
|
||||
continue
|
||||
}
|
||||
guard let portString = server["port"] as? String else {
|
||||
continue
|
||||
}
|
||||
guard let username = server["username"] as? String else {
|
||||
continue
|
||||
}
|
||||
guard let password = server["password"] as? String else {
|
||||
continue
|
||||
}
|
||||
guard let port = Int(portString) else {
|
||||
continue
|
||||
}
|
||||
result.append(CallAuxiliaryServer(
|
||||
host: host,
|
||||
port: port,
|
||||
connection: .stun
|
||||
))
|
||||
result.append(CallAuxiliaryServer(
|
||||
host: host,
|
||||
port: port,
|
||||
connection: .turn(
|
||||
username: username,
|
||||
password: password
|
||||
)
|
||||
))
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
private enum CurrentCall {
|
||||
case none
|
||||
case incomingRinging(CallSessionRingingState)
|
||||
|
|
@ -79,7 +120,7 @@ public final class PresentationCallManagerImpl: PresentationCallManager {
|
|||
return OngoingCallContext.versions(includeExperimental: includeExperimental)
|
||||
}
|
||||
|
||||
public init(accountManager: AccountManager, getDeviceAccessData: @escaping () -> (presentationData: PresentationData, present: (ViewController, Any?) -> Void, openSettings: () -> Void), isMediaPlaying: @escaping () -> Bool, resumeMediaPlayback: @escaping () -> Void, audioSession: ManagedAudioSession, activeAccounts: Signal<[Account], NoError>) {
|
||||
public init(accountManager: AccountManager, enableVideoCalls: Bool, getDeviceAccessData: @escaping () -> (presentationData: PresentationData, present: (ViewController, Any?) -> Void, openSettings: () -> Void), isMediaPlaying: @escaping () -> Bool, resumeMediaPlayback: @escaping () -> Void, audioSession: ManagedAudioSession, activeAccounts: Signal<[Account], NoError>) {
|
||||
self.getDeviceAccessData = getDeviceAccessData
|
||||
self.accountManager = accountManager
|
||||
self.audioSession = audioSession
|
||||
|
|
@ -87,15 +128,15 @@ public final class PresentationCallManagerImpl: PresentationCallManager {
|
|||
self.isMediaPlaying = isMediaPlaying
|
||||
self.resumeMediaPlayback = resumeMediaPlayback
|
||||
|
||||
var startCallImpl: ((Account, UUID, String) -> Signal<Bool, NoError>)?
|
||||
var startCallImpl: ((Account, UUID, String, Bool) -> Signal<Bool, NoError>)?
|
||||
var answerCallImpl: ((UUID) -> Void)?
|
||||
var endCallImpl: ((UUID) -> Signal<Bool, NoError>)?
|
||||
var setCallMutedImpl: ((UUID, Bool) -> Void)?
|
||||
var audioSessionActivationChangedImpl: ((Bool) -> Void)?
|
||||
|
||||
self.callKitIntegration = CallKitIntegration(startCall: { account, uuid, handle in
|
||||
self.callKitIntegration = CallKitIntegration(enableVideoCalls: enableVideoCalls, startCall: { account, uuid, handle, isVideo in
|
||||
if let startCallImpl = startCallImpl {
|
||||
return startCallImpl(account, uuid, handle)
|
||||
return startCallImpl(account, uuid, handle, isVideo)
|
||||
} else {
|
||||
return .single(false)
|
||||
}
|
||||
|
|
@ -169,9 +210,9 @@ public final class PresentationCallManagerImpl: PresentationCallManager {
|
|||
self?.ringingStatesUpdated(ringingStates, enableCallKit: enableCallKit)
|
||||
})
|
||||
|
||||
startCallImpl = { [weak self] account, uuid, handle in
|
||||
startCallImpl = { [weak self] account, uuid, handle, isVideo in
|
||||
if let strongSelf = self, let userId = Int32(handle) {
|
||||
return strongSelf.startCall(account: account, peerId: PeerId(namespace: Namespaces.Peer.CloudUser, id: userId), internalId: uuid)
|
||||
return strongSelf.startCall(account: account, peerId: PeerId(namespace: Namespaces.Peer.CloudUser, id: userId), isVideo: isVideo, internalId: uuid)
|
||||
|> take(1)
|
||||
|> map { result -> Bool in
|
||||
return result
|
||||
|
|
@ -245,7 +286,7 @@ public final class PresentationCallManagerImpl: PresentationCallManager {
|
|||
let semaphore = DispatchSemaphore(value: 0)
|
||||
var data: (PreferencesView, AccountSharedDataView, Peer?)?
|
||||
let _ = combineLatest(
|
||||
account.postbox.preferencesView(keys: [PreferencesKeys.voipConfiguration, ApplicationSpecificPreferencesKeys.voipDerivedState])
|
||||
account.postbox.preferencesView(keys: [PreferencesKeys.voipConfiguration, ApplicationSpecificPreferencesKeys.voipDerivedState, PreferencesKeys.appConfiguration])
|
||||
|> take(1),
|
||||
accountManager.sharedData(keys: [SharedDataKeys.autodownloadSettings])
|
||||
|> take(1),
|
||||
|
|
@ -260,12 +301,13 @@ public final class PresentationCallManagerImpl: PresentationCallManager {
|
|||
|
||||
if let (preferences, sharedData, maybePeer) = data, let peer = maybePeer {
|
||||
let configuration = preferences.values[PreferencesKeys.voipConfiguration] as? VoipConfiguration ?? .defaultValue
|
||||
let appConfiguration = preferences.values[PreferencesKeys.appConfiguration] as? AppConfiguration ?? AppConfiguration.defaultValue
|
||||
let derivedState = preferences.values[ApplicationSpecificPreferencesKeys.voipDerivedState] as? VoipDerivedState ?? .default
|
||||
let autodownloadSettings = sharedData.entries[SharedDataKeys.autodownloadSettings] as? AutodownloadSettings ?? .defaultSettings
|
||||
|
||||
let enableCallKit = true
|
||||
|
||||
let call = PresentationCallImpl(account: account, audioSession: self.audioSession, callSessionManager: account.callSessionManager, callKitIntegration: enableCallKit ? callKitIntegrationIfEnabled(self.callKitIntegration, settings: self.callSettings) : nil, serializedData: configuration.serializedData, dataSaving: effectiveDataSaving(for: self.callSettings, autodownloadSettings: autodownloadSettings), derivedState: derivedState, getDeviceAccessData: self.getDeviceAccessData, initialState: callSession, internalId: ringingState.id, peerId: ringingState.peerId, isOutgoing: false, peer: peer, proxyServer: self.proxyServer, currentNetworkType: .none, updatedNetworkType: account.networkType)
|
||||
let call = PresentationCallImpl(account: account, audioSession: self.audioSession, callSessionManager: account.callSessionManager, callKitIntegration: enableCallKit ? callKitIntegrationIfEnabled(self.callKitIntegration, settings: self.callSettings) : nil, serializedData: configuration.serializedData, dataSaving: effectiveDataSaving(for: self.callSettings, autodownloadSettings: autodownloadSettings), derivedState: derivedState, getDeviceAccessData: self.getDeviceAccessData, initialState: callSession, internalId: ringingState.id, peerId: ringingState.peerId, isOutgoing: false, peer: peer, proxyServer: self.proxyServer, auxiliaryServers: auxiliaryServers(appConfiguration: appConfiguration), currentNetworkType: .none, updatedNetworkType: account.networkType)
|
||||
self.updateCurrentCall(call)
|
||||
self.currentCallPromise.set(.single(call))
|
||||
self.hasActiveCallsPromise.set(true)
|
||||
|
|
@ -285,7 +327,7 @@ public final class PresentationCallManagerImpl: PresentationCallManager {
|
|||
private func ringingStatesUpdated(_ ringingStates: [(Account, Peer, CallSessionRingingState, Bool, NetworkType)], enableCallKit: Bool) {
|
||||
if let firstState = ringingStates.first {
|
||||
if self.currentCall == nil {
|
||||
self.currentCallDisposable.set((combineLatest(firstState.0.postbox.preferencesView(keys: [PreferencesKeys.voipConfiguration, ApplicationSpecificPreferencesKeys.voipDerivedState]) |> take(1), accountManager.sharedData(keys: [SharedDataKeys.autodownloadSettings]) |> take(1))
|
||||
self.currentCallDisposable.set((combineLatest(firstState.0.postbox.preferencesView(keys: [PreferencesKeys.voipConfiguration, ApplicationSpecificPreferencesKeys.voipDerivedState, PreferencesKeys.appConfiguration]) |> take(1), accountManager.sharedData(keys: [SharedDataKeys.autodownloadSettings]) |> take(1))
|
||||
|> deliverOnMainQueue).start(next: { [weak self] preferences, sharedData in
|
||||
guard let strongSelf = self else {
|
||||
return
|
||||
|
|
@ -294,8 +336,9 @@ public final class PresentationCallManagerImpl: PresentationCallManager {
|
|||
let configuration = preferences.values[PreferencesKeys.voipConfiguration] as? VoipConfiguration ?? .defaultValue
|
||||
let derivedState = preferences.values[ApplicationSpecificPreferencesKeys.voipDerivedState] as? VoipDerivedState ?? .default
|
||||
let autodownloadSettings = sharedData.entries[SharedDataKeys.autodownloadSettings] as? AutodownloadSettings ?? .defaultSettings
|
||||
let appConfiguration = preferences.values[PreferencesKeys.appConfiguration] as? AppConfiguration ?? AppConfiguration.defaultValue
|
||||
|
||||
let call = PresentationCallImpl(account: firstState.0, audioSession: strongSelf.audioSession, callSessionManager: firstState.0.callSessionManager, callKitIntegration: enableCallKit ? callKitIntegrationIfEnabled(strongSelf.callKitIntegration, settings: strongSelf.callSettings) : nil, serializedData: configuration.serializedData, dataSaving: effectiveDataSaving(for: strongSelf.callSettings, autodownloadSettings: autodownloadSettings), derivedState: derivedState, getDeviceAccessData: strongSelf.getDeviceAccessData, initialState: nil, internalId: firstState.2.id, peerId: firstState.2.peerId, isOutgoing: false, peer: firstState.1, proxyServer: strongSelf.proxyServer, currentNetworkType: firstState.4, updatedNetworkType: firstState.0.networkType)
|
||||
let call = PresentationCallImpl(account: firstState.0, audioSession: strongSelf.audioSession, callSessionManager: firstState.0.callSessionManager, callKitIntegration: enableCallKit ? callKitIntegrationIfEnabled(strongSelf.callKitIntegration, settings: strongSelf.callSettings) : nil, serializedData: configuration.serializedData, dataSaving: effectiveDataSaving(for: strongSelf.callSettings, autodownloadSettings: autodownloadSettings), derivedState: derivedState, getDeviceAccessData: strongSelf.getDeviceAccessData, initialState: nil, internalId: firstState.2.id, peerId: firstState.2.peerId, isOutgoing: false, peer: firstState.1, proxyServer: strongSelf.proxyServer, auxiliaryServers: auxiliaryServers(appConfiguration: appConfiguration), currentNetworkType: firstState.4, updatedNetworkType: firstState.0.networkType)
|
||||
strongSelf.updateCurrentCall(call)
|
||||
strongSelf.currentCallPromise.set(.single(call))
|
||||
strongSelf.hasActiveCallsPromise.set(true)
|
||||
|
|
@ -320,7 +363,7 @@ public final class PresentationCallManagerImpl: PresentationCallManager {
|
|||
}
|
||||
}
|
||||
|
||||
public func requestCall(account: Account, peerId: PeerId, endCurrentIfAny: Bool) -> RequestCallResult {
|
||||
public func requestCall(account: Account, peerId: PeerId, isVideo: Bool, endCurrentIfAny: Bool) -> RequestCallResult {
|
||||
if let call = self.currentCall, !endCurrentIfAny {
|
||||
return .alreadyInProgress(call.peerId)
|
||||
}
|
||||
|
|
@ -337,8 +380,19 @@ public final class PresentationCallManagerImpl: PresentationCallManager {
|
|||
}, openSettings: {
|
||||
openSettings()
|
||||
}, { value in
|
||||
subscriber.putNext(value)
|
||||
subscriber.putCompletion()
|
||||
if isVideo {
|
||||
DeviceAccess.authorizeAccess(to: .camera, presentationData: presentationData, present: { c, a in
|
||||
present(c, a)
|
||||
}, openSettings: {
|
||||
openSettings()
|
||||
}, { value in
|
||||
subscriber.putNext(value)
|
||||
subscriber.putCompletion()
|
||||
})
|
||||
} else {
|
||||
subscriber.putNext(value)
|
||||
subscriber.putCompletion()
|
||||
}
|
||||
})
|
||||
return EmptyDisposable
|
||||
}
|
||||
|
|
@ -357,7 +411,7 @@ public final class PresentationCallManagerImpl: PresentationCallManager {
|
|||
guard let strongSelf = self, let peer = peer else {
|
||||
return
|
||||
}
|
||||
strongSelf.callKitIntegration?.startCall(account: account, peerId: peerId, displayTitle: peer.debugDisplayTitle)
|
||||
strongSelf.callKitIntegration?.startCall(account: account, peerId: peerId, isVideo: isVideo, displayTitle: peer.debugDisplayTitle)
|
||||
}))
|
||||
}
|
||||
if let currentCall = self.currentCall {
|
||||
|
|
@ -374,7 +428,7 @@ public final class PresentationCallManagerImpl: PresentationCallManager {
|
|||
guard let strongSelf = self else {
|
||||
return
|
||||
}
|
||||
let _ = strongSelf.startCall(account: account, peerId: peerId).start()
|
||||
let _ = strongSelf.startCall(account: account, peerId: peerId, isVideo: isVideo).start()
|
||||
}
|
||||
if let currentCall = self.currentCall {
|
||||
self.startCallDisposable.set((currentCall.hangUp()
|
||||
|
|
@ -388,7 +442,7 @@ public final class PresentationCallManagerImpl: PresentationCallManager {
|
|||
return .requested
|
||||
}
|
||||
|
||||
private func startCall(account: Account, peerId: PeerId, internalId: CallSessionInternalId = CallSessionInternalId()) -> Signal<Bool, NoError> {
|
||||
private func startCall(account: Account, peerId: PeerId, isVideo: Bool, internalId: CallSessionInternalId = CallSessionInternalId()) -> Signal<Bool, NoError> {
|
||||
let (presentationData, present, openSettings) = self.getDeviceAccessData()
|
||||
|
||||
let accessEnabledSignal: Signal<Bool, NoError> = Signal { subscriber in
|
||||
|
|
@ -397,8 +451,19 @@ public final class PresentationCallManagerImpl: PresentationCallManager {
|
|||
}, openSettings: {
|
||||
openSettings()
|
||||
}, { value in
|
||||
subscriber.putNext(value)
|
||||
subscriber.putCompletion()
|
||||
if isVideo {
|
||||
DeviceAccess.authorizeAccess(to: .camera, presentationData: presentationData, present: { c, a in
|
||||
present(c, a)
|
||||
}, openSettings: {
|
||||
openSettings()
|
||||
}, { value in
|
||||
subscriber.putNext(value)
|
||||
subscriber.putCompletion()
|
||||
})
|
||||
} else {
|
||||
subscriber.putNext(value)
|
||||
subscriber.putCompletion()
|
||||
}
|
||||
})
|
||||
return EmptyDisposable
|
||||
}
|
||||
|
|
@ -411,9 +476,9 @@ public final class PresentationCallManagerImpl: PresentationCallManager {
|
|||
if !accessEnabled {
|
||||
return .single(false)
|
||||
}
|
||||
return (combineLatest(queue: .mainQueue(), account.callSessionManager.request(peerId: peerId, internalId: internalId), networkType |> take(1), account.postbox.peerView(id: peerId) |> map { peerView -> Bool in
|
||||
return (combineLatest(queue: .mainQueue(), account.callSessionManager.request(peerId: peerId, isVideo: isVideo, internalId: internalId), networkType |> take(1), account.postbox.peerView(id: peerId) |> map { peerView -> Bool in
|
||||
return peerView.peerIsContact
|
||||
} |> take(1), account.postbox.preferencesView(keys: [PreferencesKeys.voipConfiguration, ApplicationSpecificPreferencesKeys.voipDerivedState]) |> take(1), accountManager.sharedData(keys: [SharedDataKeys.autodownloadSettings]) |> take(1))
|
||||
} |> take(1), account.postbox.preferencesView(keys: [PreferencesKeys.voipConfiguration, ApplicationSpecificPreferencesKeys.voipDerivedState, PreferencesKeys.appConfiguration]) |> take(1), accountManager.sharedData(keys: [SharedDataKeys.autodownloadSettings]) |> take(1))
|
||||
|> deliverOnMainQueue
|
||||
|> beforeNext { internalId, currentNetworkType, isContact, preferences, sharedData in
|
||||
if let strongSelf = self, accessEnabled {
|
||||
|
|
@ -424,8 +489,9 @@ public final class PresentationCallManagerImpl: PresentationCallManager {
|
|||
let configuration = preferences.values[PreferencesKeys.voipConfiguration] as? VoipConfiguration ?? .defaultValue
|
||||
let derivedState = preferences.values[ApplicationSpecificPreferencesKeys.voipDerivedState] as? VoipDerivedState ?? .default
|
||||
let autodownloadSettings = sharedData.entries[SharedDataKeys.autodownloadSettings] as? AutodownloadSettings ?? .defaultSettings
|
||||
let appConfiguration = preferences.values[PreferencesKeys.appConfiguration] as? AppConfiguration ?? AppConfiguration.defaultValue
|
||||
|
||||
let call = PresentationCallImpl(account: account, audioSession: strongSelf.audioSession, callSessionManager: account.callSessionManager, callKitIntegration: callKitIntegrationIfEnabled(strongSelf.callKitIntegration, settings: strongSelf.callSettings), serializedData: configuration.serializedData, dataSaving: effectiveDataSaving(for: strongSelf.callSettings, autodownloadSettings: autodownloadSettings), derivedState: derivedState, getDeviceAccessData: strongSelf.getDeviceAccessData, initialState: nil, internalId: internalId, peerId: peerId, isOutgoing: true, peer: nil, proxyServer: strongSelf.proxyServer, currentNetworkType: currentNetworkType, updatedNetworkType: account.networkType)
|
||||
let call = PresentationCallImpl(account: account, audioSession: strongSelf.audioSession, callSessionManager: account.callSessionManager, callKitIntegration: callKitIntegrationIfEnabled(strongSelf.callKitIntegration, settings: strongSelf.callSettings), serializedData: configuration.serializedData, dataSaving: effectiveDataSaving(for: strongSelf.callSettings, autodownloadSettings: autodownloadSettings), derivedState: derivedState, getDeviceAccessData: strongSelf.getDeviceAccessData, initialState: nil, internalId: internalId, peerId: peerId, isOutgoing: true, peer: nil, proxyServer: strongSelf.proxyServer, auxiliaryServers: auxiliaryServers(appConfiguration: appConfiguration), currentNetworkType: currentNetworkType, updatedNetworkType: account.networkType)
|
||||
strongSelf.updateCurrentCall(call)
|
||||
strongSelf.currentCallPromise.set(.single(call))
|
||||
strongSelf.hasActiveCallsPromise.set(true)
|
||||
|
|
|
|||
|
|
@ -1322,7 +1322,7 @@ public final class AccountViewTracker {
|
|||
var lhsOther = false
|
||||
inner: for media in lhs.media {
|
||||
if let action = media as? TelegramMediaAction {
|
||||
if case let .phoneCall(_, discardReason, _) = action.action {
|
||||
if case let .phoneCall(_, discardReason, _, _) = action.action {
|
||||
if lhs.flags.contains(.Incoming), let discardReason = discardReason, case .missed = discardReason {
|
||||
lhsMissed = true
|
||||
} else {
|
||||
|
|
@ -1336,7 +1336,7 @@ public final class AccountViewTracker {
|
|||
var rhsOther = false
|
||||
inner: for media in rhs.media {
|
||||
if let action = media as? TelegramMediaAction {
|
||||
if case let .phoneCall(_, discardReason, _) = action.action {
|
||||
if case let .phoneCall(_, discardReason, _, _) = action.action {
|
||||
if rhs.flags.contains(.Incoming), let discardReason = discardReason, case .missed = discardReason {
|
||||
rhsMissed = true
|
||||
} else {
|
||||
|
|
|
|||
|
|
@ -8,6 +8,8 @@ import SyncCore
|
|||
private func copyOrMoveResourceData(from fromResource: MediaResource, to toResource: MediaResource, mediaBox: MediaBox) {
|
||||
if fromResource is CloudFileMediaResource || fromResource is CloudDocumentMediaResource || fromResource is SecretFileMediaResource {
|
||||
mediaBox.copyResourceData(from: fromResource.id, to: toResource.id)
|
||||
} else if let fromResource = fromResource as? LocalFileMediaResource, fromResource.isSecretRelated {
|
||||
mediaBox.copyResourceData(from: fromResource.id, to: toResource.id)
|
||||
} else {
|
||||
mediaBox.moveResourceData(from: fromResource.id, to: toResource.id)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -179,8 +179,14 @@ public enum CallSessionState {
|
|||
}
|
||||
|
||||
public struct CallSession {
|
||||
public enum CallType {
|
||||
case audio
|
||||
case video
|
||||
}
|
||||
|
||||
public let id: CallSessionInternalId
|
||||
public let isOutgoing: Bool
|
||||
public let type: CallType
|
||||
public let state: CallSessionState
|
||||
}
|
||||
|
||||
|
|
@ -211,6 +217,7 @@ private func parseConnectionSet(primary: Api.PhoneConnection, alternative: [Api.
|
|||
private final class CallSessionContext {
|
||||
let peerId: PeerId
|
||||
let isOutgoing: Bool
|
||||
let type: CallSession.CallType
|
||||
var state: CallSessionInternalState
|
||||
let subscribers = Bag<(CallSession) -> Void>()
|
||||
let signalingSubscribers = Bag<(Data) -> Void>()
|
||||
|
|
@ -227,9 +234,10 @@ private final class CallSessionContext {
|
|||
}
|
||||
}
|
||||
|
||||
init(peerId: PeerId, isOutgoing: Bool, state: CallSessionInternalState) {
|
||||
init(peerId: PeerId, isOutgoing: Bool, type: CallSession.CallType, state: CallSessionInternalState) {
|
||||
self.peerId = peerId
|
||||
self.isOutgoing = isOutgoing
|
||||
self.type = type
|
||||
self.state = state
|
||||
}
|
||||
|
||||
|
|
@ -311,7 +319,7 @@ private final class CallSessionManagerContext {
|
|||
let index = context.subscribers.add { next in
|
||||
subscriber.putNext(next)
|
||||
}
|
||||
subscriber.putNext(CallSession(id: internalId, isOutgoing: context.isOutgoing, state: CallSessionState(context)))
|
||||
subscriber.putNext(CallSession(id: internalId, isOutgoing: context.isOutgoing, type: context.type, state: CallSessionState(context)))
|
||||
disposable.set(ActionDisposable {
|
||||
queue.async {
|
||||
if let strongSelf = self, let context = strongSelf.contexts[internalId] {
|
||||
|
|
@ -372,14 +380,14 @@ private final class CallSessionManagerContext {
|
|||
|
||||
private func contextUpdated(internalId: CallSessionInternalId) {
|
||||
if let context = self.contexts[internalId] {
|
||||
let session = CallSession(id: internalId, isOutgoing: context.isOutgoing, state: CallSessionState(context))
|
||||
let session = CallSession(id: internalId, isOutgoing: context.isOutgoing, type: context.type, state: CallSessionState(context))
|
||||
for subscriber in context.subscribers.copyItems() {
|
||||
subscriber(session)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func addIncoming(peerId: PeerId, stableId: CallSessionStableId, accessHash: Int64, timestamp: Int32, gAHash: Data, versions: [String]) -> CallSessionInternalId? {
|
||||
private func addIncoming(peerId: PeerId, stableId: CallSessionStableId, accessHash: Int64, timestamp: Int32, gAHash: Data, versions: [String], isVideo: Bool) -> CallSessionInternalId? {
|
||||
if self.contextIdByStableId[stableId] != nil {
|
||||
return nil
|
||||
}
|
||||
|
|
@ -390,7 +398,7 @@ private final class CallSessionManagerContext {
|
|||
|
||||
if randomStatus == 0 {
|
||||
let internalId = CallSessionInternalId()
|
||||
let context = CallSessionContext(peerId: peerId, isOutgoing: false, state: .ringing(id: stableId, accessHash: accessHash, gAHash: gAHash, b: b, versions: versions))
|
||||
let context = CallSessionContext(peerId: peerId, isOutgoing: false, type: isVideo ? .video : .audio, state: .ringing(id: stableId, accessHash: accessHash, gAHash: gAHash, b: b, versions: versions))
|
||||
self.contexts[internalId] = context
|
||||
let queue = self.queue
|
||||
context.acknowledgeIncomingCallDisposable.set(self.network.request(Api.functions.phone.receivedCall(peer: .inputPhoneCall(id: stableId, accessHash: accessHash))).start(error: { [weak self] _ in
|
||||
|
|
@ -414,6 +422,7 @@ private final class CallSessionManagerContext {
|
|||
if let context = self.contexts[internalId] {
|
||||
var dropData: (CallSessionStableId, Int64, DropCallSessionReason)?
|
||||
var wasRinging = false
|
||||
let isVideo = context.type == .video
|
||||
switch context.state {
|
||||
case let .ringing(id, accessHash, _, _, _):
|
||||
wasRinging = true
|
||||
|
|
@ -471,7 +480,7 @@ private final class CallSessionManagerContext {
|
|||
|
||||
if let (id, accessHash, reason) = dropData {
|
||||
self.contextIdByStableId.removeValue(forKey: id)
|
||||
context.state = .dropping((dropCallSession(network: self.network, addUpdates: self.addUpdates, stableId: id, accessHash: accessHash, reason: reason)
|
||||
context.state = .dropping((dropCallSession(network: self.network, addUpdates: self.addUpdates, stableId: id, accessHash: accessHash, isVideo: isVideo, reason: reason)
|
||||
|> deliverOn(self.queue)).start(next: { [weak self] reportRating, sendDebugLogs in
|
||||
if let strongSelf = self {
|
||||
if let context = strongSelf.contexts[internalId] {
|
||||
|
|
@ -722,13 +731,14 @@ private final class CallSessionManagerContext {
|
|||
}
|
||||
}
|
||||
case let .phoneCallRequested(flags, id, accessHash, date, adminId, _, gAHash, requestedProtocol):
|
||||
let isVideo = (flags & (1 << 5)) != 0
|
||||
let versions: [String]
|
||||
switch requestedProtocol {
|
||||
case let .phoneCallProtocol(_, _, _, libraryVersions):
|
||||
versions = libraryVersions
|
||||
}
|
||||
if self.contextIdByStableId[id] == nil {
|
||||
let internalId = self.addIncoming(peerId: PeerId(namespace: Namespaces.Peer.CloudUser, id: adminId), stableId: id, accessHash: accessHash, timestamp: date, gAHash: gAHash.makeData(), versions: versions)
|
||||
let internalId = self.addIncoming(peerId: PeerId(namespace: Namespaces.Peer.CloudUser, id: adminId), stableId: id, accessHash: accessHash, timestamp: date, gAHash: gAHash.makeData(), versions: versions, isVideo: isVideo)
|
||||
if let internalId = internalId {
|
||||
var resultRingingStateValue: CallSessionRingingState?
|
||||
for ringingState in self.ringingStatesValue() {
|
||||
|
|
@ -738,7 +748,7 @@ private final class CallSessionManagerContext {
|
|||
}
|
||||
}
|
||||
if let context = self.contexts[internalId] {
|
||||
let callSession = CallSession(id: internalId, isOutgoing: context.isOutgoing, state: CallSessionState(context))
|
||||
let callSession = CallSession(id: internalId, isOutgoing: context.isOutgoing, type: context.type, state: CallSessionState(context))
|
||||
if let resultRingingStateValue = resultRingingStateValue {
|
||||
resultRingingState = (resultRingingStateValue, callSession)
|
||||
}
|
||||
|
|
@ -802,12 +812,12 @@ private final class CallSessionManagerContext {
|
|||
return (key, keyId, keyVisualHash)
|
||||
}
|
||||
|
||||
func request(peerId: PeerId, internalId: CallSessionInternalId) -> CallSessionInternalId? {
|
||||
func request(peerId: PeerId, internalId: CallSessionInternalId, isVideo: Bool) -> CallSessionInternalId? {
|
||||
let aBytes = malloc(256)!
|
||||
let randomStatus = SecRandomCopyBytes(nil, 256, aBytes.assumingMemoryBound(to: UInt8.self))
|
||||
let a = Data(bytesNoCopy: aBytes, count: 256, deallocator: .free)
|
||||
if randomStatus == 0 {
|
||||
self.contexts[internalId] = CallSessionContext(peerId: peerId, isOutgoing: true, state: .requesting(a: a, disposable: (requestCallSession(postbox: self.postbox, network: self.network, peerId: peerId, a: a, maxLayer: self.maxLayer, versions: self.versions) |> deliverOn(queue)).start(next: { [weak self] result in
|
||||
self.contexts[internalId] = CallSessionContext(peerId: peerId, isOutgoing: true, type: isVideo ? .video : .audio, state: .requesting(a: a, disposable: (requestCallSession(postbox: self.postbox, network: self.network, peerId: peerId, a: a, maxLayer: self.maxLayer, versions: self.versions, isVideo: isVideo) |> deliverOn(queue)).start(next: { [weak self] result in
|
||||
if let strongSelf = self, let context = strongSelf.contexts[internalId] {
|
||||
if case .requesting = context.state {
|
||||
switch result {
|
||||
|
|
@ -900,12 +910,12 @@ public final class CallSessionManager {
|
|||
}
|
||||
}
|
||||
|
||||
public func request(peerId: PeerId, internalId: CallSessionInternalId = CallSessionInternalId()) -> Signal<CallSessionInternalId, NoError> {
|
||||
public func request(peerId: PeerId, isVideo: Bool, internalId: CallSessionInternalId = CallSessionInternalId()) -> Signal<CallSessionInternalId, NoError> {
|
||||
return Signal { [weak self] subscriber in
|
||||
let disposable = MetaDisposable()
|
||||
|
||||
self?.withContext { context in
|
||||
if let internalId = context.request(peerId: peerId, internalId: internalId) {
|
||||
if let internalId = context.request(peerId: peerId, internalId: internalId, isVideo: isVideo) {
|
||||
subscriber.putNext(internalId)
|
||||
subscriber.putCompletion()
|
||||
}
|
||||
|
|
@ -1040,7 +1050,7 @@ private enum RequestCallSessionResult {
|
|||
case failed(CallSessionError)
|
||||
}
|
||||
|
||||
private func requestCallSession(postbox: Postbox, network: Network, peerId: PeerId, a: Data, maxLayer: Int32, versions: [String]) -> Signal<RequestCallSessionResult, NoError> {
|
||||
private func requestCallSession(postbox: Postbox, network: Network, peerId: PeerId, a: Data, maxLayer: Int32, versions: [String], isVideo: Bool) -> Signal<RequestCallSessionResult, NoError> {
|
||||
return validatedEncryptionConfig(postbox: postbox, network: network)
|
||||
|> mapToSignal { config -> Signal<RequestCallSessionResult, NoError> in
|
||||
return postbox.transaction { transaction -> Signal<RequestCallSessionResult, NoError> in
|
||||
|
|
@ -1056,12 +1066,17 @@ private func requestCallSession(postbox: Postbox, network: Network, peerId: Peer
|
|||
|
||||
let gAHash = MTSha256(ga)!
|
||||
|
||||
return network.request(Api.functions.phone.requestCall(flags: 0, userId: inputUser, randomId: Int32(bitPattern: arc4random()), gAHash: Buffer(data: gAHash), protocol: .phoneCallProtocol(flags: (1 << 0) | (1 << 1), minLayer: minLayer, maxLayer: maxLayer, libraryVersions: versions)))
|
||||
var callFlags: Int32 = 0
|
||||
if isVideo {
|
||||
callFlags |= 1 << 0
|
||||
}
|
||||
|
||||
return network.request(Api.functions.phone.requestCall(flags: callFlags, userId: inputUser, randomId: Int32(bitPattern: arc4random()), gAHash: Buffer(data: gAHash), protocol: .phoneCallProtocol(flags: (1 << 0) | (1 << 1), minLayer: minLayer, maxLayer: maxLayer, libraryVersions: versions)))
|
||||
|> map { result -> RequestCallSessionResult in
|
||||
switch result {
|
||||
case let .phoneCall(phoneCall, _):
|
||||
switch phoneCall {
|
||||
case let .phoneCallRequested(flags, id, accessHash, _, _, _, _, _):
|
||||
case let .phoneCallRequested(_, id, accessHash, _, _, _, _, _):
|
||||
return .success(id: id, accessHash: accessHash, config: config, gA: ga, remoteConfirmationTimestamp: nil)
|
||||
case let .phoneCallWaiting(_, id, accessHash, _, _, _, _, receiveDate):
|
||||
return .success(id: id, accessHash: accessHash, config: config, gA: ga, remoteConfirmationTimestamp: receiveDate)
|
||||
|
|
@ -1118,7 +1133,7 @@ private enum DropCallSessionReason {
|
|||
case missed
|
||||
}
|
||||
|
||||
private func dropCallSession(network: Network, addUpdates: @escaping (Api.Updates) -> Void, stableId: CallSessionStableId, accessHash: Int64, reason: DropCallSessionReason) -> Signal<(Bool, Bool), NoError> {
|
||||
private func dropCallSession(network: Network, addUpdates: @escaping (Api.Updates) -> Void, stableId: CallSessionStableId, accessHash: Int64, isVideo: Bool, reason: DropCallSessionReason) -> Signal<(Bool, Bool), NoError> {
|
||||
var mappedReason: Api.PhoneCallDiscardReason
|
||||
var duration: Int32 = 0
|
||||
switch reason {
|
||||
|
|
@ -1134,7 +1149,13 @@ private func dropCallSession(network: Network, addUpdates: @escaping (Api.Update
|
|||
case .missed:
|
||||
mappedReason = .phoneCallDiscardReasonMissed
|
||||
}
|
||||
return network.request(Api.functions.phone.discardCall(flags: 0, peer: Api.InputPhoneCall.inputPhoneCall(id: stableId, accessHash: accessHash), duration: duration, reason: mappedReason, connectionId: 0))
|
||||
|
||||
var callFlags: Int32 = 0
|
||||
if isVideo {
|
||||
callFlags |= 1 << 0
|
||||
}
|
||||
|
||||
return network.request(Api.functions.phone.discardCall(flags: callFlags, peer: Api.InputPhoneCall.inputPhoneCall(id: stableId, accessHash: accessHash), duration: duration, reason: mappedReason, connectionId: 0))
|
||||
|> map(Optional.init)
|
||||
|> `catch` { _ -> Signal<Api.Updates?, NoError> in
|
||||
return .single(nil)
|
||||
|
|
|
|||
|
|
@ -69,7 +69,7 @@ private func filterMessageAttributesForOutgoingMessage(_ attributes: [MessageAtt
|
|||
case _ as InlineBotMessageAttribute:
|
||||
return true
|
||||
case _ as OutgoingMessageInfoAttribute:
|
||||
return true
|
||||
return false
|
||||
case _ as OutgoingContentInfoMessageAttribute:
|
||||
return true
|
||||
case _ as ReplyMarkupMessageAttribute:
|
||||
|
|
|
|||
|
|
@ -27,6 +27,9 @@ extension PeerStatusSettings {
|
|||
if (flags & (1 << 5)) != 0 {
|
||||
result.insert(.canReportIrrelevantGeoLocation)
|
||||
}
|
||||
if (flags & (1 << 7)) != 0 {
|
||||
result.insert(.autoArchived)
|
||||
}
|
||||
self = PeerStatusSettings(flags: result, geoDistance: geoDistance)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -198,10 +198,10 @@ private func requestChannelStats(postbox: Postbox, network: Network, datacenterI
|
|||
signal = network.download(datacenterId: Int(datacenterId), isMedia: false, tag: nil)
|
||||
|> castError(MTRpcError.self)
|
||||
|> mapToSignal { worker in
|
||||
return worker.request(Api.functions.stats.getBroadcastStats(flags: flags, channel: inputChannel))
|
||||
return worker.request(Api.functions.stats.getBroadcastStats(flags: flags, channel: inputChannel, tzOffset: 0))
|
||||
}
|
||||
} else {
|
||||
signal = network.request(Api.functions.stats.getBroadcastStats(flags: flags, channel: inputChannel))
|
||||
signal = network.request(Api.functions.stats.getBroadcastStats(flags: flags, channel: inputChannel, tzOffset: 0))
|
||||
}
|
||||
|
||||
return signal
|
||||
|
|
|
|||
|
|
@ -662,7 +662,7 @@ private func uploadedMediaFileContent(network: Network, postbox: Postbox, auxili
|
|||
return .single(.pending)
|
||||
case let .done(media):
|
||||
if let media = media as? TelegramMediaFile, let smallestThumbnail = smallestImageRepresentation(media.previewRepresentations) {
|
||||
if peerId.namespace == Namespaces.Peer.SecretChat || (smallestThumbnail.resource is LocalFileMediaResource) {
|
||||
if peerId.namespace == Namespaces.Peer.SecretChat {
|
||||
return .single(.done(media, .none))
|
||||
} else {
|
||||
let fileReference: AnyMediaReference
|
||||
|
|
|
|||
|
|
@ -95,9 +95,10 @@ public struct AccountPrivacySettings: Equatable {
|
|||
public let phoneNumber: SelectivePrivacySettings
|
||||
public let phoneDiscoveryEnabled: Bool
|
||||
|
||||
public let automaticallyArchiveAndMuteNonContacts: Bool
|
||||
public let accountRemovalTimeout: Int32
|
||||
|
||||
public init(presence: SelectivePrivacySettings, groupInvitations: SelectivePrivacySettings, voiceCalls: SelectivePrivacySettings, voiceCallsP2P: SelectivePrivacySettings, profilePhoto: SelectivePrivacySettings, forwards: SelectivePrivacySettings, phoneNumber: SelectivePrivacySettings, phoneDiscoveryEnabled: Bool, accountRemovalTimeout: Int32) {
|
||||
public init(presence: SelectivePrivacySettings, groupInvitations: SelectivePrivacySettings, voiceCalls: SelectivePrivacySettings, voiceCallsP2P: SelectivePrivacySettings, profilePhoto: SelectivePrivacySettings, forwards: SelectivePrivacySettings, phoneNumber: SelectivePrivacySettings, phoneDiscoveryEnabled: Bool, automaticallyArchiveAndMuteNonContacts: Bool, accountRemovalTimeout: Int32) {
|
||||
self.presence = presence
|
||||
self.groupInvitations = groupInvitations
|
||||
self.voiceCalls = voiceCalls
|
||||
|
|
@ -106,6 +107,7 @@ public struct AccountPrivacySettings: Equatable {
|
|||
self.forwards = forwards
|
||||
self.phoneNumber = phoneNumber
|
||||
self.phoneDiscoveryEnabled = phoneDiscoveryEnabled
|
||||
self.automaticallyArchiveAndMuteNonContacts = automaticallyArchiveAndMuteNonContacts
|
||||
self.accountRemovalTimeout = accountRemovalTimeout
|
||||
}
|
||||
|
||||
|
|
@ -134,6 +136,9 @@ public struct AccountPrivacySettings: Equatable {
|
|||
if lhs.phoneDiscoveryEnabled != rhs.phoneDiscoveryEnabled {
|
||||
return false
|
||||
}
|
||||
if lhs.automaticallyArchiveAndMuteNonContacts != rhs.automaticallyArchiveAndMuteNonContacts {
|
||||
return false
|
||||
}
|
||||
if lhs.accountRemovalTimeout != rhs.accountRemovalTimeout {
|
||||
return false
|
||||
}
|
||||
|
|
|
|||
|
|
@ -75,7 +75,7 @@ public func tagsForStoreMessage(incoming: Bool, attributes: [MessageAttribute],
|
|||
tags.insert(.webPage)
|
||||
} else if let action = attachment as? TelegramMediaAction {
|
||||
switch action.action {
|
||||
case let .phoneCall(_, discardReason, _):
|
||||
case let .phoneCall(_, discardReason, _, _):
|
||||
globalTags.insert(.Calls)
|
||||
if incoming, let discardReason = discardReason, case .missed = discardReason {
|
||||
globalTags.insert(.MissedCalls)
|
||||
|
|
|
|||
|
|
@ -32,12 +32,13 @@ func telegramMediaActionFromApiAction(_ action: Api.MessageAction) -> TelegramMe
|
|||
return TelegramMediaAction(action: .pinnedMessageUpdated)
|
||||
case let .messageActionGameScore(gameId, score):
|
||||
return TelegramMediaAction(action: .gameScore(gameId: gameId, score: score))
|
||||
case let .messageActionPhoneCall(_, callId, reason, duration):
|
||||
case let .messageActionPhoneCall(flags, callId, reason, duration):
|
||||
var discardReason: PhoneCallDiscardReason?
|
||||
if let reason = reason {
|
||||
discardReason = PhoneCallDiscardReason(apiReason: reason)
|
||||
}
|
||||
return TelegramMediaAction(action: .phoneCall(callId: callId, discardReason: discardReason, duration: duration))
|
||||
let isVideo = (flags & (1 << 2)) != 0
|
||||
return TelegramMediaAction(action: .phoneCall(callId: callId, discardReason: discardReason, duration: duration, isVideo: isVideo))
|
||||
case .messageActionEmpty:
|
||||
return nil
|
||||
case let .messageActionPaymentSent(currency, totalAmount):
|
||||
|
|
|
|||
|
|
@ -15,11 +15,12 @@ public func requestAccountPrivacySettings(account: Account) -> Signal<AccountPri
|
|||
let phoneNumberPrivacy = account.network.request(Api.functions.account.getPrivacy(key: .inputPrivacyKeyPhoneNumber))
|
||||
let phoneDiscoveryPrivacy = account.network.request(Api.functions.account.getPrivacy(key: .inputPrivacyKeyAddedByPhone))
|
||||
let autoremoveTimeout = account.network.request(Api.functions.account.getAccountTTL())
|
||||
return combineLatest(lastSeenPrivacy, groupPrivacy, voiceCallPrivacy, voiceCallP2P, profilePhotoPrivacy, forwardPrivacy, phoneNumberPrivacy, phoneDiscoveryPrivacy, autoremoveTimeout)
|
||||
let globalPrivacySettings = account.network.request(Api.functions.account.getGlobalPrivacySettings())
|
||||
return combineLatest(lastSeenPrivacy, groupPrivacy, voiceCallPrivacy, voiceCallP2P, profilePhotoPrivacy, forwardPrivacy, phoneNumberPrivacy, phoneDiscoveryPrivacy, autoremoveTimeout, globalPrivacySettings)
|
||||
|> `catch` { _ in
|
||||
return .complete()
|
||||
}
|
||||
|> mapToSignal { lastSeenPrivacy, groupPrivacy, voiceCallPrivacy, voiceCallP2P, profilePhotoPrivacy, forwardPrivacy, phoneNumberPrivacy, phoneDiscoveryPrivacy, autoremoveTimeout -> Signal<AccountPrivacySettings, NoError> in
|
||||
|> mapToSignal { lastSeenPrivacy, groupPrivacy, voiceCallPrivacy, voiceCallP2P, profilePhotoPrivacy, forwardPrivacy, phoneNumberPrivacy, phoneDiscoveryPrivacy, autoremoveTimeout, globalPrivacySettings -> Signal<AccountPrivacySettings, NoError> in
|
||||
let accountTimeoutSeconds: Int32
|
||||
switch autoremoveTimeout {
|
||||
case let .accountDaysTTL(days):
|
||||
|
|
@ -119,16 +120,35 @@ public func requestAccountPrivacySettings(account: Account) -> Signal<AccountPri
|
|||
peerMap[peer.peer.id] = peer
|
||||
}
|
||||
|
||||
let automaticallyArchiveAndMuteNonContacts: Bool
|
||||
switch globalPrivacySettings {
|
||||
case let .globalPrivacySettings(_, archiveAndMuteNewNoncontactPeers):
|
||||
if let archiveAndMuteNewNoncontactPeers = archiveAndMuteNewNoncontactPeers {
|
||||
automaticallyArchiveAndMuteNonContacts = archiveAndMuteNewNoncontactPeers == .boolTrue
|
||||
} else {
|
||||
automaticallyArchiveAndMuteNonContacts = false
|
||||
}
|
||||
}
|
||||
|
||||
return account.postbox.transaction { transaction -> AccountPrivacySettings in
|
||||
updatePeers(transaction: transaction, peers: peers.map { $0.peer }, update: { _, updated in
|
||||
return updated
|
||||
})
|
||||
|
||||
return AccountPrivacySettings(presence: SelectivePrivacySettings(apiRules: lastSeenRules, peers: peerMap), groupInvitations: SelectivePrivacySettings(apiRules: groupRules, peers: peerMap), voiceCalls: SelectivePrivacySettings(apiRules: voiceRules, peers: peerMap), voiceCallsP2P: SelectivePrivacySettings(apiRules: voiceP2PRules, peers: peerMap), profilePhoto: SelectivePrivacySettings(apiRules: profilePhotoRules, peers: peerMap), forwards: SelectivePrivacySettings(apiRules: forwardRules, peers: peerMap), phoneNumber: SelectivePrivacySettings(apiRules: phoneNumberRules, peers: peerMap), phoneDiscoveryEnabled: phoneDiscoveryValue, accountRemovalTimeout: accountTimeoutSeconds)
|
||||
return AccountPrivacySettings(presence: SelectivePrivacySettings(apiRules: lastSeenRules, peers: peerMap), groupInvitations: SelectivePrivacySettings(apiRules: groupRules, peers: peerMap), voiceCalls: SelectivePrivacySettings(apiRules: voiceRules, peers: peerMap), voiceCallsP2P: SelectivePrivacySettings(apiRules: voiceP2PRules, peers: peerMap), profilePhoto: SelectivePrivacySettings(apiRules: profilePhotoRules, peers: peerMap), forwards: SelectivePrivacySettings(apiRules: forwardRules, peers: peerMap), phoneNumber: SelectivePrivacySettings(apiRules: phoneNumberRules, peers: peerMap), phoneDiscoveryEnabled: phoneDiscoveryValue, automaticallyArchiveAndMuteNonContacts: automaticallyArchiveAndMuteNonContacts, accountRemovalTimeout: accountTimeoutSeconds)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public func updateAccountAutoArchiveChats(account: Account, value: Bool) -> Signal<Never, NoError> {
|
||||
|
||||
return account.network.request(Api.functions.account.setGlobalPrivacySettings(
|
||||
settings: .globalPrivacySettings(flags: 1 << 0, archiveAndMuteNewNoncontactPeers: value ? .boolTrue : .boolFalse)
|
||||
))
|
||||
|> retryRequest
|
||||
|> ignoreValues
|
||||
}
|
||||
|
||||
public func updateAccountRemovalTimeout(account: Account, timeout: Int32) -> Signal<Void, NoError> {
|
||||
return account.network.request(Api.functions.account.setAccountTTL(ttl: .accountDaysTTL(days: timeout / (24 * 60 * 60))))
|
||||
|> retryRequest
|
||||
|
|
|
|||
|
|
@ -45,9 +45,9 @@ public extension TabBarControllerTheme {
|
|||
}
|
||||
|
||||
public extension NavigationBarTheme {
|
||||
convenience init(rootControllerTheme: PresentationTheme) {
|
||||
convenience init(rootControllerTheme: PresentationTheme, hideBackground: Bool = false) {
|
||||
let theme = rootControllerTheme.rootController.navigationBar
|
||||
self.init(buttonColor: theme.buttonColor, disabledButtonColor: theme.disabledButtonColor, primaryTextColor: theme.primaryTextColor, backgroundColor: theme.backgroundColor, separatorColor: theme.separatorColor, badgeBackgroundColor: theme.badgeBackgroundColor, badgeStrokeColor: theme.badgeStrokeColor, badgeTextColor: theme.badgeTextColor)
|
||||
self.init(buttonColor: theme.buttonColor, disabledButtonColor: theme.disabledButtonColor, primaryTextColor: theme.primaryTextColor, backgroundColor: hideBackground ? .clear : theme.backgroundColor, separatorColor: hideBackground ? .clear : theme.separatorColor, badgeBackgroundColor: theme.badgeBackgroundColor, badgeStrokeColor: hideBackground ? .clear : theme.badgeStrokeColor, badgeTextColor: theme.badgeTextColor)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -62,6 +62,10 @@ public extension NavigationBarPresentationData {
|
|||
self.init(theme: NavigationBarTheme(rootControllerTheme: presentationData.theme), strings: NavigationBarStrings(presentationStrings: presentationData.strings))
|
||||
}
|
||||
|
||||
convenience init(presentationData: PresentationData, hideBackground: Bool) {
|
||||
self.init(theme: NavigationBarTheme(rootControllerTheme: presentationData.theme, hideBackground: hideBackground), strings: NavigationBarStrings(presentationStrings: presentationData.strings))
|
||||
}
|
||||
|
||||
convenience init(presentationTheme: PresentationTheme, presentationStrings: PresentationStrings) {
|
||||
self.init(theme: NavigationBarTheme(rootControllerTheme: presentationTheme), strings: NavigationBarStrings(presentationStrings: presentationStrings))
|
||||
}
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
|
|
@ -371,7 +371,7 @@ public func universalServiceMessageString(presentationData: (PresentationTheme,
|
|||
} else {
|
||||
attributedString = NSAttributedString(string: strings.Message_PaymentSent(formatCurrencyAmount(totalAmount, currency: currency)).0, font: titleFont, textColor: primaryTextColor)
|
||||
}
|
||||
case let .phoneCall(_, discardReason, _):
|
||||
case let .phoneCall(_, discardReason, _, _):
|
||||
var titleString: String
|
||||
let incoming: Bool
|
||||
if message.flags.contains(.Incoming) {
|
||||
|
|
|
|||
12
submodules/TelegramUI/Images.xcassets/Call/CallSwitchCameraButton.imageset/Contents.json
vendored
Normal file
12
submodules/TelegramUI/Images.xcassets/Call/CallSwitchCameraButton.imageset/Contents.json
vendored
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
{
|
||||
"images" : [
|
||||
{
|
||||
"filename" : "Video.pdf",
|
||||
"idiom" : "universal"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
BIN
submodules/TelegramUI/Images.xcassets/Call/CallSwitchCameraButton.imageset/Video.pdf
vendored
Normal file
BIN
submodules/TelegramUI/Images.xcassets/Call/CallSwitchCameraButton.imageset/Video.pdf
vendored
Normal file
Binary file not shown.
12
submodules/TelegramUI/Images.xcassets/Peer Info/ButtonVideo.imageset/Contents.json
vendored
Normal file
12
submodules/TelegramUI/Images.xcassets/Peer Info/ButtonVideo.imageset/Contents.json
vendored
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
{
|
||||
"images" : [
|
||||
{
|
||||
"filename" : "Video.pdf",
|
||||
"idiom" : "universal"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
BIN
submodules/TelegramUI/Images.xcassets/Peer Info/ButtonVideo.imageset/Video.pdf
vendored
Normal file
BIN
submodules/TelegramUI/Images.xcassets/Peer Info/ButtonVideo.imageset/Video.pdf
vendored
Normal file
Binary file not shown.
Binary file not shown.
|
|
@ -141,6 +141,14 @@ protocol SupportedStartCallIntent {
|
|||
@available(iOS 10.0, *)
|
||||
extension INStartAudioCallIntent: SupportedStartCallIntent {}
|
||||
|
||||
protocol SupportedStartVideoCallIntent {
|
||||
@available(iOS 10.0, *)
|
||||
var contacts: [INPerson]? { get }
|
||||
}
|
||||
|
||||
@available(iOS 10.0, *)
|
||||
extension INStartVideoCallIntent: SupportedStartVideoCallIntent {}
|
||||
|
||||
private enum QueuedWakeup: Int32 {
|
||||
case call
|
||||
case backgroundLocation
|
||||
|
|
@ -1725,9 +1733,19 @@ final class SharedApplicationContext {
|
|||
|
||||
func application(_ application: UIApplication, continue userActivity: NSUserActivity, restorationHandler: @escaping ([UIUserActivityRestoring]?) -> Void) -> Bool {
|
||||
if #available(iOS 10.0, *) {
|
||||
var startCallContacts: [INPerson]?
|
||||
var startCallIsVideo = false
|
||||
if let startCallIntent = userActivity.interaction?.intent as? SupportedStartCallIntent {
|
||||
startCallContacts = startCallIntent.contacts
|
||||
startCallIsVideo = false
|
||||
} else if let startCallIntent = userActivity.interaction?.intent as? SupportedStartVideoCallIntent {
|
||||
startCallContacts = startCallIntent.contacts
|
||||
startCallIsVideo = true
|
||||
}
|
||||
|
||||
if let startCallContacts = startCallContacts {
|
||||
let startCall: (Int32) -> Void = { userId in
|
||||
self.startCallWhenReady(accountId: nil, peerId: PeerId(namespace: Namespaces.Peer.CloudUser, id: userId))
|
||||
self.startCallWhenReady(accountId: nil, peerId: PeerId(namespace: Namespaces.Peer.CloudUser, id: userId), isVideo: startCallIsVideo)
|
||||
}
|
||||
|
||||
func cleanPhoneNumber(_ text: String) -> String {
|
||||
|
|
@ -1754,7 +1772,7 @@ final class SharedApplicationContext {
|
|||
}
|
||||
}
|
||||
|
||||
if let contact = startCallIntent.contacts?.first {
|
||||
if let contact = startCallContacts.first {
|
||||
var processed = false
|
||||
if let handle = contact.customIdentifier, handle.hasPrefix("tg") {
|
||||
let string = handle.suffix(from: handle.index(handle.startIndex, offsetBy: 2))
|
||||
|
|
@ -1914,7 +1932,7 @@ final class SharedApplicationContext {
|
|||
})
|
||||
}
|
||||
|
||||
private func startCallWhenReady(accountId: AccountRecordId?, peerId: PeerId) {
|
||||
private func startCallWhenReady(accountId: AccountRecordId?, peerId: PeerId, isVideo: Bool) {
|
||||
let signal = self.sharedContextPromise.get()
|
||||
|> take(1)
|
||||
|> mapToSignal { sharedApplicationContext -> Signal<AuthorizedApplicationContext, NoError> in
|
||||
|
|
@ -1932,7 +1950,7 @@ final class SharedApplicationContext {
|
|||
}
|
||||
self.openChatWhenReadyDisposable.set((signal
|
||||
|> deliverOnMainQueue).start(next: { context in
|
||||
context.startCall(peerId: peerId)
|
||||
context.startCall(peerId: peerId, isVideo: isVideo)
|
||||
}))
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -750,7 +750,7 @@ final class AuthorizedApplicationContext {
|
|||
}
|
||||
}
|
||||
|
||||
func startCall(peerId: PeerId) {
|
||||
func startCall(peerId: PeerId, isVideo: Bool) {
|
||||
guard let appLockContext = self.context.sharedContext.appLockContext as? AppLockContextImpl else {
|
||||
return
|
||||
}
|
||||
|
|
@ -763,7 +763,7 @@ final class AuthorizedApplicationContext {
|
|||
guard let strongSelf = self else {
|
||||
return
|
||||
}
|
||||
let _ = strongSelf.context.sharedContext.callManager?.requestCall(account: strongSelf.context.account, peerId: peerId, endCurrentIfAny: false)
|
||||
let _ = strongSelf.context.sharedContext.callManager?.requestCall(account: strongSelf.context.account, peerId: peerId, isVideo: isVideo, endCurrentIfAny: false)
|
||||
}))
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -320,6 +320,8 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
|
|||
|
||||
private let peekData: ChatPeekTimeout?
|
||||
private let peekTimerDisposable = MetaDisposable()
|
||||
|
||||
private var hasEmbeddedTitleContent = false
|
||||
|
||||
public override var customData: Any? {
|
||||
return self.chatLocation
|
||||
|
|
@ -373,7 +375,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
|
|||
case .inline:
|
||||
navigationBarPresentationData = nil
|
||||
default:
|
||||
navigationBarPresentationData = NavigationBarPresentationData(presentationData: self.presentationData)
|
||||
navigationBarPresentationData = NavigationBarPresentationData(presentationData: self.presentationData, hideBackground: true)
|
||||
}
|
||||
super.init(context: context, navigationBarPresentationData: navigationBarPresentationData, mediaAccessoryPanelVisibility: mediaAccessoryPanelVisibility, locationBroadcastPanelSource: locationBroadcastPanelSource)
|
||||
|
||||
|
|
@ -469,8 +471,8 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
|
|||
self?.openUrl(url, concealed: false, message: nil)
|
||||
}, openPeer: { peer, navigation in
|
||||
self?.openPeer(peerId: peer.id, navigation: navigation, fromMessage: nil)
|
||||
}, callPeer: { peerId in
|
||||
self?.controllerInteraction?.callPeer(peerId)
|
||||
}, callPeer: { peerId, isVideo in
|
||||
self?.controllerInteraction?.callPeer(peerId, isVideo)
|
||||
}, enqueueMessage: { message in
|
||||
self?.sendMessages([message])
|
||||
}, sendSticker: canSendMessagesToChat(strongSelf.presentationInterfaceState) ? { fileReference, sourceNode, sourceRect in
|
||||
|
|
@ -1178,7 +1180,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
|
|||
return self?.chatDisplayNode.reactionContainerNode
|
||||
}, presentGlobalOverlayController: { [weak self] controller, arguments in
|
||||
self?.presentInGlobalOverlay(controller, with: arguments)
|
||||
}, callPeer: { [weak self] peerId in
|
||||
}, callPeer: { [weak self] peerId, isVideo in
|
||||
if let strongSelf = self {
|
||||
strongSelf.commitPurposefulAction()
|
||||
|
||||
|
|
@ -1199,7 +1201,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
|
|||
return
|
||||
}
|
||||
|
||||
let callResult = context.sharedContext.callManager?.requestCall(account: context.account, peerId: peer.id, endCurrentIfAny: false)
|
||||
let callResult = context.sharedContext.callManager?.requestCall(account: context.account, peerId: peer.id, isVideo: isVideo, endCurrentIfAny: false)
|
||||
if let callResult = callResult, case let .alreadyInProgress(currentPeerId) = callResult {
|
||||
if currentPeerId == peer.id {
|
||||
context.sharedContext.navigateToCurrentCall()
|
||||
|
|
@ -1211,7 +1213,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
|
|||
|> deliverOnMainQueue).start(next: { peer, current in
|
||||
if let peer = peer, let current = current {
|
||||
strongSelf.present(textAlertController(context: strongSelf.context, title: presentationData.strings.Call_CallInProgressTitle, text: presentationData.strings.Call_CallInProgressMessage(current.compactDisplayTitle, peer.compactDisplayTitle).0, actions: [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_Cancel, action: {}), TextAlertAction(type: .genericAction, title: presentationData.strings.Common_OK, action: {
|
||||
let _ = context.sharedContext.callManager?.requestCall(account: context.account, peerId: peer.id, endCurrentIfAny: true)
|
||||
let _ = context.sharedContext.callManager?.requestCall(account: context.account, peerId: peer.id, isVideo: isVideo, endCurrentIfAny: true)
|
||||
})]), in: .window(.root))
|
||||
}
|
||||
})
|
||||
|
|
@ -2779,8 +2781,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
|
|||
case .inline:
|
||||
self.statusBar.statusBarStyle = .Ignore
|
||||
}
|
||||
self.navigationBar?.updatePresentationData(NavigationBarPresentationData(presentationData: self.presentationData))
|
||||
self.chatTitleView?.updateThemeAndStrings(theme: self.presentationData.theme, strings: self.presentationData.strings)
|
||||
self.updateNavigationBarPresentation()
|
||||
self.updateChatPresentationInterfaceState(animated: false, interactive: false, { state in
|
||||
var state = state
|
||||
state = state.updatedTheme(self.presentationData.theme)
|
||||
|
|
@ -2794,6 +2795,20 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
|
|||
self.currentContextController?.updateTheme(presentationData: self.presentationData)
|
||||
}
|
||||
|
||||
private func updateNavigationBarPresentation() {
|
||||
let navigationBarTheme: NavigationBarTheme
|
||||
|
||||
if self.hasEmbeddedTitleContent {
|
||||
navigationBarTheme = NavigationBarTheme(rootControllerTheme: defaultDarkPresentationTheme, hideBackground: true)
|
||||
} else {
|
||||
navigationBarTheme = NavigationBarTheme(rootControllerTheme: self.presentationData.theme, hideBackground: true)
|
||||
}
|
||||
|
||||
self.navigationBar?.updatePresentationData(NavigationBarPresentationData(theme: navigationBarTheme, strings: NavigationBarStrings(presentationStrings: self.presentationData.strings)))
|
||||
|
||||
self.chatTitleView?.updateThemeAndStrings(theme: self.presentationData.theme, strings: self.presentationData.strings, hasEmbeddedTitleContent: self.hasEmbeddedTitleContent)
|
||||
}
|
||||
|
||||
override public func loadDisplayNode() {
|
||||
self.displayNode = ChatControllerNode(context: self.context, chatLocation: self.chatLocation, subject: self.subject, controllerInteraction: self.controllerInteraction!, chatPresentationInterfaceState: self.presentationInterfaceState, automaticMediaDownloadSettings: self.automaticMediaDownloadSettings, navigationBar: self.navigationBar, controller: self)
|
||||
|
||||
|
|
@ -4254,9 +4269,9 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
|
|||
self?.dismissPeerContactOptions()
|
||||
}, deleteChat: { [weak self] in
|
||||
self?.deleteChat(reportChatSpam: false)
|
||||
}, beginCall: { [weak self] in
|
||||
}, beginCall: { [weak self] isVideo in
|
||||
if let strongSelf = self, case let .peer(peerId) = strongSelf.chatLocation {
|
||||
strongSelf.controllerInteraction?.callPeer(peerId)
|
||||
strongSelf.controllerInteraction?.callPeer(peerId, isVideo)
|
||||
}
|
||||
}, toggleMessageStickerStarred: { [weak self] messageId in
|
||||
if let strongSelf = self, let message = strongSelf.chatDisplayNode.historyNode.messageInCurrentHistoryView(messageId) {
|
||||
|
|
@ -4735,6 +4750,32 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
|
|||
}))
|
||||
}
|
||||
|
||||
self.chatDisplayNode.updateHasEmbeddedTitleContent = { [weak self] hasEmbeddedTitleContent in
|
||||
guard let strongSelf = self else {
|
||||
return
|
||||
}
|
||||
if strongSelf.hasEmbeddedTitleContent != hasEmbeddedTitleContent {
|
||||
strongSelf.hasEmbeddedTitleContent = hasEmbeddedTitleContent
|
||||
|
||||
if strongSelf.hasEmbeddedTitleContent {
|
||||
strongSelf.statusBar.statusBarStyle = .White
|
||||
} else {
|
||||
strongSelf.statusBar.statusBarStyle = strongSelf.presentationData.theme.rootController.statusBarStyle.style
|
||||
}
|
||||
|
||||
if let navigationBar = strongSelf.navigationBar {
|
||||
if let navigationBarCopy = navigationBar.view.snapshotContentTree() {
|
||||
navigationBar.view.superview?.insertSubview(navigationBarCopy, aboveSubview: navigationBar.view)
|
||||
navigationBarCopy.alpha = 0.0
|
||||
navigationBarCopy.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.25, timingFunction: kCAMediaTimingFunctionSpring, completion: { [weak navigationBarCopy] _ in
|
||||
navigationBarCopy?.removeFromSuperview()
|
||||
})
|
||||
}
|
||||
}
|
||||
strongSelf.updateNavigationBarPresentation()
|
||||
}
|
||||
}
|
||||
|
||||
self.interfaceInteraction = interfaceInteraction
|
||||
|
||||
if let search = self.focusOnSearchAfterAppearance {
|
||||
|
|
@ -5143,7 +5184,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
|
|||
|
||||
switch self.presentationInterfaceState.mode {
|
||||
case .standard, .inline:
|
||||
break
|
||||
break
|
||||
case .overlay:
|
||||
if case .Ignore = self.statusBar.statusBarStyle {
|
||||
} else if layout.safeInsets.top.isZero {
|
||||
|
|
@ -5503,7 +5544,11 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
|
|||
|
||||
switch updatedChatPresentationInterfaceState.mode {
|
||||
case .standard:
|
||||
self.statusBar.statusBarStyle = self.presentationData.theme.rootController.statusBarStyle.style
|
||||
if self.hasEmbeddedTitleContent {
|
||||
self.statusBar.statusBarStyle = .White
|
||||
} else {
|
||||
self.statusBar.statusBarStyle = self.presentationData.theme.rootController.statusBarStyle.style
|
||||
}
|
||||
self.deferScreenEdgeGestures = []
|
||||
case .overlay:
|
||||
self.deferScreenEdgeGestures = [.top]
|
||||
|
|
@ -9256,6 +9301,14 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
|
|||
self.focusOnSearchAfterAppearance = (domain, query)
|
||||
self.interfaceInteraction?.beginMessageSearch(domain, query)
|
||||
}
|
||||
|
||||
override public func updatePossibleControllerDropContent(content: NavigationControllerDropContent?) {
|
||||
self.chatDisplayNode.updateEmbeddedTitlePeekContent(content: content)
|
||||
}
|
||||
|
||||
override public func acceptPossibleControllerDropContent(content: NavigationControllerDropContent) -> Bool {
|
||||
return self.chatDisplayNode.acceptEmbeddedTitlePeekContent(content: content)
|
||||
}
|
||||
}
|
||||
|
||||
private final class ContextControllerContentSourceImpl: ContextControllerContentSource {
|
||||
|
|
|
|||
|
|
@ -90,7 +90,7 @@ public final class ChatControllerInteraction {
|
|||
let chatControllerNode: () -> ASDisplayNode?
|
||||
let reactionContainerNode: () -> ReactionSelectionParentNode?
|
||||
let presentGlobalOverlayController: (ViewController, Any?) -> Void
|
||||
let callPeer: (PeerId) -> Void
|
||||
let callPeer: (PeerId, Bool) -> Void
|
||||
let longTap: (ChatControllerInteractionLongTapAction, Message?) -> Void
|
||||
let openCheckoutOrReceipt: (MessageId) -> Void
|
||||
let openSearch: () -> Void
|
||||
|
|
@ -136,7 +136,7 @@ public final class ChatControllerInteraction {
|
|||
var searchTextHighightState: (String, [MessageIndex])?
|
||||
var seenOneTimeAnimatedMedia = Set<MessageId>()
|
||||
|
||||
init(openMessage: @escaping (Message, ChatControllerInteractionOpenMessageMode) -> Bool, openPeer: @escaping (PeerId?, ChatControllerInteractionNavigateToPeer, Message?) -> Void, openPeerMention: @escaping (String) -> Void, openMessageContextMenu: @escaping (Message, Bool, ASDisplayNode, CGRect, UIGestureRecognizer?) -> Void, openMessageContextActions: @escaping (Message, ASDisplayNode, CGRect, ContextGesture?) -> Void, navigateToMessage: @escaping (MessageId, MessageId) -> Void, tapMessage: ((Message) -> Void)?, clickThroughMessage: @escaping () -> Void, toggleMessagesSelection: @escaping ([MessageId], Bool) -> Void, sendCurrentMessage: @escaping (Bool) -> Void, sendMessage: @escaping (String) -> Void, sendSticker: @escaping (FileMediaReference, Bool, ASDisplayNode, CGRect) -> Bool, sendGif: @escaping (FileMediaReference, ASDisplayNode, CGRect) -> Bool, sendBotContextResultAsGif: @escaping (ChatContextResultCollection, ChatContextResult, ASDisplayNode, CGRect) -> Bool, requestMessageActionCallback: @escaping (MessageId, MemoryBuffer?, Bool) -> Void, requestMessageActionUrlAuth: @escaping (String, MessageId, Int32) -> Void, activateSwitchInline: @escaping (PeerId?, String) -> Void, openUrl: @escaping (String, Bool, Bool?, Message?) -> Void, shareCurrentLocation: @escaping () -> Void, shareAccountContact: @escaping () -> Void, sendBotCommand: @escaping (MessageId?, String) -> Void, openInstantPage: @escaping (Message, ChatMessageItemAssociatedData?) -> Void, openWallpaper: @escaping (Message) -> Void, openTheme: @escaping (Message) -> Void, openHashtag: @escaping (String?, String) -> Void, updateInputState: @escaping ((ChatTextInputState) -> ChatTextInputState) -> Void, updateInputMode: @escaping ((ChatInputMode) -> ChatInputMode) -> Void, openMessageShareMenu: @escaping (MessageId) -> Void, presentController: @escaping (ViewController, Any?) -> Void, navigationController: @escaping () -> NavigationController?, chatControllerNode: @escaping () -> ASDisplayNode?, reactionContainerNode: @escaping () -> ReactionSelectionParentNode?, presentGlobalOverlayController: @escaping (ViewController, Any?) -> Void, callPeer: @escaping (PeerId) -> Void, longTap: @escaping (ChatControllerInteractionLongTapAction, Message?) -> Void, openCheckoutOrReceipt: @escaping (MessageId) -> Void, openSearch: @escaping () -> Void, setupReply: @escaping (MessageId) -> Void, canSetupReply: @escaping (Message) -> ChatControllerInteractionSwipeAction, navigateToFirstDateMessage: @escaping(Int32) ->Void, requestRedeliveryOfFailedMessages: @escaping (MessageId) -> Void, addContact: @escaping (String) -> Void, rateCall: @escaping (Message, CallId) -> Void, requestSelectMessagePollOptions: @escaping (MessageId, [Data]) -> Void, requestOpenMessagePollResults: @escaping (MessageId, MediaId) -> Void, openAppStorePage: @escaping () -> Void, displayMessageTooltip: @escaping (MessageId, String, ASDisplayNode?, CGRect?) -> Void, seekToTimecode: @escaping (Message, Double, Bool) -> Void, scheduleCurrentMessage: @escaping () -> Void, sendScheduledMessagesNow: @escaping ([MessageId]) -> Void, editScheduledMessagesTime: @escaping ([MessageId]) -> Void, performTextSelectionAction: @escaping (UInt32, NSAttributedString, TextSelectionAction) -> Void, updateMessageLike: @escaping (MessageId, Bool) -> Void, openMessageReactions: @escaping (MessageId) -> Void, displaySwipeToReplyHint: @escaping () -> Void, dismissReplyMarkupMessage: @escaping (Message) -> Void, openMessagePollResults: @escaping (MessageId, Data) -> Void, openPollCreation: @escaping (Bool?) -> Void, displayPollSolution: @escaping (TelegramMediaPollResults.Solution, ASDisplayNode) -> Void, displayPsa: @escaping (String, ASDisplayNode) -> Void, displayDiceTooltip: @escaping (TelegramMediaDice) -> Void, animateDiceSuccess: @escaping () -> Void, requestMessageUpdate: @escaping (MessageId) -> Void, cancelInteractiveKeyboardGestures: @escaping () -> Void, automaticMediaDownloadSettings: MediaAutoDownloadSettings, pollActionState: ChatInterfacePollActionState, stickerSettings: ChatInterfaceStickerSettings) {
|
||||
init(openMessage: @escaping (Message, ChatControllerInteractionOpenMessageMode) -> Bool, openPeer: @escaping (PeerId?, ChatControllerInteractionNavigateToPeer, Message?) -> Void, openPeerMention: @escaping (String) -> Void, openMessageContextMenu: @escaping (Message, Bool, ASDisplayNode, CGRect, UIGestureRecognizer?) -> Void, openMessageContextActions: @escaping (Message, ASDisplayNode, CGRect, ContextGesture?) -> Void, navigateToMessage: @escaping (MessageId, MessageId) -> Void, tapMessage: ((Message) -> Void)?, clickThroughMessage: @escaping () -> Void, toggleMessagesSelection: @escaping ([MessageId], Bool) -> Void, sendCurrentMessage: @escaping (Bool) -> Void, sendMessage: @escaping (String) -> Void, sendSticker: @escaping (FileMediaReference, Bool, ASDisplayNode, CGRect) -> Bool, sendGif: @escaping (FileMediaReference, ASDisplayNode, CGRect) -> Bool, sendBotContextResultAsGif: @escaping (ChatContextResultCollection, ChatContextResult, ASDisplayNode, CGRect) -> Bool, requestMessageActionCallback: @escaping (MessageId, MemoryBuffer?, Bool) -> Void, requestMessageActionUrlAuth: @escaping (String, MessageId, Int32) -> Void, activateSwitchInline: @escaping (PeerId?, String) -> Void, openUrl: @escaping (String, Bool, Bool?, Message?) -> Void, shareCurrentLocation: @escaping () -> Void, shareAccountContact: @escaping () -> Void, sendBotCommand: @escaping (MessageId?, String) -> Void, openInstantPage: @escaping (Message, ChatMessageItemAssociatedData?) -> Void, openWallpaper: @escaping (Message) -> Void, openTheme: @escaping (Message) -> Void, openHashtag: @escaping (String?, String) -> Void, updateInputState: @escaping ((ChatTextInputState) -> ChatTextInputState) -> Void, updateInputMode: @escaping ((ChatInputMode) -> ChatInputMode) -> Void, openMessageShareMenu: @escaping (MessageId) -> Void, presentController: @escaping (ViewController, Any?) -> Void, navigationController: @escaping () -> NavigationController?, chatControllerNode: @escaping () -> ASDisplayNode?, reactionContainerNode: @escaping () -> ReactionSelectionParentNode?, presentGlobalOverlayController: @escaping (ViewController, Any?) -> Void, callPeer: @escaping (PeerId, Bool) -> Void, longTap: @escaping (ChatControllerInteractionLongTapAction, Message?) -> Void, openCheckoutOrReceipt: @escaping (MessageId) -> Void, openSearch: @escaping () -> Void, setupReply: @escaping (MessageId) -> Void, canSetupReply: @escaping (Message) -> ChatControllerInteractionSwipeAction, navigateToFirstDateMessage: @escaping(Int32) ->Void, requestRedeliveryOfFailedMessages: @escaping (MessageId) -> Void, addContact: @escaping (String) -> Void, rateCall: @escaping (Message, CallId) -> Void, requestSelectMessagePollOptions: @escaping (MessageId, [Data]) -> Void, requestOpenMessagePollResults: @escaping (MessageId, MediaId) -> Void, openAppStorePage: @escaping () -> Void, displayMessageTooltip: @escaping (MessageId, String, ASDisplayNode?, CGRect?) -> Void, seekToTimecode: @escaping (Message, Double, Bool) -> Void, scheduleCurrentMessage: @escaping () -> Void, sendScheduledMessagesNow: @escaping ([MessageId]) -> Void, editScheduledMessagesTime: @escaping ([MessageId]) -> Void, performTextSelectionAction: @escaping (UInt32, NSAttributedString, TextSelectionAction) -> Void, updateMessageLike: @escaping (MessageId, Bool) -> Void, openMessageReactions: @escaping (MessageId) -> Void, displaySwipeToReplyHint: @escaping () -> Void, dismissReplyMarkupMessage: @escaping (Message) -> Void, openMessagePollResults: @escaping (MessageId, Data) -> Void, openPollCreation: @escaping (Bool?) -> Void, displayPollSolution: @escaping (TelegramMediaPollResults.Solution, ASDisplayNode) -> Void, displayPsa: @escaping (String, ASDisplayNode) -> Void, displayDiceTooltip: @escaping (TelegramMediaDice) -> Void, animateDiceSuccess: @escaping () -> Void, requestMessageUpdate: @escaping (MessageId) -> Void, cancelInteractiveKeyboardGestures: @escaping () -> Void, automaticMediaDownloadSettings: MediaAutoDownloadSettings, pollActionState: ChatInterfacePollActionState, stickerSettings: ChatInterfaceStickerSettings) {
|
||||
self.openMessage = openMessage
|
||||
self.openPeer = openPeer
|
||||
self.openPeerMention = openPeerMention
|
||||
|
|
@ -218,7 +218,7 @@ public final class ChatControllerInteraction {
|
|||
return nil
|
||||
}, reactionContainerNode: {
|
||||
return nil
|
||||
}, presentGlobalOverlayController: { _, _ in }, callPeer: { _ in }, longTap: { _, _ in }, openCheckoutOrReceipt: { _ in }, openSearch: { }, setupReply: { _ in
|
||||
}, presentGlobalOverlayController: { _, _ in }, callPeer: { _, _ in }, longTap: { _, _ in }, openCheckoutOrReceipt: { _ in }, openSearch: { }, setupReply: { _ in
|
||||
}, canSetupReply: { _ in
|
||||
return .none
|
||||
}, navigateToFirstDateMessage: { _ in
|
||||
|
|
|
|||
|
|
@ -12,6 +12,15 @@ import TextFormat
|
|||
import AccountContext
|
||||
import TelegramNotices
|
||||
import ReactionSelectionNode
|
||||
import TelegramUniversalVideoContent
|
||||
|
||||
final class VideoNavigationControllerDropContentItem: NavigationControllerDropContentItem {
|
||||
let itemNode: OverlayMediaItemNode
|
||||
|
||||
init(itemNode: OverlayMediaItemNode) {
|
||||
self.itemNode = itemNode
|
||||
}
|
||||
}
|
||||
|
||||
private final class ChatControllerNodeView: UITracingLayerView, WindowInputAccessoryHeightProvider, PreviewingHostView {
|
||||
var inputAccessoryHeight: (() -> CGFloat)?
|
||||
|
|
@ -56,6 +65,175 @@ private struct ChatControllerNodeDerivedLayoutState {
|
|||
var upperInputPositionBound: CGFloat?
|
||||
}
|
||||
|
||||
private final class ChatEmbeddedTitleContentNode: ASDisplayNode {
|
||||
private let context: AccountContext
|
||||
private let backgroundNode: ASDisplayNode
|
||||
private let videoNode: OverlayUniversalVideoNode
|
||||
|
||||
private var validLayout: (CGSize, CGFloat, CGFloat)?
|
||||
|
||||
private let dismissed: () -> Void
|
||||
private let interactiveExtensionUpdated: (ContainedViewLayoutTransition) -> Void
|
||||
|
||||
private(set) var interactiveExtension: CGFloat = 0.0
|
||||
private var freezeInteractiveExtension = false
|
||||
|
||||
init(context: AccountContext, videoNode: OverlayUniversalVideoNode, interactiveExtensionUpdated: @escaping (ContainedViewLayoutTransition) -> Void, dismissed: @escaping () -> Void) {
|
||||
self.dismissed = dismissed
|
||||
self.interactiveExtensionUpdated = interactiveExtensionUpdated
|
||||
|
||||
self.context = context
|
||||
|
||||
self.backgroundNode = ASDisplayNode()
|
||||
self.backgroundNode.backgroundColor = .black
|
||||
|
||||
self.videoNode = videoNode
|
||||
|
||||
super.init()
|
||||
|
||||
self.clipsToBounds = true
|
||||
|
||||
self.addSubnode(self.backgroundNode)
|
||||
|
||||
self.view.addGestureRecognizer(UIPanGestureRecognizer(target: self, action: #selector(self.panGesture(_:))))
|
||||
}
|
||||
|
||||
@objc private func panGesture(_ recognizer: UIPanGestureRecognizer) {
|
||||
switch recognizer.state {
|
||||
case .began:
|
||||
break
|
||||
case .changed:
|
||||
let translation = recognizer.translation(in: self.view)
|
||||
|
||||
func rubberBandingOffset(offset: CGFloat, bandingStart: CGFloat) -> CGFloat {
|
||||
let bandedOffset = offset - bandingStart
|
||||
let range: CGFloat = 600.0
|
||||
let coefficient: CGFloat = 0.4
|
||||
return bandingStart + (1.0 - (1.0 / ((bandedOffset * coefficient / range) + 1.0))) * range
|
||||
}
|
||||
|
||||
let offset = rubberBandingOffset(offset: translation.y, bandingStart: 0.0)
|
||||
|
||||
if translation.y > 80.0 {
|
||||
self.freezeInteractiveExtension = true
|
||||
self.videoNode.customExpand?()
|
||||
} else {
|
||||
self.interactiveExtension = max(0.0, offset)
|
||||
self.interactiveExtensionUpdated(.immediate)
|
||||
}
|
||||
case .cancelled, .ended:
|
||||
if !freezeInteractiveExtension {
|
||||
self.interactiveExtension = 0.0
|
||||
self.interactiveExtensionUpdated(.animated(duration: 0.3, curve: .spring))
|
||||
}
|
||||
default:
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
func calculateHeight(width: CGFloat) -> CGFloat {
|
||||
return self.videoNode.content.dimensions.aspectFilled(CGSize(width: width, height: 16.0)).height
|
||||
}
|
||||
|
||||
func updateLayout(size: CGSize, topInset: CGFloat, interactiveExtension: CGFloat, transition: ContainedViewLayoutTransition, transitionSurface: ASDisplayNode?, navigationBar: NavigationBar?) {
|
||||
let isFirstTime = self.validLayout == nil
|
||||
|
||||
self.validLayout = (size, topInset, interactiveExtension)
|
||||
|
||||
let videoFrame = CGRect(origin: CGPoint(x: 0.0, y: topInset + interactiveExtension), size: CGSize(width: size.width, height: size.height - topInset - interactiveExtension))
|
||||
|
||||
if isFirstTime, let transitionSurface = transitionSurface {
|
||||
let sourceFrame = self.videoNode.view.convert(self.videoNode.bounds, to: transitionSurface.view)
|
||||
let targetFrame = self.view.convert(videoFrame, to: transitionSurface.view)
|
||||
|
||||
self.context.sharedContext.mediaManager.setOverlayVideoNode(nil)
|
||||
transitionSurface.addSubnode(self.videoNode)
|
||||
|
||||
let navigationBarCopy = navigationBar?.view.snapshotView(afterScreenUpdates: true)
|
||||
let navigationBarContainer = UIView()
|
||||
navigationBarContainer.frame = targetFrame
|
||||
navigationBarContainer.clipsToBounds = true
|
||||
transitionSurface.view.addSubview(navigationBarContainer)
|
||||
|
||||
navigationBarContainer.layer.animateFrame(from: sourceFrame, to: targetFrame, duration: 0.25, timingFunction: kCAMediaTimingFunctionSpring)
|
||||
|
||||
if let navigationBar = navigationBar, let navigationBarCopy = navigationBarCopy {
|
||||
let navigationFrame = navigationBar.view.convert(navigationBar.bounds, to: transitionSurface.view)
|
||||
let navigationSourceFrame = navigationFrame.offsetBy(dx: -sourceFrame.minX, dy: -sourceFrame.minY)
|
||||
let navigationTargetFrame = navigationFrame.offsetBy(dx: -targetFrame.minX, dy: -targetFrame.minY)
|
||||
navigationBarCopy.frame = navigationTargetFrame
|
||||
navigationBarContainer.addSubview(navigationBarCopy)
|
||||
|
||||
navigationBarCopy.layer.animateFrame(from: navigationSourceFrame, to: navigationTargetFrame, duration: 0.25, timingFunction: kCAMediaTimingFunctionSpring)
|
||||
navigationBarCopy.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.25, timingFunction: kCAMediaTimingFunctionSpring)
|
||||
}
|
||||
|
||||
self.videoNode.updateRoundCorners(false, transition: .animated(duration: 0.25, curve: .spring))
|
||||
self.videoNode.showControls()
|
||||
|
||||
self.videoNode.updateLayout(targetFrame.size, transition: .animated(duration: 0.25, curve: .spring))
|
||||
self.videoNode.frame = targetFrame
|
||||
self.videoNode.layer.animateFrame(from: sourceFrame, to: targetFrame, duration: 0.25, timingFunction: kCAMediaTimingFunctionSpring, completion: { [weak self] _ in
|
||||
guard let strongSelf = self else {
|
||||
return
|
||||
}
|
||||
navigationBarContainer.removeFromSuperview()
|
||||
strongSelf.addSubnode(strongSelf.videoNode)
|
||||
if let (size, topInset, interactiveExtension) = strongSelf.validLayout {
|
||||
strongSelf.updateLayout(size: size, topInset: topInset, interactiveExtension: interactiveExtension, transition: .immediate, transitionSurface: nil, navigationBar: nil)
|
||||
}
|
||||
})
|
||||
self.backgroundNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2)
|
||||
|
||||
self.videoNode.customExpand = { [weak self] in
|
||||
guard let strongSelf = self else {
|
||||
return
|
||||
}
|
||||
|
||||
let transition: ContainedViewLayoutTransition = .animated(duration: 0.25, curve: .spring)
|
||||
|
||||
strongSelf.videoNode.customExpand = nil
|
||||
strongSelf.videoNode.customClose = nil
|
||||
|
||||
let previousFrame = strongSelf.videoNode.frame
|
||||
strongSelf.context.sharedContext.mediaManager.setOverlayVideoNode(strongSelf.videoNode)
|
||||
strongSelf.videoNode.updateRoundCorners(true, transition: transition)
|
||||
|
||||
if let targetSuperview = strongSelf.videoNode.view.superview {
|
||||
let sourceFrame = strongSelf.view.convert(previousFrame, to: targetSuperview)
|
||||
let targetFrame = strongSelf.videoNode.frame
|
||||
strongSelf.videoNode.frame = sourceFrame
|
||||
strongSelf.videoNode.updateLayout(sourceFrame.size, transition: .immediate)
|
||||
|
||||
transition.updateFrame(node: strongSelf.videoNode, frame: targetFrame)
|
||||
strongSelf.videoNode.updateLayout(targetFrame.size, transition: transition)
|
||||
}
|
||||
|
||||
strongSelf.dismissed()
|
||||
}
|
||||
|
||||
self.videoNode.customClose = { [weak self] in
|
||||
guard let strongSelf = self else {
|
||||
return
|
||||
}
|
||||
strongSelf.videoNode.customClose = nil
|
||||
strongSelf.dismissed()
|
||||
}
|
||||
}
|
||||
transition.updateFrame(node: self.backgroundNode, frame: CGRect(origin: CGPoint(), size: size))
|
||||
|
||||
if self.videoNode.supernode == self {
|
||||
self.videoNode.layer.transform = CATransform3DIdentity
|
||||
transition.updateFrame(node: self.videoNode, frame: videoFrame)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
enum ChatEmbeddedTitlePeekContent: Equatable {
|
||||
case none
|
||||
case peek
|
||||
}
|
||||
|
||||
class ChatControllerNode: ASDisplayNode, UIScrollViewDelegate {
|
||||
let context: AccountContext
|
||||
let chatLocation: ChatLocation
|
||||
|
|
@ -63,6 +241,8 @@ class ChatControllerNode: ASDisplayNode, UIScrollViewDelegate {
|
|||
private weak var controller: ChatControllerImpl?
|
||||
|
||||
let navigationBar: NavigationBar?
|
||||
private let navigationBarBackroundNode: ASDisplayNode
|
||||
private let navigationBarSeparatorNode: ASDisplayNode
|
||||
|
||||
private var backgroundEffectNode: ASDisplayNode?
|
||||
private var containerBackgroundNode: ASImageNode?
|
||||
|
|
@ -204,6 +384,10 @@ class ChatControllerNode: ASDisplayNode, UIScrollViewDelegate {
|
|||
|
||||
private var onLayoutCompletions: [(ContainedViewLayoutTransition) -> Void] = []
|
||||
|
||||
private var embeddedTitlePeekContent: ChatEmbeddedTitlePeekContent = .none
|
||||
private var embeddedTitleContentNode: ChatEmbeddedTitleContentNode?
|
||||
private var dismissedEmbeddedTitleContentNode: ChatEmbeddedTitleContentNode?
|
||||
|
||||
init(context: AccountContext, chatLocation: ChatLocation, subject: ChatControllerSubject?, controllerInteraction: ChatControllerInteraction, chatPresentationInterfaceState: ChatPresentationInterfaceState, automaticMediaDownloadSettings: MediaAutoDownloadSettings, navigationBar: NavigationBar?, controller: ChatControllerImpl?) {
|
||||
self.context = context
|
||||
self.chatLocation = chatLocation
|
||||
|
|
@ -248,6 +432,12 @@ class ChatControllerNode: ASDisplayNode, UIScrollViewDelegate {
|
|||
self.navigateButtons = ChatHistoryNavigationButtons(theme: self.chatPresentationInterfaceState.theme)
|
||||
self.navigateButtons.accessibilityElementsHidden = true
|
||||
|
||||
self.navigationBarBackroundNode = ASDisplayNode()
|
||||
self.navigationBarBackroundNode.backgroundColor = chatPresentationInterfaceState.theme.rootController.navigationBar.backgroundColor
|
||||
|
||||
self.navigationBarSeparatorNode = ASDisplayNode()
|
||||
self.navigationBarSeparatorNode.backgroundColor = chatPresentationInterfaceState.theme.rootController.navigationBar.separatorColor
|
||||
|
||||
super.init()
|
||||
|
||||
self.controller?.presentationContext.topLevelSubview = { [weak self] in
|
||||
|
|
@ -329,6 +519,9 @@ class ChatControllerNode: ASDisplayNode, UIScrollViewDelegate {
|
|||
|
||||
self.addSubnode(self.navigateButtons)
|
||||
|
||||
self.addSubnode(self.navigationBarBackroundNode)
|
||||
self.addSubnode(self.navigationBarSeparatorNode)
|
||||
|
||||
self.historyNode.view.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.tapGesture(_:))))
|
||||
|
||||
self.textInputPanelNode = ChatTextInputPanelNode(presentationInterfaceState: chatPresentationInterfaceState, presentController: { [weak self] controller in
|
||||
|
|
@ -696,12 +889,63 @@ class ChatControllerNode: ASDisplayNode, UIScrollViewDelegate {
|
|||
} else {
|
||||
insets = layout.insets(options: [.input])
|
||||
}
|
||||
if case .overlay = self.chatPresentationInterfaceState.mode {
|
||||
insets.top = 44.0
|
||||
|
||||
let statusBarHeight = layout.insets(options: [.statusBar]).top
|
||||
|
||||
if let embeddedTitleContentNode = self.embeddedTitleContentNode {
|
||||
let embeddedSize = CGSize(width: layout.size.width, height: min(400.0, embeddedTitleContentNode.calculateHeight(width: layout.size.width)) + statusBarHeight + embeddedTitleContentNode.interactiveExtension)
|
||||
if embeddedTitleContentNode.supernode == nil {
|
||||
self.insertSubnode(embeddedTitleContentNode, aboveSubnode: self.navigationBarBackroundNode)
|
||||
|
||||
var previousTopInset = insets.top
|
||||
if case .overlay = self.chatPresentationInterfaceState.mode {
|
||||
previousTopInset = 44.0
|
||||
} else {
|
||||
previousTopInset += navigationBarHeight
|
||||
}
|
||||
|
||||
if case .peek = self.embeddedTitlePeekContent {
|
||||
previousTopInset += 32.0
|
||||
}
|
||||
|
||||
embeddedTitleContentNode.frame = CGRect(origin: CGPoint(), size: CGSize(width: layout.size.width, height: previousTopInset))
|
||||
transition.updateFrame(node: embeddedTitleContentNode, frame: CGRect(origin: CGPoint(), size: embeddedSize))
|
||||
embeddedTitleContentNode.updateLayout(size: embeddedSize, topInset: statusBarHeight, interactiveExtension: embeddedTitleContentNode.interactiveExtension, transition: .immediate, transitionSurface: self, navigationBar: self.navigationBar)
|
||||
} else {
|
||||
transition.updateFrame(node: embeddedTitleContentNode, frame: CGRect(origin: CGPoint(), size: embeddedSize))
|
||||
embeddedTitleContentNode.updateLayout(size: embeddedSize, topInset: statusBarHeight, interactiveExtension: embeddedTitleContentNode.interactiveExtension, transition: transition, transitionSurface: self, navigationBar: self.navigationBar)
|
||||
}
|
||||
|
||||
insets.top += embeddedSize.height
|
||||
} else {
|
||||
insets.top += navigationBarHeight
|
||||
if case .overlay = self.chatPresentationInterfaceState.mode {
|
||||
insets.top = 44.0
|
||||
} else {
|
||||
insets.top += navigationBarHeight
|
||||
}
|
||||
|
||||
if case .peek = self.embeddedTitlePeekContent {
|
||||
insets.top += 32.0
|
||||
}
|
||||
}
|
||||
|
||||
if let dismissedEmbeddedTitleContentNode = self.dismissedEmbeddedTitleContentNode {
|
||||
self.dismissedEmbeddedTitleContentNode = nil
|
||||
if transition.isAnimated {
|
||||
dismissedEmbeddedTitleContentNode.alpha = 0.0
|
||||
dismissedEmbeddedTitleContentNode.layer.allowsGroupOpacity = true
|
||||
dismissedEmbeddedTitleContentNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.15, completion: { [weak dismissedEmbeddedTitleContentNode] _ in
|
||||
dismissedEmbeddedTitleContentNode?.removeFromSupernode()
|
||||
})
|
||||
transition.updateFrame(node: dismissedEmbeddedTitleContentNode, frame: CGRect(origin: CGPoint(), size: CGSize(width: layout.size.width, height: insets.top)))
|
||||
} else {
|
||||
dismissedEmbeddedTitleContentNode.removeFromSupernode()
|
||||
}
|
||||
}
|
||||
|
||||
transition.updateFrame(node: self.navigationBarBackroundNode, frame: CGRect(origin: CGPoint(), size: CGSize(width: layout.size.width, height: insets.top)))
|
||||
transition.updateFrame(node: self.navigationBarSeparatorNode, frame: CGRect(origin: CGPoint(x: 0.0, y: insets.top), size: CGSize(width: layout.size.width, height: UIScreenPixel)))
|
||||
|
||||
var wrappingInsets = UIEdgeInsets()
|
||||
if case .overlay = self.chatPresentationInterfaceState.mode {
|
||||
let containerWidth = horizontalContainerFillingSizeForLayout(layout: layout, sideInset: 8.0 + layout.safeInsets.left)
|
||||
|
|
@ -1518,6 +1762,9 @@ class ChatControllerNode: ASDisplayNode, UIScrollViewDelegate {
|
|||
}
|
||||
self.updatePlainInputSeparator(transition: .immediate)
|
||||
self.inputPanelBackgroundSeparatorNode.backgroundColor = self.chatPresentationInterfaceState.theme.chat.inputPanel.panelSeparatorColor
|
||||
|
||||
self.navigationBarBackroundNode.backgroundColor = chatPresentationInterfaceState.theme.rootController.navigationBar.backgroundColor
|
||||
self.navigationBarSeparatorNode.backgroundColor = chatPresentationInterfaceState.theme.rootController.navigationBar.separatorColor
|
||||
}
|
||||
|
||||
let keepSendButtonEnabled = chatPresentationInterfaceState.interfaceState.forwardMessageIds != nil || chatPresentationInterfaceState.interfaceState.editMessage != nil
|
||||
|
|
@ -2384,4 +2631,58 @@ class ChatControllerNode: ASDisplayNode, UIScrollViewDelegate {
|
|||
func animateQuizCorrectOptionSelected() {
|
||||
self.view.insertSubview(ConfettiView(frame: self.view.bounds), aboveSubview: self.historyNode.view)
|
||||
}
|
||||
|
||||
func updateEmbeddedTitlePeekContent(content: NavigationControllerDropContent?) {
|
||||
guard let (_, navigationHeight) = self.validLayout else {
|
||||
return
|
||||
}
|
||||
var peekContent: ChatEmbeddedTitlePeekContent = .none
|
||||
if let content = content, let item = content.item as? VideoNavigationControllerDropContentItem, let _ = item.itemNode as? OverlayUniversalVideoNode {
|
||||
if content.position.y < navigationHeight + 32.0 {
|
||||
peekContent = .peek
|
||||
}
|
||||
}
|
||||
if self.embeddedTitlePeekContent != peekContent {
|
||||
self.embeddedTitlePeekContent = peekContent
|
||||
self.requestLayout(.animated(duration: 0.3, curve: .spring))
|
||||
}
|
||||
}
|
||||
|
||||
var updateHasEmbeddedTitleContent: ((Bool) -> Void)?
|
||||
|
||||
func acceptEmbeddedTitlePeekContent(content: NavigationControllerDropContent) -> Bool {
|
||||
guard let (_, navigationHeight) = self.validLayout else {
|
||||
return false
|
||||
}
|
||||
if content.position.y >= navigationHeight + 32.0 {
|
||||
return false
|
||||
}
|
||||
if let item = content.item as? VideoNavigationControllerDropContentItem, let itemNode = item.itemNode as? OverlayUniversalVideoNode {
|
||||
let embeddedTitleContentNode = ChatEmbeddedTitleContentNode(context: self.context, videoNode: itemNode, interactiveExtensionUpdated: { [weak self] transition in
|
||||
guard let strongSelf = self else {
|
||||
return
|
||||
}
|
||||
strongSelf.requestLayout(transition)
|
||||
}, dismissed: { [weak self] in
|
||||
guard let strongSelf = self else {
|
||||
return
|
||||
}
|
||||
if let embeddedTitleContentNode = strongSelf.embeddedTitleContentNode {
|
||||
strongSelf.embeddedTitleContentNode = nil
|
||||
strongSelf.dismissedEmbeddedTitleContentNode = embeddedTitleContentNode
|
||||
strongSelf.requestLayout(.animated(duration: 0.25, curve: .spring))
|
||||
strongSelf.updateHasEmbeddedTitleContent?(false)
|
||||
}
|
||||
})
|
||||
self.embeddedTitleContentNode = embeddedTitleContentNode
|
||||
self.embeddedTitlePeekContent = .none
|
||||
self.updateHasEmbeddedTitleContent?(true)
|
||||
DispatchQueue.main.async {
|
||||
self.requestLayout(.animated(duration: 0.25, curve: .spring))
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -223,7 +223,7 @@ final class ChatInfoTitlePanelNode: ChatTitleAccessoryPanelNode {
|
|||
case .search:
|
||||
self.interfaceInteraction?.beginMessageSearch(.everything, "")
|
||||
case .call:
|
||||
self.interfaceInteraction?.beginCall()
|
||||
self.interfaceInteraction?.beginCall(false)
|
||||
case .report:
|
||||
self.interfaceInteraction?.reportPeer()
|
||||
case .unarchive:
|
||||
|
|
|
|||
|
|
@ -406,7 +406,7 @@ func contextMenuForChatPresentationIntefaceState(chatPresentationInterfaceState:
|
|||
if data.messageActions.options.contains(.rateCall) {
|
||||
var callId: CallId?
|
||||
for media in message.media {
|
||||
if let action = media as? TelegramMediaAction, case let .phoneCall(id, discardReason, _) = action.action {
|
||||
if let action = media as? TelegramMediaAction, case let .phoneCall(id, discardReason, _, _) = action.action {
|
||||
if discardReason != .busy && discardReason != .missed {
|
||||
if let logName = callLogNameForId(id: id, account: context.account) {
|
||||
let logsPath = callLogsPath(account: context.account)
|
||||
|
|
|
|||
|
|
@ -78,7 +78,7 @@ enum ChatMessageBubbleContentTapAction {
|
|||
case instantPage
|
||||
case wallpaper
|
||||
case theme
|
||||
case call(PeerId)
|
||||
case call(peerId: PeerId, isVideo: Bool)
|
||||
case openMessage
|
||||
case timecode(Double, String)
|
||||
case tooltip(String, ASDisplayNode?, CGRect?)
|
||||
|
|
|
|||
|
|
@ -480,8 +480,7 @@ class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePrevewItemNode
|
|||
return false
|
||||
}
|
||||
else if let media = media as? TelegramMediaAction {
|
||||
if case .phoneCall(_, _, _) = media.action {
|
||||
|
||||
if case .phoneCall(_, _, _, _) = media.action {
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
|
|
@ -2547,9 +2546,9 @@ class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePrevewItemNode
|
|||
item.controllerInteraction.openTheme(item.message)
|
||||
})
|
||||
}
|
||||
case let .call(peerId):
|
||||
case let .call(peerId, isVideo):
|
||||
return .optionalAction({
|
||||
self.item?.controllerInteraction.callPeer(peerId)
|
||||
self.item?.controllerInteraction.callPeer(peerId, isVideo)
|
||||
})
|
||||
case .openMessage:
|
||||
if let item = self.item {
|
||||
|
|
|
|||
|
|
@ -76,8 +76,10 @@ class ChatMessageCallBubbleContentNode: ChatMessageBubbleContentNode {
|
|||
var titleString: String?
|
||||
var callDuration: Int32?
|
||||
var callSuccessful = true
|
||||
var isVideo = false
|
||||
for media in item.message.media {
|
||||
if let action = media as? TelegramMediaAction, case let .phoneCall(_, discardReason, duration) = action.action {
|
||||
if let action = media as? TelegramMediaAction, case let .phoneCall(_, discardReason, duration, isVideoValue) = action.action {
|
||||
isVideo = isVideoValue
|
||||
callDuration = duration
|
||||
if let discardReason = discardReason {
|
||||
switch discardReason {
|
||||
|
|
@ -98,9 +100,17 @@ class ChatMessageCallBubbleContentNode: ChatMessageBubbleContentNode {
|
|||
if titleString == nil {
|
||||
let baseString: String
|
||||
if message.flags.contains(.Incoming) {
|
||||
baseString = item.presentationData.strings.Notification_CallIncoming
|
||||
if isVideo {
|
||||
baseString = item.presentationData.strings.Notification_VideoCallIncoming
|
||||
} else {
|
||||
baseString = item.presentationData.strings.Notification_CallIncoming
|
||||
}
|
||||
} else {
|
||||
baseString = item.presentationData.strings.Notification_CallOutgoing
|
||||
if isVideo {
|
||||
baseString = item.presentationData.strings.Notification_VideoCallOutgoing
|
||||
} else {
|
||||
baseString = item.presentationData.strings.Notification_CallOutgoing
|
||||
}
|
||||
}
|
||||
|
||||
titleString = baseString
|
||||
|
|
@ -203,7 +213,13 @@ class ChatMessageCallBubbleContentNode: ChatMessageBubbleContentNode {
|
|||
|
||||
@objc func callButtonPressed() {
|
||||
if let item = self.item {
|
||||
item.controllerInteraction.callPeer(item.message.id.peerId)
|
||||
var isVideo = false
|
||||
for media in item.message.media {
|
||||
if let action = media as? TelegramMediaAction, case let .phoneCall(_, _, _, isVideoValue) = action.action {
|
||||
isVideo = isVideoValue
|
||||
}
|
||||
}
|
||||
item.controllerInteraction.callPeer(item.message.id.peerId, isVideo)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -211,7 +227,13 @@ class ChatMessageCallBubbleContentNode: ChatMessageBubbleContentNode {
|
|||
if self.buttonNode.frame.contains(point) {
|
||||
return .ignore
|
||||
} else if self.bounds.contains(point), let item = self.item {
|
||||
return .call(item.message.id.peerId)
|
||||
var isVideo = false
|
||||
for media in item.message.media {
|
||||
if let action = media as? TelegramMediaAction, case let .phoneCall(_, _, _, isVideoValue) = action.action {
|
||||
isVideo = isVideoValue
|
||||
}
|
||||
}
|
||||
return .call(peerId: item.message.id.peerId, isVideo: isVideo)
|
||||
} else {
|
||||
return .none
|
||||
}
|
||||
|
|
|
|||
|
|
@ -616,7 +616,7 @@ final class ChatMessageAccessibilityData {
|
|||
canReply = false
|
||||
}
|
||||
else if let media = media as? TelegramMediaAction {
|
||||
if case .phoneCall(_, _, _) = media.action {
|
||||
if case .phoneCall = media.action {
|
||||
} else {
|
||||
canReply = false
|
||||
}
|
||||
|
|
|
|||
|
|
@ -100,7 +100,7 @@ final class ChatPanelInterfaceInteraction {
|
|||
let presentPeerContact: () -> Void
|
||||
let dismissReportPeer: () -> Void
|
||||
let deleteChat: () -> Void
|
||||
let beginCall: () -> Void
|
||||
let beginCall: (Bool) -> Void
|
||||
let toggleMessageStickerStarred: (MessageId) -> Void
|
||||
let presentController: (ViewController, Any?) -> Void
|
||||
let getNavigationController: () -> NavigationController?
|
||||
|
|
@ -120,7 +120,7 @@ final class ChatPanelInterfaceInteraction {
|
|||
let displaySearchResultsTooltip: (ASDisplayNode, CGRect) -> Void
|
||||
let statuses: ChatPanelInterfaceInteractionStatuses?
|
||||
|
||||
init(setupReplyMessage: @escaping (MessageId, @escaping (ContainedViewLayoutTransition) -> Void) -> Void, setupEditMessage: @escaping (MessageId?, @escaping (ContainedViewLayoutTransition) -> Void) -> Void, beginMessageSelection: @escaping ([MessageId], @escaping (ContainedViewLayoutTransition) -> Void) -> Void, deleteSelectedMessages: @escaping () -> Void, reportSelectedMessages: @escaping () -> Void, reportMessages: @escaping ([Message], ContextController?) -> Void, deleteMessages: @escaping ([Message], ContextController?, @escaping (ContextMenuActionResult) -> Void) -> Void, forwardSelectedMessages: @escaping () -> Void, forwardCurrentForwardMessages: @escaping () -> Void, forwardMessages: @escaping ([Message]) -> Void, shareSelectedMessages: @escaping () -> Void, updateTextInputStateAndMode: @escaping ((ChatTextInputState, ChatInputMode) -> (ChatTextInputState, ChatInputMode)) -> Void, updateInputModeAndDismissedButtonKeyboardMessageId: @escaping ((ChatPresentationInterfaceState) -> (ChatInputMode, MessageId?)) -> Void, openStickers: @escaping () -> Void, editMessage: @escaping () -> Void, beginMessageSearch: @escaping (ChatSearchDomain, String) -> Void, dismissMessageSearch: @escaping () -> Void, updateMessageSearch: @escaping (String) -> Void, openSearchResults: @escaping () -> Void, navigateMessageSearch: @escaping (ChatPanelSearchNavigationAction) -> Void, openCalendarSearch: @escaping () -> Void, toggleMembersSearch: @escaping (Bool) -> Void, navigateToMessage: @escaping (MessageId) -> Void, navigateToChat: @escaping (PeerId) -> Void, navigateToProfile: @escaping (PeerId) -> Void, openPeerInfo: @escaping () -> Void, togglePeerNotifications: @escaping () -> Void, sendContextResult: @escaping (ChatContextResultCollection, ChatContextResult, ASDisplayNode, CGRect) -> Bool, sendBotCommand: @escaping (Peer, String) -> Void, sendBotStart: @escaping (String?) -> Void, botSwitchChatWithPayload: @escaping (PeerId, String) -> Void, beginMediaRecording: @escaping (Bool) -> Void, finishMediaRecording: @escaping (ChatFinishMediaRecordingAction) -> Void, stopMediaRecording: @escaping () -> Void, lockMediaRecording: @escaping () -> Void, deleteRecordedMedia: @escaping () -> Void, sendRecordedMedia: @escaping () -> Void, displayRestrictedInfo: @escaping (ChatPanelRestrictionInfoSubject, ChatPanelRestrictionInfoDisplayType) -> Void, displayVideoUnmuteTip: @escaping (CGPoint?) -> Void, switchMediaRecordingMode: @escaping () -> Void, setupMessageAutoremoveTimeout: @escaping () -> Void, sendSticker: @escaping (FileMediaReference, ASDisplayNode, CGRect) -> Bool, unblockPeer: @escaping () -> Void, pinMessage: @escaping (MessageId) -> Void, unpinMessage: @escaping () -> Void, shareAccountContact: @escaping () -> Void, reportPeer: @escaping () -> Void, presentPeerContact: @escaping () -> Void, dismissReportPeer: @escaping () -> Void, deleteChat: @escaping () -> Void, beginCall: @escaping () -> Void, toggleMessageStickerStarred: @escaping (MessageId) -> Void, presentController: @escaping (ViewController, Any?) -> Void, getNavigationController: @escaping () -> NavigationController?, presentGlobalOverlayController: @escaping (ViewController, Any?) -> Void, navigateFeed: @escaping () -> Void, openGrouping: @escaping () -> Void, toggleSilentPost: @escaping () -> Void, requestUnvoteInMessage: @escaping (MessageId) -> Void, requestStopPollInMessage: @escaping (MessageId) -> Void, updateInputLanguage: @escaping ((String?) -> String?) -> Void, unarchiveChat: @escaping () -> Void, openLinkEditing: @escaping () -> Void, reportPeerIrrelevantGeoLocation: @escaping () -> Void, displaySlowmodeTooltip: @escaping (ASDisplayNode, CGRect) -> Void, displaySendMessageOptions: @escaping (ASDisplayNode, ContextGesture) -> Void, openScheduledMessages: @escaping () -> Void, displaySearchResultsTooltip: @escaping (ASDisplayNode, CGRect) -> Void, statuses: ChatPanelInterfaceInteractionStatuses?) {
|
||||
init(setupReplyMessage: @escaping (MessageId, @escaping (ContainedViewLayoutTransition) -> Void) -> Void, setupEditMessage: @escaping (MessageId?, @escaping (ContainedViewLayoutTransition) -> Void) -> Void, beginMessageSelection: @escaping ([MessageId], @escaping (ContainedViewLayoutTransition) -> Void) -> Void, deleteSelectedMessages: @escaping () -> Void, reportSelectedMessages: @escaping () -> Void, reportMessages: @escaping ([Message], ContextController?) -> Void, deleteMessages: @escaping ([Message], ContextController?, @escaping (ContextMenuActionResult) -> Void) -> Void, forwardSelectedMessages: @escaping () -> Void, forwardCurrentForwardMessages: @escaping () -> Void, forwardMessages: @escaping ([Message]) -> Void, shareSelectedMessages: @escaping () -> Void, updateTextInputStateAndMode: @escaping ((ChatTextInputState, ChatInputMode) -> (ChatTextInputState, ChatInputMode)) -> Void, updateInputModeAndDismissedButtonKeyboardMessageId: @escaping ((ChatPresentationInterfaceState) -> (ChatInputMode, MessageId?)) -> Void, openStickers: @escaping () -> Void, editMessage: @escaping () -> Void, beginMessageSearch: @escaping (ChatSearchDomain, String) -> Void, dismissMessageSearch: @escaping () -> Void, updateMessageSearch: @escaping (String) -> Void, openSearchResults: @escaping () -> Void, navigateMessageSearch: @escaping (ChatPanelSearchNavigationAction) -> Void, openCalendarSearch: @escaping () -> Void, toggleMembersSearch: @escaping (Bool) -> Void, navigateToMessage: @escaping (MessageId) -> Void, navigateToChat: @escaping (PeerId) -> Void, navigateToProfile: @escaping (PeerId) -> Void, openPeerInfo: @escaping () -> Void, togglePeerNotifications: @escaping () -> Void, sendContextResult: @escaping (ChatContextResultCollection, ChatContextResult, ASDisplayNode, CGRect) -> Bool, sendBotCommand: @escaping (Peer, String) -> Void, sendBotStart: @escaping (String?) -> Void, botSwitchChatWithPayload: @escaping (PeerId, String) -> Void, beginMediaRecording: @escaping (Bool) -> Void, finishMediaRecording: @escaping (ChatFinishMediaRecordingAction) -> Void, stopMediaRecording: @escaping () -> Void, lockMediaRecording: @escaping () -> Void, deleteRecordedMedia: @escaping () -> Void, sendRecordedMedia: @escaping () -> Void, displayRestrictedInfo: @escaping (ChatPanelRestrictionInfoSubject, ChatPanelRestrictionInfoDisplayType) -> Void, displayVideoUnmuteTip: @escaping (CGPoint?) -> Void, switchMediaRecordingMode: @escaping () -> Void, setupMessageAutoremoveTimeout: @escaping () -> Void, sendSticker: @escaping (FileMediaReference, ASDisplayNode, CGRect) -> Bool, unblockPeer: @escaping () -> Void, pinMessage: @escaping (MessageId) -> Void, unpinMessage: @escaping () -> Void, shareAccountContact: @escaping () -> Void, reportPeer: @escaping () -> Void, presentPeerContact: @escaping () -> Void, dismissReportPeer: @escaping () -> Void, deleteChat: @escaping () -> Void, beginCall: @escaping (Bool) -> Void, toggleMessageStickerStarred: @escaping (MessageId) -> Void, presentController: @escaping (ViewController, Any?) -> Void, getNavigationController: @escaping () -> NavigationController?, presentGlobalOverlayController: @escaping (ViewController, Any?) -> Void, navigateFeed: @escaping () -> Void, openGrouping: @escaping () -> Void, toggleSilentPost: @escaping () -> Void, requestUnvoteInMessage: @escaping (MessageId) -> Void, requestStopPollInMessage: @escaping (MessageId) -> Void, updateInputLanguage: @escaping ((String?) -> String?) -> Void, unarchiveChat: @escaping () -> Void, openLinkEditing: @escaping () -> Void, reportPeerIrrelevantGeoLocation: @escaping () -> Void, displaySlowmodeTooltip: @escaping (ASDisplayNode, CGRect) -> Void, displaySendMessageOptions: @escaping (ASDisplayNode, ContextGesture) -> Void, openScheduledMessages: @escaping () -> Void, displaySearchResultsTooltip: @escaping (ASDisplayNode, CGRect) -> Void, statuses: ChatPanelInterfaceInteractionStatuses?) {
|
||||
self.setupReplyMessage = setupReplyMessage
|
||||
self.setupEditMessage = setupEditMessage
|
||||
self.beginMessageSelection = beginMessageSelection
|
||||
|
|
|
|||
|
|
@ -103,7 +103,7 @@ final class ChatRecentActionsController: TelegramBaseController {
|
|||
}, presentPeerContact: {
|
||||
}, dismissReportPeer: {
|
||||
}, deleteChat: {
|
||||
}, beginCall: {
|
||||
}, beginCall: { _ in
|
||||
}, toggleMessageStickerStarred: { _ in
|
||||
}, presentController: { _, _ in
|
||||
}, getNavigationController: {
|
||||
|
|
|
|||
|
|
@ -180,8 +180,8 @@ final class ChatRecentActionsControllerNode: ViewControllerTracingNode {
|
|||
self?.openUrl(url)
|
||||
}, openPeer: { peer, navigation in
|
||||
self?.openPeer(peerId: peer.id, peer: peer)
|
||||
}, callPeer: { peerId in
|
||||
self?.controllerInteraction?.callPeer(peerId)
|
||||
}, callPeer: { peerId, isVideo in
|
||||
self?.controllerInteraction?.callPeer(peerId, isVideo)
|
||||
}, enqueueMessage: { _ in
|
||||
}, sendSticker: nil, setupTemporaryHiddenMedia: { _, _, _ in }, chatAvatarHiddenMedia: { _, _ in }))
|
||||
}
|
||||
|
|
@ -242,7 +242,7 @@ final class ChatRecentActionsControllerNode: ViewControllerTracingNode {
|
|||
return self
|
||||
}, reactionContainerNode: {
|
||||
return nil
|
||||
}, presentGlobalOverlayController: { _, _ in }, callPeer: { _ in }, longTap: { [weak self] action, message in
|
||||
}, presentGlobalOverlayController: { _, _ in }, callPeer: { _, _ in }, longTap: { [weak self] action, message in
|
||||
if let strongSelf = self {
|
||||
switch action {
|
||||
case let .url(url):
|
||||
|
|
|
|||
|
|
@ -23,61 +23,6 @@ enum ChatTitleContent {
|
|||
case custom(String)
|
||||
}
|
||||
|
||||
private final class ChatTitleNetworkStatusNode: ASDisplayNode {
|
||||
private var theme: PresentationTheme
|
||||
|
||||
private let titleNode: ImmediateTextNode
|
||||
private let activityIndicator: ActivityIndicator
|
||||
|
||||
var title: String = "" {
|
||||
didSet {
|
||||
if self.title != oldValue {
|
||||
self.titleNode.attributedText = NSAttributedString(string: title, font: Font.bold(17.0), textColor: self.theme.rootController.navigationBar.primaryTextColor)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
init(theme: PresentationTheme) {
|
||||
self.theme = theme
|
||||
|
||||
self.titleNode = ImmediateTextNode()
|
||||
self.titleNode.isUserInteractionEnabled = false
|
||||
self.titleNode.displaysAsynchronously = false
|
||||
self.titleNode.maximumNumberOfLines = 1
|
||||
self.titleNode.isOpaque = false
|
||||
self.titleNode.isUserInteractionEnabled = false
|
||||
|
||||
self.activityIndicator = ActivityIndicator(type: .custom(theme.rootController.navigationBar.primaryTextColor, 22.0, 1.5, false), speed: .slow)
|
||||
let activityIndicatorSize = self.activityIndicator.measure(CGSize(width: 100.0, height: 100.0))
|
||||
self.activityIndicator.frame = CGRect(origin: CGPoint(), size: activityIndicatorSize)
|
||||
|
||||
super.init()
|
||||
|
||||
self.addSubnode(self.titleNode)
|
||||
self.addSubnode(self.activityIndicator)
|
||||
}
|
||||
|
||||
func updateTheme(theme: PresentationTheme) {
|
||||
self.theme = theme
|
||||
|
||||
self.titleNode.attributedText = NSAttributedString(string: self.title, font: Font.medium(24.0), textColor: self.theme.rootController.navigationBar.primaryTextColor)
|
||||
self.activityIndicator.type = .custom(self.theme.rootController.navigationBar.primaryTextColor, 22.0, 1.5, false)
|
||||
}
|
||||
|
||||
func updateLayout(size: CGSize, transition: ContainedViewLayoutTransition) {
|
||||
let indicatorSize = self.activityIndicator.bounds.size
|
||||
let indicatorPadding = indicatorSize.width + 6.0
|
||||
|
||||
let titleSize = self.titleNode.updateLayout(CGSize(width: max(1.0, size.width - indicatorPadding), height: size.height))
|
||||
let combinedHeight = titleSize.height
|
||||
|
||||
let titleFrame = CGRect(origin: CGPoint(x: indicatorPadding + floor((size.width - titleSize.width - indicatorPadding) / 2.0), y: floor((size.height - combinedHeight) / 2.0)), size: titleSize)
|
||||
transition.updateFrame(node: self.titleNode, frame: titleFrame)
|
||||
|
||||
transition.updateFrame(node: self.activityIndicator, frame: CGRect(origin: CGPoint(x: titleFrame.minX - indicatorSize.width - 4.0, y: titleFrame.minY - 1.0), size: indicatorSize))
|
||||
}
|
||||
}
|
||||
|
||||
private enum ChatTitleIcon {
|
||||
case none
|
||||
case lock
|
||||
|
|
@ -88,6 +33,7 @@ final class ChatTitleView: UIView, NavigationBarTitleView {
|
|||
private let account: Account
|
||||
|
||||
private var theme: PresentationTheme
|
||||
private var hasEmbeddedTitleContent: Bool = false
|
||||
private var strings: PresentationStrings
|
||||
private var dateTimeFormat: PresentationDateTimeFormat
|
||||
private var nameDisplayOrder: PresentationPersonNameOrder
|
||||
|
|
@ -120,43 +66,6 @@ final class ChatTitleView: UIView, NavigationBarTitleView {
|
|||
}
|
||||
|
||||
private func updateNetworkStatusNode(networkState: AccountNetworkState, layout: ContainerViewLayout?) {
|
||||
var isOnline = false
|
||||
if case .online = networkState {
|
||||
isOnline = true
|
||||
}
|
||||
|
||||
/*if isOnline || layout?.metrics.widthClass == .regular {
|
||||
self.contentContainer.isHidden = false
|
||||
if let networkStatusNode = self.networkStatusNode {
|
||||
self.networkStatusNode = nil
|
||||
networkStatusNode.removeFromSupernode()
|
||||
}
|
||||
} else {
|
||||
self.contentContainer.isHidden = true
|
||||
let statusNode: ChatTitleNetworkStatusNode
|
||||
if let current = self.networkStatusNode {
|
||||
statusNode = current
|
||||
} else {
|
||||
statusNode = ChatTitleNetworkStatusNode(theme: self.theme)
|
||||
self.networkStatusNode = statusNode
|
||||
self.insertSubview(statusNode.view, aboveSubview: self.contentContainer.view)
|
||||
}
|
||||
switch self.networkState {
|
||||
case .waitingForNetwork:
|
||||
statusNode.title = self.strings.State_WaitingForNetwork
|
||||
case let .connecting(proxy):
|
||||
if let layout = layout, proxy != nil && layout.size.width > 320.0 {
|
||||
statusNode.title = self.strings.State_ConnectingToProxy
|
||||
} else {
|
||||
statusNode.title = self.strings.State_Connecting
|
||||
}
|
||||
case .updating:
|
||||
statusNode.title = self.strings.State_Updating
|
||||
case .online:
|
||||
break
|
||||
}
|
||||
}*/
|
||||
|
||||
self.setNeedsLayout()
|
||||
}
|
||||
|
||||
|
|
@ -183,6 +92,8 @@ final class ChatTitleView: UIView, NavigationBarTitleView {
|
|||
var titleContent: ChatTitleContent? {
|
||||
didSet {
|
||||
if let titleContent = self.titleContent {
|
||||
let titleTheme = self.hasEmbeddedTitleContent ? defaultDarkPresentationTheme : self.theme
|
||||
|
||||
var string: NSAttributedString?
|
||||
var titleLeftIcon: ChatTitleIcon = .none
|
||||
var titleRightIcon: ChatTitleIcon = .none
|
||||
|
|
@ -192,20 +103,20 @@ final class ChatTitleView: UIView, NavigationBarTitleView {
|
|||
case let .peer(peerView, _, isScheduledMessages):
|
||||
if isScheduledMessages {
|
||||
if peerView.peerId == self.account.peerId {
|
||||
string = NSAttributedString(string: self.strings.ScheduledMessages_RemindersTitle, font: Font.medium(17.0), textColor: self.theme.rootController.navigationBar.primaryTextColor)
|
||||
string = NSAttributedString(string: self.strings.ScheduledMessages_RemindersTitle, font: Font.medium(17.0), textColor: titleTheme.rootController.navigationBar.primaryTextColor)
|
||||
} else {
|
||||
string = NSAttributedString(string: self.strings.ScheduledMessages_Title, font: Font.medium(17.0), textColor: self.theme.rootController.navigationBar.primaryTextColor)
|
||||
string = NSAttributedString(string: self.strings.ScheduledMessages_Title, font: Font.medium(17.0), textColor: titleTheme.rootController.navigationBar.primaryTextColor)
|
||||
}
|
||||
isEnabled = false
|
||||
} else {
|
||||
if let peer = peerViewMainPeer(peerView) {
|
||||
if peerView.peerId == self.account.peerId {
|
||||
string = NSAttributedString(string: self.strings.Conversation_SavedMessages, font: Font.medium(17.0), textColor: self.theme.rootController.navigationBar.primaryTextColor)
|
||||
string = NSAttributedString(string: self.strings.Conversation_SavedMessages, font: Font.medium(17.0), textColor: titleTheme.rootController.navigationBar.primaryTextColor)
|
||||
} else {
|
||||
if !peerView.peerIsContact, let user = peer as? TelegramUser, !user.flags.contains(.isSupport), user.botInfo == nil, let phone = user.phone, !phone.isEmpty {
|
||||
string = NSAttributedString(string: formatPhoneNumber(phone), font: Font.medium(17.0), textColor: self.theme.rootController.navigationBar.primaryTextColor)
|
||||
string = NSAttributedString(string: formatPhoneNumber(phone), font: Font.medium(17.0), textColor: titleTheme.rootController.navigationBar.primaryTextColor)
|
||||
} else {
|
||||
string = NSAttributedString(string: peer.displayTitle(strings: self.strings, displayOrder: self.nameDisplayOrder), font: Font.medium(17.0), textColor: self.theme.rootController.navigationBar.primaryTextColor)
|
||||
string = NSAttributedString(string: peer.displayTitle(strings: self.strings, displayOrder: self.nameDisplayOrder), font: Font.medium(17.0), textColor: titleTheme.rootController.navigationBar.primaryTextColor)
|
||||
}
|
||||
}
|
||||
titleScamIcon = peer.isScam
|
||||
|
|
@ -220,9 +131,9 @@ final class ChatTitleView: UIView, NavigationBarTitleView {
|
|||
}
|
||||
}
|
||||
case .group:
|
||||
string = NSAttributedString(string: "Feed", font: Font.medium(17.0), textColor: self.theme.rootController.navigationBar.primaryTextColor)
|
||||
string = NSAttributedString(string: "Feed", font: Font.medium(17.0), textColor: titleTheme.rootController.navigationBar.primaryTextColor)
|
||||
case let .custom(text):
|
||||
string = NSAttributedString(string: text, font: Font.medium(17.0), textColor: self.theme.rootController.navigationBar.primaryTextColor)
|
||||
string = NSAttributedString(string: text, font: Font.medium(17.0), textColor: titleTheme.rootController.navigationBar.primaryTextColor)
|
||||
}
|
||||
|
||||
if let string = string, self.titleNode.attributedText == nil || !self.titleNode.attributedText!.isEqual(to: string) {
|
||||
|
|
@ -234,7 +145,7 @@ final class ChatTitleView: UIView, NavigationBarTitleView {
|
|||
self.titleLeftIcon = titleLeftIcon
|
||||
switch titleLeftIcon {
|
||||
case .lock:
|
||||
self.titleLeftIconNode.image = PresentationResourcesChat.chatTitleLockIcon(self.theme)
|
||||
self.titleLeftIconNode.image = PresentationResourcesChat.chatTitleLockIcon(titleTheme)
|
||||
default:
|
||||
self.titleLeftIconNode.image = nil
|
||||
}
|
||||
|
|
@ -243,7 +154,7 @@ final class ChatTitleView: UIView, NavigationBarTitleView {
|
|||
|
||||
if titleScamIcon != self.titleScamIcon {
|
||||
self.titleScamIcon = titleScamIcon
|
||||
self.titleCredibilityIconNode.image = titleScamIcon ? PresentationResourcesChatList.scamIcon(self.theme, type: .regular) : nil
|
||||
self.titleCredibilityIconNode.image = titleScamIcon ? PresentationResourcesChatList.scamIcon(titleTheme, type: .regular) : nil
|
||||
self.setNeedsLayout()
|
||||
}
|
||||
|
||||
|
|
@ -251,7 +162,7 @@ final class ChatTitleView: UIView, NavigationBarTitleView {
|
|||
self.titleRightIcon = titleRightIcon
|
||||
switch titleRightIcon {
|
||||
case .mute:
|
||||
self.titleRightIconNode.image = PresentationResourcesChat.chatTitleMuteIcon(self.theme)
|
||||
self.titleRightIconNode.image = PresentationResourcesChat.chatTitleMuteIcon(titleTheme)
|
||||
default:
|
||||
self.titleRightIconNode.image = nil
|
||||
}
|
||||
|
|
@ -278,6 +189,8 @@ final class ChatTitleView: UIView, NavigationBarTitleView {
|
|||
}
|
||||
}
|
||||
|
||||
let titleTheme = self.hasEmbeddedTitleContent ? defaultDarkPresentationTheme : self.theme
|
||||
|
||||
var state = ChatTitleActivityNodeState.none
|
||||
switch self.networkState {
|
||||
case .waitingForNetwork, .connecting, .updating:
|
||||
|
|
@ -285,14 +198,14 @@ final class ChatTitleView: UIView, NavigationBarTitleView {
|
|||
switch self.networkState {
|
||||
case .waitingForNetwork:
|
||||
infoText = self.strings.ChatState_WaitingForNetwork
|
||||
case let .connecting(proxy):
|
||||
case .connecting:
|
||||
infoText = self.strings.ChatState_Connecting
|
||||
case .updating:
|
||||
infoText = self.strings.ChatState_Updating
|
||||
case .online:
|
||||
infoText = ""
|
||||
}
|
||||
state = .info(NSAttributedString(string: infoText, font: Font.regular(13.0), textColor: self.theme.rootController.navigationBar.secondaryTextColor), .generic)
|
||||
state = .info(NSAttributedString(string: infoText, font: Font.regular(13.0), textColor: titleTheme.rootController.navigationBar.secondaryTextColor), .generic)
|
||||
case .online:
|
||||
if let (peerId, inputActivities) = self.inputActivities, !inputActivities.isEmpty, inputActivitiesAllowed {
|
||||
var stringValue = ""
|
||||
|
|
@ -336,7 +249,7 @@ final class ChatTitleView: UIView, NavigationBarTitleView {
|
|||
}
|
||||
}
|
||||
}
|
||||
let color = self.theme.rootController.navigationBar.accentTextColor
|
||||
let color = titleTheme.rootController.navigationBar.accentTextColor
|
||||
let string = NSAttributedString(string: stringValue, font: Font.regular(13.0), textColor: color)
|
||||
switch mergedActivity {
|
||||
case .typingText:
|
||||
|
|
@ -357,21 +270,21 @@ final class ChatTitleView: UIView, NavigationBarTitleView {
|
|||
if let peer = peerViewMainPeer(peerView) {
|
||||
let servicePeer = isServicePeer(peer)
|
||||
if peer.id == self.account.peerId || isScheduledMessages {
|
||||
let string = NSAttributedString(string: "", font: Font.regular(13.0), textColor: self.theme.rootController.navigationBar.secondaryTextColor)
|
||||
let string = NSAttributedString(string: "", font: Font.regular(13.0), textColor: titleTheme.rootController.navigationBar.secondaryTextColor)
|
||||
state = .info(string, .generic)
|
||||
} else if let user = peer as? TelegramUser {
|
||||
if servicePeer {
|
||||
let string = NSAttributedString(string: "", font: Font.regular(13.0), textColor: self.theme.rootController.navigationBar.secondaryTextColor)
|
||||
let string = NSAttributedString(string: "", font: Font.regular(13.0), textColor: titleTheme.rootController.navigationBar.secondaryTextColor)
|
||||
state = .info(string, .generic)
|
||||
} else if user.flags.contains(.isSupport) {
|
||||
let statusText = self.strings.Bot_GenericSupportStatus
|
||||
|
||||
let string = NSAttributedString(string: statusText, font: Font.regular(13.0), textColor: self.theme.rootController.navigationBar.secondaryTextColor)
|
||||
let string = NSAttributedString(string: statusText, font: Font.regular(13.0), textColor: titleTheme.rootController.navigationBar.secondaryTextColor)
|
||||
state = .info(string, .generic)
|
||||
} else if let _ = user.botInfo {
|
||||
let statusText = self.strings.Bot_GenericBotStatus
|
||||
|
||||
let string = NSAttributedString(string: statusText, font: Font.regular(13.0), textColor: self.theme.rootController.navigationBar.secondaryTextColor)
|
||||
let string = NSAttributedString(string: statusText, font: Font.regular(13.0), textColor: titleTheme.rootController.navigationBar.secondaryTextColor)
|
||||
state = .info(string, .generic)
|
||||
} else if let peer = peerViewMainPeer(peerView) {
|
||||
let timestamp = CFAbsoluteTimeGetCurrent() + NSTimeIntervalSince1970
|
||||
|
|
@ -383,10 +296,10 @@ final class ChatTitleView: UIView, NavigationBarTitleView {
|
|||
userPresence = TelegramUserPresence(status: .none, lastActivity: 0)
|
||||
}
|
||||
let (string, activity) = stringAndActivityForUserPresence(strings: self.strings, dateTimeFormat: self.dateTimeFormat, presence: userPresence, relativeTo: Int32(timestamp))
|
||||
let attributedString = NSAttributedString(string: string, font: Font.regular(13.0), textColor: activity ? self.theme.rootController.navigationBar.accentTextColor : self.theme.rootController.navigationBar.secondaryTextColor)
|
||||
let attributedString = NSAttributedString(string: string, font: Font.regular(13.0), textColor: activity ? titleTheme.rootController.navigationBar.accentTextColor : titleTheme.rootController.navigationBar.secondaryTextColor)
|
||||
state = .info(attributedString, activity ? .online : .lastSeenTime)
|
||||
} else {
|
||||
let string = NSAttributedString(string: "", font: Font.regular(13.0), textColor: self.theme.rootController.navigationBar.secondaryTextColor)
|
||||
let string = NSAttributedString(string: "", font: Font.regular(13.0), textColor: titleTheme.rootController.navigationBar.secondaryTextColor)
|
||||
state = .info(string, .generic)
|
||||
}
|
||||
} else if let group = peer as? TelegramGroup {
|
||||
|
|
@ -408,11 +321,11 @@ final class ChatTitleView: UIView, NavigationBarTitleView {
|
|||
if onlineCount > 1 {
|
||||
let string = NSMutableAttributedString()
|
||||
|
||||
string.append(NSAttributedString(string: "\(strings.Conversation_StatusMembers(Int32(group.participantCount))), ", font: Font.regular(13.0), textColor: self.theme.rootController.navigationBar.secondaryTextColor))
|
||||
string.append(NSAttributedString(string: strings.Conversation_StatusOnline(Int32(onlineCount)), font: Font.regular(13.0), textColor: self.theme.rootController.navigationBar.secondaryTextColor))
|
||||
string.append(NSAttributedString(string: "\(strings.Conversation_StatusMembers(Int32(group.participantCount))), ", font: Font.regular(13.0), textColor: titleTheme.rootController.navigationBar.secondaryTextColor))
|
||||
string.append(NSAttributedString(string: strings.Conversation_StatusOnline(Int32(onlineCount)), font: Font.regular(13.0), textColor: titleTheme.rootController.navigationBar.secondaryTextColor))
|
||||
state = .info(string, .generic)
|
||||
} else {
|
||||
let string = NSAttributedString(string: strings.Conversation_StatusMembers(Int32(group.participantCount)), font: Font.regular(13.0), textColor: self.theme.rootController.navigationBar.secondaryTextColor)
|
||||
let string = NSAttributedString(string: strings.Conversation_StatusMembers(Int32(group.participantCount)), font: Font.regular(13.0), textColor: titleTheme.rootController.navigationBar.secondaryTextColor)
|
||||
state = .info(string, .generic)
|
||||
}
|
||||
} else if let channel = peer as? TelegramChannel {
|
||||
|
|
@ -420,17 +333,17 @@ final class ChatTitleView: UIView, NavigationBarTitleView {
|
|||
if memberCount == 0 {
|
||||
let string: NSAttributedString
|
||||
if case .group = channel.info {
|
||||
string = NSAttributedString(string: strings.Group_Status, font: Font.regular(13.0), textColor: self.theme.rootController.navigationBar.secondaryTextColor)
|
||||
string = NSAttributedString(string: strings.Group_Status, font: Font.regular(13.0), textColor: titleTheme.rootController.navigationBar.secondaryTextColor)
|
||||
} else {
|
||||
string = NSAttributedString(string: strings.Channel_Status, font: Font.regular(13.0), textColor: self.theme.rootController.navigationBar.secondaryTextColor)
|
||||
string = NSAttributedString(string: strings.Channel_Status, font: Font.regular(13.0), textColor: titleTheme.rootController.navigationBar.secondaryTextColor)
|
||||
}
|
||||
state = .info(string, .generic)
|
||||
} else {
|
||||
if case .group = channel.info, let onlineMemberCount = onlineMemberCount, onlineMemberCount > 1 {
|
||||
let string = NSMutableAttributedString()
|
||||
|
||||
string.append(NSAttributedString(string: "\(strings.Conversation_StatusMembers(Int32(memberCount))), ", font: Font.regular(13.0), textColor: self.theme.rootController.navigationBar.secondaryTextColor))
|
||||
string.append(NSAttributedString(string: strings.Conversation_StatusOnline(Int32(onlineMemberCount)), font: Font.regular(13.0), textColor: self.theme.rootController.navigationBar.secondaryTextColor))
|
||||
string.append(NSAttributedString(string: "\(strings.Conversation_StatusMembers(Int32(memberCount))), ", font: Font.regular(13.0), textColor: titleTheme.rootController.navigationBar.secondaryTextColor))
|
||||
string.append(NSAttributedString(string: strings.Conversation_StatusOnline(Int32(onlineMemberCount)), font: Font.regular(13.0), textColor: titleTheme.rootController.navigationBar.secondaryTextColor))
|
||||
state = .info(string, .generic)
|
||||
} else {
|
||||
let membersString: String
|
||||
|
|
@ -439,17 +352,17 @@ final class ChatTitleView: UIView, NavigationBarTitleView {
|
|||
} else {
|
||||
membersString = strings.Conversation_StatusSubscribers(memberCount)
|
||||
}
|
||||
let string = NSAttributedString(string: membersString, font: Font.regular(13.0), textColor: self.theme.rootController.navigationBar.secondaryTextColor)
|
||||
let string = NSAttributedString(string: membersString, font: Font.regular(13.0), textColor: titleTheme.rootController.navigationBar.secondaryTextColor)
|
||||
state = .info(string, .generic)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
switch channel.info {
|
||||
case .group:
|
||||
let string = NSAttributedString(string: strings.Group_Status, font: Font.regular(13.0), textColor: self.theme.rootController.navigationBar.secondaryTextColor)
|
||||
let string = NSAttributedString(string: strings.Group_Status, font: Font.regular(13.0), textColor: titleTheme.rootController.navigationBar.secondaryTextColor)
|
||||
state = .info(string, .generic)
|
||||
case .broadcast:
|
||||
let string = NSAttributedString(string: strings.Channel_Status, font: Font.regular(13.0), textColor: self.theme.rootController.navigationBar.secondaryTextColor)
|
||||
let string = NSAttributedString(string: strings.Channel_Status, font: Font.regular(13.0), textColor: titleTheme.rootController.navigationBar.secondaryTextColor)
|
||||
state = .info(string, .generic)
|
||||
}
|
||||
}
|
||||
|
|
@ -551,11 +464,11 @@ final class ChatTitleView: UIView, NavigationBarTitleView {
|
|||
}
|
||||
}
|
||||
|
||||
func updateThemeAndStrings(theme: PresentationTheme, strings: PresentationStrings) {
|
||||
func updateThemeAndStrings(theme: PresentationTheme, strings: PresentationStrings, hasEmbeddedTitleContent: Bool) {
|
||||
self.theme = theme
|
||||
self.hasEmbeddedTitleContent = hasEmbeddedTitleContent
|
||||
self.strings = strings
|
||||
|
||||
//self.networkStatusNode?.updateTheme(theme: theme)
|
||||
let titleContent = self.titleContent
|
||||
self.titleContent = titleContent
|
||||
self.updateStatus()
|
||||
|
|
@ -568,8 +481,6 @@ final class ChatTitleView: UIView, NavigationBarTitleView {
|
|||
func updateLayout(size: CGSize, clearBounds: CGRect, transition: ContainedViewLayoutTransition) {
|
||||
self.validLayout = (size, clearBounds)
|
||||
|
||||
let transition: ContainedViewLayoutTransition = .immediate
|
||||
|
||||
self.button.frame = clearBounds
|
||||
self.contentContainer.frame = clearBounds
|
||||
|
||||
|
|
|
|||
|
|
@ -115,7 +115,7 @@ private final class DrawingStickersScreenNode: ViewControllerTracingNode {
|
|||
return nil
|
||||
}, reactionContainerNode: {
|
||||
return nil
|
||||
}, presentGlobalOverlayController: { _, _ in }, callPeer: { _ in }, longTap: { _, _ in }, openCheckoutOrReceipt: { _ in }, openSearch: { }, setupReply: { _ in
|
||||
}, presentGlobalOverlayController: { _, _ in }, callPeer: { _, _ in }, longTap: { _, _ in }, openCheckoutOrReceipt: { _ in }, openSearch: { }, setupReply: { _ in
|
||||
}, canSetupReply: { _ in
|
||||
return .none
|
||||
}, navigateToFirstDateMessage: { _ in
|
||||
|
|
|
|||
|
|
@ -11,6 +11,9 @@ public final class OverlayMediaControllerImpl: ViewController, OverlayMediaContr
|
|||
return self.displayNode as! OverlayMediaControllerNode
|
||||
}
|
||||
|
||||
public var updatePossibleEmbeddingItem: ((OverlayMediaControllerEmbeddingItem?) -> Void)?
|
||||
public var embedPossibleEmbeddingItem: ((OverlayMediaControllerEmbeddingItem) -> Bool)?
|
||||
|
||||
public init() {
|
||||
super.init(navigationBarPresentationData: nil)
|
||||
|
||||
|
|
@ -22,7 +25,11 @@ public final class OverlayMediaControllerImpl: ViewController, OverlayMediaContr
|
|||
}
|
||||
|
||||
override public func loadDisplayNode() {
|
||||
self.displayNode = OverlayMediaControllerNode()
|
||||
self.displayNode = OverlayMediaControllerNode(updatePossibleEmbeddingItem: { [weak self] item in
|
||||
self?.updatePossibleEmbeddingItem?(item)
|
||||
}, embedPossibleEmbeddingItem: { [weak self] item in
|
||||
return self?.embedPossibleEmbeddingItem?(item) ?? false
|
||||
})
|
||||
self.displayNodeDidLoad()
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -28,7 +28,12 @@ private final class OverlayMediaVideoNodeData {
|
|||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
final class OverlayMediaControllerNode: ASDisplayNode, UIGestureRecognizerDelegate {
|
||||
private let updatePossibleEmbeddingItem: (OverlayMediaControllerEmbeddingItem?) -> Void
|
||||
private let embedPossibleEmbeddingItem: (OverlayMediaControllerEmbeddingItem) -> Bool
|
||||
|
||||
private var videoNodes: [OverlayMediaVideoNodeData] = []
|
||||
private var validLayout: ContainerViewLayout?
|
||||
|
||||
|
|
@ -40,7 +45,10 @@ final class OverlayMediaControllerNode: ASDisplayNode, UIGestureRecognizerDelega
|
|||
private var pinchingNode: OverlayMediaItemNode?
|
||||
private var pinchingNodeInitialSize: CGSize?
|
||||
|
||||
override init() {
|
||||
init(updatePossibleEmbeddingItem: @escaping (OverlayMediaControllerEmbeddingItem?) -> Void, embedPossibleEmbeddingItem: @escaping (OverlayMediaControllerEmbeddingItem) -> Bool) {
|
||||
self.updatePossibleEmbeddingItem = updatePossibleEmbeddingItem
|
||||
self.embedPossibleEmbeddingItem = embedPossibleEmbeddingItem
|
||||
|
||||
super.init()
|
||||
|
||||
self.setViewBlock({
|
||||
|
|
@ -329,34 +337,46 @@ final class OverlayMediaControllerNode: ASDisplayNode, UIGestureRecognizerDelega
|
|||
draggingNode.updateMinimizedEdge(nil, adjusting: true)
|
||||
}
|
||||
draggingNode.frame = nodeFrame
|
||||
self.updatePossibleEmbeddingItem(OverlayMediaControllerEmbeddingItem(
|
||||
position: nodeFrame.center,
|
||||
itemNode: draggingNode
|
||||
))
|
||||
}
|
||||
case .ended, .cancelled:
|
||||
if let draggingNode = self.draggingNode, let validLayout = self.validLayout, let index = self.videoNodes.firstIndex(where: { $0.node === draggingNode }){
|
||||
let nodeSize = self.videoNodes[index].currentSize
|
||||
let previousFrame = draggingNode.frame
|
||||
|
||||
let (updatedLocation, shouldDismiss) = self.nodeLocationForPosition(layout: validLayout, position: CGPoint(x: previousFrame.midX, y: previousFrame.midY), velocity: recognizer.velocity(in: self.view), size: nodeSize, tempExtendedTopInset: draggingNode.tempExtendedTopInset)
|
||||
|
||||
if shouldDismiss && draggingNode.isMinimizeable {
|
||||
draggingNode.updateMinimizedEdge(updatedLocation.x.isZero ? .left : .right, adjusting: false)
|
||||
self.videoNodes[index].isMinimized = true
|
||||
if self.embedPossibleEmbeddingItem(OverlayMediaControllerEmbeddingItem(
|
||||
position: previousFrame.center,
|
||||
itemNode: draggingNode
|
||||
)) {
|
||||
self.draggingNode = nil
|
||||
} else {
|
||||
draggingNode.updateMinimizedEdge(nil, adjusting: true)
|
||||
self.videoNodes[index].isMinimized = false
|
||||
}
|
||||
|
||||
if let group = draggingNode.group {
|
||||
self.locationByGroup[group] = updatedLocation
|
||||
}
|
||||
self.videoNodes[index].location = updatedLocation
|
||||
|
||||
draggingNode.frame = CGRect(origin: self.nodePosition(layout: validLayout, size: nodeSize, location: updatedLocation, hidden: !draggingNode.hasAttachedContext, isMinimized: self.videoNodes[index].isMinimized, tempExtendedTopInset: draggingNode.tempExtendedTopInset), size: nodeSize)
|
||||
draggingNode.layer.animateFrame(from: previousFrame, to: draggingNode.frame, duration: 0.3, timingFunction: kCAMediaTimingFunctionSpring)
|
||||
self.draggingNode = nil
|
||||
|
||||
if shouldDismiss && !draggingNode.isMinimizeable {
|
||||
draggingNode.dismiss()
|
||||
let (updatedLocation, shouldDismiss) = self.nodeLocationForPosition(layout: validLayout, position: CGPoint(x: previousFrame.midX, y: previousFrame.midY), velocity: recognizer.velocity(in: self.view), size: nodeSize, tempExtendedTopInset: draggingNode.tempExtendedTopInset)
|
||||
|
||||
if shouldDismiss && draggingNode.isMinimizeable {
|
||||
draggingNode.updateMinimizedEdge(updatedLocation.x.isZero ? .left : .right, adjusting: false)
|
||||
self.videoNodes[index].isMinimized = true
|
||||
} else {
|
||||
draggingNode.updateMinimizedEdge(nil, adjusting: true)
|
||||
self.videoNodes[index].isMinimized = false
|
||||
}
|
||||
|
||||
if let group = draggingNode.group {
|
||||
self.locationByGroup[group] = updatedLocation
|
||||
}
|
||||
self.videoNodes[index].location = updatedLocation
|
||||
|
||||
draggingNode.frame = CGRect(origin: self.nodePosition(layout: validLayout, size: nodeSize, location: updatedLocation, hidden: !draggingNode.hasAttachedContext, isMinimized: self.videoNodes[index].isMinimized, tempExtendedTopInset: draggingNode.tempExtendedTopInset), size: nodeSize)
|
||||
draggingNode.layer.animateFrame(from: previousFrame, to: draggingNode.frame, duration: 0.3, timingFunction: kCAMediaTimingFunctionSpring)
|
||||
self.draggingNode = nil
|
||||
|
||||
if shouldDismiss && !draggingNode.isMinimizeable {
|
||||
draggingNode.dismiss()
|
||||
}
|
||||
}
|
||||
self.updatePossibleEmbeddingItem(nil)
|
||||
}
|
||||
default:
|
||||
break
|
||||
|
|
|
|||
|
|
@ -98,7 +98,7 @@ final class OverlayAudioPlayerControllerNode: ViewControllerTracingNode, UIGestu
|
|||
}, reactionContainerNode: {
|
||||
return nil
|
||||
}, presentGlobalOverlayController: { _, _ in
|
||||
}, callPeer: { _ in
|
||||
}, callPeer: { _, _ in
|
||||
}, longTap: { _, _ in
|
||||
}, openCheckoutOrReceipt: { _ in
|
||||
}, openSearch: {
|
||||
|
|
@ -234,7 +234,7 @@ final class OverlayAudioPlayerControllerNode: ViewControllerTracingNode, UIGestu
|
|||
|
||||
openMessageImpl = { [weak self] id in
|
||||
if let strongSelf = self, strongSelf.isNodeLoaded, let message = strongSelf.historyNode.messageInCurrentHistoryView(id) {
|
||||
return strongSelf.context.sharedContext.openChatMessage(OpenChatMessageParams(context: strongSelf.context, message: message, standalone: false, reverseMessageGalleryOrder: false, navigationController: nil, dismissInput: { }, present: { _, _ in }, transitionNode: { _, _ in return nil }, addToTransitionSurface: { _ in }, openUrl: { _ in }, openPeer: { _, _ in }, callPeer: { _ in }, enqueueMessage: { _ in }, sendSticker: nil, setupTemporaryHiddenMedia: { _, _, _ in }, chatAvatarHiddenMedia: { _, _ in }))
|
||||
return strongSelf.context.sharedContext.openChatMessage(OpenChatMessageParams(context: strongSelf.context, message: message, standalone: false, reverseMessageGalleryOrder: false, navigationController: nil, dismissInput: { }, present: { _, _ in }, transitionNode: { _, _ in return nil }, addToTransitionSurface: { _ in }, openUrl: { _ in }, openPeer: { _, _ in }, callPeer: { _, _ in }, enqueueMessage: { _ in }, sendSticker: nil, setupTemporaryHiddenMedia: { _, _, _ in }, chatAvatarHiddenMedia: { _, _ in }))
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
|
|
|||
|
|
@ -704,7 +704,7 @@ func availableActionsForMemberOfPeer(accountPeerId: PeerId, peer: Peer, member:
|
|||
return result
|
||||
}
|
||||
|
||||
func peerInfoHeaderButtons(peer: Peer?, cachedData: CachedPeerData?, isOpenedFromChat: Bool) -> [PeerInfoHeaderButtonKey] {
|
||||
func peerInfoHeaderButtons(peer: Peer?, cachedData: CachedPeerData?, isOpenedFromChat: Bool, videoCallsEnabled: Bool) -> [PeerInfoHeaderButtonKey] {
|
||||
var result: [PeerInfoHeaderButtonKey] = []
|
||||
if let user = peer as? TelegramUser {
|
||||
if !isOpenedFromChat {
|
||||
|
|
@ -719,6 +719,9 @@ func peerInfoHeaderButtons(peer: Peer?, cachedData: CachedPeerData?, isOpenedFro
|
|||
}
|
||||
if callsAvailable {
|
||||
result.append(.call)
|
||||
if videoCallsEnabled {
|
||||
result.append(.videoCall)
|
||||
}
|
||||
}
|
||||
result.append(.mute)
|
||||
if isOpenedFromChat {
|
||||
|
|
|
|||
|
|
@ -21,6 +21,7 @@ enum PeerInfoHeaderButtonKey: Hashable {
|
|||
case message
|
||||
case discussion
|
||||
case call
|
||||
case videoCall
|
||||
case mute
|
||||
case more
|
||||
case addMember
|
||||
|
|
@ -31,6 +32,7 @@ enum PeerInfoHeaderButtonKey: Hashable {
|
|||
enum PeerInfoHeaderButtonIcon {
|
||||
case message
|
||||
case call
|
||||
case videoCall
|
||||
case mute
|
||||
case unmute
|
||||
case more
|
||||
|
|
@ -103,6 +105,8 @@ final class PeerInfoHeaderButtonNode: HighlightableButtonNode {
|
|||
imageName = "Peer Info/ButtonMessage"
|
||||
case .call:
|
||||
imageName = "Peer Info/ButtonCall"
|
||||
case .videoCall:
|
||||
imageName = "Peer Info/ButtonVideo"
|
||||
case .mute:
|
||||
imageName = "Peer Info/ButtonMute"
|
||||
case .unmute:
|
||||
|
|
@ -116,7 +120,7 @@ final class PeerInfoHeaderButtonNode: HighlightableButtonNode {
|
|||
case .leave:
|
||||
imageName = "Peer Info/ButtonLeave"
|
||||
}
|
||||
if let image = UIImage(bundleImageName: imageName) {
|
||||
if let image = generateTintedImage(image: UIImage(bundleImageName: imageName), color: .white) {
|
||||
let imageRect = CGRect(origin: CGPoint(x: floor((size.width - image.size.width) / 2.0), y: floor((size.height - image.size.height) / 2.0)), size: image.size)
|
||||
context.clip(to: imageRect, mask: image.cgImage!)
|
||||
context.fill(imageRect)
|
||||
|
|
@ -1657,6 +1661,7 @@ final class PeerInfoHeaderNode: ASDisplayNode {
|
|||
private var presentationData: PresentationData?
|
||||
|
||||
private let isOpenedFromChat: Bool
|
||||
private let videoCallsEnabled: Bool
|
||||
|
||||
private(set) var isAvatarExpanded: Bool
|
||||
|
||||
|
|
@ -1689,6 +1694,7 @@ final class PeerInfoHeaderNode: ASDisplayNode {
|
|||
self.context = context
|
||||
self.isAvatarExpanded = avatarInitiallyExpanded
|
||||
self.isOpenedFromChat = isOpenedFromChat
|
||||
self.videoCallsEnabled = context.sharedContext.immediateExperimentalUISettings.videoCalls
|
||||
|
||||
self.avatarListNode = PeerInfoAvatarListNode(context: context, readyWhenGalleryLoads: avatarInitiallyExpanded)
|
||||
|
||||
|
|
@ -1875,7 +1881,7 @@ final class PeerInfoHeaderNode: ASDisplayNode {
|
|||
let expandedAvatarListHeight = min(width, containerHeight - expandedAvatarControlsHeight)
|
||||
let expandedAvatarListSize = CGSize(width: width, height: expandedAvatarListHeight)
|
||||
|
||||
let buttonKeys: [PeerInfoHeaderButtonKey] = peerInfoHeaderButtons(peer: peer, cachedData: cachedData, isOpenedFromChat: self.isOpenedFromChat)
|
||||
let buttonKeys: [PeerInfoHeaderButtonKey] = peerInfoHeaderButtons(peer: peer, cachedData: cachedData, isOpenedFromChat: self.isOpenedFromChat, videoCallsEnabled: self.videoCallsEnabled)
|
||||
|
||||
var isVerified = false
|
||||
let titleString: NSAttributedString
|
||||
|
|
@ -2246,6 +2252,9 @@ final class PeerInfoHeaderNode: ASDisplayNode {
|
|||
case .call:
|
||||
buttonText = presentationData.strings.PeerInfo_ButtonCall
|
||||
buttonIcon = .call
|
||||
case .videoCall:
|
||||
buttonText = presentationData.strings.PeerInfo_ButtonVideoCall
|
||||
buttonIcon = .videoCall
|
||||
case .mute:
|
||||
if let notificationSettings = notificationSettings, case .muted = notificationSettings.muteState {
|
||||
buttonText = presentationData.strings.PeerInfo_ButtonUnmute
|
||||
|
|
@ -2279,14 +2288,14 @@ final class PeerInfoHeaderNode: ASDisplayNode {
|
|||
if buttonKeys.count > 3 {
|
||||
if self.isOpenedFromChat {
|
||||
switch buttonKey {
|
||||
case .message, .search:
|
||||
case .message, .search, .videoCall:
|
||||
hiddenWhileExpanded = true
|
||||
default:
|
||||
hiddenWhileExpanded = false
|
||||
}
|
||||
} else {
|
||||
switch buttonKey {
|
||||
case .mute, .search:
|
||||
case .mute, .search, .videoCall:
|
||||
hiddenWhileExpanded = true
|
||||
default:
|
||||
hiddenWhileExpanded = false
|
||||
|
|
|
|||
|
|
@ -395,7 +395,7 @@ final class PeerInfoSelectionPanelNode: ASDisplayNode {
|
|||
}, presentPeerContact: {
|
||||
}, dismissReportPeer: {
|
||||
}, deleteChat: {
|
||||
}, beginCall: {
|
||||
}, beginCall: { _ in
|
||||
}, toggleMessageStickerStarred: { _ in
|
||||
}, presentController: { _, _ in
|
||||
}, getNavigationController: {
|
||||
|
|
@ -1035,6 +1035,7 @@ private final class PeerInfoScreenNode: ViewControllerTracingNode, UIScrollViewD
|
|||
private let context: AccountContext
|
||||
private let peerId: PeerId
|
||||
private let isOpenedFromChat: Bool
|
||||
private let videoCallsEnabled: Bool
|
||||
private let callMessages: [Message]
|
||||
|
||||
private let isMediaOnly: Bool
|
||||
|
|
@ -1096,6 +1097,7 @@ private final class PeerInfoScreenNode: ViewControllerTracingNode, UIScrollViewD
|
|||
self.context = context
|
||||
self.peerId = peerId
|
||||
self.isOpenedFromChat = isOpenedFromChat
|
||||
self.videoCallsEnabled = context.sharedContext.immediateExperimentalUISettings.videoCalls
|
||||
self.presentationData = context.sharedContext.currentPresentationData.with { $0 }
|
||||
self.nearbyPeerDistance = nearbyPeerDistance
|
||||
self.callMessages = callMessages
|
||||
|
|
@ -1529,7 +1531,7 @@ private final class PeerInfoScreenNode: ViewControllerTracingNode, UIScrollViewD
|
|||
return nil
|
||||
}, reactionContainerNode: {
|
||||
return nil
|
||||
}, presentGlobalOverlayController: { _, _ in }, callPeer: { _ in
|
||||
}, presentGlobalOverlayController: { _, _ in }, callPeer: { _, _ in
|
||||
}, longTap: { [weak self] content, _ in
|
||||
guard let strongSelf = self else {
|
||||
return
|
||||
|
|
@ -2133,7 +2135,7 @@ private final class PeerInfoScreenNode: ViewControllerTracingNode, UIScrollViewD
|
|||
self?.openUrl(url: url, concealed: false, external: false)
|
||||
}, openPeer: { [weak self] peer, navigation in
|
||||
self?.openPeer(peerId: peer.id, navigation: navigation)
|
||||
}, callPeer: { peerId in
|
||||
}, callPeer: { peerId, isVideo in
|
||||
//self?.controllerInteraction?.callPeer(peerId)
|
||||
}, enqueueMessage: { _ in
|
||||
}, sendSticker: nil, setupTemporaryHiddenMedia: { _, _, _ in }, chatAvatarHiddenMedia: { _, _ in }))
|
||||
|
|
@ -2213,7 +2215,9 @@ private final class PeerInfoScreenNode: ViewControllerTracingNode, UIScrollViewD
|
|||
}
|
||||
}
|
||||
case .call:
|
||||
self.requestCall()
|
||||
self.requestCall(isVideo: false)
|
||||
case .videoCall:
|
||||
self.requestCall(isVideo: true)
|
||||
case .mute:
|
||||
if let notificationSettings = self.data?.notificationSettings, case .muted = notificationSettings.muteState {
|
||||
let _ = updatePeerMuteSetting(account: self.context.account, peerId: self.peerId, muteInterval: nil).start()
|
||||
|
|
@ -2259,7 +2263,7 @@ private final class PeerInfoScreenNode: ViewControllerTracingNode, UIScrollViewD
|
|||
actionSheet?.dismissAnimated()
|
||||
}
|
||||
var items: [ActionSheetItem] = []
|
||||
if !peerInfoHeaderButtons(peer: peer, cachedData: data.cachedData, isOpenedFromChat: self.isOpenedFromChat).contains(.search) || self.headerNode.isAvatarExpanded {
|
||||
if !peerInfoHeaderButtons(peer: peer, cachedData: data.cachedData, isOpenedFromChat: self.isOpenedFromChat, videoCallsEnabled: self.videoCallsEnabled).contains(.search) || self.headerNode.isAvatarExpanded {
|
||||
items.append(ActionSheetButtonItem(title: presentationData.strings.ChatSearch_SearchPlaceholder, color: .accent, action: { [weak self] in
|
||||
dismissAction()
|
||||
self?.openChatWithMessageSearch()
|
||||
|
|
@ -2361,7 +2365,7 @@ private final class PeerInfoScreenNode: ViewControllerTracingNode, UIScrollViewD
|
|||
self?.openDeletePeer()
|
||||
}))
|
||||
} else {
|
||||
if !peerInfoHeaderButtons(peer: peer, cachedData: data.cachedData, isOpenedFromChat: self.isOpenedFromChat).contains(.leave) {
|
||||
if !peerInfoHeaderButtons(peer: peer, cachedData: data.cachedData, isOpenedFromChat: self.isOpenedFromChat, videoCallsEnabled: self.videoCallsEnabled).contains(.leave) {
|
||||
if case .member = channel.participationStatus {
|
||||
items.append(ActionSheetButtonItem(title: presentationData.strings.Channel_LeaveChannel, color: .destructive, action: { [weak self] in
|
||||
dismissAction()
|
||||
|
|
@ -2510,7 +2514,7 @@ private final class PeerInfoScreenNode: ViewControllerTracingNode, UIScrollViewD
|
|||
self.controller?.present(shareController, in: .window(.root))
|
||||
}
|
||||
|
||||
private func requestCall() {
|
||||
private func requestCall(isVideo: Bool) {
|
||||
guard let peer = self.data?.peer as? TelegramUser, let cachedUserData = self.data?.cachedData as? CachedUserData else {
|
||||
return
|
||||
}
|
||||
|
|
@ -2519,7 +2523,7 @@ private final class PeerInfoScreenNode: ViewControllerTracingNode, UIScrollViewD
|
|||
return
|
||||
}
|
||||
|
||||
let callResult = self.context.sharedContext.callManager?.requestCall(account: self.context.account, peerId: peer.id, endCurrentIfAny: false)
|
||||
let callResult = self.context.sharedContext.callManager?.requestCall(account: self.context.account, peerId: peer.id, isVideo: isVideo, endCurrentIfAny: false)
|
||||
if let callResult = callResult, case let .alreadyInProgress(currentPeerId) = callResult {
|
||||
if currentPeerId == peer.id {
|
||||
self.context.sharedContext.navigateToCurrentCall()
|
||||
|
|
@ -2536,7 +2540,7 @@ private final class PeerInfoScreenNode: ViewControllerTracingNode, UIScrollViewD
|
|||
guard let strongSelf = self else {
|
||||
return
|
||||
}
|
||||
let _ = strongSelf.context.sharedContext.callManager?.requestCall(account: strongSelf.context.account, peerId: peer.id, endCurrentIfAny: true)
|
||||
let _ = strongSelf.context.sharedContext.callManager?.requestCall(account: strongSelf.context.account, peerId: peer.id, isVideo: isVideo, endCurrentIfAny: true)
|
||||
})]), in: .window(.root))
|
||||
}
|
||||
})
|
||||
|
|
@ -2559,7 +2563,7 @@ private final class PeerInfoScreenNode: ViewControllerTracingNode, UIScrollViewD
|
|||
ActionSheetItemGroup(items: [
|
||||
ActionSheetButtonItem(title: strongSelf.presentationData.strings.UserInfo_TelegramCall, action: {
|
||||
dismissAction()
|
||||
self?.requestCall()
|
||||
self?.requestCall(isVideo: false)
|
||||
}),
|
||||
ActionSheetButtonItem(title: strongSelf.presentationData.strings.UserInfo_PhoneCall, action: {
|
||||
dismissAction()
|
||||
|
|
|
|||
|
|
@ -127,8 +127,8 @@ public class PeerMediaCollectionController: TelegramBaseController {
|
|||
self?.openUrl(url)
|
||||
}, openPeer: { peer, navigation in
|
||||
self?.controllerInteraction?.openPeer(peer.id, navigation, nil)
|
||||
}, callPeer: { peerId in
|
||||
self?.controllerInteraction?.callPeer(peerId)
|
||||
}, callPeer: { peerId, isVideo in
|
||||
self?.controllerInteraction?.callPeer(peerId, isVideo)
|
||||
}, enqueueMessage: { _ in
|
||||
}, sendSticker: nil, setupTemporaryHiddenMedia: { _, _, _ in }, chatAvatarHiddenMedia: { _, _ in }))
|
||||
}
|
||||
|
|
@ -357,7 +357,7 @@ public class PeerMediaCollectionController: TelegramBaseController {
|
|||
return nil
|
||||
}, reactionContainerNode: {
|
||||
return nil
|
||||
}, presentGlobalOverlayController: { _, _ in }, callPeer: { _ in
|
||||
}, presentGlobalOverlayController: { _, _ in }, callPeer: { _, _ in
|
||||
}, longTap: { [weak self] content, _ in
|
||||
if let strongSelf = self {
|
||||
strongSelf.view.endEditing(true)
|
||||
|
|
@ -530,7 +530,7 @@ public class PeerMediaCollectionController: TelegramBaseController {
|
|||
}, presentPeerContact: {
|
||||
}, dismissReportPeer: {
|
||||
}, deleteChat: {
|
||||
}, beginCall: {
|
||||
}, beginCall: { _ in
|
||||
}, toggleMessageStickerStarred: { _ in
|
||||
}, presentController: { _, _ in
|
||||
}, getNavigationController: {
|
||||
|
|
|
|||
|
|
@ -207,6 +207,43 @@ public final class SharedAccountContextImpl: SharedAccountContext {
|
|||
|
||||
self.mediaManager = MediaManagerImpl(accountManager: accountManager, inForeground: applicationBindings.applicationInForeground, presentationData: presentationData)
|
||||
|
||||
self.mediaManager.overlayMediaManager.updatePossibleEmbeddingItem = { [weak self] item in
|
||||
guard let strongSelf = self else {
|
||||
return
|
||||
}
|
||||
guard let navigationController = strongSelf.mainWindow?.viewController as? NavigationController else {
|
||||
return
|
||||
}
|
||||
var content: NavigationControllerDropContent?
|
||||
if let item = item {
|
||||
content = NavigationControllerDropContent(
|
||||
position: item.position,
|
||||
item: VideoNavigationControllerDropContentItem(
|
||||
itemNode: item.itemNode
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
navigationController.updatePossibleControllerDropContent(content: content)
|
||||
}
|
||||
|
||||
self.mediaManager.overlayMediaManager.embedPossibleEmbeddingItem = { [weak self] item in
|
||||
guard let strongSelf = self else {
|
||||
return false
|
||||
}
|
||||
guard let navigationController = strongSelf.mainWindow?.viewController as? NavigationController else {
|
||||
return false
|
||||
}
|
||||
let content = NavigationControllerDropContent(
|
||||
position: item.position,
|
||||
item: VideoNavigationControllerDropContentItem(
|
||||
itemNode: item.itemNode
|
||||
)
|
||||
)
|
||||
|
||||
return navigationController.acceptPossibleControllerDropContent(content: content)
|
||||
}
|
||||
|
||||
self._autodownloadSettings.set(.single(initialPresentationDataAndSettings.autodownloadSettings)
|
||||
|> then(accountManager.sharedData(keys: [SharedDataKeys.autodownloadSettings])
|
||||
|> map { sharedData in
|
||||
|
|
@ -537,7 +574,7 @@ public final class SharedAccountContextImpl: SharedAccountContext {
|
|||
})
|
||||
|
||||
if let mainWindow = mainWindow, applicationBindings.isMainApp {
|
||||
let callManager = PresentationCallManagerImpl(accountManager: self.accountManager, getDeviceAccessData: {
|
||||
let callManager = PresentationCallManagerImpl(accountManager: self.accountManager, enableVideoCalls: self.immediateExperimentalUISettings.videoCalls, getDeviceAccessData: {
|
||||
return (self.currentPresentationData.with { $0 }, { [weak self] c, a in
|
||||
self?.presentGlobalController(c, a)
|
||||
}, {
|
||||
|
|
@ -1120,7 +1157,7 @@ public final class SharedAccountContextImpl: SharedAccountContext {
|
|||
return nil
|
||||
}, reactionContainerNode: {
|
||||
return nil
|
||||
}, presentGlobalOverlayController: { _, _ in }, callPeer: { _ in }, longTap: { _, _ in }, openCheckoutOrReceipt: { _ in }, openSearch: { }, setupReply: { _ in
|
||||
}, presentGlobalOverlayController: { _, _ in }, callPeer: { _, _ in }, longTap: { _, _ in }, openCheckoutOrReceipt: { _ in }, openSearch: { }, setupReply: { _ in
|
||||
}, canSetupReply: { _ in
|
||||
return .none
|
||||
}, navigateToFirstDateMessage: { _ in
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ import TelegramAudio
|
|||
import AccountContext
|
||||
|
||||
public final class OverlayUniversalVideoNode: OverlayMediaItemNode {
|
||||
private let content: UniversalVideoContent
|
||||
public let content: UniversalVideoContent
|
||||
private let videoNode: UniversalVideoNode
|
||||
private let decoration: OverlayVideoDecoration
|
||||
|
||||
|
|
@ -30,8 +30,16 @@ public final class OverlayUniversalVideoNode: OverlayMediaItemNode {
|
|||
}
|
||||
}
|
||||
|
||||
private let defaultExpand: () -> Void
|
||||
public var customExpand: (() -> Void)?
|
||||
public var customClose: (() -> Void)?
|
||||
|
||||
public init(postbox: Postbox, audioSession: ManagedAudioSession, manager: UniversalVideoManager, content: UniversalVideoContent, expand: @escaping () -> Void, close: @escaping () -> Void) {
|
||||
self.content = content
|
||||
self.defaultExpand = expand
|
||||
|
||||
var expandImpl: (() -> Void)?
|
||||
|
||||
var unminimizeImpl: (() -> Void)?
|
||||
var togglePlayPauseImpl: (() -> Void)?
|
||||
var closeImpl: (() -> Void)?
|
||||
|
|
@ -40,7 +48,7 @@ public final class OverlayUniversalVideoNode: OverlayMediaItemNode {
|
|||
}, togglePlayPause: {
|
||||
togglePlayPauseImpl?()
|
||||
}, expand: {
|
||||
expand()
|
||||
expandImpl?()
|
||||
}, close: {
|
||||
closeImpl?()
|
||||
})
|
||||
|
|
@ -49,6 +57,17 @@ public final class OverlayUniversalVideoNode: OverlayMediaItemNode {
|
|||
|
||||
super.init()
|
||||
|
||||
expandImpl = { [weak self] in
|
||||
guard let strongSelf = self else {
|
||||
return
|
||||
}
|
||||
if let customExpand = strongSelf.customExpand {
|
||||
customExpand()
|
||||
} else {
|
||||
strongSelf.defaultExpand()
|
||||
}
|
||||
}
|
||||
|
||||
unminimizeImpl = { [weak self] in
|
||||
self?.unminimize?()
|
||||
}
|
||||
|
|
@ -57,6 +76,10 @@ public final class OverlayUniversalVideoNode: OverlayMediaItemNode {
|
|||
}
|
||||
closeImpl = { [weak self] in
|
||||
if let strongSelf = self {
|
||||
if let customClose = strongSelf.customClose {
|
||||
customClose()
|
||||
return
|
||||
}
|
||||
if strongSelf.videoNode.hasAttachedContext {
|
||||
strongSelf.videoNode.continuePlayingWithoutSound()
|
||||
}
|
||||
|
|
@ -104,18 +127,32 @@ public final class OverlayUniversalVideoNode: OverlayMediaItemNode {
|
|||
|
||||
override public func updateLayout(_ size: CGSize) {
|
||||
if size != self.validLayoutSize {
|
||||
self.updateLayoutImpl(size)
|
||||
self.updateLayoutImpl(size, transition: .immediate)
|
||||
}
|
||||
}
|
||||
|
||||
private func updateLayoutImpl(_ size: CGSize) {
|
||||
public func updateLayout(_ size: CGSize, transition: ContainedViewLayoutTransition) {
|
||||
if size != self.validLayoutSize {
|
||||
self.updateLayoutImpl(size, transition: transition)
|
||||
}
|
||||
}
|
||||
|
||||
private func updateLayoutImpl(_ size: CGSize, transition: ContainedViewLayoutTransition) {
|
||||
self.validLayoutSize = size
|
||||
|
||||
self.videoNode.frame = CGRect(origin: CGPoint(), size: size)
|
||||
self.videoNode.updateLayout(size: size, transition: .immediate)
|
||||
transition.updateFrame(node: self.videoNode, frame: CGRect(origin: CGPoint(), size: size))
|
||||
self.videoNode.updateLayout(size: size, transition: transition)
|
||||
}
|
||||
|
||||
override public func updateMinimizedEdge(_ edge: OverlayMediaItemMinimizationEdge?, adjusting: Bool) {
|
||||
self.decoration.updateMinimizedEdge(edge, adjusting: adjusting)
|
||||
}
|
||||
|
||||
public func updateRoundCorners(_ value: Bool, transition: ContainedViewLayoutTransition) {
|
||||
transition.updateCornerRadius(node: self, cornerRadius: value ? 4.0 : 0.0)
|
||||
}
|
||||
|
||||
public func showControls() {
|
||||
self.decoration.showControls()
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -148,6 +148,13 @@ final class OverlayVideoDecoration: UniversalVideoDecoration {
|
|||
}
|
||||
}
|
||||
|
||||
func showControls() {
|
||||
if self.controlsNode.alpha.isZero {
|
||||
self.controlsNode.alpha = 1.0
|
||||
self.controlsNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.3)
|
||||
}
|
||||
}
|
||||
|
||||
func setStatus(_ status: Signal<MediaPlayerStatus?, NoError>) {
|
||||
self.controlsNode.status = status |> map { value -> MediaPlayerStatus in
|
||||
if let value = value {
|
||||
|
|
|
|||
|
|
@ -109,12 +109,12 @@ final class PictureInPictureVideoControlsNode: ASDisplayNode {
|
|||
|
||||
let buttonSize = TGEmbedPIPButtonSize
|
||||
|
||||
self.leaveButton.frame = CGRect(origin: CGPoint(x: forth - floor(buttonSize.width / 2.0) - 10.0, y: size.height - buttonSize.height - 15.0), size: buttonSize)
|
||||
transition.updateFrame(view: self.leaveButton, frame: CGRect(origin: CGPoint(x: forth - floor(buttonSize.width / 2.0) - 10.0, y: size.height - buttonSize.height - 15.0), size: buttonSize))
|
||||
|
||||
self.pauseButton.frame = CGRect(origin: CGPoint(x: floor((size.width - buttonSize.width) / 2.0), y: size.height - buttonSize.height - 15.0), size: buttonSize)
|
||||
self.playButton.frame = CGRect(origin: CGPoint(x: floor((size.width - buttonSize.width) / 2.0), y: size.height - buttonSize.height - 15.0), size: buttonSize)
|
||||
transition.updateFrame(view: self.pauseButton, frame: CGRect(origin: CGPoint(x: floor((size.width - buttonSize.width) / 2.0), y: size.height - buttonSize.height - 15.0), size: buttonSize))
|
||||
transition.updateFrame(view: self.playButton, frame: CGRect(origin: CGPoint(x: floor((size.width - buttonSize.width) / 2.0), y: size.height - buttonSize.height - 15.0), size: buttonSize))
|
||||
|
||||
self.closeButton.frame = CGRect(origin: CGPoint(x: self.playButton.frame.origin.x + forth + 10.0, y: size.height - buttonSize.height - 15.0), size: buttonSize)
|
||||
transition.updateFrame(view: self.closeButton, frame: CGRect(origin: CGPoint(x: self.playButton.frame.origin.x + forth + 10.0, y: size.height - buttonSize.height - 15.0), size: buttonSize))
|
||||
}
|
||||
|
||||
@objc func leavePressed() {
|
||||
|
|
|
|||
|
|
@ -100,13 +100,22 @@ public struct OngoingCallContextState: Equatable {
|
|||
case reconnecting
|
||||
case failed
|
||||
}
|
||||
|
||||
public enum VideoState: Equatable {
|
||||
case notAvailable
|
||||
case available(Bool)
|
||||
case active
|
||||
case activeOutgoing
|
||||
}
|
||||
|
||||
public enum RemoteVideoState: Equatable {
|
||||
case inactive
|
||||
case active
|
||||
}
|
||||
|
||||
public let state: State
|
||||
public let videoState: VideoState
|
||||
public let remoteVideoState: RemoteVideoState
|
||||
}
|
||||
|
||||
private final class OngoingCallThreadLocalContextQueueImpl: NSObject, OngoingCallThreadLocalContextQueue, OngoingCallThreadLocalContextQueueWebrtc /*, OngoingCallThreadLocalContextQueueWebrtcCustom*/ {
|
||||
|
|
@ -395,6 +404,27 @@ private extension OngoingCallContextState.State {
|
|||
}*/
|
||||
|
||||
public final class OngoingCallContext {
|
||||
public struct AuxiliaryServer {
|
||||
public enum Connection {
|
||||
case stun
|
||||
case turn(username: String, password: String)
|
||||
}
|
||||
|
||||
public let host: String
|
||||
public let port: Int
|
||||
public let connection: Connection
|
||||
|
||||
public init(
|
||||
host: String,
|
||||
port: Int,
|
||||
connection: Connection
|
||||
) {
|
||||
self.host = host
|
||||
self.port = port
|
||||
self.connection = connection
|
||||
}
|
||||
}
|
||||
|
||||
public let internalId: CallSessionInternalId
|
||||
|
||||
private let queue = Queue()
|
||||
|
|
@ -433,7 +463,7 @@ public final class OngoingCallContext {
|
|||
return result
|
||||
}
|
||||
|
||||
public init(account: Account, callSessionManager: CallSessionManager, internalId: CallSessionInternalId, proxyServer: ProxyServerSettings?, initialNetworkType: NetworkType, updatedNetworkType: Signal<NetworkType, NoError>, serializedData: String?, dataSaving: VoiceCallDataSaving, derivedState: VoipDerivedState, key: Data, isOutgoing: Bool, connections: CallSessionConnectionSet, maxLayer: Int32, version: String, allowP2P: Bool, audioSessionActive: Signal<Bool, NoError>, logName: String) {
|
||||
public init(account: Account, callSessionManager: CallSessionManager, internalId: CallSessionInternalId, proxyServer: ProxyServerSettings?, auxiliaryServers: [AuxiliaryServer], initialNetworkType: NetworkType, updatedNetworkType: Signal<NetworkType, NoError>, serializedData: String?, dataSaving: VoiceCallDataSaving, derivedState: VoipDerivedState, key: Data, isOutgoing: Bool, isVideo: Bool, connections: CallSessionConnectionSet, maxLayer: Int32, version: String, allowP2P: Bool, audioSessionActive: Signal<Bool, NoError>, logName: String) {
|
||||
let _ = setupLogs
|
||||
OngoingCallThreadLocalContext.applyServerConfig(serializedData)
|
||||
//OngoingCallThreadLocalContextWebrtc.applyServerConfig(serializedData)
|
||||
|
|
@ -491,12 +521,33 @@ public final class OngoingCallContext {
|
|||
break
|
||||
}
|
||||
}
|
||||
let context = OngoingCallThreadLocalContextWebrtc(queue: OngoingCallThreadLocalContextQueueImpl(queue: queue), proxy: voipProxyServer, networkType: ongoingNetworkTypeForTypeWebrtc(initialNetworkType), dataSaving: ongoingDataSavingForTypeWebrtc(dataSaving), derivedState: derivedState.data, key: key, isOutgoing: isOutgoing, primaryConnection: callConnectionDescriptionWebrtc(connections.primary), alternativeConnections: connections.alternatives.map(callConnectionDescriptionWebrtc), maxLayer: maxLayer, allowP2P: allowP2P, logPath: logPath, sendSignalingData: { [weak callSessionManager] data in
|
||||
var rtcServers: [VoipRtcServerWebrtc] = []
|
||||
for server in auxiliaryServers {
|
||||
switch server.connection {
|
||||
case .stun:
|
||||
rtcServers.append(VoipRtcServerWebrtc(
|
||||
host: server.host,
|
||||
port: Int32(clamping: server.port),
|
||||
username: "",
|
||||
password: "",
|
||||
isTurn: false
|
||||
))
|
||||
case let .turn(username, password):
|
||||
rtcServers.append(VoipRtcServerWebrtc(
|
||||
host: server.host,
|
||||
port: Int32(clamping: server.port),
|
||||
username: username,
|
||||
password: password,
|
||||
isTurn: true
|
||||
))
|
||||
}
|
||||
}
|
||||
let context = OngoingCallThreadLocalContextWebrtc(queue: OngoingCallThreadLocalContextQueueImpl(queue: queue), proxy: voipProxyServer, rtcServers: rtcServers, networkType: ongoingNetworkTypeForTypeWebrtc(initialNetworkType), dataSaving: ongoingDataSavingForTypeWebrtc(dataSaving), derivedState: derivedState.data, key: key, isOutgoing: isOutgoing, isVideo: isVideo, primaryConnection: callConnectionDescriptionWebrtc(connections.primary), alternativeConnections: connections.alternatives.map(callConnectionDescriptionWebrtc), maxLayer: maxLayer, allowP2P: allowP2P, logPath: logPath, sendSignalingData: { [weak callSessionManager] data in
|
||||
callSessionManager?.sendSignalingData(internalId: internalId, data: data)
|
||||
})
|
||||
|
||||
strongSelf.contextRef = Unmanaged.passRetained(OngoingCallThreadLocalContextHolder(context))
|
||||
context.stateChanged = { state, videoState in
|
||||
context.stateChanged = { state, videoState, remoteVideoState in
|
||||
queue.async {
|
||||
guard let strongSelf = self else {
|
||||
return
|
||||
|
|
@ -508,12 +559,21 @@ public final class OngoingCallContext {
|
|||
mappedVideoState = .available(true)
|
||||
case .active:
|
||||
mappedVideoState = .active
|
||||
case .invited, .requesting:
|
||||
mappedVideoState = .available(false)
|
||||
case .activeOutgoing:
|
||||
mappedVideoState = .activeOutgoing
|
||||
@unknown default:
|
||||
mappedVideoState = .available(false)
|
||||
}
|
||||
strongSelf.contextState.set(.single(OngoingCallContextState(state: mappedState, videoState: mappedVideoState)))
|
||||
let mappedRemoteVideoState: OngoingCallContextState.RemoteVideoState
|
||||
switch remoteVideoState {
|
||||
case .inactive:
|
||||
mappedRemoteVideoState = .inactive
|
||||
case .active:
|
||||
mappedRemoteVideoState = .active
|
||||
@unknown default:
|
||||
mappedRemoteVideoState = .inactive
|
||||
}
|
||||
strongSelf.contextState.set(.single(OngoingCallContextState(state: mappedState, videoState: mappedVideoState, remoteVideoState: mappedRemoteVideoState)))
|
||||
}
|
||||
}
|
||||
context.signalBarsChanged = { signalBars in
|
||||
|
|
@ -540,7 +600,7 @@ public final class OngoingCallContext {
|
|||
|
||||
strongSelf.contextRef = Unmanaged.passRetained(OngoingCallThreadLocalContextHolder(context))
|
||||
context.stateChanged = { state in
|
||||
self?.contextState.set(.single(OngoingCallContextState(state: OngoingCallContextState.State(state), videoState: .notAvailable)))
|
||||
self?.contextState.set(.single(OngoingCallContextState(state: OngoingCallContextState.State(state), videoState: .notAvailable, remoteVideoState: .inactive)))
|
||||
}
|
||||
context.signalBarsChanged = { signalBars in
|
||||
self?.receptionPromise.set(.single(signalBars))
|
||||
|
|
|
|||
|
|
@ -20,7 +20,7 @@ std::unique_ptr<webrtc::VideoEncoderFactory> makeVideoEncoderFactory();
|
|||
std::unique_ptr<webrtc::VideoDecoderFactory> makeVideoDecoderFactory();
|
||||
bool supportsH265Encoding();
|
||||
rtc::scoped_refptr<webrtc::VideoTrackSourceInterface> makeVideoSource(rtc::Thread *signalingThread, rtc::Thread *workerThread);
|
||||
std::unique_ptr<VideoCapturerInterface> makeVideoCapturer(rtc::scoped_refptr<webrtc::VideoTrackSourceInterface> source, bool useFrontCamera);
|
||||
std::unique_ptr<VideoCapturerInterface> makeVideoCapturer(rtc::scoped_refptr<webrtc::VideoTrackSourceInterface> source, bool useFrontCamera, std::function<void(bool)> isActiveUpdated);
|
||||
|
||||
#ifdef TGVOIP_NAMESPACE
|
||||
}
|
||||
|
|
|
|||
|
|
@ -40,12 +40,12 @@
|
|||
|
||||
@implementation VideoCapturerInterfaceImplReference
|
||||
|
||||
- (instancetype)initWithSource:(rtc::scoped_refptr<webrtc::VideoTrackSourceInterface>)source useFrontCamera:(bool)useFrontCamera {
|
||||
- (instancetype)initWithSource:(rtc::scoped_refptr<webrtc::VideoTrackSourceInterface>)source useFrontCamera:(bool)useFrontCamera isActiveUpdated:(void (^)(bool))isActiveUpdated {
|
||||
self = [super init];
|
||||
if (self != nil) {
|
||||
assert([NSThread isMainThread]);
|
||||
|
||||
_videoCapturer = [[VideoCameraCapturer alloc] initWithSource:source];
|
||||
_videoCapturer = [[VideoCameraCapturer alloc] initWithSource:source isActiveUpdated:isActiveUpdated];
|
||||
|
||||
AVCaptureDevice *frontCamera = nil;
|
||||
AVCaptureDevice *backCamera = nil;
|
||||
|
|
@ -130,12 +130,14 @@ namespace TGVOIP_NAMESPACE {
|
|||
|
||||
class VideoCapturerInterfaceImpl: public VideoCapturerInterface {
|
||||
public:
|
||||
VideoCapturerInterfaceImpl(rtc::scoped_refptr<webrtc::VideoTrackSourceInterface> source, bool useFrontCamera) :
|
||||
VideoCapturerInterfaceImpl(rtc::scoped_refptr<webrtc::VideoTrackSourceInterface> source, bool useFrontCamera, std::function<void(bool)> isActiveUpdated) :
|
||||
_source(source) {
|
||||
_implReference = [[VideoCapturerInterfaceImplHolder alloc] init];
|
||||
VideoCapturerInterfaceImplHolder *implReference = _implReference;
|
||||
dispatch_async(dispatch_get_main_queue(), ^{
|
||||
VideoCapturerInterfaceImplReference *value = [[VideoCapturerInterfaceImplReference alloc] initWithSource:source useFrontCamera:useFrontCamera];
|
||||
VideoCapturerInterfaceImplReference *value = [[VideoCapturerInterfaceImplReference alloc] initWithSource:source useFrontCamera:useFrontCamera isActiveUpdated:^(bool isActive) {
|
||||
isActiveUpdated(isActive);
|
||||
}];
|
||||
if (value != nil) {
|
||||
implReference.reference = (void *)CFBridgingRetain(value);
|
||||
}
|
||||
|
|
@ -185,8 +187,8 @@ rtc::scoped_refptr<webrtc::VideoTrackSourceInterface> makeVideoSource(rtc::Threa
|
|||
return webrtc::VideoTrackSourceProxy::Create(signalingThread, workerThread, objCVideoTrackSource);
|
||||
}
|
||||
|
||||
std::unique_ptr<VideoCapturerInterface> makeVideoCapturer(rtc::scoped_refptr<webrtc::VideoTrackSourceInterface> source, bool useFrontCamera) {
|
||||
return std::make_unique<VideoCapturerInterfaceImpl>(source, useFrontCamera);
|
||||
std::unique_ptr<VideoCapturerInterface> makeVideoCapturer(rtc::scoped_refptr<webrtc::VideoTrackSourceInterface> source, bool useFrontCamera, std::function<void(bool)> isActiveUpdated) {
|
||||
return std::make_unique<VideoCapturerInterfaceImpl>(source, useFrontCamera, isActiveUpdated);
|
||||
}
|
||||
|
||||
#ifdef TGVOIP_NAMESPACE
|
||||
|
|
|
|||
|
|
@ -36,15 +36,21 @@ Manager::Manager(
|
|||
rtc::Thread *thread,
|
||||
TgVoipEncryptionKey encryptionKey,
|
||||
bool enableP2P,
|
||||
std::vector<TgVoipRtcServer> const &rtcServers,
|
||||
bool isVideo,
|
||||
std::function<void (const TgVoipState &)> stateUpdated,
|
||||
std::function<void (bool)> videoStateUpdated,
|
||||
std::function<void (bool)> remoteVideoIsActiveUpdated,
|
||||
std::function<void (const std::vector<uint8_t> &)> signalingDataEmitted
|
||||
) :
|
||||
_thread(thread),
|
||||
_encryptionKey(encryptionKey),
|
||||
_enableP2P(enableP2P),
|
||||
_rtcServers(rtcServers),
|
||||
_startWithVideo(isVideo),
|
||||
_stateUpdated(stateUpdated),
|
||||
_videoStateUpdated(videoStateUpdated),
|
||||
_remoteVideoIsActiveUpdated(remoteVideoIsActiveUpdated),
|
||||
_signalingDataEmitted(signalingDataEmitted),
|
||||
_isVideoRequested(false) {
|
||||
assert(_thread->IsCurrent());
|
||||
|
|
@ -56,11 +62,12 @@ Manager::~Manager() {
|
|||
|
||||
void Manager::start() {
|
||||
auto weakThis = std::weak_ptr<Manager>(shared_from_this());
|
||||
_networkManager.reset(new ThreadLocalObject<NetworkManager>(getNetworkThread(), [encryptionKey = _encryptionKey, enableP2P = _enableP2P, thread = _thread, weakThis, signalingDataEmitted = _signalingDataEmitted]() {
|
||||
_networkManager.reset(new ThreadLocalObject<NetworkManager>(getNetworkThread(), [encryptionKey = _encryptionKey, enableP2P = _enableP2P, rtcServers = _rtcServers, thread = _thread, weakThis, signalingDataEmitted = _signalingDataEmitted]() {
|
||||
return new NetworkManager(
|
||||
getNetworkThread(),
|
||||
encryptionKey,
|
||||
enableP2P,
|
||||
rtcServers,
|
||||
[thread, weakThis](const NetworkManager::State &state) {
|
||||
thread->PostTask(RTC_FROM_HERE, [weakThis, state]() {
|
||||
auto strongThis = weakThis.lock();
|
||||
|
|
@ -104,10 +111,11 @@ void Manager::start() {
|
|||
);
|
||||
}));
|
||||
bool isOutgoing = _encryptionKey.isOutgoing;
|
||||
_mediaManager.reset(new ThreadLocalObject<MediaManager>(getMediaThread(), [isOutgoing, thread = _thread, weakThis]() {
|
||||
_mediaManager.reset(new ThreadLocalObject<MediaManager>(getMediaThread(), [isOutgoing, thread = _thread, startWithVideo = _startWithVideo, weakThis]() {
|
||||
return new MediaManager(
|
||||
getMediaThread(),
|
||||
isOutgoing,
|
||||
startWithVideo,
|
||||
[thread, weakThis](const rtc::CopyOnWriteBuffer &packet) {
|
||||
thread->PostTask(RTC_FROM_HERE, [weakThis, packet]() {
|
||||
auto strongThis = weakThis.lock();
|
||||
|
|
@ -118,6 +126,15 @@ void Manager::start() {
|
|||
networkManager->sendPacket(packet);
|
||||
});
|
||||
});
|
||||
},
|
||||
[thread, weakThis](bool isActive) {
|
||||
thread->PostTask(RTC_FROM_HERE, [weakThis, isActive]() {
|
||||
auto strongThis = weakThis.lock();
|
||||
if (strongThis == nullptr) {
|
||||
return;
|
||||
}
|
||||
strongThis->notifyIsLocalVideoActive(isActive);
|
||||
});
|
||||
}
|
||||
);
|
||||
}));
|
||||
|
|
@ -148,6 +165,11 @@ void Manager::receiveSignalingData(const std::vector<uint8_t> &data) {
|
|||
_networkManager->perform([candidatesData](NetworkManager *networkManager) {
|
||||
networkManager->receiveSignalingData(candidatesData);
|
||||
});
|
||||
} else if (mode == 4) {
|
||||
uint8_t value = 0;
|
||||
if (reader.ReadUInt8(&value)) {
|
||||
_remoteVideoIsActiveUpdated(value != 0);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -159,6 +181,7 @@ void Manager::setSendVideo(bool sendVideo) {
|
|||
rtc::CopyOnWriteBuffer buffer;
|
||||
uint8_t mode = 1;
|
||||
buffer.AppendData(&mode, 1);
|
||||
|
||||
std::vector<uint8_t> data;
|
||||
data.resize(buffer.size());
|
||||
memcpy(data.data(), buffer.data(), buffer.size());
|
||||
|
|
@ -174,12 +197,31 @@ void Manager::setSendVideo(bool sendVideo) {
|
|||
}
|
||||
}
|
||||
|
||||
void Manager::setMuteOutgoingAudio(bool mute) {
|
||||
_mediaManager->perform([mute](MediaManager *mediaManager) {
|
||||
mediaManager->setMuteOutgoingAudio(mute);
|
||||
});
|
||||
}
|
||||
|
||||
void Manager::switchVideoCamera() {
|
||||
_mediaManager->perform([](MediaManager *mediaManager) {
|
||||
mediaManager->switchVideoCamera();
|
||||
});
|
||||
}
|
||||
|
||||
void Manager::notifyIsLocalVideoActive(bool isActive) {
|
||||
rtc::CopyOnWriteBuffer buffer;
|
||||
uint8_t mode = 4;
|
||||
buffer.AppendData(&mode, 1);
|
||||
uint8_t value = isActive ? 1 : 0;
|
||||
buffer.AppendData(&value, 1);
|
||||
|
||||
std::vector<uint8_t> data;
|
||||
data.resize(buffer.size());
|
||||
memcpy(data.data(), buffer.data(), buffer.size());
|
||||
_signalingDataEmitted(data);
|
||||
}
|
||||
|
||||
void Manager::setIncomingVideoOutput(std::shared_ptr<rtc::VideoSinkInterface<webrtc::VideoFrame>> sink) {
|
||||
_mediaManager->perform([sink](MediaManager *mediaManager) {
|
||||
mediaManager->setIncomingVideoOutput(sink);
|
||||
|
|
|
|||
|
|
@ -16,8 +16,11 @@ public:
|
|||
rtc::Thread *thread,
|
||||
TgVoipEncryptionKey encryptionKey,
|
||||
bool enableP2P,
|
||||
std::vector<TgVoipRtcServer> const &rtcServers,
|
||||
bool isVideo,
|
||||
std::function<void (const TgVoipState &)> stateUpdated,
|
||||
std::function<void (bool)> videoStateUpdated,
|
||||
std::function<void (bool)> remoteVideoIsActiveUpdated,
|
||||
std::function<void (const std::vector<uint8_t> &)> signalingDataEmitted
|
||||
);
|
||||
~Manager();
|
||||
|
|
@ -25,7 +28,9 @@ public:
|
|||
void start();
|
||||
void receiveSignalingData(const std::vector<uint8_t> &data);
|
||||
void setSendVideo(bool sendVideo);
|
||||
void setMuteOutgoingAudio(bool mute);
|
||||
void switchVideoCamera();
|
||||
void notifyIsLocalVideoActive(bool isActive);
|
||||
void setIncomingVideoOutput(std::shared_ptr<rtc::VideoSinkInterface<webrtc::VideoFrame>> sink);
|
||||
void setOutgoingVideoOutput(std::shared_ptr<rtc::VideoSinkInterface<webrtc::VideoFrame>> sink);
|
||||
|
||||
|
|
@ -33,8 +38,11 @@ private:
|
|||
rtc::Thread *_thread;
|
||||
TgVoipEncryptionKey _encryptionKey;
|
||||
bool _enableP2P;
|
||||
std::vector<TgVoipRtcServer> _rtcServers;
|
||||
bool _startWithVideo;
|
||||
std::function<void (const TgVoipState &)> _stateUpdated;
|
||||
std::function<void (bool)> _videoStateUpdated;
|
||||
std::function<void (bool)> _remoteVideoIsActiveUpdated;
|
||||
std::function<void (const std::vector<uint8_t> &)> _signalingDataEmitted;
|
||||
std::unique_ptr<ThreadLocalObject<NetworkManager>> _networkManager;
|
||||
std::unique_ptr<ThreadLocalObject<MediaManager>> _mediaManager;
|
||||
|
|
|
|||
|
|
@ -172,9 +172,12 @@ static rtc::Thread *getWorkerThread() {
|
|||
MediaManager::MediaManager(
|
||||
rtc::Thread *thread,
|
||||
bool isOutgoing,
|
||||
std::function<void (const rtc::CopyOnWriteBuffer &)> packetEmitted
|
||||
bool startWithVideo,
|
||||
std::function<void (const rtc::CopyOnWriteBuffer &)> packetEmitted,
|
||||
std::function<void (bool)> localVideoCaptureActiveUpdated
|
||||
) :
|
||||
_packetEmitted(packetEmitted),
|
||||
_localVideoCaptureActiveUpdated(localVideoCaptureActiveUpdated),
|
||||
_thread(thread),
|
||||
_eventLog(std::make_unique<webrtc::RtcEventLogNull>()),
|
||||
_taskQueueFactory(webrtc::CreateDefaultTaskQueueFactory()) {
|
||||
|
|
@ -190,6 +193,7 @@ _taskQueueFactory(webrtc::CreateDefaultTaskQueueFactory()) {
|
|||
_enableFlexfec = true;
|
||||
|
||||
_isConnected = false;
|
||||
_muteOutgoingAudio = false;
|
||||
|
||||
auto videoEncoderFactory = makeVideoEncoderFactory();
|
||||
_videoCodecs = AssignPayloadTypesAndDefaultCodecs(videoEncoderFactory->GetSupportedFormats());
|
||||
|
|
@ -280,6 +284,10 @@ _taskQueueFactory(webrtc::CreateDefaultTaskQueueFactory()) {
|
|||
_videoChannel->SetInterface(_videoNetworkInterface.get(), webrtc::MediaTransportConfig());
|
||||
|
||||
_nativeVideoSource = makeVideoSource(_thread, getWorkerThread());
|
||||
|
||||
if (startWithVideo) {
|
||||
setSendVideo(true);
|
||||
}
|
||||
}
|
||||
|
||||
MediaManager::~MediaManager() {
|
||||
|
|
@ -318,7 +326,7 @@ void MediaManager::setIsConnected(bool isConnected) {
|
|||
if (_audioChannel) {
|
||||
_audioChannel->OnReadyToSend(_isConnected);
|
||||
_audioChannel->SetSend(_isConnected);
|
||||
_audioChannel->SetAudioSend(_ssrcAudio.outgoing, _isConnected, nullptr, &_audioSource);
|
||||
_audioChannel->SetAudioSend(_ssrcAudio.outgoing, _isConnected && !_muteOutgoingAudio, nullptr, &_audioSource);
|
||||
}
|
||||
if (_isSendingVideo && _videoChannel) {
|
||||
_videoChannel->OnReadyToSend(_isConnected);
|
||||
|
|
@ -364,7 +372,9 @@ void MediaManager::setSendVideo(bool sendVideo) {
|
|||
codec.SetParam(cricket::kCodecParamStartBitrate, 512);
|
||||
codec.SetParam(cricket::kCodecParamMaxBitrate, 2500);
|
||||
|
||||
_videoCapturer = makeVideoCapturer(_nativeVideoSource, _useFrontCamera);
|
||||
_videoCapturer = makeVideoCapturer(_nativeVideoSource, _useFrontCamera, [localVideoCaptureActiveUpdated = _localVideoCaptureActiveUpdated](bool isActive) {
|
||||
localVideoCaptureActiveUpdated(isActive);
|
||||
});
|
||||
|
||||
cricket::VideoSendParameters videoSendParameters;
|
||||
videoSendParameters.codecs.push_back(codec);
|
||||
|
|
@ -450,10 +460,18 @@ void MediaManager::setSendVideo(bool sendVideo) {
|
|||
}
|
||||
}
|
||||
|
||||
void MediaManager::setMuteOutgoingAudio(bool mute) {
|
||||
_muteOutgoingAudio = mute;
|
||||
|
||||
_audioChannel->SetAudioSend(_ssrcAudio.outgoing, _isConnected && !_muteOutgoingAudio, nullptr, &_audioSource);
|
||||
}
|
||||
|
||||
void MediaManager::switchVideoCamera() {
|
||||
if (_isSendingVideo) {
|
||||
_useFrontCamera = !_useFrontCamera;
|
||||
_videoCapturer = makeVideoCapturer(_nativeVideoSource, _useFrontCamera);
|
||||
_videoCapturer = makeVideoCapturer(_nativeVideoSource, _useFrontCamera, [localVideoCaptureActiveUpdated = _localVideoCaptureActiveUpdated](bool isActive) {
|
||||
localVideoCaptureActiveUpdated(isActive);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -57,7 +57,9 @@ public:
|
|||
MediaManager(
|
||||
rtc::Thread *thread,
|
||||
bool isOutgoing,
|
||||
std::function<void (const rtc::CopyOnWriteBuffer &)> packetEmitted
|
||||
bool startWithVideo,
|
||||
std::function<void (const rtc::CopyOnWriteBuffer &)> packetEmitted,
|
||||
std::function<void (bool)> localVideoCaptureActiveUpdated
|
||||
);
|
||||
~MediaManager();
|
||||
|
||||
|
|
@ -65,12 +67,14 @@ public:
|
|||
void receivePacket(const rtc::CopyOnWriteBuffer &packet);
|
||||
void notifyPacketSent(const rtc::SentPacket &sentPacket);
|
||||
void setSendVideo(bool sendVideo);
|
||||
void setMuteOutgoingAudio(bool mute);
|
||||
void switchVideoCamera();
|
||||
void setIncomingVideoOutput(std::shared_ptr<rtc::VideoSinkInterface<webrtc::VideoFrame>> sink);
|
||||
void setOutgoingVideoOutput(std::shared_ptr<rtc::VideoSinkInterface<webrtc::VideoFrame>> sink);
|
||||
|
||||
protected:
|
||||
std::function<void (const rtc::CopyOnWriteBuffer &)> _packetEmitted;
|
||||
std::function<void (bool)> _localVideoCaptureActiveUpdated;
|
||||
|
||||
private:
|
||||
rtc::Thread *_thread;
|
||||
|
|
@ -82,6 +86,7 @@ private:
|
|||
bool _enableFlexfec;
|
||||
|
||||
bool _isConnected;
|
||||
bool _muteOutgoingAudio;
|
||||
|
||||
std::vector<cricket::VideoCodec> _videoCodecs;
|
||||
bool _isSendingVideo;
|
||||
|
|
|
|||
|
|
@ -154,6 +154,7 @@ NetworkManager::NetworkManager(
|
|||
rtc::Thread *thread,
|
||||
TgVoipEncryptionKey encryptionKey,
|
||||
bool enableP2P,
|
||||
std::vector<TgVoipRtcServer> const &rtcServers,
|
||||
std::function<void (const NetworkManager::State &)> stateUpdated,
|
||||
std::function<void (const rtc::CopyOnWriteBuffer &)> packetReceived,
|
||||
std::function<void (const std::vector<uint8_t> &)> signalingDataEmitted
|
||||
|
|
@ -178,16 +179,35 @@ _signalingDataEmitted(signalingDataEmitted) {
|
|||
_portAllocator->set_flags(_portAllocator->flags() | flags);
|
||||
_portAllocator->Initialize();
|
||||
|
||||
rtc::SocketAddress defaultStunAddress = rtc::SocketAddress("134.122.52.178", 3478);
|
||||
cricket::ServerAddresses stunServers;
|
||||
stunServers.insert(defaultStunAddress);
|
||||
std::vector<cricket::RelayServerConfig> turnServers;
|
||||
turnServers.push_back(cricket::RelayServerConfig(
|
||||
rtc::SocketAddress("134.122.52.178", 3478),
|
||||
"openrelay",
|
||||
"openrelay",
|
||||
cricket::PROTO_UDP
|
||||
));
|
||||
|
||||
if (rtcServers.size() == 0) {
|
||||
rtc::SocketAddress defaultStunAddress = rtc::SocketAddress("134.122.52.178", 3478);
|
||||
stunServers.insert(defaultStunAddress);
|
||||
|
||||
turnServers.push_back(cricket::RelayServerConfig(
|
||||
rtc::SocketAddress("134.122.52.178", 3478),
|
||||
"openrelay",
|
||||
"openrelay",
|
||||
cricket::PROTO_UDP
|
||||
));
|
||||
} else {
|
||||
for (auto &server : rtcServers) {
|
||||
if (server.isTurn) {
|
||||
turnServers.push_back(cricket::RelayServerConfig(
|
||||
rtc::SocketAddress(server.host, server.port),
|
||||
server.login,
|
||||
server.password,
|
||||
cricket::PROTO_UDP
|
||||
));
|
||||
} else {
|
||||
rtc::SocketAddress stunAddress = rtc::SocketAddress(server.host, server.port);
|
||||
stunServers.insert(stunAddress);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
_portAllocator->SetConfiguration(stunServers, turnServers, 2, webrtc::NO_PRUNE);
|
||||
|
||||
_asyncResolverFactory = std::make_unique<webrtc::BasicAsyncResolverFactory>();
|
||||
|
|
|
|||
|
|
@ -41,6 +41,7 @@ public:
|
|||
rtc::Thread *thread,
|
||||
TgVoipEncryptionKey encryptionKey,
|
||||
bool enableP2P,
|
||||
std::vector<TgVoipRtcServer> const &rtcServers,
|
||||
std::function<void (const NetworkManager::State &)> stateUpdated,
|
||||
std::function<void (const rtc::CopyOnWriteBuffer &)> packetReceived,
|
||||
std::function<void (const std::vector<uint8_t> &)> signalingDataEmitted
|
||||
|
|
|
|||
|
|
@ -26,6 +26,14 @@ struct TgVoipProxy {
|
|||
std::string password;
|
||||
};
|
||||
|
||||
struct TgVoipRtcServer {
|
||||
std::string host;
|
||||
uint16_t port;
|
||||
std::string login;
|
||||
std::string password;
|
||||
bool isTurn;
|
||||
};
|
||||
|
||||
enum class TgVoipEndpointType {
|
||||
Inet,
|
||||
Lan,
|
||||
|
|
@ -135,10 +143,13 @@ public:
|
|||
TgVoipPersistentState const &persistentState,
|
||||
std::vector<TgVoipEndpoint> const &endpoints,
|
||||
std::unique_ptr<TgVoipProxy> const &proxy,
|
||||
std::vector<TgVoipRtcServer> const &rtcServers,
|
||||
TgVoipNetworkType initialNetworkType,
|
||||
TgVoipEncryptionKey const &encryptionKey,
|
||||
bool isVideo,
|
||||
std::function<void(TgVoipState)> stateUpdated,
|
||||
std::function<void(bool)> videoStateUpdated,
|
||||
std::function<void(bool)> remoteVideoIsActiveUpdated,
|
||||
std::function<void(const std::vector<uint8_t> &)> signalingDataEmitted
|
||||
);
|
||||
|
||||
|
|
|
|||
|
|
@ -139,11 +139,14 @@ public:
|
|||
std::vector<TgVoipEndpoint> const &endpoints,
|
||||
TgVoipPersistentState const &persistentState,
|
||||
std::unique_ptr<TgVoipProxy> const &proxy,
|
||||
std::vector<TgVoipRtcServer> const &rtcServers,
|
||||
TgVoipConfig const &config,
|
||||
TgVoipEncryptionKey const &encryptionKey,
|
||||
bool isVideo,
|
||||
TgVoipNetworkType initialNetworkType,
|
||||
std::function<void(TgVoipState)> stateUpdated,
|
||||
std::function<void(bool)> videoStateUpdated,
|
||||
std::function<void(bool)> remoteVideoIsActiveUpdated,
|
||||
std::function<void(const std::vector<uint8_t> &)> signalingDataEmitted
|
||||
) :
|
||||
_stateUpdated(stateUpdated),
|
||||
|
|
@ -157,17 +160,22 @@ public:
|
|||
|
||||
bool enableP2P = config.enableP2P;
|
||||
|
||||
_manager.reset(new ThreadLocalObject<Manager>(getManagerThread(), [encryptionKey = encryptionKey, enableP2P = enableP2P, stateUpdated, videoStateUpdated, signalingDataEmitted](){
|
||||
_manager.reset(new ThreadLocalObject<Manager>(getManagerThread(), [encryptionKey = encryptionKey, enableP2P = enableP2P, isVideo, stateUpdated, videoStateUpdated, remoteVideoIsActiveUpdated, signalingDataEmitted, rtcServers](){
|
||||
return new Manager(
|
||||
getManagerThread(),
|
||||
encryptionKey,
|
||||
enableP2P,
|
||||
rtcServers,
|
||||
isVideo,
|
||||
[stateUpdated](const TgVoipState &state) {
|
||||
stateUpdated(state);
|
||||
},
|
||||
[videoStateUpdated](bool isActive) {
|
||||
videoStateUpdated(isActive);
|
||||
},
|
||||
[remoteVideoIsActiveUpdated](bool isActive) {
|
||||
remoteVideoIsActiveUpdated(isActive);
|
||||
},
|
||||
[signalingDataEmitted](const std::vector<uint8_t> &data) {
|
||||
signalingDataEmitted(data);
|
||||
}
|
||||
|
|
@ -249,7 +257,9 @@ public:
|
|||
}
|
||||
|
||||
void setMuteMicrophone(bool muteMicrophone) override {
|
||||
//controller_->SetMute(muteMicrophone);
|
||||
_manager->perform([muteMicrophone](Manager *manager) {
|
||||
manager->setMuteOutgoingAudio(muteMicrophone);
|
||||
});
|
||||
}
|
||||
|
||||
void setIncomingVideoOutput(std::shared_ptr<rtc::VideoSinkInterface<webrtc::VideoFrame>> sink) override {
|
||||
|
|
@ -374,21 +384,27 @@ TgVoip *TgVoip::makeInstance(
|
|||
TgVoipPersistentState const &persistentState,
|
||||
std::vector<TgVoipEndpoint> const &endpoints,
|
||||
std::unique_ptr<TgVoipProxy> const &proxy,
|
||||
std::vector<TgVoipRtcServer> const &rtcServers,
|
||||
TgVoipNetworkType initialNetworkType,
|
||||
TgVoipEncryptionKey const &encryptionKey,
|
||||
bool isVideo,
|
||||
std::function<void(TgVoipState)> stateUpdated,
|
||||
std::function<void(bool)> videoStateUpdated,
|
||||
std::function<void(bool)> remoteVideoIsActiveUpdated,
|
||||
std::function<void(const std::vector<uint8_t> &)> signalingDataEmitted
|
||||
) {
|
||||
return new TgVoipImpl(
|
||||
endpoints,
|
||||
persistentState,
|
||||
proxy,
|
||||
rtcServers,
|
||||
config,
|
||||
encryptionKey,
|
||||
isVideo,
|
||||
initialNetworkType,
|
||||
stateUpdated,
|
||||
videoStateUpdated,
|
||||
remoteVideoIsActiveUpdated,
|
||||
signalingDataEmitted
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@
|
|||
+ (NSArray<AVCaptureDevice *> *)captureDevices;
|
||||
+ (NSArray<AVCaptureDeviceFormat *> *)supportedFormatsForDevice:(AVCaptureDevice *)device;
|
||||
|
||||
- (instancetype)initWithSource:(rtc::scoped_refptr<webrtc::VideoTrackSourceInterface>)source;
|
||||
- (instancetype)initWithSource:(rtc::scoped_refptr<webrtc::VideoTrackSourceInterface>)source isActiveUpdated:(void (^)(bool))isActiveUpdated;
|
||||
|
||||
- (void)startCaptureWithDevice:(AVCaptureDevice *)device format:(AVCaptureDeviceFormat *)format fps:(NSInteger)fps;
|
||||
- (void)stopCapture;
|
||||
|
|
|
|||
|
|
@ -37,16 +37,20 @@ static webrtc::ObjCVideoTrackSource *getObjCVideoSource(const rtc::scoped_refptr
|
|||
FourCharCode _outputPixelFormat;
|
||||
RTCVideoRotation _rotation;
|
||||
UIDeviceOrientation _orientation;
|
||||
|
||||
void (^_isActiveUpdated)(bool);
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
@implementation VideoCameraCapturer
|
||||
|
||||
- (instancetype)initWithSource:(rtc::scoped_refptr<webrtc::VideoTrackSourceInterface>)source {
|
||||
- (instancetype)initWithSource:(rtc::scoped_refptr<webrtc::VideoTrackSourceInterface>)source isActiveUpdated:(void (^)(bool))isActiveUpdated {
|
||||
self = [super init];
|
||||
if (self != nil) {
|
||||
_source = source;
|
||||
_isActiveUpdated = [isActiveUpdated copy];
|
||||
|
||||
if (![self setupCaptureSession:[[AVCaptureSession alloc] init]]) {
|
||||
return nil;
|
||||
}
|
||||
|
|
@ -116,6 +120,7 @@ static webrtc::ObjCVideoTrackSource *getObjCVideoSource(const rtc::scoped_refptr
|
|||
}
|
||||
|
||||
- (void)stopCapture {
|
||||
_isActiveUpdated = nil;
|
||||
[self stopCaptureWithCompletionHandler:nil];
|
||||
}
|
||||
|
||||
|
|
@ -310,10 +315,17 @@ static webrtc::ObjCVideoTrackSource *getObjCVideoSource(const rtc::scoped_refptr
|
|||
// allow future retries on fatal errors.
|
||||
_hasRetriedOnFatalError = NO;
|
||||
}];
|
||||
|
||||
if (_isActiveUpdated) {
|
||||
_isActiveUpdated(true);
|
||||
}
|
||||
}
|
||||
|
||||
- (void)handleCaptureSessionDidStopRunning:(NSNotification *)notification {
|
||||
RTCLog(@"Capture session stopped.");
|
||||
if (_isActiveUpdated) {
|
||||
_isActiveUpdated(false);
|
||||
}
|
||||
}
|
||||
|
||||
- (void)handleFatalError {
|
||||
|
|
|
|||
|
|
@ -25,11 +25,15 @@ typedef NS_ENUM(int32_t, OngoingCallStateWebrtc) {
|
|||
|
||||
typedef NS_ENUM(int32_t, OngoingCallVideoStateWebrtc) {
|
||||
OngoingCallVideoStateInactive,
|
||||
OngoingCallVideoStateRequesting,
|
||||
OngoingCallVideoStateInvited,
|
||||
OngoingCallVideoStateActiveOutgoing,
|
||||
OngoingCallVideoStateActive
|
||||
};
|
||||
|
||||
typedef NS_ENUM(int32_t, OngoingCallRemoteVideoStateWebrtc) {
|
||||
OngoingCallRemoteVideoStateInactive,
|
||||
OngoingCallRemoteVideoStateActive
|
||||
};
|
||||
|
||||
typedef NS_ENUM(int32_t, OngoingCallNetworkTypeWebrtc) {
|
||||
OngoingCallNetworkTypeWifi,
|
||||
OngoingCallNetworkTypeCellularGprs,
|
||||
|
|
@ -62,6 +66,18 @@ typedef NS_ENUM(int32_t, OngoingCallDataSavingWebrtc) {
|
|||
|
||||
@end
|
||||
|
||||
@interface VoipRtcServerWebrtc : NSObject
|
||||
|
||||
@property (nonatomic, strong, readonly) NSString * _Nonnull host;
|
||||
@property (nonatomic, readonly) int32_t port;
|
||||
@property (nonatomic, strong, readonly) NSString * _Nullable username;
|
||||
@property (nonatomic, strong, readonly) NSString * _Nullable password;
|
||||
@property (nonatomic, readonly) bool isTurn;
|
||||
|
||||
- (instancetype _Nonnull)initWithHost:(NSString * _Nonnull)host port:(int32_t)port username:(NSString * _Nullable)username password:(NSString * _Nullable)password isTurn:(bool)isTurn;
|
||||
|
||||
@end
|
||||
|
||||
@interface OngoingCallThreadLocalContextWebrtc : NSObject
|
||||
|
||||
+ (void)setupLoggingFunction:(void (* _Nullable)(NSString * _Nullable))loggingFunction;
|
||||
|
|
@ -69,10 +85,10 @@ typedef NS_ENUM(int32_t, OngoingCallDataSavingWebrtc) {
|
|||
+ (int32_t)maxLayer;
|
||||
+ (NSString * _Nonnull)version;
|
||||
|
||||
@property (nonatomic, copy) void (^ _Nullable stateChanged)(OngoingCallStateWebrtc, OngoingCallVideoStateWebrtc);
|
||||
@property (nonatomic, copy) void (^ _Nullable stateChanged)(OngoingCallStateWebrtc, OngoingCallVideoStateWebrtc, OngoingCallRemoteVideoStateWebrtc);
|
||||
@property (nonatomic, copy) void (^ _Nullable signalBarsChanged)(int32_t);
|
||||
|
||||
- (instancetype _Nonnull)initWithQueue:(id<OngoingCallThreadLocalContextQueueWebrtc> _Nonnull)queue proxy:(VoipProxyServerWebrtc * _Nullable)proxy networkType:(OngoingCallNetworkTypeWebrtc)networkType dataSaving:(OngoingCallDataSavingWebrtc)dataSaving derivedState:(NSData * _Nonnull)derivedState key:(NSData * _Nonnull)key isOutgoing:(bool)isOutgoing primaryConnection:(OngoingCallConnectionDescriptionWebrtc * _Nonnull)primaryConnection alternativeConnections:(NSArray<OngoingCallConnectionDescriptionWebrtc *> * _Nonnull)alternativeConnections maxLayer:(int32_t)maxLayer allowP2P:(BOOL)allowP2P logPath:(NSString * _Nonnull)logPath sendSignalingData:(void (^)(NSData * _Nonnull))sendSignalingData;
|
||||
- (instancetype _Nonnull)initWithQueue:(id<OngoingCallThreadLocalContextQueueWebrtc> _Nonnull)queue proxy:(VoipProxyServerWebrtc * _Nullable)proxy rtcServers:(NSArray<VoipRtcServerWebrtc *> * _Nonnull)rtcServers networkType:(OngoingCallNetworkTypeWebrtc)networkType dataSaving:(OngoingCallDataSavingWebrtc)dataSaving derivedState:(NSData * _Nonnull)derivedState key:(NSData * _Nonnull)key isOutgoing:(bool)isOutgoing isVideo:(bool)isVideo primaryConnection:(OngoingCallConnectionDescriptionWebrtc * _Nonnull)primaryConnection alternativeConnections:(NSArray<OngoingCallConnectionDescriptionWebrtc *> * _Nonnull)alternativeConnections maxLayer:(int32_t)maxLayer allowP2P:(BOOL)allowP2P logPath:(NSString * _Nonnull)logPath sendSignalingData:(void (^)(NSData * _Nonnull))sendSignalingData;
|
||||
- (void)stop:(void (^_Nullable)(NSString * _Nullable debugLog, int64_t bytesSentWifi, int64_t bytesReceivedWifi, int64_t bytesSentMobile, int64_t bytesReceivedMobile))completion;
|
||||
|
||||
- (bool)needRate;
|
||||
|
|
|
|||
|
|
@ -35,6 +35,7 @@ using namespace TGVOIP_NAMESPACE;
|
|||
|
||||
OngoingCallStateWebrtc _state;
|
||||
OngoingCallVideoStateWebrtc _videoState;
|
||||
OngoingCallRemoteVideoStateWebrtc _remoteVideoState;
|
||||
|
||||
int32_t _signalBars;
|
||||
NSData *_lastDerivedState;
|
||||
|
|
@ -62,6 +63,22 @@ using namespace TGVOIP_NAMESPACE;
|
|||
|
||||
@end
|
||||
|
||||
@implementation VoipRtcServerWebrtc
|
||||
|
||||
- (instancetype _Nonnull)initWithHost:(NSString * _Nonnull)host port:(int32_t)port username:(NSString * _Nullable)username password:(NSString * _Nullable)password isTurn:(bool)isTurn {
|
||||
self = [super init];
|
||||
if (self != nil) {
|
||||
_host = host;
|
||||
_port = port;
|
||||
_username = username;
|
||||
_password = password;
|
||||
_isTurn = isTurn;
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
static TgVoipNetworkType callControllerNetworkTypeForType(OngoingCallNetworkTypeWebrtc type) {
|
||||
switch (type) {
|
||||
case OngoingCallNetworkTypeWifi:
|
||||
|
|
@ -117,7 +134,7 @@ static void (*InternalVoipLoggingFunction)(NSString *) = NULL;
|
|||
return @"2.7.7";
|
||||
}
|
||||
|
||||
- (instancetype _Nonnull)initWithQueue:(id<OngoingCallThreadLocalContextQueueWebrtc> _Nonnull)queue proxy:(VoipProxyServerWebrtc * _Nullable)proxy networkType:(OngoingCallNetworkTypeWebrtc)networkType dataSaving:(OngoingCallDataSavingWebrtc)dataSaving derivedState:(NSData * _Nonnull)derivedState key:(NSData * _Nonnull)key isOutgoing:(bool)isOutgoing primaryConnection:(OngoingCallConnectionDescriptionWebrtc * _Nonnull)primaryConnection alternativeConnections:(NSArray<OngoingCallConnectionDescriptionWebrtc *> * _Nonnull)alternativeConnections maxLayer:(int32_t)maxLayer allowP2P:(BOOL)allowP2P logPath:(NSString * _Nonnull)logPath sendSignalingData:(void (^)(NSData * _Nonnull))sendSignalingData; {
|
||||
- (instancetype _Nonnull)initWithQueue:(id<OngoingCallThreadLocalContextQueueWebrtc> _Nonnull)queue proxy:(VoipProxyServerWebrtc * _Nullable)proxy rtcServers:(NSArray<VoipRtcServerWebrtc *> * _Nonnull)rtcServers networkType:(OngoingCallNetworkTypeWebrtc)networkType dataSaving:(OngoingCallDataSavingWebrtc)dataSaving derivedState:(NSData * _Nonnull)derivedState key:(NSData * _Nonnull)key isOutgoing:(bool)isOutgoing isVideo:(bool)isVideo primaryConnection:(OngoingCallConnectionDescriptionWebrtc * _Nonnull)primaryConnection alternativeConnections:(NSArray<OngoingCallConnectionDescriptionWebrtc *> * _Nonnull)alternativeConnections maxLayer:(int32_t)maxLayer allowP2P:(BOOL)allowP2P logPath:(NSString * _Nonnull)logPath sendSignalingData:(void (^)(NSData * _Nonnull))sendSignalingData; {
|
||||
self = [super init];
|
||||
if (self != nil) {
|
||||
_queue = queue;
|
||||
|
|
@ -129,7 +146,13 @@ static void (*InternalVoipLoggingFunction)(NSString *) = NULL;
|
|||
_callPacketTimeout = 10.0;
|
||||
_networkType = networkType;
|
||||
_sendSignalingData = [sendSignalingData copy];
|
||||
_videoState = OngoingCallVideoStateInactive;
|
||||
if (isVideo) {
|
||||
_videoState = OngoingCallVideoStateActiveOutgoing;
|
||||
_remoteVideoState = OngoingCallRemoteVideoStateActive;
|
||||
} else {
|
||||
_videoState = OngoingCallVideoStateInactive;
|
||||
_remoteVideoState = OngoingCallRemoteVideoStateInactive;
|
||||
}
|
||||
|
||||
std::vector<uint8_t> derivedStateValue;
|
||||
derivedStateValue.resize(derivedState.length);
|
||||
|
|
@ -145,6 +168,17 @@ static void (*InternalVoipLoggingFunction)(NSString *) = NULL;
|
|||
proxyValue = std::unique_ptr<TgVoipProxy>(proxyObject);
|
||||
}
|
||||
|
||||
std::vector<TgVoipRtcServer> parsedRtcServers;
|
||||
for (VoipRtcServerWebrtc *server in rtcServers) {
|
||||
parsedRtcServers.push_back((TgVoipRtcServer){
|
||||
.host = server.host.UTF8String,
|
||||
.port = (uint16_t)server.port,
|
||||
.login = server.username.UTF8String,
|
||||
.password = server.password.UTF8String,
|
||||
.isTurn = server.isTurn
|
||||
});
|
||||
}
|
||||
|
||||
/*TgVoipCrypto crypto;
|
||||
crypto.sha1 = &TGCallSha1;
|
||||
crypto.sha256 = &TGCallSha256;
|
||||
|
|
@ -199,8 +233,10 @@ static void (*InternalVoipLoggingFunction)(NSString *) = NULL;
|
|||
{ derivedStateValue },
|
||||
endpoints,
|
||||
proxyValue,
|
||||
parsedRtcServers,
|
||||
callControllerNetworkTypeForType(networkType),
|
||||
encryptionKey,
|
||||
isVideo,
|
||||
[weakSelf, queue](TgVoipState state) {
|
||||
[queue dispatch:^{
|
||||
__strong OngoingCallThreadLocalContextWebrtc *strongSelf = weakSelf;
|
||||
|
|
@ -222,7 +258,26 @@ static void (*InternalVoipLoggingFunction)(NSString *) = NULL;
|
|||
if (strongSelf->_videoState != videoState) {
|
||||
strongSelf->_videoState = videoState;
|
||||
if (strongSelf->_stateChanged) {
|
||||
strongSelf->_stateChanged(strongSelf->_state, strongSelf->_videoState);
|
||||
strongSelf->_stateChanged(strongSelf->_state, strongSelf->_videoState, strongSelf->_remoteVideoState);
|
||||
}
|
||||
}
|
||||
}
|
||||
}];
|
||||
},
|
||||
[weakSelf, queue](bool isActive) {
|
||||
[queue dispatch:^{
|
||||
__strong OngoingCallThreadLocalContextWebrtc *strongSelf = weakSelf;
|
||||
if (strongSelf) {
|
||||
OngoingCallRemoteVideoStateWebrtc remoteVideoState;
|
||||
if (isActive) {
|
||||
remoteVideoState = OngoingCallRemoteVideoStateActive;
|
||||
} else {
|
||||
remoteVideoState = OngoingCallRemoteVideoStateInactive;
|
||||
}
|
||||
if (strongSelf->_remoteVideoState != remoteVideoState) {
|
||||
strongSelf->_remoteVideoState = remoteVideoState;
|
||||
if (strongSelf->_stateChanged) {
|
||||
strongSelf->_stateChanged(strongSelf->_state, strongSelf->_videoState, strongSelf->_remoteVideoState);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -322,7 +377,12 @@ static void (*InternalVoipLoggingFunction)(NSString *) = NULL;
|
|||
_state = callState;
|
||||
|
||||
if (_stateChanged) {
|
||||
_stateChanged(_state, _videoState);
|
||||
if (_videoState == OngoingCallVideoStateActiveOutgoing) {
|
||||
if (_state == OngoingCallStateConnected) {
|
||||
_videoState = OngoingCallVideoStateActive;
|
||||
}
|
||||
}
|
||||
_stateChanged(_state, _videoState, _remoteVideoState);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -211,7 +211,7 @@ func makeBridgeMedia(message: Message, strings: PresentationStrings, chatPeer: P
|
|||
bridgeAction?.actionType = .channelCreated
|
||||
}
|
||||
}
|
||||
case let .phoneCall(_, discardReason, _):
|
||||
case let .phoneCall(_, discardReason, _, _):
|
||||
let bridgeAttachment = TGBridgeUnsupportedMediaAttachment()
|
||||
let incoming = message.flags.contains(.Incoming)
|
||||
var compactTitle: String = ""
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue