From 7c0d5519bf4f00c7da136280b49126da9a5f7810 Mon Sep 17 00:00:00 2001 From: Isaac <> Date: Thu, 28 Dec 2023 16:55:12 +0400 Subject: [PATCH] Various improvements --- .../AvatarNode/Sources/AvatarNode.swift | 7 +- .../Sources/ChatListSearchListPaneNode.swift | 14 +- submodules/Display/Source/DeviceMetrics.swift | 30 ++- .../InstantPageSlideshowItemNode.swift | 2 +- .../Sources/CallController.swift | 2 +- .../Sources/CallControllerNodeV2.swift | 115 ++++++--- .../Sources/CallStatusBarNode.swift | 29 ++- submodules/TelegramUI/BUILD | 1 + .../Sources/Components/ButtonGroupView.swift | 37 ++- .../Sources/PrivateCallScreen.swift | 47 +++- .../Chat/ChatMessageNotificationItem/BUILD | 38 +++ .../Sources/ChatCallNotificationItem.swift | 233 ++++++++++++++++++ .../Sources/ChatMessageNotificationItem.swift | 18 +- .../Sources/NotificationItem.swift | 4 +- .../Contents.json | 12 + .../large_hidden 1.pdf | 134 ++++++++++ .../Contents.json | 12 + .../phone.fill.svg | 3 + .../Contents.json | 12 + .../phone.down.fill.svg | 3 + .../TelegramUI/Sources/AppDelegate.swift | 3 +- .../Sources/ApplicationContext.swift | 1 + .../Sources/MakeTempAccountContext.swift | 1 + .../Sources/NavigateToChatController.swift | 1 + .../NotificationContainerController.swift | 5 + .../NotificationContainerControllerNode.swift | 83 ++++++- .../Sources/NotificationContentContext.swift | 2 +- .../NotificationItemContainerNode.swift | 25 +- .../Sources/SharedAccountContext.swift | 80 +++++- .../Sources/GroupCallContext.swift | 11 + .../Sources/OngoingCallContext.swift | 13 +- .../OngoingCallThreadLocalContext.h | 16 +- .../Sources/OngoingCallThreadLocalContext.mm | 48 +++- 33 files changed, 933 insertions(+), 109 deletions(-) create mode 100644 submodules/TelegramUI/Components/Chat/ChatMessageNotificationItem/BUILD create mode 100644 submodules/TelegramUI/Components/Chat/ChatMessageNotificationItem/Sources/ChatCallNotificationItem.swift rename submodules/TelegramUI/{ => Components/Chat/ChatMessageNotificationItem}/Sources/ChatMessageNotificationItem.swift (97%) rename submodules/TelegramUI/{ => Components/Chat/ChatMessageNotificationItem}/Sources/NotificationItem.swift (75%) create mode 100644 submodules/TelegramUI/Images.xcassets/Avatar/AnonymousSenderIcon.imageset/Contents.json create mode 100644 submodules/TelegramUI/Images.xcassets/Avatar/AnonymousSenderIcon.imageset/large_hidden 1.pdf create mode 100644 submodules/TelegramUI/Images.xcassets/Call/CallNotificationAnswerIcon.imageset/Contents.json create mode 100644 submodules/TelegramUI/Images.xcassets/Call/CallNotificationAnswerIcon.imageset/phone.fill.svg create mode 100644 submodules/TelegramUI/Images.xcassets/Call/CallNotificationDeclineIcon.imageset/Contents.json create mode 100644 submodules/TelegramUI/Images.xcassets/Call/CallNotificationDeclineIcon.imageset/phone.down.fill.svg diff --git a/submodules/AvatarNode/Sources/AvatarNode.swift b/submodules/AvatarNode/Sources/AvatarNode.swift index 2ae27d6417..1f2b11790c 100644 --- a/submodules/AvatarNode/Sources/AvatarNode.swift +++ b/submodules/AvatarNode/Sources/AvatarNode.swift @@ -21,6 +21,7 @@ public let savedMessagesIcon = generateTintedImage(image: UIImage(bundleImageNam public let repostStoryIcon = generateTintedImage(image: UIImage(bundleImageName: "Avatar/RepostStoryIcon"), color: .white) private let archivedChatsIcon = UIImage(bundleImageName: "Avatar/ArchiveAvatarIcon")?.precomposed() private let repliesIcon = generateTintedImage(image: UIImage(bundleImageName: "Avatar/RepliesMessagesIcon"), color: .white) +private let anonymousSavedMessagesIcon = generateTintedImage(image: UIImage(bundleImageName: "Avatar/AnonymousSenderIcon"), color: .white) public func avatarPlaceholderFont(size: CGFloat) -> UIFont { return Font.with(size: size, design: .round, weight: .bold) @@ -660,7 +661,7 @@ public final class AvatarNode: ASDisplayNode { icon = .repliesIcon case .anonymousSavedMessagesIcon: representation = nil - icon = .repliesIcon + icon = .anonymousSavedMessagesIcon case let .archivedChatsIcon(hiddenByDefault): representation = nil icon = .archivedChatsIcon(hiddenByDefault: hiddenByDefault) @@ -897,8 +898,8 @@ public final class AvatarNode: ASDisplayNode { context.scaleBy(x: factor, y: -factor) context.translateBy(x: -bounds.size.width / 2.0, y: -bounds.size.height / 2.0) - if let repliesIcon = repliesIcon { - context.draw(repliesIcon.cgImage!, in: CGRect(origin: CGPoint(x: floor((bounds.size.width - repliesIcon.size.width) / 2.0), y: floor((bounds.size.height - repliesIcon.size.height) / 2.0)), size: repliesIcon.size)) + if let anonymousSavedMessagesIcon = anonymousSavedMessagesIcon { + context.draw(anonymousSavedMessagesIcon.cgImage!, in: CGRect(origin: CGPoint(x: floor((bounds.size.width - anonymousSavedMessagesIcon.size.width) / 2.0), y: floor((bounds.size.height - anonymousSavedMessagesIcon.size.height) / 2.0)), size: anonymousSavedMessagesIcon.size)) } } else if case .editAvatarIcon = parameters.icon, let theme = parameters.theme, !parameters.hasImage { context.translateBy(x: bounds.size.width / 2.0, y: bounds.size.height / 2.0) diff --git a/submodules/ChatListUI/Sources/ChatListSearchListPaneNode.swift b/submodules/ChatListUI/Sources/ChatListSearchListPaneNode.swift index 06ed143139..0f3b4259f5 100644 --- a/submodules/ChatListUI/Sources/ChatListSearchListPaneNode.swift +++ b/submodules/ChatListUI/Sources/ChatListSearchListPaneNode.swift @@ -2205,7 +2205,12 @@ final class ChatListSearchListPaneNode: ASDisplayNode, ChatListSearchPaneNode { }, peerSelected: { [weak self] peer, chatPeer, threadId, _ in interaction.dismissInput() interaction.openPeer(peer, chatPeer, threadId, false) - let _ = context.engine.peers.addRecentlySearchedPeer(peerId: peer.id).startStandalone() + switch location { + case .chatList, .forum: + let _ = context.engine.peers.addRecentlySearchedPeer(peerId: peer.id).startStandalone() + case .savedMessagesChats: + break + } self?.listNode.clearHighlightAnimated(true) }, disabledPeerSelected: { _, _ in }, togglePeerSelected: { _, _ in @@ -2670,7 +2675,12 @@ final class ChatListSearchListPaneNode: ASDisplayNode, ChatListSearchPaneNode { let transition = chatListSearchContainerPreparedRecentTransition(from: previousEntries ?? [], to: entries, context: context, presentationData: presentationData, filter: peersFilter, peerSelected: { peer, threadId in interaction.openPeer(peer, nil, threadId, true) if threadId == nil { - let _ = context.engine.peers.addRecentlySearchedPeer(peerId: peer.id).startStandalone() + switch location { + case .chatList, .forum: + let _ = context.engine.peers.addRecentlySearchedPeer(peerId: peer.id).startStandalone() + case .savedMessagesChats: + break + } } self?.recentListNode.clearHighlightAnimated(true) }, disabledPeerSelected: { peer, threadId in diff --git a/submodules/Display/Source/DeviceMetrics.swift b/submodules/Display/Source/DeviceMetrics.swift index 32b74325ee..b01c2a487d 100644 --- a/submodules/Display/Source/DeviceMetrics.swift +++ b/submodules/Display/Source/DeviceMetrics.swift @@ -44,7 +44,7 @@ public enum DeviceMetrics: CaseIterable, Equatable { case iPadPro case iPadPro3rdGen case iPadMini6thGen - case unknown(screenSize: CGSize, statusBarHeight: CGFloat, onScreenNavigationHeight: CGFloat?) + case unknown(screenSize: CGSize, statusBarHeight: CGFloat, onScreenNavigationHeight: CGFloat?, screenCornerRadius: CGFloat) public static let performance = Performance() @@ -111,14 +111,24 @@ public enum DeviceMetrics: CaseIterable, Equatable { return } } - self = .unknown(screenSize: screenSize, statusBarHeight: statusBarHeight, onScreenNavigationHeight: onScreenNavigationHeight) + + let screenCornerRadius: CGFloat + if screenSize.width >= 1024.0 || screenSize.height >= 1024.0 { + screenCornerRadius = 0.0 + } else if onScreenNavigationHeight != nil { + screenCornerRadius = 39.0 + } else { + screenCornerRadius = 0.0 + } + + self = .unknown(screenSize: screenSize, statusBarHeight: statusBarHeight, onScreenNavigationHeight: onScreenNavigationHeight, screenCornerRadius: screenCornerRadius) } public var type: DeviceType { switch self { case .iPad, .iPad102Inch, .iPadPro10Inch, .iPadPro11Inch, .iPadPro, .iPadPro3rdGen: return .tablet - case let .unknown(screenSize, _, _) where screenSize.width >= 744.0 && screenSize.height >= 1024.0: + case let .unknown(screenSize, _, _, _) where screenSize.width >= 744.0 && screenSize.height >= 1024.0: return .tablet default: return .phone @@ -175,7 +185,7 @@ public enum DeviceMetrics: CaseIterable, Equatable { return CGSize(width: 1024.0, height: 1366.0) case .iPadMini6thGen: return CGSize(width: 744.0, height: 1133.0) - case let .unknown(screenSize, _, _): + case let .unknown(screenSize, _, _, _): return screenSize } } @@ -194,12 +204,8 @@ public enum DeviceMetrics: CaseIterable, Equatable { return 53.0 + UIScreenPixel case .iPhone14Pro, .iPhone14ProMax: return 55.0 - case let .unknown(_, _, onScreenNavigationHeight): - if let _ = onScreenNavigationHeight { - return 39.0 - } else { - return 0.0 - } + case let .unknown(_, _, _, screenCornerRadius): + return screenCornerRadius default: return 0.0 } @@ -230,7 +236,7 @@ public enum DeviceMetrics: CaseIterable, Equatable { } else { return nil } - case let .unknown(_, _, onScreenNavigationHeight): + case let .unknown(_, _, onScreenNavigationHeight, _): return onScreenNavigationHeight default: return nil @@ -260,7 +266,7 @@ public enum DeviceMetrics: CaseIterable, Equatable { return 44.0 case .iPadPro11Inch, .iPadPro3rdGen, .iPadMini, .iPadMini6thGen: return 24.0 - case let .unknown(_, statusBarHeight, _): + case let .unknown(_, statusBarHeight, _, _): return statusBarHeight default: return 20.0 diff --git a/submodules/InstantPageUI/Sources/InstantPageSlideshowItemNode.swift b/submodules/InstantPageUI/Sources/InstantPageSlideshowItemNode.swift index 335f060cc2..b584d5b1b6 100644 --- a/submodules/InstantPageUI/Sources/InstantPageSlideshowItemNode.swift +++ b/submodules/InstantPageUI/Sources/InstantPageSlideshowItemNode.swift @@ -415,7 +415,7 @@ final class InstantPageSlideshowNode: ASDisplayNode, InstantPageNode { super.layout() self.pagerNode.frame = self.bounds - self.pagerNode.containerLayoutUpdated(ContainerViewLayout(size: self.bounds.size, metrics: LayoutMetrics(), deviceMetrics: .unknown(screenSize: CGSize(), statusBarHeight: 0.0, onScreenNavigationHeight: nil), intrinsicInsets: UIEdgeInsets(), safeInsets: UIEdgeInsets(), additionalInsets: UIEdgeInsets(), statusBarHeight: nil, inputHeight: nil, inputHeightIsInteractivellyChanging: false, inVoiceOver: false), transition: .immediate) + self.pagerNode.containerLayoutUpdated(ContainerViewLayout(size: self.bounds.size, metrics: LayoutMetrics(), deviceMetrics: .unknown(screenSize: CGSize(), statusBarHeight: 0.0, onScreenNavigationHeight: nil, screenCornerRadius: 0.0), intrinsicInsets: UIEdgeInsets(), safeInsets: UIEdgeInsets(), additionalInsets: UIEdgeInsets(), statusBarHeight: nil, inputHeight: nil, inputHeightIsInteractivellyChanging: false, inVoiceOver: false), transition: .immediate) self.pageControlNode.layer.transform = CATransform3DIdentity self.pageControlNode.frame = CGRect(origin: CGPoint(x: 0.0, y: self.bounds.size.height - 20.0), size: CGSize(width: self.bounds.size.width, height: 20.0)) diff --git a/submodules/TelegramCallsUI/Sources/CallController.swift b/submodules/TelegramCallsUI/Sources/CallController.swift index b13783c8a2..0128b35a16 100644 --- a/submodules/TelegramCallsUI/Sources/CallController.swift +++ b/submodules/TelegramCallsUI/Sources/CallController.swift @@ -148,7 +148,7 @@ public final class CallController: ViewController { } override public func loadDisplayNode() { - var useV2 = self.call.context.sharedContext.immediateExperimentalUISettings.callV2 + var useV2 = true if let data = self.call.context.currentAppConfiguration.with({ $0 }).data, let _ = data["ios_killswitch_disable_callui_v2"] { useV2 = false } diff --git a/submodules/TelegramCallsUI/Sources/CallControllerNodeV2.swift b/submodules/TelegramCallsUI/Sources/CallControllerNodeV2.swift index 89dd7adb28..812b95aee7 100644 --- a/submodules/TelegramCallsUI/Sources/CallControllerNodeV2.swift +++ b/submodules/TelegramCallsUI/Sources/CallControllerNodeV2.swift @@ -42,6 +42,8 @@ final class CallControllerNodeV2: ViewControllerTracingNode, CallControllerNodeP private var didInitializeIsReady: Bool = false private var callStartTimestamp: Double? + private var smoothSignalQuality: Double? + private var smoothSignalQualityTarget: Double? private var callState: PresentationCallState? var isMuted: Bool = false @@ -77,6 +79,8 @@ final class CallControllerNodeV2: ViewControllerTracingNode, CallControllerNodeP private var panGestureState: PanGestureState? private var notifyDismissedInteractivelyOnPanGestureApply: Bool = false + private var signalQualityTimer: Foundation.Timer? + init( sharedContext: SharedAccountContext, account: Account, @@ -190,6 +194,22 @@ final class CallControllerNodeV2: ViewControllerTracingNode, CallControllerNodeP }) self.view.addGestureRecognizer(UIPanGestureRecognizer(target: self, action: #selector(self.panGesture(_:)))) + + self.signalQualityTimer = Foundation.Timer.scheduledTimer(withTimeInterval: 1.0, repeats: true, block: { [weak self] _ in + guard let self else { + return + } + if let smoothSignalQuality = self.smoothSignalQuality, let smoothSignalQualityTarget = self.smoothSignalQualityTarget { + let updatedSmoothSignalQuality = (smoothSignalQuality + smoothSignalQualityTarget) * 0.5 + if abs(updatedSmoothSignalQuality - smoothSignalQuality) > 0.001 { + self.smoothSignalQuality = updatedSmoothSignalQuality + + if let callState = self.callState { + self.updateCallState(callState) + } + } + } + }) } deinit { @@ -197,6 +217,7 @@ final class CallControllerNodeV2: ViewControllerTracingNode, CallControllerNodeP self.isMicrophoneMutedDisposable?.dispose() self.audioLevelDisposable?.dispose() self.audioOutputCheckTimer?.invalidate() + self.signalQualityTimer?.invalidate() } func updateAudioOutputs(availableOutputs: [AudioSessionOutput], currentOutput: AudioSessionOutput?) { @@ -211,8 +232,24 @@ final class CallControllerNodeV2: ViewControllerTracingNode, CallControllerNodeP mappedOutput = .internalSpeaker case .speaker: mappedOutput = .speaker - case .headphones, .port: - mappedOutput = .speaker + case .headphones: + mappedOutput = .headphones + case let .port(port): + switch port.type { + case .wired: + mappedOutput = .headphones + default: + let portName = port.name.lowercased() + if portName.contains("airpods pro") { + mappedOutput = .airpodsPro + } else if portName.contains("airpods max") { + mappedOutput = .airpodsMax + } else if portName.contains("airpods") { + mappedOutput = .airpods + } else { + mappedOutput = .bluetooth + } + } } } else { mappedOutput = .internalSpeaker @@ -342,10 +379,16 @@ final class CallControllerNodeV2: ViewControllerTracingNode, CallControllerNodeP case .connecting: mappedLifecycleState = .connecting case let .active(startTime, signalQuality, keyData): - self.callStartTimestamp = startTime + var signalQuality = signalQuality.flatMap(Int.init) + self.smoothSignalQualityTarget = Double(signalQuality ?? 4) - var signalQuality = signalQuality - signalQuality = 4 + if let smoothSignalQuality = self.smoothSignalQuality { + signalQuality = Int(round(smoothSignalQuality)) + } else { + signalQuality = 4 + } + + self.callStartTimestamp = startTime let _ = keyData mappedLifecycleState = .active(PrivateCallScreen.State.ActiveState( @@ -354,6 +397,9 @@ final class CallControllerNodeV2: ViewControllerTracingNode, CallControllerNodeP emojiKey: self.resolvedEmojiKey(data: keyData) )) case let .reconnecting(startTime, _, keyData): + self.smoothSignalQuality = nil + self.smoothSignalQualityTarget = nil + if self.callStartTimestamp != nil { mappedLifecycleState = .active(PrivateCallScreen.State.ActiveState( startTime: startTime + kCFAbsoluteTimeIntervalSince1970, @@ -517,51 +563,31 @@ final class CallControllerNodeV2: ViewControllerTracingNode, CallControllerNodeP callScreenState.name = peer.displayTitle(strings: self.presentationData.strings, displayOrder: self.presentationData.nameDisplayOrder) callScreenState.shortName = peer.compactDisplayTitle - if self.currentPeer?.smallProfileImage != peer.smallProfileImage { + if (self.currentPeer?.smallProfileImage != peer.smallProfileImage) || self.callScreenState?.avatarImage == nil { self.peerAvatarDisposable?.dispose() - if let smallProfileImage = peer.largeProfileImage, let peerReference = PeerReference(peer._asPeer()) { - if let thumbnailImage = smallProfileImage.immediateThumbnailData.flatMap(decodeTinyThumbnail).flatMap(UIImage.init(data:)), let cgImage = thumbnailImage.cgImage { - callScreenState.avatarImage = generateImage(CGSize(width: 128.0, height: 128.0), contextGenerator: { size, context in - context.draw(cgImage, in: CGRect(origin: CGPoint(), size: size)) - }, scale: 1.0).flatMap { image in - return blurredImage(image, radius: 10.0) - } - } - - let postbox = self.call.context.account.postbox - self.peerAvatarDisposable = (Signal { subscriber in - let fetchDisposable = fetchedMediaResource(mediaBox: postbox.mediaBox, userLocation: .other, userContentType: .avatar, reference: .avatar(peer: peerReference, resource: smallProfileImage.resource)).start() - let dataDisposable = postbox.mediaBox.resourceData(smallProfileImage.resource).start(next: { data in - if data.complete, let image = UIImage(contentsOfFile: data.path)?.precomposed() { - subscriber.putNext(image) - subscriber.putCompletion() - } - }) - - return ActionDisposable { - fetchDisposable.dispose() - dataDisposable.dispose() - } - } - |> deliverOnMainQueue).start(next: { [weak self] image in + let size = CGSize(width: 128.0, height: 128.0) + if let representation = peer.largeProfileImage, let signal = peerAvatarImage(account: self.call.context.account, peerReference: PeerReference(peer._asPeer()), authorOfMessage: nil, representation: representation, displayDimensions: size, synchronousLoad: self.callScreenState?.avatarImage == nil) { + self.peerAvatarDisposable = (signal + |> deliverOnMainQueue).startStrict(next: { [weak self] imageVersions in guard let self else { return } - if var callScreenState = self.callScreenState { + let image = imageVersions?.0 + if let image { callScreenState.avatarImage = image self.callScreenState = callScreenState self.update(transition: .immediate) } }) } else { - self.peerAvatarDisposable?.dispose() - self.peerAvatarDisposable = nil - - callScreenState.avatarImage = generateImage(CGSize(width: 512, height: 512), scale: 1.0, rotatedContext: { size, context in + let image = generateImage(size, rotatedContext: { size, context in context.clear(CGRect(origin: CGPoint(), size: size)) - drawPeerAvatarLetters(context: context, size: size, font: Font.semibold(20.0), letters: peer.displayLetters, peerId: peer.id, nameColor: peer.nameColor) - }) + drawPeerAvatarLetters(context: context, size: size, font: avatarPlaceholderFont(size: 50.0), letters: peer.displayLetters, peerId: peer.id, nameColor: peer.nameColor) + })! + callScreenState.avatarImage = image + self.callScreenState = callScreenState + self.update(transition: .immediate) } } self.currentPeer = peer @@ -893,6 +919,19 @@ private final class AdaptedCallVideoSource: VideoSource { let width = i420Buffer.width let height = i420Buffer.height + /*output = Output( + resolution: CGSize(width: CGFloat(width), height: CGFloat(height)), + textureLayout: .triPlanar(Output.TriPlanarTextureLayout( + y: yTexture, + uv: uvTexture + )), + dataBuffer: Output.NativeDataBuffer(pixelBuffer: nativeBuffer.pixelBuffer), + rotationAngle: rotationAngle, + followsDeviceOrientation: followsDeviceOrientation, + mirrorDirection: mirrorDirection, + sourceId: sourceId + )*/ + let _ = width let _ = height return diff --git a/submodules/TelegramCallsUI/Sources/CallStatusBarNode.swift b/submodules/TelegramCallsUI/Sources/CallStatusBarNode.swift index a2f37daa0f..19b7211e2d 100644 --- a/submodules/TelegramCallsUI/Sources/CallStatusBarNode.swift +++ b/submodules/TelegramCallsUI/Sources/CallStatusBarNode.swift @@ -157,7 +157,7 @@ private class CallStatusBarBackgroundNode: ASDisplayNode { } public class CallStatusBarNodeImpl: CallStatusBarNode { - public enum Content { + public enum Content: Equatable { case call(SharedAccountContext, Account, PresentationCall) case groupCall(SharedAccountContext, Account, PresentationGroupCall) @@ -167,6 +167,23 @@ public class CallStatusBarNodeImpl: CallStatusBarNode { return sharedContext } } + + public static func ==(lhs: Content, rhs: Content) -> Bool { + switch lhs { + case let .call(sharedContext, account, call): + if case let .call(rhsSharedContext, rhsAccount, rhsCall) = rhs, sharedContext === rhsSharedContext, account === rhsAccount, call === rhsCall { + return true + } else { + return false + } + case let .groupCall(sharedContext, account, groupCall): + if case let .groupCall(rhsSharedContext, rhsAccount, rhsGroupCall) = rhs, sharedContext === rhsSharedContext, account === rhsAccount, groupCall === rhsGroupCall { + return true + } else { + return false + } + } + } } private let backgroundNode: CallStatusBarBackgroundNode @@ -236,10 +253,12 @@ public class CallStatusBarNodeImpl: CallStatusBarNode { } public func update(content: Content) { - self.currentContent = content - self.backgroundNode.animationsEnabled = content.sharedContext.energyUsageSettings.fullTranslucency - if self.isCurrentlyInHierarchy { - self.update() + if self.currentContent != content { + self.currentContent = content + self.backgroundNode.animationsEnabled = content.sharedContext.energyUsageSettings.fullTranslucency + if self.isCurrentlyInHierarchy { + self.update() + } } } diff --git a/submodules/TelegramUI/BUILD b/submodules/TelegramUI/BUILD index 5199856ec8..e320ebaecf 100644 --- a/submodules/TelegramUI/BUILD +++ b/submodules/TelegramUI/BUILD @@ -419,6 +419,7 @@ swift_library( "//submodules/TelegramUI/Components/SavedMessages/SavedMessagesScreen", "//submodules/TelegramUI/Components/Settings/WallpaperGalleryScreen", "//submodules/TelegramUI/Components/Settings/WallpaperGridScreen", + "//submodules/TelegramUI/Components/Chat/ChatMessageNotificationItem", ] + select({ "@build_bazel_rules_apple//apple:ios_arm64": appcenter_targets, "//build-system:ios_sim_arm64": [], diff --git a/submodules/TelegramUI/Components/Calls/CallScreen/Sources/Components/ButtonGroupView.swift b/submodules/TelegramUI/Components/Calls/CallScreen/Sources/Components/ButtonGroupView.swift index de46ab41e9..180fa18dca 100644 --- a/submodules/TelegramUI/Components/Calls/CallScreen/Sources/Components/ButtonGroupView.swift +++ b/submodules/TelegramUI/Components/Calls/CallScreen/Sources/Components/ButtonGroupView.swift @@ -15,7 +15,7 @@ final class ButtonGroupView: OverlayMaskContainerView { case end } - case speaker(isActive: Bool) + case speaker(audioOutput: PrivateCallScreen.State.AudioOutput) case flipCamera case video(isActive: Bool) case microphone(isMuted: Bool) @@ -215,10 +215,37 @@ final class ButtonGroupView: OverlayMaskContainerView { let isActive: Bool var isDestructive: Bool = false switch button.content { - case let .speaker(isActiveValue): - title = "speaker" - image = UIImage(bundleImageName: "Call/Speaker") - isActive = isActiveValue + case let .speaker(audioOutput): + switch audioOutput { + case .internalSpeaker, .speaker: + title = "speaker" + default: + title = "audio" + } + + switch audioOutput { + case .internalSpeaker: + image = UIImage(bundleImageName: "Call/Speaker") + isActive = false + case .speaker: + image = UIImage(bundleImageName: "Call/Speaker") + isActive = true + case .airpods: + image = UIImage(bundleImageName: "Call/CallAirpodsButton") + isActive = true + case .airpodsPro: + image = UIImage(bundleImageName: "Call/CallAirpodsProButton") + isActive = true + case .airpodsMax: + image = UIImage(bundleImageName: "Call/CallAirpodsMaxButton") + isActive = true + case .headphones: + image = UIImage(bundleImageName: "Call/CallHeadphonesButton") + isActive = true + case .bluetooth: + image = UIImage(bundleImageName: "Call/CallBluetoothButton") + isActive = true + } case .flipCamera: title = "flip" image = UIImage(bundleImageName: "Call/Flip") diff --git a/submodules/TelegramUI/Components/Calls/CallScreen/Sources/PrivateCallScreen.swift b/submodules/TelegramUI/Components/Calls/CallScreen/Sources/PrivateCallScreen.swift index db793d3e58..a53cfd48ad 100644 --- a/submodules/TelegramUI/Components/Calls/CallScreen/Sources/PrivateCallScreen.swift +++ b/submodules/TelegramUI/Components/Calls/CallScreen/Sources/PrivateCallScreen.swift @@ -60,6 +60,11 @@ public final class PrivateCallScreen: OverlayMaskContainerView, AVPictureInPictu public enum AudioOutput: Equatable { case internalSpeaker case speaker + case headphones + case airpods + case airpodsPro + case airpodsMax + case bluetooth } public var lifecycleState: LifecycleState @@ -354,6 +359,7 @@ public final class PrivateCallScreen: OverlayMaskContainerView, AVPictureInPictu if !self.isUpdating { let wereControlsHidden = self.areControlsHidden self.areControlsHidden = true + self.displayEmojiTooltip = false self.update(transition: .immediate) if !wereControlsHidden { @@ -417,6 +423,24 @@ public final class PrivateCallScreen: OverlayMaskContainerView, AVPictureInPictu if self.activeRemoteVideoSource != nil || self.activeLocalVideoSource != nil { self.areControlsHidden = !self.areControlsHidden update = true + + if self.areControlsHidden { + self.displayEmojiTooltip = false + self.hideControlsTimer?.invalidate() + self.hideControlsTimer = nil + } else { + self.hideControlsTimer?.invalidate() + self.hideControlsTimer = Foundation.Timer.scheduledTimer(withTimeInterval: 5.0, repeats: false, block: { [weak self] _ in + guard let self else { + return + } + if !self.areControlsHidden { + self.areControlsHidden = true + self.displayEmojiTooltip = false + self.update(transition: .spring(duration: 0.4)) + } + }) + } } if update { @@ -515,7 +539,7 @@ public final class PrivateCallScreen: OverlayMaskContainerView, AVPictureInPictu if let previousParams = self.params, case .active = params.state.lifecycleState { switch previousParams.state.lifecycleState { case .requesting, .ringing, .connecting, .reconnecting: - if self.hideEmojiTooltipTimer == nil { + if self.hideEmojiTooltipTimer == nil && !self.areControlsHidden { self.displayEmojiTooltip = true self.hideEmojiTooltipTimer = Foundation.Timer.scheduledTimer(withTimeInterval: 3.0, repeats: false, block: { [weak self] _ in @@ -592,6 +616,7 @@ public final class PrivateCallScreen: OverlayMaskContainerView, AVPictureInPictu } if !self.areControlsHidden { self.areControlsHidden = true + self.displayEmojiTooltip = false self.update(transition: .spring(duration: 0.4)) } }) @@ -704,7 +729,7 @@ public final class PrivateCallScreen: OverlayMaskContainerView, AVPictureInPictu self.flipCameraAction?() }), at: 0) } else { - buttons.insert(ButtonGroupView.Button(content: .speaker(isActive: params.state.audioOutput != .internalSpeaker), isEnabled: !isTerminated, action: { [weak self] in + buttons.insert(ButtonGroupView.Button(content: .speaker(audioOutput: params.state.audioOutput), isEnabled: !isTerminated, action: { [weak self] in guard let self else { return } @@ -1157,9 +1182,12 @@ public final class PrivateCallScreen: OverlayMaskContainerView, AVPictureInPictu transition.setPosition(layer: self.blobLayer, position: CGPoint(x: blobFrame.width * 0.5, y: blobFrame.height * 0.5)) transition.setBounds(layer: self.blobLayer, bounds: CGRect(origin: CGPoint(), size: blobFrame.size)) + let displayAudioLevelBlob: Bool let titleString: String switch params.state.lifecycleState { case let .terminated(terminatedState): + displayAudioLevelBlob = false + self.titleView.contentMode = .center switch terminatedState.reason { @@ -1174,6 +1202,14 @@ public final class PrivateCallScreen: OverlayMaskContainerView, AVPictureInPictu case .missed: titleString = "Call Missed" } + default: + displayAudioLevelBlob = !params.state.isRemoteAudioMuted + + self.titleView.contentMode = .scaleToFill + titleString = params.state.name + } + + if !displayAudioLevelBlob { genericAlphaTransition.setScale(layer: self.blobLayer, scale: 0.3) genericAlphaTransition.setAlpha(layer: self.blobLayer, alpha: 0.0) self.canAnimateAudioLevel = false @@ -1181,11 +1217,12 @@ public final class PrivateCallScreen: OverlayMaskContainerView, AVPictureInPictu self.currentAvatarAudioScale = 1.0 transition.setScale(layer: self.avatarTransformLayer, scale: 1.0) transition.setScale(layer: self.blobTransformLayer, scale: 1.0) - default: - self.titleView.contentMode = .scaleToFill - titleString = params.state.name + } else { genericAlphaTransition.setAlpha(layer: self.blobLayer, alpha: (expandedEmojiKeyOverlapsAvatar && !havePrimaryVideo) ? 0.0 : 1.0) transition.setScale(layer: self.blobLayer, scale: expandedEmojiKeyOverlapsAvatar ? 0.001 : 1.0) + if !havePrimaryVideo { + self.canAnimateAudioLevel = true + } } let titleSize = self.titleView.update( diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageNotificationItem/BUILD b/submodules/TelegramUI/Components/Chat/ChatMessageNotificationItem/BUILD new file mode 100644 index 0000000000..0b956ee90f --- /dev/null +++ b/submodules/TelegramUI/Components/Chat/ChatMessageNotificationItem/BUILD @@ -0,0 +1,38 @@ +load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library") + +swift_library( + name = "ChatMessageNotificationItem", + module_name = "ChatMessageNotificationItem", + srcs = glob([ + "Sources/**/*.swift", + ]), + copts = [ + "-warnings-as-errors", + ], + deps = [ + "//submodules/AsyncDisplayKit", + "//submodules/Display", + "//submodules/Postbox", + "//submodules/TelegramCore", + "//submodules/SSignalKit/SwiftSignalKit", + "//submodules/TelegramPresentationData", + "//submodules/TelegramUIPreferences", + "//submodules/AvatarNode", + "//submodules/AccountContext", + "//submodules/LocalizedPeerData", + "//submodules/StickerResources", + "//submodules/PhotoResources", + "//submodules/TelegramStringFormatting", + "//submodules/TextFormat", + "//submodules/InvisibleInkDustNode", + "//submodules/TelegramUI/Components/TextNodeWithEntities", + "//submodules/TelegramUI/Components/AnimationCache", + "//submodules/TelegramUI/Components/MultiAnimationRenderer", + "//submodules/Components/MultilineTextComponent", + "//submodules/Components/BundleIconComponent", + "//submodules/TelegramUI/Components/PlainButtonComponent", + ], + visibility = [ + "//visibility:public", + ], +) diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageNotificationItem/Sources/ChatCallNotificationItem.swift b/submodules/TelegramUI/Components/Chat/ChatMessageNotificationItem/Sources/ChatCallNotificationItem.swift new file mode 100644 index 0000000000..087a18ddaa --- /dev/null +++ b/submodules/TelegramUI/Components/Chat/ChatMessageNotificationItem/Sources/ChatCallNotificationItem.swift @@ -0,0 +1,233 @@ +import Foundation +import UIKit +import AsyncDisplayKit +import Display +import Postbox +import TelegramCore +import SwiftSignalKit +import TelegramPresentationData +import TelegramUIPreferences +import AvatarNode +import AccountContext +import LocalizedPeerData +import StickerResources +import PhotoResources +import TelegramStringFormatting +import TextFormat +import InvisibleInkDustNode +import TextNodeWithEntities +import AnimationCache +import MultiAnimationRenderer +import ComponentFlow +import MultilineTextComponent +import BundleIconComponent +import PlainButtonComponent + +public final class ChatCallNotificationItem: NotificationItem { + public let context: AccountContext + public let strings: PresentationStrings + public let nameDisplayOrder: PresentationPersonNameOrder + public let peer: EnginePeer + public let isVideo: Bool + public let action: (Bool) -> Void + + public var groupingKey: AnyHashable? { + return nil + } + + public init(context: AccountContext, strings: PresentationStrings, nameDisplayOrder: PresentationPersonNameOrder, peer: EnginePeer, isVideo: Bool, action: @escaping (Bool) -> Void) { + self.context = context + self.strings = strings + self.nameDisplayOrder = nameDisplayOrder + self.peer = peer + self.isVideo = isVideo + self.action = action + } + + public func node(compact: Bool) -> NotificationItemNode { + let node = ChatCallNotificationItemNode() + node.setupItem(self, compact: compact) + return node + } + + public func tapped(_ take: @escaping () -> (ASDisplayNode?, () -> Void)) { + } + + public func canBeExpanded() -> Bool { + return false + } + + public func expand(_ take: @escaping () -> (ASDisplayNode?, () -> Void)) { + } +} + +private let compactAvatarFont = avatarPlaceholderFont(size: 20.0) +private let avatarFont = avatarPlaceholderFont(size: 24.0) + +final class ChatCallNotificationItemNode: NotificationItemNode { + private var item: ChatCallNotificationItem? + + private let avatarNode: AvatarNode + private let title = ComponentView() + private let text = ComponentView() + private let answerButton = ComponentView() + private let declineButton = ComponentView() + + private var compact: Bool? + private var validLayout: CGFloat? + + override init() { + self.avatarNode = AvatarNode(font: avatarFont) + + super.init() + + self.acceptsTouches = true + + self.addSubnode(self.avatarNode) + } + + func setupItem(_ item: ChatCallNotificationItem, compact: Bool) { + self.item = item + + self.compact = compact + if compact { + self.avatarNode.font = compactAvatarFont + } + let presentationData = item.context.sharedContext.currentPresentationData.with { $0 } + + self.avatarNode.setPeer(context: item.context, theme: presentationData.theme, peer: item.peer, overrideImage: nil, emptyColor: presentationData.theme.list.mediaPlaceholderColor) + + if let width = self.validLayout { + let _ = self.updateLayout(width: width, transition: .immediate) + } + } + + override public func updateLayout(width: CGFloat, transition: ContainedViewLayoutTransition) -> CGFloat { + self.validLayout = width + + let panelHeight: CGFloat = 66.0 + + guard let item = self.item else { + return panelHeight + } + + let presentationData = item.context.sharedContext.currentPresentationData.with { $0 } + + let leftInset: CGFloat = 14.0 + let rightInset: CGFloat = 14.0 + let avatarSize: CGFloat = 38.0 + let avatarTextSpacing: CGFloat = 10.0 + let buttonSpacing: CGFloat = 14.0 + let titleTextSpacing: CGFloat = 0.0 + + let maxTextWidth: CGFloat = width - leftInset - avatarTextSpacing - rightInset - avatarSize * 2.0 - buttonSpacing - avatarTextSpacing + + let titleSize = self.title.update( + transition: .immediate, + component: AnyComponent(MultilineTextComponent( + text: .plain(NSAttributedString(string: item.peer.displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder), font: Font.semibold(16.0), textColor: presentationData.theme.list.itemPrimaryTextColor)) + )), + environment: {}, + containerSize: CGSize(width: maxTextWidth, height: 100.0) + ) + + let textSize = self.text.update( + transition: .immediate, + component: AnyComponent(MultilineTextComponent( + text: .plain(NSAttributedString(string: item.isVideo ? presentationData.strings.Notification_VideoCallIncoming : presentationData.strings.Notification_CallIncoming, font: Font.regular(13.0), textColor: presentationData.theme.list.itemPrimaryTextColor)) + )), + environment: {}, + containerSize: CGSize(width: maxTextWidth, height: 100.0) + ) + + let titleTextHeight = titleSize.height + titleTextSpacing + textSize.height + let titleTextY = floor((panelHeight - titleTextHeight) * 0.5) + let titleFrame = CGRect(origin: CGPoint(x: leftInset + avatarSize + avatarTextSpacing, y: titleTextY), size: titleSize) + let textFrame = CGRect(origin: CGPoint(x: leftInset + avatarSize + avatarTextSpacing, y: titleTextY + titleSize.height + titleTextSpacing), size: textSize) + + if let titleView = self.title.view { + if titleView.superview == nil { + self.view.addSubview(titleView) + } + titleView.frame = titleFrame + } + + if let textView = self.text.view { + if textView.superview == nil { + self.view.addSubview(textView) + } + textView.frame = textFrame + } + + transition.updateFrame(node: self.avatarNode, frame: CGRect(origin: CGPoint(x: leftInset, y: (panelHeight - avatarSize) / 2.0), size: CGSize(width: avatarSize, height: avatarSize))) + + let answerButtonSize = self.answerButton.update( + transition: .immediate, + component: AnyComponent(PlainButtonComponent( + content: AnyComponent(ZStack([ + AnyComponentWithIdentity(id: 1, component: AnyComponent(Circle( + fillColor: UIColor(rgb: 0x34C759), + size: CGSize(width: avatarSize, height: avatarSize) + ))), + AnyComponentWithIdentity(id: 2, component: AnyComponent(BundleIconComponent( + name: "Call/CallNotificationAnswerIcon", + tintColor: .white + ))) + ])), + effectAlignment: .center, + minSize: CGSize(width: avatarSize, height: avatarSize), + action: { [weak self] in + guard let self, let item = self.item else { + return + } + item.action(true) + } + )), + environment: {}, + containerSize: CGSize(width: avatarSize, height: avatarSize) + ) + let declineButtonSize = self.declineButton.update( + transition: .immediate, + component: AnyComponent(PlainButtonComponent( + content: AnyComponent(ZStack([ + AnyComponentWithIdentity(id: 1, component: AnyComponent(Circle( + fillColor: UIColor(rgb: 0xFF3B30), + size: CGSize(width: avatarSize, height: avatarSize) + ))), + AnyComponentWithIdentity(id: 2, component: AnyComponent(BundleIconComponent( + name: "Call/CallNotificationDeclineIcon", + tintColor: .white + ))) + ])), + effectAlignment: .center, + minSize: CGSize(width: avatarSize, height: avatarSize), + action: { [weak self] in + guard let self, let item = self.item else { + return + } + item.action(false) + } + )), + environment: {}, + containerSize: CGSize(width: avatarSize, height: avatarSize) + ) + + let declineButtonFrame = CGRect(origin: CGPoint(x: width - rightInset - avatarSize - buttonSpacing - declineButtonSize.width, y: floor((panelHeight - declineButtonSize.height) * 0.5)), size: declineButtonSize) + if let declineButtonView = self.declineButton.view { + if declineButtonView.superview == nil { + self.view.addSubview(declineButtonView) + } + declineButtonView.frame = declineButtonFrame + } + + let answerButtonFrame = CGRect(origin: CGPoint(x: declineButtonFrame.maxX + buttonSpacing, y: floor((panelHeight - answerButtonSize.height) * 0.5)), size: answerButtonSize) + if let answerButtonView = self.answerButton.view { + if answerButtonView.superview == nil { + self.view.addSubview(answerButtonView) + } + answerButtonView.frame = answerButtonFrame + } + + return panelHeight + } +} diff --git a/submodules/TelegramUI/Sources/ChatMessageNotificationItem.swift b/submodules/TelegramUI/Components/Chat/ChatMessageNotificationItem/Sources/ChatMessageNotificationItem.swift similarity index 97% rename from submodules/TelegramUI/Sources/ChatMessageNotificationItem.swift rename to submodules/TelegramUI/Components/Chat/ChatMessageNotificationItem/Sources/ChatMessageNotificationItem.swift index 6b8b5f050e..1d59686554 100644 --- a/submodules/TelegramUI/Sources/ChatMessageNotificationItem.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessageNotificationItem/Sources/ChatMessageNotificationItem.swift @@ -20,14 +20,14 @@ import AnimationCache import MultiAnimationRenderer public final class ChatMessageNotificationItem: NotificationItem { - let context: AccountContext - let strings: PresentationStrings - let dateTimeFormat: PresentationDateTimeFormat - let nameDisplayOrder: PresentationPersonNameOrder - let messages: [Message] - let threadData: MessageHistoryThreadData? - let tapAction: () -> Bool - let expandAction: (@escaping () -> (ASDisplayNode?, () -> Void)) -> Void + public let context: AccountContext + public let strings: PresentationStrings + public let dateTimeFormat: PresentationDateTimeFormat + public let nameDisplayOrder: PresentationPersonNameOrder + public let messages: [Message] + public let threadData: MessageHistoryThreadData? + public let tapAction: () -> Bool + public let expandAction: (@escaping () -> (ASDisplayNode?, () -> Void)) -> Void public var groupingKey: AnyHashable? { return messages.first?.id.peerId @@ -380,7 +380,7 @@ final class ChatMessageNotificationItemNode: NotificationItemNode { } } - override func updateLayout(width: CGFloat, transition: ContainedViewLayoutTransition) -> CGFloat { + override public func updateLayout(width: CGFloat, transition: ContainedViewLayoutTransition) -> CGFloat { self.validLayout = width let compact = self.compact ?? false diff --git a/submodules/TelegramUI/Sources/NotificationItem.swift b/submodules/TelegramUI/Components/Chat/ChatMessageNotificationItem/Sources/NotificationItem.swift similarity index 75% rename from submodules/TelegramUI/Sources/NotificationItem.swift rename to submodules/TelegramUI/Components/Chat/ChatMessageNotificationItem/Sources/NotificationItem.swift index 397c7f4faa..03f0542f3c 100644 --- a/submodules/TelegramUI/Sources/NotificationItem.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessageNotificationItem/Sources/NotificationItem.swift @@ -13,7 +13,9 @@ public protocol NotificationItem { } public class NotificationItemNode: ASDisplayNode { - func updateLayout(width: CGFloat, transition: ContainedViewLayoutTransition) -> CGFloat { + public func updateLayout(width: CGFloat, transition: ContainedViewLayoutTransition) -> CGFloat { return 32.0 } + + public var acceptsTouches: Bool = false } diff --git a/submodules/TelegramUI/Images.xcassets/Avatar/AnonymousSenderIcon.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Avatar/AnonymousSenderIcon.imageset/Contents.json new file mode 100644 index 0000000000..80053edc1f --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Avatar/AnonymousSenderIcon.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "large_hidden 1.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/submodules/TelegramUI/Images.xcassets/Avatar/AnonymousSenderIcon.imageset/large_hidden 1.pdf b/submodules/TelegramUI/Images.xcassets/Avatar/AnonymousSenderIcon.imageset/large_hidden 1.pdf new file mode 100644 index 0000000000..933e557a6b --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Avatar/AnonymousSenderIcon.imageset/large_hidden 1.pdf @@ -0,0 +1,134 @@ +%PDF-1.7 + +1 0 obj + << >> +endobj + +2 0 obj + << /Length 3 0 R >> +stream +/DeviceRGB CS +/DeviceRGB cs +q +1.000000 0.000000 -0.000000 1.000000 2.886230 3.427368 cm +1.000000 1.000000 1.000000 scn +38.260708 21.876385 m +41.966354 17.197329 l +42.025131 17.123957 42.070347 17.040751 42.100487 16.951702 c +42.245766 16.522448 42.015556 16.056696 41.586330 15.911341 c +40.312458 15.480631 39.125717 14.924854 38.026104 14.244013 c +36.921986 13.560385 35.998123 12.807886 35.254517 11.986513 c +35.230656 11.961597 35.209145 11.934603 35.189388 11.906336 c +34.929810 11.534874 35.020504 11.023315 35.391689 10.763336 c +36.612873 9.912409 l +36.764828 9.808632 36.876156 9.655817 36.929893 9.479834 c +37.062233 9.046417 36.818161 8.587776 36.384750 8.455406 c +33.044167 7.440701 29.867857 6.143715 26.855820 4.564449 c +24.258381 3.202564 21.947493 1.751854 19.923153 0.212322 c +19.704218 0.045612 19.416248 0.000019 19.156437 0.090702 c +18.728577 0.240036 18.502787 0.707947 18.652121 1.135807 c +18.758976 1.434193 l +19.772457 4.209812 21.285860 6.707634 23.299189 8.927662 c +25.982121 11.886038 29.317558 14.105398 33.305504 15.585741 c +33.305504 20.218485 l +33.304989 20.432394 33.468914 20.610800 33.682098 20.628386 c +34.438572 20.690022 35.148914 20.831165 35.813126 21.051815 c +36.474316 21.271461 37.109425 21.576414 37.718449 21.966673 c +37.895584 22.080223 38.129875 22.041166 38.260708 21.876385 c +h +4.994442 22.027468 m +8.012641 21.104988 l +8.684547 18.079058 9.790257 15.434935 11.329765 13.172619 c +12.755202 11.077932 14.742273 9.031746 17.290981 7.034061 c +17.637016 6.764141 17.708672 6.269619 17.454308 5.911995 c +16.618917 4.741905 l +16.384727 4.413887 15.946983 4.304516 15.586034 4.483837 c +5.774277 9.358383 l +5.648376 9.420931 5.540658 9.514778 5.461451 9.630922 c +5.206123 10.005320 5.302648 10.515812 5.677044 10.771139 c +6.566517 11.377733 l +6.661282 11.442360 6.741444 11.526138 6.801830 11.623659 c +7.040406 12.008947 6.921472 12.514688 6.536184 12.753265 c +0.388562 16.559954 l +0.336297 16.592318 0.287836 16.630453 0.244088 16.673639 c +-0.078415 16.992004 -0.081768 17.511530 0.236598 17.834032 c +4.170660 21.819214 l +4.385132 22.036472 4.702488 22.116701 4.994442 22.027468 c +h +30.742079 20.589767 m +30.998411 20.158377 31.141327 19.678396 31.141327 19.172619 c +31.141327 17.294851 29.171381 15.772619 26.741327 15.772619 c +24.311274 15.772619 22.341328 17.294851 22.341328 19.172619 c +22.341328 19.425110 22.376945 19.671173 22.444510 19.907970 c +25.367607 19.966751 28.125259 20.198212 30.595587 20.567251 c +30.742079 20.589767 l +h +11.389331 20.560408 m +13.907309 20.186129 16.722143 19.955175 19.704977 19.904078 c +19.726641 19.824015 l +19.779579 19.613188 19.807308 19.395405 19.807308 19.172619 c +19.807308 17.294851 17.837360 15.772619 15.407308 15.772619 c +12.977255 15.772619 11.007308 17.294851 11.007308 19.172619 c +11.007308 19.666950 11.143831 20.136642 11.389331 20.560408 c +h +26.181854 41.372620 m +28.509888 41.372620 30.010063 37.754658 30.682379 30.518734 c +35.052368 29.651772 37.913662 28.207664 37.913662 26.572620 c +37.913662 23.921652 30.392046 21.772619 21.113663 21.772619 c +11.835279 21.772619 4.313663 23.921652 4.313663 26.572620 c +4.313663 28.208481 7.177811 29.653212 11.551497 30.519779 c +12.364214 37.755043 13.874769 41.372620 16.083487 41.372620 c +19.449152 41.372620 18.158268 37.207233 21.000959 37.207233 c +23.843653 37.207233 22.634565 41.372620 26.181854 41.372620 c +h +f* +n +Q + +endstream +endobj + +3 0 obj + 3357 +endobj + +4 0 obj + << /Annots [] + /Type /Page + /MediaBox [ 0.000000 0.000000 48.000000 48.000000 ] + /Resources 1 0 R + /Contents 2 0 R + /Parent 5 0 R + >> +endobj + +5 0 obj + << /Kids [ 4 0 R ] + /Count 1 + /Type /Pages + >> +endobj + +6 0 obj + << /Pages 5 0 R + /Type /Catalog + >> +endobj + +xref +0 7 +0000000000 65535 f +0000000010 00000 n +0000000034 00000 n +0000003447 00000 n +0000003470 00000 n +0000003643 00000 n +0000003717 00000 n +trailer +<< /ID [ (some) (id) ] + /Root 6 0 R + /Size 7 +>> +startxref +3776 +%%EOF \ No newline at end of file diff --git a/submodules/TelegramUI/Images.xcassets/Call/CallNotificationAnswerIcon.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Call/CallNotificationAnswerIcon.imageset/Contents.json new file mode 100644 index 0000000000..0ae3b21323 --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Call/CallNotificationAnswerIcon.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "phone.fill.svg", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/submodules/TelegramUI/Images.xcassets/Call/CallNotificationAnswerIcon.imageset/phone.fill.svg b/submodules/TelegramUI/Images.xcassets/Call/CallNotificationAnswerIcon.imageset/phone.fill.svg new file mode 100644 index 0000000000..ccfed91796 --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Call/CallNotificationAnswerIcon.imageset/phone.fill.svg @@ -0,0 +1,3 @@ + + + diff --git a/submodules/TelegramUI/Images.xcassets/Call/CallNotificationDeclineIcon.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Call/CallNotificationDeclineIcon.imageset/Contents.json new file mode 100644 index 0000000000..47bbf25854 --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Call/CallNotificationDeclineIcon.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "phone.down.fill.svg", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/submodules/TelegramUI/Images.xcassets/Call/CallNotificationDeclineIcon.imageset/phone.down.fill.svg b/submodules/TelegramUI/Images.xcassets/Call/CallNotificationDeclineIcon.imageset/phone.down.fill.svg new file mode 100644 index 0000000000..4b1eb771db --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Call/CallNotificationDeclineIcon.imageset/phone.down.fill.svg @@ -0,0 +1,3 @@ + + + diff --git a/submodules/TelegramUI/Sources/AppDelegate.swift b/submodules/TelegramUI/Sources/AppDelegate.swift index 23cfac6d4f..54609125f3 100644 --- a/submodules/TelegramUI/Sources/AppDelegate.swift +++ b/submodules/TelegramUI/Sources/AppDelegate.swift @@ -896,7 +896,7 @@ private func extractAccountManagerState(records: AccountRecordsView Void)? - let sharedContext = SharedAccountContextImpl(mainWindow: self.mainWindow, sharedContainerPath: legacyBasePath, basePath: rootPath, encryptionParameters: encryptionParameters, accountManager: accountManager, appLockContext: appLockContext, applicationBindings: applicationBindings, initialPresentationDataAndSettings: initialPresentationDataAndSettings, networkArguments: networkArguments, hasInAppPurchases: buildConfig.isAppStoreBuild && buildConfig.apiId == 1, rootPath: rootPath, legacyBasePath: legacyBasePath, apsNotificationToken: self.notificationTokenPromise.get() |> map(Optional.init), voipNotificationToken: self.voipTokenPromise.get() |> map(Optional.init), firebaseSecretStream: self.firebaseSecretStream.get(), setNotificationCall: { call in + let sharedContext = SharedAccountContextImpl(mainWindow: self.mainWindow, sharedContainerPath: legacyBasePath, basePath: rootPath, encryptionParameters: encryptionParameters, accountManager: accountManager, appLockContext: appLockContext, notificationController: nil, applicationBindings: applicationBindings, initialPresentationDataAndSettings: initialPresentationDataAndSettings, networkArguments: networkArguments, hasInAppPurchases: buildConfig.isAppStoreBuild && buildConfig.apiId == 1, rootPath: rootPath, legacyBasePath: legacyBasePath, apsNotificationToken: self.notificationTokenPromise.get() |> map(Optional.init), voipNotificationToken: self.voipTokenPromise.get() |> map(Optional.init), firebaseSecretStream: self.firebaseSecretStream.get(), setNotificationCall: { call in setPresentationCall?(call) }, navigateToChat: { accountId, peerId, messageId in self.openChatWhenReady(accountId: accountId, peerId: peerId, threadId: nil, messageId: messageId, storyId: nil) @@ -1202,6 +1202,7 @@ private func extractAccountManagerState(records: AccountRecordsView Bool) { self.controllerNode.removeItems(f) } diff --git a/submodules/TelegramUI/Sources/NotificationContainerControllerNode.swift b/submodules/TelegramUI/Sources/NotificationContainerControllerNode.swift index d2eebdf635..5870ae8426 100644 --- a/submodules/TelegramUI/Sources/NotificationContainerControllerNode.swift +++ b/submodules/TelegramUI/Sources/NotificationContainerControllerNode.swift @@ -4,6 +4,7 @@ import Display import AsyncDisplayKit import SwiftSignalKit import TelegramPresentationData +import ChatMessageNotificationItem private final class NotificationContainerControllerNodeView: UITracingLayerView { var hitTestImpl: ((CGPoint, UIEvent?) -> UIView?)? @@ -16,6 +17,7 @@ private final class NotificationContainerControllerNodeView: UITracingLayerView final class NotificationContainerControllerNode: ASDisplayNode { private var validLayout: ContainerViewLayout? private var topItemAndNode: (NotificationItem, NotificationItemContainerNode)? + private var blockingItemAndNode: (NotificationItem, NotificationItemContainerNode)? var displayingItemsUpdated: ((Bool) -> Void)? @@ -49,6 +51,9 @@ final class NotificationContainerControllerNode: ASDisplayNode { } override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { + if let (_, blockingItemNode) = self.blockingItemAndNode { + return blockingItemNode.hitTest(point, with: event) + } if let (_, topItemNode) = self.topItemAndNode { return topItemNode.hitTest(point, with: event) } @@ -77,6 +82,10 @@ final class NotificationContainerControllerNode: ASDisplayNode { } func enqueue(_ item: NotificationItem) { + if self.blockingItemAndNode != nil { + return + } + if let (_, topItemNode) = self.topItemAndNode { topItemNode.animateOut(completion: { [weak topItemNode] in topItemNode?.removeFromSupernode() @@ -89,9 +98,8 @@ final class NotificationContainerControllerNode: ASDisplayNode { } let itemNode = item.node(compact: useCompactLayout) - let containerNode = NotificationItemContainerNode(theme: self.presentationData.theme) + let containerNode = NotificationItemContainerNode(theme: self.presentationData.theme, contentNode: itemNode) containerNode.item = item - containerNode.contentNode = itemNode containerNode.dismissed = { [weak self] item in if let strongSelf = self { if let (topItem, topItemNode) = strongSelf.topItemAndNode, topItem.groupingKey != nil && topItem.groupingKey == item.groupingKey { @@ -120,7 +128,12 @@ final class NotificationContainerControllerNode: ASDisplayNode { } } self.topItemAndNode = (item, containerNode) - self.addSubnode(containerNode) + + if let blockingItemAndNode = self.blockingItemAndNode { + self.insertSubnode(containerNode, belowSubnode: blockingItemAndNode.1) + } else { + self.addSubnode(containerNode) + } if let validLayout = self.validLayout { containerNode.updateLayout(layout: validLayout, transition: .immediate) @@ -133,6 +146,70 @@ final class NotificationContainerControllerNode: ASDisplayNode { self.resetTimeoutTimer() } + func setBlocking(_ item: NotificationItem?) { + if let (_, blockingItemNode) = self.blockingItemAndNode { + blockingItemNode.animateOut(completion: { [weak blockingItemNode] in + blockingItemNode?.removeFromSupernode() + }) + self.blockingItemAndNode = nil + } + + if let item = item { + if let (_, topItemNode) = self.topItemAndNode { + topItemNode.animateOut(completion: { [weak topItemNode] in + topItemNode?.removeFromSupernode() + }) + } + self.topItemAndNode = nil + + var useCompactLayout = false + if let validLayout = self.validLayout { + useCompactLayout = min(validLayout.size.width, validLayout.size.height) < 375.0 + } + + let itemNode = item.node(compact: useCompactLayout) + let containerNode = NotificationItemContainerNode(theme: self.presentationData.theme, contentNode: itemNode) + containerNode.item = item + containerNode.dismissed = { [weak self] item in + if let strongSelf = self { + if let (topItem, topItemNode) = strongSelf.topItemAndNode, topItem.groupingKey != nil && topItem.groupingKey == item.groupingKey { + topItemNode.removeFromSupernode() + strongSelf.topItemAndNode = nil + + if let strongSelf = self, strongSelf.topItemAndNode == nil { + strongSelf.displayingItemsUpdated?(false) + } + } + } + } + containerNode.cancelTimeout = { [weak self] item in + if let strongSelf = self { + if let (topItem, _) = strongSelf.topItemAndNode, topItem.groupingKey != nil && topItem.groupingKey == item.groupingKey { + strongSelf.timeoutTimer?.invalidate() + strongSelf.timeoutTimer = nil + } + } + } + containerNode.resumeTimeout = { [weak self] item in + if let strongSelf = self { + if let (topItem, _) = strongSelf.topItemAndNode, topItem.groupingKey != nil && topItem.groupingKey == item.groupingKey { + strongSelf.resetTimeoutTimer() + } + } + } + self.blockingItemAndNode = (item, containerNode) + self.addSubnode(containerNode) + + if let validLayout = self.validLayout { + containerNode.updateLayout(layout: validLayout, transition: .immediate) + containerNode.frame = CGRect(origin: CGPoint(), size: validLayout.size) + containerNode.animateIn() + } + + self.displayingItemsUpdated?(true) + } + } + func removeItems(_ f: (NotificationItem) -> Bool) { if let (topItem, topItemNode) = self.topItemAndNode { if f(topItem) { diff --git a/submodules/TelegramUI/Sources/NotificationContentContext.swift b/submodules/TelegramUI/Sources/NotificationContentContext.swift index f63a5778f6..c5a952f405 100644 --- a/submodules/TelegramUI/Sources/NotificationContentContext.swift +++ b/submodules/TelegramUI/Sources/NotificationContentContext.swift @@ -140,7 +140,7 @@ public final class NotificationViewControllerImpl { return nil }) - sharedAccountContext = SharedAccountContextImpl(mainWindow: nil, sharedContainerPath: self.initializationData.appGroupPath, basePath: rootPath, encryptionParameters: ValueBoxEncryptionParameters(forceEncryptionIfNoSet: false, key: ValueBoxEncryptionParameters.Key(data: self.initializationData.encryptionParameters.0)!, salt: ValueBoxEncryptionParameters.Salt(data: self.initializationData.encryptionParameters.1)!), accountManager: accountManager, appLockContext: appLockContext, applicationBindings: applicationBindings, initialPresentationDataAndSettings: initialPresentationDataAndSettings!, networkArguments: NetworkInitializationArguments(apiId: self.initializationData.apiId, apiHash: self.initializationData.apiHash, languagesCategory: self.initializationData.languagesCategory, appVersion: self.initializationData.appVersion, voipMaxLayer: 0, voipVersions: [], appData: .single(self.initializationData.bundleData), autolockDeadine: .single(nil), encryptionProvider: OpenSSLEncryptionProvider(), deviceModelName: nil, useBetaFeatures: self.initializationData.useBetaFeatures, isICloudEnabled: false), hasInAppPurchases: false, rootPath: rootPath, legacyBasePath: nil, apsNotificationToken: .never(), voipNotificationToken: .never(), firebaseSecretStream: .never(), setNotificationCall: { _ in }, navigateToChat: { _, _, _ in }, appDelegate: nil) + sharedAccountContext = SharedAccountContextImpl(mainWindow: nil, sharedContainerPath: self.initializationData.appGroupPath, basePath: rootPath, encryptionParameters: ValueBoxEncryptionParameters(forceEncryptionIfNoSet: false, key: ValueBoxEncryptionParameters.Key(data: self.initializationData.encryptionParameters.0)!, salt: ValueBoxEncryptionParameters.Salt(data: self.initializationData.encryptionParameters.1)!), accountManager: accountManager, appLockContext: appLockContext, notificationController: nil, applicationBindings: applicationBindings, initialPresentationDataAndSettings: initialPresentationDataAndSettings!, networkArguments: NetworkInitializationArguments(apiId: self.initializationData.apiId, apiHash: self.initializationData.apiHash, languagesCategory: self.initializationData.languagesCategory, appVersion: self.initializationData.appVersion, voipMaxLayer: 0, voipVersions: [], appData: .single(self.initializationData.bundleData), autolockDeadine: .single(nil), encryptionProvider: OpenSSLEncryptionProvider(), deviceModelName: nil, useBetaFeatures: self.initializationData.useBetaFeatures, isICloudEnabled: false), hasInAppPurchases: false, rootPath: rootPath, legacyBasePath: nil, apsNotificationToken: .never(), voipNotificationToken: .never(), firebaseSecretStream: .never(), setNotificationCall: { _ in }, navigateToChat: { _, _, _ in }, appDelegate: nil) presentationDataPromise.set(sharedAccountContext!.presentationData) } diff --git a/submodules/TelegramUI/Sources/NotificationItemContainerNode.swift b/submodules/TelegramUI/Sources/NotificationItemContainerNode.swift index 22e32ffe50..aff92e020c 100644 --- a/submodules/TelegramUI/Sources/NotificationItemContainerNode.swift +++ b/submodules/TelegramUI/Sources/NotificationItemContainerNode.swift @@ -3,6 +3,7 @@ import UIKit import AsyncDisplayKit import Display import TelegramPresentationData +import ChatMessageNotificationItem final class NotificationItemContainerNode: ASDisplayNode { private let backgroundNode: ASImageNode @@ -45,7 +46,9 @@ final class NotificationItemContainerNode: ASDisplayNode { var cancelledTimeout = false - init(theme: PresentationTheme) { + init(theme: PresentationTheme, contentNode: NotificationItemNode?) { + self.contentNode = contentNode + self.backgroundNode = ASImageNode() self.backgroundNode.displayWithoutProcessing = true self.backgroundNode.displaysAsynchronously = false @@ -54,16 +57,21 @@ final class NotificationItemContainerNode: ASDisplayNode { super.init() self.addSubnode(self.backgroundNode) + if let contentNode { + self.addSubnode(contentNode) + } } override func didLoad() { super.didLoad() - self.view.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.tapGesture(_:)))) - let panRecognizer = UIPanGestureRecognizer(target: self, action: #selector(self.panGesture(_:))) - panRecognizer.delaysTouchesBegan = false - panRecognizer.cancelsTouchesInView = false - self.view.addGestureRecognizer(panRecognizer) + if let contentNode = self.contentNode, !contentNode.acceptsTouches { + self.view.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.tapGesture(_:)))) + let panRecognizer = UIPanGestureRecognizer(target: self, action: #selector(self.panGesture(_:))) + panRecognizer.delaysTouchesBegan = false + panRecognizer.cancelsTouchesInView = false + self.view.addGestureRecognizer(panRecognizer) + } } func animateIn() { @@ -113,6 +121,11 @@ final class NotificationItemContainerNode: ASDisplayNode { override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { if let contentNode = self.contentNode, contentNode.frame.contains(point) { + if contentNode.acceptsTouches { + if let result = contentNode.view.hitTest(self.view.convert(point, to: contentNode.view), with: event) { + return result + } + } return self.view } return nil diff --git a/submodules/TelegramUI/Sources/SharedAccountContext.swift b/submodules/TelegramUI/Sources/SharedAccountContext.swift index 0afff5e7bc..ae02b06b3c 100644 --- a/submodules/TelegramUI/Sources/SharedAccountContext.swift +++ b/submodules/TelegramUI/Sources/SharedAccountContext.swift @@ -50,6 +50,7 @@ import ChatRecentActionsController import PeerInfoScreen import ChatQrCodeScreen import UndoUI +import ChatMessageNotificationItem private final class AccountUserInterfaceInUseContext { let subscribers = Bag<(Bool) -> Void>() @@ -87,6 +88,15 @@ public final class SharedAccountContextImpl: SharedAccountContext { public let basePath: String public let accountManager: AccountManager public let appLockContext: AppLockContext + public var notificationController: NotificationContainerController? { + didSet { + if self.notificationController !== oldValue { + if let oldValue { + oldValue.setBlocking(nil) + } + } + } + } private let navigateToChatImpl: (AccountRecordId, PeerId, MessageId?) -> Void @@ -137,6 +147,7 @@ public final class SharedAccountContextImpl: SharedAccountContext { public let hasOngoingCall = ValuePromise(false) private let callState = Promise(nil) private var awaitingCallConnectionDisposable: Disposable? + private var callPeerDisposable: Disposable? private var groupCallController: VoiceChatController? public var currentGroupCallController: ViewController? { @@ -216,7 +227,7 @@ public final class SharedAccountContextImpl: SharedAccountContext { private let energyUsageAutomaticDisposable = MetaDisposable() - init(mainWindow: Window1?, sharedContainerPath: String, basePath: String, encryptionParameters: ValueBoxEncryptionParameters, accountManager: AccountManager, appLockContext: AppLockContext, applicationBindings: TelegramApplicationBindings, initialPresentationDataAndSettings: InitialPresentationDataAndSettings, networkArguments: NetworkInitializationArguments, hasInAppPurchases: Bool, rootPath: String, legacyBasePath: String?, apsNotificationToken: Signal, voipNotificationToken: Signal, firebaseSecretStream: Signal<[String: String], NoError>, setNotificationCall: @escaping (PresentationCall?) -> Void, navigateToChat: @escaping (AccountRecordId, PeerId, MessageId?) -> Void, displayUpgradeProgress: @escaping (Float?) -> Void = { _ in }, appDelegate: AppDelegate?) { + init(mainWindow: Window1?, sharedContainerPath: String, basePath: String, encryptionParameters: ValueBoxEncryptionParameters, accountManager: AccountManager, appLockContext: AppLockContext, notificationController: NotificationContainerController?, applicationBindings: TelegramApplicationBindings, initialPresentationDataAndSettings: InitialPresentationDataAndSettings, networkArguments: NetworkInitializationArguments, hasInAppPurchases: Bool, rootPath: String, legacyBasePath: String?, apsNotificationToken: Signal, voipNotificationToken: Signal, firebaseSecretStream: Signal<[String: String], NoError>, setNotificationCall: @escaping (PresentationCall?) -> Void, navigateToChat: @escaping (AccountRecordId, PeerId, MessageId?) -> Void, displayUpgradeProgress: @escaping (Float?) -> Void = { _ in }, appDelegate: AppDelegate?) { assert(Queue.mainQueue().isCurrent()) precondition(!testHasInstance) @@ -231,6 +242,7 @@ public final class SharedAccountContextImpl: SharedAccountContext { self.navigateToChatImpl = navigateToChat self.displayUpgradeProgress = displayUpgradeProgress self.appLockContext = appLockContext + self.notificationController = notificationController self.hasInAppPurchases = hasInAppPurchases self.accountManager.mediaBox.fetchCachedResourceRepresentation = { (resource, representation) -> Signal in @@ -767,18 +779,52 @@ public final class SharedAccountContextImpl: SharedAccountContext { self.callController = nil self.hasOngoingCall.set(false) + self.notificationController?.setBlocking(nil) + + self.callPeerDisposable?.dispose() + self.callPeerDisposable = nil + if let call { self.callState.set(call.state |> map(Optional.init)) self.hasOngoingCall.set(true) setNotificationCall(call) - if !call.isOutgoing && call.isIntegratedWithCallKit { + if call.isOutgoing { + self.presentControllerWithCurrentCall() + } else { + if !call.isIntegratedWithCallKit { + self.callPeerDisposable?.dispose() + self.callPeerDisposable = (call.context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: call.peerId)) + |> deliverOnMainQueue).startStrict(next: { [weak self, weak call] peer in + guard let self, let call, let peer else { + return + } + if self.call !== call { + return + } + + let presentationData = self.currentPresentationData.with { $0 } + self.notificationController?.setBlocking(ChatCallNotificationItem(context: call.context, strings: presentationData.strings, nameDisplayOrder: presentationData.nameDisplayOrder, peer: peer, isVideo: call.isVideo, action: { [weak call] answerAction in + guard let call else { + return + } + if answerAction { + call.answer() + } else { + call.rejectBusy() + } + })) + }) + } + self.awaitingCallConnectionDisposable = (call.state |> filter { state in switch state.state { case .ringing: return false + case .terminating, .terminated: + return false default: return true } @@ -788,10 +834,12 @@ public final class SharedAccountContextImpl: SharedAccountContext { guard let self else { return } + self.notificationController?.setBlocking(nil) self.presentControllerWithCurrentCall() + + self.callPeerDisposable?.dispose() + self.callPeerDisposable = nil }) - } else{ - self.presentControllerWithCurrentCall() } } else { self.callState.set(.single(nil)) @@ -861,6 +909,29 @@ public final class SharedAccountContextImpl: SharedAccountContext { let callSignal: Signal = .single(nil) |> then( callManager.currentCallSignal + |> deliverOnMainQueue + |> mapToSignal { call -> Signal in + guard let call else { + return .single(nil) + } + return call.state + |> map { [weak call] state -> PresentationCall? in + guard let call else { + return nil + } + switch state.state { + case .ringing: + return nil + case .terminating, .terminated: + return nil + default: + return call + } + } + } + |> distinctUntilChanged(isEqual: { lhs, rhs in + return lhs === rhs + }) ) let groupCallSignal: Signal = .single(nil) |> then( @@ -1002,6 +1073,7 @@ public final class SharedAccountContextImpl: SharedAccountContext { self.groupCallDisposable?.dispose() self.callStateDisposable?.dispose() self.awaitingCallConnectionDisposable?.dispose() + self.callPeerDisposable?.dispose() } private var didPerformAccountSettingsImport = false diff --git a/submodules/TelegramVoip/Sources/GroupCallContext.swift b/submodules/TelegramVoip/Sources/GroupCallContext.swift index 1860d6caee..f8fa7d5abf 100644 --- a/submodules/TelegramVoip/Sources/GroupCallContext.swift +++ b/submodules/TelegramVoip/Sources/GroupCallContext.swift @@ -25,6 +25,17 @@ final class ContextQueueImpl: NSObject, OngoingCallThreadLocalContextQueueWebrtc func isCurrent() -> Bool { return self.queue.isCurrent() } + + func scheduleBlock(_ f: @escaping () -> Void, after timeout: Double) -> GroupCallDisposable { + let timer = SwiftSignalKit.Timer(timeout: timeout, repeat: false, completion: { + f() + }, queue: self.queue) + timer.start() + + return GroupCallDisposable(block: { + timer.invalidate() + }) + } } enum BroadcastPartSubject { diff --git a/submodules/TelegramVoip/Sources/OngoingCallContext.swift b/submodules/TelegramVoip/Sources/OngoingCallContext.swift index 1c1270e8c9..5c2aba5e1b 100644 --- a/submodules/TelegramVoip/Sources/OngoingCallContext.swift +++ b/submodules/TelegramVoip/Sources/OngoingCallContext.swift @@ -213,7 +213,7 @@ public struct OngoingCallContextState: Equatable { public let remoteBatteryLevel: RemoteBatteryLevel } -private final class OngoingCallThreadLocalContextQueueImpl: NSObject, OngoingCallThreadLocalContextQueue, OngoingCallThreadLocalContextQueueWebrtc /*, OngoingCallThreadLocalContextQueueWebrtcCustom*/ { +private final class OngoingCallThreadLocalContextQueueImpl: NSObject, OngoingCallThreadLocalContextQueue, OngoingCallThreadLocalContextQueueWebrtc { private let queue: Queue init(queue: Queue) { @@ -235,6 +235,17 @@ private final class OngoingCallThreadLocalContextQueueImpl: NSObject, OngoingCal func isCurrent() -> Bool { return self.queue.isCurrent() } + + func scheduleBlock(_ f: @escaping () -> Void, after timeout: Double) -> GroupCallDisposable { + let timer = SwiftSignalKit.Timer(timeout: timeout, repeat: false, completion: { + f() + }, queue: self.queue) + timer.start() + + return GroupCallDisposable(block: { + timer.invalidate() + }) + } } private func ongoingNetworkTypeForType(_ type: NetworkType) -> OngoingCallNetworkType { diff --git a/submodules/TgVoipWebrtc/PublicHeaders/TgVoipWebrtc/OngoingCallThreadLocalContext.h b/submodules/TgVoipWebrtc/PublicHeaders/TgVoipWebrtc/OngoingCallThreadLocalContext.h index ae8a533675..e6e9833f1a 100644 --- a/submodules/TgVoipWebrtc/PublicHeaders/TgVoipWebrtc/OngoingCallThreadLocalContext.h +++ b/submodules/TgVoipWebrtc/PublicHeaders/TgVoipWebrtc/OngoingCallThreadLocalContext.h @@ -98,11 +98,20 @@ typedef NS_ENUM(int32_t, OngoingCallDataSavingWebrtc) { OngoingCallDataSavingAlways }; +@interface GroupCallDisposable : NSObject + +- (instancetype _Nonnull)initWithBlock:(dispatch_block_t _Nonnull)block; +- (void)dispose; + +@end + @protocol OngoingCallThreadLocalContextQueueWebrtc - (void)dispatch:(void (^ _Nonnull)())f; - (bool)isCurrent; +- (GroupCallDisposable * _Nonnull)scheduleBlock:(void (^ _Nonnull)())f after:(double)timeout; + @end @interface VoipProxyServerWebrtc : NSObject @@ -133,13 +142,6 @@ typedef NS_ENUM(int32_t, OngoingCallDataSavingWebrtc) { #endif @end -@interface GroupCallDisposable : NSObject - -- (instancetype _Nonnull)initWithBlock:(dispatch_block_t _Nonnull)block; -- (void)dispose; - -@end - @protocol CallVideoFrameBuffer @end diff --git a/submodules/TgVoipWebrtc/Sources/OngoingCallThreadLocalContext.mm b/submodules/TgVoipWebrtc/Sources/OngoingCallThreadLocalContext.mm index 06a7924188..f0e739f150 100644 --- a/submodules/TgVoipWebrtc/Sources/OngoingCallThreadLocalContext.mm +++ b/submodules/TgVoipWebrtc/Sources/OngoingCallThreadLocalContext.mm @@ -925,7 +925,11 @@ tgcalls::VideoCaptureInterfaceObject *GetVideoCaptureAssumingSameThread(tgcalls: std::unique_ptr _tgVoip; bool _didStop; + OngoingCallStateWebrtc _pendingState; OngoingCallStateWebrtc _state; + bool _didPushStateOnce; + GroupCallDisposable *_pushStateDisposable; + OngoingCallVideoStateWebrtc _videoState; bool _connectedOnce; OngoingCallRemoteBatteryLevelWebrtc _remoteBatteryLevel; @@ -1356,6 +1360,7 @@ static void (*InternalVoipLoggingFunction)(NSString *) = NULL; .directConnectionChannel = directConnectionChannel }); _state = OngoingCallStateInitializing; + _pendingState = OngoingCallStateInitializing; _signalBars = 4; } return self; @@ -1374,6 +1379,8 @@ static void (*InternalVoipLoggingFunction)(NSString *) = NULL; _currentAudioDeviceModuleThread = nullptr; } + [_pushStateDisposable dispose]; + if (_tgVoip != NULL) { [self stop:nil]; } @@ -1469,6 +1476,18 @@ static void (*InternalVoipLoggingFunction)(NSString *) = NULL; } } +- (void)pushPendingState { + _didPushStateOnce = true; + + if (_state != _pendingState) { + _state = _pendingState; + + if (_stateChanged) { + _stateChanged(_state, _videoState, _remoteVideoState, _remoteAudioState, _remoteBatteryLevel, _remotePreferredAspectRatio); + } + } +} + - (void)controllerStateChanged:(tgcalls::State)state { OngoingCallStateWebrtc callState = OngoingCallStateInitializing; switch (state) { @@ -1485,11 +1504,32 @@ static void (*InternalVoipLoggingFunction)(NSString *) = NULL; break; } - if (_state != callState) { - _state = callState; + if (_pendingState != callState) { + _pendingState = callState; - if (_stateChanged) { - _stateChanged(_state, _videoState, _remoteVideoState, _remoteAudioState, _remoteBatteryLevel, _remotePreferredAspectRatio); + [_pushStateDisposable dispose]; + _pushStateDisposable = nil; + + bool maybeDelayPush = false; + if (!_didPushStateOnce) { + maybeDelayPush = false; + } else if (callState == OngoingCallStateReconnecting) { + maybeDelayPush = true; + } else { + maybeDelayPush = false; + } + + if (!maybeDelayPush) { + [self pushPendingState]; + } else { + __weak OngoingCallThreadLocalContextWebrtc *weakSelf = self; + _pushStateDisposable = [_queue scheduleBlock:^{ + __strong OngoingCallThreadLocalContextWebrtc *strongSelf = weakSelf; + if (!strongSelf) { + return; + } + [strongSelf pushPendingState]; + } after:1.0]; } } }