Voice Chat Scheduling

This commit is contained in:
Ilya Laktyushin
2021-04-07 04:33:05 +03:00
parent 693aa7f106
commit b9e52f27e3
54 changed files with 6278 additions and 5164 deletions

View File

@@ -5,6 +5,7 @@ import AsyncDisplayKit
import SwiftSignalKit
import TelegramPresentationData
import TelegramUIPreferences
import TelegramStringFormatting
import TelegramVoip
import TelegramAudio
import AccountContext
@@ -29,6 +30,7 @@ import LegacyComponents
import LegacyMediaPickerUI
import WebSearchUI
import MapResourceToAvatarSizes
import SolidRoundedButtonNode
private let panelBackgroundColor = UIColor(rgb: 0x1c1c1e)
private let secondaryPanelBackgroundColor = UIColor(rgb: 0x2c2c2e)
@@ -65,105 +67,6 @@ private func cornersImage(top: Bool, bottom: Bool, dark: Bool) -> UIImage? {
})?.stretchableImage(withLeftCapWidth: 25, topCapHeight: 25)
}
private final class VoiceChatControllerTitleNode: ASDisplayNode {
private var theme: PresentationTheme
private let titleNode: ASTextNode
private let infoNode: ASTextNode
fileprivate let recordingIconNode: VoiceChatRecordingIconNode
public var isRecording: Bool = false {
didSet {
self.recordingIconNode.isHidden = !self.isRecording
}
}
var tapped: (() -> Void)?
init(theme: PresentationTheme) {
self.theme = theme
self.titleNode = ASTextNode()
self.titleNode.displaysAsynchronously = false
self.titleNode.maximumNumberOfLines = 1
self.titleNode.truncationMode = .byTruncatingTail
self.titleNode.isOpaque = false
self.infoNode = ASTextNode()
self.infoNode.displaysAsynchronously = false
self.infoNode.maximumNumberOfLines = 1
self.infoNode.truncationMode = .byTruncatingTail
self.infoNode.isOpaque = false
self.recordingIconNode = VoiceChatRecordingIconNode(hasBackground: false)
super.init()
self.addSubnode(self.titleNode)
self.addSubnode(self.infoNode)
self.addSubnode(self.recordingIconNode)
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func didLoad() {
super.didLoad()
self.view.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.tap)))
}
override func point(inside point: CGPoint, with event: UIEvent?) -> Bool {
if point.y > 0.0 && point.y < self.frame.size.height && point.x > min(self.titleNode.frame.minX, self.infoNode.frame.minX) && point.x < max(self.recordingIconNode.frame.maxX, self.infoNode.frame.maxX) {
return true
} else {
return false
}
}
@objc private func tap() {
self.tapped?()
}
func update(size: CGSize, title: String, subtitle: String, transition: ContainedViewLayoutTransition) {
var titleUpdated = false
if let previousTitle = self.titleNode.attributedText?.string {
titleUpdated = previousTitle != title
}
if titleUpdated, let snapshotView = self.titleNode.view.snapshotContentTree() {
snapshotView.frame = self.titleNode.frame
self.view.addSubview(snapshotView)
snapshotView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { [weak snapshotView] _ in
snapshotView?.removeFromSuperview()
})
self.titleNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2)
}
self.titleNode.attributedText = NSAttributedString(string: title, font: Font.medium(17.0), textColor: UIColor(rgb: 0xffffff))
self.infoNode.attributedText = NSAttributedString(string: subtitle, font: Font.regular(13.0), textColor: UIColor(rgb: 0xffffff, alpha: 0.5))
let constrainedSize = CGSize(width: size.width - 140.0, height: size.height)
let titleSize = self.titleNode.measure(constrainedSize)
let infoSize = self.infoNode.measure(constrainedSize)
let titleInfoSpacing: CGFloat = 0.0
let combinedHeight = titleSize.height + infoSize.height + titleInfoSpacing
let titleFrame = CGRect(origin: CGPoint(x: floor((size.width - titleSize.width) / 2.0), y: floor((size.height - combinedHeight) / 2.0)), size: titleSize)
self.titleNode.frame = titleFrame
self.infoNode.frame = CGRect(origin: CGPoint(x: floor((size.width - infoSize.width) / 2.0), y: floor((size.height - combinedHeight) / 2.0) + titleSize.height + titleInfoSpacing), size: infoSize)
let iconSide = 16.0 + (1.0 + UIScreenPixel) * 2.0
let iconSize: CGSize = CGSize(width: iconSide, height: iconSide)
self.recordingIconNode.frame = CGRect(origin: CGPoint(x: titleFrame.maxX + 1.0, y: titleFrame.minY + 1.0), size: iconSize)
}
}
final class GroupVideoNode: ASDisplayNode {
private let videoViewContainer: UIView
private let videoView: PresentationCallVideoView
@@ -730,7 +633,15 @@ public final class VoiceChatController: ViewController {
private let leftBorderNode: ASDisplayNode
private let rightBorderNode: ASDisplayNode
private let titleNode: VoiceChatControllerTitleNode
private var isScheduling = false
private let timerNode: VoiceChatTimerNode
private var pickerView: UIDatePicker?
private let dateFormatter: DateFormatter
private let scheduleTextNode: ImmediateTextNode
private let scheduleCancelButton: SolidRoundedButtonNode
private var scheduleButtonTitle = ""
private let titleNode: VoiceChatTitleNode
private var enqueuedTransitions: [ListTransition] = []
private var floatingHeaderOffset: CGFloat?
@@ -823,6 +734,8 @@ public final class VoiceChatController: ViewController {
self.context = call.accountContext
self.call = call
self.isScheduling = call.schedulePending
let presentationData = sharedContext.currentPresentationData.with { $0 }
self.presentationData = presentationData
@@ -836,7 +749,7 @@ public final class VoiceChatController: ViewController {
self.contentContainer.isHidden = true
self.backgroundNode = ASDisplayNode()
self.backgroundNode.backgroundColor = secondaryPanelBackgroundColor
self.backgroundNode.backgroundColor = self.isScheduling ? panelBackgroundColor : secondaryPanelBackgroundColor
self.backgroundNode.clipsToBounds = false
if sharedContext.immediateExperimentalUISettings.demoVideoChats {
@@ -844,6 +757,8 @@ public final class VoiceChatController: ViewController {
}
self.listNode = ListView()
self.listNode.alpha = self.isScheduling ? 0.0 : 1.0
self.listNode.isUserInteractionEnabled = !self.isScheduling
self.listNode.verticalScrollIndicatorColor = UIColor(white: 1.0, alpha: 0.3)
self.listNode.clipsToBounds = true
self.listNode.scroller.bounces = false
@@ -870,7 +785,7 @@ public final class VoiceChatController: ViewController {
self.closeButton = VoiceChatHeaderButton(context: self.context)
self.closeButton.setContent(.image(closeButtonImage(dark: false)))
self.titleNode = VoiceChatControllerTitleNode(theme: self.presentationData.theme)
self.titleNode = VoiceChatTitleNode(theme: self.presentationData.theme)
self.topCornersNode = ASImageNode()
self.topCornersNode.displaysAsynchronously = false
@@ -895,6 +810,13 @@ public final class VoiceChatController: ViewController {
self.switchCameraButton.isUserInteractionEnabled = false
self.leaveButton = CallControllerButtonItemNode()
self.actionButton = VoiceChatActionButton()
if self.isScheduling {
self.audioButton.alpha = 0.0
self.audioButton.isUserInteractionEnabled = false
self.leaveButton.alpha = 0.0
self.leaveButton.isUserInteractionEnabled = false
}
self.leftBorderNode = ASDisplayNode()
self.leftBorderNode.backgroundColor = panelBackgroundColor
@@ -906,6 +828,19 @@ public final class VoiceChatController: ViewController {
self.rightBorderNode.isUserInteractionEnabled = false
self.rightBorderNode.clipsToBounds = false
self.scheduleTextNode = ImmediateTextNode()
self.scheduleTextNode.isHidden = !self.isScheduling
self.scheduleCancelButton = SolidRoundedButtonNode(title: self.presentationData.strings.Common_Cancel, theme: SolidRoundedButtonTheme(backgroundColor: UIColor(rgb: 0x2b2b2f), foregroundColor: .white), height: 52.0, cornerRadius: 10.0)
self.scheduleCancelButton.isHidden = !self.isScheduling
self.dateFormatter = DateFormatter()
self.dateFormatter.timeStyle = .none
self.dateFormatter.dateStyle = .short
self.dateFormatter.timeZone = TimeZone.current
self.timerNode = VoiceChatTimerNode(strings: self.presentationData.strings, dateTimeFormat: self.presentationData.dateTimeFormat)
super.init()
let statePromise = ValuePromise(State(), ignoreRepeated: true)
@@ -1514,6 +1449,7 @@ public final class VoiceChatController: ViewController {
}
let contextController = ContextController(account: strongSelf.context.account, presentationData: strongSelf.presentationData.withUpdated(theme: strongSelf.darkTheme), source: .extracted(source), items: items, reactionItems: [], gesture: gesture)
contextController.useComplexItemsTransitionAnimation = true
strongSelf.controller?.presentInGlobalOverlay(contextController)
}, setPeerIdWithRevealedOptions: { peerId, _ in
updateState { state in
@@ -1550,6 +1486,7 @@ public final class VoiceChatController: ViewController {
}
self.bottomPanelNode.addSubnode(self.leaveButton)
self.bottomPanelNode.addSubnode(self.actionButton)
self.bottomPanelNode.addSubnode(self.scheduleCancelButton)
self.addSubnode(self.dimNode)
self.addSubnode(self.contentContainer)
@@ -1563,6 +1500,7 @@ public final class VoiceChatController: ViewController {
self.contentContainer.addSubnode(self.leftBorderNode)
self.contentContainer.addSubnode(self.rightBorderNode)
self.contentContainer.addSubnode(self.bottomPanelNode)
self.contentContainer.addSubnode(self.timerNode)
let invitedPeers: Signal<[Peer], NoError> = self.call.invitedPeers
|> mapToSignal { ids -> Signal<[Peer], NoError> in
@@ -1619,7 +1557,13 @@ public final class VoiceChatController: ViewController {
let subtitle = strongSelf.presentationData.strings.VoiceChat_Panel_Members(Int32(max(1, callMembers?.totalCount ?? 0)))
strongSelf.currentSubtitle = subtitle
if let callState = strongSelf.callState, callState.canManageCall {
if strongSelf.isScheduling {
strongSelf.optionsButtonIsAvatar = false
strongSelf.optionsButton.isUserInteractionEnabled = false
strongSelf.optionsButton.alpha = 0.0
strongSelf.closeButton.isUserInteractionEnabled = false
strongSelf.closeButton.alpha = 0.0
} else if let callState = strongSelf.callState, callState.canManageCall {
strongSelf.optionsButtonIsAvatar = false
strongSelf.optionsButton.isUserInteractionEnabled = true
strongSelf.optionsButton.alpha = 1.0
@@ -1774,16 +1718,6 @@ public final class VoiceChatController: ViewController {
}
}
// self.memberEventsDisposable.set((self.call.memberEvents
// |> deliverOnMainQueue).start(next: { [weak self] event in
// guard let strongSelf = self else {
// return
// }
// if event.joined {
// strongSelf.presentUndoOverlay(content: .invitedToVoiceChat(context: strongSelf.context, peer: event.peer, text: strongSelf.presentationData.strings.VoiceChat_PeerJoinedText(event.peer.displayTitle(strings: strongSelf.presentationData.strings, displayOrder: strongSelf.presentationData.nameDisplayOrder)).0), action: { _ in return false })
// }
// }))
self.reconnectedAsEventsDisposable.set((self.call.reconnectedAsEvents
|> deliverOnMainQueue).start(next: { [weak self] peer in
guard let strongSelf = self else {
@@ -1874,22 +1808,32 @@ public final class VoiceChatController: ViewController {
}))
self.titleNode.tapped = { [weak self] in
if let strongSelf = self, !strongSelf.titleNode.recordingIconNode.isHidden {
var hasTooltipAlready = false
strongSelf.controller?.forEachController { controller -> Bool in
if controller is TooltipScreen {
hasTooltipAlready = true
if let strongSelf = self {
if strongSelf.callState?.canManageCall ?? false {
strongSelf.openTitleEditing()
} else if !strongSelf.titleNode.recordingIconNode.isHidden {
var hasTooltipAlready = false
strongSelf.controller?.forEachController { controller -> Bool in
if controller is TooltipScreen {
hasTooltipAlready = true
}
return true
}
if !hasTooltipAlready {
let location = strongSelf.titleNode.recordingIconNode.convert(strongSelf.titleNode.recordingIconNode.bounds, to: nil)
strongSelf.controller?.present(TooltipScreen(text: presentationData.strings.VoiceChat_RecordingInProgress, icon: nil, location: .point(location.offsetBy(dx: 1.0, dy: 0.0), .top), displayDuration: .custom(3.0), shouldDismissOnTouch: { _ in
return .dismiss(consume: true)
}), in: .window(.root))
}
return true
}
if !hasTooltipAlready {
let location = strongSelf.titleNode.recordingIconNode.convert(strongSelf.titleNode.recordingIconNode.bounds, to: nil)
strongSelf.controller?.present(TooltipScreen(text: presentationData.strings.VoiceChat_RecordingInProgress, icon: nil, location: .point(location.offsetBy(dx: 1.0, dy: 0.0), .top), displayDuration: .custom(3.0), shouldDismissOnTouch: { _ in
return .dismiss(consume: true)
}), in: .window(.root))
}
}
}
self.scheduleCancelButton.pressed = { [weak self] in
if let strongSelf = self {
strongSelf.dismissScheduled()
}
}
}
deinit {
@@ -1931,7 +1875,7 @@ public final class VoiceChatController: ViewController {
let avatarSize = CGSize(width: 28.0, height: 28.0)
return combineLatest(self.displayAsPeersPromise.get(), self.context.account.postbox.loadedPeerWithId(call.peerId), self.inviteLinksPromise.get())
return combineLatest(self.displayAsPeersPromise.get(), self.context.account.postbox.loadedPeerWithId(self.call.peerId), self.inviteLinksPromise.get())
|> take(1)
|> deliverOnMainQueue
|> map { [weak self] peers, chatPeer, inviteLinks -> [ContextMenuItem] in
@@ -1965,15 +1909,7 @@ public final class VoiceChatController: ViewController {
guard let strongSelf = self else {
return
}
let controller = voiceChatTitleEditController(sharedContext: strongSelf.context.sharedContext, account: strongSelf.context.account, forceTheme: strongSelf.darkTheme, title: presentationData.strings.VoiceChat_EditTitleTitle, text: presentationData.strings.VoiceChat_EditTitleText, placeholder: chatPeer.displayTitle(strings: strongSelf.presentationData.strings, displayOrder: strongSelf.presentationData.nameDisplayOrder), value: strongSelf.callState?.title, maxLength: 40, apply: { title in
if let strongSelf = self, let title = title {
strongSelf.call.updateTitle(title)
strongSelf.presentUndoOverlay(content: .voiceChatFlag(text: title.isEmpty ? strongSelf.presentationData.strings.VoiceChat_EditTitleRemoveSuccess : strongSelf.presentationData.strings.VoiceChat_EditTitleSuccess(title).0), action: { _ in return false })
}
})
self?.controller?.present(controller, in: .window(.root))
strongSelf.openTitleEditing()
})))
var hasPermissions = true
@@ -1994,16 +1930,7 @@ public final class VoiceChatController: ViewController {
c.setItems(strongSelf.contextMenuPermissionItems())
})))
}
items.append(.action(ContextMenuActionItem(text: strongSelf.presentationData.strings.VoiceChat_EditPermissions, icon: { theme -> UIImage? in
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Restrict"), color: theme.actionSheet.primaryTextColor)
}, action: { c, _ in
guard let strongSelf = self else {
return
}
c.setItems(strongSelf.contextMenuPermissionItems())
})))
if let inviteLinks = inviteLinks {
items.append(.action(ContextMenuActionItem(text: strongSelf.presentationData.strings.VoiceChat_Share, icon: { theme in
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Link"), color: theme.actionSheet.primaryTextColor)
@@ -2044,25 +1971,27 @@ public final class VoiceChatController: ViewController {
self?.controller?.present(alertController, in: .window(.root))
}), false))
} else {
items.append(.action(ContextMenuActionItem(text: strongSelf.presentationData.strings.VoiceChat_StartRecording, icon: { theme -> UIImage? in
return generateStartRecordingIcon(color: theme.actionSheet.primaryTextColor)
}, action: { _, f in
f(.dismissWithoutContent)
if strongSelf.callState?.scheduleTimestamp == nil {
items.append(.action(ContextMenuActionItem(text: strongSelf.presentationData.strings.VoiceChat_StartRecording, icon: { theme -> UIImage? in
return generateStartRecordingIcon(color: theme.actionSheet.primaryTextColor)
}, action: { _, f in
f(.dismissWithoutContent)
guard let strongSelf = self else {
return
}
let controller = voiceChatTitleEditController(sharedContext: strongSelf.context.sharedContext, account: strongSelf.context.account, forceTheme: strongSelf.darkTheme, title: presentationData.strings.VoiceChat_StartRecordingTitle, text: presentationData.strings.VoiceChat_StartRecordingText, placeholder: presentationData.strings.VoiceChat_RecordingTitlePlaceholder, value: nil, maxLength: 40, apply: { title in
if let strongSelf = self, let title = title {
strongSelf.call.setShouldBeRecording(true, title: title)
strongSelf.presentUndoOverlay(content: .voiceChatRecording(text: strongSelf.presentationData.strings.VoiceChat_RecordingStarted), action: { _ in return false })
strongSelf.call.playTone(.recordingStarted)
guard let strongSelf = self else {
return
}
})
self?.controller?.present(controller, in: .window(.root))
})))
let controller = voiceChatTitleEditController(sharedContext: strongSelf.context.sharedContext, account: strongSelf.context.account, forceTheme: strongSelf.darkTheme, title: presentationData.strings.VoiceChat_StartRecordingTitle, text: presentationData.strings.VoiceChat_StartRecordingText, placeholder: presentationData.strings.VoiceChat_RecordingTitlePlaceholder, value: nil, maxLength: 40, apply: { title in
if let strongSelf = self, let title = title {
strongSelf.call.setShouldBeRecording(true, title: title)
strongSelf.presentUndoOverlay(content: .voiceChatRecording(text: strongSelf.presentationData.strings.VoiceChat_RecordingStarted), action: { _ in return false })
strongSelf.call.playTone(.recordingStarted)
}
})
self?.controller?.present(controller, in: .window(.root))
})))
}
}
items.append(.action(ContextMenuActionItem(text: strongSelf.isNoiseSuppressionEnabled ? "Disable Noise Suppression" : "Enable Noise Suppression", textColor: .primary, icon: { theme in
@@ -2275,6 +2204,161 @@ public final class VoiceChatController: ViewController {
panRecognizer.delaysTouchesBegan = false
panRecognizer.cancelsTouchesInView = true
self.view.addGestureRecognizer(panRecognizer)
if self.isScheduling {
self.setupPickerView()
self.updateScheduleButtonTitle()
}
}
private func updateMinimumDate() {
let timeZone = TimeZone(secondsFromGMT: 0)!
var calendar = Calendar(identifier: .gregorian)
calendar.timeZone = timeZone
let currentDate = Date()
var components = calendar.dateComponents(Set([.era, .year, .month, .day, .hour, .minute, .second]), from: currentDate)
components.second = 0
let minute = (components.minute ?? 0) % 5
let next1MinDate = calendar.date(byAdding: .minute, value: 1, to: calendar.date(from: components)!)
let next5MinDate = calendar.date(byAdding: .minute, value: 5 - minute, to: calendar.date(from: components)!)
if let date = calendar.date(byAdding: .day, value: 365, to: currentDate) {
self.pickerView?.maximumDate = date
}
if let next1MinDate = next1MinDate, let next5MinDate = next5MinDate {
self.pickerView?.minimumDate = next1MinDate
self.pickerView?.date = next5MinDate
}
}
private func setupPickerView() {
var currentDate: Date?
if let pickerView = self.pickerView {
currentDate = pickerView.date
pickerView.removeFromSuperview()
}
let textColor = UIColor.white
UILabel.setDateLabel(textColor)
let pickerView = UIDatePicker()
pickerView.timeZone = TimeZone(secondsFromGMT: 0)
pickerView.datePickerMode = .countDownTimer
pickerView.datePickerMode = .dateAndTime
pickerView.locale = Locale.current
pickerView.timeZone = TimeZone.current
pickerView.minuteInterval = 1
self.contentContainer.view.addSubview(pickerView)
pickerView.addTarget(self, action: #selector(self.datePickerUpdated), for: .valueChanged)
if #available(iOS 13.4, *) {
pickerView.preferredDatePickerStyle = .wheels
}
pickerView.setValue(textColor, forKey: "textColor")
self.pickerView = pickerView
self.updateMinimumDate()
if let currentDate = currentDate {
pickerView.date = currentDate
}
}
private let calendar = Calendar(identifier: .gregorian)
private func updateScheduleButtonTitle() {
guard let date = self.pickerView?.date else {
return
}
let calendar = Calendar(identifier: .gregorian)
let time = stringForMessageTimestamp(timestamp: Int32(date.timeIntervalSince1970), dateTimeFormat: self.presentationData.dateTimeFormat)
let buttonTitle: String
if calendar.isDateInToday(date) {
buttonTitle = self.presentationData.strings.ScheduleVoiceChat_ScheduleToday(time).0
} else if calendar.isDateInTomorrow(date) {
buttonTitle = self.presentationData.strings.ScheduleVoiceChat_ScheduleTomorrow(time).0
} else {
buttonTitle = self.presentationData.strings.ScheduleVoiceChat_ScheduleOn(self.dateFormatter.string(from: date), time).0
}
self.scheduleButtonTitle = buttonTitle
if let (layout, navigationHeight) = self.validLayout {
self.containerLayoutUpdated(layout, navigationHeight: navigationHeight, transition: .animated(duration: 0.3, curve: .spring))
}
}
@objc private func datePickerUpdated() {
self.updateScheduleButtonTitle()
}
private func schedule() {
if let date = self.pickerView?.date, date > Date() {
self.call.schedule(timestamp: Int32(date.timeIntervalSince1970))
self.isScheduling = false
self.transitionToScheduled()
if let (layout, navigationHeight) = self.validLayout {
self.containerLayoutUpdated(layout, navigationHeight: navigationHeight, transition: .animated(duration: 0.3, curve: .spring))
}
}
}
private func dismissScheduled() {
self.leaveDisposable.set((self.call.leave(terminateIfPossible: true)
|> deliverOnMainQueue).start(completed: { [weak self] in
self?.controller?.dismiss(closing: true)
}))
}
private func transitionToScheduled() {
self.optionsButton.alpha = 1.0
self.optionsButton.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2)
self.optionsButton.layer.animateSpring(from: 0.01 as NSNumber, to: 1.0 as NSNumber, keyPath: "transform.scale", duration: 0.42, damping: 104.0)
self.optionsButton.isUserInteractionEnabled = true
self.closeButton.alpha = 1.0
self.closeButton.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2)
self.closeButton.layer.animateSpring(from: 0.01 as NSNumber, to: 1.0 as NSNumber, keyPath: "transform.scale", duration: 0.42, damping: 104.0)
self.closeButton.isUserInteractionEnabled = true
self.audioButton.alpha = 1.0
self.audioButton.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2)
self.audioButton.layer.animateSpring(from: 0.01 as NSNumber, to: 1.0 as NSNumber, keyPath: "transform.scale", duration: 0.42, damping: 104.0)
self.audioButton.isUserInteractionEnabled = true
self.leaveButton.alpha = 1.0
self.leaveButton.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2)
self.leaveButton.layer.animateSpring(from: 0.01 as NSNumber, to: 1.0 as NSNumber, keyPath: "transform.scale", duration: 0.42, damping: 104.0)
self.leaveButton.isUserInteractionEnabled = true
self.scheduleCancelButton.alpha = 0.0
self.scheduleCancelButton.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2)
self.scheduleCancelButton.layer.animatePosition(from: CGPoint(), to: CGPoint(x: 0.0, y: 26.0), duration: 0.2, removeOnCompletion: false, additive: true)
if let pickerView = self.pickerView {
pickerView.alpha = 0.0
pickerView.layer.animateScale(from: 1.0, to: 0.1, duration: 0.2)
pickerView.isUserInteractionEnabled = false
}
self.timerNode.alpha = 1.0
self.timerNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2)
self.timerNode.layer.animateSpring(from: 0.4 as NSNumber, to: 1.0 as NSNumber, keyPath: "transform.scale", duration: 0.3, damping: 104.0)
self.timerNode.animateIn()
self.updateTitle(transition: .animated(duration: 0.2, curve: .easeInOut))
}
private func transitionToCall() {
self.updateIsFullscreen(false, force: true)
self.listNode.alpha = 1.0
self.listNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2)
self.timerNode.alpha = 0.0
self.timerNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2)
self.updateTitle(transition: .animated(duration: 0.2, curve: .easeInOut))
}
@objc private func optionsPressed() {
@@ -2491,7 +2575,31 @@ public final class VoiceChatController: ViewController {
guard let callState = self.callState else {
return
}
if case .connecting = callState.networkState {
if case .connecting = callState.networkState, callState.scheduleTimestamp == nil && !self.isScheduling {
return
}
if callState.scheduleTimestamp != nil || self.isScheduling {
switch gestureRecognizer.state {
case .began:
self.actionButton.pressing = true
self.hapticFeedback.impact(.light)
case .ended, .cancelled:
self.actionButton.pressing = false
let location = gestureRecognizer.location(in: self.actionButton.view)
if self.actionButton.hitTest(location, with: nil) != nil {
if self.isScheduling {
self.schedule()
} else if callState.canManageCall {
self.call.startScheduled()
self.transitionToCall()
} else {
}
}
default:
break
}
return
}
if let muteState = callState.muteState {
@@ -2548,11 +2656,27 @@ public final class VoiceChatController: ViewController {
}
@objc private func actionButtonPressed() {
if self.isScheduling {
self.schedule()
}
}
@objc private func audioOutputPressed() {
self.hapticFeedback.impact(.light)
if let _ = self.callState?.scheduleTimestamp {
let _ = (self.inviteLinksPromise.get()
|> take(1)
|> deliverOnMainQueue).start(next: { [weak self] inviteLinks in
if let inviteLinks = inviteLinks {
self?.presentShare(inviteLinks)
} else {
self?.presentShare(GroupCallInviteLinks(listenerLink: "a", speakerLink: nil))
}
})
return
}
guard let (availableOutputs, currentOutput) = self.audioOutputState else {
return
}
@@ -2743,8 +2867,8 @@ public final class VoiceChatController: ViewController {
}
var isFullscreen = false
func updateIsFullscreen(_ isFullscreen: Bool) {
guard self.isFullscreen != isFullscreen, let (layout, _) = self.validLayout else {
func updateIsFullscreen(_ isFullscreen: Bool, force: Bool = false) {
guard self.isFullscreen != isFullscreen || force, let (layout, _) = self.validLayout else {
return
}
self.isFullscreen = isFullscreen
@@ -2770,16 +2894,20 @@ public final class VoiceChatController: ViewController {
topEdgeFrame = CGRect(x: 0.0, y: 0.0, width: size.width, height: topPanelHeight)
}
var isScheduled = false
if self.isScheduling || self.callState?.scheduleTimestamp != nil {
isScheduled = true
}
let transition: ContainedViewLayoutTransition = .animated(duration: 0.3, curve: .linear)
transition.updateFrame(node: self.topPanelEdgeNode, frame: topEdgeFrame)
transition.updateCornerRadius(node: self.topPanelEdgeNode, cornerRadius: isFullscreen ? layout.deviceMetrics.screenCornerRadius - 0.5 : 12.0)
transition.updateBackgroundColor(node: self.topPanelBackgroundNode, color: isFullscreen ? fullscreenBackgroundColor : panelBackgroundColor)
transition.updateBackgroundColor(node: self.topPanelEdgeNode, color: isFullscreen ? fullscreenBackgroundColor : panelBackgroundColor)
transition.updateBackgroundColor(node: self.backgroundNode, color: isFullscreen ? panelBackgroundColor : secondaryPanelBackgroundColor)
transition.updateBackgroundColor(node: self.backgroundNode, color: isFullscreen || isScheduled ? panelBackgroundColor : secondaryPanelBackgroundColor)
transition.updateBackgroundColor(node: self.bottomPanelBackgroundNode, color: isFullscreen ? fullscreenBackgroundColor : panelBackgroundColor)
transition.updateBackgroundColor(node: self.leftBorderNode, color: isFullscreen ? fullscreenBackgroundColor : panelBackgroundColor)
transition.updateBackgroundColor(node: self.rightBorderNode, color: isFullscreen ? fullscreenBackgroundColor : panelBackgroundColor)
transition.updateBackgroundColor(node: self.rightBorderNode, color: isFullscreen ? fullscreenBackgroundColor : panelBackgroundColor)
if let snapshotView = self.topCornersNode.view.snapshotContentTree() {
snapshotView.frame = self.topCornersNode.frame
@@ -2814,22 +2942,39 @@ public final class VoiceChatController: ViewController {
return
}
var title = self.currentTitle
if !self.isFullscreen && !self.currentTitleIsCustom {
if self.isScheduling {
title = self.presentationData.strings.ScheduleVoiceChat_Title
} else if !self.isFullscreen && !self.currentTitleIsCustom {
if let navigationController = self.controller?.navigationController as? NavigationController {
for controller in navigationController.viewControllers.reversed() {
if let controller = controller as? ChatController, case let .peer(peerId) = controller.chatLocation, peerId == self.call.peerId {
title = self.presentationData.strings.VoiceChat_Title
if self.callState?.scheduleTimestamp != nil {
title = self.presentationData.strings.VoiceChat_ScheduledTitle
} else {
title = self.presentationData.strings.VoiceChat_Title
}
}
}
}
}
var subtitle = self.currentSubtitle
if self.isScheduling {
subtitle = ""
} else if self.callState?.scheduleTimestamp != nil {
if self.callState?.canManageCall ?? false {
subtitle = self.presentationData.strings.VoiceChat_TapToEditTitle
} else {
subtitle = ""
}
}
var size = layout.size
if case .regular = layout.metrics.widthClass {
size.width = floor(min(size.width, size.height) * 0.5)
}
self.titleNode.update(size: CGSize(width: size.width, height: 44.0), title: title, subtitle: self.currentSubtitle, transition: transition)
self.titleNode.update(size: CGSize(width: size.width, height: 44.0), title: title, subtitle: subtitle, transition: transition)
}
private func updateButtons(animated: Bool) {
@@ -2866,7 +3011,7 @@ public final class VoiceChatController: ViewController {
coloredButtonAppearance = .color(.custom(self.isFullscreen ? 0x1c1c1e : 0x2c2c2e, 1.0))
}
let soundImage: CallControllerButtonItemNode.Content.Image
var soundImage: CallControllerButtonItemNode.Content.Image
var soundAppearance: CallControllerButtonItemNode.Content.Appearance = coloredButtonAppearance
var soundTitle: String = self.presentationData.strings.Call_Speaker
switch audioMode {
@@ -2890,6 +3035,12 @@ public final class VoiceChatController: ViewController {
soundTitle = self.presentationData.strings.Call_Audio
}
if self.isScheduling || self.callState?.scheduleTimestamp != nil {
soundImage = .share
soundTitle = self.presentationData.strings.VoiceChat_ShareShort
soundAppearance = coloredButtonAppearance
}
let videoButtonSize: CGSize
var buttonsTitleAlpha: CGFloat
switch self.displayMode {
@@ -2916,6 +3067,7 @@ public final class VoiceChatController: ViewController {
transition.updateAlpha(node: self.leaveButton.textNode, alpha: buttonsTitleAlpha)
}
private var ignoreNextConnecting = false
func containerLayoutUpdated(_ layout: ContainerViewLayout, navigationHeight: CGFloat, transition: ContainedViewLayoutTransition) {
let isFirstTime = self.validLayout == nil
self.validLayout = (layout, navigationHeight)
@@ -2993,7 +3145,16 @@ public final class VoiceChatController: ViewController {
let bottomPanelFrame = CGRect(origin: CGPoint(x: 0.0, y: layout.size.height - bottomPanelHeight), size: CGSize(width: size.width, height: bottomPanelHeight))
transition.updateFrame(node: self.bottomPanelNode, frame: bottomPanelFrame)
let centralButtonSize = CGSize(width: 300.0, height: 300.0)
if let pickerView = self.pickerView {
transition.updateFrame(view: pickerView, frame: CGRect(x: 0.0, y: layout.size.height - bottomPanelHeight - 216.0, width: size.width, height: 216.0))
}
let timerFrame = CGRect(x: 0.0, y: layout.size.height - bottomPanelHeight - 216.0, width: size.width, height: 216.0)
transition.updateFrame(node: self.timerNode, frame: timerFrame)
self.timerNode.update(size: timerFrame.size, scheduleTime: self.callState?.scheduleTimestamp, transition: .immediate)
let centralButtonSide = min(size.width, size.height) - 32.0
let centralButtonSize = CGSize(width: centralButtonSide, height: centralButtonSide)
let cameraButtonSize = CGSize(width: 36.0, height: 36.0)
let sideButtonMinimalInset: CGFloat = 16.0
let sideButtonOffset = min(42.0, floor((((size.width - 112.0) / 2.0) - sideButtonSize.width) / 2.0))
@@ -3037,48 +3198,76 @@ public final class VoiceChatController: ViewController {
let actionButtonTitle: String
let actionButtonSubtitle: String
var actionButtonEnabled = true
if let callState = self.callState {
switch callState.networkState {
case .connecting:
if let callState = self.callState, !self.isScheduling {
var isScheduled = callState.scheduleTimestamp != nil
if isScheduled {
self.ignoreNextConnecting = true
if callState.canManageCall {
actionButtonState = .scheduled(state: .start)
actionButtonTitle = self.presentationData.strings.VoiceChat_StartNow
actionButtonSubtitle = ""
} else {
actionButtonState = .scheduled(state: .subscribe)
actionButtonTitle = self.presentationData.strings.VoiceChat_SetReminder
actionButtonSubtitle = ""
}
} else {
let connected = self.ignoreNextConnecting || callState.networkState == .connected
if case .connected = callState.networkState {
self.ignoreNextConnecting = false
}
if connected {
if let muteState = callState.muteState, !self.pushingToTalk {
if muteState.canUnmute {
actionButtonState = .active(state: .muted)
actionButtonTitle = self.presentationData.strings.VoiceChat_Unmute
actionButtonSubtitle = ""
} else {
actionButtonState = .active(state: .cantSpeak)
if callState.raisedHand {
actionButtonTitle = self.presentationData.strings.VoiceChat_AskedToSpeak
actionButtonSubtitle = self.presentationData.strings.VoiceChat_AskedToSpeakHelp
} else {
actionButtonTitle = self.presentationData.strings.VoiceChat_MutedByAdmin
actionButtonSubtitle = self.presentationData.strings.VoiceChat_MutedByAdminHelp
}
}
} else {
actionButtonState = .active(state: .on)
actionButtonTitle = self.pushingToTalk ? self.presentationData.strings.VoiceChat_Live : self.presentationData.strings.VoiceChat_Mute
actionButtonSubtitle = ""
}
} else {
actionButtonState = .connecting
actionButtonTitle = self.presentationData.strings.VoiceChat_Connecting
actionButtonSubtitle = ""
actionButtonEnabled = false
}
}
} else {
if self.isScheduling {
actionButtonState = .button(text: self.scheduleButtonTitle)
actionButtonTitle = ""
actionButtonSubtitle = ""
actionButtonEnabled = true
} else {
actionButtonState = .connecting
actionButtonTitle = self.presentationData.strings.VoiceChat_Connecting
actionButtonSubtitle = ""
actionButtonEnabled = false
case .connected:
if let muteState = callState.muteState, !self.pushingToTalk {
if muteState.canUnmute {
actionButtonState = .active(state: .muted)
actionButtonTitle = self.presentationData.strings.VoiceChat_Unmute
actionButtonSubtitle = ""
} else {
actionButtonState = .active(state: .cantSpeak)
if callState.raisedHand {
actionButtonTitle = self.presentationData.strings.VoiceChat_AskedToSpeak
actionButtonSubtitle = self.presentationData.strings.VoiceChat_AskedToSpeakHelp
} else {
actionButtonTitle = self.presentationData.strings.VoiceChat_MutedByAdmin
actionButtonSubtitle = self.presentationData.strings.VoiceChat_MutedByAdminHelp
}
}
} else {
actionButtonState = .active(state: .on)
actionButtonTitle = self.pushingToTalk ? self.presentationData.strings.VoiceChat_Live : self.presentationData.strings.VoiceChat_Mute
actionButtonSubtitle = ""
}
}
} else {
actionButtonState = .connecting
actionButtonTitle = self.presentationData.strings.VoiceChat_Connecting
actionButtonSubtitle = ""
actionButtonEnabled = false
}
self.actionButton.isDisabled = !actionButtonEnabled
self.actionButton.update(size: centralButtonSize, buttonSize: CGSize(width: 112.0, height: 112.0), state: actionButtonState, title: actionButtonTitle, subtitle: actionButtonSubtitle, dark: self.isFullscreen, small: smallButtons, animated: true)
let buttonHeight = self.scheduleCancelButton.updateLayout(width: size.width - 32.0, transition: .immediate)
self.scheduleCancelButton.frame = CGRect(x: 16.0, y: 137.0, width: size.width - 32.0, height: buttonHeight)
if self.actionButton.supernode === self.bottomPanelNode {
transition.updateFrame(node: self.actionButton, frame: thirdButtonFrame)
}
@@ -3196,6 +3385,12 @@ public final class VoiceChatController: ViewController {
}
self.enqueuedTransitions.remove(at: 0)
if self.callState?.scheduleTimestamp != nil && self.listNode.alpha > 0.0 {
self.listNode.alpha = 0.0
self.backgroundNode.backgroundColor = panelBackgroundColor
self.updateIsFullscreen(false)
}
var options = ListViewDeleteAndInsertOptions()
let isFirstTime = self.isFirstTime
if isFirstTime {
@@ -3235,7 +3430,11 @@ public final class VoiceChatController: ViewController {
let listTopInset = layoutTopInset + 63.0
let listSize = CGSize(width: size.width, height: layout.size.height - listTopInset - bottomPanelHeight)
self.topInset = max(0.0, max(listSize.height - itemsHeight, listSize.height - 46.0 - floor(56.0 * 3.5)))
if self.isScheduling || self.callState?.scheduleTimestamp != nil {
self.topInset = listSize.height - 46.0 - floor(56.0 * 3.5)
} else {
self.topInset = max(0.0, max(listSize.height - itemsHeight, listSize.height - 46.0 - floor(56.0 * 3.5)))
}
let targetY = listTopInset + (self.topInset ?? listSize.height)
@@ -3453,9 +3652,12 @@ public final class VoiceChatController: ViewController {
}
override func gestureRecognizerShouldBegin(_ gestureRecognizer: UIGestureRecognizer) -> Bool {
if gestureRecognizer is DirectionalPanGestureRecognizer {
if gestureRecognizer is UILongPressGestureRecognizer {
return !self.isScheduling
} else if gestureRecognizer is DirectionalPanGestureRecognizer {
let location = gestureRecognizer.location(in: self.bottomPanelNode.view)
if self.audioButton.frame.contains(location) || (!self.cameraButton.isHidden && self.cameraButton.frame.contains(location)) || self.leaveButton.frame.contains(location) {
let containerLocation = gestureRecognizer.location(in: self.contentContainer.view)
if self.audioButton.frame.contains(location) || (!self.cameraButton.isHidden && self.cameraButton.frame.contains(location)) || self.leaveButton.frame.contains(location) || self.pickerView?.frame.contains(containerLocation) == true {
return false
}
}
@@ -3494,6 +3696,9 @@ public final class VoiceChatController: ViewController {
self.controller?.dismissAllTooltips()
case .changed:
var translation = recognizer.translation(in: self.contentContainer.view).y
if (self.isScheduling || self.callState?.scheduleTimestamp != nil) && translation < 0.0 {
return
}
var topInset: CGFloat = 0.0
if let (currentTopInset, currentPanOffset) = self.panGestureArguments {
topInset = currentTopInset
@@ -3591,9 +3796,13 @@ public final class VoiceChatController: ViewController {
self.panGestureArguments = nil
var dismissing = false
if bounds.minY < -60 || (bounds.minY < 0.0 && velocity.y > 300.0) {
self.controller?.dismiss(closing: false, manual: true)
if self.isScheduling {
self.dismissScheduled()
} else {
self.controller?.dismiss(closing: false, manual: true)
}
dismissing = true
} else if velocity.y < -300.0 || offset < topInset / 2.0 {
} else if !self.isScheduling && (velocity.y < -300.0 || offset < topInset / 2.0) {
if velocity.y > -1500.0 && !self.isFullscreen {
DispatchQueue.main.async {
self.listNode.transaction(deleteIndices: [], insertIndicesAndItems: [], updateIndicesAndItems: [], options: [.Synchronous, .LowLatency], scrollToItem: ListViewScrollToItem(index: 0, position: .top(0.0), animated: true, curve: .Default(duration: nil), directionHint: .Up), updateSizeAndInsets: nil, stationaryItemRange: nil, updateOpaqueState: nil, completion: { _ in })
@@ -3610,7 +3819,7 @@ public final class VoiceChatController: ViewController {
self.updateFloatingHeaderOffset(offset: self.currentContentOffset ?? 0.0, transition: .animated(duration: 0.3, curve: .easeInOut), completion: {
self.animatingExpansion = false
})
} else {
} else if !self.isScheduling {
self.updateIsFullscreen(false)
self.animatingExpansion = true
self.listNode.scroller.setContentOffset(CGPoint(), animated: false)
@@ -3684,6 +3893,24 @@ public final class VoiceChatController: ViewController {
}
}
private func openTitleEditing() {
let _ = (self.context.account.postbox.loadedPeerWithId(self.call.peerId)
|> deliverOnMainQueue).start(next: { [weak self] chatPeer in
guard let strongSelf = self else {
return
}
let controller = voiceChatTitleEditController(sharedContext: strongSelf.context.sharedContext, account: strongSelf.context.account, forceTheme: strongSelf.darkTheme, title: strongSelf.presentationData.strings.VoiceChat_EditTitleTitle, text: strongSelf.presentationData.strings.VoiceChat_EditTitleText, placeholder: chatPeer.displayTitle(strings: strongSelf.presentationData.strings, displayOrder: strongSelf.presentationData.nameDisplayOrder), value: strongSelf.callState?.title, maxLength: 40, apply: { title in
if let strongSelf = self, let title = title {
strongSelf.call.updateTitle(title)
strongSelf.presentUndoOverlay(content: .voiceChatFlag(text: title.isEmpty ? strongSelf.presentationData.strings.VoiceChat_EditTitleRemoveSuccess : strongSelf.presentationData.strings.VoiceChat_EditTitleSuccess(title).0), action: { _ in return false })
}
})
strongSelf.controller?.present(controller, in: .window(.root))
})
}
private func openAvatarForEditing(fromGallery: Bool = false, completion: @escaping () -> Void = {}) {
guard let peerId = self.callState?.myPeerId else {
return
@@ -3765,7 +3992,7 @@ public final class VoiceChatController: ViewController {
return
}
let proceed = {
let proceed = {
let _ = strongSelf.currentAvatarMixin.swap(nil)
let postbox = strongSelf.context.account.postbox
strongSelf.updateAvatarDisposable.set((updatePeerPhoto(postbox: strongSelf.context.account.postbox, network: strongSelf.context.account.network, stateManager: strongSelf.context.account.stateManager, accountPeerId: strongSelf.context.account.peerId, peerId: peerId, photo: nil, mapResourceToAvatarSizes: { resource, representations in
@@ -4096,6 +4323,8 @@ public final class VoiceChatController: ViewController {
let count = navigationController.viewControllers.count
if count == 2 || navigationController.viewControllers[count - 2] is ChatController {
if case .active(.cantSpeak) = self.controllerNode.actionButton.stateValue {
} else if case .button = self.controllerNode.actionButton.stateValue {
} else if case .scheduled = self.controllerNode.actionButton.stateValue {
} else if let chatController = navigationController.viewControllers[count - 2] as? ChatController, chatController.isSendButtonVisible {
} else if let tabBarController = navigationController.viewControllers[count - 2] as? TabBarController, let chatListController = tabBarController.controllers[tabBarController.selectedIndex] as? ChatListController, chatListController.isSearchActive {
} else {