diff --git a/submodules/Display/Source/ListViewAnimation.swift b/submodules/Display/Source/ListViewAnimation.swift index a44f801c7e..aed2d242c3 100644 --- a/submodules/Display/Source/ListViewAnimation.swift +++ b/submodules/Display/Source/ListViewAnimation.swift @@ -93,6 +93,10 @@ public let listViewAnimationCurveLinear: (CGFloat) -> CGFloat = { t in return t } +public let listViewAnimationCurveEaseInOut: (CGFloat) -> CGFloat = { t in + return bezierPoint(0.42, 0.0, 0.58, 1.0, t) +} + #if os(iOS) public func listViewAnimationCurveFromAnimationOptions(animationOptions: UIView.AnimationOptions) -> (CGFloat) -> CGFloat { if animationOptions.rawValue == UInt(7 << 16) { diff --git a/submodules/Display/Source/ListViewItemNode.swift b/submodules/Display/Source/ListViewItemNode.swift index 38d8c87381..3d055c9870 100644 --- a/submodules/Display/Source/ListViewItemNode.swift +++ b/submodules/Display/Source/ListViewItemNode.swift @@ -119,6 +119,10 @@ open class ListViewItemNode: ASDisplayNode, AccessibilityFocusableNode { final let wantsScrollDynamics: Bool + open var preferredAnimationCurve: (CGFloat) -> CGFloat { + return listViewAnimationCurveSystem + } + public final var wantsTrailingItemSpaceUpdates: Bool = false public final var scrollPositioningInsets: UIEdgeInsets = UIEdgeInsets() @@ -427,7 +431,7 @@ open class ListViewItemNode: ASDisplayNode, AccessibilityFocusableNode { } public func addHeightAnimation(_ value: CGFloat, duration: Double, beginAt: Double, update: ((CGFloat, CGFloat) -> Void)? = nil) { - let animation = ListViewAnimation(from: self.bounds.height, to: value, duration: duration, curve: listViewAnimationCurveSystem, beginAt: beginAt, update: { [weak self] progress, currentValue in + let animation = ListViewAnimation(from: self.bounds.height, to: value, duration: duration, curve: self.preferredAnimationCurve, beginAt: beginAt, update: { [weak self] progress, currentValue in if let strongSelf = self { let frame = strongSelf.frame strongSelf.frame = CGRect(origin: frame.origin, size: CGSize(width: frame.width, height: currentValue)) @@ -461,7 +465,7 @@ open class ListViewItemNode: ASDisplayNode, AccessibilityFocusableNode { } public func addApparentHeightAnimation(_ value: CGFloat, duration: Double, beginAt: Double, update: ((CGFloat, CGFloat) -> Void)? = nil) { - let animation = ListViewAnimation(from: self.apparentHeight, to: value, duration: duration, curve: listViewAnimationCurveSystem, beginAt: beginAt, update: { [weak self] progress, currentValue in + let animation = ListViewAnimation(from: self.apparentHeight, to: value, duration: duration, curve: self.preferredAnimationCurve, beginAt: beginAt, update: { [weak self] progress, currentValue in if let strongSelf = self { strongSelf.apparentHeight = currentValue if let update = update { @@ -494,7 +498,7 @@ open class ListViewItemNode: ASDisplayNode, AccessibilityFocusableNode { } public func addTransitionOffsetAnimation(_ value: CGFloat, duration: Double, beginAt: Double) { - let animation = ListViewAnimation(from: self.transitionOffset, to: value, duration: duration, curve: listViewAnimationCurveSystem, beginAt: beginAt, update: { [weak self] _, currentValue in + let animation = ListViewAnimation(from: self.transitionOffset, to: value, duration: duration, curve: self.preferredAnimationCurve, beginAt: beginAt, update: { [weak self] _, currentValue in if let strongSelf = self { strongSelf.transitionOffset = currentValue } diff --git a/submodules/TelegramCallsUI/Sources/CallStatusBarNode.swift b/submodules/TelegramCallsUI/Sources/CallStatusBarNode.swift index 9ea9b563d6..d0edd71868 100644 --- a/submodules/TelegramCallsUI/Sources/CallStatusBarNode.swift +++ b/submodules/TelegramCallsUI/Sources/CallStatusBarNode.swift @@ -14,6 +14,7 @@ import AnimatedCountLabelNode private let blue = UIColor(rgb: 0x0078ff) private let lightBlue = UIColor(rgb: 0x59c7f8) private let green = UIColor(rgb: 0x33c659) +private let activeBlue = UIColor(rgb: 0x00a0b9) private class CallStatusBarBackgroundNode: ASDisplayNode { private let foregroundView: UIView @@ -46,7 +47,7 @@ private class CallStatusBarBackgroundNode: ASDisplayNode { let initialColors = self.foregroundGradientLayer.colors let targetColors: [CGColor] if let speaking = self.speaking { - targetColors = speaking ? [green.cgColor, blue.cgColor] : [blue.cgColor, lightBlue.cgColor] + targetColors = speaking ? [green.cgColor, activeBlue.cgColor] : [blue.cgColor, lightBlue.cgColor] } else { targetColors = [connectingColor.cgColor, connectingColor.cgColor] } @@ -159,6 +160,7 @@ public class CallStatusBarNodeImpl: CallStatusBarNode { private let backgroundNode: CallStatusBarBackgroundNode private let titleNode: ImmediateTextNode private let subtitleNode: ImmediateAnimatedCountLabelNode + private let speakerNode: ImmediateTextNode private let audioLevelDisposable = MetaDisposable() private let stateDisposable = MetaDisposable() @@ -175,6 +177,7 @@ public class CallStatusBarNodeImpl: CallStatusBarNode { private var currentCallState: PresentationCallState? private var currentGroupCallState: PresentationGroupCallSummaryState? private var currentIsMuted = true + private var currentMembers: PresentationGroupCallMembers? private var currentIsConnected = true public override init() { @@ -182,12 +185,14 @@ public class CallStatusBarNodeImpl: CallStatusBarNode { self.titleNode = ImmediateTextNode() self.subtitleNode = ImmediateAnimatedCountLabelNode() self.subtitleNode.reverseAnimationDirection = true + self.speakerNode = ImmediateTextNode() super.init() self.addSubnode(self.backgroundNode) self.addSubnode(self.titleNode) self.addSubnode(self.subtitleNode) + self.addSubnode(self.speakerNode) } deinit { @@ -257,12 +262,14 @@ public class CallStatusBarNodeImpl: CallStatusBarNode { (combineLatest( account.postbox.peerView(id: call.peerId), call.summaryState, - call.isMuted + call.isMuted, + call.members ) - |> deliverOnMainQueue).start(next: { [weak self] view, state, isMuted in + |> deliverOnMainQueue).start(next: { [weak self] view, state, isMuted, members in if let strongSelf = self { strongSelf.currentPeer = view.peers[view.peerId] strongSelf.currentGroupCallState = state + strongSelf.currentMembers = members var isMuted = isMuted if let state = state, let muteState = state.callState.muteState { @@ -289,17 +296,18 @@ public class CallStatusBarNodeImpl: CallStatusBarNode { return } var effectiveLevel: Float = 0.0 + var audioLevels = audioLevels if !strongSelf.currentIsMuted { - effectiveLevel = myAudioLevel - } else { - effectiveLevel = audioLevels.map { $0.1 }.max() ?? 0.0 + audioLevels.append((PeerId(0), myAudioLevel, true)) } + effectiveLevel = audioLevels.map { $0.1 }.max() ?? 0.0 strongSelf.backgroundNode.audioLevel = effectiveLevel })) } } var title: String = "" + var speakerSubtitle: String = "" let textFont = Font.regular(13.0) let textColor = UIColor.white @@ -316,6 +324,21 @@ public class CallStatusBarNodeImpl: CallStatusBarNode { membersCount = 1 } + var speakingPeer: Peer? + if let members = currentMembers { + var speakingPeers: [Peer] = [] + for member in members.participants { + if members.speakingParticipants.contains(member.peer.id) { + speakingPeers.append(member.peer) + } + } + speakingPeer = speakingPeers.first + } + + if let speakingPeer = speakingPeer { + speakerSubtitle = speakingPeer.displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder) + } + if let membersCount = membersCount { var membersPart = presentationData.strings.VoiceChat_Status_Members(membersCount) if let startIndex = membersPart.firstIndex(of: "["), let endIndex = membersPart.firstIndex(of: "]") { @@ -366,15 +389,24 @@ public class CallStatusBarNodeImpl: CallStatusBarNode { self.backgroundNode.connectingColor = color } - if self.subtitleNode.segments != segments { + if self.subtitleNode.segments != segments && speakerSubtitle.isEmpty { self.subtitleNode.segments = segments } + let alphaTransition: ContainedViewLayoutTransition = .animated(duration: 0.2, curve: .easeInOut) + alphaTransition.updateAlpha(node: self.subtitleNode, alpha: !speakerSubtitle.isEmpty ? 0.0 : 1.0) + alphaTransition.updateAlpha(node: self.speakerNode, alpha: !speakerSubtitle.isEmpty ? 1.0 : 0.0) + self.titleNode.attributedText = NSAttributedString(string: title, font: Font.semibold(13.0), textColor: .white) - + + if !speakerSubtitle.isEmpty { + self.speakerNode.attributedText = NSAttributedString(string: speakerSubtitle, font: Font.regular(13.0), textColor: .white) + } + let spacing: CGFloat = 5.0 let titleSize = self.titleNode.updateLayout(CGSize(width: 160.0, height: size.height)) let subtitleSize = self.subtitleNode.updateLayout(size: CGSize(width: 160.0, height: size.height), animated: true) + let speakerSize = self.speakerNode.updateLayout(CGSize(width: 160.0, height: size.height)) let totalWidth = titleSize.width + spacing + subtitleSize.width let horizontalOrigin: CGFloat = floor((size.width - totalWidth) / 2.0) @@ -386,6 +418,10 @@ public class CallStatusBarNodeImpl: CallStatusBarNode { transition.updateFrame(node: self.titleNode, frame: CGRect(origin: CGPoint(x: horizontalOrigin, y: verticalOrigin + floor((contentHeight - titleSize.height) / 2.0)), size: titleSize)) transition.updateFrame(node: self.subtitleNode, frame: CGRect(origin: CGPoint(x: horizontalOrigin + titleSize.width + spacing, y: verticalOrigin + floor((contentHeight - subtitleSize.height) / 2.0)), size: subtitleSize)) + if !speakerSubtitle.isEmpty { + self.speakerNode.frame = CGRect(origin: CGPoint(x: horizontalOrigin + titleSize.width + spacing, y: verticalOrigin + floor((contentHeight - speakerSize.height) / 2.0)), size: speakerSize) + } + self.backgroundNode.speaking = self.currentIsConnected ? !self.currentIsMuted : nil self.backgroundNode.frame = CGRect(origin: CGPoint(), size: CGSize(width: size.width, height: size.height + 18.0)) } diff --git a/submodules/TelegramCallsUI/Sources/VoiceChatActionButton.swift b/submodules/TelegramCallsUI/Sources/VoiceChatActionButton.swift index 8a9a447a92..7ae3c89c7e 100644 --- a/submodules/TelegramCallsUI/Sources/VoiceChatActionButton.swift +++ b/submodules/TelegramCallsUI/Sources/VoiceChatActionButton.swift @@ -14,6 +14,7 @@ private let secondaryGreyColor = UIColor(rgb: 0x1c1c1e) private let blue = UIColor(rgb: 0x0078ff) private let lightBlue = UIColor(rgb: 0x59c7f8) private let green = UIColor(rgb: 0x33c659) +private let activeBlue = UIColor(rgb: 0x00a0b9) private let areaSize = CGSize(width: 440.0, height: 440.0) private let blobSize = CGSize(width: 244.0, height: 244.0) @@ -626,7 +627,7 @@ private final class VoiceChatActionButtonBackgroundNode: ASDisplayNode { let targetScale: CGFloat if let active = active { if active { - targetColors = [blue.cgColor, green.cgColor] + targetColors = [activeBlue.cgColor, green.cgColor] targetScale = 0.89 outerColor = UIColor(rgb: 0x21674f) } else { diff --git a/submodules/TelegramCallsUI/Sources/VoiceChatActionItem.swift b/submodules/TelegramCallsUI/Sources/VoiceChatActionItem.swift index 72a41f2b64..5519f41c0e 100644 --- a/submodules/TelegramCallsUI/Sources/VoiceChatActionItem.swift +++ b/submodules/TelegramCallsUI/Sources/VoiceChatActionItem.swift @@ -257,4 +257,8 @@ class VoiceChatActionItemNode: ListViewItemNode { override public func header() -> ListViewItemHeader? { return nil } + + override var preferredAnimationCurve: (CGFloat) -> CGFloat { + return listViewAnimationCurveEaseInOut + } } diff --git a/submodules/TelegramCallsUI/Sources/VoiceChatController.swift b/submodules/TelegramCallsUI/Sources/VoiceChatController.swift index 4d1f94caf4..adfc9ac56b 100644 --- a/submodules/TelegramCallsUI/Sources/VoiceChatController.swift +++ b/submodules/TelegramCallsUI/Sources/VoiceChatController.swift @@ -128,6 +128,7 @@ public final class VoiceChatController: ViewController { let isEmpty: Bool let crossFade: Bool let count: Int + let isExpanded: Bool let animated: Bool } @@ -362,14 +363,14 @@ public final class VoiceChatController: ViewController { } } - private func preparedTransition(from fromEntries: [ListEntry], to toEntries: [ListEntry], isLoading: Bool, isEmpty: Bool, crossFade: Bool, context: AccountContext, presentationData: PresentationData, interaction: Interaction) -> ListTransition { + private func preparedTransition(from fromEntries: [ListEntry], to toEntries: [ListEntry], isLoading: Bool, isEmpty: Bool, crossFade: Bool, context: AccountContext, presentationData: PresentationData, interaction: Interaction, isExpanded: Bool) -> ListTransition { let (deleteIndices, indicesAndItems, updateIndices) = mergeListsStableWithUpdates(leftList: fromEntries, rightList: toEntries) let deletions = deleteIndices.map { ListViewDeleteItem(index: $0, directionHint: nil) } let insertions = indicesAndItems.map { ListViewInsertItem(index: $0.0, previousIndex: $0.2, item: $0.1.item(context: context, presentationData: presentationData, interaction: interaction), directionHint: nil) } let updates = updateIndices.map { ListViewUpdateItem(index: $0.0, previousIndex: $0.2, item: $0.1.item(context: context, presentationData: presentationData, interaction: interaction), directionHint: nil) } - return ListTransition(deletions: deletions, insertions: insertions, updates: updates, isLoading: isLoading, isEmpty: isEmpty, crossFade: crossFade, count: toEntries.count, animated: fromEntries.count != toEntries.count) + return ListTransition(deletions: deletions, insertions: insertions, updates: updates, isLoading: isLoading, isEmpty: isEmpty, crossFade: crossFade, count: toEntries.count, isExpanded: isExpanded, animated: true) } private weak var controller: VoiceChatController? @@ -413,6 +414,9 @@ public final class VoiceChatController: ViewController { private var currentCallMembers: [GroupCallParticipantsContext.Participant]? private var currentInvitedPeers: [Peer]? private var currentSpeakingPeers: Set? + private var currentIsExpanded: Bool = false + private var currentContentOffset: CGFloat? + private var ignoreScrolling = false private var accountPeer: Peer? private var currentAudioButtonColor: UIColor? @@ -467,12 +471,11 @@ public final class VoiceChatController: ViewController { self.backgroundNode = ASDisplayNode() self.backgroundNode.backgroundColor = secondaryPanelBackgroundColor + self.backgroundNode.clipsToBounds = false self.listNode = ListView() self.listNode.verticalScrollIndicatorColor = UIColor(white: 1.0, alpha: 0.3) self.listNode.clipsToBounds = true -// self.listNode.stackFromBottom = true -// self.listNode.keepMinimalScrollHeightWithTopInset = 0 self.topPanelNode = ASDisplayNode() self.topPanelNode.clipsToBounds = false @@ -483,8 +486,11 @@ public final class VoiceChatController: ViewController { self.topPanelEdgeNode = ASDisplayNode() self.topPanelEdgeNode.backgroundColor = panelBackgroundColor - self.topPanelEdgeNode.layer.cornerRadius = 12.0 + self.topPanelEdgeNode.cornerRadius = 12.0 self.topPanelEdgeNode.isUserInteractionEnabled = false + if #available(iOS 11.0, *) { + self.topPanelEdgeNode.layer.maskedCorners = [.layerMinXMinYCorner, .layerMaxXMinYCorner] + } self.optionsButton = VoiceChatHeaderButton() self.optionsButton.setImage(optionsButtonImage(dark: false)) @@ -517,10 +523,12 @@ public final class VoiceChatController: ViewController { self.leftBorderNode = ASDisplayNode() self.leftBorderNode.backgroundColor = panelBackgroundColor self.leftBorderNode.isUserInteractionEnabled = false + self.leftBorderNode.clipsToBounds = false self.rightBorderNode = ASDisplayNode() self.rightBorderNode.backgroundColor = panelBackgroundColor self.rightBorderNode.isUserInteractionEnabled = false + self.rightBorderNode.clipsToBounds = false super.init() @@ -878,7 +886,7 @@ public final class VoiceChatController: ViewController { } } - strongSelf.updateMembers(muteState: strongSelf.effectiveMuteState, callMembers: callMembers?.participants ?? [], invitedPeers: invitedPeers, speakingPeers: callMembers?.speakingParticipants ?? []) + strongSelf.updateMembers(muteState: strongSelf.effectiveMuteState, callMembers: callMembers?.participants ?? [], invitedPeers: invitedPeers, speakingPeers: callMembers?.speakingParticipants ?? [], isExpanded: strongSelf.currentIsExpanded) let subtitle = strongSelf.presentationData.strings.VoiceChat_Panel_Members(Int32(max(1, callMembers?.totalCount ?? 0))) strongSelf.currentSubtitle = subtitle @@ -899,7 +907,7 @@ public final class VoiceChatController: ViewController { } if !strongSelf.didSetDataReady { strongSelf.accountPeer = accountPeer - strongSelf.updateMembers(muteState: strongSelf.effectiveMuteState, callMembers: strongSelf.currentCallMembers ?? [], invitedPeers: strongSelf.currentInvitedPeers ?? [], speakingPeers: strongSelf.currentSpeakingPeers ?? Set()) + strongSelf.updateMembers(muteState: strongSelf.effectiveMuteState, callMembers: strongSelf.currentCallMembers ?? [], invitedPeers: strongSelf.currentInvitedPeers ?? [], speakingPeers: strongSelf.currentSpeakingPeers ?? Set(), isExpanded: strongSelf.currentIsExpanded) if let peer = peerViewMainPeer(view), let channel = peer as? TelegramChannel { let addressName = channel.addressName ?? "" @@ -1091,6 +1099,7 @@ public final class VoiceChatController: ViewController { self.listNode.updateFloatingHeaderOffset = { [weak self] offset, transition in if let strongSelf = self { + strongSelf.currentContentOffset = offset strongSelf.updateFloatingHeaderOffset(offset: offset, transition: transition) } } @@ -1099,10 +1108,27 @@ public final class VoiceChatController: ViewController { guard let strongSelf = self else { return } - switch strongSelf.listNode.visibleContentOffset() { + if strongSelf.ignoreScrolling { + Queue.mainQueue().after(0.5) { + strongSelf.ignoreScrolling = false + } + } + } + + self.listNode.visibleContentOffsetChanged = { [weak self] offset in + guard let strongSelf = self else { + return + } + switch offset { case let .known(value): - if value <= -10.0 { -// strongSelf.controller?.dismiss() +// strongSelf.updateFloatingHeaderOffset(offset: -value + strongSelf.listNode.insets.top, transition: strongSelf.listNode.isTracking ? .immediate : .animated(duration: 0.4, curve: .linear)) + + if value > 5, !strongSelf.currentIsExpanded && strongSelf.listNode.isTracking && !strongSelf.ignoreScrolling { + strongSelf.updateMembers(muteState: strongSelf.effectiveMuteState, callMembers:strongSelf.currentCallMembers ?? [], invitedPeers: strongSelf.currentInvitedPeers ?? [], speakingPeers: strongSelf.currentSpeakingPeers ?? Set(), isExpanded: true) + strongSelf.ignoreScrolling = true + } else if value < -5, strongSelf.currentIsExpanded && strongSelf.listNode.isTracking && !strongSelf.ignoreScrolling { + strongSelf.updateMembers(muteState: strongSelf.effectiveMuteState, callMembers: strongSelf.currentCallMembers ?? [], invitedPeers: strongSelf.currentInvitedPeers ?? [], speakingPeers: strongSelf.currentSpeakingPeers ?? [], isExpanded: false) + strongSelf.ignoreScrolling = true } default: break @@ -1197,7 +1223,7 @@ public final class VoiceChatController: ViewController { self.containerLayoutUpdated(layout, navigationHeight: navigationHeight, transition: .animated(duration: 0.3, curve: .spring)) } - self.updateMembers(muteState: self.effectiveMuteState, callMembers: self.currentCallMembers ?? [], invitedPeers: self.currentInvitedPeers ?? [], speakingPeers: self.currentSpeakingPeers ?? Set()) + self.updateMembers(muteState: self.effectiveMuteState, callMembers: self.currentCallMembers ?? [], invitedPeers: self.currentInvitedPeers ?? [], speakingPeers: self.currentSpeakingPeers ?? Set(), isExpanded: self.currentIsExpanded) } @objc private func actionButtonPressGesture(_ gestureRecognizer: UILongPressGestureRecognizer) { @@ -1242,7 +1268,7 @@ public final class VoiceChatController: ViewController { if let (layout, navigationHeight) = self.validLayout { self.containerLayoutUpdated(layout, navigationHeight: navigationHeight, transition: .animated(duration: 0.3, curve: .spring)) } - self.updateMembers(muteState: self.effectiveMuteState, callMembers: self.currentCallMembers ?? [], invitedPeers: self.currentInvitedPeers ?? [], speakingPeers: self.currentSpeakingPeers ?? Set()) + self.updateMembers(muteState: self.effectiveMuteState, callMembers: self.currentCallMembers ?? [], invitedPeers: self.currentInvitedPeers ?? [], speakingPeers: self.currentSpeakingPeers ?? Set(), isExpanded: self.currentIsExpanded) default: break } @@ -1316,82 +1342,82 @@ public final class VoiceChatController: ViewController { } private func updateFloatingHeaderOffset(offset: CGFloat, transition: ContainedViewLayoutTransition) { - guard let (validLayout, _) = self.validLayout else { + guard let (layout, _) = self.validLayout else { return } - - self.floatingHeaderOffset = offset - - let layoutTopInset: CGFloat = max(validLayout.statusBarHeight ?? 0.0, validLayout.safeInsets.top) - + + let layoutTopInset: CGFloat = max(layout.statusBarHeight ?? 0.0, layout.safeInsets.top) let topPanelHeight: CGFloat = 63.0 let listTopInset = layoutTopInset + topPanelHeight + let bottomAreaHeight: CGFloat = 268.0 + let bottomPanelHeight = bottomAreaHeight + layout.intrinsicInsets.bottom + let listSize = CGSize(width: layout.size.width, height: layout.size.height - listTopInset - bottomPanelHeight) + let topInset = self.topInset ?? listSize.height + var offset = offset + topInset + self.floatingHeaderOffset = offset + let rawPanelOffset = offset + listTopInset - topPanelHeight let panelOffset = max(layoutTopInset, rawPanelOffset) - let topPanelFrame = CGRect(origin: CGPoint(x: 0.0, y: panelOffset), size: CGSize(width: validLayout.size.width, height: topPanelHeight)) + let topPanelFrame = CGRect(origin: CGPoint(x: 0.0, y: panelOffset), size: CGSize(width: layout.size.width, height: topPanelHeight)) + + let backgroundFrame = CGRect(origin: CGPoint(x: 0.0, y: topPanelFrame.maxY), size: CGSize(width: layout.size.width, height: layout.size.height)) + let sideInset: CGFloat = 16.0 + let leftBorderFrame = CGRect(origin: CGPoint(x: 0.0, y: topPanelFrame.maxY - 16.0), size: CGSize(width: sideInset, height: layout.size.height)) + let rightBorderFrame = CGRect(origin: CGPoint(x: layout.size.width - sideInset, y: topPanelFrame.maxY - 16.0), size: CGSize(width: sideInset, height: layout.size.height)) let previousTopPanelFrame = self.topPanelNode.frame + let previousBackgroundFrame = self.backgroundNode.frame + let previousLeftBorderFrame = self.leftBorderNode.frame + let previousRightBorderFrame = self.rightBorderNode.frame + if !topPanelFrame.equalTo(previousTopPanelFrame) { self.topPanelNode.frame = topPanelFrame - - let positionDelta = CGPoint(x: topPanelFrame.minX - previousTopPanelFrame.minX, y: topPanelFrame.minY - previousTopPanelFrame.minY) + let positionDelta = CGPoint(x: 0.0, y: topPanelFrame.minY - previousTopPanelFrame.minY) transition.animateOffsetAdditive(node: self.topPanelNode, offset: positionDelta.y) + + self.backgroundNode.frame = backgroundFrame + let backgroundPositionDelta = CGPoint(x: 0.0, y: previousBackgroundFrame.minY - backgroundFrame.minY) + transition.animatePositionAdditive(node: self.backgroundNode, offset: backgroundPositionDelta) + + self.leftBorderNode.frame = leftBorderFrame + let leftBorderPositionDelta = CGPoint(x: 0.0, y: previousLeftBorderFrame.minY - leftBorderFrame.minY) + transition.animatePositionAdditive(node: self.leftBorderNode, offset: leftBorderPositionDelta) + + self.rightBorderNode.frame = rightBorderFrame + let rightBorderPositionDelta = CGPoint(x: 0.0, y: previousRightBorderFrame.minY - rightBorderFrame.minY) + transition.animatePositionAdditive(node: self.rightBorderNode, offset: rightBorderPositionDelta) } - self.topPanelBackgroundNode.frame = CGRect(x: 0.0, y: topPanelHeight - 24.0, width: validLayout.size.width, height: 24.0) - self.topPanelEdgeNode.frame = CGRect(x: 0.0, y: 0.0, width: validLayout.size.width, height: topPanelHeight) - - let backgroundFrame = CGRect(origin: CGPoint(x: 0.0, y: topPanelFrame.maxY), size: CGSize(width: validLayout.size.width, height: validLayout.size.height)) - - let sideInset: CGFloat = 16.0 - let leftBorderFrame = CGRect(origin: CGPoint(x: 0.0, y: topPanelFrame.maxY - 16.0), size: CGSize(width: sideInset, height: validLayout.size.height)) - let rightBorderFrame = CGRect(origin: CGPoint(x: validLayout.size.width - sideInset, y: topPanelFrame.maxY - 16.0), size: CGSize(width: sideInset, height: validLayout.size.height)) - + self.topPanelBackgroundNode.frame = CGRect(x: 0.0, y: topPanelHeight - 24.0, width: layout.size.width, height: 24.0) + var bottomEdge: CGFloat = 0.0 self.listNode.forEachItemNode { itemNode in if let itemNode = itemNode as? ListViewItemNode { - let convertedFrame = self.listNode.view.convert(itemNode.apparentFrame, to: self.view) + let convertedFrame = self.listNode.view.convert(itemNode.frame, to: self.view) if convertedFrame.maxY > bottomEdge { bottomEdge = convertedFrame.maxY } } } + let listMaxY = listTopInset + listSize.height + if bottomEdge.isZero { + bottomEdge = listMaxY + } var bottomOffset: CGFloat = 0.0 - if bottomEdge < self.listNode.frame.maxY { - bottomOffset = bottomEdge - self.listNode.frame.maxY + if bottomEdge < listMaxY { + bottomOffset = bottomEdge - listMaxY } - let bottomCornersFrame = CGRect(origin: CGPoint(x: sideInset, y: -50.0 + bottomOffset), size: CGSize(width: validLayout.size.width - sideInset * 2.0, height: 50.0)) + let bottomCornersFrame = CGRect(origin: CGPoint(x: sideInset, y: -50.0 + bottomOffset), size: CGSize(width: layout.size.width - sideInset * 2.0, height: 50.0)) let previousBottomCornersFrame = self.bottomCornersNode.frame if !bottomCornersFrame.equalTo(previousBottomCornersFrame) { self.bottomCornersNode.frame = bottomCornersFrame - self.bottomPanelBackgroundNode.frame = CGRect(x: 0.0, y: bottomOffset, width: validLayout.size.width, height: 2000.0) + self.bottomPanelBackgroundNode.frame = CGRect(x: 0.0, y: bottomOffset, width: layout.size.width, height: 2000.0) - let positionDelta = CGPoint(x: bottomCornersFrame.minX - previousBottomCornersFrame.minX, y: bottomCornersFrame.minY - previousBottomCornersFrame.minY) - transition.animateOffsetAdditive(node: self.bottomCornersNode, offset: positionDelta.y) - transition.animateOffsetAdditive(node: self.bottomPanelBackgroundNode, offset: positionDelta.y) - } - - let previousBackgroundFrame = self.backgroundNode.frame - let previousLeftBorderFrame = self.leftBorderNode.frame - let previousRightBorderFrame = self.rightBorderNode.frame - - self.updateColors(fullscreen: abs(panelOffset - layoutTopInset) < 1.0) - - if !backgroundFrame.equalTo(previousBackgroundFrame) { - self.backgroundNode.frame = backgroundFrame - self.leftBorderNode.frame = leftBorderFrame - self.rightBorderNode.frame = rightBorderFrame - - let backgroundPositionDelta = CGPoint(x: backgroundFrame.minX - previousBackgroundFrame.minX, y: backgroundFrame.minY - previousBackgroundFrame.minY) - transition.animateOffsetAdditive(node: self.backgroundNode, offset: backgroundPositionDelta.y) - - let leftBorderPositionDelta = CGPoint(x: leftBorderFrame.minX - previousLeftBorderFrame.minX, y: leftBorderFrame.minY - previousLeftBorderFrame.minY) - transition.animateOffsetAdditive(node: self.leftBorderNode, offset: leftBorderPositionDelta.y) - - let rightBorderPositionDelta = CGPoint(x: rightBorderFrame.minX - previousRightBorderFrame.minX, y: rightBorderFrame.minY - previousRightBorderFrame.minY) - transition.animateOffsetAdditive(node: self.rightBorderNode, offset: rightBorderPositionDelta.y) + let positionDelta = CGPoint(x: 0.0, y: previousBottomCornersFrame.minY - bottomCornersFrame.minY) + transition.animatePositionAdditive(node: self.bottomCornersNode, offset: positionDelta) + transition.animatePositionAdditive(node: self.bottomPanelBackgroundNode, offset: positionDelta) } } @@ -1406,8 +1432,22 @@ public final class VoiceChatController: ViewController { let transition: ContainedViewLayoutTransition = .animated(duration: 0.3, curve: .linear) -// transition.updateCornerRadius(node: self.topPanelEdgeNode, cornerRadius: fullscreen ? layout.deviceMetrics.screenCornerRadius : 12.0) - transition.updateBackgroundColor(node: self.dimNode, color: fullscreen ? fullscreenBackgroundColor : dimColor) + let topPanelHeight: CGFloat = 63.0 + let topEdgeFrame: CGRect + if self.isFullscreen { + let offset: CGFloat + if let statusBarHeight = layout.statusBarHeight { + offset = statusBarHeight + } else { + offset = 44.0 + } + topEdgeFrame = CGRect(x: 0.0, y: -offset, width: layout.size.width, height: topPanelHeight + offset) + } else { + topEdgeFrame = CGRect(x: 0.0, y: 0.0, width: layout.size.width, height: topPanelHeight) + } + transition.updateFrame(node: self.topPanelEdgeNode, frame: topEdgeFrame) + transition.updateCornerRadius(node: self.topPanelEdgeNode, cornerRadius: fullscreen ? layout.deviceMetrics.screenCornerRadius - 0.5 : 12.0) +// transition.updateBackgroundColor(node: self.dimNode, color: fullscreen ? fullscreenBackgroundColor : dimColor) transition.updateBackgroundColor(node: self.topPanelBackgroundNode, color: fullscreen ? fullscreenBackgroundColor : panelBackgroundColor) transition.updateBackgroundColor(node: self.topPanelEdgeNode, color: fullscreen ? fullscreenBackgroundColor : panelBackgroundColor) transition.updateBackgroundColor(node: self.backgroundNode, color: fullscreen ? panelBackgroundColor : secondaryPanelBackgroundColor) @@ -1542,13 +1582,26 @@ public final class VoiceChatController: ViewController { insets.left = layout.safeInsets.left + sideInset insets.right = layout.safeInsets.right + sideInset + let topPanelHeight: CGFloat = 63.0 + let topEdgeFrame: CGRect + if self.isFullscreen { + let offset: CGFloat + if let statusBarHeight = layout.statusBarHeight { + offset = statusBarHeight + } else { + offset = 44.0 + } + topEdgeFrame = CGRect(x: 0.0, y: -offset, width: layout.size.width, height: topPanelHeight + offset) + } else { + topEdgeFrame = CGRect(x: 0.0, y: 0.0, width: layout.size.width, height: topPanelHeight) + } + transition.updateFrame(node: self.topPanelEdgeNode, frame: topEdgeFrame) + let bottomPanelHeight = bottomAreaHeight + layout.intrinsicInsets.bottom - let listTopInset = layoutTopInset + 63.0 + let listTopInset = layoutTopInset + topPanelHeight let listSize = CGSize(width: layout.size.width, height: layout.size.height - listTopInset - bottomPanelHeight) - - insets.top = max(0.0, self.topInset ?? listSize.height) - - transition.updateFrame(node: self.listNode, frame: CGRect(origin: CGPoint(x: 0.0, y: listTopInset), size: listSize)) + + transition.updateFrame(node: self.listNode, frame: CGRect(origin: CGPoint(x: 0.0, y: listTopInset + (self.topInset ?? listSize.height)), size: listSize)) let (duration, curve) = listViewAnimationDurationAndCurve(transition: transition) let updateSizeAndInsets = ListViewUpdateSizeAndInsets(size: listSize, insets: insets, duration: duration, curve: curve) @@ -1672,7 +1725,7 @@ public final class VoiceChatController: ViewController { let topPanelFrame = self.topPanelNode.view.convert(self.topPanelNode.bounds, to: self.view) - self.contentContainer.layer.animateBoundsOriginYAdditive(from: self.contentContainer.bounds.origin.y, to: -(layout.size.height - topPanelFrame.minY), duration: 0.3, timingFunction: kCAMediaTimingFunctionSpring, removeOnCompletion: false, completion: { _ in + self.contentContainer.layer.animateBoundsOriginYAdditive(from: self.contentContainer.bounds.origin.y, to: -(layout.size.height - topPanelFrame.minY) - 44.0, duration: 0.3, timingFunction: kCAMediaTimingFunctionSpring, removeOnCompletion: false, completion: { _ in offsetCompleted = true internalCompletion() }) @@ -1698,7 +1751,9 @@ public final class VoiceChatController: ViewController { self.enqueuedTransitions.remove(at: 0) var options = ListViewDeleteAndInsertOptions() - if !isFirstTime { + if self.isFirstTime { + self.isFirstTime = false + } else { if transition.crossFade { options.insert(.AnimateCrossfade) } @@ -1709,12 +1764,6 @@ public final class VoiceChatController: ViewController { options.insert(.LowLatency) options.insert(.PreferSynchronousResourceLoading) - var scrollToItem: ListViewScrollToItem? - if self.isFirstTime { - self.isFirstTime = false -// scrollToItem = ListViewScrollToItem(index: 0, position: .bottom(0), animated: false, curve: .Default(duration: nil), directionHint: .Up) - } - var itemsHeight: CGFloat = 46.0 + CGFloat(transition.count - 1) * 56.0 let bottomAreaHeight: CGFloat = 268.0 @@ -1729,13 +1778,24 @@ public final class VoiceChatController: ViewController { let listTopInset = layoutTopInset + 63.0 let listSize = CGSize(width: layout.size.width, height: layout.size.height - listTopInset - bottomPanelHeight) - insets.top = max(0.0, max(listSize.height - itemsHeight, listSize.height - 46.0 - floor(56.0 * 3.5))) - self.topInset = insets.top + let previousIsExpanded = self.currentIsExpanded + self.currentIsExpanded = transition.isExpanded + self.topInset = max(0.0, transition.isExpanded ? 0.0 : max(listSize.height - itemsHeight, listSize.height - 46.0 - floor(56.0 * 3.5))) - let (duration, curve) = listViewAnimationDurationAndCurve(transition: .animated(duration: 0.4, curve: .spring)) + let frameTransition: ContainedViewLayoutTransition + if previousIsExpanded != self.currentIsExpanded { + frameTransition = .animated(duration: 0.4, curve: .spring) + } else { + frameTransition = .animated(duration: 0.4, curve: .easeInOut) + } + frameTransition.updateFrame(node: self.listNode, frame: CGRect(origin: CGPoint(x: 0.0, y: listTopInset + (self.topInset ?? listSize.height)), size: listSize)) + + let (duration, curve) = listViewAnimationDurationAndCurve(transition: frameTransition) let updateSizeAndInsets = ListViewUpdateSizeAndInsets(size: listSize, insets: insets, duration: duration, curve: curve) - self.listNode.transaction(deleteIndices: transition.deletions, insertIndicesAndItems: transition.insertions, updateIndicesAndItems: transition.updates, options: options, scrollToItem: scrollToItem, updateSizeAndInsets: updateSizeAndInsets, updateOpaqueState: nil, completion: { [weak self] _ in + self.updateColors(fullscreen: transition.isExpanded) + + self.listNode.transaction(deleteIndices: transition.deletions, insertIndicesAndItems: transition.insertions, updateIndicesAndItems: transition.updates, options: options, scrollToItem: nil, updateSizeAndInsets: updateSizeAndInsets, updateOpaqueState: nil, completion: { [weak self] _ in guard let strongSelf = self else { return } @@ -1744,9 +1804,12 @@ public final class VoiceChatController: ViewController { strongSelf.controller?.contentsReady.set(true) } }) + if previousIsExpanded != self.currentIsExpanded { + self.updateFloatingHeaderOffset(offset: self.currentContentOffset ?? 0.0, transition: frameTransition) + } } - private func updateMembers(muteState: GroupCallParticipantsContext.Participant.MuteState?, callMembers: [GroupCallParticipantsContext.Participant], invitedPeers: [Peer], speakingPeers: Set) { + private func updateMembers(muteState: GroupCallParticipantsContext.Participant.MuteState?, callMembers: [GroupCallParticipantsContext.Participant], invitedPeers: [Peer], speakingPeers: Set, isExpanded: Bool) { self.currentCallMembers = callMembers self.currentSpeakingPeers = speakingPeers self.currentInvitedPeers = invitedPeers @@ -1811,7 +1874,7 @@ public final class VoiceChatController: ViewController { self.currentEntries = entries let presentationData = self.presentationData.withUpdated(theme: self.darkTheme) - let transition = preparedTransition(from: previousEntries, to: entries, isLoading: false, isEmpty: false, crossFade: false, context: self.context, presentationData: presentationData, interaction: self.itemInteraction!) + let transition = preparedTransition(from: previousEntries, to: entries, isLoading: false, isEmpty: false, crossFade: false, context: self.context, presentationData: presentationData, interaction: self.itemInteraction!, isExpanded: isExpanded ?? self.currentIsExpanded) self.enqueueTransition(transition) } diff --git a/submodules/TelegramCallsUI/Sources/VoiceChatParticipantItem.swift b/submodules/TelegramCallsUI/Sources/VoiceChatParticipantItem.swift index e38bf1c139..e5e18e3c65 100644 --- a/submodules/TelegramCallsUI/Sources/VoiceChatParticipantItem.swift +++ b/submodules/TelegramCallsUI/Sources/VoiceChatParticipantItem.swift @@ -18,8 +18,8 @@ import AccountContext import LegacyComponents import AudioBlob -public final class VoiceChatParticipantItem: ListViewItem { - public enum ParticipantText { +final class VoiceChatParticipantItem: ListViewItem { + enum ParticipantText { public enum TextColor { case generic case accent @@ -31,25 +31,25 @@ public final class VoiceChatParticipantItem: ListViewItem { case none } - public enum Icon { + enum Icon { case none case microphone(Bool, UIColor) case invite(Bool) } - public struct RevealOption { - public enum RevealOptionType { + struct RevealOption { + enum RevealOptionType { case neutral case warning case destructive case accent } - public var type: RevealOptionType - public var title: String - public var action: () -> Void + var type: RevealOptionType + var title: String + var action: () -> Void - public init(type: RevealOptionType, title: String, action: @escaping () -> Void) { + init(type: RevealOptionType, title: String, action: @escaping () -> Void) { self.type = type self.title = title self.action = action @@ -138,7 +138,7 @@ public final class VoiceChatParticipantItem: ListViewItem { private let avatarFont = avatarPlaceholderFont(size: floor(40.0 * 16.0 / 37.0)) -public class VoiceChatParticipantItemNode: ItemListRevealOptionsItemNode { +class VoiceChatParticipantItemNode: ItemListRevealOptionsItemNode { private let topStripeNode: ASDisplayNode private let bottomStripeNode: ASDisplayNode private let highlightedBackgroundNode: ASDisplayNode @@ -170,7 +170,7 @@ public class VoiceChatParticipantItemNode: ItemListRevealOptionsItemNode { private var peerPresenceManager: PeerPresenceStatusManager? private var layoutParams: (VoiceChatParticipantItem, ListViewItemLayoutParams, Bool, Bool)? - public init() { + init() { self.topStripeNode = ASDisplayNode() self.topStripeNode.isLayerBacked = true @@ -281,7 +281,7 @@ public class VoiceChatParticipantItemNode: ItemListRevealOptionsItemNode { self.audioLevelDisposable.dispose() } - public func asyncLayout() -> (_ item: VoiceChatParticipantItem, _ params: ListViewItemLayoutParams, _ first: Bool, _ last: Bool) -> (ListViewItemNodeLayout, (Bool, Bool) -> Void) { + func asyncLayout() -> (_ item: VoiceChatParticipantItem, _ params: ListViewItemLayoutParams, _ first: Bool, _ last: Bool) -> (ListViewItemNodeLayout, (Bool, Bool) -> Void) { let makeTitleLayout = TextNode.asyncLayout(self.titleNode) let makeStatusLayout = TextNode.asyncLayout(self.statusNode) var currentDisabledOverlayNode = self.disabledOverlayNode @@ -706,7 +706,7 @@ public class VoiceChatParticipantItemNode: ItemListRevealOptionsItemNode { } } - override public func setHighlighted(_ highlighted: Bool, at point: CGPoint, animated: Bool) { + override func setHighlighted(_ highlighted: Bool, at point: CGPoint, animated: Bool) { super.setHighlighted(highlighted, at: point, animated: animated) self.isHighlighted = highlighted @@ -714,20 +714,19 @@ public class VoiceChatParticipantItemNode: ItemListRevealOptionsItemNode { self.updateIsHighlighted(transition: (animated && !highlighted) ? .animated(duration: 0.3, curve: .easeInOut) : .immediate) } - override public func animateInsertion(_ currentTimestamp: Double, duration: Double, short: Bool) { + override func animateInsertion(_ currentTimestamp: Double, duration: Double, short: Bool) { self.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.4) } - override public func animateRemoved(_ currentTimestamp: Double, duration: Double) { + override func animateRemoved(_ currentTimestamp: Double, duration: Double) { self.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.15, removeOnCompletion: false) } - - override public func header() -> ListViewItemHeader? { + override func header() -> ListViewItemHeader? { return nil } - override public func updateAbsoluteRect(_ rect: CGRect, within containerSize: CGSize) { + override func updateAbsoluteRect(_ rect: CGRect, within containerSize: CGSize) { var rect = rect rect.origin.y += self.insets.top self.absoluteLocation = (rect, containerSize) @@ -739,7 +738,7 @@ public class VoiceChatParticipantItemNode: ItemListRevealOptionsItemNode { } } - override public func updateRevealOffset(offset: CGFloat, transition: ContainedViewLayoutTransition) { + override func updateRevealOffset(offset: CGFloat, transition: ContainedViewLayoutTransition) { super.updateRevealOffset(offset: offset, transition: transition) if let _ = self.layoutParams?.0, let params = self.layoutParams?.1 { @@ -761,19 +760,19 @@ public class VoiceChatParticipantItemNode: ItemListRevealOptionsItemNode { } } - override public func revealOptionsInteractivelyOpened() { + override func revealOptionsInteractivelyOpened() { if let item = self.layoutParams?.0 { item.setPeerIdWithRevealedOptions(item.peer.id, nil) } } - override public func revealOptionsInteractivelyClosed() { + override func revealOptionsInteractivelyClosed() { if let item = self.layoutParams?.0 { item.setPeerIdWithRevealedOptions(nil, item.peer.id) } } - override public func revealOptionSelected(_ option: ItemListRevealOption, animated: Bool) { + override func revealOptionSelected(_ option: ItemListRevealOption, animated: Bool) { if let item = self.layoutParams?.0 { item.revealOptions[Int(option.key)].action() } @@ -781,4 +780,8 @@ public class VoiceChatParticipantItemNode: ItemListRevealOptionsItemNode { self.setRevealOptionsOpened(false, animated: true) self.revealOptionsInteractivelyClosed() } + + override var preferredAnimationCurve: (CGFloat) -> CGFloat { + return listViewAnimationCurveEaseInOut + } } diff --git a/submodules/TelegramUI/Sources/ChatMessageStickerItemNode.swift b/submodules/TelegramUI/Sources/ChatMessageStickerItemNode.swift index 22f5f64d9b..aa3eb8bea8 100644 --- a/submodules/TelegramUI/Sources/ChatMessageStickerItemNode.swift +++ b/submodules/TelegramUI/Sources/ChatMessageStickerItemNode.swift @@ -12,6 +12,7 @@ import AccountContext import StickerResources import ContextUI import Markdown +import ShimmerEffect private let nameFont = Font.medium(14.0) private let inlineBotPrefixFont = Font.regular(14.0) @@ -21,6 +22,7 @@ class ChatMessageStickerItemNode: ChatMessageItemView { private let contextSourceNode: ContextExtractedContentContainingNode private let containerNode: ContextControllerSourceNode let imageNode: TransformImageNode + private var placeholderNode: StickerShimmerEffectNode? var textNode: TextNode? private var swipeToReplyNode: ChatMessageSwipeToReplyNode? @@ -50,6 +52,8 @@ class ChatMessageStickerItemNode: ChatMessageItemView { self.contextSourceNode = ContextExtractedContentContainingNode() self.containerNode = ContextControllerSourceNode() self.imageNode = TransformImageNode() + self.placeholderNode = StickerShimmerEffectNode() + self.placeholderNode?.isUserInteractionEnabled = false self.dateAndStatusNode = ChatMessageDateAndStatusNode() super.init(layerBacked: false) @@ -96,6 +100,9 @@ class ChatMessageStickerItemNode: ChatMessageItemView { self.containerNode.addSubnode(self.contextSourceNode) self.containerNode.targetNodeForActivationProgress = self.contextSourceNode.contentNode self.addSubnode(self.containerNode) + if let placeholderNode = self.placeholderNode { + self.contextSourceNode.contentNode.addSubnode(placeholderNode) + } self.contextSourceNode.contentNode.addSubnode(self.imageNode) self.contextSourceNode.contentNode.addSubnode(self.dateAndStatusNode) @@ -115,6 +122,21 @@ class ChatMessageStickerItemNode: ChatMessageItemView { self.fetchDisposable.dispose() } + + private func removePlaceholder(animated: Bool) { + if let placeholderNode = self.placeholderNode { + self.placeholderNode = nil + if !animated { + placeholderNode.removeFromSupernode() + } else { + placeholderNode.alpha = 0.0 + placeholderNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, completion: { [weak placeholderNode] _ in + placeholderNode?.removeFromSupernode() + }) + } + } + } + override func didLoad() { super.didLoad()