Voice Chat UI improvements

This commit is contained in:
Ilya Laktyushin 2020-11-30 11:22:25 +04:00
parent 572bc95254
commit 41bcbd14f0
8 changed files with 125 additions and 62 deletions

View File

@ -1353,7 +1353,7 @@ class ChatListItemNode: ItemListRevealOptionsItemNode {
case let .peer(_, renderedPeer, _, _, presence, _ ,_ ,_, _, _, displayAsMessage, _):
if !displayAsMessage {
if let peer = renderedPeer.peer as? TelegramUser, let presence = presence as? TelegramUserPresence, !isServicePeer(peer) && !peer.flags.contains(.isSupport) && peer.id != item.context.account.peerId {
var updatedPresence = TelegramUserPresence(status: presence.status, lastActivity: 0)
let updatedPresence = TelegramUserPresence(status: presence.status, lastActivity: 0)
let relativeStatus = relativeUserPresenceStatus(updatedPresence, relativeTo: timestamp)
if case .online = relativeStatus {
online = true
@ -1361,10 +1361,10 @@ class ChatListItemNode: ItemListRevealOptionsItemNode {
animateOnline = true
} else if let channel = renderedPeer.peer as? TelegramChannel {
onlineIsVoiceChat = true
if channel.flags.contains(.hasVoiceChat) {
if channel.flags.contains(.hasVoiceChat) && item.interaction.searchTextHighightState == nil {
online = true
animateOnline = true
}
animateOnline = true
}
}

View File

@ -47,7 +47,15 @@ private final class Curve {
let minSpeed: CGFloat
let maxSpeed: CGFloat
var size: CGSize
var size: CGSize {
didSet {
if self.size != oldValue {
self.fromPoints = nil
self.toPoints = nil
self.animateToNewShape()
}
}
}
let alpha: CGFloat
var currentOffset: CGFloat = 1.0
var minOffset: CGFloat = 0.0
@ -177,7 +185,7 @@ private final class Curve {
private func generateNextCurve(for size: CGSize) -> [CGPoint] {
let randomness = minRandomness + (maxRandomness - minRandomness) * speedLevel
return curve(pointsCount: pointsCount, randomness: randomness).map {
return CGPoint(x: $0.x * CGFloat(size.width), y: size.height - 14.0 + $0.y * 12.0)
return CGPoint(x: $0.x * CGFloat(size.width), y: size.height - 17.0 + $0.y * 12.0)
}
}
@ -545,6 +553,6 @@ public class CallStatusBarNodeImpl: CallStatusBarNode {
self.subtitleNode.frame = CGRect(origin: CGPoint(x: horizontalOrigin + animationSize + iconSpacing + titleSize.width + spacing, y: verticalOrigin + floor((contentHeight - subtitleSize.height) / 2.0)), size: subtitleSize)
self.backgroundNode.speaking = !self.currentIsMuted
self.backgroundNode.frame = CGRect(origin: CGPoint(), size: CGSize(width: size.width, height: size.height + 14.0))
self.backgroundNode.frame = CGRect(origin: CGPoint(), size: CGSize(width: size.width, height: size.height + 17.0))
}
}

View File

@ -71,6 +71,7 @@ public final class GroupCallNavigationAccessoryPanel: ASDisplayNode {
let titleNode: ImmediateTextNode
let textNode: ImmediateTextNode
private var textIsActive = false
private let muteIconNode: ASImageNode
private let avatarsContext: AnimatedAvatarSetContext
@ -229,6 +230,22 @@ public final class GroupCallNavigationAccessoryPanel: ASDisplayNode {
self.muteIconNode.image = PresentationResourcesChat.chatTitleMuteIcon(presentationData.theme)
}
private func animateTextChange() {
if let snapshotView = self.textNode.view.snapshotContentTree() {
let offset: CGFloat = self.textIsActive ? -7.0 : 7.0
self.textNode.view.superview?.insertSubview(snapshotView, belowSubview: self.textNode.view)
snapshotView.frame = self.textNode.frame
snapshotView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { [weak snapshotView] _ in
snapshotView?.removeFromSuperview()
})
snapshotView.layer.animatePosition(from: CGPoint(), to: CGPoint(x: 0.0, y: -offset), duration: 0.2, removeOnCompletion: false, additive: true)
self.textNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2)
self.textNode.layer.animatePosition(from: CGPoint(x: 0.0, y: offset), to: CGPoint(), duration: 0.2, additive: true)
}
}
public func update(data: GroupCallPanelData) {
let previousData = self.currentData
self.currentData = data
@ -271,6 +288,11 @@ public final class GroupCallNavigationAccessoryPanel: ASDisplayNode {
membersTextIsActive = false
}
if strongSelf.textIsActive != membersTextIsActive {
strongSelf.textIsActive = membersTextIsActive
strongSelf.animateTextChange()
}
strongSelf.textNode.attributedText = NSAttributedString(string: membersText, font: Font.regular(13.0), textColor: membersTextIsActive ? strongSelf.theme.chat.inputPanel.panelControlAccentColor : strongSelf.theme.chat.inputPanel.secondaryTextColor)
strongSelf.avatarsContent = strongSelf.avatarsContext.update(peers: summaryState.topParticipants.map { $0.peer }, animated: false)
@ -368,6 +390,11 @@ public final class GroupCallNavigationAccessoryPanel: ASDisplayNode {
membersTextIsActive = false
}
if self.textIsActive != membersTextIsActive {
self.textIsActive = membersTextIsActive
self.animateTextChange()
}
self.textNode.attributedText = NSAttributedString(string: membersText, font: Font.regular(13.0), textColor: membersTextIsActive ? self.theme.chat.inputPanel.panelControlAccentColor : self.theme.chat.inputPanel.secondaryTextColor)
self.avatarsContent = self.avatarsContext.update(peers: data.topParticipants.map { $0.peer }, animated: false)
@ -481,7 +508,7 @@ public final class GroupCallNavigationAccessoryPanel: ASDisplayNode {
let foregroundFrame = self.micButtonForegroundNode.view.convert(self.micButtonForegroundNode.bounds, to: nil)
let backgroundView = UIView()
backgroundView.backgroundColor = UIColor(rgb: 0x30b251)
backgroundView.backgroundColor = (self.micButtonBackgroundNodeIsMuted ?? true) ? UIColor(rgb: 0xb6b6bb) : UIColor(rgb: 0x30b251)
backgroundView.frame = backgroundFrame
backgroundView.layer.cornerRadius = backgroundFrame.height / 2.0

View File

@ -576,7 +576,9 @@ private class VoiceChatActionButtonBackgroundNode: ASDisplayNode {
self.animator = animator
}
animator.isPaused = false
animator.frameInterval = state.frameInterval
if self.transition == nil {
animator.frameInterval = state.frameInterval
}
} else {
self.animator?.isPaused = true
}
@ -619,7 +621,7 @@ private class VoiceChatActionButtonBackgroundNode: ASDisplayNode {
var appearanceProgress: CGFloat = 1.0
var glowScale: CGFloat = 0.75
if let transition = parameters.transition, transition.previousState == .connecting {
if let transition = parameters.transition, transition.previousState == .connecting || transition.previousState == .disabled {
appearanceProgress = transition.transition
}
@ -751,15 +753,18 @@ private class VoiceChatActionButtonBackgroundNode: ASDisplayNode {
}
}
var clearInside: CGFloat?
var clearInsideTransition: CGFloat?
if parameters.state is VoiceChatActionButtonBackgroundNodeBlobState {
let path = CGMutablePath()
path.addEllipse(in: buttonRect.insetBy(dx: -lineWidth / 2.0, dy: -lineWidth / 2.0))
context.addPath(path)
context.clip()
if let transition = parameters.transition, transition.previousState == .connecting || transition.previousState == .disabled, transition.transition > 0.5 {
let progress = (transition.transition - 0.5) / 0.5
clearInside = progress
if let transition = parameters.transition {
if transition.previousState == .connecting, transition.transition > 0.5 {
clearInsideTransition = (transition.transition - 0.5) / 0.5
} else if transition.previousState == .disabled {
clearInsideTransition = transition.transition
}
}
drawGradient = true
@ -774,9 +779,9 @@ private class VoiceChatActionButtonBackgroundNode: ASDisplayNode {
}
}
if let clearInside = clearInside {
if let transition = clearInsideTransition {
context.setFillColor(greyColor.cgColor)
context.fillEllipse(in: buttonRect.insetBy(dx: clearInside * radius, dy: clearInside * radius))
context.fillEllipse(in: buttonRect.insetBy(dx: transition * radius, dy: transition * radius))
}
}
}

View File

@ -89,7 +89,7 @@ private final class VoiceChatControllerTitleView: UIView {
}
public final class VoiceChatController: ViewController {
private final class Node: ViewControllerTracingNode {
private final class Node: ViewControllerTracingNode, UIGestureRecognizerDelegate {
private struct ListTransition {
let deletions: [ListViewDeleteItem]
let insertions: [ListViewInsertItem]
@ -412,7 +412,7 @@ public final class VoiceChatController: ViewController {
}
}
if let callState = strongSelf.callState, (callState.canManageCall && !callState.adminIds.contains(strongSelf.context.account.peerId)) {
if let callState = strongSelf.callState, (callState.canManageCall && !callState.adminIds.contains(peer.id)) {
items.append(.action(ContextMenuActionItem(text: strongSelf.presentationData.strings.VoiceChat_RemovePeer, textColor: .destructive, icon: { theme in
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Clear"), color: theme.actionSheet.destructiveActionTextColor)
}, action: { [weak self] _, f in
@ -523,15 +523,19 @@ public final class VoiceChatController: ViewController {
return
}
if let peer = peerViewMainPeer(view), let channel = peer as? TelegramChannel {
if !(channel.addressName ?? "").isEmpty || (channel.flags.contains(.isCreator) || channel.hasPermission(.inviteMembers)) {
strongSelf.optionsButton.isHidden = false
} else {
strongSelf.optionsButton.isHidden = true
}
}
if !strongSelf.didSetDataReady {
if let peer = peerViewMainPeer(view), let channel = peer as? TelegramChannel {
let addressName = channel.addressName ?? ""
if !addressName.isEmpty || (channel.flags.contains(.isCreator) || channel.hasPermission(.inviteMembers)) {
if addressName.isEmpty {
let _ = ensuredExistingPeerExportedInvitation(account: strongSelf.context.account, peerId: call.peerId).start()
}
} else {
strongSelf.optionsButton.isUserInteractionEnabled = false
strongSelf.optionsButton.alpha = 0.0
}
}
strongSelf.didSetDataReady = true
strongSelf.controller?.dataReady.set(true)
}
@ -675,8 +679,6 @@ public final class VoiceChatController: ViewController {
optionsButtonItem.target = self
optionsButtonItem.action = #selector(self.rightNavigationButtonAction)
self.controller?.navigationItem.setRightBarButton(optionsButtonItem, animated: false)
let _ = ensuredExistingPeerExportedInvitation(account: self.context.account, peerId: call.peerId).start()
}
deinit {
@ -701,6 +703,7 @@ public final class VoiceChatController: ViewController {
let longTapRecognizer = UILongPressGestureRecognizer(target: self, action: #selector(self.actionButtonPressGesture(_:)))
longTapRecognizer.minimumPressDuration = 0.001
longTapRecognizer.delegate = self
self.actionButton.view.addGestureRecognizer(longTapRecognizer)
let panRecognizer = CallPanGestureRecognizer(target: self, action: #selector(self.panGesture(_:)))
@ -714,7 +717,9 @@ public final class VoiceChatController: ViewController {
}
@objc private func rightNavigationButtonAction() {
self.optionsButton.contextAction?(self.optionsButton.containerNode, nil)
if self.optionsButton.isUserInteractionEnabled {
self.optionsButton.contextAction?(self.optionsButton.containerNode, nil)
}
}
@objc private func leavePressed() {
@ -726,6 +731,14 @@ public final class VoiceChatController: ViewController {
private var actionButtonPressGestureStartTime: Double = 0.0
override func gestureRecognizerShouldBegin(_ gestureRecognizer: UIGestureRecognizer) -> Bool {
if let callState = self.callState, case .connected = callState.networkState, let muteState = callState.muteState, !muteState.canUnmute {
return false
} else {
return true
}
}
@objc private func actionButtonPressGesture(_ gestureRecognizer: UILongPressGestureRecognizer) {
guard let callState = self.callState else {
return
@ -1010,7 +1023,7 @@ public final class VoiceChatController: ViewController {
}
}
if let (backgroundView, foregroundView) = sourcePanel.rightButtonSnapshotViews(), !self.optionsButton.isHidden {
if let (backgroundView, foregroundView) = sourcePanel.rightButtonSnapshotViews(), self.optionsButton.isUserInteractionEnabled {
self.view.addSubview(backgroundView)
self.view.addSubview(foregroundView)
@ -1190,6 +1203,7 @@ public final class VoiceChatController: ViewController {
memberState = .speaking
} else {
memberState = .listening
memberMuteState = member.muteState
}
} else {
memberState = speakingPeers.contains(member.peer.id) ? .speaking : .listening

View File

@ -14,14 +14,14 @@ func optionsButtonImage() -> UIImage? {
})
}
final class VoiceChatOptionsButton: HighlightableButtonNode {
final class VoiceChatOptionsButton: ASDisplayNode {
let extractedContainerNode: ContextExtractedContentContainingNode
let containerNode: ContextControllerSourceNode
private let iconNode: ASImageNode
var contextAction: ((ASDisplayNode, ContextGesture?) -> Void)?
init() {
override init() {
self.extractedContainerNode = ContextExtractedContentContainingNode()
self.containerNode = ContextControllerSourceNode()
self.containerNode.isGestureEnabled = false

View File

@ -379,8 +379,8 @@ public class VoiceChatParticipantItemNode: ItemListRevealOptionsItemNode {
let verticalOffset: CGFloat = 0.0
let avatarSize: CGFloat = 40.0
let (titleLayout, titleApply) = makeTitleLayout(TextNodeLayoutArguments(attributedString: titleAttributedString, backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: params.width - leftInset - 12.0 - rightInset - 25.0, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets()))
let (statusLayout, statusApply) = makeStatusLayout(TextNodeLayoutArguments(attributedString: statusAttributedString, backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: params.width - leftInset - 8.0 - rightInset - 25.0, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets()))
let (titleLayout, titleApply) = makeTitleLayout(TextNodeLayoutArguments(attributedString: titleAttributedString, backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: params.width - leftInset - 12.0 - rightInset - 30.0, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets()))
let (statusLayout, statusApply) = makeStatusLayout(TextNodeLayoutArguments(attributedString: statusAttributedString, backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: params.width - leftInset - 8.0 - rightInset - 30.0, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets()))
let insets = UIEdgeInsets()
@ -622,11 +622,16 @@ public class VoiceChatParticipantItemNode: ItemListRevealOptionsItemNode {
animationNode = VoiceChatMicrophoneNode()
strongSelf.animationNode = animationNode
strongSelf.actionButtonNode.addSubnode(animationNode)
if let _ = strongSelf.iconNode {
animationNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2)
animationNode.layer.animateScale(from: 0.001, to: 1.0, duration: 0.2)
}
}
animationNode.update(state: VoiceChatMicrophoneNode.State(muted: muted, color: color), animated: true)
strongSelf.actionButtonNode.isUserInteractionEnabled = false
} else if let animationNode = strongSelf.animationNode {
strongSelf.animationNode = nil
animationNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false)
animationNode.layer.animateScale(from: 1.0, to: 0.001, duration: 0.2, removeOnCompletion: false, completion: { [weak animationNode] _ in
animationNode?.removeFromSupernode()
})
@ -641,6 +646,11 @@ public class VoiceChatParticipantItemNode: ItemListRevealOptionsItemNode {
iconNode.contentMode = .center
strongSelf.iconNode = iconNode
strongSelf.actionButtonNode.addSubnode(iconNode)
if let _ = strongSelf.animationNode {
iconNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2)
iconNode.layer.animateScale(from: 0.001, to: 1.0, duration: 0.2)
}
}
if invited {
@ -651,6 +661,7 @@ public class VoiceChatParticipantItemNode: ItemListRevealOptionsItemNode {
strongSelf.actionButtonNode.isUserInteractionEnabled = !invited
} else if let iconNode = strongSelf.iconNode {
strongSelf.iconNode = nil
iconNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false)
iconNode.layer.animateScale(from: 1.0, to: 0.001, duration: 0.2, removeOnCompletion: false, completion: { [weak iconNode] _ in
iconNode?.removeFromSupernode()
})

View File

@ -496,37 +496,35 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
break
}
}
case let .groupPhoneCall(callId, accessHash, duration):
case let .groupPhoneCall(callId, accessHash, nil), let .inviteToGroupPhoneCall(callId, accessHash, _):
let peerId = message.id.peerId
if duration == nil {
let callResult = strongSelf.context.sharedContext.callManager?.joinGroupCall(context: strongSelf.context, peerId: peerId, initialCall: CachedChannelData.ActiveCall(id: callId, accessHash: accessHash), endCurrentIfAny: false, sourcePanel: nil)
if let callResult = callResult, case let .alreadyInProgress(currentPeerId) = callResult {
if currentPeerId == peerId {
strongSelf.context.sharedContext.navigateToCurrentCall(sourcePanel: nil)
} else {
let presentationData = strongSelf.context.sharedContext.currentPresentationData.with { $0 }
let _ = (strongSelf.context.account.postbox.transaction { transaction -> (Peer?, Peer?) in
return (transaction.getPeer(peerId), currentPeerId.flatMap(transaction.getPeer))
}
|> deliverOnMainQueue).start(next: { [weak self] peer, current in
guard let strongSelf = self else {
return
}
guard let peer = peer else {
return
}
if let current = current {
strongSelf.present(textAlertController(context: strongSelf.context, title: presentationData.strings.Call_CallInProgressTitle, text: presentationData.strings.Call_CallInProgressMessage(current.compactDisplayTitle, peer.compactDisplayTitle).0, actions: [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_Cancel, action: {}), TextAlertAction(type: .genericAction, title: presentationData.strings.Common_OK, action: {
if let strongSelf = self {
let _ = strongSelf.context.sharedContext.callManager?.joinGroupCall(context: strongSelf.context, peerId: peerId, initialCall: CachedChannelData.ActiveCall(id: callId, accessHash: accessHash), endCurrentIfAny: true, sourcePanel: nil)
}
})]), in: .window(.root))
} else {
strongSelf.present(textAlertController(context: strongSelf.context, title: presentationData.strings.Call_CallInProgressTitle, text: presentationData.strings.Call_ExternalCallInProgressMessage, actions: [TextAlertAction(type: .genericAction, title: presentationData.strings.Common_OK, action: {
})]), in: .window(.root))
}
})
let callResult = strongSelf.context.sharedContext.callManager?.joinGroupCall(context: strongSelf.context, peerId: peerId, initialCall: CachedChannelData.ActiveCall(id: callId, accessHash: accessHash), endCurrentIfAny: false, sourcePanel: nil)
if let callResult = callResult, case let .alreadyInProgress(currentPeerId) = callResult {
if currentPeerId == peerId {
strongSelf.context.sharedContext.navigateToCurrentCall(sourcePanel: nil)
} else {
let presentationData = strongSelf.context.sharedContext.currentPresentationData.with { $0 }
let _ = (strongSelf.context.account.postbox.transaction { transaction -> (Peer?, Peer?) in
return (transaction.getPeer(peerId), currentPeerId.flatMap(transaction.getPeer))
}
|> deliverOnMainQueue).start(next: { [weak self] peer, current in
guard let strongSelf = self else {
return
}
guard let peer = peer else {
return
}
if let current = current {
strongSelf.present(textAlertController(context: strongSelf.context, title: presentationData.strings.Call_CallInProgressTitle, text: presentationData.strings.Call_CallInProgressMessage(current.compactDisplayTitle, peer.compactDisplayTitle).0, actions: [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_Cancel, action: {}), TextAlertAction(type: .genericAction, title: presentationData.strings.Common_OK, action: {
if let strongSelf = self {
let _ = strongSelf.context.sharedContext.callManager?.joinGroupCall(context: strongSelf.context, peerId: peerId, initialCall: CachedChannelData.ActiveCall(id: callId, accessHash: accessHash), endCurrentIfAny: true, sourcePanel: nil)
}
})]), in: .window(.root))
} else {
strongSelf.present(textAlertController(context: strongSelf.context, title: presentationData.strings.Call_CallInProgressTitle, text: presentationData.strings.Call_ExternalCallInProgressMessage, actions: [TextAlertAction(type: .genericAction, title: presentationData.strings.Common_OK, action: {
})]), in: .window(.root))
}
})
}
}
break