diff --git a/submodules/AccountContext/Sources/ContactSelectionController.swift b/submodules/AccountContext/Sources/ContactSelectionController.swift index 0aef7792c4..191e464c78 100644 --- a/submodules/AccountContext/Sources/ContactSelectionController.swift +++ b/submodules/AccountContext/Sources/ContactSelectionController.swift @@ -111,6 +111,7 @@ public final class ContactSelectionControllerParams { public let requirePhoneNumbers: Bool public let allowChannelsInSearch: Bool public let confirmation: (ContactListPeer) -> Signal + public let isPeerEnabled: (ContactListPeer) -> Bool public let openProfile: ((EnginePeer) -> Void)? public let sendMessage: ((EnginePeer) -> Void)? @@ -127,6 +128,7 @@ public final class ContactSelectionControllerParams { requirePhoneNumbers: Bool = false, allowChannelsInSearch: Bool = false, confirmation: @escaping (ContactListPeer) -> Signal = { _ in .single(true) }, + isPeerEnabled: @escaping (ContactListPeer) -> Bool = { _ in true }, openProfile: ((EnginePeer) -> Void)? = nil, sendMessage: ((EnginePeer) -> Void)? = nil ) { @@ -142,6 +144,7 @@ public final class ContactSelectionControllerParams { self.requirePhoneNumbers = requirePhoneNumbers self.allowChannelsInSearch = allowChannelsInSearch self.confirmation = confirmation + self.isPeerEnabled = isPeerEnabled self.openProfile = openProfile self.sendMessage = sendMessage } diff --git a/submodules/AccountContext/Sources/PresentationCallManager.swift b/submodules/AccountContext/Sources/PresentationCallManager.swift index e032400f5d..88fb9bfc5f 100644 --- a/submodules/AccountContext/Sources/PresentationCallManager.swift +++ b/submodules/AccountContext/Sources/PresentationCallManager.swift @@ -550,10 +550,6 @@ public enum PresentationCurrentCall: Equatable { } } -public enum JoinConferenceCallMode { - case joining -} - public protocol PresentationCallManager: AnyObject { var currentCallSignal: Signal { get } var currentGroupCallSignal: Signal { get } @@ -568,6 +564,6 @@ public protocol PresentationCallManager: AnyObject { accountContext: AccountContext, initialCall: EngineGroupCallDescription, reference: InternalGroupCallReference, - mode: JoinConferenceCallMode + beginWithVideo: Bool ) } diff --git a/submodules/CallListUI/Sources/CallListController.swift b/submodules/CallListUI/Sources/CallListController.swift index fa47ec33a6..4abc9c7ad7 100644 --- a/submodules/CallListUI/Sources/CallListController.swift +++ b/submodules/CallListUI/Sources/CallListController.swift @@ -231,7 +231,7 @@ public final class CallListController: TelegramBaseController { isStream: false ), reference: .id(id: call.callInfo.id, accessHash: call.callInfo.accessHash), - mode: .joining + beginWithVideo: false ) } diff --git a/submodules/ContactsPeerItem/Sources/ContactsPeerItem.swift b/submodules/ContactsPeerItem/Sources/ContactsPeerItem.swift index 83f08dc062..79ce46c3fc 100644 --- a/submodules/ContactsPeerItem/Sources/ContactsPeerItem.swift +++ b/submodules/ContactsPeerItem/Sources/ContactsPeerItem.swift @@ -692,6 +692,9 @@ public class ContactsPeerItemNode: ItemListRevealOptionsItemNode { public func updateIsHighlighted(transition: ContainedViewLayoutTransition) { var reallyHighlighted = self.isHighlighted + if let item = self.item, !item.enabled { + reallyHighlighted = false + } let highlightProgress: CGFloat = self.item?.itemHighlighting?.progress ?? 1.0 if let item = self.item { switch item.peer { @@ -1649,6 +1652,9 @@ public class ContactsPeerItemNode: ItemListRevealOptionsItemNode { actionButtonNode.setImage(actionButton.image, for: .normal) transition.updateFrame(node: actionButtonNode, frame: CGRect(origin: CGPoint(x: revealOffset + params.width - params.rightInset - 12.0 - actionButtonImage.size.width - offset, y: floor((nodeLayout.contentSize.height - actionButtonImage.size.height) / 2.0)), size: actionButtonImage.size)) + actionButtonNode.isEnabled = item.enabled + actionButtonNode.alpha = item.enabled ? 1.0 : 0.4 + offset += actionButtonImage.size.width + 12.0 } } diff --git a/submodules/TelegramCallsUI/Sources/CallController.swift b/submodules/TelegramCallsUI/Sources/CallController.swift index 57dd437ecb..c667d45901 100644 --- a/submodules/TelegramCallsUI/Sources/CallController.swift +++ b/submodules/TelegramCallsUI/Sources/CallController.swift @@ -486,7 +486,7 @@ public final class CallController: ViewController { var disablePeerIds: [EnginePeer.Id] = [] disablePeerIds.append(self.call.context.account.peerId) disablePeerIds.append(self.call.peerId) - let controller = CallController.openConferenceAddParticipant(context: self.call.context, disablePeerIds: disablePeerIds, completion: { [weak self] peers in + let controller = CallController.openConferenceAddParticipant(context: self.call.context, disablePeerIds: disablePeerIds, shareLink: nil, completion: { [weak self] peers in guard let self else { return } @@ -497,15 +497,18 @@ public final class CallController: ViewController { self.push(controller) } - static func openConferenceAddParticipant(context: AccountContext, disablePeerIds: [EnginePeer.Id], completion: @escaping ([(id: EnginePeer.Id, isVideo: Bool)]) -> Void) -> ViewController { + static func openConferenceAddParticipant(context: AccountContext, disablePeerIds: [EnginePeer.Id], shareLink: (() -> Void)?, completion: @escaping ([(id: EnginePeer.Id, isVideo: Bool)]) -> Void) -> ViewController { //TODO:localize - let presentationData = context.sharedContext.currentPresentationData.with({ $0 }).withUpdated(theme: defaultDarkPresentationTheme) + let presentationData = context.sharedContext.currentPresentationData.with({ $0 }).withUpdated(theme: defaultDarkColorPresentationTheme) var options: [ContactListAdditionalOption] = [] - //TODO:localize - options.append(ContactListAdditionalOption(title: "Share Call Link", icon: .generic(UIImage(bundleImageName: "Contact List/LinkActionIcon")!), action: { - //TODO:release - }, clearHighlightAutomatically: false)) + var openShareLinkImpl: (() -> Void)? + if shareLink != nil { + //TODO:localize + options.append(ContactListAdditionalOption(title: "Share Call Link", icon: .generic(UIImage(bundleImageName: "Contact List/LinkActionIcon")!), action: { + openShareLinkImpl?() + }, clearHighlightAutomatically: false)) + } let controller = context.sharedContext.makeContactSelectionController(ContactSelectionControllerParams( context: context, @@ -534,9 +537,32 @@ public final class CallController: ViewController { default: return .single(false) } + }, + isPeerEnabled: { peer in + switch peer { + case let .peer(peer, _, _): + let peer = EnginePeer(peer) + guard case let .user(user) = peer else { + return false + } + if disablePeerIds.contains(user.id) { + return false + } + if user.botInfo != nil { + return false + } + return true + default: + return false + } } )) + openShareLinkImpl = { [weak controller] in + controller?.dismiss() + shareLink?() + } + controller.navigationPresentation = .modal let _ = (controller.result |> take(1) |> deliverOnMainQueue).startStandalone(next: { [weak controller] result in guard let result, let peer = result.0.first, case let .peer(peer, _, _) = peer else { diff --git a/submodules/TelegramCallsUI/Sources/PresentationCall.swift b/submodules/TelegramCallsUI/Sources/PresentationCall.swift index da09ba65df..9d9207912c 100644 --- a/submodules/TelegramCallsUI/Sources/PresentationCall.swift +++ b/submodules/TelegramCallsUI/Sources/PresentationCall.swift @@ -1026,6 +1026,7 @@ public final class PresentationCallImpl: PresentationCall { keyPair: keyPair, conferenceSourceId: self.internalId, isConference: true, + beginWithVideo: false, sharedAudioContext: self.sharedAudioContext ) self.conferenceCallImpl = conferenceCall diff --git a/submodules/TelegramCallsUI/Sources/PresentationCallManager.swift b/submodules/TelegramCallsUI/Sources/PresentationCallManager.swift index 31031d7e22..7d3b79b3fd 100644 --- a/submodules/TelegramCallsUI/Sources/PresentationCallManager.swift +++ b/submodules/TelegramCallsUI/Sources/PresentationCallManager.swift @@ -850,6 +850,7 @@ public final class PresentationCallManagerImpl: PresentationCallManager { keyPair: nil, conferenceSourceId: nil, isConference: false, + beginWithVideo: false, sharedAudioContext: nil ) call.schedule(timestamp: timestamp) @@ -1076,6 +1077,7 @@ public final class PresentationCallManagerImpl: PresentationCallManager { keyPair: nil, conferenceSourceId: nil, isConference: false, + beginWithVideo: false, sharedAudioContext: nil ) self.updateCurrentGroupCall(.group(call)) @@ -1085,16 +1087,13 @@ public final class PresentationCallManagerImpl: PresentationCallManager { accountContext: AccountContext, initialCall: EngineGroupCallDescription, reference: InternalGroupCallReference, - mode: JoinConferenceCallMode + beginWithVideo: Bool ) { let keyPair: TelegramKeyPair - switch mode { - case .joining: - guard let keyPairValue = TelegramE2EEncryptionProviderImpl.shared.generateKeyPair() else { - return - } - keyPair = keyPairValue + guard let keyPairValue = TelegramE2EEncryptionProviderImpl.shared.generateKeyPair() else { + return } + keyPair = keyPairValue let call = PresentationGroupCallImpl( accountContext: accountContext, @@ -1111,6 +1110,7 @@ public final class PresentationCallManagerImpl: PresentationCallManager { keyPair: keyPair, conferenceSourceId: nil, isConference: true, + beginWithVideo: beginWithVideo, sharedAudioContext: nil ) self.updateCurrentGroupCall(.group(call)) diff --git a/submodules/TelegramCallsUI/Sources/PresentationGroupCall.swift b/submodules/TelegramCallsUI/Sources/PresentationGroupCall.swift index 83e85c6728..e0047cd47b 100644 --- a/submodules/TelegramCallsUI/Sources/PresentationGroupCall.swift +++ b/submodules/TelegramCallsUI/Sources/PresentationGroupCall.swift @@ -621,54 +621,98 @@ private final class PendingConferenceInvitationContext { case ringing } - private let callSessionManager: CallSessionManager + private let engine: TelegramEngine private var requestDisposable: Disposable? private var stateDisposable: Disposable? - private var internalId: CallSessionInternalId? + private(set) var messageId: EngineMessage.Id? + private var hadMessage: Bool = false private var didNotifyEnded: Bool = false - init(callSessionManager: CallSessionManager, groupCall: GroupCallReference, peerId: PeerId, onStateUpdated: @escaping (State) -> Void, onEnded: @escaping (Bool) -> Void) { - self.callSessionManager = callSessionManager - - preconditionFailure() - - /*self.requestDisposable = (callSessionManager.request(peerId: peerId, isVideo: false, enableVideo: true, conferenceCall: (groupCall, encryptionKey)) - |> deliverOnMainQueue).startStrict(next: { [weak self] internalId in + init(engine: TelegramEngine, reference: InternalGroupCallReference, peerId: PeerId, isVideo: Bool, onStateUpdated: @escaping (State) -> Void, onEnded: @escaping (Bool) -> Void) { + self.engine = engine + self.requestDisposable = (engine.calls.inviteConferenceCallParticipant(reference: reference, peerId: peerId, isVideo: isVideo).startStrict(next: { [weak self] messageId in guard let self else { return } - self.internalId = internalId + guard let messageId else { + if !self.didNotifyEnded { + self.didNotifyEnded = true + onEnded(false) + } + return + } + self.messageId = messageId - self.stateDisposable = (self.callSessionManager.callState(internalId: internalId) - |> deliverOnMainQueue).startStrict(next: { [weak self] state in + onStateUpdated(.ringing) + + let timeout: Double = 30.0 + let timerSignal = Signal.single(Void()) |> then( + Signal.single(Void()) + |> delay(1.0, queue: .mainQueue()) + ) |> restart + + let startTime = CFAbsoluteTimeGetCurrent() + self.stateDisposable = (combineLatest(queue: .mainQueue(), + engine.data.subscribe( + TelegramEngine.EngineData.Item.Messages.Message(id: messageId) + ), + timerSignal + ) + |> deliverOnMainQueue).startStrict(next: { [weak self] message, _ in guard let self else { return } - switch state.state { - case let .requesting(ringing, _): - if ringing { - onStateUpdated(.ringing) + if let message { + self.hadMessage = true + if message.timestamp + Int32(timeout) <= Int32(Date().timeIntervalSince1970) { + if !self.didNotifyEnded { + self.didNotifyEnded = true + onEnded(false) + } + } else { + var isActive = false + var isAccepted = false + var foundAction: TelegramMediaAction? + for media in message.media { + if let action = media as? TelegramMediaAction { + foundAction = action + break + } + } + + if let action = foundAction, case let .conferenceCall(conferenceCall) = action.action { + if conferenceCall.flags.contains(.isMissed) || conferenceCall.duration != nil { + } else { + if conferenceCall.flags.contains(.isActive) { + isAccepted = true + } else { + isActive = true + } + } + } + if !isActive { + if !self.didNotifyEnded { + self.didNotifyEnded = true + onEnded(isAccepted) + } + } } - case let .dropping(reason), let .terminated(_, reason, _): - if !self.didNotifyEnded { - self.didNotifyEnded = true - onEnded(reason == .ended(.switchedToConference)) + } else { + if self.hadMessage || CFAbsoluteTimeGetCurrent() > startTime + 1.0 { + if !self.didNotifyEnded { + self.didNotifyEnded = true + onEnded(false) + } } - default: - break } }) - })*/ + })) } deinit { self.requestDisposable?.dispose() self.stateDisposable?.dispose() - - if let internalId = self.internalId { - self.callSessionManager.drop(internalId: internalId, reason: .hangUp, debugLog: .single(nil)) - } } } @@ -1121,6 +1165,7 @@ public final class PresentationGroupCallImpl: PresentationGroupCall { private let sharedAudioContext: SharedCallAudioContext? public let isConference: Bool + private let beginWithVideo: Bool private let conferenceSourceId: CallSessionInternalId? public var conferenceSource: CallSessionInternalId? { @@ -1153,6 +1198,7 @@ public final class PresentationGroupCallImpl: PresentationGroupCall { keyPair: TelegramKeyPair?, conferenceSourceId: CallSessionInternalId?, isConference: Bool, + beginWithVideo: Bool, sharedAudioContext: SharedCallAudioContext? ) { self.account = accountContext.account @@ -1183,6 +1229,7 @@ public final class PresentationGroupCallImpl: PresentationGroupCall { self.isStream = isStream self.conferenceSourceId = conferenceSourceId self.isConference = isConference + self.beginWithVideo = beginWithVideo self.keyPair = keyPair if let keyPair, let initialCall { @@ -1490,6 +1537,10 @@ public final class PresentationGroupCallImpl: PresentationGroupCall { } strongSelf.screencastBufferClientContext = IpcGroupCallBufferBroadcastContext(basePath: basePath) })*/ + + if beginWithVideo { + self.requestVideo() + } } deinit { @@ -3844,22 +3895,17 @@ public final class PresentationGroupCallImpl: PresentationGroupCall { return false } - //TODO:release - let _ = self.accountContext.engine.calls.inviteConferenceCallParticipant(callId: initialCall.description.id, accessHash: initialCall.description.accessHash, peerId: peerId, isVideo: isVideo).start() - return false - /*guard let initialCall = self.initialCall else { - return false - } - if conferenceInvitationContexts[peerId] != nil { + if self.conferenceInvitationContexts[peerId] != nil { return false } var onStateUpdated: ((PendingConferenceInvitationContext.State) -> Void)? var onEnded: ((Bool) -> Void)? var didEndAlready = false let invitationContext = PendingConferenceInvitationContext( - callSessionManager: self.accountContext.account.callSessionManager, - groupCall: GroupCallReference(id: initialCall.id, accessHash: initialCall.accessHash), + engine: self.accountContext.engine, + reference: initialCall.reference, peerId: peerId, + isVideo: isVideo, onStateUpdated: { state in onStateUpdated?(state) }, @@ -3906,7 +3952,7 @@ public final class PresentationGroupCallImpl: PresentationGroupCall { } } - return false*/ + return false } else { guard let callInfo = self.internalState.callInfo, !self.invitedPeersValue.contains(where: { $0.id == peerId }) else { return false @@ -3933,6 +3979,13 @@ public final class PresentationGroupCallImpl: PresentationGroupCall { var updatedInvitedPeers = self.invitedPeersValue updatedInvitedPeers.removeAll(where: { $0.id == peerId}) self.invitedPeersValue = updatedInvitedPeers + + if let conferenceInvitationContext = self.conferenceInvitationContexts[peerId] { + self.conferenceInvitationContexts.removeValue(forKey: peerId) + if let messageId = conferenceInvitationContext.messageId { + self.accountContext.engine.account.callSessionManager.dropOutgoingConferenceRequest(messageId: messageId) + } + } } public func updateTitle(_ title: String) { diff --git a/submodules/TelegramCallsUI/Sources/VideoChatEncryptionKeyComponent.swift b/submodules/TelegramCallsUI/Sources/VideoChatEncryptionKeyComponent.swift index 73cef654bd..7b416571c0 100644 --- a/submodules/TelegramCallsUI/Sources/VideoChatEncryptionKeyComponent.swift +++ b/submodules/TelegramCallsUI/Sources/VideoChatEncryptionKeyComponent.swift @@ -5,6 +5,179 @@ import ComponentFlow import MultilineTextComponent import BalancedTextComponent import TelegramPresentationData +import CallsEmoji + +private final class EmojiItemComponent: Component { + let emoji: String? + + init(emoji: String?) { + self.emoji = emoji + } + + static func ==(lhs: EmojiItemComponent, rhs: EmojiItemComponent) -> Bool { + if lhs.emoji != rhs.emoji { + return false + } + return true + } + + final class View: UIView { + private let measureEmojiView = ComponentView() + private var pendingContainerView: UIView? + private var pendingEmojiViews: [ComponentView] = [] + private var emojiView: ComponentView? + + private var component: EmojiItemComponent? + private weak var state: EmptyComponentState? + + private var pendingEmojiValues: [String]? + + override init(frame: CGRect) { + super.init(frame: frame) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + deinit { + } + + func update(component: EmojiItemComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { + self.component = component + self.state = state + + let size = self.measureEmojiView.update( + transition: .immediate, + component: AnyComponent(MultilineTextComponent( + text: .plain(NSAttributedString(string: "👍", font: Font.regular(40.0), textColor: .white)) + )), + environment: {}, + containerSize: CGSize(width: 200.0, height: 200.0) + ) + + let borderEmoji = 2 + let numEmoji = borderEmoji * 2 + 3 + + if let emoji = component.emoji { + let emojiView: ComponentView + var emojiViewTransition = transition + if let current = self.emojiView { + emojiView = current + } else { + emojiViewTransition = .immediate + emojiView = ComponentView() + self.emojiView = emojiView + } + let emojiSize = emojiView.update( + transition: .immediate, + component: AnyComponent(MultilineTextComponent( + text: .plain(NSAttributedString(string: emoji, font: Font.regular(40.0), textColor: .white)) + )), + environment: {}, + containerSize: CGSize(width: 200.0, height: 200.0) + ) + let emojiFrame = CGRect(origin: CGPoint(x: floor((size.width - emojiSize.width) * 0.5), y: floor((size.height - emojiSize.height) * 0.5)), size: emojiSize) + if let emojiComponentView = emojiView.view { + if emojiComponentView.superview == nil { + self.addSubview(emojiComponentView) + } + emojiViewTransition.setFrame(view: emojiComponentView, frame: emojiFrame) + } + + self.pendingEmojiValues = nil + } else { + if let emojiView = self.emojiView { + self.emojiView = nil + emojiView.view?.removeFromSuperview() + } + + if self.pendingEmojiValues?.count != numEmoji { + var pendingEmojiValuesValue: [String] = [] + for _ in 0 ..< numEmoji - borderEmoji - 1 { + pendingEmojiValuesValue.append(randomCallsEmoji() ?? "👍") + } + for i in 0 ..< borderEmoji + 1 { + pendingEmojiValuesValue.append(pendingEmojiValuesValue[i]) + } + self.pendingEmojiValues = pendingEmojiValuesValue + } + } + + if let pendingEmojiValues, pendingEmojiValues.count == numEmoji { + let pendingContainerView: UIView + if let current = self.pendingContainerView { + pendingContainerView = current + } else { + pendingContainerView = UIView() + self.pendingContainerView = pendingContainerView + } + + for i in 0 ..< numEmoji { + let pendingEmojiView: ComponentView + if self.pendingEmojiViews.count > i { + pendingEmojiView = self.pendingEmojiViews[i] + } else { + pendingEmojiView = ComponentView() + self.pendingEmojiViews.append(pendingEmojiView) + } + let pendingEmojiViewSize = pendingEmojiView.update( + transition: .immediate, + component: AnyComponent(MultilineTextComponent( + text: .plain(NSAttributedString(string: pendingEmojiValues[i], font: Font.regular(40.0), textColor: .white)) + )), + environment: {}, + containerSize: CGSize(width: 200.0, height: 200.0) + ) + if let pendingEmojiComponentView = pendingEmojiView.view { + if pendingEmojiComponentView.superview == nil { + pendingContainerView.addSubview(pendingEmojiComponentView) + } + pendingEmojiComponentView.frame = CGRect(origin: CGPoint(x: 0.0, y: CGFloat(i) * size.height), size: pendingEmojiViewSize) + } + } + + pendingContainerView.frame = CGRect(origin: CGPoint(), size: size) + + if pendingContainerView.superview == nil { + self.addSubview(pendingContainerView) + + let animation = CABasicAnimation(keyPath: "sublayerTransform.translation.y") + //animation.duration = 4.2 + animation.duration = 0.2 + animation.fromValue = -CGFloat(numEmoji - borderEmoji) * size.height + animation.toValue = CGFloat(borderEmoji - 3) * size.height + animation.timingFunction = CAMediaTimingFunction(name: .linear) + animation.autoreverses = false + animation.repeatCount = .infinity + + pendingContainerView.layer.add(animation, forKey: "offsetCycle") + } + } else if let pendingContainerView = self.pendingContainerView { + self.pendingContainerView = nil + pendingContainerView.removeFromSuperview() + + for emojiView in self.pendingEmojiViews { + emojiView.view?.removeFromSuperview() + } + self.pendingEmojiViews.removeAll() + } + + //self.layer.borderColor = UIColor.red.cgColor + //self.layer.borderWidth = 4.0 + + return size + } + } + + func makeView() -> View { + return View(frame: CGRect()) + } + + func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { + return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) + } +} final class VideoChatEncryptionKeyComponent: Component { let theme: PresentationTheme @@ -119,7 +292,7 @@ final class VideoChatEncryptionKeyComponent: Component { let expandedButtonTopInset: CGFloat = 12.0 let expandedButtonBottomInset: CGFloat = 13.0 - let emojiItemSizes = (0 ..< component.emoji.count).map { i -> CGSize in + let emojiItemSizes = (0 ..< 4).map { i -> CGSize in let emojiItem: ComponentView if self.emojiItems.count > i { emojiItem = self.emojiItems[i] @@ -128,9 +301,9 @@ final class VideoChatEncryptionKeyComponent: Component { self.emojiItems.append(emojiItem) } return emojiItem.update( - transition: .immediate, - component: AnyComponent(MultilineTextComponent( - text: .plain(NSAttributedString(string: component.emoji[i], font: Font.regular(40.0), textColor: .white)) + transition: transition, + component: AnyComponent(EmojiItemComponent( + emoji: i < component.emoji.count ? component.emoji[i] : nil )), environment: {}, containerSize: CGSize(width: 200.0, height: 200.0) diff --git a/submodules/TelegramCallsUI/Sources/VideoChatListInviteComponent.swift b/submodules/TelegramCallsUI/Sources/VideoChatListInviteComponent.swift index 5e25476df2..8e089cec0f 100644 --- a/submodules/TelegramCallsUI/Sources/VideoChatListInviteComponent.swift +++ b/submodules/TelegramCallsUI/Sources/VideoChatListInviteComponent.swift @@ -7,16 +7,24 @@ import TelegramPresentationData import BundleIconComponent final class VideoChatListInviteComponent: Component { + enum Icon { + case addUser + case link + } + let title: String + let icon: Icon let theme: PresentationTheme let action: () -> Void init( title: String, + icon: Icon, theme: PresentationTheme, action: @escaping () -> Void ) { self.title = title + self.icon = icon self.theme = theme self.action = action } @@ -25,6 +33,9 @@ final class VideoChatListInviteComponent: Component { if lhs.title != rhs.title { return false } + if lhs.icon != rhs.icon { + return false + } if lhs.theme !== rhs.theme { return false } @@ -116,10 +127,17 @@ final class VideoChatListInviteComponent: Component { titleView.bounds = CGRect(origin: CGPoint(), size: titleFrame.size) } + let iconName: String + switch component.icon { + case .addUser: + iconName = "Chat/Context Menu/AddUser" + case .link: + iconName = "Chat/Context Menu/Link" + } let iconSize = self.icon.update( transition: .immediate, component: AnyComponent(BundleIconComponent( - name: "Chat/Context Menu/AddUser", + name: iconName, tintColor: component.theme.list.itemAccentColor )), environment: {}, diff --git a/submodules/TelegramCallsUI/Sources/VideoChatParticipantsComponent.swift b/submodules/TelegramCallsUI/Sources/VideoChatParticipantsComponent.swift index 9a1bd9f641..a8ce722e88 100644 --- a/submodules/TelegramCallsUI/Sources/VideoChatParticipantsComponent.swift +++ b/submodules/TelegramCallsUI/Sources/VideoChatParticipantsComponent.swift @@ -39,23 +39,33 @@ final class VideoChatParticipantsComponent: Component { } final class Participants: Equatable { - enum InviteType { - case invite + enum InviteType: Equatable { + case invite(isMultipleUsers: Bool) case shareLink } + struct InviteOption: Equatable { + let id: Int + let type: InviteType + + init(id: Int, type: InviteType) { + self.id = id + self.type = type + } + } + let myPeerId: EnginePeer.Id let participants: [GroupCallParticipantsContext.Participant] let totalCount: Int let loadMoreToken: String? - let inviteType: InviteType? + let inviteOptions: [InviteOption] - init(myPeerId: EnginePeer.Id, participants: [GroupCallParticipantsContext.Participant], totalCount: Int, loadMoreToken: String?, inviteType: InviteType?) { + init(myPeerId: EnginePeer.Id, participants: [GroupCallParticipantsContext.Participant], totalCount: Int, loadMoreToken: String?, inviteOptions: [InviteOption]) { self.myPeerId = myPeerId self.participants = participants self.totalCount = totalCount self.loadMoreToken = loadMoreToken - self.inviteType = inviteType + self.inviteOptions = inviteOptions } static func ==(lhs: Participants, rhs: Participants) -> Bool { @@ -74,7 +84,7 @@ final class VideoChatParticipantsComponent: Component { if lhs.loadMoreToken != rhs.loadMoreToken { return false } - if lhs.inviteType != rhs.inviteType { + if lhs.inviteOptions != rhs.inviteOptions { return false } return true @@ -142,7 +152,7 @@ final class VideoChatParticipantsComponent: Component { let updateMainParticipant: (VideoParticipantKey?, Bool?) -> Void let updateIsMainParticipantPinned: (Bool) -> Void let updateIsExpandedUIHidden: (Bool) -> Void - let openInviteMembers: () -> Void + let openInviteMembers: (Participants.InviteType) -> Void let visibleParticipantsUpdated: (Set) -> Void init( @@ -162,7 +172,7 @@ final class VideoChatParticipantsComponent: Component { updateMainParticipant: @escaping (VideoParticipantKey?, Bool?) -> Void, updateIsMainParticipantPinned: @escaping (Bool) -> Void, updateIsExpandedUIHidden: @escaping (Bool) -> Void, - openInviteMembers: @escaping () -> Void, + openInviteMembers: @escaping (Participants.InviteType) -> Void, visibleParticipantsUpdated: @escaping (Set) -> Void ) { self.call = call @@ -379,14 +389,14 @@ final class VideoChatParticipantsComponent: Component { let sideInset: CGFloat let itemCount: Int let itemHeight: CGFloat - let trailingItemHeight: CGFloat + let trailingItemHeights: [CGFloat] - init(containerSize: CGSize, sideInset: CGFloat, itemCount: Int, itemHeight: CGFloat, trailingItemHeight: CGFloat) { + init(containerSize: CGSize, sideInset: CGFloat, itemCount: Int, itemHeight: CGFloat, trailingItemHeights: [CGFloat]) { self.containerSize = containerSize self.sideInset = sideInset self.itemCount = itemCount self.itemHeight = itemHeight - self.trailingItemHeight = trailingItemHeight + self.trailingItemHeights = trailingItemHeights } func frame(at index: Int) -> CGRect { @@ -394,8 +404,15 @@ final class VideoChatParticipantsComponent: Component { return frame } - func trailingItemFrame() -> CGRect { - return CGRect(origin: CGPoint(x: self.sideInset, y: CGFloat(self.itemCount) * self.itemHeight), size: CGSize(width: self.containerSize.width - self.sideInset * 2.0, height: self.trailingItemHeight)) + func trailingItemFrame(index: Int) -> CGRect { + if index < 0 || index >= self.trailingItemHeights.count { + return CGRect() + } + var prefixHeight: CGFloat = 0.0 + for i in 0 ..< index { + prefixHeight += self.trailingItemHeights[i] + } + return CGRect(origin: CGPoint(x: self.sideInset, y: CGFloat(self.itemCount) * self.itemHeight + prefixHeight), size: CGSize(width: self.containerSize.width - self.sideInset * 2.0, height: self.trailingItemHeights[index])) } func contentHeight() -> CGFloat { @@ -403,7 +420,9 @@ final class VideoChatParticipantsComponent: Component { if self.itemCount != 0 { result = self.frame(at: self.itemCount - 1).maxY } - result += self.trailingItemHeight + for height in self.trailingItemHeights { + result += height + } return result } @@ -439,7 +458,7 @@ final class VideoChatParticipantsComponent: Component { let scrollClippingFrame: CGRect let separateVideoScrollClippingFrame: CGRect - init(containerSize: CGSize, layout: Layout, isUIHidden: Bool, expandedInsets: UIEdgeInsets, safeInsets: UIEdgeInsets, gridItemCount: Int, listItemCount: Int, listItemHeight: CGFloat, listTrailingItemHeight: CGFloat) { + init(containerSize: CGSize, layout: Layout, isUIHidden: Bool, expandedInsets: UIEdgeInsets, safeInsets: UIEdgeInsets, gridItemCount: Int, listItemCount: Int, listItemHeight: CGFloat, listTrailingItemHeights: [CGFloat]) { self.containerSize = containerSize self.layout = layout self.isUIHidden = isUIHidden @@ -465,7 +484,7 @@ final class VideoChatParticipantsComponent: Component { } self.grid = Grid(containerSize: CGSize(width: gridWidth, height: gridContainerHeight), sideInset: gridSideInset, itemCount: gridItemCount, isDedicatedColumn: layout.videoColumn != nil) - self.list = List(containerSize: CGSize(width: listWidth, height: containerSize.height), sideInset: layout.mainColumn.insets.left, itemCount: listItemCount, itemHeight: listItemHeight, trailingItemHeight: listTrailingItemHeight) + self.list = List(containerSize: CGSize(width: listWidth, height: containerSize.height), sideInset: layout.mainColumn.insets.left, itemCount: listItemCount, itemHeight: listItemHeight, trailingItemHeights: listTrailingItemHeights) self.spacing = 4.0 if let videoColumn = layout.videoColumn, !isUIHidden && !layout.isMainColumnHidden { @@ -568,8 +587,8 @@ final class VideoChatParticipantsComponent: Component { } } - func listTrailingItemFrame() -> CGRect { - return self.list.trailingItemFrame() + func listTrailingItemFrame(index: Int) -> CGRect { + return self.list.trailingItemFrame(index: index) } } @@ -641,7 +660,7 @@ final class VideoChatParticipantsComponent: Component { private var listParticipants: [GroupCallParticipantsContext.Participant] = [] private let measureListItemView = ComponentView() - private let inviteListItemView = ComponentView() + private var inviteListItemViews: [Int: ComponentView] = [:] private var gridItemViews: [VideoParticipantKey: GridItem] = [:] private let gridItemViewContainer: UIView @@ -1270,7 +1289,7 @@ final class VideoChatParticipantsComponent: Component { case .requesting: subtitle = PeerListItemComponent.Subtitle(text: "requesting...", color: .neutral) case .ringing: - subtitle = PeerListItemComponent.Subtitle(text: "ringing...", color: .neutral) + subtitle = PeerListItemComponent.Subtitle(text: "invited", color: .neutral) } peerItemComponent = PeerListItemComponent( @@ -1381,11 +1400,15 @@ final class VideoChatParticipantsComponent: Component { self.listItemViews.removeValue(forKey: itemId) } - do { + var trailingItemIndex = 0 + for inviteOption in component.participants?.inviteOptions ?? [] { + guard let itemView = self.inviteListItemViews[inviteOption.id] else { + continue + } var itemTransition = transition - let itemView = self.inviteListItemView - let itemFrame = itemLayout.listTrailingItemFrame() + let itemFrame = itemLayout.listTrailingItemFrame(index: trailingItemIndex) + trailingItemIndex += 1 if let itemComponentView = itemView.view { if itemComponentView.superview == nil { @@ -1395,6 +1418,17 @@ final class VideoChatParticipantsComponent: Component { itemTransition.setFrame(view: itemComponentView, frame: itemFrame) } } + var removeInviteListItemIds: [Int] = [] + for (id, itemView) in self.inviteListItemViews { + if let participants = component.participants, participants.inviteOptions.contains(where: { $0.id == id }) { + } else { + removeInviteListItemIds.append(id) + itemView.view?.removeFromSuperview() + } + } + for id in removeInviteListItemIds { + self.inviteListItemViews.removeValue(forKey: id) + } transition.setScale(view: self.gridItemViewContainer, scale: gridIsEmpty ? 0.001 : 1.0) transition.setPosition(view: self.gridItemViewContainer, position: CGPoint(x: itemLayout.gridItemContainerFrame().midX, y: itemLayout.gridItemContainerFrame().minY)) @@ -1748,32 +1782,51 @@ final class VideoChatParticipantsComponent: Component { containerSize: CGSize(width: availableSize.width, height: 1000.0) ) - let inviteText: String - if let participants = component.participants, let inviteType = participants.inviteType { - switch inviteType { - case .invite: - inviteText = component.strings.VoiceChat_InviteMember + var inviteListItemSizes: [CGSize] = [] + for (inviteOption) in component.participants?.inviteOptions ?? [] { + let inviteText: String + let iconType: VideoChatListInviteComponent.Icon + switch inviteOption.type { + case let .invite(isMultiple): + //TODO:localize + if isMultiple { + inviteText = component.strings.VoiceChat_InviteMember + } else { + inviteText = "Add Member" + } + iconType = .addUser case .shareLink: inviteText = component.strings.VoiceChat_Share + iconType = .link } - } else { - inviteText = component.strings.VoiceChat_InviteMember - } - let inviteListItemSize = self.inviteListItemView.update( - transition: transition, - component: AnyComponent(VideoChatListInviteComponent( - title: inviteText, - theme: component.theme, - action: { [weak self] in - guard let self, let component = self.component else { - return + + let inviteListItemView: ComponentView + var inviteListItemTransition = transition + if let current = self.inviteListItemViews[inviteOption.id] { + inviteListItemView = current + } else { + inviteListItemView = ComponentView() + self.inviteListItemViews[inviteOption.id] = inviteListItemView + inviteListItemTransition = inviteListItemTransition.withAnimation(.none) + } + + inviteListItemSizes.append(inviteListItemView.update( + transition: inviteListItemTransition, + component: AnyComponent(VideoChatListInviteComponent( + title: inviteText, + icon: iconType, + theme: component.theme, + action: { [weak self] in + guard let self, let component = self.component else { + return + } + component.openInviteMembers(inviteOption.type) } - component.openInviteMembers() - } - )), - environment: {}, - containerSize: CGSize(width: availableSize.width, height: 1000.0) - ) + )), + environment: {}, + containerSize: CGSize(width: availableSize.width, height: 1000.0) + )) + } var gridParticipants: [VideoParticipant] = [] var listParticipants: [GroupCallParticipantsContext.Participant] = [] @@ -1824,7 +1877,7 @@ final class VideoChatParticipantsComponent: Component { gridItemCount: gridParticipants.count, listItemCount: listParticipants.count + component.invitedPeers.count, listItemHeight: measureListItemSize.height, - listTrailingItemHeight: inviteListItemSize.height + listTrailingItemHeights: inviteListItemSizes.map(\.height) ) self.itemLayout = itemLayout diff --git a/submodules/TelegramCallsUI/Sources/VideoChatScreen.swift b/submodules/TelegramCallsUI/Sources/VideoChatScreen.swift index a28b80b040..cb5dd1da6d 100644 --- a/submodules/TelegramCallsUI/Sources/VideoChatScreen.swift +++ b/submodules/TelegramCallsUI/Sources/VideoChatScreen.swift @@ -1759,12 +1759,19 @@ final class VideoChatScreenComponent: Component { } } } - var inviteType: VideoChatParticipantsComponent.Participants.InviteType? - if canInvite { - if inviteIsLink { - inviteType = .shareLink - } else { - inviteType = .invite + var inviteOptions: [VideoChatParticipantsComponent.Participants.InviteOption] = [] + if case let .group(groupCall) = self.currentCall, groupCall.isConference { + inviteOptions.append(VideoChatParticipantsComponent.Participants.InviteOption(id: 0, type: .invite(isMultipleUsers: false))) + inviteOptions.append(VideoChatParticipantsComponent.Participants.InviteOption(id: 1, type: .shareLink)) + } else { + if canInvite { + let inviteType: VideoChatParticipantsComponent.Participants.InviteType + if inviteIsLink { + inviteType = .shareLink + } else { + inviteType = .invite(isMultipleUsers: false) + } + inviteOptions.append(VideoChatParticipantsComponent.Participants.InviteOption(id: 0, type: inviteType)) } } @@ -1773,7 +1780,7 @@ final class VideoChatScreenComponent: Component { participants: members.participants, totalCount: members.totalCount, loadMoreToken: members.loadMoreToken, - inviteType: inviteType + inviteOptions: inviteOptions ) } @@ -2038,7 +2045,13 @@ final class VideoChatScreenComponent: Component { } var encryptionKeyFrame: CGRect? - if let encryptionKeyEmoji = self.encryptionKeyEmoji { + var isConference = false + if case let .group(groupCall) = self.currentCall { + isConference = groupCall.isConference + } else if case .conferenceSource = self.currentCall { + isConference = true + } + if isConference { navigationHeight -= 2.0 let encryptionKey: ComponentView var encryptionKeyTransition = transition @@ -2055,7 +2068,7 @@ final class VideoChatScreenComponent: Component { component: AnyComponent(VideoChatEncryptionKeyComponent( theme: environment.theme, strings: environment.strings, - emoji: encryptionKeyEmoji, + emoji: self.encryptionKeyEmoji ?? [], isExpanded: self.isEncryptionKeyExpanded, tapAction: { [weak self] in guard let self else { @@ -2326,11 +2339,18 @@ final class VideoChatScreenComponent: Component { self.state?.updated(transition: .spring(duration: 0.4)) } }, - openInviteMembers: { [weak self] in + openInviteMembers: { [weak self] type in guard let self else { return } - self.openInviteMembers() + if case .shareLink = type { + guard let inviteLinks = self.inviteLinks else { + return + } + self.presentShare(inviteLinks) + } else { + self.openInviteMembers() + } }, visibleParticipantsUpdated: { [weak self] visibleParticipants in guard let self else { diff --git a/submodules/TelegramCallsUI/Sources/VideoChatScreenInviteMembers.swift b/submodules/TelegramCallsUI/Sources/VideoChatScreenInviteMembers.swift index ecffcf4552..259ec6a932 100644 --- a/submodules/TelegramCallsUI/Sources/VideoChatScreenInviteMembers.swift +++ b/submodules/TelegramCallsUI/Sources/VideoChatScreenInviteMembers.swift @@ -51,7 +51,15 @@ extension VideoChatScreenComponent.View { } } } - let controller = CallController.openConferenceAddParticipant(context: groupCall.accountContext, disablePeerIds: disablePeerIds, completion: { [weak self] peerIds in + let controller = CallController.openConferenceAddParticipant(context: groupCall.accountContext, disablePeerIds: disablePeerIds, shareLink: { [weak self] in + guard let self else { + return + } + guard let inviteLinks = self.inviteLinks else { + return + } + self.presentShare(inviteLinks) + }, completion: { [weak self] peerIds in guard let self, case let .group(groupCall) = self.currentCall else { return } @@ -80,7 +88,7 @@ extension VideoChatScreenComponent.View { if inviteIsLink { inviteType = .shareLink } else { - inviteType = .invite + inviteType = .invite(isMultipleUsers: true) } } diff --git a/submodules/TelegramCore/Sources/State/CallSessionManager.swift b/submodules/TelegramCore/Sources/State/CallSessionManager.swift index 910244cf49..f418269540 100644 --- a/submodules/TelegramCore/Sources/State/CallSessionManager.swift +++ b/submodules/TelegramCore/Sources/State/CallSessionManager.swift @@ -718,6 +718,23 @@ private final class CallSessionManagerContext { } } + func dropOutgoingConferenceRequest(messageId: MessageId) { + let addUpdates = self.addUpdates + let rejectSignal = self.network.request(Api.functions.phone.declineConferenceCallInvite(msgId: messageId.id)) + |> map(Optional.init) + |> `catch` { _ -> Signal in + return .single(nil) + } + |> mapToSignal { updates -> Signal in + if let updates { + addUpdates(updates) + } + return .complete() + } + + self.rejectConferenceInvitationDisposables.add(rejectSignal.startStrict()) + } + func drop(internalId: CallSessionInternalId, reason: DropCallReason, debugLog: Signal) { for (id, context) in self.incomingConferenceInvitationContexts { if context.internalId == internalId { @@ -1383,6 +1400,12 @@ public final class CallSessionManager { } } + public func dropOutgoingConferenceRequest(messageId: MessageId) { + self.withContext { context in + context.dropOutgoingConferenceRequest(messageId: messageId) + } + } + func drop(stableId: CallSessionStableId, reason: DropCallReason) { self.withContext { context in context.drop(stableId: stableId, reason: reason) diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Calls/GroupCalls.swift b/submodules/TelegramCore/Sources/TelegramEngine/Calls/GroupCalls.swift index 51247eb89d..650be6ac5c 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Calls/GroupCalls.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Calls/GroupCalls.swift @@ -831,11 +831,11 @@ func _internal_joinGroupCall(account: Account, peerId: PeerId?, joinAs: PeerId?, } } -func _internal_inviteConferenceCallParticipant(account: Account, callId: Int64, accessHash: Int64, peerId: EnginePeer.Id, isVideo: Bool) -> Signal { +func _internal_inviteConferenceCallParticipant(account: Account, reference: InternalGroupCallReference, peerId: EnginePeer.Id, isVideo: Bool) -> Signal { return account.postbox.transaction { transaction -> Api.InputUser? in return transaction.getPeer(peerId).flatMap(apiInputUser) } - |> mapToSignal { inputPeer -> Signal in + |> mapToSignal { inputPeer -> Signal in guard let inputPeer else { return .complete() } @@ -844,16 +844,19 @@ func _internal_inviteConferenceCallParticipant(account: Account, callId: Int64, if isVideo { flags |= 1 << 0 } - return account.network.request(Api.functions.phone.inviteConferenceCallParticipant(flags: flags, call: .inputGroupCall(id: callId, accessHash: accessHash), userId: inputPeer)) + return account.network.request(Api.functions.phone.inviteConferenceCallParticipant(flags: flags, call: reference.apiInputGroupCall, userId: inputPeer)) |> map(Optional.init) |> `catch` { _ -> Signal in return .single(nil) } - |> mapToSignal { result -> Signal in + |> mapToSignal { result -> Signal in if let result { account.stateManager.addUpdates(result) + if let message = result.messageIds.first { + return .single(message) + } } - return .complete() + return .single(nil) } } } diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Calls/TelegramEngineCalls.swift b/submodules/TelegramCore/Sources/TelegramEngine/Calls/TelegramEngineCalls.swift index c4d2655082..f179efbca8 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Calls/TelegramEngineCalls.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Calls/TelegramEngineCalls.swift @@ -105,8 +105,8 @@ public extension TelegramEngine { return _internal_sendConferenceCallBroadcast(account: self.account, callId: callId, accessHash: accessHash, block: block) } - public func inviteConferenceCallParticipant(callId: Int64, accessHash: Int64, peerId: EnginePeer.Id, isVideo: Bool) -> Signal { - return _internal_inviteConferenceCallParticipant(account: self.account, callId: callId, accessHash: accessHash, peerId: peerId, isVideo: isVideo) + public func inviteConferenceCallParticipant(reference: InternalGroupCallReference, peerId: EnginePeer.Id, isVideo: Bool) -> Signal { + return _internal_inviteConferenceCallParticipant(account: self.account, reference: reference, peerId: peerId, isVideo: isVideo) } public func removeGroupCallBlockchainParticipants(callId: Int64, accessHash: Int64, participantIds: [Int64], block: Data) -> Signal { diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageCallBubbleContentNode/BUILD b/submodules/TelegramUI/Components/Chat/ChatMessageCallBubbleContentNode/BUILD index 17460293d1..77e9f52615 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessageCallBubbleContentNode/BUILD +++ b/submodules/TelegramUI/Components/Chat/ChatMessageCallBubbleContentNode/BUILD @@ -10,6 +10,7 @@ swift_library( "-warnings-as-errors", ], deps = [ + "//submodules/SSignalKit/SwiftSignalKit", "//submodules/AsyncDisplayKit", "//submodules/Display", "//submodules/TelegramCore", diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageCallBubbleContentNode/Sources/ChatMessageCallBubbleContentNode.swift b/submodules/TelegramUI/Components/Chat/ChatMessageCallBubbleContentNode/Sources/ChatMessageCallBubbleContentNode.swift index a7e033ea74..75987f4d84 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessageCallBubbleContentNode/Sources/ChatMessageCallBubbleContentNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessageCallBubbleContentNode/Sources/ChatMessageCallBubbleContentNode.swift @@ -9,6 +9,7 @@ import AppBundle import ChatMessageBubbleContentNode import ChatMessageItemCommon import ChatMessageDateAndStatusNode +import SwiftSignalKit private let titleFont: UIFont = Font.medium(16.0) private let labelFont: UIFont = Font.regular(13.0) @@ -25,6 +26,8 @@ public class ChatMessageCallBubbleContentNode: ChatMessageBubbleContentNode { private let iconNode: ASImageNode private let buttonNode: HighlightableButtonNode + private var activeConferenceUpdateTimer: SwiftSignalKit.Timer? + required public init() { self.titleNode = TextNode() self.labelNode = TextNode() @@ -57,6 +60,10 @@ public class ChatMessageCallBubbleContentNode: ChatMessageBubbleContentNode { self.buttonNode.addTarget(self, action: #selector(self.callButtonPressed), forControlEvents: .touchUpInside) } + deinit { + self.activeConferenceUpdateTimer?.invalidate() + } + override public func accessibilityActivate() -> Bool { self.callButtonPressed() return true @@ -90,6 +97,8 @@ public class ChatMessageCallBubbleContentNode: ChatMessageBubbleContentNode { var callDuration: Int32? var callSuccessful = true var isVideo = false + var hasCallButton = true + var updateConferenceTimerEndTimeout: Int32? for media in item.message.media { if let action = media as? TelegramMediaAction, case let .phoneCall(_, discardReason, duration, isVideoValue) = action.action { isVideo = isVideoValue @@ -124,10 +133,31 @@ public class ChatMessageCallBubbleContentNode: ChatMessageBubbleContentNode { } break } else if let action = media as? TelegramMediaAction, case let .conferenceCall(conferenceCall) = action.action { - isVideo = false + isVideo = conferenceCall.flags.contains(.isVideo) callDuration = conferenceCall.duration //TODO:localize - titleString = "Group Call" + let missedTimeout: Int32 + #if DEBUG + missedTimeout = 5 + #else + missedTimeout = 30 + #endif + let currentTime = Int32(Date().timeIntervalSince1970) + if conferenceCall.flags.contains(.isMissed) { + titleString = "Declined Group Call" + } else if item.message.timestamp < currentTime - missedTimeout { + titleString = "Missed Group Call" + } else if conferenceCall.duration != nil { + titleString = "Cancelled Group Call" + hasCallButton = true + } else { + if incoming { + titleString = "Incoming Group Call" + } else { + titleString = "Outgoing Group Call" + } + updateConferenceTimerEndTimeout = (item.message.timestamp + missedTimeout) - currentTime + } break } } @@ -211,7 +241,9 @@ public class ChatMessageCallBubbleContentNode: ChatMessageBubbleContentNode { boundingSize.width += layoutConstants.text.bubbleInsets.left + layoutConstants.text.bubbleInsets.right boundingSize.height += layoutConstants.text.bubbleInsets.top + layoutConstants.text.bubbleInsets.bottom - boundingSize.width += 54.0 + if hasCallButton { + boundingSize.width += 54.0 + } return (boundingSize.width, { boundingWidth in return (boundingSize, { [weak self] animation, _, _ in @@ -234,6 +266,22 @@ public class ChatMessageCallBubbleContentNode: ChatMessageBubbleContentNode { if let buttonImage = buttonImage { strongSelf.buttonNode.setImage(buttonImage, for: []) strongSelf.buttonNode.frame = CGRect(origin: CGPoint(x: boundingWidth - buttonImage.size.width - 8.0, y: 15.0), size: buttonImage.size) + strongSelf.buttonNode.isHidden = !hasCallButton + } + + if let activeConferenceUpdateTimer = strongSelf.activeConferenceUpdateTimer { + activeConferenceUpdateTimer.invalidate() + strongSelf.activeConferenceUpdateTimer = nil + } + if let updateConferenceTimerEndTimeout, updateConferenceTimerEndTimeout >= 0 { + strongSelf.activeConferenceUpdateTimer?.invalidate() + strongSelf.activeConferenceUpdateTimer = SwiftSignalKit.Timer(timeout: Double(updateConferenceTimerEndTimeout) + 0.5, repeat: false, completion: { [weak strongSelf] in + guard let strongSelf else { + return + } + strongSelf.requestInlineUpdate?() + }, queue: .mainQueue()) + strongSelf.activeConferenceUpdateTimer?.start() } } }) @@ -270,6 +318,10 @@ public class ChatMessageCallBubbleContentNode: ChatMessageBubbleContentNode { } override public func tapActionAtPoint(_ point: CGPoint, gesture: TapLongTapOrDoubleTapGesture, isEstimating: Bool) -> ChatMessageBubbleContentTapAction { + if self.buttonNode.isHidden { + return ChatMessageBubbleContentTapAction(content: .none) + } + if self.buttonNode.frame.contains(point) { return ChatMessageBubbleContentTapAction(content: .ignore) } else if self.bounds.contains(point), let item = self.item { diff --git a/submodules/TelegramUI/Components/JoinSubjectScreen/Sources/JoinSubjectScreen.swift b/submodules/TelegramUI/Components/JoinSubjectScreen/Sources/JoinSubjectScreen.swift index 3a4dcdb3b6..467fe9e858 100644 --- a/submodules/TelegramUI/Components/JoinSubjectScreen/Sources/JoinSubjectScreen.swift +++ b/submodules/TelegramUI/Components/JoinSubjectScreen/Sources/JoinSubjectScreen.swift @@ -405,7 +405,7 @@ private final class JoinSubjectScreenComponent: Component { isStream: false ), reference: .link(slug: groupCall.slug), - mode: .joining + beginWithVideo: false ) self.environment?.controller()?.dismiss() diff --git a/submodules/TelegramUI/Sources/ChatController.swift b/submodules/TelegramUI/Sources/ChatController.swift index cb03079237..ffde3d3294 100644 --- a/submodules/TelegramUI/Sources/ChatController.swift +++ b/submodules/TelegramUI/Sources/ChatController.swift @@ -2908,7 +2908,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G isStream: false ), reference: .message(id: message.id), - mode: .joining + beginWithVideo: conferenceCall.flags.contains(.isVideo) ) }) }, longTap: { [weak self] action, params in diff --git a/submodules/TelegramUI/Sources/ContactSelectionController.swift b/submodules/TelegramUI/Sources/ContactSelectionController.swift index 31e7b99618..0cb897a7d2 100644 --- a/submodules/TelegramUI/Sources/ContactSelectionController.swift +++ b/submodules/TelegramUI/Sources/ContactSelectionController.swift @@ -60,6 +60,7 @@ class ContactSelectionControllerImpl: ViewController, ContactSelectionController } private let confirmation: (ContactListPeer) -> Signal + private let isPeerEnabled: (ContactListPeer) -> Bool var dismissed: (() -> Void)? var presentScheduleTimePicker: (@escaping (Int32) -> Void) -> Void = { _ in } @@ -107,6 +108,7 @@ class ContactSelectionControllerImpl: ViewController, ContactSelectionController self.displayDeviceContacts = params.displayDeviceContacts self.displayCallIcons = params.displayCallIcons self.confirmation = params.confirmation + self.isPeerEnabled = params.isPeerEnabled self.multipleSelection = params.multipleSelection self.requirePhoneNumbers = params.requirePhoneNumbers self.allowChannelsInSearch = params.allowChannelsInSearch @@ -218,7 +220,7 @@ class ContactSelectionControllerImpl: ViewController, ContactSelectionController } override func loadDisplayNode() { - self.displayNode = ContactSelectionControllerNode(context: self.context, mode: self.mode, presentationData: self.presentationData, options: self.options, displayDeviceContacts: self.displayDeviceContacts, displayCallIcons: self.displayCallIcons, multipleSelection: self.multipleSelection, requirePhoneNumbers: self.requirePhoneNumbers, allowChannelsInSearch: self.allowChannelsInSearch) + self.displayNode = ContactSelectionControllerNode(context: self.context, mode: self.mode, presentationData: self.presentationData, options: self.options, displayDeviceContacts: self.displayDeviceContacts, displayCallIcons: self.displayCallIcons, multipleSelection: self.multipleSelection, requirePhoneNumbers: self.requirePhoneNumbers, allowChannelsInSearch: self.allowChannelsInSearch, isPeerEnabled: self.isPeerEnabled) self._ready.set(self.contactsNode.contactListNode.ready) self.contactsNode.navigationBar = self.navigationBar diff --git a/submodules/TelegramUI/Sources/ContactSelectionControllerNode.swift b/submodules/TelegramUI/Sources/ContactSelectionControllerNode.swift index ebbfd6d3f3..58abf88ff3 100644 --- a/submodules/TelegramUI/Sources/ContactSelectionControllerNode.swift +++ b/submodules/TelegramUI/Sources/ContactSelectionControllerNode.swift @@ -44,6 +44,8 @@ final class ContactSelectionControllerNode: ASDisplayNode { var cancelSearch: (() -> Void)? var openPeerMore: ((ContactListPeer, ASDisplayNode?, ContextGesture?) -> Void)? + let isPeerEnabled: (ContactListPeer) -> Bool + var presentationData: PresentationData { didSet { self.presentationDataPromise.set(.single(self.presentationData)) @@ -57,12 +59,13 @@ final class ContactSelectionControllerNode: ASDisplayNode { var searchContainerNode: ContactsSearchContainerNode? - init(context: AccountContext, mode: ContactSelectionControllerMode, presentationData: PresentationData, options: Signal<[ContactListAdditionalOption], NoError>, displayDeviceContacts: Bool, displayCallIcons: Bool, multipleSelection: Bool, requirePhoneNumbers: Bool, allowChannelsInSearch: Bool) { + init(context: AccountContext, mode: ContactSelectionControllerMode, presentationData: PresentationData, options: Signal<[ContactListAdditionalOption], NoError>, displayDeviceContacts: Bool, displayCallIcons: Bool, multipleSelection: Bool, requirePhoneNumbers: Bool, allowChannelsInSearch: Bool, isPeerEnabled: @escaping (ContactListPeer) -> Bool) { self.context = context self.presentationData = presentationData self.displayDeviceContacts = displayDeviceContacts self.displayCallIcons = displayCallIcons self.allowChannelsInSearch = allowChannelsInSearch + self.isPeerEnabled = isPeerEnabled var excludeSelf = true @@ -124,7 +127,9 @@ final class ContactSelectionControllerNode: ASDisplayNode { } var contextActionImpl: ((EnginePeer, ASDisplayNode, ContextGesture?, CGPoint?) -> Void)? - self.contactListNode = ContactListNode(context: context, updatedPresentationData: (presentationData, self.presentationDataPromise.get()), presentation: presentation, filters: filters, onlyWriteable: false, isGroupInvitation: false, displayCallIcons: displayCallIcons, contextAction: multipleSelection ? { peer, node, gesture, _, _ in + self.contactListNode = ContactListNode(context: context, updatedPresentationData: (presentationData, self.presentationDataPromise.get()), presentation: presentation, filters: filters, onlyWriteable: false, isGroupInvitation: false, isPeerEnabled: { peer in + return isPeerEnabled(.peer(peer: peer._asPeer(), isGlobal: false, participantCount: nil)) + }, displayCallIcons: displayCallIcons, contextAction: multipleSelection ? { peer, node, gesture, _, _ in contextActionImpl?(peer, node, gesture, nil) } : nil, multipleSelection: multipleSelection)