mirror of
https://github.com/Swiftgram/Telegram-iOS.git
synced 2025-12-22 22:25:57 +00:00
Voice Chat Scheduling
This commit is contained in:
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user