diff --git a/build-system/generate-xcode-project.sh b/build-system/generate-xcode-project.sh index eb1e5538c8..9910f9d3b9 100755 --- a/build-system/generate-xcode-project.sh +++ b/build-system/generate-xcode-project.sh @@ -51,7 +51,6 @@ BAZEL_OPTIONS=(\ --spawn_strategy=standalone \ --strategy=SwiftCompile=standalone \ --features=swift.enable_batch_mode \ - --apple_generate_dsym \ --swiftcopt=-j${CORE_COUNT_MINUS_ONE} \ ) diff --git a/submodules/AccountContext/Sources/PresentationCallManager.swift b/submodules/AccountContext/Sources/PresentationCallManager.swift index 18fd068679..ea702ea805 100644 --- a/submodules/AccountContext/Sources/PresentationCallManager.swift +++ b/submodules/AccountContext/Sources/PresentationCallManager.swift @@ -174,6 +174,25 @@ public struct PresentationGroupCallState: Equatable { } } +public struct PresentationGroupCallSummaryState: Equatable { + public var info: GroupCallInfo + public var participantCount: Int + public var callState: PresentationGroupCallState + public var topParticipants: [GroupCallParticipantsContext.Participant] + + public init( + info: GroupCallInfo, + participantCount: Int, + callState: PresentationGroupCallState, + topParticipants: [GroupCallParticipantsContext.Participant] + ) { + self.info = info + self.participantCount = participantCount + self.callState = callState + self.topParticipants = topParticipants + } +} + public struct PresentationGroupCallMemberState: Equatable { public var ssrc: UInt32 public var muteState: GroupCallParticipantsContext.Participant.MuteState? @@ -197,9 +216,11 @@ public protocol PresentationGroupCall: class { var canBeRemoved: Signal { get } var state: Signal { get } + var summaryState: Signal { get } var members: Signal<[PeerId: PresentationGroupCallMemberState], NoError> { get } var audioLevels: Signal<[(PeerId, Float)], NoError> { get } var myAudioLevel: Signal { get } + var isMuted: Signal { get } func leave() -> Signal @@ -211,7 +232,8 @@ public protocol PresentationGroupCall: class { public protocol PresentationCallManager: class { var currentCallSignal: Signal { get } + var currentGroupCallSignal: Signal { get } func requestCall(context: AccountContext, peerId: PeerId, isVideo: Bool, endCurrentIfAny: Bool) -> RequestCallResult - func requestOrJoinGroupCall(context: AccountContext, peerId: PeerId) -> RequestOrJoinGroupCallResult + func requestOrJoinGroupCall(context: AccountContext, peerId: PeerId, initialCall: CachedChannelData.ActiveCall?, endCurrentIfAny: Bool) -> RequestOrJoinGroupCallResult } diff --git a/submodules/AnimatedAvatarSetNode/Sources/AnimatedAvatarSetNode.swift b/submodules/AnimatedAvatarSetNode/Sources/AnimatedAvatarSetNode.swift index e4fbe3b2a9..be611ab38d 100644 --- a/submodules/AnimatedAvatarSetNode/Sources/AnimatedAvatarSetNode.swift +++ b/submodules/AnimatedAvatarSetNode/Sources/AnimatedAvatarSetNode.swift @@ -146,9 +146,7 @@ public final class AnimatedAvatarSetNode: ASDisplayNode { super.init() } - public func update(context: AccountContext, content: AnimatedAvatarSetContext.Content, animated: Bool, synchronousLoad: Bool) -> CGSize { - let itemSize = CGSize(width: 30.0, height: 30.0) - + public func update(context: AccountContext, content: AnimatedAvatarSetContext.Content, itemSize: CGSize = CGSize(width: 30.0, height: 30.0), animated: Bool, synchronousLoad: Bool) -> CGSize { var contentWidth: CGFloat = 0.0 let contentHeight: CGFloat = itemSize.height diff --git a/submodules/ChatListUI/Sources/ChatListController.swift b/submodules/ChatListUI/Sources/ChatListController.swift index b93ec67df3..f2c47e8b9c 100644 --- a/submodules/ChatListUI/Sources/ChatListController.swift +++ b/submodules/ChatListUI/Sources/ChatListController.swift @@ -171,7 +171,7 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController self.tabContainerNode = ChatListFilterTabContainerNode() - super.init(context: context, navigationBarPresentationData: NavigationBarPresentationData(presentationData: self.presentationData), mediaAccessoryPanelVisibility: .always, locationBroadcastPanelSource: .summary) + super.init(context: context, navigationBarPresentationData: NavigationBarPresentationData(presentationData: self.presentationData), mediaAccessoryPanelVisibility: .always, locationBroadcastPanelSource: .summary, groupCallPanelSource: .all) self.tabBarItemContextActionType = .always diff --git a/submodules/Display/Source/Navigation/NavigationController.swift b/submodules/Display/Source/Navigation/NavigationController.swift index e9281d4c44..cae45845de 100644 --- a/submodules/Display/Source/Navigation/NavigationController.swift +++ b/submodules/Display/Source/Navigation/NavigationController.swift @@ -391,7 +391,7 @@ open class NavigationController: UINavigationController, ContainableController, } if let inCallStatusBar = self.inCallStatusBar { - let inCallStatusBarFrame = CGRect(origin: CGPoint(), size: CGSize(width: layout.size.width, height: max(40.0, layout.safeInsets.top))) + let inCallStatusBarFrame = CGRect(origin: CGPoint(), size: CGSize(width: layout.size.width, height: max(layout.statusBarHeight ?? 0.0, max(40.0, layout.safeInsets.top)))) if inCallStatusBar.frame.isEmpty { inCallStatusBar.frame = inCallStatusBarFrame } else { diff --git a/submodules/HashtagSearchUI/Sources/HashtagSearchController.swift b/submodules/HashtagSearchUI/Sources/HashtagSearchController.swift index 463006184a..03b576262f 100644 --- a/submodules/HashtagSearchUI/Sources/HashtagSearchController.swift +++ b/submodules/HashtagSearchUI/Sources/HashtagSearchController.swift @@ -33,7 +33,7 @@ public final class HashtagSearchController: TelegramBaseController { self.presentationData = context.sharedContext.currentPresentationData.with { $0 } - super.init(context: context, navigationBarPresentationData: NavigationBarPresentationData(presentationData: self.presentationData), mediaAccessoryPanelVisibility: .specific(size: .compact), locationBroadcastPanelSource: .none) + super.init(context: context, navigationBarPresentationData: NavigationBarPresentationData(presentationData: self.presentationData), mediaAccessoryPanelVisibility: .specific(size: .compact), locationBroadcastPanelSource: .none, groupCallPanelSource: .none) self.statusBar.statusBarStyle = self.presentationData.theme.rootController.statusBarStyle.style diff --git a/submodules/TelegramApi/Sources/Api0.swift b/submodules/TelegramApi/Sources/Api0.swift index c9fc696abd..829b805174 100644 --- a/submodules/TelegramApi/Sources/Api0.swift +++ b/submodules/TelegramApi/Sources/Api0.swift @@ -6,9 +6,8 @@ fileprivate let parsers: [Int32 : (BufferReader) -> Any?] = { dict[571523412] = { return $0.readDouble() } dict[-1255641564] = { return parseString($0) } dict[-1240849242] = { return Api.messages.StickerSet.parse_stickerSet($0) } - dict[-1331534976] = { return Api.GroupCall.parse_groupCallPrivate($0) } - dict[1435512961] = { return Api.GroupCall.parse_groupCall($0) } dict[2004925620] = { return Api.GroupCall.parse_groupCallDiscarded($0) } + dict[1435512961] = { return Api.GroupCall.parse_groupCall($0) } dict[-457104426] = { return Api.InputGeoPoint.parse_inputGeoPointEmpty($0) } dict[1210199983] = { return Api.InputGeoPoint.parse_inputGeoPoint($0) } dict[-784000893] = { return Api.payments.ValidatedRequestedInfo.parse_validatedRequestedInfo($0) } diff --git a/submodules/TelegramApi/Sources/Api1.swift b/submodules/TelegramApi/Sources/Api1.swift index bef5b0139a..0935ebca17 100644 --- a/submodules/TelegramApi/Sources/Api1.swift +++ b/submodules/TelegramApi/Sources/Api1.swift @@ -1910,19 +1910,18 @@ public struct messages { } public extension Api { public enum GroupCall: TypeConstructorDescription { - case groupCallPrivate(id: Int64, accessHash: Int64, participantsCount: Int32) - case groupCall(flags: Int32, id: Int64, accessHash: Int64, participantsCount: Int32, params: Api.DataJSON?, version: Int32) case groupCallDiscarded(id: Int64, accessHash: Int64, duration: Int32) + case groupCall(flags: Int32, id: Int64, accessHash: Int64, participantsCount: Int32, params: Api.DataJSON?, version: Int32) public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { switch self { - case .groupCallPrivate(let id, let accessHash, let participantsCount): + case .groupCallDiscarded(let id, let accessHash, let duration): if boxed { - buffer.appendInt32(-1331534976) + buffer.appendInt32(2004925620) } serializeInt64(id, buffer: buffer, boxed: false) serializeInt64(accessHash, buffer: buffer, boxed: false) - serializeInt32(participantsCount, buffer: buffer, boxed: false) + serializeInt32(duration, buffer: buffer, boxed: false) break case .groupCall(let flags, let id, let accessHash, let participantsCount, let params, let version): if boxed { @@ -1935,29 +1934,19 @@ public extension Api { if Int(flags) & Int(1 << 0) != 0 {params!.serialize(buffer, true)} serializeInt32(version, buffer: buffer, boxed: false) break - case .groupCallDiscarded(let id, let accessHash, let duration): - if boxed { - buffer.appendInt32(2004925620) - } - serializeInt64(id, buffer: buffer, boxed: false) - serializeInt64(accessHash, buffer: buffer, boxed: false) - serializeInt32(duration, buffer: buffer, boxed: false) - break } } public func descriptionFields() -> (String, [(String, Any)]) { switch self { - case .groupCallPrivate(let id, let accessHash, let participantsCount): - return ("groupCallPrivate", [("id", id), ("accessHash", accessHash), ("participantsCount", participantsCount)]) - case .groupCall(let flags, let id, let accessHash, let participantsCount, let params, let version): - return ("groupCall", [("flags", flags), ("id", id), ("accessHash", accessHash), ("participantsCount", participantsCount), ("params", params), ("version", version)]) case .groupCallDiscarded(let id, let accessHash, let duration): return ("groupCallDiscarded", [("id", id), ("accessHash", accessHash), ("duration", duration)]) + case .groupCall(let flags, let id, let accessHash, let participantsCount, let params, let version): + return ("groupCall", [("flags", flags), ("id", id), ("accessHash", accessHash), ("participantsCount", participantsCount), ("params", params), ("version", version)]) } } - public static func parse_groupCallPrivate(_ reader: BufferReader) -> GroupCall? { + public static func parse_groupCallDiscarded(_ reader: BufferReader) -> GroupCall? { var _1: Int64? _1 = reader.readInt64() var _2: Int64? @@ -1968,7 +1957,7 @@ public extension Api { let _c2 = _2 != nil let _c3 = _3 != nil if _c1 && _c2 && _c3 { - return Api.GroupCall.groupCallPrivate(id: _1!, accessHash: _2!, participantsCount: _3!) + return Api.GroupCall.groupCallDiscarded(id: _1!, accessHash: _2!, duration: _3!) } else { return nil @@ -2002,23 +1991,6 @@ public extension Api { return nil } } - public static func parse_groupCallDiscarded(_ reader: BufferReader) -> GroupCall? { - var _1: Int64? - _1 = reader.readInt64() - var _2: Int64? - _2 = reader.readInt64() - var _3: Int32? - _3 = reader.readInt32() - let _c1 = _1 != nil - let _c2 = _2 != nil - let _c3 = _3 != nil - if _c1 && _c2 && _c3 { - return Api.GroupCall.groupCallDiscarded(id: _1!, accessHash: _2!, duration: _3!) - } - else { - return nil - } - } } public enum InputGeoPoint: TypeConstructorDescription { diff --git a/submodules/TelegramBaseController/BUILD b/submodules/TelegramBaseController/BUILD index 9d65208c11..4d8caa9769 100644 --- a/submodules/TelegramBaseController/BUILD +++ b/submodules/TelegramBaseController/BUILD @@ -20,6 +20,8 @@ swift_library( "//submodules/OverlayStatusController:OverlayStatusController", "//submodules/PresentationDataUtils:PresentationDataUtils", "//submodules/Markdown:Markdown", + "//submodules/TelegramCallsUI:TelegramCallsUI", + "//submodules/AnimatedAvatarSetNode:AnimatedAvatarSetNode", ], visibility = [ "//visibility:public", diff --git a/submodules/TelegramBaseController/Sources/GroupCallNavigationAccessoryPanel.swift b/submodules/TelegramBaseController/Sources/GroupCallNavigationAccessoryPanel.swift new file mode 100644 index 0000000000..4482cd6fd7 --- /dev/null +++ b/submodules/TelegramBaseController/Sources/GroupCallNavigationAccessoryPanel.swift @@ -0,0 +1,326 @@ +import Foundation +import UIKit +import AsyncDisplayKit +import Display +import TelegramCore +import SyncCore +import Postbox +import TelegramPresentationData +import TelegramUIPreferences +import AccountContext +import AppBundle +import SwiftSignalKit +import AnimatedAvatarSetNode + +private let titleFont = Font.semibold(15.0) +private let subtitleFont = Font.regular(13.0) + +final class GroupCallNavigationAccessoryPanel: ASDisplayNode { + private let context: AccountContext + private var theme: PresentationTheme + private var strings: PresentationStrings + + private let tapAction: () -> Void + + private let contentNode: ASDisplayNode + + private let tapButton: HighlightTrackingButtonNode + + private let joinButton: HighlightableButtonNode + private let joinButtonTitleNode: ImmediateTextNode + private let joinButtonBackgroundNode: ASImageNode + + private let micButton: HighlightTrackingButtonNode + private let micButtonForegroundMutedNode: ASImageNode + private let micButtonForegroundUnmutedNode: ASImageNode + private let micButtonBackgroundNode: ASImageNode + + private let titleNode: ImmediateTextNode + private let textNode: ImmediateTextNode + private let muteIconNode: ASImageNode + + private let avatarsContext: AnimatedAvatarSetContext + private var avatarsContent: AnimatedAvatarSetContext.Content? + private let avatarsNode: AnimatedAvatarSetNode + + private let separatorNode: ASDisplayNode + + private let membersDisposable = MetaDisposable() + private let isMutedDisposable = MetaDisposable() + + private var currentData: GroupCallPanelData? + private var validLayout: (CGSize, CGFloat, CGFloat)? + + init(context: AccountContext, presentationData: PresentationData, tapAction: @escaping () -> Void) { + self.context = context + self.theme = presentationData.theme + self.strings = presentationData.strings + + self.tapAction = tapAction + + self.contentNode = ASDisplayNode() + + self.tapButton = HighlightTrackingButtonNode() + + self.joinButton = HighlightableButtonNode() + self.joinButtonTitleNode = ImmediateTextNode() + self.joinButtonBackgroundNode = ASImageNode() + + self.micButton = HighlightTrackingButtonNode() + self.micButtonForegroundMutedNode = ASImageNode() + self.micButtonForegroundUnmutedNode = ASImageNode() + self.micButtonBackgroundNode = ASImageNode() + + self.titleNode = ImmediateTextNode() + self.textNode = ImmediateTextNode() + + self.muteIconNode = ASImageNode() + + self.avatarsContext = AnimatedAvatarSetContext() + self.avatarsNode = AnimatedAvatarSetNode() + + self.separatorNode = ASDisplayNode() + self.separatorNode.isLayerBacked = true + + super.init() + + self.addSubnode(self.contentNode) + + self.tapButton.highligthedChanged = { [weak self] highlighted in + if let strongSelf = self { + if highlighted { + strongSelf.titleNode.layer.removeAnimation(forKey: "opacity") + strongSelf.titleNode.alpha = 0.4 + strongSelf.textNode.layer.removeAnimation(forKey: "opacity") + strongSelf.textNode.alpha = 0.4 + } else { + strongSelf.titleNode.alpha = 1.0 + strongSelf.titleNode.layer.animateAlpha(from: 0.4, to: 1.0, duration: 0.2) + strongSelf.textNode.alpha = 1.0 + strongSelf.textNode.layer.animateAlpha(from: 0.4, to: 1.0, duration: 0.2) + } + } + } + + self.contentNode.addSubnode(self.titleNode) + self.contentNode.addSubnode(self.textNode) + self.contentNode.addSubnode(self.muteIconNode) + + self.contentNode.addSubnode(self.avatarsNode) + + self.tapButton.addTarget(self, action: #selector(self.tapped), forControlEvents: [.touchUpInside]) + self.contentNode.addSubnode(self.tapButton) + + self.joinButton.addSubnode(self.joinButtonBackgroundNode) + self.joinButton.addSubnode(self.joinButtonTitleNode) + self.contentNode.addSubnode(self.joinButton) + self.joinButton.addTarget(self, action: #selector(self.tapped), forControlEvents: [.touchUpInside]) + + self.micButton.addSubnode(self.micButtonBackgroundNode) + self.micButton.addSubnode(self.micButtonForegroundMutedNode) + self.micButton.addSubnode(self.micButtonForegroundUnmutedNode) + self.contentNode.addSubnode(self.micButton) + self.micButton.addTarget(self, action: #selector(self.micTapped), forControlEvents: [.touchUpInside]) + + self.contentNode.addSubnode(self.separatorNode) + + self.updatePresentationData(presentationData) + } + + deinit { + self.membersDisposable.dispose() + self.isMutedDisposable.dispose() + } + + override func didLoad() { + super.didLoad() + + let longTapRecognizer = UILongPressGestureRecognizer(target: self, action: #selector(self.micButtonPressGesture(_:))) + longTapRecognizer.minimumPressDuration = 0.01 + self.micButton.view.addGestureRecognizer(longTapRecognizer) + } + + @objc private func tapped() { + self.tapAction() + } + + @objc private func micTapped() { + guard let call = self.currentData?.groupCall else { + return + } + call.toggleIsMuted() + } + + @objc private func micButtonPressGesture(_ gestureRecognizer: UILongPressGestureRecognizer) { + guard let call = self.currentData?.groupCall else { + return + } + switch gestureRecognizer.state { + case .began: + call.setIsMuted(false) + case .ended, .cancelled: + call.setIsMuted(true) + default: + break + } + } + + func updatePresentationData(_ presentationData: PresentationData) { + self.theme = presentationData.theme + self.strings = presentationData.strings + + self.contentNode.backgroundColor = self.theme.rootController.navigationBar.backgroundColor + + self.theme = presentationData.theme + + self.separatorNode.backgroundColor = presentationData.theme.chat.historyNavigation.strokeColor + + self.joinButtonTitleNode.attributedText = NSAttributedString(string: presentationData.strings.Channel_JoinChannel.uppercased(), font: Font.semibold(15.0), textColor: presentationData.theme.chat.inputPanel.actionControlForegroundColor) + self.joinButtonBackgroundNode.image = generateStretchableFilledCircleImage(diameter: 28.0, color: presentationData.theme.chat.inputPanel.actionControlFillColor) + + //TODO:localize + self.micButtonBackgroundNode.image = generateStretchableFilledCircleImage(diameter: 36.0, color: UIColor(rgb: 0x30B251)) + self.micButtonForegroundMutedNode.image = generateTintedImage(image: UIImage(bundleImageName: "Call/Context Menu/Mute"), color: .white) + self.micButtonForegroundUnmutedNode.image = generateTintedImage(image: UIImage(bundleImageName: "Call/Context Menu/Unmute"), color: .white) + + //TODO:localize + self.titleNode.attributedText = NSAttributedString(string: "Voice Chat", font: Font.semibold(15.0), textColor: presentationData.theme.chat.inputPanel.primaryTextColor) + self.textNode.attributedText = NSAttributedString(string: self.textNode.attributedText?.string ?? "", font: Font.regular(13.0), textColor: presentationData.theme.chat.inputPanel.secondaryTextColor) + + self.muteIconNode.image = PresentationResourcesChat.chatTitleMuteIcon(presentationData.theme) + } + + func update(data: GroupCallPanelData) { + let previousData = self.currentData + self.currentData = data + + if previousData?.groupCall !== data.groupCall { + let membersText: String + if data.participantCount == 0 { + membersText = self.strings.PeopleNearby_NoMembers + } else { + membersText = self.strings.Conversation_StatusMembers(Int32(data.participantCount)) + } + + self.avatarsContent = self.avatarsContext.update(peers: data.topParticipants.map { $0.peer }, animated: false) + + self.textNode.attributedText = NSAttributedString(string: membersText, font: Font.regular(13.0), textColor: self.theme.chat.inputPanel.secondaryTextColor) + + self.membersDisposable.set(nil) + self.isMutedDisposable.set(nil) + + if let groupCall = data.groupCall { + self.membersDisposable.set((groupCall.summaryState + |> deliverOnMainQueue).start(next: { [weak self] summaryState in + guard let strongSelf = self, let summaryState = summaryState else { + return + } + + let membersText: String + if summaryState.participantCount == 0 { + membersText = strongSelf.strings.PeopleNearby_NoMembers + } else { + membersText = strongSelf.strings.Conversation_StatusMembers(Int32(summaryState.participantCount)) + } + + strongSelf.avatarsContent = strongSelf.avatarsContext.update(peers: summaryState.topParticipants.map { $0.peer }, animated: false) + + strongSelf.textNode.attributedText = NSAttributedString(string: membersText, font: Font.regular(13.0), textColor: strongSelf.theme.chat.inputPanel.secondaryTextColor) + if let (size, leftInset, rightInset) = strongSelf.validLayout { + strongSelf.updateLayout(size: size, leftInset: leftInset, rightInset: rightInset, transition: .immediate) + } + })) + + self.isMutedDisposable.set((groupCall.isMuted + |> deliverOnMainQueue).start(next: { [weak self] isMuted in + guard let strongSelf = self else { + return + } + strongSelf.micButtonForegroundMutedNode.isHidden = !isMuted + strongSelf.micButtonForegroundUnmutedNode.isHidden = isMuted + })) + } + } else if data.groupCall == nil { + let membersText: String + if data.participantCount == 0 { + membersText = self.strings.PeopleNearby_NoMembers + } else { + membersText = self.strings.Conversation_StatusMembers(Int32(data.participantCount)) + } + + self.avatarsContent = self.avatarsContext.update(peers: data.topParticipants.map { $0.peer }, animated: false) + + self.textNode.attributedText = NSAttributedString(string: membersText, font: Font.regular(13.0), textColor: self.theme.chat.inputPanel.secondaryTextColor) + } + + if let (size, leftInset, rightInset) = self.validLayout { + self.updateLayout(size: size, leftInset: leftInset, rightInset: rightInset, transition: .immediate) + } + } + + func updateLayout(size: CGSize, leftInset: CGFloat, rightInset: CGFloat, transition: ContainedViewLayoutTransition) { + self.validLayout = (size, leftInset, rightInset) + + let panelHeight = size.height + + transition.updateFrame(node: self.contentNode, frame: CGRect(origin: CGPoint(), size: size)) + + self.tapButton.frame = CGRect(origin: CGPoint(), size: CGSize(width: size.width - 7.0 - 36.0 - 7.0, height: panelHeight)) + + if let avatarsContent = self.avatarsContent { + let avatarsSize = self.avatarsNode.update(context: self.context, content: avatarsContent, itemSize: CGSize(width: 32.0, height: 32.0), animated: true, synchronousLoad: true) + transition.updateFrame(node: self.avatarsNode, frame: CGRect(origin: CGPoint(x: 7.0, y: floor((size.height - avatarsSize.height) / 2.0)), size: avatarsSize)) + } + + let joinButtonTitleSize = self.joinButtonTitleNode.updateLayout(CGSize(width: 150.0, height: .greatestFiniteMagnitude)) + let joinButtonSize = CGSize(width: joinButtonTitleSize.width + 20.0, height: 28.0) + let joinButtonFrame = CGRect(origin: CGPoint(x: size.width - rightInset - 7.0 - joinButtonSize.width, y: floor((panelHeight - joinButtonSize.height) / 2.0)), size: joinButtonSize) + transition.updateFrame(node: self.joinButton, frame: joinButtonFrame) + transition.updateFrame(node: self.joinButtonBackgroundNode, frame: CGRect(origin: CGPoint(), size: joinButtonFrame.size)) + transition.updateFrame(node: self.joinButtonTitleNode, frame: CGRect(origin: CGPoint(x: floorToScreenPixels((joinButtonFrame.width - joinButtonTitleSize.width) / 2.0), y: floorToScreenPixels((joinButtonFrame.height - joinButtonTitleSize.height) / 2.0)), size: joinButtonTitleSize)) + + let micButtonSize = CGSize(width: 36.0, height: 36.0) + let micButtonFrame = CGRect(origin: CGPoint(x: size.width - rightInset - 7.0 - micButtonSize.width, y: floor((panelHeight - micButtonSize.height) / 2.0)), size: micButtonSize) + transition.updateFrame(node: self.micButton, frame: micButtonFrame) + transition.updateFrame(node: self.micButtonBackgroundNode, frame: CGRect(origin: CGPoint(), size: micButtonFrame.size)) + if let image = self.micButtonForegroundMutedNode.image { + transition.updateFrame(node: self.micButtonForegroundMutedNode, frame: CGRect(origin: CGPoint(x: floor((micButtonFrame.width - image.size.width) / 2.0), y: floor((micButtonFrame.height - image.size.height) / 2.0)), size: image.size)) + } + if let image = self.micButtonForegroundUnmutedNode.image { + transition.updateFrame(node: self.micButtonForegroundUnmutedNode, frame: CGRect(origin: CGPoint(x: floor((micButtonFrame.width - image.size.width) / 2.0), y: floor((micButtonFrame.height - image.size.height) / 2.0)), size: image.size)) + } + + let titleSize = self.titleNode.updateLayout(CGSize(width: size.width, height: .greatestFiniteMagnitude)) + let textSize = self.textNode.updateLayout(CGSize(width: size.width, height: .greatestFiniteMagnitude)) + + let titleFrame = CGRect(origin: CGPoint(x: floor((size.width - titleSize.width) / 2.0), y: 10.0), size: titleSize) + transition.updateFrame(node: self.titleNode, frame: titleFrame) + transition.updateFrame(node: self.textNode, frame: CGRect(origin: CGPoint(x: floor((size.width - textSize.width) / 2.0), y: titleFrame.maxY + 1.0), size: textSize)) + + if let image = self.muteIconNode.image { + transition.updateFrame(node: self.muteIconNode, frame: CGRect(origin: CGPoint(x: titleFrame.maxX + 4.0, y: titleFrame.minY + 5.0), size: image.size)) + } + self.muteIconNode.isHidden = self.currentData?.groupCall != nil + self.joinButton.isHidden = self.currentData?.groupCall != nil + self.micButton.isHidden = self.currentData?.groupCall == nil + + transition.updateFrame(node: self.separatorNode, frame: CGRect(origin: CGPoint(x: 0.0, y: panelHeight - UIScreenPixel), size: CGSize(width: size.width, height: UIScreenPixel))) + } + + func animateIn(_ transition: ContainedViewLayoutTransition) { + self.clipsToBounds = true + let contentPosition = self.contentNode.layer.position + transition.animatePosition(node: self.contentNode, from: CGPoint(x: contentPosition.x, y: contentPosition.y - 50.0), completion: { [weak self] _ in + self?.clipsToBounds = false + }) + } + + func animateOut(_ transition: ContainedViewLayoutTransition, completion: @escaping () -> Void) { + self.clipsToBounds = true + let contentPosition = self.contentNode.layer.position + transition.animatePosition(node: self.contentNode, to: CGPoint(x: contentPosition.x, y: contentPosition.y - 50.0), removeOnCompletion: false, completion: { [weak self] _ in + self?.clipsToBounds = false + completion() + }) + } +} diff --git a/submodules/TelegramBaseController/Sources/TelegramBaseController.swift b/submodules/TelegramBaseController/Sources/TelegramBaseController.swift index 86d2e44dcb..e2924dc4e3 100644 --- a/submodules/TelegramBaseController/Sources/TelegramBaseController.swift +++ b/submodules/TelegramBaseController/Sources/TelegramBaseController.swift @@ -25,6 +25,34 @@ public enum LocationBroadcastPanelSource { case peer(PeerId) } +public enum GroupCallPanelSource { + case none + case all + case peer(PeerId) +} + +final class GroupCallPanelData { + let peerId: PeerId + let info: GroupCallInfo + let topParticipants: [GroupCallParticipantsContext.Participant] + let participantCount: Int + let groupCall: PresentationGroupCall? + + init( + peerId: PeerId, + info: GroupCallInfo, + topParticipants: [GroupCallParticipantsContext.Participant], + participantCount: Int, + groupCall: PresentationGroupCall? + ) { + self.peerId = peerId + self.info = info + self.topParticipants = topParticipants + self.participantCount = participantCount + self.groupCall = groupCall + } +} + private func presentLiveLocationController(context: AccountContext, peerId: PeerId, controller: ViewController) { let presentImpl: (Message?) -> Void = { [weak controller] message in if let message = message, let strongController = controller { @@ -64,9 +92,11 @@ open class TelegramBaseController: ViewController, KeyShortcutResponder { public let mediaAccessoryPanelVisibility: MediaAccessoryPanelVisibility public let locationBroadcastPanelSource: LocationBroadcastPanelSource + public let groupCallPanelSource: GroupCallPanelSource private var mediaStatusDisposable: Disposable? private var locationBroadcastDisposable: Disposable? + private var currentGroupCallDisposable: Disposable? public private(set) var playlistStateAndType: (SharedMediaPlaylistItem, SharedMediaPlaylistItem?, SharedMediaPlaylistItem?, MusicPlaybackSettingsOrder, MediaManagerPlayerType, Account)? private var playlistLocation: SharedMediaPlaylistLocation? @@ -81,6 +111,9 @@ open class TelegramBaseController: ViewController, KeyShortcutResponder { private var locationBroadcastMessages: [MessageId: Message]? private var locationBroadcastAccessoryPanel: LocationBroadcastNavigationAccessoryPanel? + private var groupCallPanelData: GroupCallPanelData? + private var groupCallAccessoryPanel: GroupCallNavigationAccessoryPanel? + private var dismissingPanel: ASDisplayNode? private var presentationData: PresentationData @@ -101,6 +134,9 @@ open class TelegramBaseController: ViewController, KeyShortcutResponder { public var additionalHeight: CGFloat { var height: CGFloat = 0.0 + if let _ = self.groupCallAccessoryPanel { + height += 50.0 + } if let _ = self.mediaAccessoryPanel { height += MediaNavigationAccessoryHeaderNode.minimizedHeight } @@ -114,11 +150,12 @@ open class TelegramBaseController: ViewController, KeyShortcutResponder { return super.navigationHeight } - public init(context: AccountContext, navigationBarPresentationData: NavigationBarPresentationData?, mediaAccessoryPanelVisibility: MediaAccessoryPanelVisibility, locationBroadcastPanelSource: LocationBroadcastPanelSource) { + public init(context: AccountContext, navigationBarPresentationData: NavigationBarPresentationData?, mediaAccessoryPanelVisibility: MediaAccessoryPanelVisibility, locationBroadcastPanelSource: LocationBroadcastPanelSource, groupCallPanelSource: GroupCallPanelSource) { self.context = context self.presentationData = context.sharedContext.currentPresentationData.with { $0 } self.mediaAccessoryPanelVisibility = mediaAccessoryPanelVisibility self.locationBroadcastPanelSource = locationBroadcastPanelSource + self.groupCallPanelSource = groupCallPanelSource super.init(navigationBarPresentationData: navigationBarPresentationData) @@ -250,6 +287,93 @@ open class TelegramBaseController: ViewController, KeyShortcutResponder { } } + if let callManager = context.sharedContext.callManager { + switch groupCallPanelSource { + case .none: + break + default: + let currentGroupCall: Signal = callManager.currentGroupCallSignal + |> distinctUntilChanged(isEqual: { lhs, rhs in + return lhs?.internalId == rhs?.internalId + }) + |> mapToSignal { call -> Signal in + guard let call = call else { + return .single(nil) + } + return call.summaryState + |> filter { $0 != nil } + |> take(1) + |> map { summary -> GroupCallPanelData? in + guard let summary = summary else { + return nil + } + return GroupCallPanelData( + peerId: call.peerId, + info: summary.info, + topParticipants: summary.topParticipants, + participantCount: summary.participantCount, + groupCall: call + ) + } + } + + let availableGroupCall: Signal + if case let .peer(peerId) = groupCallPanelSource { + availableGroupCall = context.account.viewTracker.peerView(peerId) + |> map { peerView -> CachedChannelData.ActiveCall? in + guard let cachedData = peerView.cachedData as? CachedChannelData else { + return nil + } + return cachedData.activeCall + } + |> distinctUntilChanged + |> mapToSignal { activeCall -> Signal in + guard let activeCall = activeCall else { + return .single(nil) + } + return getCurrentGroupCall(account: context.account, callId: activeCall.id, accessHash: activeCall.accessHash) + |> `catch` { _ -> Signal in + return .single(nil) + } + |> map { summary -> GroupCallPanelData? in + guard let summary = summary else { + return nil + } + return GroupCallPanelData( + peerId: peerId, + info: summary.info, + topParticipants: summary.topParticipants, + participantCount: summary.info.participantCount, + groupCall: nil + ) + } + } + } else { + availableGroupCall = .single(nil) + } + + self.currentGroupCallDisposable = combineLatest(queue: .mainQueue(), + currentGroupCall, + availableGroupCall + ).start(next: { [weak self] currentState, availableState in + guard let strongSelf = self else { + return + } + + let panelData = currentState ?? availableState + + let wasEmpty = strongSelf.groupCallPanelData == nil + strongSelf.groupCallPanelData = panelData + let isEmpty = strongSelf.groupCallPanelData == nil + if wasEmpty != isEmpty { + strongSelf.requestLayout(transition: .animated(duration: 0.4, curve: .spring)) + } else if let groupCallPanelData = strongSelf.groupCallPanelData { + strongSelf.groupCallAccessoryPanel?.update(data: groupCallPanelData) + } + }) + } + } + self.presentationDataDisposable = (context.sharedContext.presentationData |> deliverOnMainQueue).start(next: { [weak self] presentationData in if let strongSelf = self { @@ -269,6 +393,7 @@ open class TelegramBaseController: ViewController, KeyShortcutResponder { deinit { self.mediaStatusDisposable?.dispose() self.locationBroadcastDisposable?.dispose() + self.currentGroupCallDisposable?.dispose() self.presentationDataDisposable?.dispose() self.playlistPreloadDisposable?.dispose() } @@ -287,6 +412,52 @@ open class TelegramBaseController: ViewController, KeyShortcutResponder { var additionalHeight: CGFloat = 0.0 + if let groupCallPanelData = self.groupCallPanelData { + let panelHeight: CGFloat = 50.0 + let panelFrame = CGRect(origin: CGPoint(x: 0.0, y: navigationHeight.isZero ? -panelHeight : (navigationHeight + additionalHeight + UIScreenPixel)), size: CGSize(width: layout.size.width, height: panelHeight)) + additionalHeight += panelHeight + + let groupCallAccessoryPanel: GroupCallNavigationAccessoryPanel + if let current = self.groupCallAccessoryPanel { + groupCallAccessoryPanel = current + transition.updateFrame(node: groupCallAccessoryPanel, frame: panelFrame) + groupCallAccessoryPanel.updateLayout(size: panelFrame.size, leftInset: layout.safeInsets.left, rightInset: layout.safeInsets.right, transition: transition) + } else { + let presentationData = self.context.sharedContext.currentPresentationData.with { $0 } + groupCallAccessoryPanel = GroupCallNavigationAccessoryPanel(context: self.context, presentationData: presentationData, tapAction: { [weak self] in + guard let strongSelf = self else { + return + } + strongSelf.joinGroupCall( + peerId: groupCallPanelData.peerId, + info: groupCallPanelData.info + ) + }) + if let navigationBar = self.navigationBar { + self.displayNode.insertSubnode(groupCallAccessoryPanel, aboveSubnode: navigationBar) + } else { + self.displayNode.addSubnode(groupCallAccessoryPanel) + } + self.groupCallAccessoryPanel = groupCallAccessoryPanel + groupCallAccessoryPanel.frame = panelFrame + + groupCallAccessoryPanel.update(data: groupCallPanelData) + groupCallAccessoryPanel.updateLayout(size: panelFrame.size, leftInset: layout.safeInsets.left, rightInset: layout.safeInsets.right, transition: .immediate) + if transition.isAnimated { + groupCallAccessoryPanel.animateIn(transition) + } + } + } else if let groupCallAccessoryPanel = self.groupCallAccessoryPanel { + self.groupCallAccessoryPanel = nil + if transition.isAnimated { + groupCallAccessoryPanel.animateOut(transition, completion: { [weak groupCallAccessoryPanel] in + groupCallAccessoryPanel?.removeFromSupernode() + }) + } else { + groupCallAccessoryPanel.removeFromSupernode() + } + } + if let locationBroadcastPeers = self.locationBroadcastPeers, let locationBroadcastMode = self.locationBroadcastMode { let panelHeight = MediaNavigationAccessoryHeaderNode.minimizedHeight let panelFrame = CGRect(origin: CGPoint(x: 0.0, y: navigationHeight.isZero ? -panelHeight : (navigationHeight + additionalHeight + UIScreenPixel)), size: CGSize(width: layout.size.width, height: panelHeight)) @@ -675,4 +846,36 @@ open class TelegramBaseController: ViewController, KeyShortcutResponder { } })] } + + private func joinGroupCall(peerId: PeerId, info: GroupCallInfo) { + let callResult = self.context.sharedContext.callManager?.requestOrJoinGroupCall(context: self.context, peerId: peerId, initialCall: CachedChannelData.ActiveCall(id: info.id, accessHash: info.accessHash), endCurrentIfAny: false) + if let callResult = callResult, case let .alreadyInProgress(currentPeerId) = callResult { + if currentPeerId == peerId { + self.context.sharedContext.navigateToCurrentCall() + } else { + let presentationData = self.context.sharedContext.currentPresentationData.with { $0 } + let _ = (self.context.account.postbox.transaction { transaction -> (Peer?, Peer?) in + return (transaction.getPeer(peerId), currentPeerId.flatMap(transaction.getPeer)) + } + |> deliverOnMainQueue).start(next: { [weak self] peer, current in + guard let strongSelf = self else { + return + } + guard let peer = peer else { + return + } + if 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?.requestOrJoinGroupCall(context: strongSelf.context, peerId: peerId, initialCall: CachedChannelData.ActiveCall(id: info.id, accessHash: info.accessHash), endCurrentIfAny: true) + } + })]), in: .window(.root)) + } else { + strongSelf.present(textAlertController(context: strongSelf.context, title: presentationData.strings.Call_CallInProgressTitle, text: presentationData.strings.Call_ExternalCallInProgressMessage, actions: [TextAlertAction(type: .genericAction, title: presentationData.strings.Common_OK, action: { + })]), in: .window(.root)) + } + }) + } + } + } } diff --git a/submodules/TelegramCallsUI/Sources/PresentationCallManager.swift b/submodules/TelegramCallsUI/Sources/PresentationCallManager.swift index e809cf1d28..03a25b1def 100644 --- a/submodules/TelegramCallsUI/Sources/PresentationCallManager.swift +++ b/submodules/TelegramCallsUI/Sources/PresentationCallManager.swift @@ -592,17 +592,32 @@ public final class PresentationCallManagerImpl: PresentationCallManager { } } - public func requestOrJoinGroupCall(context: AccountContext, peerId: PeerId) -> RequestOrJoinGroupCallResult { - if let currentGroupCall = self.currentGroupCallValue { - return .alreadyInProgress(currentGroupCall.peerId) + public func requestOrJoinGroupCall(context: AccountContext, peerId: PeerId, initialCall: CachedChannelData.ActiveCall?, endCurrentIfAny: Bool) -> RequestOrJoinGroupCallResult { + let begin: () -> Void = { [weak self] in + let _ = self?.startGroupCall(accountContext: context, peerId: peerId, initialCall: initialCall).start() + } + if let currentGroupCall = self.currentGroupCallValue { + if endCurrentIfAny { + let endSignal = currentGroupCall.leave() + |> filter { $0 } + |> take(1) + |> deliverOnMainQueue + self.startCallDisposable.set(endSignal.start(next: { _ in + begin() + })) + } else { + return .alreadyInProgress(currentGroupCall.peerId) + } + } else { + begin() } - let _ = self.startGroupCall(accountContext: context, peerId: peerId).start() return .requested } private func startGroupCall( accountContext: AccountContext, peerId: PeerId, + initialCall: CachedChannelData.ActiveCall?, internalId: CallSessionInternalId = CallSessionInternalId() ) -> Signal { let (presentationData, present, openSettings) = self.getDeviceAccessData() @@ -649,6 +664,7 @@ public final class PresentationCallManagerImpl: PresentationCallManager { audioSession: strongSelf.audioSession, callKitIntegration: nil, getDeviceAccessData: strongSelf.getDeviceAccessData, + initialCall: initialCall, internalId: internalId, peerId: peerId, peer: nil diff --git a/submodules/TelegramCallsUI/Sources/PresentationGroupCall.swift b/submodules/TelegramCallsUI/Sources/PresentationGroupCall.swift index 7ab0c55bc6..74cd2d8671 100644 --- a/submodules/TelegramCallsUI/Sources/PresentationGroupCall.swift +++ b/submodules/TelegramCallsUI/Sources/PresentationGroupCall.swift @@ -41,6 +41,29 @@ public final class PresentationGroupCallImpl: PresentationGroupCall { } } + private struct SummaryInfoState: Equatable { + public var info: GroupCallInfo + + public init( + info: GroupCallInfo + ) { + self.info = info + } + } + + private struct SummaryParticipantsState: Equatable { + public var participantCount: Int + public var topParticipants: [GroupCallParticipantsContext.Participant] + + public init( + participantCount: Int, + topParticipants: [GroupCallParticipantsContext.Participant] + ) { + self.participantCount = participantCount + self.topParticipants = topParticipants + } + } + public let account: Account public let accountContext: AccountContext private let audioSession: ManagedAudioSession @@ -51,6 +74,7 @@ public final class PresentationGroupCallImpl: PresentationGroupCall { private let getDeviceAccessData: () -> (presentationData: PresentationData, present: (ViewController, Any?) -> Void, openSettings: () -> Void) + private let initialCall: CachedChannelData.ActiveCall? public let internalId: CallSessionInternalId public let peerId: PeerId public let peer: Peer? @@ -60,7 +84,14 @@ public final class PresentationGroupCallImpl: PresentationGroupCall { private var callContext: OngoingGroupCallContext? private var ssrcMapping: [UInt32: PeerId] = [:] - private var sessionStateDisposable: Disposable? + private var summaryInfoState = Promise(nil) + private var summaryParticipantsState = Promise(nil) + + private let summaryStatePromise = Promise(nil) + public var summaryState: Signal { + return self.summaryStatePromise.get() + } + private var summaryStateDisposable: Disposable? private let isMutedPromise = ValuePromise(true) private var isMutedValue = true @@ -143,6 +174,7 @@ public final class PresentationGroupCallImpl: PresentationGroupCall { audioSession: ManagedAudioSession, callKitIntegration: CallKitIntegration?, getDeviceAccessData: @escaping () -> (presentationData: PresentationData, present: (ViewController, Any?) -> Void, openSettings: () -> Void), + initialCall: CachedChannelData.ActiveCall?, internalId: CallSessionInternalId, peerId: PeerId, peer: Peer? @@ -153,6 +185,7 @@ public final class PresentationGroupCallImpl: PresentationGroupCall { self.callKitIntegration = callKitIntegration self.getDeviceAccessData = getDeviceAccessData + self.initialCall = initialCall self.internalId = internalId self.peerId = peerId self.peer = peer @@ -257,13 +290,33 @@ public final class PresentationGroupCallImpl: PresentationGroupCall { } }) + self.summaryStatePromise.set(combineLatest(queue: .mainQueue(), + self.summaryInfoState.get(), + self.summaryParticipantsState.get(), + self.statePromise.get() + ) + |> map { infoState, participantsState, callState -> PresentationGroupCallSummaryState? in + guard let infoState = infoState else { + return nil + } + guard let participantsState = participantsState else { + return nil + } + return PresentationGroupCallSummaryState( + info: infoState.info, + participantCount: participantsState.participantCount, + callState: callState, + topParticipants: participantsState.topParticipants + ) + }) + self.requestCall() } deinit { self.audioSessionShouldBeActiveDisposable?.dispose() self.audioSessionActiveDisposable?.dispose() - self.sessionStateDisposable?.dispose() + self.summaryStateDisposable?.dispose() self.audioSessionDisposable?.dispose() self.requestDisposable.dispose() self.groupCallParticipantUpdatesDisposable?.dispose() @@ -397,6 +450,8 @@ public final class PresentationGroupCallImpl: PresentationGroupCall { break default: if case let .estabilished(callInfo, clientParams, _, initialState) = internalState { + self.summaryInfoState.set(.single(SummaryInfoState(info: callInfo))) + self.ssrcMapping.removeAll() var ssrcs: [UInt32] = [] for participant in initialState.participants { @@ -419,7 +474,12 @@ public final class PresentationGroupCallImpl: PresentationGroupCall { } var memberStates: [PeerId: PresentationGroupCallMemberState] = [:] + var topParticipants: [GroupCallParticipantsContext.Participant] = [] for participant in state.participants { + if topParticipants.count < 3 { + topParticipants.append(participant) + } + strongSelf.ssrcMapping[participant.ssrc] = participant.peer.id memberStates[participant.peer.id] = PresentationGroupCallMemberState( @@ -428,6 +488,11 @@ public final class PresentationGroupCallImpl: PresentationGroupCall { ) } strongSelf.membersValue = memberStates + + strongSelf.summaryParticipantsState.set(.single(SummaryParticipantsState( + participantCount: state.totalCount, + topParticipants: topParticipants + ))) })) if let isCurrentlyConnecting = self.isCurrentlyConnecting, isCurrentlyConnecting { @@ -530,9 +595,17 @@ public final class PresentationGroupCallImpl: PresentationGroupCall { let account = self.account let peerId = self.peerId - let currentCall = getCurrentGroupCall(account: account, peerId: peerId) - |> mapError { _ -> CallError in - return .generic + let currentCall: Signal + if let initialCall = self.initialCall { + currentCall = getCurrentGroupCall(account: account, callId: initialCall.id, accessHash: initialCall.accessHash) + |> mapError { _ -> CallError in + return .generic + } + |> map { summary -> GroupCallInfo? in + return summary?.info + } + } else { + currentCall = .single(nil) } let currentOrRequestedCall = currentCall diff --git a/submodules/TelegramCallsUI/Sources/VoiceChatController.swift b/submodules/TelegramCallsUI/Sources/VoiceChatController.swift index abe73ed694..ce94f1f31d 100644 --- a/submodules/TelegramCallsUI/Sources/VoiceChatController.swift +++ b/submodules/TelegramCallsUI/Sources/VoiceChatController.swift @@ -442,6 +442,7 @@ public final class VoiceChatController: ViewController { guard let peer = view.peers[view.peerId] else { return } + //TODO:localize var subtitle = "group" if let cachedData = view.cachedData as? CachedChannelData { if let memberCount = cachedData.participantsSummary.memberCount { @@ -450,7 +451,7 @@ public final class VoiceChatController: ViewController { } let titleView = VoiceChatControllerTitleView(theme: strongSelf.presentationData.theme) - titleView.set(title: peer.debugDisplayTitle, subtitle: subtitle) + titleView.set(title: "Voice Chat", subtitle: subtitle) strongSelf.controller?.navigationItem.titleView = titleView if !strongSelf.didSetDataReady { @@ -522,7 +523,7 @@ public final class VoiceChatController: ViewController { } var items: [ContextMenuItem] = [] - items.append(.action(ContextMenuActionItem(text: strongSelf.presentationData.strings.VoiceChat_SpeakPermissionEveryone, icon: { theme in + /*items.append(.action(ContextMenuActionItem(text: strongSelf.presentationData.strings.VoiceChat_SpeakPermissionEveryone, icon: { theme in return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Check"), color: theme.actionSheet.primaryTextColor) }, action: { _, f in f(.dismissWithoutContent) @@ -532,7 +533,7 @@ public final class VoiceChatController: ViewController { f(.dismissWithoutContent) }))) - items.append(.separator) + items.append(.separator)*/ items.append(.action(ContextMenuActionItem(text: strongSelf.presentationData.strings.VoiceChat_Share, icon: { theme in return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Link"), color: theme.actionSheet.primaryTextColor) }, action: { [weak self] _, f in diff --git a/submodules/TelegramCore/Sources/GroupCalls.swift b/submodules/TelegramCore/Sources/GroupCalls.swift index 0300eea1f3..2bb9cfaefb 100644 --- a/submodules/TelegramCore/Sources/GroupCalls.swift +++ b/submodules/TelegramCore/Sources/GroupCalls.swift @@ -7,19 +7,19 @@ import SyncCore public struct GroupCallInfo: Equatable { public var id: Int64 public var accessHash: Int64 + public var participantCount: Int public var clientParams: String? } +public struct GroupCallSummary: Equatable { + public var info: GroupCallInfo + public var topParticipants: [GroupCallParticipantsContext.Participant] +} + private extension GroupCallInfo { init?(_ call: Api.GroupCall) { switch call { - case let .groupCallPrivate(id, accessHash, _): - self.init( - id: id, - accessHash: accessHash, - clientParams: nil - ) - case let .groupCall(_, id, accessHash, _, params, _): + case let .groupCall(_, id, accessHash, participantCount, params, _): var clientParams: String? if let params = params { switch params { @@ -30,6 +30,7 @@ private extension GroupCallInfo { self.init( id: id, accessHash: accessHash, + participantCount: Int(participantCount), clientParams: clientParams ) case .groupCallDiscarded: @@ -42,49 +43,66 @@ public enum GetCurrentGroupCallError { case generic } -public func getCurrentGroupCall(account: Account, peerId: PeerId) -> Signal { - return account.postbox.transaction { transaction -> Api.InputChannel? in - transaction.getPeer(peerId).flatMap(apiInputChannel) +public func getCurrentGroupCall(account: Account, callId: Int64, accessHash: Int64) -> Signal { + return account.network.request(Api.functions.phone.getGroupCall(call: .inputGroupCall(id: callId, accessHash: accessHash))) + |> mapError { _ -> GetCurrentGroupCallError in + return .generic } - |> castError(GetCurrentGroupCallError.self) - |> mapToSignal { inputPeer -> Signal in - guard let inputPeer = inputPeer else { - return .fail(.generic) - } - return account.network.request(Api.functions.channels.getFullChannel(channel: inputPeer)) - |> mapError { _ -> GetCurrentGroupCallError in - return .generic - } - |> mapToSignal { result -> Signal in - switch result { - case let .chatFull(fullChat, _, _): - switch fullChat { - case let .channelFull(_, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, inputCall): - return .single(inputCall) - default: - return .single(nil) + |> mapToSignal { result -> Signal in + switch result { + case let .groupCall(call, _, participants, users): + return account.postbox.transaction { transaction -> GroupCallSummary? in + guard let info = GroupCallInfo(call) else { + return nil } + + var peers: [Peer] = [] + var peerPresences: [PeerId: PeerPresence] = [:] + + for user in users { + let telegramUser = TelegramUser(user: user) + peers.append(telegramUser) + if let presence = TelegramUserPresence(apiUser: user) { + peerPresences[telegramUser.id] = presence + } + } + + updatePeers(transaction: transaction, peers: peers, update: { _, updated -> Peer in + return updated + }) + updatePeerPresences(transaction: transaction, accountPeerId: account.peerId, peerPresences: peerPresences) + + var parsedParticipants: [GroupCallParticipantsContext.Participant] = [] + + loop: for participant in participants { + switch participant { + case let .groupCallParticipant(flags, userId, date, source): + let peerId = PeerId(namespace: Namespaces.Peer.CloudUser, id: userId) + let ssrc = UInt32(bitPattern: source) + guard let peer = transaction.getPeer(peerId) else { + continue loop + } + var muteState: GroupCallParticipantsContext.Participant.MuteState? + if (flags & (1 << 0)) != 0 { + let canUnmute = (flags & (1 << 2)) != 0 + muteState = GroupCallParticipantsContext.Participant.MuteState(canUnmute: canUnmute) + } + parsedParticipants.append(GroupCallParticipantsContext.Participant( + peer: peer, + ssrc: ssrc, + joinTimestamp: date, + muteState: muteState + )) + } + } + + return GroupCallSummary( + info: info, + topParticipants: parsedParticipants + ) } - } - } - |> mapToSignal { inputCall -> Signal in - guard let inputCall = inputCall else { - return .single(nil) - } - - return account.network.request(Api.functions.phone.getGroupCall(call: inputCall)) - |> mapError { _ -> GetCurrentGroupCallError in - return .generic - } - |> mapToSignal { result -> Signal in - switch result { - case let .groupCall(call, _, _, _): - return account.postbox.transaction { transaction -> GroupCallInfo? in - return GroupCallInfo(call) - } - |> mapError { _ -> GetCurrentGroupCallError in - return .generic - } + |> mapError { _ -> GetCurrentGroupCallError in + return .generic } } } diff --git a/submodules/TelegramUI/Sources/ChatController.swift b/submodules/TelegramUI/Sources/ChatController.swift index 43c568ea24..dbeaad1624 100644 --- a/submodules/TelegramUI/Sources/ChatController.swift +++ b/submodules/TelegramUI/Sources/ChatController.swift @@ -397,13 +397,16 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G self.peekData = peekData var locationBroadcastPanelSource: LocationBroadcastPanelSource + var groupCallPanelSource: GroupCallPanelSource switch chatLocation { case let .peer(peerId): locationBroadcastPanelSource = .peer(peerId) + groupCallPanelSource = .peer(peerId) self.chatLocationInfoData = .peer(Promise()) case let .replyThread(replyThreadMessage): locationBroadcastPanelSource = .none + groupCallPanelSource = .all let promise = Promise() let key = PostboxViewKey.messages([replyThreadMessage.messageId]) promise.set(context.account.postbox.combinedView(keys: [key]) @@ -428,6 +431,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G mediaAccessoryPanelVisibility = .specific(size: .compact) } else { locationBroadcastPanelSource = .none + groupCallPanelSource = .none } let navigationBarPresentationData: NavigationBarPresentationData? switch mode { @@ -436,7 +440,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G default: navigationBarPresentationData = NavigationBarPresentationData(presentationData: self.presentationData, hideBackground: self.context.sharedContext.immediateExperimentalUISettings.playerEmbedding ? true : false, hideBadge: false) } - super.init(context: context, navigationBarPresentationData: navigationBarPresentationData, mediaAccessoryPanelVisibility: mediaAccessoryPanelVisibility, locationBroadcastPanelSource: locationBroadcastPanelSource) + super.init(context: context, navigationBarPresentationData: navigationBarPresentationData, mediaAccessoryPanelVisibility: mediaAccessoryPanelVisibility, locationBroadcastPanelSource: locationBroadcastPanelSource, groupCallPanelSource: groupCallPanelSource) self.automaticallyControlPresentationContextLayout = false self.blocksBackgroundWhenInOverlay = true @@ -5982,7 +5986,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G guard let strongSelf = self, let peer = strongSelf.presentationInterfaceState.renderedPeer?.peer else { return } - let callResult = strongSelf.context.sharedContext.callManager?.requestOrJoinGroupCall(context: strongSelf.context, peerId: peer.id) + let callResult = strongSelf.context.sharedContext.callManager?.requestOrJoinGroupCall(context: strongSelf.context, peerId: peer.id, initialCall: activeCall, endCurrentIfAny: false) if let callResult = callResult, case let .alreadyInProgress(currentPeerId) = callResult { if currentPeerId == peer.id { strongSelf.context.sharedContext.navigateToCurrentCall() @@ -5990,12 +5994,12 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G let presentationData = strongSelf.context.sharedContext.currentPresentationData.with { $0 } let _ = (strongSelf.context.account.postbox.transaction { transaction -> (Peer?, Peer?) in return (transaction.getPeer(peer.id), currentPeerId.flatMap(transaction.getPeer)) - } |> deliverOnMainQueue).start(next: { [weak self] peer, current in + } |> deliverOnMainQueue).start(next: { peer, current in if let peer = peer { if let strongSelf = self, 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(context: context, peerId: peerId, isVideo: isVideo, endCurrentIfAny: true) + let _ = strongSelf.context.sharedContext.callManager?.requestOrJoinGroupCall(context: strongSelf.context, peerId: peer.id, initialCall: activeCall, endCurrentIfAny: true) } })]), in: .window(.root)) } else { diff --git a/submodules/TelegramUI/Sources/ChatInterfaceTitlePanelNodes.swift b/submodules/TelegramUI/Sources/ChatInterfaceTitlePanelNodes.swift index c698ec6967..90ca8622d4 100644 --- a/submodules/TelegramUI/Sources/ChatInterfaceTitlePanelNodes.swift +++ b/submodules/TelegramUI/Sources/ChatInterfaceTitlePanelNodes.swift @@ -64,16 +64,6 @@ func titlePanelForChatPresentationInterfaceState(_ chatPresentationInterfaceStat } } - if chatPresentationInterfaceState.activeGroupCallInfo != nil { - if let currentPanel = currentPanel as? ChatCallTitlePanelNode { - return currentPanel - } else { - let panel = ChatCallTitlePanelNode(context: context) - panel.interfaceInteraction = interfaceInteraction - return panel - } - } - if let selectedContext = selectedContext { switch selectedContext { case .pinnedMessage: diff --git a/submodules/TelegramUI/Sources/ChatRecentActionsController.swift b/submodules/TelegramUI/Sources/ChatRecentActionsController.swift index 9398c9f4e9..26cacb9956 100644 --- a/submodules/TelegramUI/Sources/ChatRecentActionsController.swift +++ b/submodules/TelegramUI/Sources/ChatRecentActionsController.swift @@ -36,7 +36,7 @@ final class ChatRecentActionsController: TelegramBaseController { self.titleView = ChatRecentActionsTitleView(color: self.presentationData.theme.rootController.navigationBar.primaryTextColor) - super.init(context: context, navigationBarPresentationData: NavigationBarPresentationData(presentationData: self.presentationData), mediaAccessoryPanelVisibility: .specific(size: .compact), locationBroadcastPanelSource: .none) + super.init(context: context, navigationBarPresentationData: NavigationBarPresentationData(presentationData: self.presentationData), mediaAccessoryPanelVisibility: .specific(size: .compact), locationBroadcastPanelSource: .none, groupCallPanelSource: .none) self.statusBar.statusBarStyle = self.presentationData.theme.rootController.statusBarStyle.style diff --git a/submodules/TelegramUI/Sources/PeerInfo/PeerInfoScreen.swift b/submodules/TelegramUI/Sources/PeerInfo/PeerInfoScreen.swift index 31f8156343..179d2563e2 100644 --- a/submodules/TelegramUI/Sources/PeerInfo/PeerInfoScreen.swift +++ b/submodules/TelegramUI/Sources/PeerInfo/PeerInfoScreen.swift @@ -3166,7 +3166,10 @@ private final class PeerInfoScreenNode: ViewControllerTracingNode, UIScrollViewD private func requestCall(isVideo: Bool) { if let peer = self.data?.peer as? TelegramChannel { - self.context.sharedContext.callManager?.requestOrJoinGroupCall(context: self.context, peerId: peer.id) + guard let cachedChannelData = self.data?.cachedData as? CachedChannelData else { + return + } + let _ = self.context.sharedContext.callManager?.requestOrJoinGroupCall(context: self.context, peerId: peer.id, initialCall: cachedChannelData.activeCall, endCurrentIfAny: false) return } diff --git a/submodules/TelegramUI/Sources/SharedAccountContext.swift b/submodules/TelegramUI/Sources/SharedAccountContext.swift index f1f76af857..e9b11b138d 100644 --- a/submodules/TelegramUI/Sources/SharedAccountContext.swift +++ b/submodules/TelegramUI/Sources/SharedAccountContext.swift @@ -655,23 +655,26 @@ public final class SharedAccountContextImpl: SharedAccountContext { } }) - self.callStateDisposable = (self.callState.get() - |> deliverOnMainQueue).start(next: { [weak self] state in + self.callStateDisposable = combineLatest(queue: .mainQueue(), + self.callState.get(), + callManager.currentGroupCallSignal + |> map { call -> Bool in + return call != nil + } + ).start(next: { [weak self] state, hasGroupCall in if let strongSelf = self { let resolvedText: CallStatusText if let state = state { -// if [.active, .paused].contains(state.videoState) || [.active, .paused].contains(state.remoteVideoState) { -// resolvedText = .none -// } else { - switch state.state { - case .connecting, .requesting, .terminating, .ringing, .waiting: - resolvedText = .inProgress(nil) - case .terminated: - resolvedText = .none - case .active(let timestamp, _, _), .reconnecting(let timestamp, _, _): - resolvedText = .inProgress(timestamp) - } -// } + switch state.state { + case .connecting, .requesting, .terminating, .ringing, .waiting: + resolvedText = .inProgress(nil) + case .terminated: + resolvedText = .none + case .active(let timestamp, _, _), .reconnecting(let timestamp, _, _): + resolvedText = .inProgress(timestamp) + } + } else if hasGroupCall { + resolvedText = .inProgress(nil) } else { resolvedText = .none } @@ -705,7 +708,10 @@ public final class SharedAccountContextImpl: SharedAccountContext { }) mainWindow.inCallNavigate = { [weak self] in - if let strongSelf = self, let callController = strongSelf.callController { + guard let strongSelf = self else { + return + } + if let callController = strongSelf.callController { if callController.isNodeLoaded { mainWindow.hostView.containerView.endEditing(true) if callController.view.superview == nil { @@ -714,6 +720,13 @@ public final class SharedAccountContextImpl: SharedAccountContext { callController.expandFromPipIfPossible() } } + } else if let groupCallController = strongSelf.groupCallController { + if groupCallController.isNodeLoaded { + mainWindow.hostView.containerView.endEditing(true) + if groupCallController.view.superview == nil { + mainWindow.present(groupCallController, on: .calls) + } + } } } } else {