Various improvements

This commit is contained in:
Isaac 2023-12-28 16:55:12 +04:00
parent 4b16494e20
commit 7c0d5519bf
33 changed files with 933 additions and 109 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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<UIImage?, NoError> { 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

View File

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

View File

@ -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": [],

View File

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

View File

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

View File

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

View File

@ -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<Empty>()
private let text = ComponentView<Empty>()
private let answerButton = ComponentView<Empty>()
private let declineButton = ComponentView<Empty>()
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
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1,3 @@
<svg width="17" height="17" viewBox="0 0 17 17" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M5.00575 11.8681C7.53795 14.4003 10.6134 16.3542 13.1281 16.3542C14.2584 16.3542 15.2485 15.96 16.0458 15.0838C16.5102 14.5668 16.7993 13.9622 16.7993 13.3664C16.7993 12.9283 16.6328 12.5078 16.221 12.2098L13.5311 10.2997C13.1193 10.0194 12.7776 9.87917 12.4622 9.87917C12.0679 9.87917 11.7086 10.107 11.3056 10.5013L10.6835 11.1146C10.5871 11.211 10.4644 11.2548 10.3505 11.2548C10.2191 11.2548 10.0877 11.2022 10.0001 11.1584C9.45681 10.8693 8.52805 10.0719 7.66062 9.21326C6.80195 8.3546 6.00461 7.42583 5.72423 6.88259C5.68042 6.78621 5.62785 6.66354 5.62785 6.53211C5.62785 6.41821 5.6629 6.3043 5.75928 6.20792L6.38137 5.5683C6.7669 5.16525 7.00347 4.81477 7.00347 4.41172C7.00347 4.09629 6.85452 3.75458 6.56538 3.34277L4.68156 0.687902C4.37489 0.267329 3.94556 0.0833282 3.47241 0.0833282C2.89412 0.0833282 2.29831 0.346186 1.78136 0.845617C0.931451 1.66048 0.554688 2.6681 0.554688 3.78086C0.554688 6.29554 2.47355 9.34469 5.00575 11.8681Z" fill="white"/>
</svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

View File

@ -0,0 +1,12 @@
{
"images" : [
{
"filename" : "phone.down.fill.svg",
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

View File

@ -0,0 +1,3 @@
<svg width="22" height="9" viewBox="0 0 22 9" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M10.7451 0.305557C6.95123 0.305557 3.38512 1.10289 1.60645 2.88157C0.80035 3.68766 0.3973 4.66024 0.449872 5.83434C0.48492 6.54406 0.703968 7.17491 1.11578 7.58673C1.42245 7.90216 1.85178 8.07739 2.35121 7.99854L5.60189 7.44654C6.09256 7.36768 6.43427 7.21872 6.65332 6.99091C6.94247 6.71053 7.03009 6.28996 7.03009 5.73796V4.853C7.03009 4.71281 7.09142 4.60767 7.17904 4.52005C7.26666 4.41491 7.39809 4.3711 7.49447 4.34481C8.09028 4.20462 9.29943 4.07319 10.7451 4.07319C12.1909 4.07319 13.3913 4.17833 13.9958 4.35357C14.0834 4.37986 14.2061 4.43243 14.3025 4.52005C14.3813 4.60767 14.4339 4.70405 14.4427 4.84424L14.4514 5.73796C14.4602 6.28996 14.5478 6.71053 14.8282 6.99091C15.056 7.21872 15.3977 7.36768 15.8884 7.44654L19.0953 7.98978C19.6122 8.07739 20.0503 7.89339 20.392 7.56044C20.8039 7.15739 21.0317 6.53529 21.0492 5.82558C21.0755 4.64272 20.6199 3.67014 19.8313 2.88157C18.0526 1.10289 14.5391 0.305557 10.7451 0.305557Z" fill="white"/>
</svg>

After

Width:  |  Height:  |  Size: 1.0 KiB

View File

@ -896,7 +896,7 @@ private func extractAccountManagerState(records: AccountRecordsView<TelegramAcco
})
var setPresentationCall: ((PresentationCall?) -> 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<TelegramAcco
return true
})
self.mainWindow.topLevelOverlayControllers = [context.sharedApplicationContext.overlayMediaController, context.notificationController]
(context.context.sharedContext as? SharedAccountContextImpl)?.notificationController = context.notificationController
var authorizeNotifications = true
if #available(iOS 10.0, *) {
authorizeNotifications = false

View File

@ -28,6 +28,7 @@ import TelegramCallsUI
import AuthorizationUI
import ChatListUI
import StoryContainerScreen
import ChatMessageNotificationItem
final class UnauthorizedApplicationContext {
let sharedContext: SharedAccountContextImpl

View File

@ -27,6 +27,7 @@ public func makeTempContext(
encryptionParameters: encryptionParameters,
accountManager: accountManager,
appLockContext: appLockContext,
notificationController: nil,
applicationBindings: applicationBindings,
initialPresentationDataAndSettings: initialPresentationDataAndSettings,
networkArguments: networkArguments,

View File

@ -20,6 +20,7 @@ import MediaEditorScreen
import ChatControllerInteraction
import SavedMessagesScreen
import WallpaperGalleryScreen
import ChatMessageNotificationItem
public func navigateToChatControllerImpl(_ params: NavigateToChatControllerParams) {
if case let .peer(peer) = params.chatLocation {

View File

@ -6,6 +6,7 @@ import TelegramCore
import SwiftSignalKit
import TelegramPresentationData
import AccountContext
import ChatMessageNotificationItem
public final class NotificationContainerController: ViewController {
private var controllerNode: NotificationContainerControllerNode {
@ -97,6 +98,10 @@ public final class NotificationContainerController: ViewController {
self.controllerNode.enqueue(item)
}
public func setBlocking(_ item: NotificationItem?) {
self.controllerNode.setBlocking(item)
}
public func removeItems(_ f: (NotificationItem) -> Bool) {
self.controllerNode.removeItems(f)
}

View File

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

View File

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

View File

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

View File

@ -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<TelegramAccountManagerTypes>
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<Bool>(false)
private let callState = Promise<PresentationCallState?>(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<TelegramAccountManagerTypes>, appLockContext: AppLockContext, applicationBindings: TelegramApplicationBindings, initialPresentationDataAndSettings: InitialPresentationDataAndSettings, networkArguments: NetworkInitializationArguments, hasInAppPurchases: Bool, rootPath: String, legacyBasePath: String?, apsNotificationToken: Signal<Data?, NoError>, voipNotificationToken: Signal<Data?, NoError>, 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<TelegramAccountManagerTypes>, appLockContext: AppLockContext, notificationController: NotificationContainerController?, applicationBindings: TelegramApplicationBindings, initialPresentationDataAndSettings: InitialPresentationDataAndSettings, networkArguments: NetworkInitializationArguments, hasInAppPurchases: Bool, rootPath: String, legacyBasePath: String?, apsNotificationToken: Signal<Data?, NoError>, voipNotificationToken: Signal<Data?, NoError>, 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<CachedMediaResourceRepresentationResult, NoError> 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<PresentationCall?, NoError> = .single(nil)
|> then(
callManager.currentCallSignal
|> deliverOnMainQueue
|> mapToSignal { call -> Signal<PresentationCall?, NoError> 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<PresentationGroupCall?, NoError> = .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

View File

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

View File

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

View File

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

View File

@ -925,7 +925,11 @@ tgcalls::VideoCaptureInterfaceObject *GetVideoCaptureAssumingSameThread(tgcalls:
std::unique_ptr<tgcalls::Instance> _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];
}
}
}