Merge branch 'master' of gitlab.com:peter-iakovlev/telegram-ios

This commit is contained in:
Ilya Laktyushin 2020-06-24 18:06:15 +03:00
commit eac6d297d0
95 changed files with 6089 additions and 4686 deletions

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -0,0 +1,12 @@
{
"images" : [
{
"filename" : "Video.pdf",
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

View file

@ -0,0 +1,12 @@
{
"images" : [
{
"filename" : "Video.pdf",
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -103,7 +103,7 @@ final class ChatRecentActionsController: TelegramBaseController {
}, presentPeerContact: {
}, dismissReportPeer: {
}, deleteChat: {
}, beginCall: {
}, beginCall: { _ in
}, toggleMessageStickerStarred: { _ in
}, presentController: { _, _ in
}, getNavigationController: {

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -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() {

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -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>();

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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